├── .gitignore
├── .vscode
└── settings.json
├── .editorconfig
├── index.d.ts
├── tsconfig.json
├── package.json
├── LICENSE
├── test.js
├── README.md
├── index.html
├── index.js
├── index.iife.js
└── index.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.codeActionsOnSave": {
3 | "source.organizeImports": true
4 | },
5 | "editor.formatOnSave": true,
6 | "typescript.tsdk": "node_modules/typescript/lib"
7 | }
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | end_of_line = lf
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | interface Point {
2 | readonly x: number;
3 | readonly y: number;
4 | }
5 | declare const simplifySvgPath: (points: readonly (readonly [number, number])[] | readonly Point[], options?: {
6 | closed?: boolean;
7 | tolerance?: number;
8 | precision?: number;
9 | }) => string;
10 | export default simplifySvgPath;
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "module": "es2015",
5 | "moduleResolution": "node",
6 | "strict": true,
7 | "noFallthroughCasesInSwitch": true,
8 | "noUnusedLocals": true,
9 | "noUnusedParameters": true,
10 | "newLine": "LF",
11 | "declaration": true,
12 | "outDir": "."
13 | },
14 | "files": ["index.ts"]
15 | }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@luncheon/simplify-svg-path",
3 | "version": "0.2.0",
4 | "description": "Extracts Path#simplify() from Paper.js.",
5 | "license": "MIT",
6 | "repository": "luncheon/simplify-svg-path",
7 | "files": [
8 | "index.js",
9 | "index.d.ts",
10 | "index.iife.js"
11 | ],
12 | "type": "module",
13 | "main": "index.js",
14 | "jsdelivr": "index.iife.js",
15 | "unpkg": "index.iife.js",
16 | "keywords": [
17 | "paper",
18 | "paper.js",
19 | "svg",
20 | "path"
21 | ],
22 | "prettier": {
23 | "printWidth": 140,
24 | "endOfLine": "lf",
25 | "singleQuote": true,
26 | "trailingComma": "all",
27 | "semi": false,
28 | "arrowParens": "avoid"
29 | },
30 | "scripts": {
31 | "build": "tsc -p . && node build-iife.js && npm t",
32 | "test": "node test.js"
33 | },
34 | "devDependencies": {
35 | "jsdom": "^20.0.3",
36 | "paper": "^0.12.17",
37 | "prettier": "^2.8.0",
38 | "typescript": "^4.9.3"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 null
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import Paper from 'paper'
3 | import simplifySvgPath from './index.js'
4 |
5 | new Paper.Project()
6 |
7 | const simplifySvgPathByPaper = (segments, { closed, tolerance, precision }) => {
8 | const path = new Paper.Path(segments)
9 | closed && path.closePath()
10 | path.simplify(tolerance)
11 | return path.getPathData(undefined, precision)
12 | }
13 |
14 | let actualTime = 0
15 | let expectedTime = 0
16 |
17 | for (let i = 0; i < 100; i++) {
18 | const points = []
19 | for (let i = 0; i < 1000; i++) {
20 | points.push([Math.random() * 100, Math.random() * 100])
21 | }
22 | const options = {
23 | closed: Math.random() < 0.5,
24 | tolerance: Math.random() * 5 || 2.5,
25 | precision: i % 4,
26 | }
27 | const now1 = performance.now()
28 | const actual = simplifySvgPath(points, options)
29 | const now2 = performance.now()
30 | const expected = simplifySvgPathByPaper(points, options)
31 | const now3 = performance.now()
32 | actualTime += now2 - now1
33 | expectedTime += now3 - now2
34 | assert.strictEqual(
35 | actual,
36 | expected,
37 | `simplified path does not equal to Paper.js. closed: ${options.closed}, tolerance: ${options.tolerance}, precision: ${options.precision}`,
38 | )
39 | }
40 |
41 | console.log('passed', actualTime, '[ms]', Math.round((actualTime / expectedTime) * 10000) / 100, '%')
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # simplify-svg-path
2 |
3 | Extracts `Path#simplify()` from Paper.js.
4 | http://paperjs.org/reference/path/#simplify
5 |
6 | ## Installation & Usage
7 |
8 | ### [npm](https://www.npmjs.com/package/@luncheon/simplify-svg-path)
9 |
10 | ```bash
11 | $ npm i @luncheon/simplify-svg-path
12 | ```
13 |
14 | ```javascript
15 | import simplifySvgPath from '@luncheon/simplify-svg-path'
16 |
17 | const points = [[10, 10], [10, 20], [20, 20]];
18 | const path = simplifySvgPath(points);
19 | // "M10,10c0,3.33333 -2.35702,7.64298 0,10c2.35702,2.35702 6.66667,0 10,0"
20 | ```
21 |
22 | ### CDN ([jsDelivr](https://www.jsdelivr.com/package/npm/@luncheon/simplify-svg-path))
23 |
24 | ```html
25 |
26 |
29 | ```
30 |
31 | ## API
32 |
33 | ```ts
34 | simplifySvgPath(
35 | points: [x: number, y: number][], // `{ x: number, y: number }[]` is also acceptable
36 | {
37 | closed: boolean = false,
38 | tolerance: number = 2.5,
39 | precision: number = 5,
40 | } = {}
41 | ): string
42 |
43 | // SVG path command string such as
44 | // "M10,10c0,3.33333 -2.35702,7.64298 0,10c2.35702,2.35702 6.66667,0 10,0"
45 | ```
46 |
47 | ## Note
48 |
49 | The logic is a copy of Paper.js v0.12.11.
50 | If you like this, please send your thanks and the star to [Paper.js](https://github.com/paperjs/paper.js).
51 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | simplify-svg-path demo
94 | Simplifies and smooths SVG <path>s.
95 | Drag to draw. Open the development tools and compare the original path to the simplified path.
96 |
97 |
98 |
139 |
140 |
141 |
142 |
197 |
198 |
199 |
200 |
201 |
202 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * simplify-svg-path
3 | *
4 | * The logic is a copy of Paper.js v0.12.11.
5 | */
6 | /*
7 | * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
8 | * http://paperjs.org/
9 | *
10 | * Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey
11 | * http://juerglehni.com/ & https://puckey.studio/
12 | *
13 | * Distributed under the MIT license. See LICENSE file for details.
14 | *
15 | * All rights reserved.
16 | */
17 | // An Algorithm for Automatically Fitting Digitized Curves
18 | // by Philip J. Schneider
19 | // from "Graphics Gems", Academic Press, 1990
20 | // Modifications and optimizations of original algorithm by Jürg Lehni.
21 | const EPSILON = 1e-12;
22 | const MACHINE_EPSILON = 1.12e-16;
23 | const isMachineZero = (val) => val >= -MACHINE_EPSILON && val <= MACHINE_EPSILON;
24 | // `Math.sqrt(x * x + y * y)` seems to be faster than `Math.hypot(x, y)`
25 | const hypot = (x, y) => Math.sqrt(x * x + y * y);
26 | const point = (x, y) => ({ x, y });
27 | const pointLength = (p) => hypot(p.x, p.y);
28 | const pointNegate = (p) => point(-p.x, -p.y);
29 | const pointAdd = (p1, p2) => point(p1.x + p2.x, p1.y + p2.y);
30 | const pointSubtract = (p1, p2) => point(p1.x - p2.x, p1.y - p2.y);
31 | const pointMultiplyScalar = (p, n) => point(p.x * n, p.y * n);
32 | const pointDot = (p1, p2) => p1.x * p2.x + p1.y * p2.y;
33 | const pointDistance = (p1, p2) => hypot(p1.x - p2.x, p1.y - p2.y);
34 | const pointNormalize = (p, length = 1) => pointMultiplyScalar(p, length / (pointLength(p) || Infinity));
35 | const createSegment = (p, i) => ({ p, i });
36 | const fit = (points, closed, error) => {
37 | // We need to duplicate the first and last segment when simplifying a
38 | // closed path.
39 | if (closed) {
40 | points.unshift(points[points.length - 1]);
41 | points.push(points[1]); // The point previously at index 0 is now 1.
42 | }
43 | const length = points.length;
44 | if (length === 0) {
45 | return [];
46 | }
47 | // To support reducing paths with multiple points in the same place
48 | // to one segment:
49 | const segments = [createSegment(points[0])];
50 | fitCubic(points, segments, error, 0, length - 1,
51 | // Left Tangent
52 | pointSubtract(points[1], points[0]),
53 | // Right Tangent
54 | pointSubtract(points[length - 2], points[length - 1]));
55 | // Remove the duplicated segments for closed paths again.
56 | if (closed) {
57 | segments.shift();
58 | segments.pop();
59 | }
60 | return segments;
61 | };
62 | // Fit a Bezier curve to a (sub)set of digitized points
63 | const fitCubic = (points, segments, error, first, last, tan1, tan2) => {
64 | // Use heuristic if region only has two points in it
65 | if (last - first === 1) {
66 | const pt1 = points[first], pt2 = points[last], dist = pointDistance(pt1, pt2) / 3;
67 | addCurve(segments, [pt1, pointAdd(pt1, pointNormalize(tan1, dist)), pointAdd(pt2, pointNormalize(tan2, dist)), pt2]);
68 | return;
69 | }
70 | // Parameterize points, and attempt to fit curve
71 | const uPrime = chordLengthParameterize(points, first, last);
72 | let maxError = Math.max(error, error * error), split, parametersInOrder = true;
73 | // Try not 4 but 5 iterations
74 | for (let i = 0; i <= 4; i++) {
75 | const curve = generateBezier(points, first, last, uPrime, tan1, tan2);
76 | // Find max deviation of points to fitted curve
77 | const max = findMaxError(points, first, last, curve, uPrime);
78 | if (max.error < error && parametersInOrder) {
79 | addCurve(segments, curve);
80 | return;
81 | }
82 | split = max.index;
83 | // If error not too large, try reparameterization and iteration
84 | if (max.error >= maxError)
85 | break;
86 | parametersInOrder = reparameterize(points, first, last, uPrime, curve);
87 | maxError = max.error;
88 | }
89 | // Fitting failed -- split at max error point and fit recursively
90 | const tanCenter = pointSubtract(points[split - 1], points[split + 1]);
91 | fitCubic(points, segments, error, first, split, tan1, tanCenter);
92 | fitCubic(points, segments, error, split, last, pointNegate(tanCenter), tan2);
93 | };
94 | const addCurve = (segments, curve) => {
95 | const prev = segments[segments.length - 1];
96 | prev.o = pointSubtract(curve[1], curve[0]);
97 | segments.push(createSegment(curve[3], pointSubtract(curve[2], curve[3])));
98 | };
99 | // Use least-squares method to find Bezier control points for region.
100 | const generateBezier = (points, first, last, uPrime, tan1, tan2) => {
101 | const epsilon = /*#=*/ EPSILON, abs = Math.abs, pt1 = points[first], pt2 = points[last],
102 | // Create the C and X matrices
103 | C = [
104 | [0, 0],
105 | [0, 0],
106 | ], X = [0, 0];
107 | for (let i = 0, l = last - first + 1; i < l; i++) {
108 | const u = uPrime[i], t = 1 - u, b = 3 * u * t, b0 = t * t * t, b1 = b * t, b2 = b * u, b3 = u * u * u, a1 = pointNormalize(tan1, b1), a2 = pointNormalize(tan2, b2), tmp = pointSubtract(pointSubtract(points[first + i], pointMultiplyScalar(pt1, b0 + b1)), pointMultiplyScalar(pt2, b2 + b3));
109 | C[0][0] += pointDot(a1, a1);
110 | C[0][1] += pointDot(a1, a2);
111 | // C[1][0] += a1.dot(a2);
112 | C[1][0] = C[0][1];
113 | C[1][1] += pointDot(a2, a2);
114 | X[0] += pointDot(a1, tmp);
115 | X[1] += pointDot(a2, tmp);
116 | }
117 | // Compute the determinants of C and X
118 | const detC0C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
119 | let alpha1;
120 | let alpha2;
121 | if (abs(detC0C1) > epsilon) {
122 | // Kramer's rule
123 | const detC0X = C[0][0] * X[1] - C[1][0] * X[0], detXC1 = X[0] * C[1][1] - X[1] * C[0][1];
124 | // Derive alpha values
125 | alpha1 = detXC1 / detC0C1;
126 | alpha2 = detC0X / detC0C1;
127 | }
128 | else {
129 | // Matrix is under-determined, try assuming alpha1 == alpha2
130 | const c0 = C[0][0] + C[0][1], c1 = C[1][0] + C[1][1];
131 | alpha1 = alpha2 = abs(c0) > epsilon ? X[0] / c0 : abs(c1) > epsilon ? X[1] / c1 : 0;
132 | }
133 | // If alpha negative, use the Wu/Barsky heuristic (see text)
134 | // (if alpha is 0, you get coincident control points that lead to
135 | // divide by zero in any subsequent NewtonRaphsonRootFind() call.
136 | const segLength = pointDistance(pt2, pt1), eps = epsilon * segLength;
137 | let handle1, handle2;
138 | if (alpha1 < eps || alpha2 < eps) {
139 | // fall back on standard (probably inaccurate) formula,
140 | // and subdivide further if needed.
141 | alpha1 = alpha2 = segLength / 3;
142 | }
143 | else {
144 | // Check if the found control points are in the right order when
145 | // projected onto the line through pt1 and pt2.
146 | const line = pointSubtract(pt2, pt1);
147 | // Control points 1 and 2 are positioned an alpha distance out
148 | // on the tangent vectors, left and right, respectively
149 | handle1 = pointNormalize(tan1, alpha1);
150 | handle2 = pointNormalize(tan2, alpha2);
151 | if (pointDot(handle1, line) - pointDot(handle2, line) > segLength * segLength) {
152 | // Fall back to the Wu/Barsky heuristic above.
153 | alpha1 = alpha2 = segLength / 3;
154 | handle1 = handle2 = null; // Force recalculation
155 | }
156 | }
157 | // First and last control points of the Bezier curve are
158 | // positioned exactly at the first and last data points
159 | return [pt1, pointAdd(pt1, handle1 || pointNormalize(tan1, alpha1)), pointAdd(pt2, handle2 || pointNormalize(tan2, alpha2)), pt2];
160 | };
161 | // Given set of points and their parameterization, try to find
162 | // a better parameterization.
163 | const reparameterize = (points, first, last, u, curve) => {
164 | for (let i = first; i <= last; i++) {
165 | u[i - first] = findRoot(curve, points[i], u[i - first]);
166 | }
167 | // Detect if the new parameterization has reordered the points.
168 | // In that case, we would fit the points of the path in the wrong order.
169 | for (let i = 1, l = u.length; i < l; i++) {
170 | if (u[i] <= u[i - 1])
171 | return false;
172 | }
173 | return true;
174 | };
175 | // Use Newton-Raphson iteration to find better root.
176 | const findRoot = (curve, point, u) => {
177 | const curve1 = [], curve2 = [];
178 | // Generate control vertices for Q'
179 | for (let i = 0; i <= 2; i++) {
180 | curve1[i] = pointMultiplyScalar(pointSubtract(curve[i + 1], curve[i]), 3);
181 | }
182 | // Generate control vertices for Q''
183 | for (let i = 0; i <= 1; i++) {
184 | curve2[i] = pointMultiplyScalar(pointSubtract(curve1[i + 1], curve1[i]), 2);
185 | }
186 | // Compute Q(u), Q'(u) and Q''(u)
187 | const pt = evaluate(3, curve, u), pt1 = evaluate(2, curve1, u), pt2 = evaluate(1, curve2, u), diff = pointSubtract(pt, point), df = pointDot(pt1, pt1) + pointDot(diff, pt2);
188 | // u = u - f(u) / f'(u)
189 | return isMachineZero(df) ? u : u - pointDot(diff, pt1) / df;
190 | };
191 | // Evaluate a bezier curve at a particular parameter value
192 | const evaluate = (degree, curve, t) => {
193 | // Copy array
194 | const tmp = curve.slice();
195 | // Triangle computation
196 | for (let i = 1; i <= degree; i++) {
197 | for (let j = 0; j <= degree - i; j++) {
198 | tmp[j] = pointAdd(pointMultiplyScalar(tmp[j], 1 - t), pointMultiplyScalar(tmp[j + 1], t));
199 | }
200 | }
201 | return tmp[0];
202 | };
203 | // Assign parameter values to digitized points
204 | // using relative distances between points.
205 | const chordLengthParameterize = (points, first, last) => {
206 | const u = [0];
207 | for (let i = first + 1; i <= last; i++) {
208 | u[i - first] = u[i - first - 1] + pointDistance(points[i], points[i - 1]);
209 | }
210 | for (let i = 1, m = last - first; i <= m; i++) {
211 | u[i] /= u[m];
212 | }
213 | return u;
214 | };
215 | // Find the maximum squared distance of digitized points to fitted curve.
216 | const findMaxError = (points, first, last, curve, u) => {
217 | let index = Math.floor((last - first + 1) / 2), maxDist = 0;
218 | for (let i = first + 1; i < last; i++) {
219 | const P = evaluate(3, curve, u[i - first]);
220 | const v = pointSubtract(P, points[i]);
221 | const dist = v.x * v.x + v.y * v.y; // squared
222 | if (dist >= maxDist) {
223 | maxDist = dist;
224 | index = i;
225 | }
226 | }
227 | return {
228 | error: maxDist,
229 | index: index,
230 | };
231 | };
232 | const getSegmentsPathData = (segments, closed, precision) => {
233 | const length = segments.length;
234 | const precisionMultiplier = 10 ** precision;
235 | const round = precision < 16 ? (n) => Math.round(n * precisionMultiplier) / precisionMultiplier : (n) => n;
236 | const formatPair = (x, y) => round(x) + ',' + round(y);
237 | let first = true;
238 | let prevX, prevY, outX, outY;
239 | const parts = [];
240 | const addSegment = (segment, skipLine) => {
241 | const curX = segment.p.x;
242 | const curY = segment.p.y;
243 | if (first) {
244 | parts.push('M' + formatPair(curX, curY));
245 | first = false;
246 | }
247 | else {
248 | const inX = curX + (segment.i?.x ?? 0);
249 | const inY = curY + (segment.i?.y ?? 0);
250 | if (inX === curX && inY === curY && outX === prevX && outY === prevY) {
251 | // l = relative lineto:
252 | if (!skipLine) {
253 | const dx = curX - prevX;
254 | const dy = curY - prevY;
255 | parts.push(dx === 0 ? 'v' + round(dy) : dy === 0 ? 'h' + round(dx) : 'l' + formatPair(dx, dy));
256 | }
257 | }
258 | else {
259 | // c = relative curveto:
260 | parts.push('c' +
261 | formatPair(outX - prevX, outY - prevY) +
262 | ' ' +
263 | formatPair(inX - prevX, inY - prevY) +
264 | ' ' +
265 | formatPair(curX - prevX, curY - prevY));
266 | }
267 | }
268 | prevX = curX;
269 | prevY = curY;
270 | outX = curX + (segment.o?.x ?? 0);
271 | outY = curY + (segment.o?.y ?? 0);
272 | };
273 | if (!length)
274 | return '';
275 | for (let i = 0; i < length; i++)
276 | addSegment(segments[i]);
277 | // Close path by drawing first segment again
278 | if (closed && length > 0) {
279 | addSegment(segments[0], true);
280 | parts.push('z');
281 | }
282 | return parts.join('');
283 | };
284 | const simplifySvgPath = (points, options = {}) => {
285 | if (points.length === 0) {
286 | return '';
287 | }
288 | return getSegmentsPathData(fit(points.map(typeof points[0].x === 'number' ? (p) => point(p.x, p.y) : (p) => point(p[0], p[1])), options.closed, options.tolerance ?? 2.5), options.closed, options.precision ?? 5);
289 | };
290 | export default simplifySvgPath;
291 |
--------------------------------------------------------------------------------
/index.iife.js:
--------------------------------------------------------------------------------
1 | var simplifySvgPath=(()=>{/*
2 | * simplify-svg-path
3 | *
4 | * The logic is a copy of Paper.js v0.12.11.
5 | */
6 | /*
7 | * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
8 | * http://paperjs.org/
9 | *
10 | * Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey
11 | * http://juerglehni.com/ & https://puckey.studio/
12 | *
13 | * Distributed under the MIT license. See LICENSE file for details.
14 | *
15 | * All rights reserved.
16 | */
17 | // An Algorithm for Automatically Fitting Digitized Curves
18 | // by Philip J. Schneider
19 | // from "Graphics Gems", Academic Press, 1990
20 | // Modifications and optimizations of original algorithm by Jürg Lehni.
21 | const EPSILON = 1e-12;
22 | const MACHINE_EPSILON = 1.12e-16;
23 | const isMachineZero = (val) => val >= -MACHINE_EPSILON && val <= MACHINE_EPSILON;
24 | // `Math.sqrt(x * x + y * y)` seems to be faster than `Math.hypot(x, y)`
25 | const hypot = (x, y) => Math.sqrt(x * x + y * y);
26 | const point = (x, y) => ({ x, y });
27 | const pointLength = (p) => hypot(p.x, p.y);
28 | const pointNegate = (p) => point(-p.x, -p.y);
29 | const pointAdd = (p1, p2) => point(p1.x + p2.x, p1.y + p2.y);
30 | const pointSubtract = (p1, p2) => point(p1.x - p2.x, p1.y - p2.y);
31 | const pointMultiplyScalar = (p, n) => point(p.x * n, p.y * n);
32 | const pointDot = (p1, p2) => p1.x * p2.x + p1.y * p2.y;
33 | const pointDistance = (p1, p2) => hypot(p1.x - p2.x, p1.y - p2.y);
34 | const pointNormalize = (p, length = 1) => pointMultiplyScalar(p, length / (pointLength(p) || Infinity));
35 | const createSegment = (p, i) => ({ p, i });
36 | const fit = (points, closed, error) => {
37 | // We need to duplicate the first and last segment when simplifying a
38 | // closed path.
39 | if (closed) {
40 | points.unshift(points[points.length - 1]);
41 | points.push(points[1]); // The point previously at index 0 is now 1.
42 | }
43 | const length = points.length;
44 | if (length === 0) {
45 | return [];
46 | }
47 | // To support reducing paths with multiple points in the same place
48 | // to one segment:
49 | const segments = [createSegment(points[0])];
50 | fitCubic(points, segments, error, 0, length - 1,
51 | // Left Tangent
52 | pointSubtract(points[1], points[0]),
53 | // Right Tangent
54 | pointSubtract(points[length - 2], points[length - 1]));
55 | // Remove the duplicated segments for closed paths again.
56 | if (closed) {
57 | segments.shift();
58 | segments.pop();
59 | }
60 | return segments;
61 | };
62 | // Fit a Bezier curve to a (sub)set of digitized points
63 | const fitCubic = (points, segments, error, first, last, tan1, tan2) => {
64 | // Use heuristic if region only has two points in it
65 | if (last - first === 1) {
66 | const pt1 = points[first], pt2 = points[last], dist = pointDistance(pt1, pt2) / 3;
67 | addCurve(segments, [pt1, pointAdd(pt1, pointNormalize(tan1, dist)), pointAdd(pt2, pointNormalize(tan2, dist)), pt2]);
68 | return;
69 | }
70 | // Parameterize points, and attempt to fit curve
71 | const uPrime = chordLengthParameterize(points, first, last);
72 | let maxError = Math.max(error, error * error), split, parametersInOrder = true;
73 | // Try not 4 but 5 iterations
74 | for (let i = 0; i <= 4; i++) {
75 | const curve = generateBezier(points, first, last, uPrime, tan1, tan2);
76 | // Find max deviation of points to fitted curve
77 | const max = findMaxError(points, first, last, curve, uPrime);
78 | if (max.error < error && parametersInOrder) {
79 | addCurve(segments, curve);
80 | return;
81 | }
82 | split = max.index;
83 | // If error not too large, try reparameterization and iteration
84 | if (max.error >= maxError)
85 | break;
86 | parametersInOrder = reparameterize(points, first, last, uPrime, curve);
87 | maxError = max.error;
88 | }
89 | // Fitting failed -- split at max error point and fit recursively
90 | const tanCenter = pointSubtract(points[split - 1], points[split + 1]);
91 | fitCubic(points, segments, error, first, split, tan1, tanCenter);
92 | fitCubic(points, segments, error, split, last, pointNegate(tanCenter), tan2);
93 | };
94 | const addCurve = (segments, curve) => {
95 | const prev = segments[segments.length - 1];
96 | prev.o = pointSubtract(curve[1], curve[0]);
97 | segments.push(createSegment(curve[3], pointSubtract(curve[2], curve[3])));
98 | };
99 | // Use least-squares method to find Bezier control points for region.
100 | const generateBezier = (points, first, last, uPrime, tan1, tan2) => {
101 | const epsilon = /*#=*/ EPSILON, abs = Math.abs, pt1 = points[first], pt2 = points[last],
102 | // Create the C and X matrices
103 | C = [
104 | [0, 0],
105 | [0, 0],
106 | ], X = [0, 0];
107 | for (let i = 0, l = last - first + 1; i < l; i++) {
108 | const u = uPrime[i], t = 1 - u, b = 3 * u * t, b0 = t * t * t, b1 = b * t, b2 = b * u, b3 = u * u * u, a1 = pointNormalize(tan1, b1), a2 = pointNormalize(tan2, b2), tmp = pointSubtract(pointSubtract(points[first + i], pointMultiplyScalar(pt1, b0 + b1)), pointMultiplyScalar(pt2, b2 + b3));
109 | C[0][0] += pointDot(a1, a1);
110 | C[0][1] += pointDot(a1, a2);
111 | // C[1][0] += a1.dot(a2);
112 | C[1][0] = C[0][1];
113 | C[1][1] += pointDot(a2, a2);
114 | X[0] += pointDot(a1, tmp);
115 | X[1] += pointDot(a2, tmp);
116 | }
117 | // Compute the determinants of C and X
118 | const detC0C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1];
119 | let alpha1;
120 | let alpha2;
121 | if (abs(detC0C1) > epsilon) {
122 | // Kramer's rule
123 | const detC0X = C[0][0] * X[1] - C[1][0] * X[0], detXC1 = X[0] * C[1][1] - X[1] * C[0][1];
124 | // Derive alpha values
125 | alpha1 = detXC1 / detC0C1;
126 | alpha2 = detC0X / detC0C1;
127 | }
128 | else {
129 | // Matrix is under-determined, try assuming alpha1 == alpha2
130 | const c0 = C[0][0] + C[0][1], c1 = C[1][0] + C[1][1];
131 | alpha1 = alpha2 = abs(c0) > epsilon ? X[0] / c0 : abs(c1) > epsilon ? X[1] / c1 : 0;
132 | }
133 | // If alpha negative, use the Wu/Barsky heuristic (see text)
134 | // (if alpha is 0, you get coincident control points that lead to
135 | // divide by zero in any subsequent NewtonRaphsonRootFind() call.
136 | const segLength = pointDistance(pt2, pt1), eps = epsilon * segLength;
137 | let handle1, handle2;
138 | if (alpha1 < eps || alpha2 < eps) {
139 | // fall back on standard (probably inaccurate) formula,
140 | // and subdivide further if needed.
141 | alpha1 = alpha2 = segLength / 3;
142 | }
143 | else {
144 | // Check if the found control points are in the right order when
145 | // projected onto the line through pt1 and pt2.
146 | const line = pointSubtract(pt2, pt1);
147 | // Control points 1 and 2 are positioned an alpha distance out
148 | // on the tangent vectors, left and right, respectively
149 | handle1 = pointNormalize(tan1, alpha1);
150 | handle2 = pointNormalize(tan2, alpha2);
151 | if (pointDot(handle1, line) - pointDot(handle2, line) > segLength * segLength) {
152 | // Fall back to the Wu/Barsky heuristic above.
153 | alpha1 = alpha2 = segLength / 3;
154 | handle1 = handle2 = null; // Force recalculation
155 | }
156 | }
157 | // First and last control points of the Bezier curve are
158 | // positioned exactly at the first and last data points
159 | return [pt1, pointAdd(pt1, handle1 || pointNormalize(tan1, alpha1)), pointAdd(pt2, handle2 || pointNormalize(tan2, alpha2)), pt2];
160 | };
161 | // Given set of points and their parameterization, try to find
162 | // a better parameterization.
163 | const reparameterize = (points, first, last, u, curve) => {
164 | for (let i = first; i <= last; i++) {
165 | u[i - first] = findRoot(curve, points[i], u[i - first]);
166 | }
167 | // Detect if the new parameterization has reordered the points.
168 | // In that case, we would fit the points of the path in the wrong order.
169 | for (let i = 1, l = u.length; i < l; i++) {
170 | if (u[i] <= u[i - 1])
171 | return false;
172 | }
173 | return true;
174 | };
175 | // Use Newton-Raphson iteration to find better root.
176 | const findRoot = (curve, point, u) => {
177 | const curve1 = [], curve2 = [];
178 | // Generate control vertices for Q'
179 | for (let i = 0; i <= 2; i++) {
180 | curve1[i] = pointMultiplyScalar(pointSubtract(curve[i + 1], curve[i]), 3);
181 | }
182 | // Generate control vertices for Q''
183 | for (let i = 0; i <= 1; i++) {
184 | curve2[i] = pointMultiplyScalar(pointSubtract(curve1[i + 1], curve1[i]), 2);
185 | }
186 | // Compute Q(u), Q'(u) and Q''(u)
187 | const pt = evaluate(3, curve, u), pt1 = evaluate(2, curve1, u), pt2 = evaluate(1, curve2, u), diff = pointSubtract(pt, point), df = pointDot(pt1, pt1) + pointDot(diff, pt2);
188 | // u = u - f(u) / f'(u)
189 | return isMachineZero(df) ? u : u - pointDot(diff, pt1) / df;
190 | };
191 | // Evaluate a bezier curve at a particular parameter value
192 | const evaluate = (degree, curve, t) => {
193 | // Copy array
194 | const tmp = curve.slice();
195 | // Triangle computation
196 | for (let i = 1; i <= degree; i++) {
197 | for (let j = 0; j <= degree - i; j++) {
198 | tmp[j] = pointAdd(pointMultiplyScalar(tmp[j], 1 - t), pointMultiplyScalar(tmp[j + 1], t));
199 | }
200 | }
201 | return tmp[0];
202 | };
203 | // Assign parameter values to digitized points
204 | // using relative distances between points.
205 | const chordLengthParameterize = (points, first, last) => {
206 | const u = [0];
207 | for (let i = first + 1; i <= last; i++) {
208 | u[i - first] = u[i - first - 1] + pointDistance(points[i], points[i - 1]);
209 | }
210 | for (let i = 1, m = last - first; i <= m; i++) {
211 | u[i] /= u[m];
212 | }
213 | return u;
214 | };
215 | // Find the maximum squared distance of digitized points to fitted curve.
216 | const findMaxError = (points, first, last, curve, u) => {
217 | let index = Math.floor((last - first + 1) / 2), maxDist = 0;
218 | for (let i = first + 1; i < last; i++) {
219 | const P = evaluate(3, curve, u[i - first]);
220 | const v = pointSubtract(P, points[i]);
221 | const dist = v.x * v.x + v.y * v.y; // squared
222 | if (dist >= maxDist) {
223 | maxDist = dist;
224 | index = i;
225 | }
226 | }
227 | return {
228 | error: maxDist,
229 | index: index,
230 | };
231 | };
232 | const getSegmentsPathData = (segments, closed, precision) => {
233 | const length = segments.length;
234 | const precisionMultiplier = 10 ** precision;
235 | const round = precision < 16 ? (n) => Math.round(n * precisionMultiplier) / precisionMultiplier : (n) => n;
236 | const formatPair = (x, y) => round(x) + ',' + round(y);
237 | let first = true;
238 | let prevX, prevY, outX, outY;
239 | const parts = [];
240 | const addSegment = (segment, skipLine) => {
241 | const curX = segment.p.x;
242 | const curY = segment.p.y;
243 | if (first) {
244 | parts.push('M' + formatPair(curX, curY));
245 | first = false;
246 | }
247 | else {
248 | const inX = curX + (segment.i?.x ?? 0);
249 | const inY = curY + (segment.i?.y ?? 0);
250 | if (inX === curX && inY === curY && outX === prevX && outY === prevY) {
251 | // l = relative lineto:
252 | if (!skipLine) {
253 | const dx = curX - prevX;
254 | const dy = curY - prevY;
255 | parts.push(dx === 0 ? 'v' + round(dy) : dy === 0 ? 'h' + round(dx) : 'l' + formatPair(dx, dy));
256 | }
257 | }
258 | else {
259 | // c = relative curveto:
260 | parts.push('c' +
261 | formatPair(outX - prevX, outY - prevY) +
262 | ' ' +
263 | formatPair(inX - prevX, inY - prevY) +
264 | ' ' +
265 | formatPair(curX - prevX, curY - prevY));
266 | }
267 | }
268 | prevX = curX;
269 | prevY = curY;
270 | outX = curX + (segment.o?.x ?? 0);
271 | outY = curY + (segment.o?.y ?? 0);
272 | };
273 | if (!length)
274 | return '';
275 | for (let i = 0; i < length; i++)
276 | addSegment(segments[i]);
277 | // Close path by drawing first segment again
278 | if (closed && length > 0) {
279 | addSegment(segments[0], true);
280 | parts.push('z');
281 | }
282 | return parts.join('');
283 | };
284 | const simplifySvgPath = (points, options = {}) => {
285 | if (points.length === 0) {
286 | return '';
287 | }
288 | return getSegmentsPathData(fit(points.map(typeof points[0].x === 'number' ? (p) => point(p.x, p.y) : (p) => point(p[0], p[1])), options.closed, options.tolerance ?? 2.5), options.closed, options.precision ?? 5);
289 | };
290 | return simplifySvgPath;
291 | })()
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | * simplify-svg-path
3 | *
4 | * The logic is a copy of Paper.js v0.12.11.
5 | */
6 |
7 | /*
8 | * Paper.js - The Swiss Army Knife of Vector Graphics Scripting.
9 | * http://paperjs.org/
10 | *
11 | * Copyright (c) 2011 - 2020, Jürg Lehni & Jonathan Puckey
12 | * http://juerglehni.com/ & https://puckey.studio/
13 | *
14 | * Distributed under the MIT license. See LICENSE file for details.
15 | *
16 | * All rights reserved.
17 | */
18 |
19 | // An Algorithm for Automatically Fitting Digitized Curves
20 | // by Philip J. Schneider
21 | // from "Graphics Gems", Academic Press, 1990
22 | // Modifications and optimizations of original algorithm by Jürg Lehni.
23 | const EPSILON = 1e-12
24 | const MACHINE_EPSILON = 1.12e-16
25 | const isMachineZero = (val: number) => val >= -MACHINE_EPSILON && val <= MACHINE_EPSILON
26 |
27 | // `Math.sqrt(x * x + y * y)` seems to be faster than `Math.hypot(x, y)`
28 | const hypot = (x: number, y: number) => Math.sqrt(x * x + y * y)
29 |
30 | interface Point {
31 | readonly x: number
32 | readonly y: number
33 | }
34 | const point = (x: number, y: number) => ({ x, y })
35 | const pointLength = (p: Point) => hypot(p.x, p.y)
36 | const pointNegate = (p: Point) => point(-p.x, -p.y)
37 | const pointAdd = (p1: Point, p2: Point) => point(p1.x + p2.x, p1.y + p2.y)
38 | const pointSubtract = (p1: Point, p2: Point) => point(p1.x - p2.x, p1.y - p2.y)
39 | const pointMultiplyScalar = (p: Point, n: number) => point(p.x * n, p.y * n)
40 | const pointDot = (p1: Point, p2: Point) => p1.x * p2.x + p1.y * p2.y
41 | const pointDistance = (p1: Point, p2: Point) => hypot(p1.x - p2.x, p1.y - p2.y)
42 | const pointNormalize = (p: Point, length = 1) => pointMultiplyScalar(p, length / (pointLength(p) || Infinity))
43 |
44 | interface Segment {
45 | readonly p: Point
46 | readonly i?: Point // handleIn
47 | o?: Point // handleOut
48 | }
49 | const createSegment = (p: Point, i?: Point) => ({ p, i })
50 |
51 | const fit = (points: Point[], closed: unknown, error: number) => {
52 | // We need to duplicate the first and last segment when simplifying a
53 | // closed path.
54 | if (closed) {
55 | points.unshift(points[points.length - 1])
56 | points.push(points[1]) // The point previously at index 0 is now 1.
57 | }
58 | const length = points.length
59 | if (length === 0) {
60 | return []
61 | }
62 | // To support reducing paths with multiple points in the same place
63 | // to one segment:
64 | const segments = [createSegment(points[0])]
65 | fitCubic(
66 | points,
67 | segments,
68 | error,
69 | 0,
70 | length - 1,
71 | // Left Tangent
72 | pointSubtract(points[1], points[0]),
73 | // Right Tangent
74 | pointSubtract(points[length - 2], points[length - 1]),
75 | )
76 | // Remove the duplicated segments for closed paths again.
77 | if (closed) {
78 | segments.shift()
79 | segments.pop()
80 | }
81 | return segments
82 | }
83 |
84 | // Fit a Bezier curve to a (sub)set of digitized points
85 | const fitCubic = (points: readonly Point[], segments: Segment[], error: number, first: number, last: number, tan1: Point, tan2: Point) => {
86 | // Use heuristic if region only has two points in it
87 | if (last - first === 1) {
88 | const pt1 = points[first],
89 | pt2 = points[last],
90 | dist = pointDistance(pt1, pt2) / 3
91 | addCurve(segments, [pt1, pointAdd(pt1, pointNormalize(tan1, dist)), pointAdd(pt2, pointNormalize(tan2, dist)), pt2])
92 | return
93 | }
94 | // Parameterize points, and attempt to fit curve
95 | const uPrime = chordLengthParameterize(points, first, last)
96 | let maxError = Math.max(error, error * error),
97 | split: number,
98 | parametersInOrder = true
99 | // Try not 4 but 5 iterations
100 | for (let i = 0; i <= 4; i++) {
101 | const curve = generateBezier(points, first, last, uPrime, tan1, tan2)
102 | // Find max deviation of points to fitted curve
103 | const max = findMaxError(points, first, last, curve, uPrime)
104 | if (max.error < error && parametersInOrder) {
105 | addCurve(segments, curve)
106 | return
107 | }
108 | split = max.index
109 | // If error not too large, try reparameterization and iteration
110 | if (max.error >= maxError) break
111 | parametersInOrder = reparameterize(points, first, last, uPrime, curve)
112 | maxError = max.error
113 | }
114 | // Fitting failed -- split at max error point and fit recursively
115 | const tanCenter = pointSubtract(points[split! - 1], points[split! + 1])
116 | fitCubic(points, segments, error, first, split!, tan1, tanCenter)
117 | fitCubic(points, segments, error, split!, last, pointNegate(tanCenter), tan2)
118 | }
119 |
120 | const addCurve = (segments: Segment[], curve: readonly Point[]) => {
121 | const prev = segments[segments.length - 1]
122 | prev.o = pointSubtract(curve[1], curve[0])
123 | segments.push(createSegment(curve[3], pointSubtract(curve[2], curve[3])))
124 | }
125 |
126 | // Use least-squares method to find Bezier control points for region.
127 | const generateBezier = (points: readonly Point[], first: number, last: number, uPrime: readonly number[], tan1: Point, tan2: Point) => {
128 | const epsilon = /*#=*/ EPSILON,
129 | abs = Math.abs,
130 | pt1 = points[first],
131 | pt2 = points[last],
132 | // Create the C and X matrices
133 | C = [
134 | [0, 0],
135 | [0, 0],
136 | ],
137 | X = [0, 0]
138 |
139 | for (let i = 0, l = last - first + 1; i < l; i++) {
140 | const u = uPrime[i],
141 | t = 1 - u,
142 | b = 3 * u * t,
143 | b0 = t * t * t,
144 | b1 = b * t,
145 | b2 = b * u,
146 | b3 = u * u * u,
147 | a1 = pointNormalize(tan1, b1),
148 | a2 = pointNormalize(tan2, b2),
149 | tmp = pointSubtract(pointSubtract(points[first + i], pointMultiplyScalar(pt1, b0 + b1)), pointMultiplyScalar(pt2, b2 + b3))
150 | C[0][0] += pointDot(a1, a1)
151 | C[0][1] += pointDot(a1, a2)
152 | // C[1][0] += a1.dot(a2);
153 | C[1][0] = C[0][1]
154 | C[1][1] += pointDot(a2, a2)
155 | X[0] += pointDot(a1, tmp)
156 | X[1] += pointDot(a2, tmp)
157 | }
158 |
159 | // Compute the determinants of C and X
160 | const detC0C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1]
161 | let alpha1
162 | let alpha2
163 | if (abs(detC0C1) > epsilon) {
164 | // Kramer's rule
165 | const detC0X = C[0][0] * X[1] - C[1][0] * X[0],
166 | detXC1 = X[0] * C[1][1] - X[1] * C[0][1]
167 | // Derive alpha values
168 | alpha1 = detXC1 / detC0C1
169 | alpha2 = detC0X / detC0C1
170 | } else {
171 | // Matrix is under-determined, try assuming alpha1 == alpha2
172 | const c0 = C[0][0] + C[0][1],
173 | c1 = C[1][0] + C[1][1]
174 | alpha1 = alpha2 = abs(c0) > epsilon ? X[0] / c0 : abs(c1) > epsilon ? X[1] / c1 : 0
175 | }
176 |
177 | // If alpha negative, use the Wu/Barsky heuristic (see text)
178 | // (if alpha is 0, you get coincident control points that lead to
179 | // divide by zero in any subsequent NewtonRaphsonRootFind() call.
180 | const segLength = pointDistance(pt2, pt1),
181 | eps = epsilon * segLength
182 | let handle1, handle2
183 | if (alpha1 < eps || alpha2 < eps) {
184 | // fall back on standard (probably inaccurate) formula,
185 | // and subdivide further if needed.
186 | alpha1 = alpha2 = segLength / 3
187 | } else {
188 | // Check if the found control points are in the right order when
189 | // projected onto the line through pt1 and pt2.
190 | const line = pointSubtract(pt2, pt1)
191 | // Control points 1 and 2 are positioned an alpha distance out
192 | // on the tangent vectors, left and right, respectively
193 | handle1 = pointNormalize(tan1, alpha1)
194 | handle2 = pointNormalize(tan2, alpha2)
195 | if (pointDot(handle1, line) - pointDot(handle2, line) > segLength * segLength) {
196 | // Fall back to the Wu/Barsky heuristic above.
197 | alpha1 = alpha2 = segLength / 3
198 | handle1 = handle2 = null // Force recalculation
199 | }
200 | }
201 |
202 | // First and last control points of the Bezier curve are
203 | // positioned exactly at the first and last data points
204 | return [pt1, pointAdd(pt1, handle1 || pointNormalize(tan1, alpha1)), pointAdd(pt2, handle2 || pointNormalize(tan2, alpha2)), pt2]
205 | }
206 |
207 | // Given set of points and their parameterization, try to find
208 | // a better parameterization.
209 | const reparameterize = (points: readonly Point[], first: number, last: number, u: number[], curve: Point[]) => {
210 | for (let i = first; i <= last; i++) {
211 | u[i - first] = findRoot(curve, points[i], u[i - first])
212 | }
213 | // Detect if the new parameterization has reordered the points.
214 | // In that case, we would fit the points of the path in the wrong order.
215 | for (let i = 1, l = u.length; i < l; i++) {
216 | if (u[i] <= u[i - 1]) return false
217 | }
218 | return true
219 | }
220 |
221 | // Use Newton-Raphson iteration to find better root.
222 | const findRoot = (curve: readonly Point[], point: Point, u: number) => {
223 | const curve1 = [],
224 | curve2 = []
225 | // Generate control vertices for Q'
226 | for (let i = 0; i <= 2; i++) {
227 | curve1[i] = pointMultiplyScalar(pointSubtract(curve[i + 1], curve[i]), 3)
228 | }
229 | // Generate control vertices for Q''
230 | for (let i = 0; i <= 1; i++) {
231 | curve2[i] = pointMultiplyScalar(pointSubtract(curve1[i + 1], curve1[i]), 2)
232 | }
233 | // Compute Q(u), Q'(u) and Q''(u)
234 | const pt = evaluate(3, curve, u),
235 | pt1 = evaluate(2, curve1, u),
236 | pt2 = evaluate(1, curve2, u),
237 | diff = pointSubtract(pt, point),
238 | df = pointDot(pt1, pt1) + pointDot(diff, pt2)
239 | // u = u - f(u) / f'(u)
240 | return isMachineZero(df) ? u : u - pointDot(diff, pt1) / df
241 | }
242 |
243 | // Evaluate a bezier curve at a particular parameter value
244 | const evaluate = (degree: number, curve: readonly Point[], t: number) => {
245 | // Copy array
246 | const tmp = curve.slice()
247 | // Triangle computation
248 | for (let i = 1; i <= degree; i++) {
249 | for (let j = 0; j <= degree - i; j++) {
250 | tmp[j] = pointAdd(pointMultiplyScalar(tmp[j], 1 - t), pointMultiplyScalar(tmp[j + 1], t))
251 | }
252 | }
253 | return tmp[0]
254 | }
255 |
256 | // Assign parameter values to digitized points
257 | // using relative distances between points.
258 | const chordLengthParameterize = (points: readonly Point[], first: number, last: number) => {
259 | const u = [0]
260 | for (let i = first + 1; i <= last; i++) {
261 | u[i - first] = u[i - first - 1] + pointDistance(points[i], points[i - 1])
262 | }
263 | for (let i = 1, m = last - first; i <= m; i++) {
264 | u[i] /= u[m]
265 | }
266 | return u
267 | }
268 |
269 | // Find the maximum squared distance of digitized points to fitted curve.
270 | const findMaxError = (points: readonly Point[], first: number, last: number, curve: Point[], u: number[]) => {
271 | let index = Math.floor((last - first + 1) / 2),
272 | maxDist = 0
273 | for (let i = first + 1; i < last; i++) {
274 | const P = evaluate(3, curve, u[i - first])
275 | const v = pointSubtract(P, points[i])
276 | const dist = v.x * v.x + v.y * v.y // squared
277 | if (dist >= maxDist) {
278 | maxDist = dist
279 | index = i
280 | }
281 | }
282 | return {
283 | error: maxDist,
284 | index: index,
285 | }
286 | }
287 |
288 | const getSegmentsPathData = (segments: Segment[], closed: unknown, precision: number) => {
289 | const length = segments.length
290 | const precisionMultiplier = 10 ** precision
291 | const round = precision < 16 ? (n: number) => Math.round(n * precisionMultiplier) / precisionMultiplier : (n: number) => n
292 | const formatPair = (x: number, y: number) => round(x) + ',' + round(y)
293 | let first = true
294 | let prevX: number, prevY: number, outX: number, outY: number
295 | const parts: string[] = []
296 |
297 | const addSegment = (segment: Segment, skipLine?: boolean) => {
298 | const curX = segment.p.x
299 | const curY = segment.p.y
300 | if (first) {
301 | parts.push('M' + formatPair(curX, curY))
302 | first = false
303 | } else {
304 | const inX = curX + (segment.i?.x ?? 0)
305 | const inY = curY + (segment.i?.y ?? 0)
306 | if (inX === curX && inY === curY && outX === prevX && outY === prevY) {
307 | // l = relative lineto:
308 | if (!skipLine) {
309 | const dx = curX - prevX
310 | const dy = curY - prevY
311 | parts.push(dx === 0 ? 'v' + round(dy) : dy === 0 ? 'h' + round(dx) : 'l' + formatPair(dx, dy))
312 | }
313 | } else {
314 | // c = relative curveto:
315 | parts.push(
316 | 'c' +
317 | formatPair(outX - prevX, outY - prevY) +
318 | ' ' +
319 | formatPair(inX - prevX, inY - prevY) +
320 | ' ' +
321 | formatPair(curX - prevX, curY - prevY),
322 | )
323 | }
324 | }
325 | prevX = curX
326 | prevY = curY
327 | outX = curX + (segment.o?.x ?? 0)
328 | outY = curY + (segment.o?.y ?? 0)
329 | }
330 |
331 | if (!length) return ''
332 |
333 | for (let i = 0; i < length; i++) addSegment(segments[i])
334 | // Close path by drawing first segment again
335 | if (closed && length > 0) {
336 | addSegment(segments[0], true)
337 | parts.push('z')
338 | }
339 | return parts.join('')
340 | }
341 |
342 | const simplifySvgPath = (
343 | points: readonly (readonly [number, number])[] | readonly Point[],
344 | options: { closed?: boolean; tolerance?: number; precision?: number } = {},
345 | ) => {
346 | if (points.length === 0) {
347 | return ''
348 | }
349 | return getSegmentsPathData(
350 | fit(
351 | points.map(typeof (points[0] as { readonly x: number }).x === 'number' ? (p: any) => point(p.x, p.y) : (p: any) => point(p[0], p[1])),
352 | options.closed,
353 | options.tolerance ?? 2.5,
354 | ),
355 | options.closed,
356 | options.precision ?? 5,
357 | )
358 | }
359 |
360 | export default simplifySvgPath
361 |
--------------------------------------------------------------------------------