|
@@ -32,7 +32,7 @@ import { isPathALoop } from "../math";
|
32
|
32
|
import rough from "roughjs/bin/rough";
|
33
|
33
|
import { Zoom } from "../types";
|
34
|
34
|
import { getDefaultAppState } from "../appState";
|
35
|
|
-import getFreeDrawShape from "perfect-freehand";
|
|
35
|
+import { getStroke, StrokeOptions } from "perfect-freehand";
|
36
|
36
|
import { MAX_DECIMALS_FOR_SVG_EXPORT } from "../constants";
|
37
|
37
|
|
38
|
38
|
const defaultAppState = getDefaultAppState();
|
|
@@ -789,40 +789,55 @@ export function getFreeDrawPath2D(element: ExcalidrawFreeDrawElement) {
|
789
|
789
|
}
|
790
|
790
|
|
791
|
791
|
export function getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
|
|
792
|
+ // If input points are empty (should they ever be?) return a dot
|
792
|
793
|
const inputPoints = element.simulatePressure
|
793
|
794
|
? element.points
|
794
|
795
|
: element.points.length
|
795
|
796
|
? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
|
796
|
|
- : [[0, 0, 0]];
|
|
797
|
+ : [[0, 0, 0.5]];
|
797
|
798
|
|
798
|
799
|
// Consider changing the options for simulated pressure vs real pressure
|
799
|
|
- const options = {
|
|
800
|
+ const options: StrokeOptions = {
|
800
|
801
|
simulatePressure: element.simulatePressure,
|
801
|
|
- size: element.strokeWidth * 6,
|
802
|
|
- thinning: 0.5,
|
|
802
|
+ size: element.strokeWidth * 4.25,
|
|
803
|
+ thinning: 0.6,
|
803
|
804
|
smoothing: 0.5,
|
804
|
805
|
streamline: 0.5,
|
805
|
|
- easing: (t: number) => t * (2 - t),
|
806
|
|
- last: true,
|
|
806
|
+ easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine
|
|
807
|
+ last: false,
|
807
|
808
|
};
|
808
|
809
|
|
809
|
|
- const points = getFreeDrawShape(inputPoints as number[][], options);
|
810
|
|
- const d: (string | number)[] = [];
|
|
810
|
+ return getSvgPathFromStroke(getStroke(inputPoints as number[][], options));
|
|
811
|
+}
|
811
|
812
|
|
812
|
|
- let [p0, p1] = points;
|
|
813
|
+function med(A: number[], B: number[]) {
|
|
814
|
+ return [(A[0] + B[0]) / 2, (A[1] + B[1]) / 2];
|
|
815
|
+}
|
813
|
816
|
|
814
|
|
- d.push("M", p0[0], p0[1], "Q");
|
|
817
|
+// Trim SVG path data so number are each two decimal points. This
|
|
818
|
+// improves SVG exports, and prevents rendering errors on points
|
|
819
|
+// with long decimals.
|
|
820
|
+const TO_FIXED_PRECISION = /(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g;
|
815
|
821
|
|
816
|
|
- for (let i = 0; i < points.length; i++) {
|
817
|
|
- d.push(p0[0], p0[1], (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2);
|
818
|
|
- p0 = p1;
|
819
|
|
- p1 = points[i];
|
|
822
|
+function getSvgPathFromStroke(points: number[][]): string {
|
|
823
|
+ if (!points.length) {
|
|
824
|
+ return "";
|
820
|
825
|
}
|
821
|
826
|
|
822
|
|
- p1 = points[0];
|
823
|
|
- d.push(p0[0], p0[1], (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2);
|
824
|
|
-
|
825
|
|
- d.push("Z");
|
|
827
|
+ const max = points.length - 1;
|
826
|
828
|
|
827
|
|
- return d.join(" ");
|
|
829
|
+ return points
|
|
830
|
+ .reduce(
|
|
831
|
+ (acc, point, i, arr) => {
|
|
832
|
+ if (i === max) {
|
|
833
|
+ acc.push(point, med(point, arr[0]), "L", arr[0], "Z");
|
|
834
|
+ } else {
|
|
835
|
+ acc.push(point, med(point, arr[i + 1]));
|
|
836
|
+ }
|
|
837
|
+ return acc;
|
|
838
|
+ },
|
|
839
|
+ ["M", points[0], "Q"],
|
|
840
|
+ )
|
|
841
|
+ .join(" ")
|
|
842
|
+ .replaceAll(TO_FIXED_PRECISION, "$1");
|
828
|
843
|
}
|