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