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