├── .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 | --------------------------------------------------------------------------------