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