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