svg-path-simplify 0.0.7 → 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.
- package/dist/svg-path-simplify.esm.js +775 -169
- package/dist/svg-path-simplify.esm.min.js +1 -1
- package/dist/svg-path-simplify.js +4674 -4068
- package/dist/svg-path-simplify.min.js +1 -1
- package/dist/svg-path-simplify.node.js +775 -169
- package/dist/svg-path-simplify.node.min.js +1 -1
- package/index.html +11 -8
- package/package.json +5 -3
- package/src/dom-polyfill.js +29 -0
- package/src/dom-polyfill_back.js +22 -0
- package/src/index.js +7 -1
- package/src/pathData_simplify_cubic.js +1 -37
- package/src/pathData_simplify_cubic_extrapolate.js +43 -28
- package/src/pathSimplify-main.js +50 -9
- package/src/svgii/geometry.js +8 -155
- package/src/svgii/geometry_flatness.js +101 -0
- package/src/svgii/pathData_analyze.js +11 -596
- package/src/svgii/pathData_parse_els.js +239 -0
- package/src/svgii/pathData_remove_collinear.js +3 -1
- package/src/svgii/pathData_remove_orphaned.js +20 -0
- package/src/svgii/pathData_remove_zerolength.js +12 -2
- package/src/svgii/pathData_reorder.js +3 -1
- package/src/svgii/pathData_split.js +1 -0
- package/src/svgii/rounding.js +25 -19
- package/src/svgii/simplify_refineExtremes.js +173 -0
- package/src/svgii/svg_cleanup.js +4 -1
- package/testSVG.js +39 -0
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
function renderPoint(
|
|
4
|
+
svg,
|
|
5
|
+
coords,
|
|
6
|
+
fill = "red",
|
|
7
|
+
r = "1%",
|
|
8
|
+
opacity = "1",
|
|
9
|
+
title = '',
|
|
10
|
+
render = true,
|
|
11
|
+
id = "",
|
|
12
|
+
className = ""
|
|
13
|
+
) {
|
|
14
|
+
if (Array.isArray(coords)) {
|
|
15
|
+
coords = {
|
|
16
|
+
x: coords[0],
|
|
17
|
+
y: coords[1]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
|
|
21
|
+
<title>${title}</title></circle>`;
|
|
22
|
+
|
|
23
|
+
if (render) {
|
|
24
|
+
svg.insertAdjacentHTML("beforeend", marker);
|
|
25
|
+
} else {
|
|
26
|
+
return marker;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function 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
|
+
|
|
3
42
|
function detectInputType(input) {
|
|
4
43
|
let type = 'string';
|
|
5
44
|
/*
|
|
@@ -67,19 +106,24 @@ function getAngle(p1, p2, normalize = false) {
|
|
|
67
106
|
* http://jsfiddle.net/justin_c_rounds/Gd2S2/light/
|
|
68
107
|
*/
|
|
69
108
|
|
|
70
|
-
function checkLineIntersection(p1, p2, p3, p4, exact = true) {
|
|
109
|
+
function checkLineIntersection(p1=null, p2=null, p3=null, p4=null, exact = true, debug=false) {
|
|
71
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
|
|
72
111
|
let denominator, a, b, numerator1, numerator2;
|
|
73
112
|
let intersectionPoint = {};
|
|
74
113
|
|
|
114
|
+
if(!p1 || !p2 || !p3 || !p4){
|
|
115
|
+
if(debug) console.warn('points missing');
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
|
|
75
119
|
try {
|
|
76
120
|
denominator = ((p4.y - p3.y) * (p2.x - p1.x)) - ((p4.x - p3.x) * (p2.y - p1.y));
|
|
77
121
|
if (denominator == 0) {
|
|
78
122
|
return false;
|
|
79
123
|
}
|
|
80
|
-
|
|
81
124
|
} catch {
|
|
82
|
-
console.
|
|
125
|
+
if(debug) console.warn('!catch', p1, p2, 'p3:', p3, 'p4:', p4);
|
|
126
|
+
return false
|
|
83
127
|
}
|
|
84
128
|
|
|
85
129
|
a = p1.y - p3.y;
|
|
@@ -96,8 +140,6 @@ function checkLineIntersection(p1, p2, p3, p4, exact = true) {
|
|
|
96
140
|
y: p1.y + (a * (p2.y - p1.y))
|
|
97
141
|
};
|
|
98
142
|
|
|
99
|
-
// console.log('intersectionPoint', intersectionPoint, p1, p2);
|
|
100
|
-
|
|
101
143
|
let intersection = false;
|
|
102
144
|
// if line1 is a segment and line2 is infinite, they intersect if:
|
|
103
145
|
if ((a > 0 && a < 1) && (b > 0 && b < 1)) {
|
|
@@ -497,6 +539,110 @@ function getBezierExtremeT(pts) {
|
|
|
497
539
|
return tArr;
|
|
498
540
|
}
|
|
499
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
|
+
|
|
500
646
|
// cubic bezier.
|
|
501
647
|
function cubicBezierExtremeT(p0, cp1, cp2, p) {
|
|
502
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];
|
|
@@ -605,90 +751,6 @@ function quadraticBezierExtremeT(p0, cp1, p) {
|
|
|
605
751
|
return extemeT
|
|
606
752
|
}
|
|
607
753
|
|
|
608
|
-
function commandIsFlat(points, tolerance = 0.025) {
|
|
609
|
-
|
|
610
|
-
let p0 = points[0];
|
|
611
|
-
let p = points[points.length - 1];
|
|
612
|
-
|
|
613
|
-
let xArr = points.map(pt => { return pt.x });
|
|
614
|
-
let yArr = points.map(pt => { return pt.y });
|
|
615
|
-
|
|
616
|
-
let xMin = Math.min(...xArr);
|
|
617
|
-
let xMax = Math.max(...xArr);
|
|
618
|
-
let yMin = Math.min(...yArr);
|
|
619
|
-
let yMax = Math.max(...yArr);
|
|
620
|
-
let w = xMax - xMin;
|
|
621
|
-
let h = yMax - yMin;
|
|
622
|
-
|
|
623
|
-
if (points.length < 3 || (w === 0 || h === 0)) {
|
|
624
|
-
return { area: 0, flat: true, thresh: 0.0001, ratio: 0 };
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
let squareDist = getSquareDistance(p0, p);
|
|
628
|
-
let squareDist1 = getSquareDistance(p0, points[0]);
|
|
629
|
-
let squareDist2 = points.length > 3 ? getSquareDistance(p, points[1]) : squareDist1;
|
|
630
|
-
let squareDistAvg = (squareDist1 + squareDist2) / 2;
|
|
631
|
-
|
|
632
|
-
tolerance = 0.5;
|
|
633
|
-
let thresh = (w + h) * 0.5 * tolerance;
|
|
634
|
-
|
|
635
|
-
let area = 0;
|
|
636
|
-
for (let i = 0, l = points.length; i < l; i++) {
|
|
637
|
-
let addX = points[i].x;
|
|
638
|
-
let addY = points[i === points.length - 1 ? 0 : i + 1].y;
|
|
639
|
-
let subX = points[i === points.length - 1 ? 0 : i + 1].x;
|
|
640
|
-
let subY = points[i].y;
|
|
641
|
-
area += addX * addY * 0.5 - subX * subY * 0.5;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
area = +Math.abs(area).toFixed(9);
|
|
645
|
-
let areaThresh = 1000;
|
|
646
|
-
|
|
647
|
-
let ratio = area / (squareDistAvg);
|
|
648
|
-
|
|
649
|
-
let isFlat = area === 0 ? true : area < squareDistAvg / areaThresh;
|
|
650
|
-
|
|
651
|
-
return { area: area, flat: isFlat, thresh: thresh, ratio: ratio, squareDist: squareDist, areaThresh: squareDist / areaThresh };
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function checkBezierFlatness(p0, cpts, p) {
|
|
655
|
-
|
|
656
|
-
let isFlat = false;
|
|
657
|
-
|
|
658
|
-
let isCubic = cpts.length===2;
|
|
659
|
-
|
|
660
|
-
let cp1 = cpts[0];
|
|
661
|
-
let cp2 = isCubic ? cpts[1] : cp1;
|
|
662
|
-
|
|
663
|
-
if(p0.x===cp1.x && p0.y===cp1.y && p.x===cp2.x && p.y===cp2.y) return true;
|
|
664
|
-
|
|
665
|
-
let dx1 = cp1.x - p0.x;
|
|
666
|
-
let dy1 = cp1.y - p0.y;
|
|
667
|
-
|
|
668
|
-
let dx2 = p.x - cp2.x;
|
|
669
|
-
let dy2 = p.y - cp2.y;
|
|
670
|
-
|
|
671
|
-
let cross1 = Math.abs(dx1 * dy2 - dy1 * dx2);
|
|
672
|
-
|
|
673
|
-
if(!cross1) return true
|
|
674
|
-
|
|
675
|
-
let dx0 = p.x - p0.x;
|
|
676
|
-
let dy0 = p.y - p0.y;
|
|
677
|
-
let cross0 = Math.abs(dx0 * dy1 - dy0 * dx1);
|
|
678
|
-
|
|
679
|
-
if(!cross0) return true
|
|
680
|
-
|
|
681
|
-
let rat = (cross0/cross1);
|
|
682
|
-
|
|
683
|
-
if (rat<1.1 ) {
|
|
684
|
-
|
|
685
|
-
isFlat = true;
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
return isFlat;
|
|
689
|
-
|
|
690
|
-
}
|
|
691
|
-
|
|
692
754
|
/**
|
|
693
755
|
* sloppy distance calculation
|
|
694
756
|
* based on x/y differences
|
|
@@ -1156,6 +1218,73 @@ function getPathDataPoly(pathData) {
|
|
|
1156
1218
|
return poly;
|
|
1157
1219
|
}
|
|
1158
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
|
+
|
|
1159
1288
|
/**
|
|
1160
1289
|
* get pathdata area
|
|
1161
1290
|
*/
|
|
@@ -1431,7 +1560,7 @@ function pathDataToD(pathData, optimize = 0) {
|
|
|
1431
1560
|
return d;
|
|
1432
1561
|
}
|
|
1433
1562
|
|
|
1434
|
-
function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
|
|
1563
|
+
function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1, debug = false) {
|
|
1435
1564
|
|
|
1436
1565
|
// cubic Bézier derivative
|
|
1437
1566
|
const cubicDerivative = (p0, p1, p2, p3, t) => {
|
|
@@ -1453,8 +1582,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
|
|
|
1453
1582
|
let commands = [com1, com2];
|
|
1454
1583
|
|
|
1455
1584
|
// detect dominant
|
|
1456
|
-
let dist1 =
|
|
1457
|
-
let dist2 =
|
|
1585
|
+
let dist1 = getDistAv(com1.p0, com1.p);
|
|
1586
|
+
let dist2 = getDistAv(com2.p0, com2.p);
|
|
1587
|
+
|
|
1458
1588
|
let reverse = dist1 > dist2;
|
|
1459
1589
|
|
|
1460
1590
|
// backup original commands
|
|
@@ -1513,11 +1643,9 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
|
|
|
1513
1643
|
let dP = cubicDerivative(com2.p0, com2.cp1, com2.cp2, com2.p, t0);
|
|
1514
1644
|
let r = sub(P, com1.p0);
|
|
1515
1645
|
|
|
1516
|
-
|
|
1517
1646
|
t0 -= dot(r, dP) / dot(dP, dP);
|
|
1518
1647
|
|
|
1519
1648
|
// construct merged cubic over [t0, 1]
|
|
1520
|
-
|
|
1521
1649
|
let Q0 = pointAtT([com2.p0, com2.cp1, com2.cp2, com2.p], t0);
|
|
1522
1650
|
let Q3 = com2.p;
|
|
1523
1651
|
|
|
@@ -1545,7 +1673,7 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
|
|
|
1545
1673
|
};
|
|
1546
1674
|
}
|
|
1547
1675
|
|
|
1548
|
-
let tMid = (1 - t0)*0.5
|
|
1676
|
+
let tMid = (1 - t0) * 0.5;
|
|
1549
1677
|
|
|
1550
1678
|
let ptM = pointAtT([result.p0, result.cp1, result.cp2, result.p], tMid, false, true);
|
|
1551
1679
|
let seg1_cp2 = ptM.cpts[2];
|
|
@@ -1557,17 +1685,18 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
|
|
|
1557
1685
|
let cp2_2 = interpolate(result.p, ptI_2, 1.333);
|
|
1558
1686
|
|
|
1559
1687
|
// test self intersections and exit
|
|
1560
|
-
let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true
|
|
1561
|
-
if(cp_intersection){
|
|
1688
|
+
let cp_intersection = checkLineIntersection(com1_o.p0, cp1_2, com2_o.p, cp2_2, true);
|
|
1689
|
+
if (cp_intersection) {
|
|
1562
1690
|
|
|
1563
1691
|
return commands;
|
|
1564
1692
|
}
|
|
1565
1693
|
|
|
1694
|
+
if (debug) renderPoint(markers, ptM, 'purple');
|
|
1695
|
+
|
|
1566
1696
|
result.cp1 = cp1_2;
|
|
1567
1697
|
result.cp2 = cp2_2;
|
|
1568
1698
|
|
|
1569
|
-
// check distances
|
|
1570
|
-
|
|
1699
|
+
// check distances between original starting point and extrapolated
|
|
1571
1700
|
let dist3 = getDistAv(com1_o.p0, result.p0);
|
|
1572
1701
|
let dist4 = getDistAv(com2_o.p, result.p);
|
|
1573
1702
|
let dist5 = (dist3 + dist4);
|
|
@@ -1579,11 +1708,24 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
|
|
|
1579
1708
|
result.corner = com2_o.corner;
|
|
1580
1709
|
result.dimA = com2_o.dimA;
|
|
1581
1710
|
result.directionChange = com2_o.directionChange;
|
|
1711
|
+
result.type = 'C';
|
|
1582
1712
|
result.values = [result.cp1.x, result.cp1.y, result.cp2.x, result.cp2.y, result.p.x, result.p.y];
|
|
1583
1713
|
|
|
1584
|
-
//
|
|
1714
|
+
// extrapolated starting point is not completely off
|
|
1585
1715
|
if (dist5 < maxDist) {
|
|
1586
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
|
+
|
|
1587
1729
|
// compare combined with original area
|
|
1588
1730
|
let pathData0 = [
|
|
1589
1731
|
{ type: 'M', values: [com1_o.p0.x, com1_o.p0.y] },
|
|
@@ -1600,16 +1742,18 @@ function getCombinedByDominant(com1, com2, maxDist = 0, tolerance = 1) {
|
|
|
1600
1742
|
let areaN = getPathArea(pathDataN);
|
|
1601
1743
|
let areaDiff = Math.abs(areaN / area0 - 1);
|
|
1602
1744
|
|
|
1603
|
-
result.error = areaDiff *
|
|
1745
|
+
result.error = areaDiff * 5 * tolerance;
|
|
1604
1746
|
|
|
1605
|
-
|
|
1747
|
+
if (debug) {
|
|
1748
|
+
let d = pathDataToD(pathDataN);
|
|
1749
|
+
renderPath(markers, d, 'orange');
|
|
1750
|
+
}
|
|
1606
1751
|
|
|
1607
|
-
// success
|
|
1608
|
-
if (areaDiff < 0.
|
|
1752
|
+
// success!!!
|
|
1753
|
+
if (areaDiff < 0.05 * tolerance) {
|
|
1609
1754
|
commands = [result];
|
|
1610
1755
|
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1756
|
+
}
|
|
1613
1757
|
}
|
|
1614
1758
|
|
|
1615
1759
|
return commands
|
|
@@ -1693,14 +1837,6 @@ function combineCubicPairs(com1, com2, extrapolateDominant = false, tolerance =
|
|
|
1693
1837
|
} // end 1st try
|
|
1694
1838
|
|
|
1695
1839
|
|
|
1696
|
-
/*
|
|
1697
|
-
if (extrapolateDominant && com2.extreme) {
|
|
1698
|
-
renderPoint(markers, com2.p)
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1701
|
-
*/
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
1840
|
|
|
1705
1841
|
// try extrapolated dominant curve
|
|
1706
1842
|
|
|
@@ -1769,25 +1905,59 @@ function getExtrapolatedCommand(com1, com2, t1 = 0, t2 = 0) {
|
|
|
1769
1905
|
|
|
1770
1906
|
function findSplitT(com1, com2) {
|
|
1771
1907
|
|
|
1772
|
-
|
|
1773
|
-
let
|
|
1908
|
+
let len3 = getDistance(com1.cp2, com1.p);
|
|
1909
|
+
let len4 = getDistance(com1.cp2, com2.cp1);
|
|
1774
1910
|
|
|
1775
|
-
|
|
1776
|
-
let ptI = checkLineIntersection(pt1, com2.p, com2.p0, com2.cp1, false);
|
|
1911
|
+
let t = Math.min(len3) / len4;
|
|
1777
1912
|
|
|
1778
|
-
|
|
1779
|
-
|
|
1913
|
+
return t
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function checkBezierFlatness(p0, cpts, p) {
|
|
1780
1917
|
|
|
1781
|
-
let
|
|
1918
|
+
let isFlat = false;
|
|
1782
1919
|
|
|
1783
|
-
|
|
1920
|
+
let isCubic = cpts.length === 2;
|
|
1784
1921
|
|
|
1785
|
-
let
|
|
1786
|
-
let
|
|
1922
|
+
let cp1 = cpts[0];
|
|
1923
|
+
let cp2 = isCubic ? cpts[1] : cp1;
|
|
1787
1924
|
|
|
1788
|
-
|
|
1925
|
+
if (p0.x === cp1.x && p0.y === cp1.y && p.x === cp2.x && p.y === cp2.y) return true;
|
|
1789
1926
|
|
|
1790
|
-
|
|
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;
|
|
1791
1961
|
|
|
1792
1962
|
}
|
|
1793
1963
|
|
|
@@ -1833,7 +2003,6 @@ function analyzePathData(pathData = []) {
|
|
|
1833
2003
|
* this way we can skip certain tests
|
|
1834
2004
|
*/
|
|
1835
2005
|
let commandPts = [p0];
|
|
1836
|
-
let isFlat = false;
|
|
1837
2006
|
|
|
1838
2007
|
// init properties
|
|
1839
2008
|
com.idx = c - 1;
|
|
@@ -1864,7 +2033,7 @@ function analyzePathData(pathData = []) {
|
|
|
1864
2033
|
com.p0 = p0;
|
|
1865
2034
|
com.p = p;
|
|
1866
2035
|
|
|
1867
|
-
let cp1, cp2, cp1N,
|
|
2036
|
+
let cp1, cp2, cp1N, pN, typeN, area1;
|
|
1868
2037
|
|
|
1869
2038
|
let dimA = getDistAv(p0, p);
|
|
1870
2039
|
com.dimA = dimA;
|
|
@@ -1904,6 +2073,7 @@ function analyzePathData(pathData = []) {
|
|
|
1904
2073
|
if (type === 'C') commandPts.push(cp2);
|
|
1905
2074
|
commandPts.push(p);
|
|
1906
2075
|
|
|
2076
|
+
/*
|
|
1907
2077
|
let commandFlatness = commandIsFlat(commandPts);
|
|
1908
2078
|
isFlat = commandFlatness.flat;
|
|
1909
2079
|
com.flat = isFlat;
|
|
@@ -1911,6 +2081,7 @@ function analyzePathData(pathData = []) {
|
|
|
1911
2081
|
if (isFlat) {
|
|
1912
2082
|
com.extreme = false;
|
|
1913
2083
|
}
|
|
2084
|
+
*/
|
|
1914
2085
|
}
|
|
1915
2086
|
|
|
1916
2087
|
/**
|
|
@@ -1919,7 +2090,7 @@ function analyzePathData(pathData = []) {
|
|
|
1919
2090
|
* so we interpret maximum x/y on-path points as well as extremes
|
|
1920
2091
|
* but we ignore linetos to allow chunk compilation
|
|
1921
2092
|
*/
|
|
1922
|
-
if (
|
|
2093
|
+
if (type !== 'L' && (p.x === left || p.y === top || p.x === right || p.y === bottom)) {
|
|
1923
2094
|
com.extreme = true;
|
|
1924
2095
|
}
|
|
1925
2096
|
|
|
@@ -1932,7 +2103,7 @@ function analyzePathData(pathData = []) {
|
|
|
1932
2103
|
pN = comN ? { x: comNValsL[0], y: comNValsL[1] } : null;
|
|
1933
2104
|
|
|
1934
2105
|
cp1N = { x: comN.values[0], y: comN.values[1] };
|
|
1935
|
-
|
|
2106
|
+
comN.type === 'C' ? { x: comN.values[2], y: comN.values[3] } : null;
|
|
1936
2107
|
}
|
|
1937
2108
|
|
|
1938
2109
|
/**
|
|
@@ -1962,19 +2133,15 @@ function analyzePathData(pathData = []) {
|
|
|
1962
2133
|
// check extremes
|
|
1963
2134
|
let cpts = commandPts.slice(1);
|
|
1964
2135
|
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
let thresh = (w + h) / 2 * 0.1;
|
|
1968
|
-
let pts1 = type === 'C' ? [p, cp1N, cp2N, pN] : [p, cp1N, pN];
|
|
1969
|
-
|
|
1970
|
-
let flatness2 = commandIsFlat(pts1, thresh);
|
|
1971
|
-
let isFlat2 = flatness2.flat;
|
|
2136
|
+
pN ? Math.abs(pN.x - p0.x) : 0;
|
|
2137
|
+
pN ? Math.abs(pN.y - p0.y) : 0;
|
|
1972
2138
|
|
|
1973
2139
|
/**
|
|
1974
2140
|
* if current and next cubic are flat
|
|
1975
2141
|
* we don't flag them as extremes to allow simplification
|
|
1976
2142
|
*/
|
|
1977
|
-
|
|
2143
|
+
|
|
2144
|
+
let hasExtremes = (!com.extreme ? bezierhasExtreme(p0, cpts, angleThreshold) : true);
|
|
1978
2145
|
|
|
1979
2146
|
if (hasExtremes) {
|
|
1980
2147
|
com.extreme = true;
|
|
@@ -2020,7 +2187,7 @@ function detectAccuracy(pathData) {
|
|
|
2020
2187
|
|
|
2021
2188
|
// Reference first MoveTo command (M)
|
|
2022
2189
|
let M = { x: pathData[0].values[0], y: pathData[0].values[1] };
|
|
2023
|
-
let p0 =
|
|
2190
|
+
let p0 = M;
|
|
2024
2191
|
let p = M;
|
|
2025
2192
|
pathData[0].decimals = 0;
|
|
2026
2193
|
|
|
@@ -2032,28 +2199,33 @@ function detectAccuracy(pathData) {
|
|
|
2032
2199
|
let { type, values } = com;
|
|
2033
2200
|
|
|
2034
2201
|
let lastVals = values.length ? values.slice(-2) : [M.x, M.y];
|
|
2035
|
-
p={x:lastVals[0], y:lastVals[1]};
|
|
2202
|
+
p = { x: lastVals[0], y: lastVals[1] };
|
|
2036
2203
|
|
|
2037
2204
|
// use existing averave dimension value or calculate
|
|
2038
|
-
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;
|
|
2039
2206
|
|
|
2040
|
-
if(dimA) dims.add(dimA);
|
|
2041
|
-
|
|
2207
|
+
if (dimA) dims.add(dimA);
|
|
2042
2208
|
|
|
2043
|
-
if(type==='M'){
|
|
2044
|
-
M=p;
|
|
2209
|
+
if (type === 'M') {
|
|
2210
|
+
M = p;
|
|
2045
2211
|
}
|
|
2046
2212
|
p0 = p;
|
|
2047
2213
|
}
|
|
2048
2214
|
|
|
2049
2215
|
let dim_min = Array.from(dims).sort();
|
|
2050
|
-
let sliceIdx = Math.ceil(dim_min.length/8);
|
|
2051
|
-
dim_min = dim_min.slice(0, sliceIdx );
|
|
2052
2216
|
|
|
2053
|
-
|
|
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;
|
|
2054
2226
|
|
|
2055
|
-
let threshold =
|
|
2056
|
-
let decimalsAuto =
|
|
2227
|
+
let threshold = 40;
|
|
2228
|
+
let decimalsAuto = minVal > threshold*1.5 ? 0 : Math.floor(threshold / minVal).toString().length;
|
|
2057
2229
|
|
|
2058
2230
|
// clamp
|
|
2059
2231
|
return Math.min(Math.max(0, decimalsAuto), 8)
|
|
@@ -2069,13 +2241,13 @@ function roundPathData(pathData, decimals = -1) {
|
|
|
2069
2241
|
// has recommended decimals
|
|
2070
2242
|
let hasDecimal = decimals == 'auto' && pathData[0].hasOwnProperty('decimals') ? true : false;
|
|
2071
2243
|
|
|
2072
|
-
for(let c=0, len=pathData.length; c<len; c++){
|
|
2073
|
-
let com=pathData[c];
|
|
2244
|
+
for (let c = 0, len = pathData.length; c < len; c++) {
|
|
2245
|
+
let com = pathData[c];
|
|
2074
2246
|
|
|
2075
|
-
if (decimals
|
|
2247
|
+
if (decimals > -1 || hasDecimal) {
|
|
2076
2248
|
decimals = hasDecimal ? com.decimals : decimals;
|
|
2077
2249
|
|
|
2078
|
-
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 });
|
|
2079
2251
|
|
|
2080
2252
|
}
|
|
2081
2253
|
} return pathData;
|
|
@@ -3465,6 +3637,242 @@ function parsePathDataString(d, debug = true) {
|
|
|
3465
3637
|
|
|
3466
3638
|
}
|
|
3467
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
|
+
|
|
3468
3876
|
function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = true) {
|
|
3469
3877
|
|
|
3470
3878
|
let pathDataN = [pathData[0]];
|
|
@@ -3504,6 +3912,7 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
|
|
|
3504
3912
|
(type === 'Q' ? [{ x: values[0], y: values[1] }] : []);
|
|
3505
3913
|
|
|
3506
3914
|
isFlatBez = checkBezierFlatness(p0, cpts, p);
|
|
3915
|
+
|
|
3507
3916
|
// console.log();
|
|
3508
3917
|
|
|
3509
3918
|
if (isFlatBez && c < l - 1 && comPrev.type !== 'C') {
|
|
@@ -3545,20 +3954,43 @@ function pathDataRemoveColinear(pathData, tolerance = 1, flatBezierToLinetos = t
|
|
|
3545
3954
|
|
|
3546
3955
|
}
|
|
3547
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
|
+
/*
|
|
3548
3979
|
// remove zero-length segments introduced by rounding
|
|
3549
|
-
function removeZeroLengthLinetos_post(pathData) {
|
|
3550
|
-
let pathDataOpt = []
|
|
3980
|
+
export function removeZeroLengthLinetos_post(pathData) {
|
|
3981
|
+
let pathDataOpt = []
|
|
3551
3982
|
pathData.forEach((com, i) => {
|
|
3552
3983
|
let { type, values } = com;
|
|
3553
3984
|
if (type === 'l' || type === 'v' || type === 'h') {
|
|
3554
|
-
let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0
|
|
3555
|
-
if (hasLength) pathDataOpt.push(com)
|
|
3985
|
+
let hasLength = type === 'l' ? (values.join('') !== '00') : values[0] !== 0
|
|
3986
|
+
if (hasLength) pathDataOpt.push(com)
|
|
3556
3987
|
} else {
|
|
3557
|
-
pathDataOpt.push(com)
|
|
3988
|
+
pathDataOpt.push(com)
|
|
3558
3989
|
}
|
|
3559
|
-
})
|
|
3990
|
+
})
|
|
3560
3991
|
return pathDataOpt
|
|
3561
3992
|
}
|
|
3993
|
+
*/
|
|
3562
3994
|
|
|
3563
3995
|
function removeZeroLengthLinetos(pathData) {
|
|
3564
3996
|
|
|
@@ -3572,14 +4004,21 @@ function removeZeroLengthLinetos(pathData) {
|
|
|
3572
4004
|
let com = pathData[c];
|
|
3573
4005
|
let { type, values } = com;
|
|
3574
4006
|
|
|
3575
|
-
let
|
|
3576
|
-
|
|
4007
|
+
let valsLen = values.length;
|
|
4008
|
+
|
|
4009
|
+
p = { x: values[valsLen-2], y: values[valsLen-1] };
|
|
3577
4010
|
|
|
3578
4011
|
// skip lineto
|
|
3579
4012
|
if (type === 'L' && p.x === p0.x && p.y === p0.y) {
|
|
3580
4013
|
continue
|
|
3581
4014
|
}
|
|
3582
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
|
+
|
|
3583
4022
|
pathDataN.push(com);
|
|
3584
4023
|
p0 = p;
|
|
3585
4024
|
}
|
|
@@ -3918,6 +4357,145 @@ function reversePathData(pathData, {
|
|
|
3918
4357
|
return pathDataNew;
|
|
3919
4358
|
}
|
|
3920
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
|
+
|
|
3921
4499
|
function removeEmptySVGEls(svg) {
|
|
3922
4500
|
let els = svg.querySelectorAll('g, defs');
|
|
3923
4501
|
els.forEach(el => {
|
|
@@ -3940,7 +4518,7 @@ function cleanUpSVG(svgMarkup, {
|
|
|
3940
4518
|
.querySelector("svg");
|
|
3941
4519
|
|
|
3942
4520
|
|
|
3943
|
-
let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class'];
|
|
4521
|
+
let allowed=['viewBox', 'xmlns', 'width', 'height', 'id', 'class', 'fill', 'stroke', 'stroke-width'];
|
|
3944
4522
|
removeExcludedAttribues(svg, allowed);
|
|
3945
4523
|
|
|
3946
4524
|
let removeEls = ['metadata', 'script'];
|
|
@@ -4033,11 +4611,13 @@ function svgPathSimplify(input = '', {
|
|
|
4033
4611
|
flatBezierToLinetos = true,
|
|
4034
4612
|
revertToQuadratics = true,
|
|
4035
4613
|
|
|
4614
|
+
refineExtremes = true,
|
|
4036
4615
|
keepExtremes = true,
|
|
4037
4616
|
keepCorners = true,
|
|
4038
4617
|
extrapolateDominant = true,
|
|
4039
4618
|
keepInflections = false,
|
|
4040
4619
|
addExtremes = false,
|
|
4620
|
+
removeOrphanSubpaths = false,
|
|
4041
4621
|
|
|
4042
4622
|
// svg path optimizations
|
|
4043
4623
|
decimals = 3,
|
|
@@ -4051,6 +4631,7 @@ function svgPathSimplify(input = '', {
|
|
|
4051
4631
|
mergePaths = false,
|
|
4052
4632
|
removeHidden = true,
|
|
4053
4633
|
removeUnused = true,
|
|
4634
|
+
shapesToPaths = true,
|
|
4054
4635
|
|
|
4055
4636
|
} = {}) {
|
|
4056
4637
|
|
|
@@ -4093,6 +4674,14 @@ function svgPathSimplify(input = '', {
|
|
|
4093
4674
|
svg = cleanUpSVG(input, { returnDom, removeHidden, removeUnused }
|
|
4094
4675
|
);
|
|
4095
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
|
+
|
|
4096
4685
|
// collect paths
|
|
4097
4686
|
let pathEls = svg.querySelectorAll('path');
|
|
4098
4687
|
pathEls.forEach(path => {
|
|
@@ -4119,12 +4708,15 @@ function svgPathSimplify(input = '', {
|
|
|
4119
4708
|
|
|
4120
4709
|
let pathDataO = parsePathDataNormalized(d, { quadraticToCubic, toAbsolute, arcToCubic });
|
|
4121
4710
|
|
|
4122
|
-
// create clone for fallback
|
|
4123
|
-
let pathData = JSON.parse(JSON.stringify(pathDataO));
|
|
4124
|
-
|
|
4125
4711
|
// count commands for evaluation
|
|
4126
4712
|
let comCount = pathDataO.length;
|
|
4127
4713
|
|
|
4714
|
+
// create clone for fallback
|
|
4715
|
+
|
|
4716
|
+
let pathData = pathDataO;
|
|
4717
|
+
|
|
4718
|
+
if(removeOrphanSubpaths) pathData = removeOrphanedM(pathData);
|
|
4719
|
+
|
|
4128
4720
|
/**
|
|
4129
4721
|
* get sub paths
|
|
4130
4722
|
*/
|
|
@@ -4162,6 +4754,12 @@ function svgPathSimplify(input = '', {
|
|
|
4162
4754
|
|
|
4163
4755
|
pathData = simplifyBezier ? simplifyPathDataCubic(pathData, { simplifyBezier, keepInflections, keepExtremes, keepCorners, extrapolateDominant, revertToQuadratics, tolerance, reverse }) : pathData;
|
|
4164
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
|
+
}
|
|
4762
|
+
|
|
4165
4763
|
// cubic to arcs
|
|
4166
4764
|
if (cubicToArc) {
|
|
4167
4765
|
|
|
@@ -4229,13 +4827,15 @@ function svgPathSimplify(input = '', {
|
|
|
4229
4827
|
*/
|
|
4230
4828
|
if (autoAccuracy) {
|
|
4231
4829
|
decimals = detectAccuracy(pathData);
|
|
4830
|
+
pathOptions.decimals = decimals;
|
|
4831
|
+
|
|
4232
4832
|
}
|
|
4233
4833
|
|
|
4234
4834
|
// optimize path data
|
|
4235
4835
|
pathData = convertPathData(pathData, pathOptions);
|
|
4236
4836
|
|
|
4237
4837
|
// remove zero-length segments introduced by rounding
|
|
4238
|
-
pathData =
|
|
4838
|
+
pathData = removeZeroLengthLinetos(pathData);
|
|
4239
4839
|
|
|
4240
4840
|
// compare command count
|
|
4241
4841
|
let comCountS = pathData.length;
|
|
@@ -4269,7 +4869,8 @@ function svgPathSimplify(input = '', {
|
|
|
4269
4869
|
let pathData = convertPathData(pathData_merged, pathOptions);
|
|
4270
4870
|
|
|
4271
4871
|
// remove zero-length segments introduced by rounding
|
|
4272
|
-
|
|
4872
|
+
|
|
4873
|
+
pathData = removeZeroLengthLinetos(pathData);
|
|
4273
4874
|
|
|
4274
4875
|
let dOpt = pathDataToD(pathData, minifyD);
|
|
4275
4876
|
|
|
@@ -4374,6 +4975,7 @@ function simplifyPathDataCubic(pathData, {
|
|
|
4374
4975
|
// add cumulative error to prevent distortions
|
|
4375
4976
|
|
|
4376
4977
|
error += combined[0].error * 0.5;
|
|
4978
|
+
|
|
4377
4979
|
offset++;
|
|
4378
4980
|
}
|
|
4379
4981
|
com = combined[0];
|
|
@@ -4410,7 +5012,11 @@ const {
|
|
|
4410
5012
|
log, hypot, max, min, pow, random, round, sin, sqrt, tan, PI
|
|
4411
5013
|
} = Math;
|
|
4412
5014
|
|
|
4413
|
-
|
|
5015
|
+
/*
|
|
5016
|
+
import {XMLSerializerPoly, DOMParserPoly} from './dom_polyfills';
|
|
5017
|
+
export {XMLSerializerPoly as XMLSerializerPoly};
|
|
5018
|
+
export {DOMParserPoly as DOMParserPoly};
|
|
5019
|
+
*/
|
|
4414
5020
|
|
|
4415
5021
|
// IIFE
|
|
4416
5022
|
if (typeof window !== 'undefined') {
|