From 7199d13f484b8c9a79ec241066e1058fe9ff7139 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sat, 18 Sep 2021 15:56:55 +0100 Subject: [PATCH] feat: improve freedraw shape (#3984) --- package.json | 2 +- src/renderer/renderElement.ts | 55 ++++++++++++++++++++++------------- yarn.lock | 8 ++--- 3 files changed, 40 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index e188cd77..00a5f1a1 100644 --- a/package.json +++ b/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", diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 5605fe99..c3dd3c75 100644 --- a/src/renderer/renderElement.ts +++ b/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); + const max = points.length - 1; - d.push("Z"); - - 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"); } diff --git a/yarn.lock b/yarn.lock index bcb8113f..7211decb 100644 --- a/yarn.lock +++ b/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"