svg-path-simplify 0.0.1 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +339 -21
  2. package/README.md +61 -2
  3. package/dist/svg-path-simplify.esm.js +4308 -0
  4. package/dist/svg-path-simplify.esm.min.js +1 -0
  5. package/dist/svg-path-simplify.js +4334 -0
  6. package/dist/svg-path-simplify.min.js +1 -0
  7. package/dist/svg-path-simplify.node.js +4331 -0
  8. package/dist/svg-path-simplify.node.min.js +1 -0
  9. package/index.html +230 -0
  10. package/package.json +5 -6
  11. package/src/constants.js +4 -0
  12. package/src/detect_input.js +42 -0
  13. package/src/index.js +21 -3
  14. package/src/pathData_simplify_cubic.js +324 -0
  15. package/src/pathData_simplify_cubic_arr.js +50 -0
  16. package/src/pathData_simplify_cubic_extrapolate.js +220 -0
  17. package/src/pathSimplify-main.js +400 -0
  18. package/src/svg_getViewbox.js +32 -0
  19. package/src/svgii/...parse.js +402 -0
  20. package/src/svgii/geometry.js +1143 -0
  21. package/src/svgii/geometry_area.js +265 -0
  22. package/src/svgii/geometry_bbox.js +223 -0
  23. package/src/svgii/pathData_analyze.js +896 -0
  24. package/src/svgii/pathData_convert.js +1180 -0
  25. package/src/svgii/pathData_parse.js +487 -0
  26. package/src/svgii/pathData_remove_collinear.js +98 -0
  27. package/src/svgii/pathData_remove_zerolength.js +28 -0
  28. package/src/svgii/pathData_reorder.js +238 -0
  29. package/src/svgii/pathData_reverse.js +124 -0
  30. package/src/svgii/pathData_scale.js +42 -0
  31. package/src/svgii/pathData_split.js +449 -0
  32. package/src/svgii/pathData_stringify.js +145 -0
  33. package/src/svgii/pathData_toPolygon.js +92 -0
  34. package/src/svgii/pathdata_cleanup.js +363 -0
  35. package/src/svgii/poly_analyze.js +172 -0
  36. package/src/svgii/poly_to_pathdata.js +185 -0
  37. package/src/svgii/rounding.js +162 -0
  38. package/src/svgii/simplify.js +248 -0
  39. package/src/svgii/simplify_bezier.js +470 -0
  40. package/src/svgii/simplify_linetos.js +93 -0
  41. package/src/svgii/simplify_polygon.js +135 -0
  42. package/src/svgii/stringify.js +103 -0
  43. package/src/svgii/svg_cleanup.js +86 -0
  44. package/src/svgii/visualize.js +317 -0
@@ -0,0 +1,4308 @@
1
+ function renderPoint(
2
+ svg,
3
+ coords,
4
+ fill = "red",
5
+ r = "1%",
6
+ opacity = "1",
7
+ title = '',
8
+ render = true,
9
+ id = "",
10
+ className = ""
11
+ ) {
12
+ if (Array.isArray(coords)) {
13
+ coords = {
14
+ x: coords[0],
15
+ y: coords[1]
16
+ };
17
+ }
18
+ let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
19
+ <title>${title}</title></circle>`;
20
+
21
+ if (render) {
22
+ svg.insertAdjacentHTML("beforeend", marker);
23
+ } else {
24
+ return marker;
25
+ }
26
+ }
27
+
28
+ function detectInputType(input) {
29
+ let type = 'string';
30
+ if (input instanceof HTMLImageElement) return "img";
31
+ if (input instanceof SVGElement) return "svg";
32
+ if (input instanceof HTMLCanvasElement) return "canvas";
33
+ if (input instanceof File) return "file";
34
+ if (input instanceof ArrayBuffer) return "buffer";
35
+ if (input instanceof Blob) return "blob";
36
+ if (Array.isArray(input)) return "array";
37
+
38
+ if (typeof input === "string") {
39
+ input = input.trim();
40
+ let isSVG = input.includes('<svg') && input.includes('</svg');
41
+ let isPathData = input.startsWith('M') || input.startsWith('m');
42
+ let isPolyString = !isNaN(input.substring(0, 1)) && !isNaN(input.substring(input.length-1, input.length));
43
+
44
+
45
+ if(isSVG) {
46
+ type='svgMarkup';
47
+ }
48
+ else if(isPathData) {
49
+ type='pathDataString';
50
+ }
51
+ else if(isPolyString) {
52
+ type='polyString';
53
+ }
54
+
55
+ else {
56
+ let url = /^(file:|https?:\/\/|\/|\.\/|\.\.\/)/.test(input);
57
+ let dataUrl = input.startsWith('data:image');
58
+ type = url || dataUrl ? "url" : "string";
59
+ }
60
+
61
+ return type
62
+ }
63
+
64
+ type = typeof input;
65
+ let constructor = input.constructor.name;
66
+
67
+ return (constructor || type).toLowerCase();
68
+ }
69
+
70
+ /*
71
+ import {abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
72
+ log, max, min, pow, random, round, sin, sqrt, tan, PI} from '/.constants.js';
73
+ */
74
+
75
+ const {
76
+ abs: abs$1, acos: acos$1, asin: asin$1, atan: atan$1, atan2: atan2$1, ceil: ceil$1, cos: cos$1, exp: exp$1, floor: floor$1,
77
+ log: log$1, max: max$1, min: min$1, pow: pow$1, random: random$1, round: round$1, sin: sin$1, sqrt: sqrt$1, tan: tan$1, PI: PI$1
78
+ } = Math;
79
+
80
+ // get angle helper
81
+ function getAngle(p1, p2, normalize = false) {
82
+ let angle = atan2$1(p2.y - p1.y, p2.x - p1.x);
83
+ // normalize negative angles
84
+ if (normalize && angle < 0) angle += Math.PI * 2;
85
+ return angle
86
+ }
87
+
88
+ /**
89
+ * based on: Justin C. Round's
90
+ * http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
91
+ */
92
+
93
+ function checkLineIntersection(p1, p2, p3, p4, exact = true) {
94
+ // if the lines intersect, the result contains the x and y of the intersection (treating the lines as infinite) and booleans for whether line segment 1 or line segment 2 contain the point
95
+ let denominator, a, b, numerator1, numerator2;
96
+ let intersectionPoint = {};
97
+
98
+ try {
99
+ denominator = ((p4.y - p3.y) * (p2.x - p1.x)) - ((p4.x - p3.x) * (p2.y - p1.y));
100
+ if (denominator == 0) {
101
+ return false;
102
+ }
103
+
104
+ } catch {
105
+ console.log('!catch', p1, p2, 'p3:', p3, p4);
106
+ }
107
+
108
+ a = p1.y - p3.y;
109
+ b = p1.x - p3.x;
110
+ numerator1 = ((p4.x - p3.x) * a) - ((p4.y - p3.y) * b);
111
+ numerator2 = ((p2.x - p1.x) * a) - ((p2.y - p1.y) * b);
112
+
113
+ a = numerator1 / denominator;
114
+ b = numerator2 / denominator;
115
+
116
+ // if we cast these lines infinitely in both directions, they intersect here:
117
+ intersectionPoint = {
118
+ x: p1.x + (a * (p2.x - p1.x)),
119
+ y: p1.y + (a * (p2.y - p1.y))
120
+ };
121
+
122
+ // console.log('intersectionPoint', intersectionPoint, p1, p2);
123
+
124
+ let intersection = false;
125
+ // if line1 is a segment and line2 is infinite, they intersect if:
126
+ if ((a > 0 && a < 1) && (b > 0 && b < 1)) {
127
+ intersection = true;
128
+
129
+ }
130
+
131
+ if (exact && !intersection) {
132
+
133
+ return false;
134
+ }
135
+
136
+ // if line1 and line2 are segments, they intersect if both of the above are true
137
+
138
+ return intersectionPoint;
139
+ }
140
+
141
+ /**
142
+ * get distance between 2 points
143
+ * pythagorean theorem
144
+ */
145
+ function getDistance(p1, p2) {
146
+ return sqrt$1(
147
+ (p2.x - p1.x) * (p2.x - p1.x) + (p2.y - p1.y) * (p2.y - p1.y)
148
+ );
149
+ }
150
+
151
+ function getSquareDistance(p1, p2) {
152
+ return (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2
153
+ }
154
+
155
+ /**
156
+ * Linear interpolation (LERP) helper
157
+ */
158
+ function interpolate(p1, p2, t, getTangent = false) {
159
+
160
+ let pt = {
161
+ x: (p2.x - p1.x) * t + p1.x,
162
+ y: (p2.y - p1.y) * t + p1.y,
163
+ };
164
+
165
+ if (getTangent) {
166
+ pt.angle = getAngle(p1, p2);
167
+
168
+ // normalize negative angles
169
+ if (pt.angle < 0) pt.angle += PI$1 * 2;
170
+ }
171
+
172
+ return pt
173
+ }
174
+
175
+ function pointAtT(pts, t = 0.5, getTangent = false, getCpts = false) {
176
+
177
+ const getPointAtBezierT = (pts, t, getTangent = false) => {
178
+
179
+ let isCubic = pts.length === 4;
180
+ let p0 = pts[0];
181
+ let cp1 = pts[1];
182
+ let cp2 = isCubic ? pts[2] : pts[1];
183
+ let p = pts[pts.length - 1];
184
+ let pt = { x: 0, y: 0 };
185
+
186
+ if (getTangent || getCpts) {
187
+ let m0, m1, m2, m3, m4;
188
+ let shortCp1 = p0.x === cp1.x && p0.y === cp1.y;
189
+ let shortCp2 = p.x === cp2.x && p.y === cp2.y;
190
+
191
+ if (t === 0 && !shortCp1) {
192
+ pt.x = p0.x;
193
+ pt.y = p0.y;
194
+ pt.angle = getAngle(p0, cp1);
195
+ }
196
+
197
+ else if (t === 1 && !shortCp2) {
198
+ pt.x = p.x;
199
+ pt.y = p.y;
200
+ pt.angle = getAngle(cp2, p);
201
+ }
202
+
203
+ else {
204
+ // adjust if cps are on start or end point
205
+ if (shortCp1) t += 0.0000001;
206
+ if (shortCp2) t -= 0.0000001;
207
+
208
+ m0 = interpolate(p0, cp1, t);
209
+ if (isCubic) {
210
+ m1 = interpolate(cp1, cp2, t);
211
+ m2 = interpolate(cp2, p, t);
212
+ m3 = interpolate(m0, m1, t);
213
+ m4 = interpolate(m1, m2, t);
214
+ pt = interpolate(m3, m4, t);
215
+
216
+ // add angles
217
+ pt.angle = getAngle(m3, m4);
218
+
219
+ // add control points
220
+ if (getCpts) pt.cpts = [m1, m2, m3, m4];
221
+ } else {
222
+ m1 = interpolate(p0, cp1, t);
223
+ m2 = interpolate(cp1, p, t);
224
+ pt = interpolate(m1, m2, t);
225
+ pt.angle = getAngle(m1, m2);
226
+
227
+ // add control points
228
+ if (getCpts) pt.cpts = [m1, m2];
229
+ }
230
+ }
231
+
232
+ }
233
+ // take simplified calculations without tangent angles
234
+ else {
235
+ let t1 = 1 - t;
236
+
237
+ // cubic beziers
238
+ if (isCubic) {
239
+ pt = {
240
+ x:
241
+ t1 ** 3 * p0.x +
242
+ 3 * t1 ** 2 * t * cp1.x +
243
+ 3 * t1 * t ** 2 * cp2.x +
244
+ t ** 3 * p.x,
245
+ y:
246
+ t1 ** 3 * p0.y +
247
+ 3 * t1 ** 2 * t * cp1.y +
248
+ 3 * t1 * t ** 2 * cp2.y +
249
+ t ** 3 * p.y,
250
+ };
251
+
252
+ }
253
+ // quadratic beziers
254
+ else {
255
+ pt = {
256
+ x: t1 * t1 * p0.x + 2 * t1 * t * cp1.x + t ** 2 * p.x,
257
+ y: t1 * t1 * p0.y + 2 * t1 * t * cp1.y + t ** 2 * p.y,
258
+ };
259
+ }
260
+
261
+ }
262
+
263
+ return pt
264
+
265
+ };
266
+
267
+ let pt;
268
+ if (pts.length > 2) {
269
+ pt = getPointAtBezierT(pts, t, getTangent);
270
+ }
271
+
272
+ else {
273
+ pt = interpolate(pts[0], pts[1], t, getTangent);
274
+ }
275
+
276
+ // normalize negative angles
277
+ if (getTangent && pt.angle < 0) pt.angle += PI$1 * 2;
278
+
279
+ return pt
280
+ }
281
+
282
+ /**
283
+ * get vertices from path command final on-path points
284
+ */
285
+ function getPathDataVertices(pathData) {
286
+ let polyPoints = [];
287
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
288
+
289
+ pathData.forEach((com) => {
290
+ let { type, values } = com;
291
+ // get final on path point from last 2 values
292
+ if (values.length) {
293
+ let pt = values.length > 1 ? { x: values[values.length - 2], y: values[values.length - 1] }
294
+ : (type === 'V' ? { x: p0.x, y: values[0] } : { x: values[0], y: p0.y });
295
+ polyPoints.push(pt);
296
+ p0 = pt;
297
+ }
298
+ });
299
+ return polyPoints;
300
+ }
301
+
302
+ /**
303
+ * based on @cuixiping;
304
+ * https://stackoverflow.com/questions/9017100/calculate-center-of-svg-arc/12329083#12329083
305
+ */
306
+ function svgArcToCenterParam(x1, y1, rx, ry, xAxisRotation, largeArc, sweep, x2, y2) {
307
+
308
+ // helper for angle calculation
309
+ const getAngle = (cx, cy, x, y) => {
310
+ return atan2$1(y - cy, x - cx);
311
+ };
312
+
313
+ // make sure rx, ry are positive
314
+ rx = abs$1(rx);
315
+ ry = abs$1(ry);
316
+
317
+ // create data object
318
+ let arcData = {
319
+ cx: 0,
320
+ cy: 0,
321
+ // rx/ry values may be deceptive in arc commands
322
+ rx: rx,
323
+ ry: ry,
324
+ startAngle: 0,
325
+ endAngle: 0,
326
+ deltaAngle: 0,
327
+ clockwise: sweep,
328
+ // copy explicit arc properties
329
+ xAxisRotation,
330
+ largeArc,
331
+ sweep
332
+ };
333
+
334
+ if (rx == 0 || ry == 0) {
335
+ // invalid arguments
336
+ throw Error("rx and ry can not be 0");
337
+ }
338
+
339
+ let shortcut = true;
340
+
341
+ if (rx === ry && shortcut) {
342
+
343
+ // test semicircles
344
+ let diffX = Math.abs(x2 - x1);
345
+ let diffY = Math.abs(y2 - y1);
346
+ let r = diffX;
347
+
348
+ let xMin = Math.min(x1, x2),
349
+ yMin = Math.min(y1, y2),
350
+ PIHalf = Math.PI * 0.5;
351
+
352
+ // semi circles
353
+ if (diffX === 0 && diffY || diffY === 0 && diffX) {
354
+
355
+ r = diffX === 0 && diffY ? diffY / 2 : diffX / 2;
356
+ arcData.rx = r;
357
+ arcData.ry = r;
358
+
359
+ // verical
360
+ if (diffX === 0 && diffY) {
361
+ arcData.cx = x1;
362
+ arcData.cy = yMin + diffY / 2;
363
+ arcData.startAngle = y1 > y2 ? PIHalf : -PIHalf;
364
+ arcData.endAngle = y1 > y2 ? -PIHalf : PIHalf;
365
+ arcData.deltaAngle = sweep ? Math.PI : -Math.PI;
366
+
367
+ }
368
+ // horizontal
369
+ else if (diffY === 0 && diffX) {
370
+ arcData.cx = xMin + diffX / 2;
371
+ arcData.cy = y1;
372
+ arcData.startAngle = x1 > x2 ? Math.PI : 0;
373
+ arcData.endAngle = x1 > x2 ? -Math.PI : Math.PI;
374
+ arcData.deltaAngle = sweep ? Math.PI : -Math.PI;
375
+ }
376
+
377
+ return arcData;
378
+ }
379
+ }
380
+
381
+ /**
382
+ * if rx===ry x-axis rotation is ignored
383
+ * otherwise convert degrees to radians
384
+ */
385
+ let phi = rx === ry ? 0 : (xAxisRotation * PI$1) / 180;
386
+ let cx, cy;
387
+
388
+ let s_phi = !phi ? 0 : sin$1(phi);
389
+ let c_phi = !phi ? 1 : cos$1(phi);
390
+
391
+ let hd_x = (x1 - x2) / 2;
392
+ let hd_y = (y1 - y2) / 2;
393
+ let hs_x = (x1 + x2) / 2;
394
+ let hs_y = (y1 + y2) / 2;
395
+
396
+ // F6.5.1
397
+ let x1_ = !phi ? hd_x : c_phi * hd_x + s_phi * hd_y;
398
+ let y1_ = !phi ? hd_y : c_phi * hd_y - s_phi * hd_x;
399
+
400
+ // F.6.6 Correction of out-of-range radii
401
+ // Step 3: Ensure radii are large enough
402
+ let lambda = (x1_ * x1_) / (rx * rx) + (y1_ * y1_) / (ry * ry);
403
+ if (lambda > 1) {
404
+ rx = rx * sqrt$1(lambda);
405
+ ry = ry * sqrt$1(lambda);
406
+
407
+ // save real rx/ry
408
+ arcData.rx = rx;
409
+ arcData.ry = ry;
410
+ }
411
+
412
+ let rxry = rx * ry;
413
+ let rxy1_ = rx * y1_;
414
+ let ryx1_ = ry * x1_;
415
+ let sum_of_sq = rxy1_ ** 2 + ryx1_ ** 2; // sum of square
416
+ if (!sum_of_sq) {
417
+
418
+ throw Error("start point can not be same as end point");
419
+ }
420
+ let coe = sqrt$1(abs$1((rxry * rxry - sum_of_sq) / sum_of_sq));
421
+ if (largeArc == sweep) {
422
+ coe = -coe;
423
+ }
424
+
425
+ // F6.5.2
426
+ let cx_ = (coe * rxy1_) / ry;
427
+ let cy_ = (-coe * ryx1_) / rx;
428
+
429
+ /** F6.5.3
430
+ * center point of ellipse
431
+ */
432
+ cx = !phi ? hs_x + cx_ : c_phi * cx_ - s_phi * cy_ + hs_x;
433
+ cy = !phi ? hs_y + cy_ : s_phi * cx_ + c_phi * cy_ + hs_y;
434
+ arcData.cy = cy;
435
+ arcData.cx = cx;
436
+
437
+ /** F6.5.5
438
+ * calculate angles between center point and
439
+ * commands starting and final on path point
440
+ */
441
+ let startAngle = getAngle(cx, cy, x1, y1);
442
+ let endAngle = getAngle(cx, cy, x2, y2);
443
+
444
+ // adjust end angle
445
+ if (!sweep && endAngle > startAngle) {
446
+
447
+ endAngle -= Math.PI * 2;
448
+ }
449
+
450
+ if (sweep && startAngle > endAngle) {
451
+
452
+ endAngle = endAngle <= 0 ? endAngle + Math.PI * 2 : endAngle;
453
+ }
454
+
455
+ let deltaAngle = endAngle - startAngle;
456
+ arcData.startAngle = startAngle;
457
+ arcData.endAngle = endAngle;
458
+ arcData.deltaAngle = deltaAngle;
459
+
460
+ return arcData;
461
+ }
462
+
463
+ function getPointOnEllipse(cx, cy, rx, ry, angle, ellipseRotation = 0, parametricAngle = true, degrees = false) {
464
+
465
+ // Convert degrees to radians
466
+ angle = degrees ? (angle * PI$1) / 180 : angle;
467
+ ellipseRotation = degrees ? (ellipseRotation * PI$1) / 180 : ellipseRotation;
468
+ // reset rotation for circles or 360 degree
469
+ ellipseRotation = rx !== ry ? (ellipseRotation !== PI$1 * 2 ? ellipseRotation : 0) : 0;
470
+
471
+ // is ellipse
472
+ if (parametricAngle && rx !== ry) {
473
+ // adjust angle for ellipse rotation
474
+ angle = ellipseRotation ? angle - ellipseRotation : angle;
475
+ // Get the parametric angle for the ellipse
476
+ let angleParametric = atan$1(tan$1(angle) * (rx / ry));
477
+ // Ensure the parametric angle is in the correct quadrant
478
+ angle = cos$1(angle) < 0 ? angleParametric + PI$1 : angleParametric;
479
+ }
480
+
481
+ // Calculate the point on the ellipse without rotation
482
+ let x = cx + rx * cos$1(angle),
483
+ y = cy + ry * sin$1(angle);
484
+ let pt = {
485
+ x: x,
486
+ y: y
487
+ };
488
+
489
+ if (ellipseRotation) {
490
+ pt.x = cx + (x - cx) * cos$1(ellipseRotation) - (y - cy) * sin$1(ellipseRotation);
491
+ pt.y = cy + (x - cx) * sin$1(ellipseRotation) + (y - cy) * cos$1(ellipseRotation);
492
+ }
493
+ return pt
494
+ }
495
+
496
+ function bezierhasExtreme(p0, cpts = [], angleThreshold = 0.05) {
497
+ let isCubic = cpts.length === 3 ? true : false;
498
+ let cp1 = cpts[0] || null;
499
+ let cp2 = isCubic ? cpts[1] : null;
500
+ let p = isCubic ? cpts[2] : cpts[1];
501
+ let PIquarter = Math.PI * 0.5;
502
+
503
+ let extCp1 = false,
504
+ extCp2 = false;
505
+
506
+ let ang1 = cp1 ? getAngle(p, cp1, true) : null;
507
+
508
+ extCp1 = Math.abs((ang1 % PIquarter)) < angleThreshold || Math.abs((ang1 % PIquarter) - PIquarter) < angleThreshold;
509
+
510
+ if (isCubic) {
511
+ let ang2 = cp2 ? getAngle(cp2, p, true) : 0;
512
+ extCp2 = Math.abs((ang2 % PIquarter)) <= angleThreshold ||
513
+ Math.abs((ang2 % PIquarter) - PIquarter) <= angleThreshold;
514
+ }
515
+ return (extCp1 || extCp2)
516
+ }
517
+
518
+ function getBezierExtremeT(pts) {
519
+ let tArr = pts.length === 4 ? cubicBezierExtremeT(pts[0], pts[1], pts[2], pts[3]) : quadraticBezierExtremeT(pts[0], pts[1], pts[2]);
520
+ return tArr;
521
+ }
522
+
523
+ // cubic bezier.
524
+ function cubicBezierExtremeT(p0, cp1, cp2, p) {
525
+ let [x0, y0, x1, y1, x2, y2, x3, y3] = [p0.x, p0.y, cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
526
+
527
+ /**
528
+ * if control points are within
529
+ * bounding box of start and end point
530
+ * we cant't have extremes
531
+ */
532
+ let top = Math.min(p0.y, p.y);
533
+ let left = Math.min(p0.x, p.x);
534
+ let right = Math.max(p0.x, p.x);
535
+ let bottom = Math.max(p0.y, p.y);
536
+
537
+ if (
538
+ cp1.y >= top && cp1.y <= bottom &&
539
+ cp2.y >= top && cp2.y <= bottom &&
540
+ cp1.x >= left && cp1.x <= right &&
541
+ cp2.x >= left && cp2.x <= right
542
+ ) {
543
+ return []
544
+ }
545
+
546
+ let tArr = [],
547
+ a, b, c, t, t1, t2, b2ac, sqrt_b2ac;
548
+ for (let i = 0; i < 2; ++i) {
549
+ if (i == 0) {
550
+ b = 6 * x0 - 12 * x1 + 6 * x2;
551
+ a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
552
+ c = 3 * x1 - 3 * x0;
553
+ } else {
554
+ b = 6 * y0 - 12 * y1 + 6 * y2;
555
+ a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
556
+ c = 3 * y1 - 3 * y0;
557
+ }
558
+ if (Math.abs(a) < 1e-12) {
559
+ if (Math.abs(b) < 1e-12) {
560
+ continue;
561
+ }
562
+ t = -c / b;
563
+ if (0 < t && t < 1) {
564
+ tArr.push(t);
565
+ }
566
+ continue;
567
+ }
568
+ b2ac = b * b - 4 * c * a;
569
+ if (b2ac < 0) {
570
+ if (Math.abs(b2ac) < 1e-12) {
571
+ t = -b / (2 * a);
572
+ if (0 < t && t < 1) {
573
+ tArr.push(t);
574
+ }
575
+ }
576
+ continue;
577
+ }
578
+ sqrt_b2ac = Math.sqrt(b2ac);
579
+ t1 = (-b + sqrt_b2ac) / (2 * a);
580
+ if (0 < t1 && t1 < 1) {
581
+ tArr.push(t1);
582
+ }
583
+ t2 = (-b - sqrt_b2ac) / (2 * a);
584
+ if (0 < t2 && t2 < 1) {
585
+ tArr.push(t2);
586
+ }
587
+ }
588
+
589
+ let j = tArr.length;
590
+ while (j--) {
591
+ t = tArr[j];
592
+ }
593
+ return tArr;
594
+ }
595
+
596
+ function quadraticBezierExtremeT(p0, cp1, p) {
597
+ /**
598
+ * if control points are within
599
+ * bounding box of start and end point
600
+ * we cant't have extremes
601
+ */
602
+ let top = Math.min(p0.y, p.y);
603
+ let left = Math.min(p0.x, p.x);
604
+ let right = Math.max(p0.x, p.x);
605
+ let bottom = Math.max(p0.y, p.y);
606
+ let a, b, t;
607
+
608
+ if (
609
+ cp1.y >= top && cp1.y <= bottom &&
610
+ cp1.x >= left && cp1.x <= right
611
+ ) {
612
+ return []
613
+ }
614
+
615
+ let [x0, y0, x1, y1, x2, y2] = [p0.x, p0.y, cp1.x, cp1.y, p.x, p.y];
616
+ let extemeT = [];
617
+
618
+ for (let i = 0; i < 2; ++i) {
619
+ a = i == 0 ? x0 - 2 * x1 + x2 : y0 - 2 * y1 + y2;
620
+ b = i == 0 ? -2 * x0 + 2 * x1 : -2 * y0 + 2 * y1;
621
+ if (Math.abs(a) > 1e-12) {
622
+ t = -b / (2 * a);
623
+ if (t > 0 && t < 1) {
624
+ extemeT.push(t);
625
+ }
626
+ }
627
+ }
628
+ return extemeT
629
+ }
630
+
631
+ function commandIsFlat(points, tolerance = 0.025) {
632
+
633
+ let p0 = points[0];
634
+ let p = points[points.length - 1];
635
+
636
+ let xArr = points.map(pt => { return pt.x });
637
+ let yArr = points.map(pt => { return pt.y });
638
+
639
+ let xMin = Math.min(...xArr);
640
+ let xMax = Math.max(...xArr);
641
+ let yMin = Math.min(...yArr);
642
+ let yMax = Math.max(...yArr);
643
+ let w = xMax - xMin;
644
+ let h = yMax - yMin;
645
+
646
+ if (points.length < 3 || (w === 0 || h === 0)) {
647
+ return { area: 0, flat: true, thresh: 0.0001, ratio: 0 };
648
+ }
649
+
650
+ let squareDist = getSquareDistance(p0, p);
651
+ let squareDist1 = getSquareDistance(p0, points[0]);
652
+ let squareDist2 = points.length > 3 ? getSquareDistance(p, points[1]) : squareDist1;
653
+ let squareDistAvg = (squareDist1 + squareDist2) / 2;
654
+
655
+ tolerance = 0.5;
656
+ let thresh = (w + h) * 0.5 * tolerance;
657
+
658
+ let area = 0;
659
+ for (let i = 0, l = points.length; i < l; i++) {
660
+ let addX = points[i].x;
661
+ let addY = points[i === points.length - 1 ? 0 : i + 1].y;
662
+ let subX = points[i === points.length - 1 ? 0 : i + 1].x;
663
+ let subY = points[i].y;
664
+ area += addX * addY * 0.5 - subX * subY * 0.5;
665
+ }
666
+
667
+ area = +Math.abs(area).toFixed(9);
668
+ let areaThresh = 1000;
669
+
670
+ let ratio = area / (squareDistAvg);
671
+
672
+ let isFlat = area === 0 ? true : area < squareDistAvg / areaThresh;
673
+
674
+ return { area: area, flat: isFlat, thresh: thresh, ratio: ratio, squareDist: squareDist, areaThresh: squareDist / areaThresh };
675
+ }
676
+
677
+ function checkBezierFlatness(p0, cpts, p) {
678
+
679
+ let isFlat = false;
680
+
681
+ let isCubic = cpts.length===2;
682
+
683
+ let cp1 = cpts[0];
684
+ let cp2 = isCubic ? cpts[1] : cp1;
685
+
686
+ if(p0.x===cp1.x && p0.y===cp1.y && p.x===cp2.x && p.y===cp2.y) return true;
687
+
688
+ let dx1 = cp1.x - p0.x;
689
+ let dy1 = cp1.y - p0.y;
690
+
691
+ let dx2 = p.x - cp2.x;
692
+ let dy2 = p.y - cp2.y;
693
+
694
+ let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
695
+
696
+ if(!cross1) return true
697
+
698
+ let dx0 = p.x - p0.x;
699
+ let dy0 = p.y - p0.y;
700
+ let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
701
+
702
+ if(!cross0) return true
703
+
704
+ let rat = (cross0/cross1);
705
+
706
+ if (rat<1.1 ) {
707
+
708
+ isFlat = true;
709
+ }
710
+
711
+ return isFlat;
712
+
713
+ }
714
+
715
+ /**
716
+ * sloppy distance calculation
717
+ * based on x/y differences
718
+ */
719
+ function getDistAv(pt1, pt2) {
720
+
721
+ let diffX = Math.abs(pt2.x - pt1.x);
722
+ let diffY = Math.abs(pt2.y - pt1.y);
723
+ let diff = (diffX + diffY) / 2;
724
+
725
+ /*
726
+ let diffX = pt2.x - pt1.x;
727
+ let diffY = pt2.y - pt1.y;
728
+ let diff = Math.abs(diffX + diffY) / 2;
729
+ */
730
+
731
+ return diff;
732
+ }
733
+
734
+ /**
735
+ * split compound paths into
736
+ * sub path data array
737
+ */
738
+ function splitSubpaths(pathData) {
739
+
740
+ let subPathArr = [];
741
+
742
+
743
+ try{
744
+ let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
745
+
746
+ }catch{
747
+ console.log('catch', pathData);
748
+ }
749
+
750
+ let subPathIndices = pathData.map((com, i) => (com.type.toLowerCase() === 'm' ? i : -1)).filter(i => i !== -1);
751
+
752
+ // no compound path
753
+ if (subPathIndices.length === 1) {
754
+ return [pathData]
755
+ }
756
+ subPathIndices.forEach((index, i) => {
757
+ subPathArr.push(pathData.slice(index, subPathIndices[i + 1]));
758
+ });
759
+
760
+ return subPathArr;
761
+ }
762
+
763
+ /**
764
+ * calculate split command points
765
+ * for single t value
766
+ */
767
+ function splitCommand(points, t) {
768
+
769
+ let seg1 = [];
770
+ let seg2 = [];
771
+
772
+ let p0 = points[0];
773
+ let cp1 = points[1];
774
+ let cp2 = points[points.length - 2];
775
+ let p = points[points.length - 1];
776
+ let m0,m1,m2,m3,m4, p2;
777
+
778
+ // cubic
779
+ if (points.length === 4) {
780
+ m0 = pointAtT([p0, cp1], t);
781
+ m1 = pointAtT([cp1, cp2], t);
782
+ m2 = pointAtT([cp2, p], t);
783
+ m3 = pointAtT([m0, m1], t);
784
+ m4 = pointAtT([m1, m2], t);
785
+
786
+ // split end point
787
+ p2 = pointAtT([m3, m4], t);
788
+
789
+ // 1. segment
790
+ seg1.push(
791
+ { x: p0.x, y: p0.y },
792
+ { x: m0.x, y: m0.y },
793
+ { x: m3.x, y: m3.y },
794
+ { x: p2.x, y: p2.y },
795
+ );
796
+ // 2. segment
797
+ seg2.push(
798
+ { x: p2.x, y: p2.y },
799
+ { x: m4.x, y: m4.y },
800
+ { x: m2.x, y: m2.y },
801
+ { x: p.x, y: p.y },
802
+ );
803
+ }
804
+
805
+ // quadratic
806
+ else if (points.length === 3) {
807
+ m1 = pointAtT([p0, cp1], t);
808
+ m2 = pointAtT([cp1, p], t);
809
+ p2 = pointAtT([m1, m2], t);
810
+
811
+ // 1. segment
812
+ seg1.push(
813
+ { x: p0.x, y: p0.y },
814
+ { x: m1.x, y: m1.y },
815
+ { x: p2.x, y: p2.y },
816
+ );
817
+
818
+ // 1. segment
819
+ seg2.push(
820
+ { x: p2.x, y: p2.y },
821
+ { x: m2.x, y: m2.y },
822
+ { x: p.x, y: p.y },
823
+ );
824
+ }
825
+
826
+ // lineto
827
+ else if (points.length === 2) {
828
+ m1 = pointAtT([p0, p], t);
829
+
830
+ // 1. segment
831
+ seg1.push(
832
+ { x: p0.x, y: p0.y },
833
+ { x: m1.x, y: m1.y },
834
+ );
835
+
836
+ // 1. segment
837
+ seg2.push(
838
+ { x: m1.x, y: m1.y },
839
+ { x: p.x, y: p.y },
840
+ );
841
+ }
842
+ return [seg1, seg2];
843
+ }
844
+
845
+ /**
846
+ * calculate command extremes
847
+ */
848
+
849
+ function addExtemesToCommand(p0, values, tMin=0, tMax=1) {
850
+
851
+ let pathDataNew = [];
852
+
853
+ let type = values.length === 6 ? 'C' : 'Q';
854
+ let cp1 = { x: values[0], y: values[1] };
855
+ let cp2 = type === 'C' ? { x: values[2], y: values[3] } : cp1;
856
+ let p = { x: values[4], y: values[5] };
857
+
858
+ // get inner bbox
859
+ let xMax = Math.max(p.x, p0.x);
860
+ let xMin = Math.min(p.x, p0.x);
861
+ let yMax = Math.max(p.y, p0.y);
862
+ let yMin = Math.min(p.y, p0.y);
863
+
864
+ let extremeCount = 0;
865
+
866
+ if (
867
+ cp1.x < xMin ||
868
+ cp1.x > xMax ||
869
+ cp1.y < yMin ||
870
+ cp1.y > yMax ||
871
+ cp2.x < xMin ||
872
+ cp2.x > xMax ||
873
+ cp2.y < yMin ||
874
+ cp2.y > yMax
875
+
876
+ ) {
877
+ let pts = type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p];
878
+ let tArr = getBezierExtremeT(pts).sort();
879
+
880
+ // avoid t split too close to start or end
881
+ tArr = tArr.filter(t=>t>tMin && t<tMax);
882
+
883
+ if(tArr.length){
884
+ let commandsSplit = splitCommandAtTValues(p0, values, tArr);
885
+ pathDataNew.push(...commandsSplit);
886
+ extremeCount += commandsSplit.length;
887
+ }else {
888
+
889
+ pathDataNew.push({ type: type, values: values });
890
+ }
891
+
892
+ }
893
+ // no extremes
894
+ else {
895
+ pathDataNew.push({ type: type, values: values });
896
+ }
897
+
898
+ return { pathData: pathDataNew, count: extremeCount };
899
+
900
+ }
901
+
902
+ function addExtremePoints(pathData, tMin=0, tMax=1) {
903
+ let pathDataNew = [pathData[0]];
904
+ // previous on path point
905
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
906
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
907
+ let len = pathData.length;
908
+
909
+ for (let c = 1; len && c < len; c++) {
910
+ let com = pathData[c];
911
+
912
+ let { type, values } = com;
913
+ let valsL = values.slice(-2);
914
+ ({ x: valsL[0], y: valsL[1] });
915
+
916
+ if (type !== 'C' && type !== 'Q') {
917
+ pathDataNew.push(com);
918
+ }
919
+
920
+ else {
921
+ // add extremes
922
+ if (type === 'C' || type === 'Q') {
923
+ let comExt = addExtemesToCommand(p0, values, tMin, tMax).pathData;
924
+
925
+ pathDataNew.push(...comExt );
926
+ }
927
+ }
928
+
929
+ p0 = { x: valsL[0], y: valsL[1] };
930
+
931
+ if (type.toLowerCase() === "z") {
932
+ p0 = M;
933
+ } else if (type === "M") {
934
+ M = { x: valsL[0], y: valsL[1] };
935
+ }
936
+ }
937
+
938
+ return pathDataNew;
939
+ }
940
+
941
+ /**
942
+ * split commands multiple times
943
+ * based on command points
944
+ * and t array
945
+ */
946
+ function splitCommandAtTValues(p0, values, tArr, returnCommand = true) {
947
+ let segmentPoints = [];
948
+
949
+ if (!tArr.length) {
950
+ return false
951
+ }
952
+
953
+ let valuesL = values.length;
954
+ let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
955
+ let cp1, cp2, points;
956
+
957
+ if (values.length === 2) {
958
+ points = [p0, p];
959
+ }
960
+ else if (values.length === 4) {
961
+ cp1 = { x: values[0], y: values[1] };
962
+ points = [p0, cp1, p];
963
+ }
964
+ else if (values.length === 6) {
965
+ cp1 = { x: values[0], y: values[1] };
966
+ cp2 = { x: values[2], y: values[3] };
967
+ points = [p0, cp1, cp2, p];
968
+ }
969
+
970
+ if (tArr.length) {
971
+ // single t
972
+ if (tArr.length === 1) {
973
+ let segs = splitCommand(points, tArr[0]);
974
+ let points1 = segs[0];
975
+ let points2 = segs[1];
976
+ segmentPoints.push(points1, points2);
977
+
978
+ } else {
979
+
980
+ // 1st segment
981
+ let t1 = tArr[0];
982
+ let seg0 = splitCommand(points, t1);
983
+ let points0 = seg0[0];
984
+ segmentPoints.push(points0);
985
+ points = seg0[1];
986
+
987
+ for (let i = 1; i < tArr.length; i++) {
988
+ t1 = tArr[i - 1];
989
+ let t2 = tArr[i];
990
+
991
+ // new t value for 2nd segment
992
+ let t2_1 = (t2 - t1) / (1 - t1);
993
+ let segs2 = splitCommand(points, t2_1);
994
+ segmentPoints.push(segs2[0]);
995
+
996
+ if (i === tArr.length - 1) {
997
+ segmentPoints.push(segs2[segs2.length - 1]);
998
+ }
999
+ // take 2nd segment for next splitting
1000
+ points = segs2[1];
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ if (returnCommand) {
1006
+
1007
+ let pathData = [];
1008
+ let com, values;
1009
+
1010
+ segmentPoints.forEach(seg => {
1011
+ com = { type: '', values: [] };
1012
+ seg.shift();
1013
+ values = seg.map(val => { return Object.values(val) }).flat();
1014
+ com.values = values;
1015
+
1016
+ // cubic
1017
+ if (seg.length === 3) {
1018
+ com.type = 'C';
1019
+ }
1020
+
1021
+ // quadratic
1022
+ else if (seg.length === 2) {
1023
+ com.type = 'Q';
1024
+ }
1025
+
1026
+ // lineto
1027
+ else if (seg.length === 1) {
1028
+ com.type = 'L';
1029
+ }
1030
+ pathData.push(com);
1031
+ });
1032
+ return pathData;
1033
+ }
1034
+
1035
+ return segmentPoints;
1036
+ }
1037
+
1038
+ /**
1039
+ * calculate polygon bbox
1040
+ */
1041
+ function getPolyBBox(vertices, decimals = -1) {
1042
+ let xArr = vertices.map(pt => pt.x);
1043
+ let yArr = vertices.map(pt => pt.y);
1044
+ let left = Math.min(...xArr);
1045
+ let right = Math.max(...xArr);
1046
+ let top = Math.min(...yArr);
1047
+ let bottom = Math.max(...yArr);
1048
+ let bb = {
1049
+ x: left,
1050
+ left: left,
1051
+ right: right,
1052
+ y: top,
1053
+ top: top,
1054
+ bottom: bottom,
1055
+ width: right - left,
1056
+ height: bottom - top
1057
+ };
1058
+
1059
+ // round
1060
+
1061
+ if (decimals > -1) {
1062
+ for (let prop in bb) {
1063
+ bb[prop] = +bb[prop].toFixed(decimals);
1064
+ }
1065
+ }
1066
+
1067
+ return bb;
1068
+ }
1069
+
1070
+ function getSubPathBBoxes(subPaths) {
1071
+ let bboxArr = [];
1072
+ subPaths.forEach((pathData) => {
1073
+
1074
+ let bb = getPathDataBBox_sloppy(pathData);
1075
+ bboxArr.push(bb);
1076
+ });
1077
+
1078
+ return bboxArr;
1079
+ }
1080
+
1081
+ function checkBBoxIntersections(bb, bb1) {
1082
+ let [x, y, width, height, right, bottom] = [
1083
+ bb.x,
1084
+ bb.y,
1085
+ bb.width,
1086
+ bb.height,
1087
+ bb.x + bb.width,
1088
+ bb.y + bb.height
1089
+ ];
1090
+ let [x1, y1, width1, height1, right1, bottom1] = [
1091
+ bb1.x,
1092
+ bb1.y,
1093
+ bb1.width,
1094
+ bb1.height,
1095
+ bb1.x + bb1.width,
1096
+ bb1.y + bb1.height
1097
+ ];
1098
+ let intersects = false;
1099
+ if (width * height != width1 * height1) {
1100
+ if (width * height > width1 * height1) {
1101
+ if (x < x1 && right > right1 && y < y1 && bottom > bottom1) {
1102
+ intersects = true;
1103
+ }
1104
+ }
1105
+ }
1106
+ return intersects;
1107
+ }
1108
+
1109
+ /**
1110
+ * sloppy path bbox aaproximation
1111
+ */
1112
+
1113
+ function getPathDataBBox_sloppy(pathData) {
1114
+ let pts = getPathDataPoly(pathData);
1115
+ let bb = getPolyBBox(pts);
1116
+ return bb;
1117
+ }
1118
+
1119
+ /**
1120
+ * get path data poly
1121
+ * including command points
1122
+ * handy for faster/sloppy bbox approximations
1123
+ */
1124
+
1125
+ function getPathDataPoly(pathData) {
1126
+
1127
+ let poly = [];
1128
+ for (let i = 0; i < pathData.length; i++) {
1129
+ let com = pathData[i];
1130
+ let prev = i > 0 ? pathData[i - 1] : pathData[i];
1131
+ let { type, values } = com;
1132
+ let p0 = { x: prev.values[prev.values.length - 2], y: prev.values[prev.values.length - 1] };
1133
+ let p = values.length ? { x: values[values.length - 2], y: values[values.length - 1] } : '';
1134
+ let cp1 = values.length ? { x: values[0], y: values[1] } : '';
1135
+
1136
+ switch (type) {
1137
+
1138
+ // convert to cubic to get polygon
1139
+ case 'A':
1140
+
1141
+ if (typeof arcToBezier !== 'function') {
1142
+
1143
+ // get real radii
1144
+ let rx = getDistance(p0, p) / 2;
1145
+ let ptMid = interpolate(p0, p, 0.5);
1146
+
1147
+ let pt1 = getPointOnEllipse(ptMid.x, ptMid.y, rx, rx, 0);
1148
+ let pt2 = getPointOnEllipse(ptMid.x, ptMid.y, rx, rx, Math.PI);
1149
+ poly.push(pt1, pt2, p);
1150
+
1151
+ break;
1152
+ }
1153
+ let cubic = arcToBezier(p0, values);
1154
+ cubic.forEach(com => {
1155
+ let vals = com.values;
1156
+ let cp1 = { x: vals[0], y: vals[1] };
1157
+ let cp2 = { x: vals[2], y: vals[3] };
1158
+ let p = { x: vals[4], y: vals[5] };
1159
+ poly.push(cp1, cp2, p);
1160
+ });
1161
+ break;
1162
+
1163
+ case 'C':
1164
+ let cp2 = { x: values[2], y: values[3] };
1165
+ poly.push(cp1, cp2);
1166
+ break;
1167
+ case 'Q':
1168
+ poly.push(cp1);
1169
+ break;
1170
+ }
1171
+
1172
+ // M and L commands
1173
+ if (type.toLowerCase() !== 'z') {
1174
+ poly.push(p);
1175
+ }
1176
+ }
1177
+
1178
+ return poly;
1179
+ }
1180
+
1181
+ /**
1182
+ * get pathdata area
1183
+ */
1184
+
1185
+ function getPathArea(pathData, decimals = 9) {
1186
+ let totalArea = 0;
1187
+ let polyPoints = [];
1188
+
1189
+ let subPathsData = splitSubpaths(pathData);
1190
+ let isCompoundPath = subPathsData.length > 1 ? true : false;
1191
+ let counterShapes = [];
1192
+
1193
+ // check intersections for compund paths
1194
+ if (isCompoundPath) {
1195
+ let bboxArr = getSubPathBBoxes(subPathsData);
1196
+
1197
+ bboxArr.forEach(function (bb, b) {
1198
+
1199
+ for (let i = 0; i < bboxArr.length; i++) {
1200
+ let bb2 = bboxArr[i];
1201
+ if (bb != bb2) {
1202
+ let intersects = checkBBoxIntersections(bb, bb2);
1203
+ if (intersects) {
1204
+ counterShapes.push(i);
1205
+ }
1206
+ }
1207
+ }
1208
+ });
1209
+ }
1210
+
1211
+ subPathsData.forEach((pathData, d) => {
1212
+
1213
+ polyPoints = [];
1214
+ let comArea = 0;
1215
+ let pathArea = 0;
1216
+ let multiplier = 1;
1217
+ let pts = [];
1218
+
1219
+ pathData.forEach(function (com, i) {
1220
+ let [type, values] = [com.type, com.values];
1221
+ let valuesL = values.length;
1222
+
1223
+ if (values.length) {
1224
+ let prevC = i > 0 ? pathData[i - 1] : pathData[0];
1225
+ let prevCVals = prevC.values;
1226
+ let prevCValsL = prevCVals.length;
1227
+ let p0 = { x: prevCVals[prevCValsL - 2], y: prevCVals[prevCValsL - 1] };
1228
+ let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
1229
+
1230
+ // C commands
1231
+ if (type === 'C' || type === 'Q') {
1232
+ let cp1 = { x: values[0], y: values[1] };
1233
+ pts = type === 'C' ? [p0, cp1, { x: values[2], y: values[3] }, p] : [p0, cp1, p];
1234
+ let areaBez = Math.abs(getBezierArea(pts));
1235
+ comArea += areaBez;
1236
+
1237
+ polyPoints.push(p0, p);
1238
+ }
1239
+
1240
+ // A commands
1241
+ else if (type === 'A') {
1242
+ let arcData = svgArcToCenterParam(p0.x, p0.y, com.values[0], com.values[1], com.values[2], com.values[3], com.values[4], p.x, p.y);
1243
+ let { cx, cy, rx, ry, startAngle, endAngle, deltaAngle } = arcData;
1244
+
1245
+ let arcArea = Math.abs(getEllipseArea(rx, ry, startAngle, endAngle));
1246
+
1247
+ // subtract remaining polygon between p0, center and p
1248
+ let polyArea = Math.abs(getPolygonArea([p0, { x: cx, y: cy }, p]));
1249
+ arcArea -= polyArea;
1250
+
1251
+ polyPoints.push(p0, p);
1252
+ comArea += arcArea;
1253
+ }
1254
+
1255
+ // L commands
1256
+ else {
1257
+ polyPoints.push(p0, p);
1258
+ }
1259
+ }
1260
+ });
1261
+
1262
+ let areaPoly = getPolygonArea(polyPoints);
1263
+
1264
+ if (counterShapes.indexOf(d) !== -1) {
1265
+ multiplier = -1;
1266
+ }
1267
+
1268
+ if (
1269
+ (areaPoly < 0 && comArea < 0)
1270
+ ) {
1271
+ // are negative
1272
+ pathArea = (Math.abs(comArea) - Math.abs(areaPoly)) * multiplier;
1273
+
1274
+ } else {
1275
+ pathArea = (Math.abs(comArea) + Math.abs(areaPoly)) * multiplier;
1276
+ }
1277
+
1278
+ totalArea += pathArea;
1279
+ });
1280
+
1281
+ return totalArea;
1282
+ }
1283
+
1284
+ /**
1285
+ * get ellipse area
1286
+ * skips to circle calculation if rx===ry
1287
+ */
1288
+
1289
+ function getEllipseArea(rx, ry, startAngle, endAngle) {
1290
+ const totalArea = Math.PI * rx * ry;
1291
+ let angleDiff = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
1292
+ // If circle, use simple circular formula
1293
+ if (rx === ry) return totalArea * (angleDiff / (2 * Math.PI));
1294
+
1295
+ // Convert absolute angles to parametric angles
1296
+ const absoluteToParametric = (phi)=>{
1297
+ return Math.atan2(rx * Math.sin(phi), ry * Math.cos(phi));
1298
+ };
1299
+ startAngle = absoluteToParametric(startAngle);
1300
+ endAngle = absoluteToParametric(endAngle);
1301
+ angleDiff = (endAngle - startAngle + 2 * Math.PI) % (2 * Math.PI);
1302
+ return totalArea * (angleDiff / (2 * Math.PI));
1303
+ }
1304
+
1305
+ /**
1306
+ * compare areas
1307
+ * for thresholds
1308
+ * returns a percentage value
1309
+ */
1310
+
1311
+ function getRelativeAreaDiff(area0, area1) {
1312
+ let diff = Math.abs(area0 - area1);
1313
+ return Math.abs(100 - (100 / area0 * (area0 + diff)))
1314
+ }
1315
+
1316
+ /**
1317
+ * get bezier area
1318
+ */
1319
+ function getBezierArea(pts, absolute=false) {
1320
+
1321
+ let [p0, cp1, cp2, p] = [pts[0], pts[1], pts[2], pts[pts.length - 1]];
1322
+ let area;
1323
+
1324
+ if (pts.length < 3) return 0;
1325
+
1326
+ // quadratic beziers
1327
+ if (pts.length === 3) {
1328
+ cp1 = {
1329
+ x: pts[0].x * 1 / 3 + pts[1].x * 2 / 3,
1330
+ y: pts[0].y * 1 / 3 + pts[1].y * 2 / 3
1331
+ };
1332
+
1333
+ cp2 = {
1334
+ x: pts[2].x * 1 / 3 + pts[1].x * 2 / 3,
1335
+ y: pts[2].y * 1 / 3 + pts[1].y * 2 / 3
1336
+ };
1337
+ }
1338
+
1339
+ area = ((p0.x * (-2 * cp1.y - cp2.y + 3 * p.y) +
1340
+ cp1.x * (2 * p0.y - cp2.y - p.y) +
1341
+ cp2.x * (p0.y + cp1.y - 2 * p.y) +
1342
+ p.x * (-3 * p0.y + cp1.y + 2 * cp2.y)) *
1343
+ 3) / 20;
1344
+
1345
+ return absolute ? Math.abs(area) : area;
1346
+ }
1347
+
1348
+ function getPolygonArea(points, absolute=false) {
1349
+ let area = 0;
1350
+ for (let i = 0, len = points.length; len && i < len; i++) {
1351
+ let addX = points[i].x;
1352
+ let addY = points[i === points.length - 1 ? 0 : i + 1].y;
1353
+ let subX = points[i === points.length - 1 ? 0 : i + 1].x;
1354
+ let subY = points[i].y;
1355
+ area += addX * addY * 0.5 - subX * subY * 0.5;
1356
+ }
1357
+ if(absolute) area=Math.abs(area);
1358
+ return area;
1359
+ }
1360
+
1361
+ /**
1362
+ * serialize pathData array to
1363
+ * d attribute string
1364
+ */
1365
+
1366
+ function pathDataToD(pathData, optimize = 0) {
1367
+
1368
+ optimize = parseFloat(optimize);
1369
+
1370
+ let len = pathData.length;
1371
+ let beautify = optimize > 1;
1372
+ let minify = beautify || optimize ? false : true;
1373
+
1374
+ // Convert first "M" to "m" if followed by "l" (when minified)
1375
+ /*
1376
+ if (pathData[1].type === "l" && minify) {
1377
+ pathData[0].type = "m";
1378
+ }
1379
+ */
1380
+
1381
+ let d = '';
1382
+ let separator_command = beautify ? `\n` : (minify ? '' : ' ');
1383
+ let separator_type = !minify ? ' ' : '';
1384
+
1385
+ d = `${pathData[0].type}${separator_type}${pathData[0].values.join(" ")}${separator_command}`;
1386
+
1387
+ for (let i = 1; i < len; i++) {
1388
+ let com0 = pathData[i - 1];
1389
+ let com = pathData[i];
1390
+ let { type, values } = com;
1391
+
1392
+ // Minify Arc commands (A/a) – actually sucks!
1393
+ if (minify && (type === 'A' || type === 'a')) {
1394
+ values = [
1395
+ values[0], values[1], values[2],
1396
+ `${values[3]}${values[4]}${values[5]}`,
1397
+ values[6]
1398
+ ];
1399
+ }
1400
+
1401
+ // Omit type for repeated commands
1402
+ type = (com0.type === com.type && com.type.toLowerCase() !== 'm' && minify)
1403
+ ? " "
1404
+ : (
1405
+ (com0.type === "M" && com.type === "L")
1406
+ ) && minify
1407
+ ? " "
1408
+ : com.type;
1409
+
1410
+ // concatenate subsequent floating point values
1411
+ if (minify) {
1412
+
1413
+ let valsString = '';
1414
+ let prevWasFloat = false;
1415
+
1416
+ for (let v = 0, l = values.length; v < l; v++) {
1417
+ let val = values[v];
1418
+ let valStr = val.toString();
1419
+ let isFloat = valStr.includes('.');
1420
+ let isSmallFloat = isFloat && Math.abs(val) < 1;
1421
+
1422
+ // Remove leading zero from small floats *only* if the previous was also a float
1423
+ if (isSmallFloat && prevWasFloat) {
1424
+ valStr = valStr.replace(/^0\./, '.');
1425
+ }
1426
+
1427
+ // Add space unless this is the first value OR previous was a small float
1428
+ if (v > 0 && !(prevWasFloat && isSmallFloat)) {
1429
+ valsString += ' ';
1430
+ }
1431
+
1432
+ valsString += valStr;
1433
+ prevWasFloat = isSmallFloat;
1434
+ }
1435
+
1436
+ d += `${type}${separator_type}${valsString}${separator_command}`;
1437
+
1438
+ }
1439
+ // regular non-minified output
1440
+ else {
1441
+ d += `${type}${separator_type}${values.join(' ')}${separator_command}`;
1442
+ }
1443
+ }
1444
+
1445
+ if (minify) {
1446
+ d = d
1447
+ .replace(/ 0\./g, " .") // Space before small decimals
1448
+ .replace(/ -/g, "-") // Remove space before negatives
1449
+ .replace(/-0\./g, "-.") // Remove leading zero from negative decimals
1450
+ .replace(/Z/g, "z"); // Convert uppercase 'Z' to lowercase
1451
+ }
1452
+
1453
+ return d;
1454
+ }
1455
+
1456
+ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1457
+
1458
+ // cubic Bézier derivative
1459
+ const cubicDerivative = (p0, p1, p2, p3, t) => {
1460
+ let mt = 1 - t;
1461
+
1462
+ return {
1463
+ x:
1464
+ 3 * mt * mt * (p1.x - p0.x) +
1465
+ 6 * mt * t * (p2.x - p1.x) +
1466
+ 3 * t * t * (p3.x - p2.x),
1467
+ y:
1468
+ 3 * mt * mt * (p1.y - p0.y) +
1469
+ 6 * mt * t * (p2.y - p1.y) +
1470
+ 3 * t * t * (p3.y - p2.y)
1471
+ };
1472
+ };
1473
+
1474
+ // if combining fails return original commands
1475
+ let commands = [com1, com2];
1476
+
1477
+ // detect dominant
1478
+ let dist1 = getSquareDistance(com1.p0, com1.p);
1479
+ let dist2 = getSquareDistance(com2.p0, com2.p);
1480
+ let reverse = dist1 > dist2;
1481
+
1482
+ // backup original commands
1483
+ let com1_o = JSON.parse(JSON.stringify(com1));
1484
+ let com2_o = JSON.parse(JSON.stringify(com2));
1485
+
1486
+ let ptI = checkLineIntersection(com1_o.p0, com1_o.cp1, com2_o.p, com2_o.cp2, false);
1487
+
1488
+ if (!ptI) {
1489
+
1490
+ return commands
1491
+ }
1492
+
1493
+ if (reverse) {
1494
+ let com2_R = {
1495
+ p0: { x: com1.p.x, y: com1.p.y },
1496
+ cp1: { x: com1.cp2.x, y: com1.cp2.y },
1497
+ cp2: { x: com1.cp1.x, y: com1.cp1.y },
1498
+ p: { x: com1.p0.x, y: com1.p0.y },
1499
+ };
1500
+
1501
+ let com1_R = {
1502
+ p0: { x: com2.p.x, y: com2.p.y },
1503
+ cp1: { x: com2.cp2.x, y: com2.cp2.y },
1504
+ cp2: { x: com2.cp1.x, y: com2.cp1.y },
1505
+ p: { x: com2.p0.x, y: com2.p0.y },
1506
+ };
1507
+
1508
+ com1 = com1_R;
1509
+ com2 = com2_R;
1510
+ }
1511
+
1512
+ let add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
1513
+ let sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
1514
+ let mul = (a, s) => ({ x: a.x * s, y: a.y * s });
1515
+ let dot = (a, b) => a.x * b.x + a.y * b.y;
1516
+
1517
+ // estimate extrapolation parameter t0
1518
+
1519
+ let B0 = com2.p0;
1520
+ let D0 = cubicDerivative(
1521
+ com2.p0,
1522
+ com2.cp1,
1523
+ com2.cp2,
1524
+ com2.p,
1525
+ 0
1526
+ );
1527
+
1528
+ let v = sub(com1.p0, B0);
1529
+
1530
+ // first-order projection onto tangent
1531
+ let t0 = dot(v, D0) / dot(D0, D0);
1532
+
1533
+ // refine with one Newton iteration (optional but cheap)
1534
+ let P = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
1535
+ let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
1536
+ let r = sub(P, com1.p0);
1537
+
1538
+ t0 -= dot(r, dP) / dot(dP, dP);
1539
+
1540
+ // construct merged cubic over [t0, 1]
1541
+
1542
+ let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
1543
+ let Q3 = com2.p;
1544
+
1545
+ let d0 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
1546
+ let d1 = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, 1);
1547
+
1548
+ let scale = 1 - t0;
1549
+
1550
+ let Q1 = add(Q0, mul(d0, scale / 3));
1551
+ let Q2 = sub(Q3, mul(d1, scale / 3));
1552
+
1553
+ let result = {
1554
+ p0: Q0,
1555
+ cp1: Q1,
1556
+ cp2: Q2,
1557
+ p: Q3,
1558
+ };
1559
+
1560
+ if (reverse) {
1561
+ result = {
1562
+ p0: Q3,
1563
+ cp1: Q2,
1564
+ cp2: Q1,
1565
+ p: Q0,
1566
+ };
1567
+ }
1568
+
1569
+ let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], 0.5, false, true);
1570
+ let seg1_cp2 = ptM.cpts[2];
1571
+
1572
+ let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
1573
+ let ptI_2 = checkLineIntersection(ptM, seg1_cp2, result.p, ptI, false);
1574
+
1575
+ let cp1_2 = interpolate(result.p0, ptI_1, 1.333);
1576
+ let cp2_2 = interpolate(result.p, ptI_2, 1.333);
1577
+
1578
+ // test self intersections and exit
1579
+ let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true );
1580
+ if(cp_intersection){
1581
+
1582
+ return commands;
1583
+ }
1584
+
1585
+ result.cp1 = cp1_2;
1586
+ result.cp2 = cp2_2;
1587
+
1588
+ // check distances
1589
+
1590
+ let dist3 = getDistAv(com1_o.p0, result.p0);
1591
+ let dist4 = getDistAv(com2_o.p, result.p);
1592
+ let dist5 = (dist3 + dist4);
1593
+
1594
+ // use original points
1595
+ result.p0 = com1_o.p0;
1596
+ result.p = com2_o.p;
1597
+ result.extreme = com2_o.extreme;
1598
+ result.corner = com2_o.corner;
1599
+ result.dimA = com2_o.dimA;
1600
+ result.directionChange = com2_o.directionChange;
1601
+ result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
1602
+
1603
+ // check if completely off
1604
+ if (dist5 < maxDist) {
1605
+
1606
+ // compare combined with original area
1607
+ let pathData0 = [
1608
+ { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
1609
+ { type: 'C', values: [com1_o.cp1.x, com1_o.cp1.y, com1_o.cp2.x, com1_o.cp2.y, com1_o.p.x, com1_o.p.y] },
1610
+ { type: 'C', values: [com2_o.cp1.x, com2_o.cp1.y, com2_o.cp2.x, com2_o.cp2.y, com2_o.p.x, com2_o.p.y] },
1611
+ ];
1612
+
1613
+ let area0 = getPathArea(pathData0);
1614
+ let pathDataN = [
1615
+ { type: 'M', values: [result.p0.x, result.p0.y] },
1616
+ { type: 'C', values: [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y] },
1617
+ ];
1618
+
1619
+ let areaN = getPathArea(pathDataN);
1620
+ let areaDiff = Math.abs(areaN / area0 - 1);
1621
+
1622
+ result.error = areaDiff * 10 * tolerance;
1623
+
1624
+ pathDataToD(pathDataN);
1625
+
1626
+ // success
1627
+ if (areaDiff < 0.01) {
1628
+ commands = [result];
1629
+
1630
+ }
1631
+
1632
+ }
1633
+
1634
+ return commands
1635
+
1636
+ }
1637
+
1638
+ function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance = 1) {
1639
+
1640
+ let commands = [com1, com2];
1641
+ let t = findSplitT(com1, com2);
1642
+
1643
+ let distAv1 = getDistAv(com1.p0, com1.p);
1644
+ let distAv2 = getDistAv(com2.p0, com2.p);
1645
+ let distMin = Math.min(distAv1, distAv2);
1646
+
1647
+ let distScale = 0.05;
1648
+ let maxDist = distMin * distScale * tolerance;
1649
+
1650
+ let comS = getExtrapolatedCommand(com1, com2, t, t);
1651
+
1652
+ // test on path point against original
1653
+ let pt = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t);
1654
+
1655
+ let dist0 = getDistAv(com1.p, pt);
1656
+ let dist1 = 0, dist2 = 0;
1657
+ let close = dist0 < maxDist;
1658
+ let success = false;
1659
+
1660
+ // collect error data
1661
+ let error = dist0;
1662
+
1663
+ /*
1664
+ if (com2.directionChange) {
1665
+
1666
+ }
1667
+ */
1668
+
1669
+ if (close) {
1670
+
1671
+ /**
1672
+ * check additional points
1673
+ * to prevent distortions
1674
+ */
1675
+
1676
+ // 2nd segment mid
1677
+ let pt_2 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], 0.5);
1678
+
1679
+ // simplified path
1680
+ let t3 = (1 + t) * 0.5;
1681
+ let ptS_2 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t3);
1682
+ dist1 = getDistAv(pt_2, ptS_2);
1683
+
1684
+ error += dist1;
1685
+
1686
+ // quit - paths not congruent
1687
+
1688
+ if (dist1 < maxDist) {
1689
+
1690
+ // 1st segment mid
1691
+ let pt_1 = pointAtT([com1.p0, com1.cp1, com1.cp2, com1.p], 0.5);
1692
+
1693
+ let t2 = t * 0.5;
1694
+ let ptS_1 = pointAtT([comS.p0, comS.cp1, comS.cp2, comS.p], t2);
1695
+ dist2 = getDistAv(pt_1, ptS_1);
1696
+
1697
+ /*
1698
+ if(dist1>tolerance){
1699
+ renderPoint(markers, pt_1, 'blue')
1700
+ renderPoint(markers, ptS_1, 'orange', '0.5%')
1701
+ }
1702
+ */
1703
+
1704
+ // quit - paths not congruent
1705
+ if (dist1 + dist2 < maxDist) success = true;
1706
+
1707
+ // collect error data
1708
+ error += dist2;
1709
+
1710
+ }
1711
+
1712
+ } // end 1st try
1713
+
1714
+
1715
+ /*
1716
+ if (extrapolateDominant && com2.extreme) {
1717
+ renderPoint(markers, com2.p)
1718
+
1719
+ }
1720
+ */
1721
+
1722
+
1723
+
1724
+ // try extrapolated dominant curve
1725
+
1726
+ // && !com1.extreme
1727
+ if (extrapolateDominant && !success ) {
1728
+
1729
+ let combinedEx = getCombinedByDominant(com1, com2, maxDist, tolerance);
1730
+
1731
+ if(combinedEx.length===1){
1732
+ success = true;
1733
+ comS = combinedEx[0];
1734
+ error = comS.error;
1735
+
1736
+ }
1737
+
1738
+
1739
+ }
1740
+
1741
+ // add meta
1742
+ if (success) {
1743
+
1744
+
1745
+ // correct to exact start and end points
1746
+ comS.p0 = com1.p0;
1747
+ comS.p = com2.p;
1748
+
1749
+ comS.dimA = getDistAv(comS.p0, comS.p);
1750
+ comS.type = 'C';
1751
+ comS.extreme = com2.extreme;
1752
+ comS.directionChange = com2.directionChange;
1753
+ comS.corner = com2.corner;
1754
+
1755
+ comS.values = [comS.cp1.x, comS.cp1.y, comS.cp2.x, comS.cp2.y, comS.p.x, comS.p.y];
1756
+
1757
+ // relative error
1758
+ comS.error = error / maxDist;
1759
+
1760
+ commands = [comS];
1761
+
1762
+ }
1763
+
1764
+ return commands;
1765
+ }
1766
+
1767
+ function getExtrapolatedCommand(com1, com2, t1 = 0, t2 = 0) {
1768
+
1769
+ let { p0, cp1 } = com1;
1770
+ let { p, cp2 } = com2;
1771
+
1772
+ // extrapolate control points
1773
+ let cp1_S = {
1774
+ x: (cp1.x - (1 - t1) * p0.x) / t1,
1775
+ y: (cp1.y - (1 - t1) * p0.y) / t1
1776
+ };
1777
+
1778
+ let cp2_S = {
1779
+ x: (cp2.x - t2 * p.x) / (1 - t2),
1780
+ y: (cp2.y - t2 * p.y) / (1 - t2)
1781
+ };
1782
+
1783
+ let comS = { p0, cp1: cp1_S, cp2: cp2_S, p };
1784
+
1785
+ return comS
1786
+
1787
+ }
1788
+
1789
+ function findSplitT(com1, com2) {
1790
+
1791
+ // control tangent intersection
1792
+ let pt1 = checkLineIntersection(com1.p0, com1.cp1, com2.cp2, com2.p, false);
1793
+
1794
+ // intersection 2nd cp1 tangent and global tangent intersection
1795
+ let ptI = checkLineIntersection(pt1, com2.p, com2.p0, com2.cp1, false);
1796
+
1797
+ let len1 = getDistance(pt1, com2.p);
1798
+ let len2 = getDistance(ptI, com2.p);
1799
+
1800
+ let t = 1 - len2 / len1;
1801
+
1802
+ // check self intersections
1803
+
1804
+ let len3 = getDistance(com1.cp2, com1.p);
1805
+ let len4 = getDistance(com1.cp2, com2.cp1);
1806
+
1807
+ t = Math.min(len3) / len4;
1808
+
1809
+ return t
1810
+
1811
+ }
1812
+
1813
+ function analyzePathData(pathData = []) {
1814
+
1815
+ let pathDataPlus = [];
1816
+
1817
+ let pathPoly = getPathDataVertices(pathData);
1818
+ let bb = getPolyBBox(pathPoly);
1819
+ let { left, right, top, bottom, width, height } = bb;
1820
+
1821
+ // initial starting point coordinates
1822
+ let M0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
1823
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
1824
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
1825
+ let p;
1826
+
1827
+ // init starting point data
1828
+ pathData[0].idx = 0;
1829
+ pathData[0].p0 = M;
1830
+ pathData[0].p = M;
1831
+ pathData[0].lineto = false;
1832
+ pathData[0].corner = false;
1833
+ pathData[0].extreme = false;
1834
+ pathData[0].directionChange = false;
1835
+ pathData[0].closePath = false;
1836
+ pathData[0].dimA = 0;
1837
+
1838
+ // add first M command
1839
+ let pathDataProps = [pathData[0]];
1840
+ let area0 = 0;
1841
+ let len = pathData.length;
1842
+
1843
+ for (let c = 2; len && c <= len; c++) {
1844
+
1845
+ let com = pathData[c - 1];
1846
+ let { type, values } = com;
1847
+ let valsL = values.slice(-2);
1848
+
1849
+ /**
1850
+ * get command points for
1851
+ * flatness checks:
1852
+ * this way we can skip certain tests
1853
+ */
1854
+ let commandPts = [p0];
1855
+ let isFlat = false;
1856
+
1857
+ // init properties
1858
+ com.idx = c - 1;
1859
+ com.lineto = false;
1860
+ com.corner = false;
1861
+ com.extreme = false;
1862
+ com.directionChange = false;
1863
+ com.closePath = false;
1864
+ com.dimA = 0;
1865
+
1866
+ /**
1867
+ * define angle threshold for
1868
+ * corner detection
1869
+ */
1870
+ let angleThreshold = 0.05;
1871
+ p = valsL.length ? { x: valsL[0], y: valsL[1] } : M;
1872
+
1873
+ // update M for Z starting points
1874
+ if (type === 'M') {
1875
+ M = p;
1876
+ p0 = p;
1877
+ }
1878
+ else if (type.toLowerCase() === 'z') {
1879
+ p = M;
1880
+ }
1881
+
1882
+ // add on-path points
1883
+ com.p0 = p0;
1884
+ com.p = p;
1885
+
1886
+ let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
1887
+
1888
+ let dimA = getDistAv(p0, p);
1889
+ com.dimA = dimA;
1890
+
1891
+ /**
1892
+ * explicit and implicit linetos
1893
+ * - introduced by Z
1894
+ */
1895
+ if (type === 'L') com.lineto = true;
1896
+
1897
+ if (type === 'Z') {
1898
+ com.closePath = true;
1899
+ // if Z introduces an implicit lineto with a length
1900
+ if (M.x !== M0.x && M.y !== M0.y) {
1901
+ com.lineto = true;
1902
+ }
1903
+ }
1904
+
1905
+ // if bezier
1906
+ if (type === 'Q' || type === 'C') {
1907
+ cp1 = { x: values[0], y: values[1] };
1908
+ cp2 = type === 'C' ? { x: values[2], y: values[3] } : null;
1909
+ com.cp1 = cp1;
1910
+ if (cp2) com.cp2 = cp2;
1911
+ }
1912
+
1913
+ /**
1914
+ * check command flatness
1915
+ * we leave it to the bezier simplifier
1916
+ * to convert flat beziers to linetos
1917
+ * otherwise we may strip rather flat starting segments
1918
+ * preventing a better simplification
1919
+ */
1920
+
1921
+ if (values.length > 2) {
1922
+ if (type === 'Q' || type === 'C') commandPts.push(cp1);
1923
+ if (type === 'C') commandPts.push(cp2);
1924
+ commandPts.push(p);
1925
+
1926
+ let commandFlatness = commandIsFlat(commandPts);
1927
+ isFlat = commandFlatness.flat;
1928
+ com.flat = isFlat;
1929
+
1930
+ if (isFlat) {
1931
+ com.extreme = false;
1932
+ }
1933
+ }
1934
+
1935
+ /**
1936
+ * is extreme relative to bounding box
1937
+ * in case elements are rotated we can't rely on 90degree angles
1938
+ * so we interpret maximum x/y on-path points as well as extremes
1939
+ * but we ignore linetos to allow chunk compilation
1940
+ */
1941
+ if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
1942
+ com.extreme = true;
1943
+ }
1944
+
1945
+ let comN = pathData[c] ? pathData[c] : null;
1946
+ let comNValsL = comN ? comN.values.slice(-2) : null;
1947
+ typeN = comN ? comN.type : null;
1948
+
1949
+ // get bezier control points
1950
+ if (comN && (comN.type === 'Q' || comN.type === 'C')) {
1951
+ pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
1952
+
1953
+ cp1N = { x: comN.values[0], y: comN.values[1] };
1954
+ cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
1955
+ }
1956
+
1957
+ /**
1958
+ * Detect direction change points
1959
+ * this will prevent distortions when simplifying
1960
+ * e.g in the "spine" of an "S" glyph
1961
+ */
1962
+ area1 = getPolygonArea(commandPts);
1963
+ let signChange = (area0 < 0 && area1 > 0) || (area0 > 0 && area1 < 0) ? true : false;
1964
+ // update area
1965
+ area0 = area1;
1966
+
1967
+ if (signChange) {
1968
+
1969
+ com.directionChange = true;
1970
+ }
1971
+
1972
+ /**
1973
+ * check extremes or corners
1974
+ * for adjacent curves by
1975
+ * control point angles
1976
+ */
1977
+ if ((type === 'Q' || type === 'C')) {
1978
+
1979
+ if ((type === 'Q' && typeN === 'Q') || (type === 'C' && typeN === 'C')) {
1980
+
1981
+ // check extremes
1982
+ let cpts = commandPts.slice(1);
1983
+
1984
+ let w = pN ? Math.abs(pN.x - p0.x) : 0;
1985
+ let h = pN ? Math.abs(pN.y - p0.y) : 0;
1986
+ let thresh = (w + h) / 2 * 0.1;
1987
+ let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
1988
+
1989
+ let flatness2 = commandIsFlat(pts1, thresh);
1990
+ let isFlat2 = flatness2.flat;
1991
+
1992
+ /**
1993
+ * if current and next cubic are flat
1994
+ * we don't flag them as extremes to allow simplification
1995
+ */
1996
+ let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
1997
+
1998
+ if (hasExtremes) {
1999
+ com.extreme = true;
2000
+ }
2001
+
2002
+ // check corners
2003
+ else {
2004
+
2005
+ let cpts1 = cp2 ? [cp2, p] : [cp1, p];
2006
+ let cpts2 = cp2 ? [p, cp1N] : [p, cp1N];
2007
+
2008
+ let angCom1 = getAngle(...cpts1, true);
2009
+ let angCom2 = getAngle(...cpts2, true);
2010
+ let angDiff = Math.abs(angCom1 - angCom2) * 180 / Math.PI;
2011
+
2012
+ let cpDist1 = getSquareDistance(...cpts1);
2013
+ let cpDist2 = getSquareDistance(...cpts2);
2014
+
2015
+ let cornerThreshold = 10;
2016
+ let isCorner = angDiff > cornerThreshold && cpDist1 && cpDist2;
2017
+
2018
+ if (isCorner) {
2019
+ com.corner = true;
2020
+ }
2021
+ }
2022
+ }
2023
+ }
2024
+
2025
+ pathDataProps.push(com);
2026
+ p0 = p;
2027
+
2028
+ }
2029
+
2030
+ let dimA = (width + height) / 2;
2031
+
2032
+ pathDataPlus = { pathData: pathDataProps, bb: bb, dimA: dimA };
2033
+
2034
+ return pathDataPlus
2035
+
2036
+ }
2037
+
2038
+ function detectAccuracy(pathData) {
2039
+
2040
+ // Reference first MoveTo command (M)
2041
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
2042
+ let p0 = M;
2043
+ let p = M;
2044
+ pathData[0].decimals = 0;
2045
+
2046
+ let dims = new Set();
2047
+
2048
+ // add average distances
2049
+ for (let i = 0, len = pathData.length; i < len; i++) {
2050
+ let com = pathData[i];
2051
+ let { type, values } = com;
2052
+
2053
+ let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
2054
+ p={x:lastVals[0], y:lastVals[1]};
2055
+
2056
+ // use existing averave dimension value or calculate
2057
+ let dimA = com.dimA ? +com.dimA.toFixed(8) : type!=='M' ? +getDistAv(p0, p).toFixed(8) : 0;
2058
+
2059
+ if(dimA) dims.add(dimA);
2060
+
2061
+
2062
+ if(type==='M'){
2063
+ M=p;
2064
+ }
2065
+ p0 = p;
2066
+ }
2067
+
2068
+ let dim_min = Array.from(dims).sort();
2069
+ let sliceIdx = Math.ceil(dim_min.length/8);
2070
+ dim_min = dim_min.slice(0, sliceIdx );
2071
+
2072
+ let dimVal = dim_min.reduce((a,b)=>a+b, 0) / sliceIdx;
2073
+
2074
+ let threshold = 50;
2075
+ let decimalsAuto = dimVal > threshold ? 0 : Math.floor(threshold / dimVal).toString().length;
2076
+
2077
+ // clamp
2078
+ return Math.min(Math.max(0, decimalsAuto), 8)
2079
+
2080
+ }
2081
+
2082
+ /**
2083
+ * round path data
2084
+ * either by explicit decimal value or
2085
+ * based on suggested accuracy in path data
2086
+ */
2087
+ function roundPathData(pathData, decimals = -1) {
2088
+ // has recommended decimals
2089
+ let hasDecimal = decimals == 'auto' && pathData[0].hasOwnProperty('decimals') ? true : false;
2090
+
2091
+ for(let c=0, len=pathData.length; c<len; c++){
2092
+ let com=pathData[c];
2093
+
2094
+ if (decimals >-1 || hasDecimal) {
2095
+ decimals = hasDecimal ? com.decimals : decimals;
2096
+
2097
+ pathData[c].values = com.values.map(val=>{return val ? +val.toFixed(decimals) : val });
2098
+
2099
+ }
2100
+ } return pathData;
2101
+ }
2102
+
2103
+ function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
2104
+
2105
+ // test if cubic can be simplified to quadratic
2106
+ let cp1X = interpolate(p0, cp1, 1.5);
2107
+ let cp2X = interpolate(p, cp2, 1.5);
2108
+
2109
+ let dist0 = getDistAv(p0, p);
2110
+ let threshold = dist0 * 0.01;
2111
+ let dist1 = getDistAv(cp1X, cp2X);
2112
+
2113
+ let cp1_Q = null;
2114
+ let type = 'C';
2115
+ let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
2116
+
2117
+ if (dist1 < threshold) {
2118
+ cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
2119
+ if (cp1_Q) {
2120
+
2121
+ type = 'Q';
2122
+ values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
2123
+ }
2124
+ }
2125
+
2126
+ return { type, values }
2127
+
2128
+ }
2129
+
2130
+ function convertPathData(pathData, {
2131
+ toShorthands = true,
2132
+ toRelative = true,
2133
+ decimals = 3
2134
+ } = {}) {
2135
+
2136
+ if (toShorthands) pathData = pathDataToShorthands(pathData);
2137
+
2138
+ // pre round - before relative conversion to minimize distortions
2139
+ pathData = roundPathData(pathData, decimals);
2140
+ if (toRelative) pathData = pathDataToRelative(pathData);
2141
+ if (decimals > -1) pathData = roundPathData(pathData, decimals);
2142
+ return pathData
2143
+ }
2144
+
2145
+ /**
2146
+ * convert cubic circle approximations
2147
+ * to more compact arcs
2148
+ */
2149
+
2150
+ function pathDataArcsToCubics(pathData, {
2151
+ arcAccuracy = 1
2152
+ } = {}) {
2153
+
2154
+ let pathDataCubic = [pathData[0]];
2155
+ for (let i = 1, len = pathData.length; i < len; i++) {
2156
+
2157
+ let com = pathData[i];
2158
+ let comPrev = pathData[i - 1];
2159
+ let valuesPrev = comPrev.values;
2160
+ let valuesPrevL = valuesPrev.length;
2161
+ let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
2162
+
2163
+ if (com.type === 'A') {
2164
+ // add all C commands instead of Arc
2165
+ let cubicArcs = arcToBezier$1(p0, com.values, arcAccuracy);
2166
+ cubicArcs.forEach((cubicArc) => {
2167
+ pathDataCubic.push(cubicArc);
2168
+ });
2169
+ }
2170
+
2171
+ else {
2172
+ // add command
2173
+ pathDataCubic.push(com);
2174
+ }
2175
+ }
2176
+
2177
+ return pathDataCubic
2178
+
2179
+ }
2180
+
2181
+ function pathDataQuadraticToCubic(pathData) {
2182
+
2183
+ let pathDataQuadratic = [pathData[0]];
2184
+ for (let i = 1, len = pathData.length; i < len; i++) {
2185
+
2186
+ let com = pathData[i];
2187
+ let comPrev = pathData[i - 1];
2188
+ let valuesPrev = comPrev.values;
2189
+ let valuesPrevL = valuesPrev.length;
2190
+ let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
2191
+
2192
+ if (com.type === 'Q') {
2193
+ pathDataQuadratic.push(quadratic2Cubic(p0, com.values));
2194
+ }
2195
+
2196
+ else {
2197
+ // add command
2198
+ pathDataQuadratic.push(com);
2199
+ }
2200
+ }
2201
+
2202
+ return pathDataQuadratic
2203
+ }
2204
+
2205
+ /**
2206
+ * convert quadratic commands to cubic
2207
+ */
2208
+ function quadratic2Cubic(p0, values) {
2209
+ if (Array.isArray(p0)) {
2210
+ p0 = {
2211
+ x: p0[0],
2212
+ y: p0[1]
2213
+ };
2214
+ }
2215
+ let cp1 = {
2216
+ x: p0.x + 2 / 3 * (values[0] - p0.x),
2217
+ y: p0.y + 2 / 3 * (values[1] - p0.y)
2218
+ };
2219
+ let cp2 = {
2220
+ x: values[2] + 2 / 3 * (values[0] - values[2]),
2221
+ y: values[3] + 2 / 3 * (values[1] - values[3])
2222
+ };
2223
+ return ({ type: "C", values: [cp1.x, cp1.y, cp2.x, cp2.y, values[2], values[3]] });
2224
+ }
2225
+
2226
+ /**
2227
+ * convert pathData to
2228
+ * This is just a port of Dmitry Baranovskiy's
2229
+ * pathToRelative/Absolute methods used in snap.svg
2230
+ * https://github.com/adobe-webplatform/Snap.svg/
2231
+ */
2232
+
2233
+ function pathDataToAbsoluteOrRelative(pathData, toRelative = false, decimals = -1) {
2234
+ if (decimals >= 0) {
2235
+ pathData[0].values = pathData[0].values.map(val => +val.toFixed(decimals));
2236
+ }
2237
+
2238
+ let M = pathData[0].values;
2239
+ let x = M[0],
2240
+ y = M[1],
2241
+ mx = x,
2242
+ my = y;
2243
+
2244
+ for (let i = 1, len = pathData.length; i < len; i++) {
2245
+ let com = pathData[i];
2246
+ let { type, values } = com;
2247
+ let newType = toRelative ? type.toLowerCase() : type.toUpperCase();
2248
+
2249
+ if (type !== newType) {
2250
+ type = newType;
2251
+ com.type = type;
2252
+
2253
+ switch (type) {
2254
+ case "a":
2255
+ case "A":
2256
+ values[5] = toRelative ? values[5] - x : values[5] + x;
2257
+ values[6] = toRelative ? values[6] - y : values[6] + y;
2258
+ break;
2259
+ case "v":
2260
+ case "V":
2261
+ values[0] = toRelative ? values[0] - y : values[0] + y;
2262
+ break;
2263
+ case "h":
2264
+ case "H":
2265
+ values[0] = toRelative ? values[0] - x : values[0] + x;
2266
+ break;
2267
+ case "m":
2268
+ case "M":
2269
+ if (toRelative) {
2270
+ values[0] -= x;
2271
+ values[1] -= y;
2272
+ } else {
2273
+ values[0] += x;
2274
+ values[1] += y;
2275
+ }
2276
+ mx = toRelative ? values[0] + x : values[0];
2277
+ my = toRelative ? values[1] + y : values[1];
2278
+ break;
2279
+ default:
2280
+ if (values.length) {
2281
+ for (let v = 0; v < values.length; v++) {
2282
+ values[v] = toRelative
2283
+ ? values[v] - (v % 2 ? y : x)
2284
+ : values[v] + (v % 2 ? y : x);
2285
+ }
2286
+ }
2287
+ }
2288
+ }
2289
+
2290
+ let vLen = values.length;
2291
+ switch (type) {
2292
+ case "z":
2293
+ case "Z":
2294
+ x = mx;
2295
+ y = my;
2296
+ break;
2297
+ case "h":
2298
+ case "H":
2299
+ x = toRelative ? x + values[0] : values[0];
2300
+ break;
2301
+ case "v":
2302
+ case "V":
2303
+ y = toRelative ? y + values[0] : values[0];
2304
+ break;
2305
+ case "m":
2306
+ case "M":
2307
+ mx = values[vLen - 2] + (toRelative ? x : 0);
2308
+ my = values[vLen - 1] + (toRelative ? y : 0);
2309
+ default:
2310
+ x = values[vLen - 2] + (toRelative ? x : 0);
2311
+ y = values[vLen - 1] + (toRelative ? y : 0);
2312
+ }
2313
+
2314
+ if (decimals >= 0) {
2315
+ com.values = com.values.map(val => +val.toFixed(decimals));
2316
+ }
2317
+ }
2318
+ return pathData;
2319
+ }
2320
+
2321
+ function pathDataToRelative(pathData, decimals = -1) {
2322
+ return pathDataToAbsoluteOrRelative(pathData, true, decimals)
2323
+ }
2324
+
2325
+ function pathDataToAbsolute(pathData, decimals = -1) {
2326
+ return pathDataToAbsoluteOrRelative(pathData, false, decimals)
2327
+ }
2328
+
2329
+ /**
2330
+ * decompose/convert shorthands to "longhand" commands:
2331
+ * H, V, S, T => L, L, C, Q
2332
+ * reversed method: pathDataToShorthands()
2333
+ */
2334
+
2335
+ function pathDataToLonghands(pathData, decimals = -1, test = true) {
2336
+
2337
+ // analyze pathdata – if you're sure your data is already absolute skip it via test=false
2338
+ let hasRel = false;
2339
+
2340
+ if (test) {
2341
+ let commandTokens = pathData.map(com => { return com.type }).join('');
2342
+ let hasShorthands = /[hstv]/gi.test(commandTokens);
2343
+ hasRel = /[astvqmhlc]/g.test(commandTokens);
2344
+
2345
+ if (!hasShorthands) {
2346
+ return pathData;
2347
+ }
2348
+ }
2349
+
2350
+ pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
2351
+
2352
+ let pathDataLonghand = [];
2353
+ let comPrev = {
2354
+ type: "M",
2355
+ values: pathData[0].values
2356
+ };
2357
+ pathDataLonghand.push(comPrev);
2358
+
2359
+ for (let i = 1, len = pathData.length; i < len; i++) {
2360
+ let com = pathData[i];
2361
+ let { type, values } = com;
2362
+ let valuesL = values.length;
2363
+ let valuesPrev = comPrev.values;
2364
+ let valuesPrevL = valuesPrev.length;
2365
+ let [x, y] = [values[valuesL - 2], values[valuesL - 1]];
2366
+ let cp1X, cp1Y, cpN1X, cpN1Y, cpN2X, cpN2Y, cp2X, cp2Y;
2367
+ let [prevX, prevY] = [
2368
+ valuesPrev[valuesPrevL - 2],
2369
+ valuesPrev[valuesPrevL - 1]
2370
+ ];
2371
+ switch (type) {
2372
+ case "H":
2373
+ comPrev = {
2374
+ type: "L",
2375
+ values: [values[0], prevY]
2376
+ };
2377
+ break;
2378
+ case "V":
2379
+ comPrev = {
2380
+ type: "L",
2381
+ values: [prevX, values[0]]
2382
+ };
2383
+ break;
2384
+ case "T":
2385
+ [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
2386
+ [prevX, prevY] = [
2387
+ valuesPrev[valuesPrevL - 2],
2388
+ valuesPrev[valuesPrevL - 1]
2389
+ ];
2390
+ // new control point
2391
+ cpN1X = prevX + (prevX - cp1X);
2392
+ cpN1Y = prevY + (prevY - cp1Y);
2393
+ comPrev = {
2394
+ type: "Q",
2395
+ values: [cpN1X, cpN1Y, x, y]
2396
+ };
2397
+ break;
2398
+ case "S":
2399
+
2400
+ [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
2401
+ [prevX, prevY] = [
2402
+ valuesPrev[valuesPrevL - 2],
2403
+ valuesPrev[valuesPrevL - 1]
2404
+ ];
2405
+
2406
+ [cp2X, cp2Y] =
2407
+ valuesPrevL > 2 && comPrev.type !== 'A' ?
2408
+ [valuesPrev[2], valuesPrev[3]] :
2409
+ [prevX, prevY];
2410
+
2411
+ // new control points
2412
+ cpN1X = 2 * prevX - cp2X;
2413
+ cpN1Y = 2 * prevY - cp2Y;
2414
+ cpN2X = values[0];
2415
+ cpN2Y = values[1];
2416
+ comPrev = {
2417
+ type: "C",
2418
+ values: [cpN1X, cpN1Y, cpN2X, cpN2Y, x, y]
2419
+ };
2420
+
2421
+ break;
2422
+ default:
2423
+ comPrev = {
2424
+ type: type,
2425
+ values: values
2426
+ };
2427
+ }
2428
+ // round final longhand values
2429
+ if (decimals > -1) {
2430
+ comPrev.values = comPrev.values.map(val => { return +val.toFixed(decimals) });
2431
+ }
2432
+
2433
+ pathDataLonghand.push(comPrev);
2434
+ }
2435
+ return pathDataLonghand;
2436
+ }
2437
+
2438
+ /**
2439
+ * apply shorthand commands if possible
2440
+ * L, L, C, Q => H, V, S, T
2441
+ * reversed method: pathDataToLonghands()
2442
+ */
2443
+ function pathDataToShorthands(pathData, decimals = -1, test = true) {
2444
+
2445
+ /**
2446
+ * analyze pathdata – if you're sure your data is already absolute skip it via test=false
2447
+ */
2448
+ let hasRel;
2449
+ if (test) {
2450
+ let commandTokens = pathData.map(com => { return com.type }).join('');
2451
+ hasRel = /[astvqmhlc]/g.test(commandTokens);
2452
+ }
2453
+
2454
+ pathData = test && hasRel ? pathDataToAbsolute(pathData, decimals) : pathData;
2455
+
2456
+ let comShort = {
2457
+ type: "M",
2458
+ values: pathData[0].values
2459
+ };
2460
+
2461
+ if (pathData[0].decimals) {
2462
+
2463
+ comShort.decimals = pathData[0].decimals;
2464
+ }
2465
+
2466
+ let pathDataShorts = [comShort];
2467
+
2468
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] };
2469
+ let p;
2470
+ let tolerance = 0.01;
2471
+
2472
+ for (let i = 1, len = pathData.length; i < len; i++) {
2473
+
2474
+ let com = pathData[i];
2475
+ let { type, values } = com;
2476
+ let valuesLast = values.slice(-2);
2477
+
2478
+ // previoius command
2479
+ let comPrev = pathData[i - 1];
2480
+ let typePrev = comPrev.type;
2481
+
2482
+ p = { x: valuesLast[0], y: valuesLast[1] };
2483
+
2484
+ // first bezier control point for S/T shorthand tests
2485
+ let cp1 = { x: values[0], y: values[1] };
2486
+
2487
+ let w = Math.abs(p.x - p0.x);
2488
+ let h = Math.abs(p.y - p0.y);
2489
+ let thresh = (w + h) / 2 * tolerance;
2490
+
2491
+ let diffX, diffY, diff, cp1_reflected;
2492
+
2493
+ switch (type) {
2494
+ case "L":
2495
+
2496
+ if (h === 0 || (h < thresh && w > thresh)) {
2497
+
2498
+ comShort = {
2499
+ type: "H",
2500
+ values: [values[0]]
2501
+ };
2502
+ }
2503
+
2504
+ // V
2505
+ else if (w === 0 || (h > thresh && w < thresh)) {
2506
+
2507
+ comShort = {
2508
+ type: "V",
2509
+ values: [values[1]]
2510
+ };
2511
+ } else {
2512
+
2513
+ comShort = com;
2514
+ }
2515
+
2516
+ break;
2517
+
2518
+ case "Q":
2519
+
2520
+ // skip test
2521
+ if (typePrev !== 'Q') {
2522
+
2523
+ p0 = { x: valuesLast[0], y: valuesLast[1] };
2524
+ pathDataShorts.push(com);
2525
+ continue;
2526
+ }
2527
+
2528
+ let cp1_prev = { x: comPrev.values[0], y: comPrev.values[1] };
2529
+ // reflected Q control points
2530
+ cp1_reflected = { x: (2 * p0.x - cp1_prev.x), y: (2 * p0.y - cp1_prev.y) };
2531
+
2532
+ diffX = Math.abs(cp1.x - cp1_reflected.x);
2533
+ diffY = Math.abs(cp1.y - cp1_reflected.y);
2534
+ diff = (diffX + diffY) / 2;
2535
+
2536
+ if (diff < thresh) {
2537
+
2538
+ comShort = {
2539
+ type: "T",
2540
+ values: [p.x, p.y]
2541
+ };
2542
+ } else {
2543
+ comShort = com;
2544
+ }
2545
+
2546
+ break;
2547
+ case "C":
2548
+
2549
+ let cp2 = { x: values[2], y: values[3] };
2550
+
2551
+ if (typePrev !== 'C') {
2552
+
2553
+ pathDataShorts.push(com);
2554
+ p0 = { x: valuesLast[0], y: valuesLast[1] };
2555
+ continue;
2556
+ }
2557
+
2558
+ let cp2_prev = { x: comPrev.values[2], y: comPrev.values[3] };
2559
+
2560
+ // reflected C control points
2561
+ cp1_reflected = { x: (2 * p0.x - cp2_prev.x), y: (2 * p0.y - cp2_prev.y) };
2562
+
2563
+ diffX = Math.abs(cp1.x - cp1_reflected.x);
2564
+ diffY = Math.abs(cp1.y - cp1_reflected.y);
2565
+ diff = (diffX + diffY) / 2;
2566
+
2567
+ if (diff < thresh) {
2568
+
2569
+ comShort = {
2570
+ type: "S",
2571
+ values: [cp2.x, cp2.y, p.x, p.y]
2572
+ };
2573
+ } else {
2574
+ comShort = com;
2575
+ }
2576
+ break;
2577
+ default:
2578
+ comShort = {
2579
+ type: type,
2580
+ values: values
2581
+ };
2582
+ }
2583
+
2584
+ // add decimal info
2585
+ if (com.decimals || com.decimals === 0) {
2586
+ comShort.decimals = com.decimals;
2587
+ }
2588
+
2589
+ // round final values
2590
+ if (decimals > -1) {
2591
+ comShort.values = comShort.values.map(val => { return +val.toFixed(decimals) });
2592
+ }
2593
+
2594
+ p0 = { x: valuesLast[0], y: valuesLast[1] };
2595
+ pathDataShorts.push(comShort);
2596
+ }
2597
+ return pathDataShorts;
2598
+ }
2599
+
2600
+ /**
2601
+ * convert arctocommands to cubic bezier
2602
+ * based on puzrin's a2c.js
2603
+ * https://github.com/fontello/svgpath/blob/master/lib/a2c.js
2604
+ * returns pathData array
2605
+ */
2606
+
2607
+ function arcToBezier$1(p0, values, splitSegments = 1) {
2608
+ const TAU = Math.PI * 2;
2609
+ let [rx, ry, rotation, largeArcFlag, sweepFlag, x, y] = values;
2610
+
2611
+ if (rx === 0 || ry === 0) {
2612
+ return []
2613
+ }
2614
+
2615
+ let phi = rotation ? rotation * TAU / 360 : 0;
2616
+ let sinphi = phi ? Math.sin(phi) : 0;
2617
+ let cosphi = phi ? Math.cos(phi) : 1;
2618
+ let pxp = cosphi * (p0.x - x) / 2 + sinphi * (p0.y - y) / 2;
2619
+ let pyp = -sinphi * (p0.x - x) / 2 + cosphi * (p0.y - y) / 2;
2620
+
2621
+ if (pxp === 0 && pyp === 0) {
2622
+ return []
2623
+ }
2624
+ rx = Math.abs(rx);
2625
+ ry = Math.abs(ry);
2626
+ let lambda =
2627
+ pxp * pxp / (rx * rx) +
2628
+ pyp * pyp / (ry * ry);
2629
+ if (lambda > 1) {
2630
+ let lambdaRt = Math.sqrt(lambda);
2631
+ rx *= lambdaRt;
2632
+ ry *= lambdaRt;
2633
+ }
2634
+
2635
+ /**
2636
+ * parametrize arc to
2637
+ * get center point start and end angles
2638
+ */
2639
+ let rxsq = rx * rx,
2640
+ rysq = rx === ry ? rxsq : ry * ry;
2641
+
2642
+ let pxpsq = pxp * pxp,
2643
+ pypsq = pyp * pyp;
2644
+ let radicant = (rxsq * rysq) - (rxsq * pypsq) - (rysq * pxpsq);
2645
+
2646
+ if (radicant <= 0) {
2647
+ radicant = 0;
2648
+ } else {
2649
+ radicant /= (rxsq * pypsq) + (rysq * pxpsq);
2650
+ radicant = Math.sqrt(radicant) * (largeArcFlag === sweepFlag ? -1 : 1);
2651
+ }
2652
+
2653
+ let centerxp = radicant ? radicant * rx / ry * pyp : 0;
2654
+ let centeryp = radicant ? radicant * -ry / rx * pxp : 0;
2655
+ let centerx = cosphi * centerxp - sinphi * centeryp + (p0.x + x) / 2;
2656
+ let centery = sinphi * centerxp + cosphi * centeryp + (p0.y + y) / 2;
2657
+
2658
+ let vx1 = (pxp - centerxp) / rx;
2659
+ let vy1 = (pyp - centeryp) / ry;
2660
+ let vx2 = (-pxp - centerxp) / rx;
2661
+ let vy2 = (-pyp - centeryp) / ry;
2662
+
2663
+ // get start and end angle
2664
+ const vectorAngle = (ux, uy, vx, vy) => {
2665
+ let dot = +(ux * vx + uy * vy).toFixed(9);
2666
+ if (dot === 1 || dot === -1) {
2667
+ return dot === 1 ? 0 : Math.PI
2668
+ }
2669
+ dot = dot > 1 ? 1 : (dot < -1 ? -1 : dot);
2670
+ let sign = (ux * vy - uy * vx < 0) ? -1 : 1;
2671
+ return sign * Math.acos(dot);
2672
+ };
2673
+
2674
+ let ang1 = vectorAngle(1, 0, vx1, vy1),
2675
+ ang2 = vectorAngle(vx1, vy1, vx2, vy2);
2676
+
2677
+ if (sweepFlag === 0 && ang2 > 0) {
2678
+ ang2 -= Math.PI * 2;
2679
+ }
2680
+ else if (sweepFlag === 1 && ang2 < 0) {
2681
+ ang2 += Math.PI * 2;
2682
+ }
2683
+
2684
+ let ratio = +(Math.abs(ang2) / (TAU / 4)).toFixed(0) || 1;
2685
+
2686
+ // increase segments for more accureate length calculations
2687
+ let segments = ratio * splitSegments;
2688
+ ang2 /= segments;
2689
+ let pathDataArc = [];
2690
+
2691
+ // If 90 degree circular arc, use a constant
2692
+ // https://pomax.github.io/bezierinfo/#circles_cubic
2693
+ // k=0.551784777779014
2694
+ const angle90 = 1.5707963267948966;
2695
+ const k = 0.551785;
2696
+ let a = ang2 === angle90 ? k :
2697
+ (
2698
+ ang2 === -angle90 ? -k : 4 / 3 * Math.tan(ang2 / 4)
2699
+ );
2700
+
2701
+ let cos2 = ang2 ? Math.cos(ang2) : 1;
2702
+ let sin2 = ang2 ? Math.sin(ang2) : 0;
2703
+ let type = 'C';
2704
+
2705
+ const approxUnitArc = (ang1, ang2, a, cos2, sin2) => {
2706
+ let x1 = ang1 != ang2 ? Math.cos(ang1) : cos2;
2707
+ let y1 = ang1 != ang2 ? Math.sin(ang1) : sin2;
2708
+ let x2 = Math.cos(ang1 + ang2);
2709
+ let y2 = Math.sin(ang1 + ang2);
2710
+
2711
+ return [
2712
+ { x: x1 - y1 * a, y: y1 + x1 * a },
2713
+ { x: x2 + y2 * a, y: y2 - x2 * a },
2714
+ { x: x2, y: y2 }
2715
+ ];
2716
+ };
2717
+
2718
+ for (let i = 0; i < segments; i++) {
2719
+ let com = { type: type, values: [] };
2720
+ let curve = approxUnitArc(ang1, ang2, a, cos2, sin2);
2721
+
2722
+ curve.forEach((pt) => {
2723
+ let x = pt.x * rx;
2724
+ let y = pt.y * ry;
2725
+ com.values.push(cosphi * x - sinphi * y + centerx, sinphi * x + cosphi * y + centery);
2726
+ });
2727
+ pathDataArc.push(com);
2728
+ ang1 += ang2;
2729
+ }
2730
+
2731
+ return pathDataArc;
2732
+ }
2733
+
2734
+ /**
2735
+ * cubics to arcs
2736
+ */
2737
+
2738
+ function cubicCommandToArc(p0, cp1, cp2, p, tolerance = 7.5) {
2739
+
2740
+ let com = { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] };
2741
+
2742
+ let arcSegArea = 0, isArc = false;
2743
+
2744
+ // check angles
2745
+ let angle1 = getAngle(p0, cp1, true);
2746
+ let angle2 = getAngle(p, cp2, true);
2747
+ let deltaAngle = Math.abs(angle1 - angle2) * 180 / Math.PI;
2748
+
2749
+ let angleDiff = Math.abs((deltaAngle % 180) - 90);
2750
+ let isRightAngle = angleDiff < 3;
2751
+
2752
+ /*
2753
+ let cp1_r = rotatePoint(cp1, p0.x, p0.y, (Math.PI * -0.5))
2754
+ let cp2_r = rotatePoint(cp2, p.x, p.y, (Math.PI * 0.5))
2755
+
2756
+ // assumed centroid
2757
+ let ptC = checkLineIntersection(p0, cp1_r, p, cp2_r, false)
2758
+
2759
+ let dist0 = getSquareDistance(p0, p)
2760
+ let dist1 = getSquareDistance(p0, ptC)
2761
+ let dist2 = getSquareDistance(p, ptC)
2762
+
2763
+ // let mid point
2764
+ let ptM = pointAtT([p0, cp1, cp2, p], 0.5)
2765
+
2766
+ let diff1 = Math.abs(dist1 - dist2)
2767
+
2768
+ if (diff1 <= dist0 * 0.01) {
2769
+
2770
+ let r = Math.sqrt((dist1 + dist2) / 2)
2771
+
2772
+ let arcArea = getPolygonArea([p0, cp1, cp2, p])
2773
+ let sweep = arcArea < 0 ? 0 : 1;
2774
+
2775
+ // new arc command
2776
+ let comArc = { type: 'A', values: [r, r, 0, 0, sweep, p.x, p.y] };
2777
+
2778
+ isArc = true;
2779
+
2780
+ return { com: comArc, isArc, area: arcSegArea }
2781
+
2782
+ }
2783
+ */
2784
+
2785
+ if (isRightAngle) {
2786
+ // point between cps
2787
+
2788
+ let pI = checkLineIntersection(p0, cp1, p, cp2, false);
2789
+
2790
+ if (pI) {
2791
+
2792
+ let r1 = getDistance(p0, pI);
2793
+ let r2 = getDistance(p, pI);
2794
+
2795
+ let rMax = +Math.max(r1, r2).toFixed(8);
2796
+ let rMin = +Math.min(r1, r2).toFixed(8);
2797
+
2798
+ let rx = rMin;
2799
+ let ry = rMax;
2800
+
2801
+ let arcArea = getPolygonArea([p0, cp1, cp2, p]);
2802
+ let sweep = arcArea < 0 ? 0 : 1;
2803
+
2804
+ let w = Math.abs(p.x - p0.x);
2805
+ let h = Math.abs(p.y - p0.y);
2806
+ let landscape = w > h;
2807
+
2808
+ let circular = (100 / rx * Math.abs(rx - ry)) < 5;
2809
+
2810
+ if (circular) {
2811
+
2812
+ rx = rMax;
2813
+ ry = rx;
2814
+ }
2815
+
2816
+ if (landscape) {
2817
+
2818
+ rx = rMax;
2819
+ ry = rMin;
2820
+ }
2821
+
2822
+ // get original cubic area
2823
+ let comO = [
2824
+ { type: 'M', values: [p0.x, p0.y] },
2825
+ { type: 'C', values: [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y] }
2826
+ ];
2827
+
2828
+ let comArea = getPathArea(comO);
2829
+
2830
+ // new arc command
2831
+ let comArc = { type: 'A', values: [rx, ry, 0, 0, sweep, p.x, p.y] };
2832
+
2833
+ // calculate arc seg area
2834
+ arcSegArea = (Math.PI * (rx * ry)) / 4;
2835
+
2836
+ // subtract polygon between start, end and center point
2837
+ arcSegArea -= Math.abs(getPolygonArea([p0, p, pI]));
2838
+
2839
+ let areaDiff = getRelativeAreaDiff(comArea, arcSegArea);
2840
+
2841
+ if (areaDiff < tolerance) {
2842
+ isArc = true;
2843
+ com = comArc;
2844
+ }
2845
+
2846
+ }
2847
+ }
2848
+
2849
+ return { com: com, isArc, area: arcSegArea }
2850
+
2851
+ }
2852
+
2853
+ /**
2854
+ * combine adjacent arcs
2855
+ */
2856
+
2857
+ function combineArcs(pathData) {
2858
+
2859
+ let arcSeq = [[]];
2860
+ let ind = 0;
2861
+ let arcIndices = [[]];
2862
+ let p0 = { x: pathData[0].values[0], y: pathData[0].values[1] }, p;
2863
+
2864
+ for (let i = 0, len = pathData.length; i < len; i++) {
2865
+ let com = pathData[i];
2866
+ let { type, values } = com;
2867
+
2868
+ if (type === 'A') {
2869
+
2870
+ let comPrev = pathData[i - 1];
2871
+
2872
+ /**
2873
+ * previous p0 values might not be correct
2874
+ * anymore due to cubic simplification
2875
+ */
2876
+ let valsL = comPrev.values.slice(-2);
2877
+ p0 = { x: valsL[0], y: valsL[1] };
2878
+
2879
+ let [rx, ry, xAxisRotation, largeArc, sweep, x, y] = values;
2880
+
2881
+ // check if arc is circular
2882
+ let circular = (100 / rx * Math.abs(rx - ry)) < 5;
2883
+
2884
+ p = { x: values[5], y: values[6] };
2885
+ com.p0 = p0;
2886
+ com.p = p;
2887
+ com.circular = circular;
2888
+
2889
+ let comNext = pathData[i + 1];
2890
+
2891
+ if (!arcSeq[ind].length && comNext && comNext.type === 'A') {
2892
+ arcSeq[ind].push(com);
2893
+ arcIndices[ind].push(i);
2894
+ }
2895
+
2896
+ if (comNext && comNext.type === 'A') {
2897
+ let [rx1, ry1, xAxisRotation0, largeArc, sweep, x, y] = comNext.values;
2898
+ let diffRx = rx != rx1 ? 100 / rx * Math.abs(rx - rx1) : 0;
2899
+ let diffRy = ry != ry1 ? 100 / ry * Math.abs(ry - ry1) : 0;
2900
+
2901
+ p = { x: comNext.values[5], y: comNext.values[6] };
2902
+ comNext.p0 = p0;
2903
+ comNext.p = p;
2904
+
2905
+ // add if radii are almost same
2906
+ if (diffRx < 5 && diffRy < 5) {
2907
+
2908
+ arcSeq[ind].push(comNext);
2909
+ arcIndices[ind].push(i + 1);
2910
+ } else {
2911
+
2912
+ // start new segment
2913
+ arcSeq.push([]);
2914
+ arcIndices.push([]);
2915
+ ind++;
2916
+
2917
+ }
2918
+ }
2919
+
2920
+ else {
2921
+
2922
+ arcSeq.push([]);
2923
+ arcIndices.push([]);
2924
+ ind++;
2925
+ }
2926
+ }
2927
+ }
2928
+
2929
+ if (!arcIndices.length) return pathData;
2930
+
2931
+ arcSeq = arcSeq.filter(item => item.length);
2932
+ arcIndices = arcIndices.filter(item => item.length);
2933
+
2934
+ // Process in reverse to avoid index shifting
2935
+ for (let i = arcSeq.length - 1; i >= 0; i--) {
2936
+ const seq = arcSeq[i];
2937
+ const start = arcIndices[i][0];
2938
+ const len = seq.length;
2939
+
2940
+ // Average radii to prevent distortions
2941
+ let rxA = 0, ryA = 0;
2942
+ seq.forEach(({ values }) => {
2943
+ const [rx, ry] = values;
2944
+ rxA += rx;
2945
+ ryA += ry;
2946
+ });
2947
+ rxA /= len;
2948
+ ryA /= len;
2949
+
2950
+ // Correct near-circular arcs
2951
+
2952
+ // check if arc is circular
2953
+ let circular = (100 / rxA * Math.abs(rxA - ryA)) < 5;
2954
+
2955
+ if (circular) {
2956
+ // average radii
2957
+ rxA = (rxA + ryA) / 2;
2958
+ ryA = rxA;
2959
+ }
2960
+
2961
+ let comPrev = pathData[start - 1];
2962
+ let comPrevVals = comPrev.values.slice(-2);
2963
+ ({ type: 'M', values: [comPrevVals[0], comPrevVals[1]] });
2964
+
2965
+ if (len === 4) {
2966
+
2967
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[1].values;
2968
+ let [, , , , , x2, y2] = seq[3].values;
2969
+
2970
+ if (circular) {
2971
+
2972
+ // simplify radii
2973
+ rxA = 1;
2974
+ ryA = 1;
2975
+ }
2976
+
2977
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x1, y1] };
2978
+ let com2 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
2979
+
2980
+ // This now correctly replaces the original 4 arc commands with 2
2981
+ pathData.splice(start, len, com1, com2);
2982
+
2983
+ }
2984
+
2985
+ else if (len === 3) {
2986
+
2987
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
2988
+ let [rx2, ry2, , , , x2, y2] = seq[2].values;
2989
+
2990
+ // must be large arc
2991
+ largeArc = 1;
2992
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
2993
+
2994
+ // replace
2995
+ pathData.splice(start, len, com1);
2996
+
2997
+ }
2998
+
2999
+ else if (len === 2) {
3000
+
3001
+ let [rx, ry, xAxisRotation, largeArc, sweep, x1, y1] = seq[0].values;
3002
+ let [rx2, ry2, , , , x2, y2] = seq[1].values;
3003
+
3004
+ // if circular or non-elliptic xAxisRotation has no effect
3005
+ if (circular) {
3006
+ rxA = 1;
3007
+ ryA = 1;
3008
+ xAxisRotation = 0;
3009
+ }
3010
+
3011
+ // check if arc is already ideal
3012
+ let { p0, p } = seq[0];
3013
+ let [p0_1, p_1] = [seq[1].p0, seq[1].p];
3014
+
3015
+ if (p0.x !== p_1.x || p0.y !== p_1.y) {
3016
+
3017
+ let com1 = { type: 'A', values: [rxA, ryA, xAxisRotation, largeArc, sweep, x2, y2] };
3018
+
3019
+ // replace
3020
+ pathData.splice(start, len, com1);
3021
+ }
3022
+ }
3023
+
3024
+ else ;
3025
+ }
3026
+
3027
+ return pathData
3028
+ }
3029
+
3030
+ /**
3031
+ * parse normalized
3032
+ */
3033
+
3034
+ function normalizePathData(pathData = [],
3035
+ {
3036
+ toAbsolute = true,
3037
+ toLonghands = true,
3038
+ quadraticToCubic = false,
3039
+ arcToCubic = false,
3040
+ arcAccuracy = 2,
3041
+ } = {},
3042
+
3043
+ {
3044
+ hasRelatives = true, hasShorthands = true, hasQuadratics = true, hasArcs = true, testTypes = false
3045
+ } = {}
3046
+ ) {
3047
+
3048
+ // pathdata properties - test= true adds a manual test
3049
+ if (testTypes) {
3050
+
3051
+ let commands = Array.from(new Set(pathData.map(com => com.type))).join('');
3052
+ hasRelatives = /[lcqamts]/gi.test(commands);
3053
+ hasQuadratics = /[qt]/gi.test(commands);
3054
+ hasArcs = /[a]/gi.test(commands);
3055
+ hasShorthands = /[vhst]/gi.test(commands);
3056
+ isPoly = /[mlz]/gi.test(commands);
3057
+ }
3058
+
3059
+ /**
3060
+ * normalize:
3061
+ * convert to all absolute
3062
+ * all longhands
3063
+ */
3064
+
3065
+ if ((hasQuadratics && quadraticToCubic) || (hasArcs && arcToCubic)) {
3066
+ toLonghands = true;
3067
+ toAbsolute = true;
3068
+ }
3069
+
3070
+ if (hasRelatives && toAbsolute) pathData = pathDataToAbsoluteOrRelative(pathData, false);
3071
+ if (hasShorthands && toLonghands) pathData = pathDataToLonghands(pathData, -1, false);
3072
+ if (hasArcs && arcToCubic) pathData = pathDataArcsToCubics(pathData, arcAccuracy);
3073
+ if (hasQuadratics && quadraticToCubic) pathData = pathDataQuadraticToCubic(pathData);
3074
+
3075
+ return pathData;
3076
+
3077
+ }
3078
+
3079
+ function parsePathDataNormalized(d,
3080
+ {
3081
+ // necessary for most calculations
3082
+ toAbsolute = true,
3083
+ toLonghands = true,
3084
+
3085
+ // not necessary unless you need cubics only
3086
+ quadraticToCubic = false,
3087
+
3088
+ // mostly a fallback if arc calculations fail
3089
+ arcToCubic = false,
3090
+ // arc to cubic precision - adds more segments for better precision
3091
+ arcAccuracy = 4,
3092
+ } = {}
3093
+ ) {
3094
+
3095
+ let pathDataObj = parsePathDataString(d);
3096
+ let { hasRelatives, hasShorthands, hasQuadratics, hasArcs } = pathDataObj;
3097
+ let pathData = pathDataObj.pathData;
3098
+
3099
+ // normalize
3100
+ pathData = normalizePathData(pathData,
3101
+ { toAbsolute, toLonghands, quadraticToCubic, arcToCubic, arcAccuracy },
3102
+
3103
+ { hasRelatives, hasShorthands, hasQuadratics, hasArcs }
3104
+ );
3105
+
3106
+ return pathData;
3107
+ }
3108
+
3109
+ const commandSet = new Set([
3110
+ 0x4D, 0x6D, 0x41, 0x61, 0x43, 0x63,
3111
+ 0x4C, 0x6C, 0x51, 0x71, 0x53, 0x73,
3112
+ 0x54, 0x74, 0x48, 0x68, 0x56, 0x76,
3113
+ 0x5A, 0x7A
3114
+ ]);
3115
+
3116
+ const paramCountsArr = new Uint8Array(128);
3117
+ // M starting point
3118
+ paramCountsArr[0x4D] = 2;
3119
+ paramCountsArr[0x6D] = 2;
3120
+
3121
+ // A Arc
3122
+ paramCountsArr[0x41] = 7;
3123
+ paramCountsArr[0x61] = 7;
3124
+
3125
+ // C Cubic Bézier
3126
+ paramCountsArr[0x43] = 6;
3127
+ paramCountsArr[0x63] = 6;
3128
+
3129
+ // L Line To
3130
+ paramCountsArr[0x4C] = 2;
3131
+ paramCountsArr[0x6C] = 2;
3132
+
3133
+ // Q Quadratic Bézier
3134
+ paramCountsArr[0x51] = 4;
3135
+ paramCountsArr[0x71] = 4;
3136
+
3137
+ // S Smooth Cubic Bézier
3138
+ paramCountsArr[0x53] = 4;
3139
+ paramCountsArr[0x73] = 4;
3140
+
3141
+ // T Smooth Quadratic Bézier
3142
+ paramCountsArr[0x54] = 2;
3143
+ paramCountsArr[0x74] = 2;
3144
+
3145
+ // H Horizontal Line
3146
+ paramCountsArr[0x48] = 1;
3147
+ paramCountsArr[0x68] = 1;
3148
+
3149
+ // V Vertical Line
3150
+ paramCountsArr[0x56] = 1;
3151
+ paramCountsArr[0x76] = 1;
3152
+
3153
+ // Z Close Path
3154
+ paramCountsArr[0x5A] = 0;
3155
+ paramCountsArr[0x7A] = 0;
3156
+
3157
+ function parsePathDataString(d, debug = true) {
3158
+ d = d.trim();
3159
+
3160
+ if (d === '') {
3161
+ return {
3162
+ pathData: [],
3163
+ hasRelatives: false,
3164
+ hasShorthands: false,
3165
+ hasQuadratics: false,
3166
+ hasArcs: false
3167
+ }
3168
+ }
3169
+
3170
+ const SPECIAL_SPACES = new Set([
3171
+ 0x1680, 0x180E, 0x2000, 0x2001, 0x2002, 0x2003, 0x2004, 0x2005, 0x2006,
3172
+ 0x2007, 0x2008, 0x2009, 0x200A, 0x202F, 0x205F, 0x3000, 0xFEFF
3173
+ ]);
3174
+
3175
+ const isSpace = (ch) => {
3176
+ return (ch === 0x20) || (ch === 0x002C) || // White spaces or comma
3177
+ (ch === 0x0A) || (ch === 0x0D) || // nl cr
3178
+ (ch === 0x2028) || (ch === 0x2029) || // Line terminators
3179
+ (ch === 0x09) || (ch === 0x0B) || (ch === 0x0C) || (ch === 0xA0) ||
3180
+ (ch >= 0x1680 && SPECIAL_SPACES.has(ch));
3181
+ };
3182
+
3183
+ let i = 0, len = d.length;
3184
+ let lastCommand = "";
3185
+ let pathData = [];
3186
+ let itemCount = -1;
3187
+ let val = '';
3188
+ let wasE = false;
3189
+ let floatCount = 0;
3190
+ let valueIndex = 0;
3191
+ let maxParams = 0;
3192
+ let needsNewSegment = false;
3193
+ let foundCommands = new Set([]);
3194
+
3195
+ // collect errors
3196
+ let log = [];
3197
+ let feedback;
3198
+
3199
+ const addSeg = () => {
3200
+ // Create new segment if needed before adding the minus sign
3201
+ if (needsNewSegment) {
3202
+
3203
+ // sanitize implicit linetos
3204
+ if (lastCommand === 'M') lastCommand = 'L';
3205
+ else if (lastCommand === 'm') lastCommand = 'l';
3206
+
3207
+ pathData.push({ type: lastCommand, values: [] });
3208
+
3209
+ itemCount++;
3210
+ valueIndex = 0;
3211
+ needsNewSegment = false;
3212
+ }
3213
+ };
3214
+
3215
+ const pushVal = (checkFloats = false) => {
3216
+
3217
+ // regular value or float
3218
+ if (!checkFloats ? val !== '' : floatCount > 0) {
3219
+
3220
+ // error: no first command
3221
+ if (debug && itemCount === -1) {
3222
+
3223
+ feedback = 'Pathdata must start with M command';
3224
+ log.push(feedback);
3225
+
3226
+ // add M command to collect subsequent errors
3227
+ lastCommand = 'M';
3228
+ pathData.push({ type: lastCommand, values: [] });
3229
+ maxParams = 2;
3230
+ valueIndex = 0;
3231
+ itemCount++;
3232
+
3233
+ }
3234
+
3235
+ if (lastCommand === 'A' || lastCommand === 'a') {
3236
+ val = sanitizeArc();
3237
+
3238
+ pathData[itemCount].values.push(...val);
3239
+
3240
+ } else {
3241
+ // error: leading zeroes
3242
+ if (debug && val[1] && val[1] !== '.' && val[0] === '0') {
3243
+ feedback = `${itemCount}. command: Leading zeros not valid: ${val}`;
3244
+ log.push(feedback);
3245
+ }
3246
+ pathData[itemCount].values.push(+val);
3247
+ }
3248
+
3249
+ valueIndex++;
3250
+ val = '';
3251
+ floatCount = 0;
3252
+
3253
+ // Mark that a new segment is needed if maxParams is reached
3254
+ needsNewSegment = valueIndex >= maxParams;
3255
+
3256
+ }
3257
+ };
3258
+
3259
+ const sanitizeArc = () => {
3260
+
3261
+ let valLen = val.length;
3262
+ let arcSucks = false;
3263
+
3264
+ // large arc and sweep
3265
+ if (valueIndex === 3 && valLen === 2) {
3266
+
3267
+ val = [+val[0], +val[1]];
3268
+ arcSucks = true;
3269
+ valueIndex++;
3270
+ }
3271
+
3272
+ // sweep and final
3273
+ else if (valueIndex === 4 && valLen > 1) {
3274
+
3275
+ val = [+val[0], +val[1]];
3276
+ arcSucks = true;
3277
+ valueIndex++;
3278
+ }
3279
+
3280
+ // large arc, sweep and final pt combined
3281
+ else if (valueIndex === 3 && valLen >= 3) {
3282
+
3283
+ val = [+val[0], +val[1], +val.substring(2)];
3284
+ arcSucks = true;
3285
+ valueIndex += 2;
3286
+ }
3287
+
3288
+ return !arcSucks ? [+val] : val;
3289
+
3290
+ };
3291
+
3292
+ const validateCommand = () => {
3293
+
3294
+ if (itemCount > 0) {
3295
+ let lastCom = pathData[itemCount];
3296
+ let valLen = lastCom.values.length;
3297
+
3298
+ if ((valLen && valLen < maxParams) || (valLen && valLen > maxParams) || ((lastCommand === 'z' || lastCommand === 'Z') && valLen > 0)) {
3299
+ let diff = maxParams - valLen;
3300
+ feedback = `${itemCount}. command of type "${lastCommand}": ${diff} values too few - ${maxParams} expected`;
3301
+
3302
+ let prevFeedback = log[log.length - 1];
3303
+
3304
+ if (prevFeedback !== feedback) {
3305
+ log.push(feedback);
3306
+ }
3307
+ }
3308
+ }
3309
+ };
3310
+
3311
+ let isE = false;
3312
+ let isMinusorPlus = false;
3313
+ let isDot = false;
3314
+
3315
+ while (i < len) {
3316
+
3317
+ let charCode = d.charCodeAt(i);
3318
+
3319
+ let isDigit = (charCode > 47 && charCode < 58);
3320
+ if (!isDigit) {
3321
+ isE = (charCode === 101 || charCode === 69);
3322
+ isMinusorPlus = (charCode === 45 || charCode === 43);
3323
+ isDot = charCode === 46;
3324
+ }
3325
+
3326
+ /**
3327
+ * number related:
3328
+ * digit, e-notation, dot or -/+ operator
3329
+ */
3330
+
3331
+ if (
3332
+ isDigit ||
3333
+ isMinusorPlus ||
3334
+ isDot ||
3335
+ isE
3336
+ ) {
3337
+
3338
+ // minus or float/dot separated: 0x2D=hyphen; 0x2E=dot
3339
+ if (!wasE && (charCode === 0x2D || charCode === 0x2E)) {
3340
+
3341
+ // checkFloats changes condition for value adding
3342
+ let checkFloats = charCode === 0x2E;
3343
+
3344
+ // new val
3345
+ pushVal(checkFloats);
3346
+
3347
+ // new segment
3348
+ addSeg();
3349
+
3350
+ // concatenated floats
3351
+ if (checkFloats) {
3352
+ floatCount++;
3353
+ }
3354
+ }
3355
+
3356
+ // regular splitting
3357
+ else {
3358
+
3359
+ addSeg();
3360
+ }
3361
+
3362
+ val += d[i];
3363
+
3364
+ // e/scientific notation in value
3365
+ wasE = isE;
3366
+ i++;
3367
+ continue;
3368
+ }
3369
+
3370
+ /**
3371
+ * Separated by white space
3372
+ */
3373
+ if ((charCode < 48 || charCode > 5759) && isSpace(charCode)) {
3374
+
3375
+ // push value
3376
+ pushVal();
3377
+
3378
+ i++;
3379
+ continue;
3380
+ }
3381
+
3382
+ /**
3383
+ * New command introduced by
3384
+ * alphabetic A-Z character
3385
+ */
3386
+ if (charCode > 64) {
3387
+
3388
+ // is valid command
3389
+ let isValid = commandSet.has(charCode);
3390
+
3391
+ if (!isValid) {
3392
+ feedback = `${itemCount}. command "${d[i]}" is not a valid type`;
3393
+ log.push(feedback);
3394
+ i++;
3395
+ continue
3396
+ }
3397
+
3398
+ // command is concatenated without whitespace
3399
+ if (val !== '') {
3400
+ pathData[itemCount].values.push(+val);
3401
+ valueIndex++;
3402
+ val = '';
3403
+ }
3404
+
3405
+ // check if previous command was correctly closed
3406
+ if (debug) validateCommand();
3407
+
3408
+ lastCommand = d[i];
3409
+ maxParams = paramCountsArr[charCode];
3410
+ let isM = lastCommand === 'M' || lastCommand === 'm';
3411
+ let wasClosePath = itemCount > 0 && (pathData[itemCount].type === 'z' || pathData[itemCount].type === 'Z');
3412
+
3413
+ foundCommands.add(lastCommand);
3414
+
3415
+ // add omitted M command after Z
3416
+ if (wasClosePath && !isM) {
3417
+ pathData.push({ type: 'm', values: [0, 0] });
3418
+ itemCount++;
3419
+ }
3420
+
3421
+ // init new command
3422
+ pathData.push({ type: lastCommand, values: [] });
3423
+ itemCount++;
3424
+
3425
+ // reset counters
3426
+ floatCount = 0;
3427
+ valueIndex = 0;
3428
+ needsNewSegment = false;
3429
+
3430
+ i++;
3431
+ continue;
3432
+ }
3433
+
3434
+ // exceptions - prevent infinite loop
3435
+ if (!isDigit) {
3436
+ feedback = `${itemCount}. ${d[i]} is not a valid separarator or token`;
3437
+ log.push(feedback);
3438
+ val = '';
3439
+ }
3440
+
3441
+ i++;
3442
+
3443
+ }
3444
+
3445
+ // final value
3446
+ pushVal();
3447
+ if (debug) validateCommand();
3448
+
3449
+ // return error log
3450
+ if (debug && log.length) {
3451
+ feedback = 'Invalid path data:\n' + log.join('\n');
3452
+ if (debug === 'log') {
3453
+ console.log(feedback);
3454
+ } else {
3455
+ throw new Error(feedback)
3456
+ }
3457
+ }
3458
+
3459
+ pathData[0].type = 'M';
3460
+
3461
+ /**
3462
+ * check if absolute/relative or
3463
+ * shorthands are present
3464
+ * to specify if normalization is required
3465
+ */
3466
+
3467
+ let commands = Array.from(foundCommands).join('');
3468
+ let hasRelatives = /[lcqamts]/g.test(commands);
3469
+ let hasShorthands = /[vhst]/gi.test(commands);
3470
+ let hasArcs = /[a]/gi.test(commands);
3471
+ let hasQuadratics = /[qt]/gi.test(commands);
3472
+
3473
+ return {
3474
+ pathData,
3475
+ hasRelatives,
3476
+ hasShorthands,
3477
+ hasQuadratics,
3478
+ hasArcs
3479
+ }
3480
+
3481
+ }
3482
+
3483
+ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = true) {
3484
+
3485
+ let pathDataN = [pathData[0]];
3486
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3487
+ let p0 = M;
3488
+ let p = M;
3489
+ pathData[pathData.length - 1].type.toLowerCase() === 'z';
3490
+
3491
+ for (let c = 1, l = pathData.length; c < l; c++) {
3492
+ let comPrev = pathData[c - 1];
3493
+ let com = pathData[c];
3494
+ let comN = pathData[c + 1] || pathData[l - 1];
3495
+ let p1 = comN.type.toLowerCase() === 'z' ? M : { x: comN.values[comN.values.length - 2], y: comN.values[comN.values.length - 1] };
3496
+
3497
+ let { type, values } = com;
3498
+ let valsL = values.slice(-2);
3499
+ p = type !== 'Z' ? { x: valsL[0], y: valsL[1] } : M;
3500
+
3501
+ let area = getPolygonArea([p0, p, p1], true);
3502
+
3503
+ let distSquare = getSquareDistance(p0, p1);
3504
+ let distMax = distSquare / 100 * tolerance;
3505
+
3506
+ let isFlat = area < distMax;
3507
+ let isFlatBez = false;
3508
+
3509
+ if (!flatBezierToLinetos && type === 'C') isFlat = false;
3510
+
3511
+ // convert flat beziers to linetos
3512
+ if (flatBezierToLinetos && (type === 'C' || type === 'Q')) {
3513
+
3514
+ let cpts = type === 'C' ?
3515
+ [{ x: values[0], y: values[1] }, { x: values[2], y: values[3] }] :
3516
+ (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
3517
+
3518
+ isFlatBez = checkBezierFlatness(p0, cpts, p);
3519
+ // console.log();
3520
+
3521
+ if (isFlatBez && c < l - 1 && comPrev.type !== 'C') {
3522
+ type = "L";
3523
+ com.type = "L";
3524
+ com.values = valsL;
3525
+
3526
+ }
3527
+
3528
+ }
3529
+
3530
+ // update end point
3531
+ p0 = p;
3532
+
3533
+ // colinear – exclude arcs (as always =) as semicircles won't have an area
3534
+ if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
3535
+
3536
+ continue;
3537
+ }
3538
+
3539
+ if (type === 'M') {
3540
+ M = p;
3541
+ p0 = M;
3542
+ }
3543
+
3544
+ else if (type === 'Z') {
3545
+ p0 = M;
3546
+ }
3547
+
3548
+ // proceed and add command
3549
+ pathDataN.push(com);
3550
+
3551
+ }
3552
+
3553
+ return pathDataN;
3554
+
3555
+ }
3556
+
3557
+ function removeZeroLengthLinetos(pathData) {
3558
+
3559
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
3560
+ let p0 = M;
3561
+ let p = p0;
3562
+
3563
+ let pathDataN = [pathData[0]];
3564
+
3565
+ for (let c = 1, l = pathData.length; c < l; c++) {
3566
+ let com = pathData[c];
3567
+ let { type, values } = com;
3568
+
3569
+ let valsL = values.slice(-2);
3570
+ p = { x: valsL[0], y: valsL[1] };
3571
+
3572
+ // skip lineto
3573
+ if (type === 'L' && p.x === p0.x && p.y === p0.y) {
3574
+ continue
3575
+ }
3576
+
3577
+ pathDataN.push(com);
3578
+ p0 = p;
3579
+ }
3580
+
3581
+ return pathDataN
3582
+
3583
+ }
3584
+
3585
+ function pathDataToTopLeft(pathData) {
3586
+
3587
+ let len = pathData.length;
3588
+ let isClosed = pathData[len - 1].type.toLowerCase() === 'z';
3589
+
3590
+ // we can't change starting point for non closed paths
3591
+ if (!isClosed) {
3592
+ return pathData
3593
+ }
3594
+
3595
+ let newIndex = 0;
3596
+
3597
+ let indices = [];
3598
+ for (let i = 0; i < len; i++) {
3599
+ let com = pathData[i];
3600
+ let { type, values } = com;
3601
+ let valsLen = values.length;
3602
+ if (valsLen) {
3603
+ let p = { type: type, x: values[valsLen-2], y: values[valsLen-1], index: 0};
3604
+ p.index = i;
3605
+ indices.push(p);
3606
+ }
3607
+ }
3608
+
3609
+ // reorder to top left most
3610
+ indices = indices.sort((a, b) => +a.y.toFixed(3) - +b.y.toFixed(3) || a.x - b.x);
3611
+ newIndex = indices[0].index;
3612
+
3613
+ return newIndex ? shiftSvgStartingPoint(pathData, newIndex) : pathData;
3614
+ }
3615
+
3616
+ /**
3617
+ * shift starting point
3618
+ */
3619
+ function shiftSvgStartingPoint(pathData, offset) {
3620
+ let pathDataL = pathData.length;
3621
+ let newStartIndex = 0;
3622
+ let lastCommand = pathData[pathDataL - 1]["type"];
3623
+ let isClosed = lastCommand.toLowerCase() === "z";
3624
+
3625
+ if (!isClosed || offset < 1 || pathData.length < 3) {
3626
+ return pathData;
3627
+ }
3628
+
3629
+ let trimRight = isClosed ? 1 : 0;
3630
+
3631
+ // add explicit lineto
3632
+ addClosePathLineto(pathData);
3633
+
3634
+ // M start offset
3635
+ newStartIndex =
3636
+ offset + 1 < pathData.length - 1
3637
+ ? offset + 1
3638
+ : pathData.length - 1 - trimRight;
3639
+
3640
+ // slice array to reorder
3641
+ let pathDataStart = pathData.slice(newStartIndex);
3642
+ let pathDataEnd = pathData.slice(0, newStartIndex);
3643
+
3644
+ // remove original M
3645
+ pathDataEnd.shift();
3646
+ let pathDataEndL = pathDataEnd.length;
3647
+
3648
+ let pathDataEndLastValues, pathDataEndLastXY;
3649
+ pathDataEndLastValues = pathDataEnd[pathDataEndL - 1].values || [];
3650
+ pathDataEndLastXY = [
3651
+ pathDataEndLastValues[pathDataEndLastValues.length - 2],
3652
+ pathDataEndLastValues[pathDataEndLastValues.length - 1]
3653
+ ];
3654
+
3655
+ if (trimRight) {
3656
+ pathDataStart.pop();
3657
+ pathDataEnd.push({
3658
+ type: "Z",
3659
+ values: []
3660
+ });
3661
+ }
3662
+ // prepend new M command and concatenate array chunks
3663
+ pathData = [
3664
+ {
3665
+ type: "M",
3666
+ values: pathDataEndLastXY
3667
+ },
3668
+ ...pathDataStart,
3669
+ ...pathDataEnd,
3670
+ ];
3671
+
3672
+ return pathData;
3673
+ }
3674
+
3675
+ /**
3676
+ * Add closing lineto:
3677
+ * needed for path reversing or adding points
3678
+ */
3679
+
3680
+ function addClosePathLineto(pathData) {
3681
+ let pathDataL = pathData.length;
3682
+ let closed = pathData[pathDataL - 1].type.toLowerCase() === "z" ? true : false;
3683
+
3684
+ let M = pathData[0];
3685
+ let [x0, y0] = [M.values[0], M.values[1]].map(val => { return +val.toFixed(8) });
3686
+ let comLast = closed ? pathData[pathDataL - 2] : pathData[pathDataL - 1];
3687
+ let comLastL = comLast.values.length;
3688
+
3689
+ // last explicit on-path coordinates
3690
+ let [xL, yL] = [comLast.values[comLastL - 2], comLast.values[comLastL - 1]].map(val => { return +val.toFixed(8) });
3691
+
3692
+ if (closed && (x0 != xL || y0 != yL)) {
3693
+
3694
+ pathData.pop();
3695
+ pathData.push(
3696
+ {
3697
+ type: "L",
3698
+ values: [x0, y0]
3699
+ },
3700
+ {
3701
+ type: "Z",
3702
+ values: []
3703
+ }
3704
+ );
3705
+ }
3706
+
3707
+ return pathData;
3708
+ }
3709
+
3710
+ /**
3711
+ * reverse pathdata
3712
+ * make sure all command coordinates are absolute and
3713
+ * shorthands are converted to long notation
3714
+ */
3715
+ function reversePathData(pathData, {
3716
+ arcToCubic = false,
3717
+ quadraticToCubic = false,
3718
+ toClockwise = false,
3719
+ returnD = false
3720
+ } = {}) {
3721
+
3722
+ /**
3723
+ * Add closing lineto:
3724
+ * needed for path reversing or adding points
3725
+ */
3726
+ const addClosePathLineto = (pathData) => {
3727
+ let closed = pathData[pathData.length - 1].type.toLowerCase() === "z";
3728
+ let M = pathData[0];
3729
+ let [x0, y0] = [M.values[0], M.values[1]];
3730
+ let lastCom = closed ? pathData[pathData.length - 2] : pathData[pathData.length - 1];
3731
+ let [xE, yE] = [lastCom.values[lastCom.values.length - 2], lastCom.values[lastCom.values.length - 1]];
3732
+
3733
+ if (closed && (x0 != xE || y0 != yE)) {
3734
+
3735
+ pathData.pop();
3736
+ pathData.push(
3737
+ {
3738
+ type: "L",
3739
+ values: [x0, y0]
3740
+ },
3741
+ {
3742
+ type: "Z",
3743
+ values: []
3744
+ }
3745
+ );
3746
+ }
3747
+ return pathData;
3748
+ };
3749
+
3750
+ // helper to rearrange control points for all command types
3751
+ const reverseControlPoints = (type, values) => {
3752
+ let controlPoints = [];
3753
+ let endPoints = [];
3754
+ if (type !== "A") {
3755
+ for (let p = 0; p < values.length; p += 2) {
3756
+ controlPoints.push([values[p], values[p + 1]]);
3757
+ }
3758
+ endPoints = controlPoints.pop();
3759
+ controlPoints.reverse();
3760
+ }
3761
+ // is arc
3762
+ else {
3763
+
3764
+ let sweep = values[4] == 0 ? 1 : 0;
3765
+ controlPoints = [values[0], values[1], values[2], values[3], sweep];
3766
+ endPoints = [values[5], values[6]];
3767
+ }
3768
+ return { controlPoints, endPoints };
3769
+ };
3770
+
3771
+ // start compiling new path data
3772
+ let pathDataNew = [];
3773
+
3774
+ let closed =
3775
+ pathData[pathData.length - 1].type.toLowerCase() === "z" ? true : false;
3776
+ if (closed) {
3777
+ // add lineto closing space between Z and M
3778
+ pathData = addClosePathLineto(pathData);
3779
+ // remove Z closepath
3780
+ pathData.pop();
3781
+ }
3782
+
3783
+ // define last point as new M if path isn't closed
3784
+ let valuesLast = pathData[pathData.length - 1].values;
3785
+ let valuesLastL = valuesLast.length;
3786
+ let M = closed
3787
+ ? pathData[0]
3788
+ : {
3789
+ type: "M",
3790
+ values: [valuesLast[valuesLastL - 2], valuesLast[valuesLastL - 1]]
3791
+ };
3792
+ // starting M stays the same – unless the path is not closed
3793
+ pathDataNew.push(M);
3794
+
3795
+ // reverse path data command order for processing
3796
+ pathData.reverse();
3797
+ for (let i = 1; i < pathData.length; i++) {
3798
+ let com = pathData[i];
3799
+ let type = com.type;
3800
+ let values = com.values;
3801
+ let comPrev = pathData[i - 1];
3802
+ let typePrev = comPrev.type;
3803
+ let valuesPrev = comPrev.values;
3804
+
3805
+ // get reversed control points and new end coordinates
3806
+ let controlPointsPrev = reverseControlPoints(typePrev, valuesPrev).controlPoints;
3807
+ let endPoints = reverseControlPoints(type, values).endPoints;
3808
+
3809
+ // create new path data
3810
+ let newValues = [];
3811
+ newValues = [controlPointsPrev, endPoints].flat();
3812
+ pathDataNew.push({
3813
+ type: typePrev,
3814
+ values: newValues.flat()
3815
+ });
3816
+ }
3817
+
3818
+ // add previously removed Z close path
3819
+ if (closed) {
3820
+ pathDataNew.push({
3821
+ type: "z",
3822
+ values: []
3823
+ });
3824
+ }
3825
+
3826
+ return pathDataNew;
3827
+ }
3828
+
3829
+ function cleanUpSVG(svgMarkup, {
3830
+ returnDom=false,
3831
+ removeHidden=true,
3832
+ removeUnused=true,
3833
+ }={}) {
3834
+ svgMarkup = cleanSvgPrologue(svgMarkup);
3835
+
3836
+ // replace namespaced refs
3837
+ svgMarkup = svgMarkup.replaceAll("xlink:href=", "href=");
3838
+
3839
+ let svg = new DOMParser()
3840
+ .parseFromString(svgMarkup, "text/html")
3841
+ .querySelector("svg");
3842
+
3843
+
3844
+ let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
3845
+ removeExcludedAttribues(svg, allowed);
3846
+
3847
+ let removeEls = ['metadata', 'script'];
3848
+
3849
+ let els = svg.querySelectorAll('*');
3850
+ els.forEach(el=>{
3851
+ let name = el.nodeName;
3852
+ // remove hidden elements
3853
+ let style = el.getAttribute('style') || '';
3854
+ let isHiddenByStyle = style ? style.trim().includes('display:none') : false;
3855
+ let isHidden = (el.getAttribute('display') && el.getAttribute('display') === 'none') || isHiddenByStyle;
3856
+ if(name.includes(':') || removeEls.includes(name) || (removeHidden && isHidden )) {
3857
+ el.remove();
3858
+ }else {
3859
+ // remove BS elements
3860
+ removeNameSpaceAtts(el);
3861
+ }
3862
+ });
3863
+
3864
+ if(returnDom) return svg
3865
+
3866
+ let markup = stringifySVG(svg);
3867
+ console.log(markup);
3868
+
3869
+ return markup;
3870
+ }
3871
+
3872
+ function cleanSvgPrologue(svgString) {
3873
+ return (
3874
+ svgString
3875
+ // Remove XML prologues like <?xml ... ?>
3876
+ .replace(/<\?xml[\s\S]*?\?>/gi, "")
3877
+ // Remove DOCTYPE declarations
3878
+ .replace(/<!DOCTYPE[\s\S]*?>/gi, "")
3879
+ // Remove comments <!-- ... -->
3880
+ .replace(/<!--[\s\S]*?-->/g, "")
3881
+ // Trim extra whitespace
3882
+ .trim()
3883
+ );
3884
+ }
3885
+
3886
+ function removeExcludedAttribues(el, allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class']){
3887
+ let atts = [...el.attributes].map((att) => att.name);
3888
+ atts.forEach((att) => {
3889
+ if (!allowed.includes(att)) {
3890
+ el.removeAttribute(att);
3891
+ }
3892
+ });
3893
+ }
3894
+
3895
+ function removeNameSpaceAtts(el) {
3896
+ let atts = [...el.attributes].map((att) => att.name);
3897
+ atts.forEach((att) => {
3898
+ if (att.includes(":")) {
3899
+ el.removeAttribute(att);
3900
+ }
3901
+ });
3902
+ }
3903
+
3904
+ function stringifySVG(svg){
3905
+ let markup = new XMLSerializer().serializeToString(svg);
3906
+ markup = markup
3907
+ .replace(/\t/g, "")
3908
+ .replace(/[\n\r|]/g, "\n")
3909
+ .replace(/\n\s*\n/g, '\n')
3910
+ .replace(/ +/g, ' ');
3911
+
3912
+ return markup
3913
+ }
3914
+
3915
+ function svgPathSimplify(input = '', {
3916
+ toAbsolute = true,
3917
+ toRelative = true,
3918
+ toShorthands = true,
3919
+ decimals = 3,
3920
+
3921
+ // not necessary unless you need cubics only
3922
+ quadraticToCubic = true,
3923
+
3924
+ // mostly a fallback if arc calculations fail
3925
+ arcToCubic = false,
3926
+ cubicToArc = false,
3927
+
3928
+ // arc to cubic precision - adds more segments for better precision
3929
+ arcAccuracy = 4,
3930
+ keepExtremes = true,
3931
+ keepCorners = true,
3932
+ keepInflections = true,
3933
+ extrapolateDominant = false,
3934
+ addExtremes = false,
3935
+ optimizeOrder = true,
3936
+ removeColinear = true,
3937
+ simplifyBezier = true,
3938
+ autoAccuracy = true,
3939
+ flatBezierToLinetos = true,
3940
+ revertToQuadratics = true,
3941
+ minifyD = 0,
3942
+ tolerance = 1,
3943
+ reverse = false,
3944
+
3945
+ // svg cleanup options
3946
+ removeHidden = true,
3947
+ removeUnused = true,
3948
+
3949
+ // return svg markup or object
3950
+ getObject = false
3951
+
3952
+ } = {}) {
3953
+
3954
+ // clamp tolerance
3955
+ tolerance = Math.max(0.1, tolerance);
3956
+
3957
+ let inputType = detectInputType(input);
3958
+
3959
+ let svg = '';
3960
+ let svgSize = 0;
3961
+ let svgSizeOpt = 0;
3962
+ let compression = 0;
3963
+ let report = {};
3964
+ let d = '';
3965
+ let mode = inputType === 'svgMarkup' ? 1 : 0;
3966
+
3967
+ let paths = [];
3968
+
3969
+ /**
3970
+ * normalize input
3971
+ * switch mode
3972
+ */
3973
+
3974
+ // original size
3975
+ svgSize = new Blob([input]).size;
3976
+
3977
+ // single path
3978
+ if (!mode) {
3979
+ if (inputType === 'pathDataString') {
3980
+ d = input;
3981
+ } else if (inputType === 'polyString') {
3982
+ d = 'M' + input;
3983
+ }
3984
+ paths.push({ d, el: null });
3985
+ }
3986
+ // process svg
3987
+ else {
3988
+
3989
+ let returnDom = true;
3990
+ svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
3991
+ );
3992
+
3993
+ // collect paths
3994
+ let pathEls = svg.querySelectorAll('path');
3995
+ pathEls.forEach(path => {
3996
+ paths.push({ d: path.getAttribute('d'), el: path });
3997
+ });
3998
+ }
3999
+
4000
+ /**
4001
+ * process all paths
4002
+ */
4003
+ paths.forEach(path => {
4004
+ let { d, el } = path;
4005
+
4006
+ let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
4007
+
4008
+ // create clone for fallback
4009
+ let pathData = JSON.parse(JSON.stringify(pathDataO));
4010
+
4011
+ // count commands for evaluation
4012
+ let comCount = pathDataO.length;
4013
+
4014
+ /**
4015
+ * get sub paths
4016
+ */
4017
+ let subPathArr = splitSubpaths(pathData);
4018
+
4019
+ // cleaned up pathData
4020
+ let pathDataArrN = [];
4021
+
4022
+ for (let i = 0, l = subPathArr.length; i < l; i++) {
4023
+
4024
+ let pathDataSub = subPathArr[i];
4025
+
4026
+ // try simplification in reversed order
4027
+ if (reverse) pathDataSub = reversePathData(pathDataSub);
4028
+
4029
+ // remove zero length linetos
4030
+ if (removeColinear) pathDataSub = removeZeroLengthLinetos(pathDataSub);
4031
+
4032
+ // add extremes
4033
+
4034
+ let tMin = 0, tMax = 1;
4035
+ if (addExtremes) pathDataSub = addExtremePoints(pathDataSub, tMin, tMax);
4036
+
4037
+ // sort to top left
4038
+ if (optimizeOrder) pathDataSub = pathDataToTopLeft(pathDataSub);
4039
+
4040
+ // remove colinear/flat
4041
+ if (removeColinear) pathDataSub = pathDataRemoveColinear(pathDataSub, tolerance, flatBezierToLinetos);
4042
+
4043
+ // analyze pathdata to add info about signicant properties such as extremes, corners
4044
+ let pathDataPlus = analyzePathData(pathDataSub);
4045
+
4046
+ // simplify beziers
4047
+ let { pathData, bb, dimA } = pathDataPlus;
4048
+
4049
+ pathData = simplifyBezier ? simplifyPathData(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4050
+
4051
+ // cubic to arcs
4052
+ if (cubicToArc) {
4053
+
4054
+ let thresh = 3;
4055
+
4056
+ pathData.forEach((com, c) => {
4057
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4058
+ if (type === 'C') {
4059
+
4060
+ let comA = cubicCommandToArc(p0, cp1, cp2, p, thresh);
4061
+ if (comA.isArc) pathData[c] = comA.com;
4062
+
4063
+ }
4064
+ });
4065
+
4066
+ // combine adjacent cubics
4067
+ pathData = combineArcs(pathData);
4068
+
4069
+ }
4070
+
4071
+ // simplify to quadratics
4072
+ if (revertToQuadratics) {
4073
+ pathData.forEach((com, c) => {
4074
+ let { type, values, p0, cp1 = null, cp2 = null, p = null } = com;
4075
+ if (type === 'C') {
4076
+
4077
+ let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
4078
+ if (comQ.type === 'Q') pathData[c] = comQ;
4079
+ }
4080
+ });
4081
+ }
4082
+
4083
+ // update
4084
+ pathDataArrN.push(pathData);
4085
+ }
4086
+
4087
+ // flatten compound paths
4088
+ pathData = pathDataArrN.flat();
4089
+
4090
+ /**
4091
+ * detect accuracy
4092
+ */
4093
+ if (autoAccuracy) {
4094
+ decimals = detectAccuracy(pathData);
4095
+ }
4096
+
4097
+ // optimize
4098
+ let pathOptions = {
4099
+ toRelative,
4100
+ toShorthands,
4101
+ decimals,
4102
+ };
4103
+
4104
+ // optimize path data
4105
+ pathData = convertPathData(pathData, pathOptions);
4106
+
4107
+ // remove zero-length segments introduced by rounding
4108
+ let pathDataOpt = [];
4109
+
4110
+ pathData.forEach((com, i) => {
4111
+ let { type, values } = com;
4112
+ if (type === 'l' || type === 'v' || type === 'h') {
4113
+ let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0;
4114
+ if (hasLength) pathDataOpt.push(com);
4115
+ } else {
4116
+ pathDataOpt.push(com);
4117
+ }
4118
+ });
4119
+
4120
+ pathData = pathDataOpt;
4121
+
4122
+ // compare command count
4123
+ let comCountS = pathData.length;
4124
+
4125
+ let dOpt = pathDataToD(pathData, minifyD);
4126
+ svgSizeOpt = new Blob([dOpt]).size;
4127
+
4128
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4129
+
4130
+ path.d = dOpt;
4131
+ path.report = {
4132
+ original: comCount,
4133
+ new: comCountS,
4134
+ saved: comCount - comCountS,
4135
+ compression,
4136
+ decimals,
4137
+
4138
+ };
4139
+
4140
+ // apply new path for svgs
4141
+ if (el) el.setAttribute('d', dOpt);
4142
+
4143
+ });
4144
+
4145
+ // stringify new SVG
4146
+ if (mode) {
4147
+ svg = new XMLSerializer().serializeToString(svg);
4148
+ svgSizeOpt = new Blob([svg]).size;
4149
+
4150
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4151
+
4152
+ svgSize = +(svgSize / 1024).toFixed(3);
4153
+ svgSizeOpt = +(svgSizeOpt / 1024).toFixed(3);
4154
+
4155
+ report = {
4156
+ svgSize,
4157
+ svgSizeOpt,
4158
+ compression
4159
+ };
4160
+
4161
+ } else {
4162
+ ({ d, report } = paths[0]);
4163
+ }
4164
+
4165
+ return !getObject ? (d ? d : svg) : { svg, d, report, inputType, mode };
4166
+
4167
+ }
4168
+
4169
+ function simplifyPathData(pathData, {
4170
+ keepExtremes = true,
4171
+ keepInflections = true,
4172
+ keepCorners = true,
4173
+ extrapolateDominant = true,
4174
+ tolerance = 1,
4175
+ reverse = false
4176
+ } = {}) {
4177
+
4178
+ let pathDataN = [pathData[0]];
4179
+
4180
+ for (let i = 2, l = pathData.length; l && i <= l; i++) {
4181
+ let com = pathData[i - 1];
4182
+ let comN = i < l ? pathData[i] : null;
4183
+ let typeN = comN?.type || null;
4184
+
4185
+ let isDirChange = com?.directionChange || null;
4186
+ let isDirChangeN = comN?.directionChange || null;
4187
+
4188
+ let { type, values, p0, p, cp1 = null, cp2 = null, extreme = false, corner = false, dimA = 0 } = com;
4189
+
4190
+ // next is also cubic
4191
+ if (type === 'C' && typeN === 'C') {
4192
+
4193
+ // cannot be combined as crossing extremes or corners
4194
+ if (
4195
+ (keepInflections && isDirChangeN) ||
4196
+ (keepCorners && corner) ||
4197
+ (!isDirChange && keepExtremes && extreme)
4198
+ ) {
4199
+
4200
+ pathDataN.push(com);
4201
+ }
4202
+
4203
+ // try simplification
4204
+ else {
4205
+
4206
+ let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
4207
+ let error = 0;
4208
+
4209
+ // combining successful! try next segment
4210
+ if (combined.length === 1) {
4211
+ com = combined[0];
4212
+ let offset = 1;
4213
+ error += com.error;
4214
+
4215
+ // find next candidates
4216
+ for (let n = i + 1; error < tolerance && n < l; n++) {
4217
+ let comN = pathData[n];
4218
+ if (comN.type !== 'C' ||
4219
+ (
4220
+ (keepInflections && comN.directionChange) ||
4221
+ (keepCorners && com.corner) ||
4222
+ (keepExtremes && com.extreme)
4223
+ )
4224
+ ) {
4225
+ break
4226
+ }
4227
+
4228
+ let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
4229
+ if (combined.length === 1) {
4230
+ offset++;
4231
+ }
4232
+ com = combined[0];
4233
+ }
4234
+
4235
+ pathDataN.push(com);
4236
+
4237
+ if (i < l) {
4238
+ i += offset;
4239
+ }
4240
+
4241
+ } else {
4242
+ pathDataN.push(com);
4243
+ }
4244
+ }
4245
+
4246
+ } // end of bezier command
4247
+
4248
+ // other commands
4249
+ else {
4250
+ pathDataN.push(com);
4251
+ }
4252
+
4253
+ } // end command loop
4254
+
4255
+ // reverse back
4256
+ if (reverse) pathDataN = reversePathData(pathDataN);
4257
+
4258
+ return pathDataN
4259
+ }
4260
+
4261
+ /**
4262
+ * get viewBox
4263
+ * either from explicit attribute or
4264
+ * width and height attributes
4265
+ */
4266
+
4267
+ function getViewBox(svg = null, round = false) {
4268
+
4269
+ // browser default
4270
+ if (!svg) return { x: 0, y: 0, width: 300, height: 150 }
4271
+
4272
+ let style = window.getComputedStyle(svg);
4273
+
4274
+ // the baseVal API method also converts physical units to pixels/user-units
4275
+ let w = svg.hasAttribute('width') ? svg.width.baseVal.value : parseFloat(style.width) || 300;
4276
+ let h = svg.hasAttribute('height') ? svg.height.baseVal.value : parseFloat(style.height) || 150;
4277
+
4278
+ let viewBox = svg.getAttribute('viewBox') ? svg.viewBox.baseVal : { x: 0, y: 0, width: w, height: h };
4279
+
4280
+ // remove SVG constructor
4281
+ let { x, y, width, height } = viewBox;
4282
+ viewBox = { x, y, width, height };
4283
+
4284
+ // round to integers
4285
+ if (round) {
4286
+ for (let prop in viewBox) {
4287
+ viewBox[prop] = Math.ceil(viewBox[prop]);
4288
+ }
4289
+ }
4290
+
4291
+ return viewBox
4292
+ }
4293
+
4294
+ const {
4295
+ abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
4296
+ log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
4297
+ } = Math;
4298
+
4299
+ // just for visual debugging
4300
+
4301
+ // IIFE
4302
+ if (typeof window !== 'undefined') {
4303
+ window.svgPathSimplify = svgPathSimplify;
4304
+ window.getViewBox = getViewBox;
4305
+ window.renderPoint = renderPoint;
4306
+ }
4307
+
4308
+ export { PI, abs, acos, asin, atan, atan2, ceil, cos, exp, floor, getViewBox, hypot, log, max, min, pow, random, round, sin, sqrt, svgPathSimplify, tan };