svg-path-simplify 0.0.5 → 0.0.8

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.
@@ -25,14 +25,28 @@ function renderPoint(
25
25
  }
26
26
  }
27
27
 
28
+ function renderPath(svg, d = '', stroke = 'green', strokeWidth = '1%', render = true) {
29
+
30
+ let path = `<path d="${d}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth}" /> `;
31
+
32
+ if (render) {
33
+ svg.insertAdjacentHTML("beforeend", path);
34
+ } else {
35
+ return path;
36
+ }
37
+
38
+ }
39
+
28
40
  function detectInputType(input) {
29
41
  let type = 'string';
42
+ /*
30
43
  if (input instanceof HTMLImageElement) return "img";
31
44
  if (input instanceof SVGElement) return "svg";
32
45
  if (input instanceof HTMLCanvasElement) return "canvas";
33
46
  if (input instanceof File) return "file";
34
47
  if (input instanceof ArrayBuffer) return "buffer";
35
48
  if (input instanceof Blob) return "blob";
49
+ */
36
50
  if (Array.isArray(input)) return "array";
37
51
 
38
52
  if (typeof input === "string") {
@@ -90,19 +104,24 @@ function getAngle(p1, p2, normalize = false) {
90
104
  * http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
91
105
  */
92
106
 
93
- function checkLineIntersection(p1, p2, p3, p4, exact = true) {
107
+ function checkLineIntersection(p1=null, p2=null, p3=null, p4=null, exact = true, debug=false) {
94
108
  // if the lines intersect, the result contains the x and y of the intersection (treating the lines as infinite) and booleans for whether line segment 1 or line segment 2 contain the point
95
109
  let denominator, a, b, numerator1, numerator2;
96
110
  let intersectionPoint = {};
97
111
 
112
+ if(!p1 || !p2 || !p3 || !p4){
113
+ if(debug) console.warn('points missing');
114
+ return false
115
+ }
116
+
98
117
  try {
99
118
  denominator = ((p4.y - p3.y) * (p2.x - p1.x)) - ((p4.x - p3.x) * (p2.y - p1.y));
100
119
  if (denominator == 0) {
101
120
  return false;
102
121
  }
103
-
104
122
  } catch {
105
- console.log('!catch', p1, p2, 'p3:', p3, p4);
123
+ if(debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
124
+ return false
106
125
  }
107
126
 
108
127
  a = p1.y - p3.y;
@@ -119,8 +138,6 @@ function checkLineIntersection(p1, p2, p3, p4, exact = true) {
119
138
  y: p1.y + (a * (p2.y - p1.y))
120
139
  };
121
140
 
122
- // console.log('intersectionPoint', intersectionPoint, p1, p2);
123
-
124
141
  let intersection = false;
125
142
  // if line1 is a segment and line2 is infinite, they intersect if:
126
143
  if ((a > 0 && a < 1) && (b > 0 && b < 1)) {
@@ -520,6 +537,110 @@ function getBezierExtremeT(pts) {
520
537
  return tArr;
521
538
  }
522
539
 
540
+ /**
541
+ * based on Nikos M.'s answer
542
+ * how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
543
+ * https://stackoverflow.com/questions/87734/#75031511
544
+ * See also: https://github.com/foo123/Geometrize
545
+ */
546
+ function getArcExtemes(p0, values) {
547
+ // compute point on ellipse from angle around ellipse (theta)
548
+ const arc = (theta, cx, cy, rx, ry, alpha) => {
549
+ // theta is angle in radians around arc
550
+ // alpha is angle of rotation of ellipse in radians
551
+ var cos = Math.cos(alpha),
552
+ sin = Math.sin(alpha),
553
+ x = rx * Math.cos(theta),
554
+ y = ry * Math.sin(theta);
555
+
556
+ return {
557
+ x: cx + cos * x - sin * y,
558
+ y: cy + sin * x + cos * y
559
+ };
560
+ };
561
+
562
+ let arcData = svgArcToCenterParam(p0.x, p0.y, values[0], values[1], values[2], values[3], values[4], values[5], values[6]);
563
+ let { rx, ry, cx, cy, endAngle, deltaAngle } = arcData;
564
+
565
+ // arc rotation
566
+ let deg = values[2];
567
+
568
+ // final on path point
569
+ let p = { x: values[5], y: values[6] };
570
+
571
+ // collect extreme points – add end point
572
+ let extremes = [p];
573
+
574
+ // rotation to radians
575
+ let alpha = deg * Math.PI / 180;
576
+ let tan = Math.tan(alpha),
577
+ p1, p2, p3, p4, theta;
578
+
579
+ /**
580
+ * find min/max from zeroes of directional derivative along x and y
581
+ * along x axis
582
+ */
583
+ theta = Math.atan2(-ry * tan, rx);
584
+
585
+ let angle1 = theta;
586
+ let angle2 = theta + Math.PI;
587
+ let angle3 = Math.atan2(ry, rx * tan);
588
+ let angle4 = angle3 + Math.PI;
589
+
590
+ // inner bounding box
591
+ let xArr = [p0.x, p.x];
592
+ let yArr = [p0.y, p.y];
593
+ let xMin = Math.min(...xArr);
594
+ let xMax = Math.max(...xArr);
595
+ let yMin = Math.min(...yArr);
596
+ let yMax = Math.max(...yArr);
597
+
598
+ // on path point close after start
599
+ let angleAfterStart = endAngle - deltaAngle * 0.001;
600
+ let pP2 = arc(angleAfterStart, cx, cy, rx, ry, alpha);
601
+
602
+ // on path point close before end
603
+ let angleBeforeEnd = endAngle - deltaAngle * 0.999;
604
+ let pP3 = arc(angleBeforeEnd, cx, cy, rx, ry, alpha);
605
+
606
+ /**
607
+ * expected extremes
608
+ * if leaving inner bounding box
609
+ * (between segment start and end point)
610
+ * otherwise exclude elliptic extreme points
611
+ */
612
+
613
+ // right
614
+ if (pP2.x > xMax || pP3.x > xMax) {
615
+ // get point for this theta
616
+ p1 = arc(angle1, cx, cy, rx, ry, alpha);
617
+ extremes.push(p1);
618
+ }
619
+
620
+ // left
621
+ if (pP2.x < xMin || pP3.x < xMin) {
622
+ // get anti-symmetric point
623
+ p2 = arc(angle2, cx, cy, rx, ry, alpha);
624
+ extremes.push(p2);
625
+ }
626
+
627
+ // top
628
+ if (pP2.y < yMin || pP3.y < yMin) {
629
+ // get anti-symmetric point
630
+ p4 = arc(angle4, cx, cy, rx, ry, alpha);
631
+ extremes.push(p4);
632
+ }
633
+
634
+ // bottom
635
+ if (pP2.y > yMax || pP3.y > yMax) {
636
+ // get point for this theta
637
+ p3 = arc(angle3, cx, cy, rx, ry, alpha);
638
+ extremes.push(p3);
639
+ }
640
+
641
+ return extremes;
642
+ }
643
+
523
644
  // cubic bezier.
524
645
  function cubicBezierExtremeT(p0, cp1, cp2, p) {
525
646
  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];
@@ -628,90 +749,6 @@ function quadraticBezierExtremeT(p0, cp1, p) {
628
749
  return extemeT
629
750
  }
630
751
 
631
- function commandIsFlat(points, tolerance = 0.025) {
632
-
633
- let p0 = points[0];
634
- let p = points[points.length - 1];
635
-
636
- let xArr = points.map(pt => { return pt.x });
637
- let yArr = points.map(pt => { return pt.y });
638
-
639
- let xMin = Math.min(...xArr);
640
- let xMax = Math.max(...xArr);
641
- let yMin = Math.min(...yArr);
642
- let yMax = Math.max(...yArr);
643
- let w = xMax - xMin;
644
- let h = yMax - yMin;
645
-
646
- if (points.length < 3 || (w === 0 || h === 0)) {
647
- return { area: 0, flat: true, thresh: 0.0001, ratio: 0 };
648
- }
649
-
650
- let squareDist = getSquareDistance(p0, p);
651
- let squareDist1 = getSquareDistance(p0, points[0]);
652
- let squareDist2 = points.length > 3 ? getSquareDistance(p, points[1]) : squareDist1;
653
- let squareDistAvg = (squareDist1 + squareDist2) / 2;
654
-
655
- tolerance = 0.5;
656
- let thresh = (w + h) * 0.5 * tolerance;
657
-
658
- let area = 0;
659
- for (let i = 0, l = points.length; i < l; i++) {
660
- let addX = points[i].x;
661
- let addY = points[i === points.length - 1 ? 0 : i + 1].y;
662
- let subX = points[i === points.length - 1 ? 0 : i + 1].x;
663
- let subY = points[i].y;
664
- area += addX * addY * 0.5 - subX * subY * 0.5;
665
- }
666
-
667
- area = +Math.abs(area).toFixed(9);
668
- let areaThresh = 1000;
669
-
670
- let ratio = area / (squareDistAvg);
671
-
672
- let isFlat = area === 0 ? true : area < squareDistAvg / areaThresh;
673
-
674
- return { area: area, flat: isFlat, thresh: thresh, ratio: ratio, squareDist: squareDist, areaThresh: squareDist / areaThresh };
675
- }
676
-
677
- function checkBezierFlatness(p0, cpts, p) {
678
-
679
- let isFlat = false;
680
-
681
- let isCubic = cpts.length===2;
682
-
683
- let cp1 = cpts[0];
684
- let cp2 = isCubic ? cpts[1] : cp1;
685
-
686
- if(p0.x===cp1.x && p0.y===cp1.y && p.x===cp2.x && p.y===cp2.y) return true;
687
-
688
- let dx1 = cp1.x - p0.x;
689
- let dy1 = cp1.y - p0.y;
690
-
691
- let dx2 = p.x - cp2.x;
692
- let dy2 = p.y - cp2.y;
693
-
694
- let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
695
-
696
- if(!cross1) return true
697
-
698
- let dx0 = p.x - p0.x;
699
- let dy0 = p.y - p0.y;
700
- let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
701
-
702
- if(!cross0) return true
703
-
704
- let rat = (cross0/cross1);
705
-
706
- if (rat<1.1 ) {
707
-
708
- isFlat = true;
709
- }
710
-
711
- return isFlat;
712
-
713
- }
714
-
715
752
  /**
716
753
  * sloppy distance calculation
717
754
  * based on x/y differences
@@ -882,6 +919,7 @@ function addExtemesToCommand(p0, values, tMin=0, tMax=1) {
882
919
 
883
920
  if(tArr.length){
884
921
  let commandsSplit = splitCommandAtTValues(p0, values, tArr);
922
+
885
923
  pathDataNew.push(...commandsSplit);
886
924
  extremeCount += commandsSplit.length;
887
925
  }else {
@@ -1178,6 +1216,73 @@ function getPathDataPoly(pathData) {
1178
1216
  return poly;
1179
1217
  }
1180
1218
 
1219
+ /**
1220
+ * get exact path BBox
1221
+ * calculating extremes for all command types
1222
+ */
1223
+
1224
+ function getPathDataBBox(pathData) {
1225
+
1226
+ // save extreme values
1227
+ let xMin = Infinity;
1228
+ let xMax = -Infinity;
1229
+ let yMin = Infinity;
1230
+ let yMax = -Infinity;
1231
+
1232
+ const setXYmaxMin = (pt) => {
1233
+ if (pt.x < xMin) {
1234
+ xMin = pt.x;
1235
+ }
1236
+ if (pt.x > xMax) {
1237
+ xMax = pt.x;
1238
+ }
1239
+ if (pt.y < yMin) {
1240
+ yMin = pt.y;
1241
+ }
1242
+ if (pt.y > yMax) {
1243
+ yMax = pt.y;
1244
+ }
1245
+ };
1246
+
1247
+ for (let i = 0; i < pathData.length; i++) {
1248
+ let com = pathData[i];
1249
+ let { type, values } = com;
1250
+ let valuesL = values.length;
1251
+ let comPrev = pathData[i - 1] ? pathData[i - 1] : pathData[i];
1252
+ let valuesPrev = comPrev.values;
1253
+ let valuesPrevL = valuesPrev.length;
1254
+
1255
+ if (valuesL) {
1256
+ let p0 = { x: valuesPrev[valuesPrevL - 2], y: valuesPrev[valuesPrevL - 1] };
1257
+ let p = { x: values[valuesL - 2], y: values[valuesL - 1] };
1258
+ // add final on path point
1259
+ setXYmaxMin(p);
1260
+
1261
+ if (type === 'C' || type === 'Q') {
1262
+ let cp1 = { x: values[0], y: values[1] };
1263
+ let cp2 = type === 'C' ? { x: values[2], y: values[3] } : cp1;
1264
+ let pts = type === 'C' ? [p0, cp1, cp2, p] : [p0, cp1, p];
1265
+
1266
+ let bezierExtremesT = getBezierExtremeT(pts);
1267
+ bezierExtremesT.forEach(t => {
1268
+ let pt = pointAtT(pts, t);
1269
+ setXYmaxMin(pt);
1270
+ });
1271
+ }
1272
+
1273
+ else if (type === 'A') {
1274
+ let arcExtremes = getArcExtemes(p0, values);
1275
+ arcExtremes.forEach(pt => {
1276
+ setXYmaxMin(pt);
1277
+ });
1278
+ }
1279
+ }
1280
+ }
1281
+
1282
+ let bbox = { x: xMin, y: yMin, width: xMax - xMin, height: yMax - yMin };
1283
+ return bbox
1284
+ }
1285
+
1181
1286
  /**
1182
1287
  * get pathdata area
1183
1288
  */
@@ -1453,7 +1558,7 @@ function pathDataToD(pathData, optimize = 0) {
1453
1558
  return d;
1454
1559
  }
1455
1560
 
1456
- function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1561
+ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
1457
1562
 
1458
1563
  // cubic Bézier derivative
1459
1564
  const cubicDerivative = (p0, p1, p2, p3, t) => {
@@ -1475,8 +1580,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1475
1580
  let commands = [com1, com2];
1476
1581
 
1477
1582
  // detect dominant
1478
- let dist1 = getSquareDistance(com1.p0, com1.p);
1479
- let dist2 = getSquareDistance(com2.p0, com2.p);
1583
+ let dist1 = getDistAv(com1.p0, com1.p);
1584
+ let dist2 = getDistAv(com2.p0, com2.p);
1585
+
1480
1586
  let reverse = dist1 > dist2;
1481
1587
 
1482
1588
  // backup original commands
@@ -1538,7 +1644,6 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1538
1644
  t0 -= dot(r, dP) / dot(dP, dP);
1539
1645
 
1540
1646
  // construct merged cubic over [t0, 1]
1541
-
1542
1647
  let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
1543
1648
  let Q3 = com2.p;
1544
1649
 
@@ -1566,7 +1671,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1566
1671
  };
1567
1672
  }
1568
1673
 
1569
- let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], 0.5, false, true);
1674
+ let tMid = (1 - t0) * 0.5;
1675
+
1676
+ let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true);
1570
1677
  let seg1_cp2 = ptM.cpts[2];
1571
1678
 
1572
1679
  let ptI_1 = checkLineIntersection(ptM, seg1_cp2, result.p0, ptI, false);
@@ -1576,17 +1683,18 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1576
1683
  let cp2_2 = interpolate(result.p, ptI_2, 1.333);
1577
1684
 
1578
1685
  // test self intersections and exit
1579
- let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true );
1580
- if(cp_intersection){
1686
+ let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true);
1687
+ if (cp_intersection) {
1581
1688
 
1582
1689
  return commands;
1583
1690
  }
1584
1691
 
1692
+ if (debug) renderPoint(markers, ptM, 'purple');
1693
+
1585
1694
  result.cp1 = cp1_2;
1586
1695
  result.cp2 = cp2_2;
1587
1696
 
1588
- // check distances
1589
-
1697
+ // check distances between original starting point and extrapolated
1590
1698
  let dist3 = getDistAv(com1_o.p0, result.p0);
1591
1699
  let dist4 = getDistAv(com2_o.p, result.p);
1592
1700
  let dist5 = (dist3 + dist4);
@@ -1598,11 +1706,24 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1598
1706
  result.corner = com2_o.corner;
1599
1707
  result.dimA = com2_o.dimA;
1600
1708
  result.directionChange = com2_o.directionChange;
1709
+ result.type = 'C';
1601
1710
  result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
1602
1711
 
1603
- // check if completely off
1712
+ // extrapolated starting point is not completely off
1604
1713
  if (dist5 < maxDist) {
1605
1714
 
1715
+ // split t to meet original mid segment start point
1716
+ let tSplit = reverse ? 1 + t0 : Math.abs(t0);
1717
+
1718
+ let ptSplit = pointAtT([result.p0, result.cp1, result.cp2, result.p], tSplit);
1719
+ let distSplit = getDistAv(ptSplit, com1.p);
1720
+
1721
+ // not close enough - exit
1722
+ if (distSplit > maxDist * tolerance) {
1723
+
1724
+ return commands;
1725
+ }
1726
+
1606
1727
  // compare combined with original area
1607
1728
  let pathData0 = [
1608
1729
  { type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
@@ -1619,16 +1740,18 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
1619
1740
  let areaN = getPathArea(pathDataN);
1620
1741
  let areaDiff = Math.abs(areaN / area0 - 1);
1621
1742
 
1622
- result.error = areaDiff * 10 * tolerance;
1743
+ result.error = areaDiff * 5 * tolerance;
1623
1744
 
1624
- pathDataToD(pathDataN);
1745
+ if (debug) {
1746
+ let d = pathDataToD(pathDataN);
1747
+ renderPath(markers, d, 'orange');
1748
+ }
1625
1749
 
1626
- // success
1627
- if (areaDiff < 0.01) {
1750
+ // success!!!
1751
+ if (areaDiff < 0.05 * tolerance) {
1628
1752
  commands = [result];
1629
1753
 
1630
- }
1631
-
1754
+ }
1632
1755
  }
1633
1756
 
1634
1757
  return commands
@@ -1712,14 +1835,6 @@ function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance =
1712
1835
  } // end 1st try
1713
1836
 
1714
1837
 
1715
- /*
1716
- if (extrapolateDominant && com2.extreme) {
1717
- renderPoint(markers, com2.p)
1718
-
1719
- }
1720
- */
1721
-
1722
-
1723
1838
 
1724
1839
  // try extrapolated dominant curve
1725
1840
 
@@ -1788,25 +1903,59 @@ function getExtrapolatedCommand(com1, com2, t1 = 0, t2 = 0) {
1788
1903
 
1789
1904
  function findSplitT(com1, com2) {
1790
1905
 
1791
- // control tangent intersection
1792
- let pt1 = checkLineIntersection(com1.p0, com1.cp1, com2.cp2, com2.p, false);
1906
+ let len3 = getDistance(com1.cp2, com1.p);
1907
+ let len4 = getDistance(com1.cp2, com2.cp1);
1793
1908
 
1794
- // intersection 2nd cp1 tangent and global tangent intersection
1795
- let ptI = checkLineIntersection(pt1, com2.p, com2.p0, com2.cp1, false);
1909
+ let t = Math.min(len3) / len4;
1796
1910
 
1797
- let len1 = getDistance(pt1, com2.p);
1798
- let len2 = getDistance(ptI, com2.p);
1911
+ return t
1912
+ }
1799
1913
 
1800
- let t = 1 - len2 / len1;
1914
+ function checkBezierFlatness(p0, cpts, p) {
1801
1915
 
1802
- // check self intersections
1916
+ let isFlat = false;
1803
1917
 
1804
- let len3 = getDistance(com1.cp2, com1.p);
1805
- let len4 = getDistance(com1.cp2, com2.cp1);
1918
+ let isCubic = cpts.length === 2;
1806
1919
 
1807
- t = Math.min(len3) / len4;
1920
+ let cp1 = cpts[0];
1921
+ let cp2 = isCubic ? cpts[1] : cp1;
1808
1922
 
1809
- return t
1923
+ if (p0.x === cp1.x && p0.y === cp1.y && p.x === cp2.x && p.y === cp2.y) return true;
1924
+
1925
+ let dx1 = cp1.x - p0.x;
1926
+ let dy1 = cp1.y - p0.y;
1927
+
1928
+ let dx2 = p.x - cp2.x;
1929
+ let dy2 = p.y - cp2.y;
1930
+
1931
+ let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
1932
+
1933
+ if (!cross1) return true
1934
+
1935
+ let dx0 = p.x - p0.x;
1936
+ let dy0 = p.y - p0.y;
1937
+ let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
1938
+
1939
+ if (!cross0) return true
1940
+
1941
+ let area = getPolygonArea([p0,...cpts, p], true);
1942
+ let dist1 = getSquareDistance(p0, p);
1943
+ let thresh = dist1/200;
1944
+
1945
+ // if(area<thresh) return true;
1946
+ isFlat = area<thresh;
1947
+
1948
+ /*
1949
+
1950
+ let rat = (cross0 / cross1)
1951
+
1952
+ if (rat < 1.1) {
1953
+ console.log('cross', cross0, cross1, 'rat', rat );
1954
+ isFlat = true;
1955
+ }
1956
+ */
1957
+
1958
+ return isFlat;
1810
1959
 
1811
1960
  }
1812
1961
 
@@ -1852,7 +2001,6 @@ function analyzePathData(pathData = []) {
1852
2001
  * this way we can skip certain tests
1853
2002
  */
1854
2003
  let commandPts = [p0];
1855
- let isFlat = false;
1856
2004
 
1857
2005
  // init properties
1858
2006
  com.idx = c - 1;
@@ -1883,7 +2031,7 @@ function analyzePathData(pathData = []) {
1883
2031
  com.p0 = p0;
1884
2032
  com.p = p;
1885
2033
 
1886
- let cp1, cp2, cp1N, cp2N, pN, typeN, area1;
2034
+ let cp1, cp2, cp1N, pN, typeN, area1;
1887
2035
 
1888
2036
  let dimA = getDistAv(p0, p);
1889
2037
  com.dimA = dimA;
@@ -1923,6 +2071,7 @@ function analyzePathData(pathData = []) {
1923
2071
  if (type === 'C') commandPts.push(cp2);
1924
2072
  commandPts.push(p);
1925
2073
 
2074
+ /*
1926
2075
  let commandFlatness = commandIsFlat(commandPts);
1927
2076
  isFlat = commandFlatness.flat;
1928
2077
  com.flat = isFlat;
@@ -1930,6 +2079,7 @@ function analyzePathData(pathData = []) {
1930
2079
  if (isFlat) {
1931
2080
  com.extreme = false;
1932
2081
  }
2082
+ */
1933
2083
  }
1934
2084
 
1935
2085
  /**
@@ -1938,7 +2088,7 @@ function analyzePathData(pathData = []) {
1938
2088
  * so we interpret maximum x/y on-path points as well as extremes
1939
2089
  * but we ignore linetos to allow chunk compilation
1940
2090
  */
1941
- if (!isFlat && type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
2091
+ if (type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
1942
2092
  com.extreme = true;
1943
2093
  }
1944
2094
 
@@ -1951,7 +2101,7 @@ function analyzePathData(pathData = []) {
1951
2101
  pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
1952
2102
 
1953
2103
  cp1N = { x: comN.values[0], y: comN.values[1] };
1954
- cp2N = comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
2104
+ comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
1955
2105
  }
1956
2106
 
1957
2107
  /**
@@ -1981,19 +2131,15 @@ function analyzePathData(pathData = []) {
1981
2131
  // check extremes
1982
2132
  let cpts = commandPts.slice(1);
1983
2133
 
1984
- let w = pN ? Math.abs(pN.x - p0.x) : 0;
1985
- let h = pN ? Math.abs(pN.y - p0.y) : 0;
1986
- let thresh = (w + h) / 2 * 0.1;
1987
- let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
1988
-
1989
- let flatness2 = commandIsFlat(pts1, thresh);
1990
- let isFlat2 = flatness2.flat;
2134
+ pN ? Math.abs(pN.x - p0.x) : 0;
2135
+ pN ? Math.abs(pN.y - p0.y) : 0;
1991
2136
 
1992
2137
  /**
1993
2138
  * if current and next cubic are flat
1994
2139
  * we don't flag them as extremes to allow simplification
1995
2140
  */
1996
- let hasExtremes = (isFlat && isFlat2) ? false : (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
2141
+
2142
+ let hasExtremes = (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
1997
2143
 
1998
2144
  if (hasExtremes) {
1999
2145
  com.extreme = true;
@@ -2039,7 +2185,7 @@ function detectAccuracy(pathData) {
2039
2185
 
2040
2186
  // Reference first MoveTo command (M)
2041
2187
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
2042
- let p0 = M;
2188
+ let p0 = M;
2043
2189
  let p = M;
2044
2190
  pathData[0].decimals = 0;
2045
2191
 
@@ -2051,28 +2197,33 @@ function detectAccuracy(pathData) {
2051
2197
  let { type, values } = com;
2052
2198
 
2053
2199
  let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
2054
- p={x:lastVals[0], y:lastVals[1]};
2200
+ p = { x: lastVals[0], y: lastVals[1] };
2055
2201
 
2056
2202
  // use existing averave dimension value or calculate
2057
- let dimA = com.dimA ? +com.dimA.toFixed(8) : type!=='M' ? +getDistAv(p0, p).toFixed(8) : 0;
2203
+ let dimA = com.dimA ? +com.dimA.toFixed(8) : type !== 'M' ? +getDistAv(p0, p).toFixed(8) : 0;
2058
2204
 
2059
- if(dimA) dims.add(dimA);
2060
-
2205
+ if (dimA) dims.add(dimA);
2061
2206
 
2062
- if(type==='M'){
2063
- M=p;
2207
+ if (type === 'M') {
2208
+ M = p;
2064
2209
  }
2065
2210
  p0 = p;
2066
2211
  }
2067
2212
 
2068
2213
  let dim_min = Array.from(dims).sort();
2069
- let sliceIdx = Math.ceil(dim_min.length/8);
2070
- dim_min = dim_min.slice(0, sliceIdx );
2071
2214
 
2072
- let dimVal = dim_min.reduce((a,b)=>a+b, 0) / sliceIdx;
2215
+ /*
2216
+ let minVal = dim_min.length > 15 ?
2217
+ (dim_min[0] + dim_min[2]) / 2 :
2218
+ dim_min[0];
2219
+ */
2220
+
2221
+ let sliceIdx = Math.ceil(dim_min.length / 10);
2222
+ dim_min = dim_min.slice(0, sliceIdx);
2223
+ let minVal = dim_min.reduce((a, b) => a + b, 0) / sliceIdx;
2073
2224
 
2074
- let threshold = 50;
2075
- let decimalsAuto = dimVal > threshold ? 0 : Math.floor(threshold / dimVal).toString().length;
2225
+ let threshold = 40;
2226
+ let decimalsAuto = minVal > threshold*1.5 ? 0 : Math.floor(threshold / minVal).toString().length;
2076
2227
 
2077
2228
  // clamp
2078
2229
  return Math.min(Math.max(0, decimalsAuto), 8)
@@ -2088,13 +2239,13 @@ function roundPathData(pathData, decimals = -1) {
2088
2239
  // has recommended decimals
2089
2240
  let hasDecimal = decimals == 'auto' && pathData[0].hasOwnProperty('decimals') ? true : false;
2090
2241
 
2091
- for(let c=0, len=pathData.length; c<len; c++){
2092
- let com=pathData[c];
2242
+ for (let c = 0, len = pathData.length; c < len; c++) {
2243
+ let com = pathData[c];
2093
2244
 
2094
- if (decimals >-1 || hasDecimal) {
2245
+ if (decimals > -1 || hasDecimal) {
2095
2246
  decimals = hasDecimal ? com.decimals : decimals;
2096
2247
 
2097
- pathData[c].values = com.values.map(val=>{return val ? +val.toFixed(decimals) : val });
2248
+ pathData[c].values = com.values.map(val => { return val ? +val.toFixed(decimals) : val });
2098
2249
 
2099
2250
  }
2100
2251
  } return pathData;
@@ -2113,17 +2264,21 @@ function revertCubicQuadratic(p0 = {}, cp1 = {}, cp2 = {}, p = {}) {
2113
2264
  let cp1_Q = null;
2114
2265
  let type = 'C';
2115
2266
  let values = [cp1.x, cp1.y, cp2.x, cp2.y, p.x, p.y];
2267
+ let comN = {type, values};
2116
2268
 
2117
2269
  if (dist1 < threshold) {
2118
2270
  cp1_Q = checkLineIntersection(p0, cp1, p, cp2, false);
2119
2271
  if (cp1_Q) {
2120
2272
 
2121
- type = 'Q';
2122
- values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
2273
+ comN.type = 'Q';
2274
+ comN.values = [cp1_Q.x, cp1_Q.y, p.x, p.y];
2275
+ comN.p0 = p0;
2276
+ comN.cp1 = cp1_Q;
2277
+ comN.p = p;
2123
2278
  }
2124
2279
  }
2125
2280
 
2126
- return { type, values }
2281
+ return comN
2127
2282
 
2128
2283
  }
2129
2284
 
@@ -3480,6 +3635,242 @@ function parsePathDataString(d, debug = true) {
3480
3635
 
3481
3636
  }
3482
3637
 
3638
+ function stringifyPathData(pathData) {
3639
+ return pathData.map(com => { return `${com.type} ${com.values.join(' ')}` }).join(' ');
3640
+ }
3641
+
3642
+ function shapeElToPath(el){
3643
+
3644
+ let nodeName = el.nodeName.toLowerCase();
3645
+ if(nodeName==='path')return el;
3646
+
3647
+ let pathData = getPathDataFromEl(el);
3648
+ let d = pathData.map(com=>{return `${com.type} ${com.values} `}).join(' ');
3649
+ let attributes = [...el.attributes].map(att=>att.name);
3650
+
3651
+ let pathN = document.createElementNS('http://www.w3.org/2000/svg', 'path');
3652
+ pathN.setAttribute('d', d );
3653
+
3654
+ let exclude = ['x', 'y', 'cx', 'cy', 'dx', 'dy', 'r', 'rx', 'ry', 'width', 'height', 'points'];
3655
+
3656
+ attributes.forEach(att=>{
3657
+ if(!exclude.includes(att)){
3658
+ let val = el.getAttribute(att);
3659
+ pathN.setAttribute(att, val);
3660
+ }
3661
+ });
3662
+
3663
+ return pathN
3664
+
3665
+ }
3666
+
3667
+ // retrieve pathdata from svg geometry elements
3668
+ function getPathDataFromEl(el, stringify=false) {
3669
+
3670
+ let pathData = [];
3671
+ let type = el.nodeName;
3672
+ let atts, attNames, d, x, y, width, height, r, rx, ry, cx, cy, x1, x2, y1, y2;
3673
+
3674
+ // convert relative or absolute units
3675
+ const svgElUnitsToPixel = (el, decimals = 9) => {
3676
+
3677
+ const svg = el.nodeName !== "svg" ? el.closest("svg") : el;
3678
+
3679
+ // convert real life units to pixels
3680
+ const translateUnitToPixel = (value) => {
3681
+
3682
+ if (value === null) {
3683
+ return 0
3684
+ }
3685
+
3686
+ let dpi = 96;
3687
+ let unit = value.match(/([a-z]+)/gi);
3688
+ unit = unit ? unit[0] : "";
3689
+ let val = parseFloat(value);
3690
+ let rat;
3691
+
3692
+ // no unit - already pixes/user unit
3693
+ if (!unit) {
3694
+ return val;
3695
+ }
3696
+
3697
+ switch (unit) {
3698
+ case "in":
3699
+ rat = dpi;
3700
+ break;
3701
+ case "pt":
3702
+ rat = (1 / 72) * 96;
3703
+ break;
3704
+ case "cm":
3705
+ rat = (1 / 2.54) * 96;
3706
+ break;
3707
+ case "mm":
3708
+ rat = ((1 / 2.54) * 96) / 10;
3709
+ break;
3710
+ // just a default approximation
3711
+ case "em":
3712
+ case "rem":
3713
+ rat = 16;
3714
+ break;
3715
+ default:
3716
+ rat = 1;
3717
+ }
3718
+ let valuePx = val * rat;
3719
+ return +valuePx.toFixed(decimals);
3720
+ };
3721
+
3722
+ // svg width and height attributes
3723
+ let width = svg.getAttribute("width");
3724
+ width = width ? translateUnitToPixel(width) : 300;
3725
+ let height = svg.getAttribute("height");
3726
+ height = width ? translateUnitToPixel(height) : 150;
3727
+
3728
+ let vB = svg.getAttribute("viewBox");
3729
+ vB = vB
3730
+ ? vB
3731
+ .replace(/,/g, " ")
3732
+ .split(" ")
3733
+ .filter(Boolean)
3734
+ .map((val) => {
3735
+ return +val;
3736
+ })
3737
+ : [];
3738
+
3739
+ let w = vB.length ? vB[2] : width;
3740
+ let h = vB.length ? vB[3] : height;
3741
+ let scaleX = w / 100;
3742
+ let scaleY = h / 100;
3743
+ let scalRoot = Math.sqrt((Math.pow(scaleX, 2) + Math.pow(scaleY, 2)) / 2);
3744
+
3745
+ let attsH = ["x", "width", "x1", "x2", "rx", "cx", "r"];
3746
+ let attsV = ["y", "height", "y1", "y2", "ry", "cy"];
3747
+
3748
+ let atts = el.getAttributeNames();
3749
+ atts.forEach((att) => {
3750
+ let val = el.getAttribute(att);
3751
+ let valAbs = val;
3752
+ if (attsH.includes(att) || attsV.includes(att)) {
3753
+ let scale = attsH.includes(att) ? scaleX : scaleY;
3754
+ scale = att === "r" && w != h ? scalRoot : scale;
3755
+ let unit = val.match(/([a-z|%]+)/gi);
3756
+ unit = unit ? unit[0] : "";
3757
+ if (val.includes("%")) {
3758
+ valAbs = parseFloat(val) * scale;
3759
+ }
3760
+
3761
+ else {
3762
+ valAbs = translateUnitToPixel(val);
3763
+ }
3764
+ el.setAttribute(att, +valAbs);
3765
+ }
3766
+ });
3767
+ };
3768
+
3769
+ svgElUnitsToPixel(el);
3770
+
3771
+ const getAtts = (attNames) => {
3772
+ atts = {};
3773
+ attNames.forEach(att => {
3774
+ atts[att] = +el.getAttribute(att);
3775
+ });
3776
+ return atts
3777
+ };
3778
+
3779
+ switch (type) {
3780
+ case 'path':
3781
+ d = el.getAttribute("d");
3782
+ pathData = parsePathDataNormalized(d);
3783
+ break;
3784
+
3785
+ case 'rect':
3786
+ attNames = ['x', 'y', 'width', 'height', 'rx', 'ry'];
3787
+ ({ x, y, width, height, rx, ry } = getAtts(attNames));
3788
+
3789
+ if (!rx && !ry) {
3790
+ pathData = [
3791
+ { type: "M", values: [x, y] },
3792
+ { type: "L", values: [x + width, y] },
3793
+ { type: "L", values: [x + width, y + height] },
3794
+ { type: "L", values: [x, y + height] },
3795
+ { type: "Z", values: [] }
3796
+ ];
3797
+ } else {
3798
+
3799
+ if (rx > width / 2) {
3800
+ rx = width / 2;
3801
+ }
3802
+ if (ry > height / 2) {
3803
+ ry = height / 2;
3804
+ }
3805
+ pathData = [
3806
+ { type: "M", values: [x + rx, y] },
3807
+ { type: "L", values: [x + width - rx, y] },
3808
+ { type: "A", values: [rx, ry, 0, 0, 1, x + width, y + ry] },
3809
+ { type: "L", values: [x + width, y + height - ry] },
3810
+ { type: "A", values: [rx, ry, 0, 0, 1, x + width - rx, y + height] },
3811
+ { type: "L", values: [x + rx, y + height] },
3812
+ { type: "A", values: [rx, ry, 0, 0, 1, x, y + height - ry] },
3813
+ { type: "L", values: [x, y + ry] },
3814
+ { type: "A", values: [rx, ry, 0, 0, 1, x + rx, y] },
3815
+ { type: "Z", values: [] }
3816
+ ];
3817
+ }
3818
+ break;
3819
+
3820
+ case 'circle':
3821
+ case 'ellipse':
3822
+
3823
+ attNames = ['cx', 'cy', 'rx', 'ry', 'r'];
3824
+ ({ cx, cy, r, rx, ry } = getAtts(attNames));
3825
+
3826
+ if (type === 'circle') {
3827
+ r = r;
3828
+ rx = r;
3829
+ ry = r;
3830
+ } else {
3831
+ rx = rx ? rx : r;
3832
+ ry = ry ? ry : r;
3833
+ }
3834
+
3835
+ pathData = [
3836
+ { type: "M", values: [cx + rx, cy] },
3837
+ { type: "A", values: [rx, ry, 0, 1, 1, cx - rx, cy] },
3838
+ { type: "A", values: [rx, ry, 0, 1, 1, cx + rx, cy] },
3839
+ ];
3840
+
3841
+ break;
3842
+ case 'line':
3843
+ attNames = ['x1', 'y1', 'x2', 'y2'];
3844
+ ({ x1, y1, x2, y2 } = getAtts(attNames));
3845
+ pathData = [
3846
+ { type: "M", values: [x1, y1] },
3847
+ { type: "L", values: [x2, y2] }
3848
+ ];
3849
+ break;
3850
+ case 'polygon':
3851
+ case 'polyline':
3852
+
3853
+ let points = el.getAttribute('points').replaceAll(',', ' ').split(' ').filter(Boolean);
3854
+
3855
+ for (let i = 0; i < points.length; i += 2) {
3856
+ pathData.push({
3857
+ type: (i === 0 ? "M" : "L"),
3858
+ values: [+points[i], +points[i + 1]]
3859
+ });
3860
+ }
3861
+ if (type === 'polygon') {
3862
+ pathData.push({
3863
+ type: "Z",
3864
+ values: []
3865
+ });
3866
+ }
3867
+ break;
3868
+ }
3869
+
3870
+ return stringify ? stringifyPathData(pathData): pathData;
3871
+
3872
+ }
3873
+
3483
3874
  function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = true) {
3484
3875
 
3485
3876
  let pathDataN = [pathData[0]];
@@ -3500,8 +3891,11 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3500
3891
 
3501
3892
  let area = getPolygonArea([p0, p, p1], true);
3502
3893
 
3894
+ getSquareDistance(p0, p);
3895
+ getSquareDistance(p, p1);
3503
3896
  let distSquare = getSquareDistance(p0, p1);
3504
- let distMax = distSquare / 100 * tolerance;
3897
+
3898
+ let distMax = distSquare / 200 * tolerance;
3505
3899
 
3506
3900
  let isFlat = area < distMax;
3507
3901
  let isFlatBez = false;
@@ -3516,6 +3910,7 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3516
3910
  (type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
3517
3911
 
3518
3912
  isFlatBez = checkBezierFlatness(p0, cpts, p);
3913
+
3519
3914
  // console.log();
3520
3915
 
3521
3916
  if (isFlatBez && c < l - 1 && comPrev.type !== 'C') {
@@ -3531,7 +3926,10 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3531
3926
  p0 = p;
3532
3927
 
3533
3928
  // colinear – exclude arcs (as always =) as semicircles won't have an area
3534
- if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez))) {
3929
+
3930
+ if ( isFlat && c < l - 1 && (type === 'L' || (flatBezierToLinetos && isFlatBez)) ) {
3931
+
3932
+
3535
3933
 
3536
3934
  continue;
3537
3935
  }
@@ -3554,6 +3952,44 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
3554
3952
 
3555
3953
  }
3556
3954
 
3955
+ function removeOrphanedM(pathData) {
3956
+
3957
+ for (let i = 0, l = pathData.length; i < l; i++) {
3958
+ let com = pathData[i];
3959
+ if (!com) continue;
3960
+ let { type = null, values = [] } = com;
3961
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
3962
+ if ((type === 'M' || type === 'm')) {
3963
+
3964
+ if (!comN || (comN && (comN.type === 'Z' || comN.type === 'z'))) {
3965
+ pathData[i] = null;
3966
+ pathData[i + 1] = null;
3967
+ }
3968
+ }
3969
+ }
3970
+
3971
+ pathData = pathData.filter(Boolean);
3972
+ return pathData;
3973
+
3974
+ }
3975
+
3976
+ /*
3977
+ // remove zero-length segments introduced by rounding
3978
+ export function removeZeroLengthLinetos_post(pathData) {
3979
+ let pathDataOpt = []
3980
+ pathData.forEach((com, i) => {
3981
+ let { type, values } = com;
3982
+ if (type === 'l' || type === 'v' || type === 'h') {
3983
+ let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0
3984
+ if (hasLength) pathDataOpt.push(com)
3985
+ } else {
3986
+ pathDataOpt.push(com)
3987
+ }
3988
+ })
3989
+ return pathDataOpt
3990
+ }
3991
+ */
3992
+
3557
3993
  function removeZeroLengthLinetos(pathData) {
3558
3994
 
3559
3995
  let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
@@ -3566,14 +4002,21 @@ function removeZeroLengthLinetos(pathData) {
3566
4002
  let com = pathData[c];
3567
4003
  let { type, values } = com;
3568
4004
 
3569
- let valsL = values.slice(-2);
3570
- p = { x: valsL[0], y: valsL[1] };
4005
+ let valsLen = values.length;
4006
+
4007
+ p = { x: values[valsLen-2], y: values[valsLen-1] };
3571
4008
 
3572
4009
  // skip lineto
3573
4010
  if (type === 'L' && p.x === p0.x && p.y === p0.y) {
3574
4011
  continue
3575
4012
  }
3576
4013
 
4014
+ // skip minified zero length
4015
+ if (type === 'l' || type === 'v' || type === 'h') {
4016
+ let noLength = type === 'l' ? (values.join('') === '00') : values[0] === 0;
4017
+ if(noLength) continue
4018
+ }
4019
+
3577
4020
  pathDataN.push(com);
3578
4021
  p0 = p;
3579
4022
  }
@@ -3912,6 +4355,152 @@ function reversePathData(pathData, {
3912
4355
  return pathDataNew;
3913
4356
  }
3914
4357
 
4358
+ function refineAdjacentExtremes(pathData, {
4359
+ threshold = null, tolerance = 1
4360
+ } = {}) {
4361
+
4362
+ if (!threshold) {
4363
+ let bb = getPathDataBBox(pathData);
4364
+ threshold = (bb.width + bb.height) / 2 * 0.05;
4365
+
4366
+ }
4367
+
4368
+ let l = pathData.length;
4369
+
4370
+ for (let i = 0; i < l; i++) {
4371
+ let com = pathData[i];
4372
+ let { type, values, extreme, corner=false, dimA, p0, p } = com;
4373
+ let comN = pathData[i + 1] ? pathData[i + 1] : null;
4374
+
4375
+ // adjacent
4376
+
4377
+ if (comN && type === 'C' && comN.type === 'C' && extreme && !corner) {
4378
+
4379
+ // check dist
4380
+ let diff = getDistAv(p, comN.p);
4381
+ let isCose = diff < threshold;
4382
+
4383
+ if (isCose) {
4384
+
4385
+ let dx1 = (com.cp1.x - comN.p0.x);
4386
+ let dy1 = (com.cp1.y - comN.p0.y);
4387
+
4388
+ let horizontal = Math.abs(dy1) < Math.abs(dx1);
4389
+
4390
+ let pN = comN.p;
4391
+ let ptI;
4392
+ let t = 1;
4393
+
4394
+ if (comN.extreme) {
4395
+
4396
+ // extend cp2
4397
+ if (horizontal) {
4398
+ t = Math.abs(Math.abs(comN.cp2.x - comN.p.x) / Math.abs(com.cp2.x - com.p.x));
4399
+
4400
+ ptI = interpolate(comN.p, com.cp2, 1 + t);
4401
+ com.cp2.x = ptI.x;
4402
+
4403
+ }
4404
+ else {
4405
+
4406
+ t = Math.abs(Math.abs(comN.cp2.y - comN.p.y) / Math.abs(com.cp2.y - com.p.y));
4407
+ ptI = interpolate(comN.p, com.cp2, 1 + t);
4408
+ com.cp2.y = ptI.y;
4409
+ }
4410
+
4411
+ pathData[i + 1].values = [com.cp1.x, com.cp1.y, com.cp2.x, com.cp2.y, pN.x, pN.y];
4412
+ pathData[i + 1].cp1 = com.cp1;
4413
+ pathData[i + 1].cp2 = com.cp2;
4414
+ pathData[i + 1].p0 = com.p0;
4415
+ pathData[i + 1].p = pN;
4416
+ pathData[i + 1].extreme = true;
4417
+
4418
+ // nullify 1st
4419
+ pathData[i] = null;
4420
+ continue
4421
+
4422
+ }
4423
+
4424
+ // extend fist command
4425
+ else {
4426
+
4427
+ let comN2 = pathData[i + 2] ? pathData[i + 2] : null;
4428
+ if (!comN2 && comN2.type !== 'C') continue
4429
+
4430
+ // extrapolate
4431
+ let comEx = getCombinedByDominant(comN, comN2, threshold, tolerance, false);
4432
+
4433
+ if (comEx.length === 1) {
4434
+ pathData[i + 1] = null;
4435
+
4436
+ comEx = comEx[0];
4437
+
4438
+ pathData[i + 2].values = [comEx.cp1.x, comEx.cp1.y, comEx.cp2.x, comEx.cp2.y, comEx.p.x, comEx.p.y];
4439
+ pathData[i + 2].cp1 = comEx.cp1;
4440
+ pathData[i + 2].cp2 = comEx.cp2;
4441
+ pathData[i + 2].p0 = comEx.p0;
4442
+ pathData[i + 2].p = comEx.p;
4443
+ pathData[i + 2].extreme = comEx.extreme;
4444
+
4445
+ i++;
4446
+ continue
4447
+ }
4448
+
4449
+ }
4450
+
4451
+ }
4452
+ }
4453
+ }
4454
+
4455
+ // remove commands
4456
+ pathData = pathData.filter(Boolean);
4457
+ l = pathData.length;
4458
+
4459
+ /**
4460
+ * refine closing commands
4461
+ */
4462
+
4463
+ let closed = pathData[l - 1].type.toLowerCase() === 'z';
4464
+ let lastIdx = closed ? l - 2 : l - 1;
4465
+ let lastCom = pathData[lastIdx];
4466
+ let penultimateCom = pathData[lastIdx - 1] || null;
4467
+ let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
4468
+
4469
+ let dec = 8;
4470
+ let lastVals = lastCom.values.slice(-2);
4471
+ let isClosingTo = +lastVals[0].toFixed(dec) === +M.x.toFixed(dec) && +lastVals[1].toFixed(dec) === +M.y.toFixed(dec);
4472
+ let fistExt = pathData[1].type === 'C' && pathData[1].extreme ? pathData[1] : null;
4473
+
4474
+ let diff = getDistAv(lastCom.p0, lastCom.p);
4475
+ let isCose = diff < threshold;
4476
+
4477
+ if (penultimateCom && penultimateCom.type === 'C' && isCose && isClosingTo && fistExt) {
4478
+
4479
+ Math.abs(fistExt.cp1.x - M.x);
4480
+ Math.abs(fistExt.cp1.y - M.y);
4481
+
4482
+ let comEx = getCombinedByDominant(penultimateCom, lastCom, threshold, tolerance, false);
4483
+ console.log('comEx', comEx);
4484
+
4485
+ if (comEx.length === 1) {
4486
+ pathData[lastIdx - 1] = comEx[0];
4487
+ pathData[lastIdx] = null;
4488
+ pathData = pathData.filter(Boolean);
4489
+ }
4490
+
4491
+ }
4492
+
4493
+ return pathData
4494
+
4495
+ }
4496
+
4497
+ function removeEmptySVGEls(svg) {
4498
+ let els = svg.querySelectorAll('g, defs');
4499
+ els.forEach(el => {
4500
+ if (!el.children.length) el.remove();
4501
+ });
4502
+ }
4503
+
3915
4504
  function cleanUpSVG(svgMarkup, {
3916
4505
  returnDom=false,
3917
4506
  removeHidden=true,
@@ -3927,7 +4516,7 @@ function cleanUpSVG(svgMarkup, {
3927
4516
  .querySelector("svg");
3928
4517
 
3929
4518
 
3930
- let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
4519
+ let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width'];
3931
4520
  removeExcludedAttribues(svg, allowed);
3932
4521
 
3933
4522
  let removeEls = ['metadata', 'script'];
@@ -3999,10 +4588,13 @@ function stringifySVG(svg){
3999
4588
  }
4000
4589
 
4001
4590
  function svgPathSimplify(input = '', {
4591
+
4592
+ // return svg markup or object
4593
+ getObject = false,
4594
+
4002
4595
  toAbsolute = true,
4003
4596
  toRelative = true,
4004
4597
  toShorthands = true,
4005
- decimals = 3,
4006
4598
 
4007
4599
  // not necessary unless you need cubics only
4008
4600
  quadraticToCubic = true,
@@ -4011,29 +4603,33 @@ function svgPathSimplify(input = '', {
4011
4603
  arcToCubic = false,
4012
4604
  cubicToArc = false,
4013
4605
 
4014
- // arc to cubic precision - adds more segments for better precision
4015
- arcAccuracy = 4,
4016
- keepExtremes = true,
4017
- keepCorners = true,
4018
- keepInflections = true,
4019
- extrapolateDominant = false,
4020
- addExtremes = false,
4606
+ simplifyBezier = true,
4021
4607
  optimizeOrder = true,
4022
4608
  removeColinear = true,
4023
- simplifyBezier = true,
4024
- autoAccuracy = true,
4025
4609
  flatBezierToLinetos = true,
4026
4610
  revertToQuadratics = true,
4611
+
4612
+ refineExtremes = true,
4613
+ keepExtremes = true,
4614
+ keepCorners = true,
4615
+ extrapolateDominant = true,
4616
+ keepInflections = false,
4617
+ addExtremes = false,
4618
+ removeOrphanSubpaths = false,
4619
+
4620
+ // svg path optimizations
4621
+ decimals = 3,
4622
+ autoAccuracy = true,
4623
+
4027
4624
  minifyD = 0,
4028
4625
  tolerance = 1,
4029
4626
  reverse = false,
4030
4627
 
4031
4628
  // svg cleanup options
4629
+ mergePaths = false,
4032
4630
  removeHidden = true,
4033
4631
  removeUnused = true,
4034
-
4035
- // return svg markup or object
4036
- getObject = false
4632
+ shapesToPaths = true,
4037
4633
 
4038
4634
  } = {}) {
4039
4635
 
@@ -4076,6 +4672,14 @@ function svgPathSimplify(input = '', {
4076
4672
  svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
4077
4673
  );
4078
4674
 
4675
+ if(shapesToPaths){
4676
+ let shapes = svg.querySelectorAll('polygon, polyline, line, rect, circle, ellipse');
4677
+ shapes.forEach(shape=>{
4678
+ let path = shapeElToPath(shape);
4679
+ shape.replaceWith(path);
4680
+ });
4681
+ }
4682
+
4079
4683
  // collect paths
4080
4684
  let pathEls = svg.querySelectorAll('path');
4081
4685
  pathEls.forEach(path => {
@@ -4086,17 +4690,31 @@ function svgPathSimplify(input = '', {
4086
4690
  /**
4087
4691
  * process all paths
4088
4692
  */
4693
+
4694
+ // SVG optimization options
4695
+ let pathOptions = {
4696
+ toRelative,
4697
+ toShorthands,
4698
+ decimals,
4699
+ };
4700
+
4701
+ // combinded path data for SVGs with mergePaths enabled
4702
+ let pathData_merged = [];
4703
+
4089
4704
  paths.forEach(path => {
4090
4705
  let { d, el } = path;
4091
4706
 
4092
4707
  let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
4093
4708
 
4094
- // create clone for fallback
4095
- let pathData = JSON.parse(JSON.stringify(pathDataO));
4096
-
4097
4709
  // count commands for evaluation
4098
4710
  let comCount = pathDataO.length;
4099
4711
 
4712
+ // create clone for fallback
4713
+
4714
+ let pathData = pathDataO;
4715
+
4716
+ if(removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
4717
+
4100
4718
  /**
4101
4719
  * get sub paths
4102
4720
  */
@@ -4132,7 +4750,13 @@ function svgPathSimplify(input = '', {
4132
4750
  // simplify beziers
4133
4751
  let { pathData, bb, dimA } = pathDataPlus;
4134
4752
 
4135
- pathData = simplifyBezier ? simplifyPathData(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4753
+ pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
4754
+
4755
+ // refine extremes
4756
+ if(refineExtremes){
4757
+ let thresholdEx = (bb.width + bb.height) / 2 * 0.05;
4758
+ pathData = refineAdjacentExtremes(pathData, {threshold:thresholdEx, tolerance});
4759
+ }
4136
4760
 
4137
4761
  // cubic to arcs
4138
4762
  if (cubicToArc) {
@@ -4161,80 +4785,107 @@ function svgPathSimplify(input = '', {
4161
4785
  if (type === 'C') {
4162
4786
 
4163
4787
  let comQ = revertCubicQuadratic(p0, cp1, cp2, p);
4164
- if (comQ.type === 'Q') pathData[c] = comQ;
4788
+ if (comQ.type === 'Q') {
4789
+ /*
4790
+ comQ.p0 = com.p0
4791
+ comQ.cp1 = {x:comQ.values[0], y:comQ.values[1]}
4792
+ comQ.p = com.p
4793
+ */
4794
+ comQ.extreme = com.extreme;
4795
+ comQ.corner = com.corner;
4796
+ comQ.dimA = com.dimA;
4797
+
4798
+ pathData[c] = comQ;
4799
+ }
4165
4800
  }
4166
4801
  });
4167
4802
  }
4168
4803
 
4169
4804
  // optimize close path
4170
- if(optimizeOrder) pathData=optimizeClosePath(pathData);
4805
+ if (optimizeOrder) pathData = optimizeClosePath(pathData);
4806
+
4807
+ // poly
4171
4808
 
4172
4809
  // update
4173
4810
  pathDataArrN.push(pathData);
4174
4811
  }
4175
4812
 
4176
-
4177
4813
  // flatten compound paths
4178
4814
  pathData = pathDataArrN.flat();
4179
4815
 
4180
- /**
4181
- * detect accuracy
4182
- */
4183
- if (autoAccuracy) {
4184
- decimals = detectAccuracy(pathData);
4816
+ // collect for merged svg paths
4817
+ if (el && mergePaths) {
4818
+ pathData_merged.push(...pathData);
4185
4819
  }
4820
+ // single output
4821
+ else {
4186
4822
 
4187
- // optimize
4188
- let pathOptions = {
4189
- toRelative,
4190
- toShorthands,
4191
- decimals,
4192
- };
4193
-
4194
- // optimize path data
4195
- pathData = convertPathData(pathData, pathOptions);
4196
-
4197
- // remove zero-length segments introduced by rounding
4198
- let pathDataOpt = [];
4823
+ /**
4824
+ * detect accuracy
4825
+ */
4826
+ if (autoAccuracy) {
4827
+ decimals = detectAccuracy(pathData);
4828
+ pathOptions.decimals = decimals;
4199
4829
 
4200
- pathData.forEach((com, i) => {
4201
- let { type, values } = com;
4202
- if (type === 'l' || type === 'v' || type === 'h') {
4203
- let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0;
4204
- if (hasLength) pathDataOpt.push(com);
4205
- } else {
4206
- pathDataOpt.push(com);
4207
4830
  }
4208
- });
4209
-
4210
- pathData = pathDataOpt;
4211
4831
 
4212
- // compare command count
4213
- let comCountS = pathData.length;
4832
+ // optimize path data
4833
+ pathData = convertPathData(pathData, pathOptions);
4214
4834
 
4215
- let dOpt = pathDataToD(pathData, minifyD);
4216
- svgSizeOpt = new Blob([dOpt]).size;
4835
+ // remove zero-length segments introduced by rounding
4836
+ pathData = removeZeroLengthLinetos(pathData);
4217
4837
 
4218
- compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4838
+ // compare command count
4839
+ let comCountS = pathData.length;
4219
4840
 
4220
- path.d = dOpt;
4221
- path.report = {
4222
- original: comCount,
4223
- new: comCountS,
4224
- saved: comCount - comCountS,
4225
- compression,
4226
- decimals,
4841
+ let dOpt = pathDataToD(pathData, minifyD);
4842
+ svgSizeOpt = new Blob([dOpt]).size;
4843
+ compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
4227
4844
 
4228
- };
4845
+ path.d = dOpt;
4846
+ path.report = {
4847
+ original: comCount,
4848
+ new: comCountS,
4849
+ saved: comCount - comCountS,
4850
+ compression,
4851
+ decimals,
4229
4852
 
4230
- // apply new path for svgs
4231
- if (el) el.setAttribute('d', dOpt);
4853
+ };
4232
4854
 
4855
+ // apply new path for svgs
4856
+ if (el) el.setAttribute('d', dOpt);
4857
+ }
4233
4858
  });
4234
4859
 
4235
- // stringify new SVG
4860
+ /**
4861
+ * stringify new SVG
4862
+ */
4236
4863
  if (mode) {
4237
- svg = new XMLSerializer().serializeToString(svg);
4864
+
4865
+ if (pathData_merged.length) {
4866
+ // optimize path data
4867
+ let pathData = convertPathData(pathData_merged, pathOptions);
4868
+
4869
+ // remove zero-length segments introduced by rounding
4870
+
4871
+ pathData = removeZeroLengthLinetos(pathData);
4872
+
4873
+ let dOpt = pathDataToD(pathData, minifyD);
4874
+
4875
+ // apply new path for svgs
4876
+ paths[0].el.setAttribute('d', dOpt);
4877
+
4878
+ // remove other paths
4879
+ for (let i = 1; i < paths.length; i++) {
4880
+ let pathEl = paths[i].el;
4881
+ if (pathEl) pathEl.remove();
4882
+ }
4883
+
4884
+ // remove empty groups e.g groups
4885
+ removeEmptySVGEls(svg);
4886
+ }
4887
+
4888
+ svg = stringifySVG(svg);
4238
4889
  svgSizeOpt = new Blob([svg]).size;
4239
4890
 
4240
4891
  compression = +(100 / svgSize * (svgSizeOpt)).toFixed(2);
@@ -4256,7 +4907,7 @@ function svgPathSimplify(input = '', {
4256
4907
 
4257
4908
  }
4258
4909
 
4259
- function simplifyPathData(pathData, {
4910
+ function simplifyPathDataCubic(pathData, {
4260
4911
  keepExtremes = true,
4261
4912
  keepInflections = true,
4262
4913
  keepCorners = true,
@@ -4300,6 +4951,8 @@ function simplifyPathData(pathData, {
4300
4951
  if (combined.length === 1) {
4301
4952
  com = combined[0];
4302
4953
  let offset = 1;
4954
+
4955
+ // add cumulative error to prevent distortions
4303
4956
  error += com.error;
4304
4957
 
4305
4958
  // find next candidates
@@ -4317,6 +4970,10 @@ function simplifyPathData(pathData, {
4317
4970
 
4318
4971
  let combined = combineCubicPairs(com, comN, extrapolateDominant, tolerance);
4319
4972
  if (combined.length === 1) {
4973
+ // add cumulative error to prevent distortions
4974
+
4975
+ error += combined[0].error * 0.5;
4976
+
4320
4977
  offset++;
4321
4978
  }
4322
4979
  com = combined[0];
@@ -4348,51 +5005,21 @@ function simplifyPathData(pathData, {
4348
5005
  return pathDataN
4349
5006
  }
4350
5007
 
4351
- /**
4352
- * get viewBox
4353
- * either from explicit attribute or
4354
- * width and height attributes
4355
- */
4356
-
4357
- function getViewBox(svg = null, round = false) {
4358
-
4359
- // browser default
4360
- if (!svg) return { x: 0, y: 0, width: 300, height: 150 }
4361
-
4362
- let style = window.getComputedStyle(svg);
4363
-
4364
- // the baseVal API method also converts physical units to pixels/user-units
4365
- let w = svg.hasAttribute('width') ? svg.width.baseVal.value : parseFloat(style.width) || 300;
4366
- let h = svg.hasAttribute('height') ? svg.height.baseVal.value : parseFloat(style.height) || 150;
4367
-
4368
- let viewBox = svg.getAttribute('viewBox') ? svg.viewBox.baseVal : { x: 0, y: 0, width: w, height: h };
4369
-
4370
- // remove SVG constructor
4371
- let { x, y, width, height } = viewBox;
4372
- viewBox = { x, y, width, height };
4373
-
4374
- // round to integers
4375
- if (round) {
4376
- for (let prop in viewBox) {
4377
- viewBox[prop] = Math.ceil(viewBox[prop]);
4378
- }
4379
- }
4380
-
4381
- return viewBox
4382
- }
4383
-
4384
5008
  const {
4385
5009
  abs, acos, asin, atan, atan2, ceil, cos, exp, floor,
4386
5010
  log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
4387
5011
  } = Math;
4388
5012
 
4389
- // just for visual debugging
5013
+ /*
5014
+ import {XMLSerializerPoly, DOMParserPoly} from './dom_polyfills';
5015
+ export {XMLSerializerPoly as XMLSerializerPoly};
5016
+ export {DOMParserPoly as DOMParserPoly};
5017
+ */
4390
5018
 
4391
5019
  // IIFE
4392
5020
  if (typeof window !== 'undefined') {
4393
5021
  window.svgPathSimplify = svgPathSimplify;
4394
- window.getViewBox = getViewBox;
4395
- window.renderPoint = renderPoint;
5022
+
4396
5023
  }
4397
5024
 
4398
- export { PI, abs, acos, asin, atan, atan2, ceil, cos, exp, floor, getViewBox, hypot, log, max, min, pow, random, round, sin, sqrt, svgPathSimplify, tan };
5025
+ export { PI, abs, acos, asin, atan, atan2, ceil, cos, exp, floor, hypot, log, max, min, pow, random, round, sin, sqrt, svgPathSimplify, tan };