├── .editorconfig ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── build-iife.js ├── index.d.ts ├── index.html ├── index.iife.js ├── index.js ├── index.ts ├── package-lock.json ├── package.json ├── test.js └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /build-iife.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import { dirname, resolve } from 'path' 3 | import { fileURLToPath } from 'url' 4 | 5 | const __filename = fileURLToPath(import.meta.url) 6 | const __dirname = dirname(__filename) 7 | 8 | const esm = fs.readFileSync(resolve(__dirname, 'index.js'), 'utf8') 9 | const iife = `var simplifySvgPath=(()=>{${esm.replace('export default', 'return')}})()` 10 | fs.writeFileSync(resolve(__dirname, 'index.iife.js'), iife, 'utf8') 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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.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.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 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@luncheon/simplify-svg-path", 3 | "version": "0.2.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@luncheon/simplify-svg-path", 9 | "version": "0.2.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "jsdom": "^20.0.3", 13 | "paper": "^0.12.17", 14 | "prettier": "^2.8.0", 15 | "typescript": "^4.9.3" 16 | } 17 | }, 18 | "node_modules/@tootallnate/once": { 19 | "version": "2.0.0", 20 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", 21 | "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", 22 | "dev": true, 23 | "engines": { 24 | "node": ">= 10" 25 | } 26 | }, 27 | "node_modules/abab": { 28 | "version": "2.0.6", 29 | "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", 30 | "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", 31 | "dev": true 32 | }, 33 | "node_modules/acorn": { 34 | "version": "8.8.1", 35 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", 36 | "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", 37 | "dev": true, 38 | "bin": { 39 | "acorn": "bin/acorn" 40 | }, 41 | "engines": { 42 | "node": ">=0.4.0" 43 | } 44 | }, 45 | "node_modules/acorn-globals": { 46 | "version": "7.0.1", 47 | "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", 48 | "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", 49 | "dev": true, 50 | "dependencies": { 51 | "acorn": "^8.1.0", 52 | "acorn-walk": "^8.0.2" 53 | } 54 | }, 55 | "node_modules/acorn-walk": { 56 | "version": "8.2.0", 57 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", 58 | "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", 59 | "dev": true, 60 | "engines": { 61 | "node": ">=0.4.0" 62 | } 63 | }, 64 | "node_modules/agent-base": { 65 | "version": "6.0.2", 66 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 67 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 68 | "dev": true, 69 | "dependencies": { 70 | "debug": "4" 71 | }, 72 | "engines": { 73 | "node": ">= 6.0.0" 74 | } 75 | }, 76 | "node_modules/asynckit": { 77 | "version": "0.4.0", 78 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 79 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 80 | "dev": true 81 | }, 82 | "node_modules/combined-stream": { 83 | "version": "1.0.8", 84 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 85 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 86 | "dev": true, 87 | "dependencies": { 88 | "delayed-stream": "~1.0.0" 89 | }, 90 | "engines": { 91 | "node": ">= 0.8" 92 | } 93 | }, 94 | "node_modules/cssom": { 95 | "version": "0.5.0", 96 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", 97 | "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", 98 | "dev": true 99 | }, 100 | "node_modules/cssstyle": { 101 | "version": "2.3.0", 102 | "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", 103 | "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", 104 | "dev": true, 105 | "dependencies": { 106 | "cssom": "~0.3.6" 107 | }, 108 | "engines": { 109 | "node": ">=8" 110 | } 111 | }, 112 | "node_modules/cssstyle/node_modules/cssom": { 113 | "version": "0.3.8", 114 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", 115 | "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", 116 | "dev": true 117 | }, 118 | "node_modules/data-urls": { 119 | "version": "3.0.2", 120 | "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", 121 | "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", 122 | "dev": true, 123 | "dependencies": { 124 | "abab": "^2.0.6", 125 | "whatwg-mimetype": "^3.0.0", 126 | "whatwg-url": "^11.0.0" 127 | }, 128 | "engines": { 129 | "node": ">=12" 130 | } 131 | }, 132 | "node_modules/debug": { 133 | "version": "4.3.4", 134 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 135 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 136 | "dev": true, 137 | "dependencies": { 138 | "ms": "2.1.2" 139 | }, 140 | "engines": { 141 | "node": ">=6.0" 142 | }, 143 | "peerDependenciesMeta": { 144 | "supports-color": { 145 | "optional": true 146 | } 147 | } 148 | }, 149 | "node_modules/decimal.js": { 150 | "version": "10.4.2", 151 | "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz", 152 | "integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==", 153 | "dev": true 154 | }, 155 | "node_modules/deep-is": { 156 | "version": "0.1.4", 157 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 158 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 159 | "dev": true 160 | }, 161 | "node_modules/delayed-stream": { 162 | "version": "1.0.0", 163 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 164 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 165 | "dev": true, 166 | "engines": { 167 | "node": ">=0.4.0" 168 | } 169 | }, 170 | "node_modules/domexception": { 171 | "version": "4.0.0", 172 | "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", 173 | "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", 174 | "dev": true, 175 | "dependencies": { 176 | "webidl-conversions": "^7.0.0" 177 | }, 178 | "engines": { 179 | "node": ">=12" 180 | } 181 | }, 182 | "node_modules/entities": { 183 | "version": "4.4.0", 184 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", 185 | "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", 186 | "dev": true, 187 | "engines": { 188 | "node": ">=0.12" 189 | }, 190 | "funding": { 191 | "url": "https://github.com/fb55/entities?sponsor=1" 192 | } 193 | }, 194 | "node_modules/escodegen": { 195 | "version": "2.0.0", 196 | "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", 197 | "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", 198 | "dev": true, 199 | "dependencies": { 200 | "esprima": "^4.0.1", 201 | "estraverse": "^5.2.0", 202 | "esutils": "^2.0.2", 203 | "optionator": "^0.8.1" 204 | }, 205 | "bin": { 206 | "escodegen": "bin/escodegen.js", 207 | "esgenerate": "bin/esgenerate.js" 208 | }, 209 | "engines": { 210 | "node": ">=6.0" 211 | }, 212 | "optionalDependencies": { 213 | "source-map": "~0.6.1" 214 | } 215 | }, 216 | "node_modules/esprima": { 217 | "version": "4.0.1", 218 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 219 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 220 | "dev": true, 221 | "bin": { 222 | "esparse": "bin/esparse.js", 223 | "esvalidate": "bin/esvalidate.js" 224 | }, 225 | "engines": { 226 | "node": ">=4" 227 | } 228 | }, 229 | "node_modules/estraverse": { 230 | "version": "5.3.0", 231 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 232 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 233 | "dev": true, 234 | "engines": { 235 | "node": ">=4.0" 236 | } 237 | }, 238 | "node_modules/esutils": { 239 | "version": "2.0.3", 240 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 241 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 242 | "dev": true, 243 | "engines": { 244 | "node": ">=0.10.0" 245 | } 246 | }, 247 | "node_modules/fast-levenshtein": { 248 | "version": "2.0.6", 249 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 250 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 251 | "dev": true 252 | }, 253 | "node_modules/form-data": { 254 | "version": "4.0.0", 255 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 256 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 257 | "dev": true, 258 | "dependencies": { 259 | "asynckit": "^0.4.0", 260 | "combined-stream": "^1.0.8", 261 | "mime-types": "^2.1.12" 262 | }, 263 | "engines": { 264 | "node": ">= 6" 265 | } 266 | }, 267 | "node_modules/html-encoding-sniffer": { 268 | "version": "3.0.0", 269 | "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", 270 | "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", 271 | "dev": true, 272 | "dependencies": { 273 | "whatwg-encoding": "^2.0.0" 274 | }, 275 | "engines": { 276 | "node": ">=12" 277 | } 278 | }, 279 | "node_modules/http-proxy-agent": { 280 | "version": "5.0.0", 281 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", 282 | "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", 283 | "dev": true, 284 | "dependencies": { 285 | "@tootallnate/once": "2", 286 | "agent-base": "6", 287 | "debug": "4" 288 | }, 289 | "engines": { 290 | "node": ">= 6" 291 | } 292 | }, 293 | "node_modules/https-proxy-agent": { 294 | "version": "5.0.1", 295 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 296 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 297 | "dev": true, 298 | "dependencies": { 299 | "agent-base": "6", 300 | "debug": "4" 301 | }, 302 | "engines": { 303 | "node": ">= 6" 304 | } 305 | }, 306 | "node_modules/iconv-lite": { 307 | "version": "0.6.3", 308 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 309 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 310 | "dev": true, 311 | "dependencies": { 312 | "safer-buffer": ">= 2.1.2 < 3.0.0" 313 | }, 314 | "engines": { 315 | "node": ">=0.10.0" 316 | } 317 | }, 318 | "node_modules/is-potential-custom-element-name": { 319 | "version": "1.0.1", 320 | "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", 321 | "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", 322 | "dev": true 323 | }, 324 | "node_modules/jsdom": { 325 | "version": "20.0.3", 326 | "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", 327 | "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", 328 | "dev": true, 329 | "dependencies": { 330 | "abab": "^2.0.6", 331 | "acorn": "^8.8.1", 332 | "acorn-globals": "^7.0.0", 333 | "cssom": "^0.5.0", 334 | "cssstyle": "^2.3.0", 335 | "data-urls": "^3.0.2", 336 | "decimal.js": "^10.4.2", 337 | "domexception": "^4.0.0", 338 | "escodegen": "^2.0.0", 339 | "form-data": "^4.0.0", 340 | "html-encoding-sniffer": "^3.0.0", 341 | "http-proxy-agent": "^5.0.0", 342 | "https-proxy-agent": "^5.0.1", 343 | "is-potential-custom-element-name": "^1.0.1", 344 | "nwsapi": "^2.2.2", 345 | "parse5": "^7.1.1", 346 | "saxes": "^6.0.0", 347 | "symbol-tree": "^3.2.4", 348 | "tough-cookie": "^4.1.2", 349 | "w3c-xmlserializer": "^4.0.0", 350 | "webidl-conversions": "^7.0.0", 351 | "whatwg-encoding": "^2.0.0", 352 | "whatwg-mimetype": "^3.0.0", 353 | "whatwg-url": "^11.0.0", 354 | "ws": "^8.11.0", 355 | "xml-name-validator": "^4.0.0" 356 | }, 357 | "engines": { 358 | "node": ">=14" 359 | }, 360 | "peerDependencies": { 361 | "canvas": "^2.5.0" 362 | }, 363 | "peerDependenciesMeta": { 364 | "canvas": { 365 | "optional": true 366 | } 367 | } 368 | }, 369 | "node_modules/levn": { 370 | "version": "0.3.0", 371 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 372 | "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", 373 | "dev": true, 374 | "dependencies": { 375 | "prelude-ls": "~1.1.2", 376 | "type-check": "~0.3.2" 377 | }, 378 | "engines": { 379 | "node": ">= 0.8.0" 380 | } 381 | }, 382 | "node_modules/mime-db": { 383 | "version": "1.52.0", 384 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 385 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 386 | "dev": true, 387 | "engines": { 388 | "node": ">= 0.6" 389 | } 390 | }, 391 | "node_modules/mime-types": { 392 | "version": "2.1.35", 393 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 394 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 395 | "dev": true, 396 | "dependencies": { 397 | "mime-db": "1.52.0" 398 | }, 399 | "engines": { 400 | "node": ">= 0.6" 401 | } 402 | }, 403 | "node_modules/ms": { 404 | "version": "2.1.2", 405 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 406 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 407 | "dev": true 408 | }, 409 | "node_modules/nwsapi": { 410 | "version": "2.2.2", 411 | "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", 412 | "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", 413 | "dev": true 414 | }, 415 | "node_modules/optionator": { 416 | "version": "0.8.3", 417 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", 418 | "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", 419 | "dev": true, 420 | "dependencies": { 421 | "deep-is": "~0.1.3", 422 | "fast-levenshtein": "~2.0.6", 423 | "levn": "~0.3.0", 424 | "prelude-ls": "~1.1.2", 425 | "type-check": "~0.3.2", 426 | "word-wrap": "~1.2.3" 427 | }, 428 | "engines": { 429 | "node": ">= 0.8.0" 430 | } 431 | }, 432 | "node_modules/paper": { 433 | "version": "0.12.17", 434 | "resolved": "https://registry.npmjs.org/paper/-/paper-0.12.17.tgz", 435 | "integrity": "sha512-oCe+e1C2w8hKIcGoAqUjD0GGxGPv+itrRXlEFUmp3H8tY/NTnHOkYgpJFPGw6OJ8Q1Wa6+RgzlY7Dx/2WWHtkA==", 436 | "dev": true, 437 | "engines": { 438 | "node": ">=8.0.0" 439 | } 440 | }, 441 | "node_modules/parse5": { 442 | "version": "7.1.2", 443 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", 444 | "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", 445 | "dev": true, 446 | "dependencies": { 447 | "entities": "^4.4.0" 448 | }, 449 | "funding": { 450 | "url": "https://github.com/inikulin/parse5?sponsor=1" 451 | } 452 | }, 453 | "node_modules/prelude-ls": { 454 | "version": "1.1.2", 455 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 456 | "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", 457 | "dev": true, 458 | "engines": { 459 | "node": ">= 0.8.0" 460 | } 461 | }, 462 | "node_modules/prettier": { 463 | "version": "2.8.0", 464 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz", 465 | "integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==", 466 | "dev": true, 467 | "bin": { 468 | "prettier": "bin-prettier.js" 469 | }, 470 | "engines": { 471 | "node": ">=10.13.0" 472 | }, 473 | "funding": { 474 | "url": "https://github.com/prettier/prettier?sponsor=1" 475 | } 476 | }, 477 | "node_modules/psl": { 478 | "version": "1.9.0", 479 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", 480 | "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", 481 | "dev": true 482 | }, 483 | "node_modules/punycode": { 484 | "version": "2.1.1", 485 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 486 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 487 | "dev": true, 488 | "engines": { 489 | "node": ">=6" 490 | } 491 | }, 492 | "node_modules/querystringify": { 493 | "version": "2.2.0", 494 | "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", 495 | "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", 496 | "dev": true 497 | }, 498 | "node_modules/requires-port": { 499 | "version": "1.0.0", 500 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 501 | "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", 502 | "dev": true 503 | }, 504 | "node_modules/safer-buffer": { 505 | "version": "2.1.2", 506 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 507 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 508 | "dev": true 509 | }, 510 | "node_modules/saxes": { 511 | "version": "6.0.0", 512 | "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", 513 | "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", 514 | "dev": true, 515 | "dependencies": { 516 | "xmlchars": "^2.2.0" 517 | }, 518 | "engines": { 519 | "node": ">=v12.22.7" 520 | } 521 | }, 522 | "node_modules/source-map": { 523 | "version": "0.6.1", 524 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 525 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 526 | "dev": true, 527 | "optional": true, 528 | "engines": { 529 | "node": ">=0.10.0" 530 | } 531 | }, 532 | "node_modules/symbol-tree": { 533 | "version": "3.2.4", 534 | "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", 535 | "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", 536 | "dev": true 537 | }, 538 | "node_modules/tough-cookie": { 539 | "version": "4.1.2", 540 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", 541 | "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", 542 | "dev": true, 543 | "dependencies": { 544 | "psl": "^1.1.33", 545 | "punycode": "^2.1.1", 546 | "universalify": "^0.2.0", 547 | "url-parse": "^1.5.3" 548 | }, 549 | "engines": { 550 | "node": ">=6" 551 | } 552 | }, 553 | "node_modules/tr46": { 554 | "version": "3.0.0", 555 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", 556 | "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", 557 | "dev": true, 558 | "dependencies": { 559 | "punycode": "^2.1.1" 560 | }, 561 | "engines": { 562 | "node": ">=12" 563 | } 564 | }, 565 | "node_modules/type-check": { 566 | "version": "0.3.2", 567 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 568 | "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", 569 | "dev": true, 570 | "dependencies": { 571 | "prelude-ls": "~1.1.2" 572 | }, 573 | "engines": { 574 | "node": ">= 0.8.0" 575 | } 576 | }, 577 | "node_modules/typescript": { 578 | "version": "4.9.3", 579 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", 580 | "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", 581 | "dev": true, 582 | "bin": { 583 | "tsc": "bin/tsc", 584 | "tsserver": "bin/tsserver" 585 | }, 586 | "engines": { 587 | "node": ">=4.2.0" 588 | } 589 | }, 590 | "node_modules/universalify": { 591 | "version": "0.2.0", 592 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", 593 | "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", 594 | "dev": true, 595 | "engines": { 596 | "node": ">= 4.0.0" 597 | } 598 | }, 599 | "node_modules/url-parse": { 600 | "version": "1.5.10", 601 | "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", 602 | "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", 603 | "dev": true, 604 | "dependencies": { 605 | "querystringify": "^2.1.1", 606 | "requires-port": "^1.0.0" 607 | } 608 | }, 609 | "node_modules/w3c-xmlserializer": { 610 | "version": "4.0.0", 611 | "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", 612 | "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", 613 | "dev": true, 614 | "dependencies": { 615 | "xml-name-validator": "^4.0.0" 616 | }, 617 | "engines": { 618 | "node": ">=14" 619 | } 620 | }, 621 | "node_modules/webidl-conversions": { 622 | "version": "7.0.0", 623 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 624 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 625 | "dev": true, 626 | "engines": { 627 | "node": ">=12" 628 | } 629 | }, 630 | "node_modules/whatwg-encoding": { 631 | "version": "2.0.0", 632 | "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", 633 | "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", 634 | "dev": true, 635 | "dependencies": { 636 | "iconv-lite": "0.6.3" 637 | }, 638 | "engines": { 639 | "node": ">=12" 640 | } 641 | }, 642 | "node_modules/whatwg-mimetype": { 643 | "version": "3.0.0", 644 | "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", 645 | "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", 646 | "dev": true, 647 | "engines": { 648 | "node": ">=12" 649 | } 650 | }, 651 | "node_modules/whatwg-url": { 652 | "version": "11.0.0", 653 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", 654 | "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", 655 | "dev": true, 656 | "dependencies": { 657 | "tr46": "^3.0.0", 658 | "webidl-conversions": "^7.0.0" 659 | }, 660 | "engines": { 661 | "node": ">=12" 662 | } 663 | }, 664 | "node_modules/word-wrap": { 665 | "version": "1.2.3", 666 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 667 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", 668 | "dev": true, 669 | "engines": { 670 | "node": ">=0.10.0" 671 | } 672 | }, 673 | "node_modules/ws": { 674 | "version": "8.11.0", 675 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", 676 | "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", 677 | "dev": true, 678 | "engines": { 679 | "node": ">=10.0.0" 680 | }, 681 | "peerDependencies": { 682 | "bufferutil": "^4.0.1", 683 | "utf-8-validate": "^5.0.2" 684 | }, 685 | "peerDependenciesMeta": { 686 | "bufferutil": { 687 | "optional": true 688 | }, 689 | "utf-8-validate": { 690 | "optional": true 691 | } 692 | } 693 | }, 694 | "node_modules/xml-name-validator": { 695 | "version": "4.0.0", 696 | "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", 697 | "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", 698 | "dev": true, 699 | "engines": { 700 | "node": ">=12" 701 | } 702 | }, 703 | "node_modules/xmlchars": { 704 | "version": "2.2.0", 705 | "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", 706 | "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", 707 | "dev": true 708 | } 709 | }, 710 | "dependencies": { 711 | "@tootallnate/once": { 712 | "version": "2.0.0", 713 | "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", 714 | "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", 715 | "dev": true 716 | }, 717 | "abab": { 718 | "version": "2.0.6", 719 | "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", 720 | "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", 721 | "dev": true 722 | }, 723 | "acorn": { 724 | "version": "8.8.1", 725 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", 726 | "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", 727 | "dev": true 728 | }, 729 | "acorn-globals": { 730 | "version": "7.0.1", 731 | "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", 732 | "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", 733 | "dev": true, 734 | "requires": { 735 | "acorn": "^8.1.0", 736 | "acorn-walk": "^8.0.2" 737 | } 738 | }, 739 | "acorn-walk": { 740 | "version": "8.2.0", 741 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", 742 | "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", 743 | "dev": true 744 | }, 745 | "agent-base": { 746 | "version": "6.0.2", 747 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", 748 | "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", 749 | "dev": true, 750 | "requires": { 751 | "debug": "4" 752 | } 753 | }, 754 | "asynckit": { 755 | "version": "0.4.0", 756 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 757 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 758 | "dev": true 759 | }, 760 | "combined-stream": { 761 | "version": "1.0.8", 762 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 763 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 764 | "dev": true, 765 | "requires": { 766 | "delayed-stream": "~1.0.0" 767 | } 768 | }, 769 | "cssom": { 770 | "version": "0.5.0", 771 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", 772 | "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", 773 | "dev": true 774 | }, 775 | "cssstyle": { 776 | "version": "2.3.0", 777 | "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", 778 | "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", 779 | "dev": true, 780 | "requires": { 781 | "cssom": "~0.3.6" 782 | }, 783 | "dependencies": { 784 | "cssom": { 785 | "version": "0.3.8", 786 | "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", 787 | "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", 788 | "dev": true 789 | } 790 | } 791 | }, 792 | "data-urls": { 793 | "version": "3.0.2", 794 | "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", 795 | "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", 796 | "dev": true, 797 | "requires": { 798 | "abab": "^2.0.6", 799 | "whatwg-mimetype": "^3.0.0", 800 | "whatwg-url": "^11.0.0" 801 | } 802 | }, 803 | "debug": { 804 | "version": "4.3.4", 805 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 806 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 807 | "dev": true, 808 | "requires": { 809 | "ms": "2.1.2" 810 | } 811 | }, 812 | "decimal.js": { 813 | "version": "10.4.2", 814 | "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz", 815 | "integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA==", 816 | "dev": true 817 | }, 818 | "deep-is": { 819 | "version": "0.1.4", 820 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 821 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 822 | "dev": true 823 | }, 824 | "delayed-stream": { 825 | "version": "1.0.0", 826 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 827 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 828 | "dev": true 829 | }, 830 | "domexception": { 831 | "version": "4.0.0", 832 | "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", 833 | "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", 834 | "dev": true, 835 | "requires": { 836 | "webidl-conversions": "^7.0.0" 837 | } 838 | }, 839 | "entities": { 840 | "version": "4.4.0", 841 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", 842 | "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", 843 | "dev": true 844 | }, 845 | "escodegen": { 846 | "version": "2.0.0", 847 | "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", 848 | "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", 849 | "dev": true, 850 | "requires": { 851 | "esprima": "^4.0.1", 852 | "estraverse": "^5.2.0", 853 | "esutils": "^2.0.2", 854 | "optionator": "^0.8.1", 855 | "source-map": "~0.6.1" 856 | } 857 | }, 858 | "esprima": { 859 | "version": "4.0.1", 860 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 861 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 862 | "dev": true 863 | }, 864 | "estraverse": { 865 | "version": "5.3.0", 866 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 867 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 868 | "dev": true 869 | }, 870 | "esutils": { 871 | "version": "2.0.3", 872 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 873 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 874 | "dev": true 875 | }, 876 | "fast-levenshtein": { 877 | "version": "2.0.6", 878 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 879 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 880 | "dev": true 881 | }, 882 | "form-data": { 883 | "version": "4.0.0", 884 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 885 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 886 | "dev": true, 887 | "requires": { 888 | "asynckit": "^0.4.0", 889 | "combined-stream": "^1.0.8", 890 | "mime-types": "^2.1.12" 891 | } 892 | }, 893 | "html-encoding-sniffer": { 894 | "version": "3.0.0", 895 | "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", 896 | "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", 897 | "dev": true, 898 | "requires": { 899 | "whatwg-encoding": "^2.0.0" 900 | } 901 | }, 902 | "http-proxy-agent": { 903 | "version": "5.0.0", 904 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", 905 | "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", 906 | "dev": true, 907 | "requires": { 908 | "@tootallnate/once": "2", 909 | "agent-base": "6", 910 | "debug": "4" 911 | } 912 | }, 913 | "https-proxy-agent": { 914 | "version": "5.0.1", 915 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", 916 | "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", 917 | "dev": true, 918 | "requires": { 919 | "agent-base": "6", 920 | "debug": "4" 921 | } 922 | }, 923 | "iconv-lite": { 924 | "version": "0.6.3", 925 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 926 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 927 | "dev": true, 928 | "requires": { 929 | "safer-buffer": ">= 2.1.2 < 3.0.0" 930 | } 931 | }, 932 | "is-potential-custom-element-name": { 933 | "version": "1.0.1", 934 | "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", 935 | "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", 936 | "dev": true 937 | }, 938 | "jsdom": { 939 | "version": "20.0.3", 940 | "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", 941 | "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", 942 | "dev": true, 943 | "requires": { 944 | "abab": "^2.0.6", 945 | "acorn": "^8.8.1", 946 | "acorn-globals": "^7.0.0", 947 | "cssom": "^0.5.0", 948 | "cssstyle": "^2.3.0", 949 | "data-urls": "^3.0.2", 950 | "decimal.js": "^10.4.2", 951 | "domexception": "^4.0.0", 952 | "escodegen": "^2.0.0", 953 | "form-data": "^4.0.0", 954 | "html-encoding-sniffer": "^3.0.0", 955 | "http-proxy-agent": "^5.0.0", 956 | "https-proxy-agent": "^5.0.1", 957 | "is-potential-custom-element-name": "^1.0.1", 958 | "nwsapi": "^2.2.2", 959 | "parse5": "^7.1.1", 960 | "saxes": "^6.0.0", 961 | "symbol-tree": "^3.2.4", 962 | "tough-cookie": "^4.1.2", 963 | "w3c-xmlserializer": "^4.0.0", 964 | "webidl-conversions": "^7.0.0", 965 | "whatwg-encoding": "^2.0.0", 966 | "whatwg-mimetype": "^3.0.0", 967 | "whatwg-url": "^11.0.0", 968 | "ws": "^8.11.0", 969 | "xml-name-validator": "^4.0.0" 970 | } 971 | }, 972 | "levn": { 973 | "version": "0.3.0", 974 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 975 | "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", 976 | "dev": true, 977 | "requires": { 978 | "prelude-ls": "~1.1.2", 979 | "type-check": "~0.3.2" 980 | } 981 | }, 982 | "mime-db": { 983 | "version": "1.52.0", 984 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 985 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 986 | "dev": true 987 | }, 988 | "mime-types": { 989 | "version": "2.1.35", 990 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 991 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 992 | "dev": true, 993 | "requires": { 994 | "mime-db": "1.52.0" 995 | } 996 | }, 997 | "ms": { 998 | "version": "2.1.2", 999 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1000 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1001 | "dev": true 1002 | }, 1003 | "nwsapi": { 1004 | "version": "2.2.2", 1005 | "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.2.tgz", 1006 | "integrity": "sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==", 1007 | "dev": true 1008 | }, 1009 | "optionator": { 1010 | "version": "0.8.3", 1011 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", 1012 | "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", 1013 | "dev": true, 1014 | "requires": { 1015 | "deep-is": "~0.1.3", 1016 | "fast-levenshtein": "~2.0.6", 1017 | "levn": "~0.3.0", 1018 | "prelude-ls": "~1.1.2", 1019 | "type-check": "~0.3.2", 1020 | "word-wrap": "~1.2.3" 1021 | } 1022 | }, 1023 | "paper": { 1024 | "version": "0.12.17", 1025 | "resolved": "https://registry.npmjs.org/paper/-/paper-0.12.17.tgz", 1026 | "integrity": "sha512-oCe+e1C2w8hKIcGoAqUjD0GGxGPv+itrRXlEFUmp3H8tY/NTnHOkYgpJFPGw6OJ8Q1Wa6+RgzlY7Dx/2WWHtkA==", 1027 | "dev": true 1028 | }, 1029 | "parse5": { 1030 | "version": "7.1.2", 1031 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", 1032 | "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", 1033 | "dev": true, 1034 | "requires": { 1035 | "entities": "^4.4.0" 1036 | } 1037 | }, 1038 | "prelude-ls": { 1039 | "version": "1.1.2", 1040 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 1041 | "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", 1042 | "dev": true 1043 | }, 1044 | "prettier": { 1045 | "version": "2.8.0", 1046 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.0.tgz", 1047 | "integrity": "sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==", 1048 | "dev": true 1049 | }, 1050 | "psl": { 1051 | "version": "1.9.0", 1052 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", 1053 | "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", 1054 | "dev": true 1055 | }, 1056 | "punycode": { 1057 | "version": "2.1.1", 1058 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1059 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 1060 | "dev": true 1061 | }, 1062 | "querystringify": { 1063 | "version": "2.2.0", 1064 | "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", 1065 | "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", 1066 | "dev": true 1067 | }, 1068 | "requires-port": { 1069 | "version": "1.0.0", 1070 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 1071 | "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", 1072 | "dev": true 1073 | }, 1074 | "safer-buffer": { 1075 | "version": "2.1.2", 1076 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1077 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1078 | "dev": true 1079 | }, 1080 | "saxes": { 1081 | "version": "6.0.0", 1082 | "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", 1083 | "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", 1084 | "dev": true, 1085 | "requires": { 1086 | "xmlchars": "^2.2.0" 1087 | } 1088 | }, 1089 | "source-map": { 1090 | "version": "0.6.1", 1091 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1092 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1093 | "dev": true, 1094 | "optional": true 1095 | }, 1096 | "symbol-tree": { 1097 | "version": "3.2.4", 1098 | "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", 1099 | "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", 1100 | "dev": true 1101 | }, 1102 | "tough-cookie": { 1103 | "version": "4.1.2", 1104 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", 1105 | "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", 1106 | "dev": true, 1107 | "requires": { 1108 | "psl": "^1.1.33", 1109 | "punycode": "^2.1.1", 1110 | "universalify": "^0.2.0", 1111 | "url-parse": "^1.5.3" 1112 | } 1113 | }, 1114 | "tr46": { 1115 | "version": "3.0.0", 1116 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", 1117 | "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", 1118 | "dev": true, 1119 | "requires": { 1120 | "punycode": "^2.1.1" 1121 | } 1122 | }, 1123 | "type-check": { 1124 | "version": "0.3.2", 1125 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 1126 | "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", 1127 | "dev": true, 1128 | "requires": { 1129 | "prelude-ls": "~1.1.2" 1130 | } 1131 | }, 1132 | "typescript": { 1133 | "version": "4.9.3", 1134 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.3.tgz", 1135 | "integrity": "sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==", 1136 | "dev": true 1137 | }, 1138 | "universalify": { 1139 | "version": "0.2.0", 1140 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", 1141 | "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", 1142 | "dev": true 1143 | }, 1144 | "url-parse": { 1145 | "version": "1.5.10", 1146 | "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", 1147 | "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", 1148 | "dev": true, 1149 | "requires": { 1150 | "querystringify": "^2.1.1", 1151 | "requires-port": "^1.0.0" 1152 | } 1153 | }, 1154 | "w3c-xmlserializer": { 1155 | "version": "4.0.0", 1156 | "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", 1157 | "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", 1158 | "dev": true, 1159 | "requires": { 1160 | "xml-name-validator": "^4.0.0" 1161 | } 1162 | }, 1163 | "webidl-conversions": { 1164 | "version": "7.0.0", 1165 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", 1166 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", 1167 | "dev": true 1168 | }, 1169 | "whatwg-encoding": { 1170 | "version": "2.0.0", 1171 | "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", 1172 | "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", 1173 | "dev": true, 1174 | "requires": { 1175 | "iconv-lite": "0.6.3" 1176 | } 1177 | }, 1178 | "whatwg-mimetype": { 1179 | "version": "3.0.0", 1180 | "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", 1181 | "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", 1182 | "dev": true 1183 | }, 1184 | "whatwg-url": { 1185 | "version": "11.0.0", 1186 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", 1187 | "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", 1188 | "dev": true, 1189 | "requires": { 1190 | "tr46": "^3.0.0", 1191 | "webidl-conversions": "^7.0.0" 1192 | } 1193 | }, 1194 | "word-wrap": { 1195 | "version": "1.2.3", 1196 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 1197 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", 1198 | "dev": true 1199 | }, 1200 | "ws": { 1201 | "version": "8.11.0", 1202 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", 1203 | "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", 1204 | "dev": true, 1205 | "requires": {} 1206 | }, 1207 | "xml-name-validator": { 1208 | "version": "4.0.0", 1209 | "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", 1210 | "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", 1211 | "dev": true 1212 | }, 1213 | "xmlchars": { 1214 | "version": "2.2.0", 1215 | "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", 1216 | "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", 1217 | "dev": true 1218 | } 1219 | } 1220 | } 1221 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------