浏览代码

feat: improve freedraw shape (#3984)

Steve Ruiz 3 年之前
父节点
当前提交
7199d13f48
共有 3 个文件被更改,包括 40 次插入25 次删除
  1. 1 1
      package.json
  2. 35 20
      src/renderer/renderElement.ts
  3. 4 4
      yarn.lock

+ 1 - 1
package.json

@@ -35,7 +35,7 @@
     "nanoid": "3.1.22",
     "open-color": "1.8.0",
     "pako": "1.0.11",
-    "perfect-freehand": "0.4.7",
+    "perfect-freehand": "1.0.6",
     "png-chunk-text": "1.0.0",
     "png-chunks-encode": "1.0.0",
     "png-chunks-extract": "1.0.0",

+ 35 - 20
src/renderer/renderElement.ts

@@ -32,7 +32,7 @@ import { isPathALoop } from "../math";
 import rough from "roughjs/bin/rough";
 import { Zoom } from "../types";
 import { getDefaultAppState } from "../appState";
-import getFreeDrawShape from "perfect-freehand";
+import { getStroke, StrokeOptions } from "perfect-freehand";
 import { MAX_DECIMALS_FOR_SVG_EXPORT } from "../constants";
 
 const defaultAppState = getDefaultAppState();
@@ -789,40 +789,55 @@ export function getFreeDrawPath2D(element: ExcalidrawFreeDrawElement) {
 }
 
 export function getFreeDrawSvgPath(element: ExcalidrawFreeDrawElement) {
+  // If input points are empty (should they ever be?) return a dot
   const inputPoints = element.simulatePressure
     ? element.points
     : element.points.length
     ? element.points.map(([x, y], i) => [x, y, element.pressures[i]])
-    : [[0, 0, 0]];
+    : [[0, 0, 0.5]];
 
   // Consider changing the options for simulated pressure vs real pressure
-  const options = {
+  const options: StrokeOptions = {
     simulatePressure: element.simulatePressure,
-    size: element.strokeWidth * 6,
-    thinning: 0.5,
+    size: element.strokeWidth * 4.25,
+    thinning: 0.6,
     smoothing: 0.5,
     streamline: 0.5,
-    easing: (t: number) => t * (2 - t),
-    last: true,
+    easing: (t) => Math.sin((t * Math.PI) / 2), // https://easings.net/#easeOutSine
+    last: false,
   };
 
-  const points = getFreeDrawShape(inputPoints as number[][], options);
-  const d: (string | number)[] = [];
+  return getSvgPathFromStroke(getStroke(inputPoints as number[][], options));
+}
 
-  let [p0, p1] = points;
+function med(A: number[], B: number[]) {
+  return [(A[0] + B[0]) / 2, (A[1] + B[1]) / 2];
+}
 
-  d.push("M", p0[0], p0[1], "Q");
+// Trim SVG path data so number are each two decimal points. This
+// improves SVG exports, and prevents rendering errors on points
+// with long decimals.
+const TO_FIXED_PRECISION = /(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g;
 
-  for (let i = 0; i < points.length; i++) {
-    d.push(p0[0], p0[1], (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2);
-    p0 = p1;
-    p1 = points[i];
+function getSvgPathFromStroke(points: number[][]): string {
+  if (!points.length) {
+    return "";
   }
 
-  p1 = points[0];
-  d.push(p0[0], p0[1], (p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2);
-
-  d.push("Z");
+  const max = points.length - 1;
 
-  return d.join(" ");
+  return points
+    .reduce(
+      (acc, point, i, arr) => {
+        if (i === max) {
+          acc.push(point, med(point, arr[0]), "L", arr[0], "Z");
+        } else {
+          acc.push(point, med(point, arr[i + 1]));
+        }
+        return acc;
+      },
+      ["M", points[0], "Q"],
+    )
+    .join(" ")
+    .replaceAll(TO_FIXED_PRECISION, "$1");
 }

+ 4 - 4
yarn.lock

@@ -9260,10 +9260,10 @@ pepjs@0.5.3:
   version "0.5.3"
   resolved "https://registry.npmjs.org/pepjs/-/pepjs-0.5.3.tgz"
 
-perfect-freehand@0.4.7:
-  version "0.4.7"
-  resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-0.4.7.tgz#4d85fd64881ba81b2a4eaa6ac4e8983ccb21dd43"
-  integrity sha512-SSSFL8VzXiOHQdUTyNyOb0JC+btVZRy9bi6jos7Nb7PBTI0PHX5jM6RgCTSrubQ8Ul9qOYWmWgJBrwVGHwyJZQ==
+perfect-freehand@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/perfect-freehand/-/perfect-freehand-1.0.6.tgz#feeb25450241f036ec13b43fa84bbb16f8e78e0f"
+  integrity sha512-wWkFwpgUirsfBDTb9nG6+VnFR0ge119QKU2Nu96vR4MHZMPGfOsQRD7cUk+9CK5P+TUmnrtX8yOEzUrQ6KHJoA==
 
 performance-now@^2.1.0:
   version "2.1.0"