svg-path-commander 2.1.10 → 2.2.0
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/AGENTS.md +106 -0
- package/CHANGELOG.md +25 -0
- package/README.md +45 -3
- package/dist/index.d.ts +733 -0
- package/dist/index.js +4674 -0
- package/dist/index.js.map +1 -0
- package/dist/index.min.js +3 -0
- package/dist/index.min.js.map +1 -0
- package/dist/util.d.ts +1261 -0
- package/dist/util.js +4277 -0
- package/dist/util.js.map +1 -0
- package/package.json +29 -26
- package/tsdown.config.ts +75 -0
- package/dist/svg-path-commander.cjs +0 -2
- package/dist/svg-path-commander.cjs.map +0 -1
- package/dist/svg-path-commander.d.cts +0 -1347
- package/dist/svg-path-commander.d.ts +0 -1347
- package/dist/svg-path-commander.js +0 -2
- package/dist/svg-path-commander.js.map +0 -1
- package/dist/svg-path-commander.mjs +0 -2
- package/dist/svg-path-commander.mjs.map +0 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,4674 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* SVGPathCommander v2.2.0 (http://thednp.github.io/svg-path-commander)
|
|
3
|
+
* Copyright 2026 © thednp
|
|
4
|
+
* Licensed under MIT (https://github.com/thednp/svg-path-commander/blob/master/LICENSE)
|
|
5
|
+
*/
|
|
6
|
+
import CSSMatrix from "@thednp/dommatrix";
|
|
7
|
+
//#region package.json
|
|
8
|
+
var version = "2.2.0";
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/math/midPoint.ts
|
|
11
|
+
/**
|
|
12
|
+
* Returns the coordinates of a specified distance
|
|
13
|
+
* ratio between two points.
|
|
14
|
+
*
|
|
15
|
+
* @param a the first point coordinates
|
|
16
|
+
* @param b the second point coordinates
|
|
17
|
+
* @param t the ratio
|
|
18
|
+
* @returns the midpoint coordinates
|
|
19
|
+
*/
|
|
20
|
+
const midPoint = ([ax, ay], [bx, by], t) => {
|
|
21
|
+
return [ax + (bx - ax) * t, ay + (by - ay) * t];
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/math/distanceSquareRoot.ts
|
|
25
|
+
/**
|
|
26
|
+
* Returns the square root of the distance
|
|
27
|
+
* between two given points.
|
|
28
|
+
*
|
|
29
|
+
* @param a the first point coordinates
|
|
30
|
+
* @param b the second point coordinates
|
|
31
|
+
* @returns the distance value
|
|
32
|
+
*/
|
|
33
|
+
const distanceSquareRoot = (a, b) => {
|
|
34
|
+
return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]));
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/math/lineTools.ts
|
|
38
|
+
/**
|
|
39
|
+
* Returns length for line segments (MoveTo, LineTo).
|
|
40
|
+
*
|
|
41
|
+
* @param x1 the starting point X
|
|
42
|
+
* @param y1 the starting point Y
|
|
43
|
+
* @param x2 the ending point X
|
|
44
|
+
* @param y2 the ending point Y
|
|
45
|
+
* @returns the line segment length
|
|
46
|
+
*/
|
|
47
|
+
const getLineLength = (x1, y1, x2, y2) => {
|
|
48
|
+
return distanceSquareRoot([x1, y1], [x2, y2]);
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Returns a point along the line segments (MoveTo, LineTo).
|
|
52
|
+
*
|
|
53
|
+
* @param x1 the starting point X
|
|
54
|
+
* @param y1 the starting point Y
|
|
55
|
+
* @param x2 the ending point X
|
|
56
|
+
* @param y2 the ending point Y
|
|
57
|
+
* @param distance the distance to point in [0-1] range
|
|
58
|
+
* @returns the point at length
|
|
59
|
+
*/
|
|
60
|
+
const getPointAtLineLength = (x1, y1, x2, y2, distance) => {
|
|
61
|
+
let point = {
|
|
62
|
+
x: x1,
|
|
63
|
+
y: y1
|
|
64
|
+
};
|
|
65
|
+
if (typeof distance === "number") {
|
|
66
|
+
const length = distanceSquareRoot([x1, y1], [x2, y2]);
|
|
67
|
+
if (distance <= 0) point = {
|
|
68
|
+
x: x1,
|
|
69
|
+
y: y1
|
|
70
|
+
};
|
|
71
|
+
else if (distance >= length) point = {
|
|
72
|
+
x: x2,
|
|
73
|
+
y: y2
|
|
74
|
+
};
|
|
75
|
+
else {
|
|
76
|
+
const [x, y] = midPoint([x1, y1], [x2, y2], distance / length);
|
|
77
|
+
point = {
|
|
78
|
+
x,
|
|
79
|
+
y
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return point;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Returns bounding box for line segments (MoveTo, LineTo).
|
|
87
|
+
*
|
|
88
|
+
* @param x1 the starting point X
|
|
89
|
+
* @param y1 the starting point Y
|
|
90
|
+
* @param x2 the ending point X
|
|
91
|
+
* @param y2 the ending point Y
|
|
92
|
+
* @returns the bounding box for line segments
|
|
93
|
+
*/
|
|
94
|
+
const getLineBBox = (x1, y1, x2, y2) => {
|
|
95
|
+
const { min, max } = Math;
|
|
96
|
+
return [
|
|
97
|
+
min(x1, x2),
|
|
98
|
+
min(y1, y2),
|
|
99
|
+
max(x1, x2),
|
|
100
|
+
max(y1, y2)
|
|
101
|
+
];
|
|
102
|
+
};
|
|
103
|
+
const lineTools = {
|
|
104
|
+
getLineBBox,
|
|
105
|
+
getLineLength,
|
|
106
|
+
getPointAtLineLength
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/math/arcTools.ts
|
|
110
|
+
/**
|
|
111
|
+
* Returns the Arc segment length.
|
|
112
|
+
* @param rx radius along X axis
|
|
113
|
+
* @param ry radius along Y axis
|
|
114
|
+
* @param theta the angle in radians
|
|
115
|
+
* @returns the arc length
|
|
116
|
+
*/
|
|
117
|
+
const arcLength = (rx, ry, theta) => {
|
|
118
|
+
const halfTheta = theta / 2;
|
|
119
|
+
const sinHalfTheta = Math.sin(halfTheta);
|
|
120
|
+
const cosHalfTheta = Math.cos(halfTheta);
|
|
121
|
+
const term1 = rx ** 2 * sinHalfTheta ** 2;
|
|
122
|
+
const term2 = ry ** 2 * cosHalfTheta ** 2;
|
|
123
|
+
const length = Math.sqrt(term1 + term2) * theta;
|
|
124
|
+
return Math.abs(length);
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Find point on ellipse at given angle around ellipse (theta);
|
|
128
|
+
* @param cx the center X
|
|
129
|
+
* @param cy the center Y
|
|
130
|
+
* @param rx the radius X
|
|
131
|
+
* @param ry the radius Y
|
|
132
|
+
* @param alpha the arc rotation angle in radians
|
|
133
|
+
* @param theta the arc sweep angle in radians
|
|
134
|
+
* @returns a point around ellipse at given angle
|
|
135
|
+
*/
|
|
136
|
+
const arcPoint = (cx, cy, rx, ry, alpha, theta) => {
|
|
137
|
+
const { sin, cos } = Math;
|
|
138
|
+
const cosA = cos(alpha);
|
|
139
|
+
const sinA = sin(alpha);
|
|
140
|
+
const x = rx * cos(theta);
|
|
141
|
+
const y = ry * sin(theta);
|
|
142
|
+
return [cx + cosA * x - sinA * y, cy + sinA * x + cosA * y];
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Returns the angle between two points.
|
|
146
|
+
* @param v0 starting point
|
|
147
|
+
* @param v1 ending point
|
|
148
|
+
* @returns the angle in radian
|
|
149
|
+
*/
|
|
150
|
+
const angleBetween = (v0, v1) => {
|
|
151
|
+
const { x: v0x, y: v0y } = v0;
|
|
152
|
+
const { x: v1x, y: v1y } = v1;
|
|
153
|
+
const p = v0x * v1x + v0y * v1y;
|
|
154
|
+
const n = Math.sqrt((v0x ** 2 + v0y ** 2) * (v1x ** 2 + v1y ** 2));
|
|
155
|
+
return (v0x * v1y - v0y * v1x < 0 ? -1 : 1) * Math.acos(p / n);
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* Returns the following properties for an Arc segment: center, start angle,
|
|
159
|
+
* end angle, and radiuses on X and Y axis.
|
|
160
|
+
*
|
|
161
|
+
* @param x1 the starting point X
|
|
162
|
+
* @param y1 the starting point Y
|
|
163
|
+
* @param RX the radius on X axis
|
|
164
|
+
* @param RY the radius on Y axis
|
|
165
|
+
* @param angle the ellipse rotation in degrees
|
|
166
|
+
* @param LAF the large arc flag
|
|
167
|
+
* @param SF the sweep flag
|
|
168
|
+
* @param x2 the ending point X
|
|
169
|
+
* @param y2 the ending point Y
|
|
170
|
+
* @returns properties specific to Arc segments
|
|
171
|
+
*/
|
|
172
|
+
const getArcProps = (x1, y1, RX, RY, angle, LAF, SF, x, y) => {
|
|
173
|
+
const { abs, sin, cos, sqrt, PI } = Math;
|
|
174
|
+
let rx = abs(RX);
|
|
175
|
+
let ry = abs(RY);
|
|
176
|
+
const xRotRad = (angle % 360 + 360) % 360 * (PI / 180);
|
|
177
|
+
if (x1 === x && y1 === y) return {
|
|
178
|
+
rx,
|
|
179
|
+
ry,
|
|
180
|
+
startAngle: 0,
|
|
181
|
+
endAngle: 0,
|
|
182
|
+
center: {
|
|
183
|
+
x,
|
|
184
|
+
y
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
if (rx === 0 || ry === 0) return {
|
|
188
|
+
rx,
|
|
189
|
+
ry,
|
|
190
|
+
startAngle: 0,
|
|
191
|
+
endAngle: 0,
|
|
192
|
+
center: {
|
|
193
|
+
x: (x + x1) / 2,
|
|
194
|
+
y: (y + y1) / 2
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const dx = (x1 - x) / 2;
|
|
198
|
+
const dy = (y1 - y) / 2;
|
|
199
|
+
const transformedPoint = {
|
|
200
|
+
x: cos(xRotRad) * dx + sin(xRotRad) * dy,
|
|
201
|
+
y: -sin(xRotRad) * dx + cos(xRotRad) * dy
|
|
202
|
+
};
|
|
203
|
+
const radiiCheck = transformedPoint.x ** 2 / rx ** 2 + transformedPoint.y ** 2 / ry ** 2;
|
|
204
|
+
if (radiiCheck > 1) {
|
|
205
|
+
rx *= sqrt(radiiCheck);
|
|
206
|
+
ry *= sqrt(radiiCheck);
|
|
207
|
+
}
|
|
208
|
+
let cRadicand = (rx ** 2 * ry ** 2 - rx ** 2 * transformedPoint.y ** 2 - ry ** 2 * transformedPoint.x ** 2) / (rx ** 2 * transformedPoint.y ** 2 + ry ** 2 * transformedPoint.x ** 2);
|
|
209
|
+
cRadicand = cRadicand < 0 ? 0 : cRadicand;
|
|
210
|
+
const cCoef = (LAF !== SF ? 1 : -1) * sqrt(cRadicand);
|
|
211
|
+
const transformedCenter = {
|
|
212
|
+
x: cCoef * (rx * transformedPoint.y / ry),
|
|
213
|
+
y: cCoef * (-(ry * transformedPoint.x) / rx)
|
|
214
|
+
};
|
|
215
|
+
const center = {
|
|
216
|
+
x: cos(xRotRad) * transformedCenter.x - sin(xRotRad) * transformedCenter.y + (x1 + x) / 2,
|
|
217
|
+
y: sin(xRotRad) * transformedCenter.x + cos(xRotRad) * transformedCenter.y + (y1 + y) / 2
|
|
218
|
+
};
|
|
219
|
+
const startVector = {
|
|
220
|
+
x: (transformedPoint.x - transformedCenter.x) / rx,
|
|
221
|
+
y: (transformedPoint.y - transformedCenter.y) / ry
|
|
222
|
+
};
|
|
223
|
+
const startAngle = angleBetween({
|
|
224
|
+
x: 1,
|
|
225
|
+
y: 0
|
|
226
|
+
}, startVector);
|
|
227
|
+
let sweepAngle = angleBetween(startVector, {
|
|
228
|
+
x: (-transformedPoint.x - transformedCenter.x) / rx,
|
|
229
|
+
y: (-transformedPoint.y - transformedCenter.y) / ry
|
|
230
|
+
});
|
|
231
|
+
if (!SF && sweepAngle > 0) sweepAngle -= 2 * PI;
|
|
232
|
+
else if (SF && sweepAngle < 0) sweepAngle += 2 * PI;
|
|
233
|
+
sweepAngle %= 2 * PI;
|
|
234
|
+
return {
|
|
235
|
+
center,
|
|
236
|
+
startAngle,
|
|
237
|
+
endAngle: startAngle + sweepAngle,
|
|
238
|
+
rx,
|
|
239
|
+
ry
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
/**
|
|
243
|
+
* Returns the length of an Arc segment.
|
|
244
|
+
*
|
|
245
|
+
* @param x1 the starting point X
|
|
246
|
+
* @param y1 the starting point Y
|
|
247
|
+
* @param c1x the first control point X
|
|
248
|
+
* @param c1y the first control point Y
|
|
249
|
+
* @param c2x the second control point X
|
|
250
|
+
* @param c2y the second control point Y
|
|
251
|
+
* @param x2 the ending point X
|
|
252
|
+
* @param y2 the ending point Y
|
|
253
|
+
* @returns the length of the Arc segment
|
|
254
|
+
*/
|
|
255
|
+
const getArcLength = (x1, y1, RX, RY, angle, LAF, SF, x, y) => {
|
|
256
|
+
const { rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
|
|
257
|
+
return arcLength(rx, ry, endAngle - startAngle);
|
|
258
|
+
};
|
|
259
|
+
/**
|
|
260
|
+
* Returns a point along an Arc segment at a given distance.
|
|
261
|
+
*
|
|
262
|
+
* @param x1 the starting point X
|
|
263
|
+
* @param y1 the starting point Y
|
|
264
|
+
* @param RX the radius on X axis
|
|
265
|
+
* @param RY the radius on Y axis
|
|
266
|
+
* @param angle the ellipse rotation in degrees
|
|
267
|
+
* @param LAF the large arc flag
|
|
268
|
+
* @param SF the sweep flag
|
|
269
|
+
* @param x2 the ending point X
|
|
270
|
+
* @param y2 the ending point Y
|
|
271
|
+
* @param distance the distance along the arc
|
|
272
|
+
* @returns a point along the Arc segment
|
|
273
|
+
*/
|
|
274
|
+
const getPointAtArcLength = (x1, y1, RX, RY, angle, LAF, SF, x, y, distance) => {
|
|
275
|
+
let point = {
|
|
276
|
+
x: x1,
|
|
277
|
+
y: y1
|
|
278
|
+
};
|
|
279
|
+
const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
|
|
280
|
+
if (typeof distance === "number") {
|
|
281
|
+
const length = arcLength(rx, ry, endAngle - startAngle);
|
|
282
|
+
if (distance <= 0) point = {
|
|
283
|
+
x: x1,
|
|
284
|
+
y: y1
|
|
285
|
+
};
|
|
286
|
+
else if (distance >= length) point = {
|
|
287
|
+
x,
|
|
288
|
+
y
|
|
289
|
+
};
|
|
290
|
+
else {
|
|
291
|
+
if (x1 === x && y1 === y) return {
|
|
292
|
+
x,
|
|
293
|
+
y
|
|
294
|
+
};
|
|
295
|
+
if (rx === 0 || ry === 0) return getPointAtLineLength(x1, y1, x, y, distance);
|
|
296
|
+
const { PI, cos, sin } = Math;
|
|
297
|
+
const sweepAngle = endAngle - startAngle;
|
|
298
|
+
const xRotRad = (angle % 360 + 360) % 360 * (PI / 180);
|
|
299
|
+
const alpha = startAngle + sweepAngle * (distance / length);
|
|
300
|
+
const ellipseComponentX = rx * cos(alpha);
|
|
301
|
+
const ellipseComponentY = ry * sin(alpha);
|
|
302
|
+
point = {
|
|
303
|
+
x: cos(xRotRad) * ellipseComponentX - sin(xRotRad) * ellipseComponentY + center.x,
|
|
304
|
+
y: sin(xRotRad) * ellipseComponentX + cos(xRotRad) * ellipseComponentY + center.y
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return point;
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* Returns the extrema for an Arc segment in the following format:
|
|
312
|
+
* [MIN_X, MIN_Y, MAX_X, MAX_Y]
|
|
313
|
+
*
|
|
314
|
+
* @see https://github.com/herrstrietzel/svg-pathdata-getbbox
|
|
315
|
+
*
|
|
316
|
+
* @param x1 the starting point X
|
|
317
|
+
* @param y1 the starting point Y
|
|
318
|
+
* @param RX the radius on X axis
|
|
319
|
+
* @param RY the radius on Y axis
|
|
320
|
+
* @param angle the ellipse rotation in degrees
|
|
321
|
+
* @param LAF the large arc flag
|
|
322
|
+
* @param SF the sweep flag
|
|
323
|
+
* @param x2 the ending point X
|
|
324
|
+
* @param y2 the ending point Y
|
|
325
|
+
* @returns the extrema of the Arc segment
|
|
326
|
+
*/
|
|
327
|
+
const getArcBBox = (x1, y1, RX, RY, angle, LAF, SF, x, y) => {
|
|
328
|
+
const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
|
|
329
|
+
const deltaAngle = endAngle - startAngle;
|
|
330
|
+
const { min, max, tan, atan2, PI } = Math;
|
|
331
|
+
const { x: cx, y: cy } = center;
|
|
332
|
+
const alpha = angle * PI / 180;
|
|
333
|
+
const tangent = tan(alpha);
|
|
334
|
+
/**
|
|
335
|
+
* find min/max from zeroes of directional derivative along x and y
|
|
336
|
+
* along x axis
|
|
337
|
+
*/
|
|
338
|
+
const theta = atan2(-ry * tangent, rx);
|
|
339
|
+
const angle1 = theta;
|
|
340
|
+
const angle2 = theta + PI;
|
|
341
|
+
const angle3 = atan2(ry, rx * tangent);
|
|
342
|
+
const angle4 = angle3 + PI;
|
|
343
|
+
const xArray = [x];
|
|
344
|
+
const yArray = [y];
|
|
345
|
+
let xMin = min(x1, x);
|
|
346
|
+
let xMax = max(x1, x);
|
|
347
|
+
let yMin = min(y1, y);
|
|
348
|
+
let yMax = max(y1, y);
|
|
349
|
+
const pP2 = arcPoint(cx, cy, rx, ry, alpha, endAngle - deltaAngle * 1e-5);
|
|
350
|
+
const pP3 = arcPoint(cx, cy, rx, ry, alpha, endAngle - deltaAngle * .99999);
|
|
351
|
+
/**
|
|
352
|
+
* expected extremes
|
|
353
|
+
* if leaving inner bounding box
|
|
354
|
+
* (between segment start and end point)
|
|
355
|
+
* otherwise exclude elliptic extreme points
|
|
356
|
+
*/
|
|
357
|
+
if (pP2[0] > xMax || pP3[0] > xMax) {
|
|
358
|
+
const p1 = arcPoint(cx, cy, rx, ry, alpha, angle1);
|
|
359
|
+
xArray.push(p1[0]);
|
|
360
|
+
yArray.push(p1[1]);
|
|
361
|
+
}
|
|
362
|
+
if (pP2[0] < xMin || pP3[0] < xMin) {
|
|
363
|
+
const p2 = arcPoint(cx, cy, rx, ry, alpha, angle2);
|
|
364
|
+
xArray.push(p2[0]);
|
|
365
|
+
yArray.push(p2[1]);
|
|
366
|
+
}
|
|
367
|
+
if (pP2[1] < yMin || pP3[1] < yMin) {
|
|
368
|
+
const p4 = arcPoint(cx, cy, rx, ry, alpha, angle4);
|
|
369
|
+
xArray.push(p4[0]);
|
|
370
|
+
yArray.push(p4[1]);
|
|
371
|
+
}
|
|
372
|
+
if (pP2[1] > yMax || pP3[1] > yMax) {
|
|
373
|
+
const p3 = arcPoint(cx, cy, rx, ry, alpha, angle3);
|
|
374
|
+
xArray.push(p3[0]);
|
|
375
|
+
yArray.push(p3[1]);
|
|
376
|
+
}
|
|
377
|
+
xMin = min.apply([], xArray);
|
|
378
|
+
yMin = min.apply([], yArray);
|
|
379
|
+
xMax = max.apply([], xArray);
|
|
380
|
+
yMax = max.apply([], yArray);
|
|
381
|
+
return [
|
|
382
|
+
xMin,
|
|
383
|
+
yMin,
|
|
384
|
+
xMax,
|
|
385
|
+
yMax
|
|
386
|
+
];
|
|
387
|
+
};
|
|
388
|
+
const arcTools = {
|
|
389
|
+
angleBetween,
|
|
390
|
+
arcLength,
|
|
391
|
+
arcPoint,
|
|
392
|
+
getArcBBox,
|
|
393
|
+
getArcLength,
|
|
394
|
+
getArcProps,
|
|
395
|
+
getPointAtArcLength
|
|
396
|
+
};
|
|
397
|
+
//#endregion
|
|
398
|
+
//#region src/math/bezier.ts
|
|
399
|
+
/**
|
|
400
|
+
* Tools from bezier.js by Mike 'Pomax' Kamermans
|
|
401
|
+
* @see https://github.com/Pomax/bezierjs
|
|
402
|
+
*/
|
|
403
|
+
const Tvalues = [
|
|
404
|
+
-.06405689286260563,
|
|
405
|
+
.06405689286260563,
|
|
406
|
+
-.1911188674736163,
|
|
407
|
+
.1911188674736163,
|
|
408
|
+
-.3150426796961634,
|
|
409
|
+
.3150426796961634,
|
|
410
|
+
-.4337935076260451,
|
|
411
|
+
.4337935076260451,
|
|
412
|
+
-.5454214713888396,
|
|
413
|
+
.5454214713888396,
|
|
414
|
+
-.6480936519369755,
|
|
415
|
+
.6480936519369755,
|
|
416
|
+
-.7401241915785544,
|
|
417
|
+
.7401241915785544,
|
|
418
|
+
-.820001985973903,
|
|
419
|
+
.820001985973903,
|
|
420
|
+
-.8864155270044011,
|
|
421
|
+
.8864155270044011,
|
|
422
|
+
-.9382745520027328,
|
|
423
|
+
.9382745520027328,
|
|
424
|
+
-.9747285559713095,
|
|
425
|
+
.9747285559713095,
|
|
426
|
+
-.9951872199970213,
|
|
427
|
+
.9951872199970213
|
|
428
|
+
];
|
|
429
|
+
const Cvalues = [
|
|
430
|
+
.12793819534675216,
|
|
431
|
+
.12793819534675216,
|
|
432
|
+
.1258374563468283,
|
|
433
|
+
.1258374563468283,
|
|
434
|
+
.12167047292780339,
|
|
435
|
+
.12167047292780339,
|
|
436
|
+
.1155056680537256,
|
|
437
|
+
.1155056680537256,
|
|
438
|
+
.10744427011596563,
|
|
439
|
+
.10744427011596563,
|
|
440
|
+
.09761865210411388,
|
|
441
|
+
.09761865210411388,
|
|
442
|
+
.08619016153195327,
|
|
443
|
+
.08619016153195327,
|
|
444
|
+
.0733464814110803,
|
|
445
|
+
.0733464814110803,
|
|
446
|
+
.05929858491543678,
|
|
447
|
+
.05929858491543678,
|
|
448
|
+
.04427743881741981,
|
|
449
|
+
.04427743881741981,
|
|
450
|
+
.028531388628933663,
|
|
451
|
+
.028531388628933663,
|
|
452
|
+
.0123412297999872,
|
|
453
|
+
.0123412297999872
|
|
454
|
+
];
|
|
455
|
+
/**
|
|
456
|
+
* @param points
|
|
457
|
+
* @returns
|
|
458
|
+
*/
|
|
459
|
+
const deriveBezier = (points) => {
|
|
460
|
+
const dpoints = [];
|
|
461
|
+
for (let p = points, d = p.length, c = d - 1; d > 1; d -= 1, c -= 1) {
|
|
462
|
+
const list = [];
|
|
463
|
+
for (let j = 0; j < c; j += 1) list.push({
|
|
464
|
+
x: c * (p[j + 1].x - p[j].x),
|
|
465
|
+
y: c * (p[j + 1].y - p[j].y),
|
|
466
|
+
t: 0
|
|
467
|
+
});
|
|
468
|
+
dpoints.push(list);
|
|
469
|
+
p = list;
|
|
470
|
+
}
|
|
471
|
+
return dpoints;
|
|
472
|
+
};
|
|
473
|
+
/**
|
|
474
|
+
* @param points
|
|
475
|
+
* @param t
|
|
476
|
+
*/
|
|
477
|
+
const computeBezier = (points, t) => {
|
|
478
|
+
if (t === 0) {
|
|
479
|
+
points[0].t = 0;
|
|
480
|
+
return points[0];
|
|
481
|
+
}
|
|
482
|
+
const order = points.length - 1;
|
|
483
|
+
if (t === 1) {
|
|
484
|
+
points[order].t = 1;
|
|
485
|
+
return points[order];
|
|
486
|
+
}
|
|
487
|
+
const mt = 1 - t;
|
|
488
|
+
let p = points;
|
|
489
|
+
if (order === 0) {
|
|
490
|
+
points[0].t = t;
|
|
491
|
+
return points[0];
|
|
492
|
+
}
|
|
493
|
+
if (order === 1) return {
|
|
494
|
+
x: mt * p[0].x + t * p[1].x,
|
|
495
|
+
y: mt * p[0].y + t * p[1].y,
|
|
496
|
+
t
|
|
497
|
+
};
|
|
498
|
+
const mt2 = mt * mt;
|
|
499
|
+
const t2 = t * t;
|
|
500
|
+
let a = 0;
|
|
501
|
+
let b = 0;
|
|
502
|
+
let c = 0;
|
|
503
|
+
let d = 0;
|
|
504
|
+
if (order === 2) {
|
|
505
|
+
p = [
|
|
506
|
+
p[0],
|
|
507
|
+
p[1],
|
|
508
|
+
p[2],
|
|
509
|
+
{
|
|
510
|
+
x: 0,
|
|
511
|
+
y: 0
|
|
512
|
+
}
|
|
513
|
+
];
|
|
514
|
+
a = mt2;
|
|
515
|
+
b = mt * t * 2;
|
|
516
|
+
c = t2;
|
|
517
|
+
} else if (order === 3) {
|
|
518
|
+
a = mt2 * mt;
|
|
519
|
+
b = mt2 * t * 3;
|
|
520
|
+
c = mt * t2 * 3;
|
|
521
|
+
d = t * t2;
|
|
522
|
+
}
|
|
523
|
+
return {
|
|
524
|
+
x: a * p[0].x + b * p[1].x + c * p[2].x + d * p[3].x,
|
|
525
|
+
y: a * p[0].y + b * p[1].y + c * p[2].y + d * p[3].y,
|
|
526
|
+
t
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
const calculateBezier = (derivativeFn, t) => {
|
|
530
|
+
const d = derivativeFn(t);
|
|
531
|
+
const l = d.x * d.x + d.y * d.y;
|
|
532
|
+
return Math.sqrt(l);
|
|
533
|
+
};
|
|
534
|
+
const bezierLength = (derivativeFn) => {
|
|
535
|
+
const z = .5;
|
|
536
|
+
const len = Tvalues.length;
|
|
537
|
+
let sum = 0;
|
|
538
|
+
for (let i = 0, t; i < len; i++) {
|
|
539
|
+
t = z * Tvalues[i] + z;
|
|
540
|
+
sum += Cvalues[i] * calculateBezier(derivativeFn, t);
|
|
541
|
+
}
|
|
542
|
+
return z * sum;
|
|
543
|
+
};
|
|
544
|
+
/**
|
|
545
|
+
* Returns the length of CubicBezier / Quad segment.
|
|
546
|
+
* @param curve cubic / quad bezier segment
|
|
547
|
+
*/
|
|
548
|
+
const getBezierLength = (curve) => {
|
|
549
|
+
const points = [];
|
|
550
|
+
for (let idx = 0, len = curve.length, step = 2; idx < len; idx += step) points.push({
|
|
551
|
+
x: curve[idx],
|
|
552
|
+
y: curve[idx + 1]
|
|
553
|
+
});
|
|
554
|
+
const dpoints = deriveBezier(points);
|
|
555
|
+
return bezierLength((t) => {
|
|
556
|
+
return computeBezier(dpoints[0], t);
|
|
557
|
+
});
|
|
558
|
+
};
|
|
559
|
+
const CBEZIER_MINMAX_EPSILON = 1e-8;
|
|
560
|
+
/**
|
|
561
|
+
* Returns the most extreme points in a Quad Bezier segment.
|
|
562
|
+
* @param A an array which consist of X/Y values
|
|
563
|
+
*/
|
|
564
|
+
const minmaxQ = ([v1, cp, v2]) => {
|
|
565
|
+
const min = Math.min(v1, v2);
|
|
566
|
+
const max = Math.max(v1, v2);
|
|
567
|
+
if (cp >= v1 ? v2 >= cp : v2 <= cp) return [min, max];
|
|
568
|
+
const E = (v1 * v2 - cp * cp) / (v1 - 2 * cp + v2);
|
|
569
|
+
return E < min ? [E, max] : [min, E];
|
|
570
|
+
};
|
|
571
|
+
/**
|
|
572
|
+
* Returns the most extreme points in a Cubic Bezier segment.
|
|
573
|
+
* @param A an array which consist of X/Y values
|
|
574
|
+
* @see https://github.com/kpym/SVGPathy/blob/acd1a50c626b36d81969f6e98e8602e128ba4302/lib/box.js#L127
|
|
575
|
+
*/
|
|
576
|
+
const minmaxC = ([v1, cp1, cp2, v2]) => {
|
|
577
|
+
const K = v1 - 3 * cp1 + 3 * cp2 - v2;
|
|
578
|
+
if (Math.abs(K) < 1e-8) {
|
|
579
|
+
if (v1 === v2 && v1 === cp1) return [v1, v2];
|
|
580
|
+
return minmaxQ([
|
|
581
|
+
v1,
|
|
582
|
+
-.5 * v1 + 1.5 * cp1,
|
|
583
|
+
v1 - 3 * cp1 + 3 * cp2
|
|
584
|
+
]);
|
|
585
|
+
}
|
|
586
|
+
const T = -v1 * cp2 + v1 * v2 - cp1 * cp2 - cp1 * v2 + cp1 * cp1 + cp2 * cp2;
|
|
587
|
+
if (T <= 0) return [Math.min(v1, v2), Math.max(v1, v2)];
|
|
588
|
+
const S = Math.sqrt(T);
|
|
589
|
+
let min = Math.min(v1, v2);
|
|
590
|
+
let max = Math.max(v1, v2);
|
|
591
|
+
const L = v1 - 2 * cp1 + cp2;
|
|
592
|
+
for (let R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) if (R > 0 && R < 1) {
|
|
593
|
+
const Q = v1 * (1 - R) * (1 - R) * (1 - R) + cp1 * 3 * (1 - R) * (1 - R) * R + cp2 * 3 * (1 - R) * R * R + v2 * R * R * R;
|
|
594
|
+
if (Q < min) min = Q;
|
|
595
|
+
if (Q > max) max = Q;
|
|
596
|
+
}
|
|
597
|
+
return [min, max];
|
|
598
|
+
};
|
|
599
|
+
const bezierTools = {
|
|
600
|
+
bezierLength,
|
|
601
|
+
calculateBezier,
|
|
602
|
+
CBEZIER_MINMAX_EPSILON,
|
|
603
|
+
computeBezier,
|
|
604
|
+
Cvalues,
|
|
605
|
+
deriveBezier,
|
|
606
|
+
getBezierLength,
|
|
607
|
+
minmaxC,
|
|
608
|
+
minmaxQ,
|
|
609
|
+
Tvalues
|
|
610
|
+
};
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/math/cubicTools.ts
|
|
613
|
+
/**
|
|
614
|
+
* Returns a point at a given length of a CubicBezier segment.
|
|
615
|
+
*
|
|
616
|
+
* @param x1 the starting point X
|
|
617
|
+
* @param y1 the starting point Y
|
|
618
|
+
* @param c1x the first control point X
|
|
619
|
+
* @param c1y the first control point Y
|
|
620
|
+
* @param c2x the second control point X
|
|
621
|
+
* @param c2y the second control point Y
|
|
622
|
+
* @param x2 the ending point X
|
|
623
|
+
* @param y2 the ending point Y
|
|
624
|
+
* @param t a [0-1] ratio
|
|
625
|
+
* @returns the point at cubic-bezier segment length
|
|
626
|
+
*/
|
|
627
|
+
const getPointAtCubicSegmentLength = ([x1, y1, c1x, c1y, c2x, c2y, x2, y2], t) => {
|
|
628
|
+
const t1 = 1 - t;
|
|
629
|
+
return {
|
|
630
|
+
x: t1 ** 3 * x1 + 3 * t1 ** 2 * t * c1x + 3 * t1 * t ** 2 * c2x + t ** 3 * x2,
|
|
631
|
+
y: t1 ** 3 * y1 + 3 * t1 ** 2 * t * c1y + 3 * t1 * t ** 2 * c2y + t ** 3 * y2
|
|
632
|
+
};
|
|
633
|
+
};
|
|
634
|
+
/**
|
|
635
|
+
* Returns the length of a CubicBezier segment.
|
|
636
|
+
*
|
|
637
|
+
* @param x1 the starting point X
|
|
638
|
+
* @param y1 the starting point Y
|
|
639
|
+
* @param c1x the first control point X
|
|
640
|
+
* @param c1y the first control point Y
|
|
641
|
+
* @param c2x the second control point X
|
|
642
|
+
* @param c2y the second control point Y
|
|
643
|
+
* @param x2 the ending point X
|
|
644
|
+
* @param y2 the ending point Y
|
|
645
|
+
* @returns the CubicBezier segment length
|
|
646
|
+
*/
|
|
647
|
+
const getCubicLength = (x1, y1, c1x, c1y, c2x, c2y, x2, y2) => {
|
|
648
|
+
return getBezierLength([
|
|
649
|
+
x1,
|
|
650
|
+
y1,
|
|
651
|
+
c1x,
|
|
652
|
+
c1y,
|
|
653
|
+
c2x,
|
|
654
|
+
c2y,
|
|
655
|
+
x2,
|
|
656
|
+
y2
|
|
657
|
+
]);
|
|
658
|
+
};
|
|
659
|
+
/**
|
|
660
|
+
* Returns the point along a CubicBezier segment at a given distance.
|
|
661
|
+
*
|
|
662
|
+
* @param x1 the starting point X
|
|
663
|
+
* @param y1 the starting point Y
|
|
664
|
+
* @param c1x the first control point X
|
|
665
|
+
* @param c1y the first control point Y
|
|
666
|
+
* @param c2x the second control point X
|
|
667
|
+
* @param c2y the second control point Y
|
|
668
|
+
* @param x2 the ending point X
|
|
669
|
+
* @param y2 the ending point Y
|
|
670
|
+
* @param distance the distance to look at
|
|
671
|
+
* @returns the point at CubicBezier length
|
|
672
|
+
*/
|
|
673
|
+
const getPointAtCubicLength = (x1, y1, c1x, c1y, c2x, c2y, x2, y2, distance) => {
|
|
674
|
+
const distanceIsNumber = typeof distance === "number";
|
|
675
|
+
let point = {
|
|
676
|
+
x: x1,
|
|
677
|
+
y: y1
|
|
678
|
+
};
|
|
679
|
+
if (distanceIsNumber) {
|
|
680
|
+
const currentLength = getBezierLength([
|
|
681
|
+
x1,
|
|
682
|
+
y1,
|
|
683
|
+
c1x,
|
|
684
|
+
c1y,
|
|
685
|
+
c2x,
|
|
686
|
+
c2y,
|
|
687
|
+
x2,
|
|
688
|
+
y2
|
|
689
|
+
]);
|
|
690
|
+
if (distance <= 0) {} else if (distance >= currentLength) point = {
|
|
691
|
+
x: x2,
|
|
692
|
+
y: y2
|
|
693
|
+
};
|
|
694
|
+
else point = getPointAtCubicSegmentLength([
|
|
695
|
+
x1,
|
|
696
|
+
y1,
|
|
697
|
+
c1x,
|
|
698
|
+
c1y,
|
|
699
|
+
c2x,
|
|
700
|
+
c2y,
|
|
701
|
+
x2,
|
|
702
|
+
y2
|
|
703
|
+
], distance / currentLength);
|
|
704
|
+
}
|
|
705
|
+
return point;
|
|
706
|
+
};
|
|
707
|
+
/**
|
|
708
|
+
* Returns the bounding box of a CubicBezier segment in the following format:
|
|
709
|
+
* [MIN_X, MIN_Y, MAX_X, MAX_Y]
|
|
710
|
+
*
|
|
711
|
+
* @param x1 the starting point X
|
|
712
|
+
* @param y1 the starting point Y
|
|
713
|
+
* @param c1x the first control point X
|
|
714
|
+
* @param c1y the first control point Y
|
|
715
|
+
* @param c2x the second control point X
|
|
716
|
+
* @param c2y the second control point Y
|
|
717
|
+
* @param x2 the ending point X
|
|
718
|
+
* @param y2 the ending point Y
|
|
719
|
+
* @returns the extrema of the CubicBezier segment
|
|
720
|
+
*/
|
|
721
|
+
const getCubicBBox = (x1, y1, c1x, c1y, c2x, c2y, x2, y2) => {
|
|
722
|
+
const cxMinMax = minmaxC([
|
|
723
|
+
x1,
|
|
724
|
+
c1x,
|
|
725
|
+
c2x,
|
|
726
|
+
x2
|
|
727
|
+
]);
|
|
728
|
+
const cyMinMax = minmaxC([
|
|
729
|
+
y1,
|
|
730
|
+
c1y,
|
|
731
|
+
c2y,
|
|
732
|
+
y2
|
|
733
|
+
]);
|
|
734
|
+
return [
|
|
735
|
+
cxMinMax[0],
|
|
736
|
+
cyMinMax[0],
|
|
737
|
+
cxMinMax[1],
|
|
738
|
+
cyMinMax[1]
|
|
739
|
+
];
|
|
740
|
+
};
|
|
741
|
+
const cubicTools = {
|
|
742
|
+
getCubicBBox,
|
|
743
|
+
getCubicLength,
|
|
744
|
+
getPointAtCubicLength,
|
|
745
|
+
getPointAtCubicSegmentLength
|
|
746
|
+
};
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/math/quadTools.ts
|
|
749
|
+
/**
|
|
750
|
+
* Returns the {x,y} coordinates of a point at a
|
|
751
|
+
* given length of a quadratic-bezier segment.
|
|
752
|
+
*
|
|
753
|
+
* @see https://github.com/substack/point-at-length
|
|
754
|
+
*
|
|
755
|
+
* @param x1 the starting point X
|
|
756
|
+
* @param y1 the starting point Y
|
|
757
|
+
* @param cx the control point X
|
|
758
|
+
* @param cy the control point Y
|
|
759
|
+
* @param x2 the ending point X
|
|
760
|
+
* @param y2 the ending point Y
|
|
761
|
+
* @param t a [0-1] ratio
|
|
762
|
+
* @returns the requested {x,y} coordinates
|
|
763
|
+
*/
|
|
764
|
+
const getPointAtQuadSegmentLength = ([x1, y1, cx, cy, x2, y2], t) => {
|
|
765
|
+
const t1 = 1 - t;
|
|
766
|
+
return {
|
|
767
|
+
x: t1 ** 2 * x1 + 2 * t1 * t * cx + t ** 2 * x2,
|
|
768
|
+
y: t1 ** 2 * y1 + 2 * t1 * t * cy + t ** 2 * y2
|
|
769
|
+
};
|
|
770
|
+
};
|
|
771
|
+
/**
|
|
772
|
+
* Returns the length of a QuadraticBezier segment.
|
|
773
|
+
*
|
|
774
|
+
* @param x1 the starting point X
|
|
775
|
+
* @param y1 the starting point Y
|
|
776
|
+
* @param cx the control point X
|
|
777
|
+
* @param cy the control point Y
|
|
778
|
+
* @param x2 the ending point X
|
|
779
|
+
* @param y2 the ending point Y
|
|
780
|
+
* @returns the QuadraticBezier segment length
|
|
781
|
+
*/
|
|
782
|
+
const getQuadLength = (x1, y1, cx, cy, x2, y2) => {
|
|
783
|
+
return getBezierLength([
|
|
784
|
+
x1,
|
|
785
|
+
y1,
|
|
786
|
+
cx,
|
|
787
|
+
cy,
|
|
788
|
+
x2,
|
|
789
|
+
y2
|
|
790
|
+
]);
|
|
791
|
+
};
|
|
792
|
+
/**
|
|
793
|
+
* Returns the point along a QuadraticBezier segment at a given distance.
|
|
794
|
+
*
|
|
795
|
+
* @param x1 the starting point X
|
|
796
|
+
* @param y1 the starting point Y
|
|
797
|
+
* @param cx the control point X
|
|
798
|
+
* @param cy the control point Y
|
|
799
|
+
* @param x2 the ending point X
|
|
800
|
+
* @param y2 the ending point Y
|
|
801
|
+
* @param distance the distance to look at
|
|
802
|
+
* @returns the point at QuadraticBezier length
|
|
803
|
+
*/
|
|
804
|
+
const getPointAtQuadLength = (x1, y1, cx, cy, x2, y2, distance) => {
|
|
805
|
+
const distanceIsNumber = typeof distance === "number";
|
|
806
|
+
let point = {
|
|
807
|
+
x: x1,
|
|
808
|
+
y: y1
|
|
809
|
+
};
|
|
810
|
+
if (distanceIsNumber) {
|
|
811
|
+
const currentLength = getBezierLength([
|
|
812
|
+
x1,
|
|
813
|
+
y1,
|
|
814
|
+
cx,
|
|
815
|
+
cy,
|
|
816
|
+
x2,
|
|
817
|
+
y2
|
|
818
|
+
]);
|
|
819
|
+
if (distance <= 0) {} else if (distance >= currentLength) point = {
|
|
820
|
+
x: x2,
|
|
821
|
+
y: y2
|
|
822
|
+
};
|
|
823
|
+
else point = getPointAtQuadSegmentLength([
|
|
824
|
+
x1,
|
|
825
|
+
y1,
|
|
826
|
+
cx,
|
|
827
|
+
cy,
|
|
828
|
+
x2,
|
|
829
|
+
y2
|
|
830
|
+
], distance / currentLength);
|
|
831
|
+
}
|
|
832
|
+
return point;
|
|
833
|
+
};
|
|
834
|
+
/**
|
|
835
|
+
* Returns the bounding box of a QuadraticBezier segment in the following format:
|
|
836
|
+
* [MIN_X, MIN_Y, MAX_X, MAX_Y]
|
|
837
|
+
*
|
|
838
|
+
* @param x1 the starting point X
|
|
839
|
+
* @param y1 the starting point Y
|
|
840
|
+
* @param cx the control point X
|
|
841
|
+
* @param cy the control point Y
|
|
842
|
+
* @param x2 the ending point X
|
|
843
|
+
* @param y2 the ending point Y
|
|
844
|
+
* @returns the extrema of the QuadraticBezier segment
|
|
845
|
+
*/
|
|
846
|
+
const getQuadBBox = (x1, y1, cx, cy, x2, y2) => {
|
|
847
|
+
const cxMinMax = minmaxQ([
|
|
848
|
+
x1,
|
|
849
|
+
cx,
|
|
850
|
+
x2
|
|
851
|
+
]);
|
|
852
|
+
const cyMinMax = minmaxQ([
|
|
853
|
+
y1,
|
|
854
|
+
cy,
|
|
855
|
+
y2
|
|
856
|
+
]);
|
|
857
|
+
return [
|
|
858
|
+
cxMinMax[0],
|
|
859
|
+
cyMinMax[0],
|
|
860
|
+
cxMinMax[1],
|
|
861
|
+
cyMinMax[1]
|
|
862
|
+
];
|
|
863
|
+
};
|
|
864
|
+
const quadTools = {
|
|
865
|
+
getPointAtQuadLength,
|
|
866
|
+
getPointAtQuadSegmentLength,
|
|
867
|
+
getQuadBBox,
|
|
868
|
+
getQuadLength
|
|
869
|
+
};
|
|
870
|
+
//#endregion
|
|
871
|
+
//#region src/math/polygonTools.ts
|
|
872
|
+
/**
|
|
873
|
+
* d3-polygon-area
|
|
874
|
+
* @see https://github.com/d3/d3-polygon
|
|
875
|
+
*
|
|
876
|
+
* Returns the area of a polygon.
|
|
877
|
+
*
|
|
878
|
+
* @param polygon Array of [x, y]
|
|
879
|
+
* @returns Signed area
|
|
880
|
+
*/
|
|
881
|
+
const polygonArea = (polygon) => {
|
|
882
|
+
const n = polygon.length;
|
|
883
|
+
let i = -1;
|
|
884
|
+
let a;
|
|
885
|
+
let b = polygon[n - 1];
|
|
886
|
+
let area = 0;
|
|
887
|
+
while (++i < n) {
|
|
888
|
+
a = b;
|
|
889
|
+
b = polygon[i];
|
|
890
|
+
area += a[1] * b[0] - a[0] * b[1];
|
|
891
|
+
}
|
|
892
|
+
return area / 2;
|
|
893
|
+
};
|
|
894
|
+
/**
|
|
895
|
+
* d3-polygon-length
|
|
896
|
+
* https://github.com/d3/d3-polygon
|
|
897
|
+
*
|
|
898
|
+
* Returns the perimeter of a polygon.
|
|
899
|
+
*
|
|
900
|
+
* @param polygon an array of coordinates
|
|
901
|
+
* @returns the polygon length
|
|
902
|
+
*/
|
|
903
|
+
const polygonLength = (polygon) => {
|
|
904
|
+
return polygon.reduce((length, point, i) => {
|
|
905
|
+
if (i) return length + distanceSquareRoot(polygon[i - 1], point);
|
|
906
|
+
return 0;
|
|
907
|
+
}, 0);
|
|
908
|
+
};
|
|
909
|
+
/**
|
|
910
|
+
* Computes the centroid (geometric center) of a polygon.
|
|
911
|
+
* Uses average of all endpoint coordinates (robust for polygons and curves).
|
|
912
|
+
*
|
|
913
|
+
* @param polygon A polygon with consists of [x, y] tuples
|
|
914
|
+
* @returns [x, y] centroid
|
|
915
|
+
*/
|
|
916
|
+
const polygonCentroid = (polygon) => {
|
|
917
|
+
if (polygon.length === 0) return [0, 0];
|
|
918
|
+
let sumX = 0;
|
|
919
|
+
let sumY = 0;
|
|
920
|
+
for (const [x, y] of polygon) {
|
|
921
|
+
sumX += x;
|
|
922
|
+
sumY += y;
|
|
923
|
+
}
|
|
924
|
+
const count = polygon.length;
|
|
925
|
+
return [sumX / count, sumY / count];
|
|
926
|
+
};
|
|
927
|
+
const polygonTools = {
|
|
928
|
+
polygonArea,
|
|
929
|
+
polygonLength,
|
|
930
|
+
polygonCentroid
|
|
931
|
+
};
|
|
932
|
+
//#endregion
|
|
933
|
+
//#region src/math/rotateVector.ts
|
|
934
|
+
/**
|
|
935
|
+
* Returns an {x,y} vector rotated by a given
|
|
936
|
+
* angle in radian.
|
|
937
|
+
*
|
|
938
|
+
* @param x the initial vector x
|
|
939
|
+
* @param y the initial vector y
|
|
940
|
+
* @param rad the radian vector angle
|
|
941
|
+
* @returns the rotated vector
|
|
942
|
+
*/
|
|
943
|
+
const rotateVector = (x, y, rad) => {
|
|
944
|
+
const { sin, cos } = Math;
|
|
945
|
+
return {
|
|
946
|
+
x: x * cos(rad) - y * sin(rad),
|
|
947
|
+
y: x * sin(rad) + y * cos(rad)
|
|
948
|
+
};
|
|
949
|
+
};
|
|
950
|
+
//#endregion
|
|
951
|
+
//#region src/math/roundTo.ts
|
|
952
|
+
/**
|
|
953
|
+
* Rounds a number to the specified number of decimal places.
|
|
954
|
+
*
|
|
955
|
+
* @param n - The number to round
|
|
956
|
+
* @param round - Number of decimal places
|
|
957
|
+
* @returns The rounded number
|
|
958
|
+
*/
|
|
959
|
+
const roundTo = (n, round) => {
|
|
960
|
+
const pow = round >= 1 ? 10 ** round : 1;
|
|
961
|
+
return round > 0 ? Math.round(n * pow) / pow : Math.round(n);
|
|
962
|
+
};
|
|
963
|
+
//#endregion
|
|
964
|
+
//#region src/options/options.ts
|
|
965
|
+
/** SVGPathCommander default options */
|
|
966
|
+
const defaultOptions = {
|
|
967
|
+
origin: [
|
|
968
|
+
0,
|
|
969
|
+
0,
|
|
970
|
+
0
|
|
971
|
+
],
|
|
972
|
+
round: 4
|
|
973
|
+
};
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region src/parser/paramsCount.ts
|
|
976
|
+
/** Segment params length */
|
|
977
|
+
const paramsCounts = {
|
|
978
|
+
a: 7,
|
|
979
|
+
c: 6,
|
|
980
|
+
h: 1,
|
|
981
|
+
l: 2,
|
|
982
|
+
m: 2,
|
|
983
|
+
r: 4,
|
|
984
|
+
q: 4,
|
|
985
|
+
s: 4,
|
|
986
|
+
t: 2,
|
|
987
|
+
v: 1,
|
|
988
|
+
z: 0
|
|
989
|
+
};
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/parser/finalizeSegment.ts
|
|
992
|
+
/**
|
|
993
|
+
* Breaks the parsing of a pathString once a segment is finalized.
|
|
994
|
+
*
|
|
995
|
+
* @param path - The PathParser instance
|
|
996
|
+
*/
|
|
997
|
+
const finalizeSegment = (path) => {
|
|
998
|
+
let pathCommand = path.pathValue[path.segmentStart];
|
|
999
|
+
let relativeCommand = pathCommand.toLowerCase();
|
|
1000
|
+
const { data } = path;
|
|
1001
|
+
while (data.length >= paramsCounts[relativeCommand]) {
|
|
1002
|
+
if (relativeCommand === "m" && data.length > 2) {
|
|
1003
|
+
path.segments.push([pathCommand].concat(data.splice(0, 2)));
|
|
1004
|
+
relativeCommand = "l";
|
|
1005
|
+
pathCommand = pathCommand === "m" ? "l" : "L";
|
|
1006
|
+
} else path.segments.push([pathCommand].concat(data.splice(0, paramsCounts[relativeCommand])));
|
|
1007
|
+
if (!paramsCounts[relativeCommand]) break;
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/util/error.ts
|
|
1012
|
+
/** Error prefix used in all SVGPathCommander TypeError messages. */
|
|
1013
|
+
const error = "SVGPathCommanderError";
|
|
1014
|
+
//#endregion
|
|
1015
|
+
//#region src/parser/scanFlag.ts
|
|
1016
|
+
/**
|
|
1017
|
+
* Validates an A (arc-to) specific path command value.
|
|
1018
|
+
* Usually a `large-arc-flag` or `sweep-flag`.
|
|
1019
|
+
*
|
|
1020
|
+
* @param path - The PathParser instance
|
|
1021
|
+
*/
|
|
1022
|
+
const scanFlag = (path) => {
|
|
1023
|
+
const { index, pathValue } = path;
|
|
1024
|
+
const code = pathValue.charCodeAt(index);
|
|
1025
|
+
if (code === 48) {
|
|
1026
|
+
path.param = 0;
|
|
1027
|
+
path.index += 1;
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (code === 49) {
|
|
1031
|
+
path.param = 1;
|
|
1032
|
+
path.index += 1;
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
path.err = `${error}: invalid Arc flag "${pathValue[index]}", expecting 0 or 1 at index ${index}`;
|
|
1036
|
+
};
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/parser/isDigit.ts
|
|
1039
|
+
/**
|
|
1040
|
+
* Checks if a character is a digit.
|
|
1041
|
+
*
|
|
1042
|
+
* @param code the character to check
|
|
1043
|
+
* @returns check result
|
|
1044
|
+
*/
|
|
1045
|
+
const isDigit = (code) => {
|
|
1046
|
+
return code >= 48 && code <= 57;
|
|
1047
|
+
};
|
|
1048
|
+
//#endregion
|
|
1049
|
+
//#region src/parser/invalidPathValue.ts
|
|
1050
|
+
/** Error message prefix used when a path string cannot be parsed. */
|
|
1051
|
+
const invalidPathValue = "Invalid path value";
|
|
1052
|
+
//#endregion
|
|
1053
|
+
//#region src/parser/scanParam.ts
|
|
1054
|
+
/**
|
|
1055
|
+
* Validates every character of the path string,
|
|
1056
|
+
* every path command, negative numbers or floating point numbers.
|
|
1057
|
+
*
|
|
1058
|
+
* @param path - The PathParser instance
|
|
1059
|
+
*/
|
|
1060
|
+
const scanParam = (path) => {
|
|
1061
|
+
const { max, pathValue, index: start } = path;
|
|
1062
|
+
let index = start;
|
|
1063
|
+
let zeroFirst = false;
|
|
1064
|
+
let hasCeiling = false;
|
|
1065
|
+
let hasDecimal = false;
|
|
1066
|
+
let hasDot = false;
|
|
1067
|
+
let ch;
|
|
1068
|
+
if (index >= max) {
|
|
1069
|
+
path.err = `${error}: ${invalidPathValue} at index ${index}, "pathValue" is missing param`;
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
ch = pathValue.charCodeAt(index);
|
|
1073
|
+
if (ch === 43 || ch === 45) {
|
|
1074
|
+
index += 1;
|
|
1075
|
+
ch = pathValue.charCodeAt(index);
|
|
1076
|
+
}
|
|
1077
|
+
if (!isDigit(ch) && ch !== 46) {
|
|
1078
|
+
path.err = `${error}: ${invalidPathValue} at index ${index}, "${pathValue[index]}" is not a number`;
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (ch !== 46) {
|
|
1082
|
+
zeroFirst = ch === 48;
|
|
1083
|
+
index += 1;
|
|
1084
|
+
ch = pathValue.charCodeAt(index);
|
|
1085
|
+
if (zeroFirst && index < max) {
|
|
1086
|
+
if (ch && isDigit(ch)) {
|
|
1087
|
+
path.err = `${error}: ${invalidPathValue} at index ${start}, "${pathValue[start]}" illegal number`;
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
while (index < max && isDigit(pathValue.charCodeAt(index))) {
|
|
1092
|
+
index += 1;
|
|
1093
|
+
hasCeiling = true;
|
|
1094
|
+
}
|
|
1095
|
+
ch = pathValue.charCodeAt(index);
|
|
1096
|
+
}
|
|
1097
|
+
if (ch === 46) {
|
|
1098
|
+
hasDot = true;
|
|
1099
|
+
index += 1;
|
|
1100
|
+
while (isDigit(pathValue.charCodeAt(index))) {
|
|
1101
|
+
index += 1;
|
|
1102
|
+
hasDecimal = true;
|
|
1103
|
+
}
|
|
1104
|
+
ch = pathValue.charCodeAt(index);
|
|
1105
|
+
}
|
|
1106
|
+
if (ch === 101 || ch === 69) {
|
|
1107
|
+
if (hasDot && !hasCeiling && !hasDecimal) {
|
|
1108
|
+
path.err = `${error}: ${invalidPathValue} at index ${index}, "${pathValue[index]}" invalid float exponent`;
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
index += 1;
|
|
1112
|
+
ch = pathValue.charCodeAt(index);
|
|
1113
|
+
if (ch === 43 || ch === 45) index += 1;
|
|
1114
|
+
if (index < max && isDigit(pathValue.charCodeAt(index))) while (index < max && isDigit(pathValue.charCodeAt(index))) index += 1;
|
|
1115
|
+
else {
|
|
1116
|
+
path.err = `${error}: ${invalidPathValue} at index ${index}, "${pathValue[index]}" invalid integer exponent`;
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
path.index = index;
|
|
1121
|
+
path.param = +path.pathValue.slice(start, index);
|
|
1122
|
+
};
|
|
1123
|
+
//#endregion
|
|
1124
|
+
//#region src/parser/isSpace.ts
|
|
1125
|
+
/**
|
|
1126
|
+
* Checks if the character is a space.
|
|
1127
|
+
*
|
|
1128
|
+
* @param ch the character to check
|
|
1129
|
+
* @returns check result
|
|
1130
|
+
*/
|
|
1131
|
+
const isSpace = (ch) => {
|
|
1132
|
+
return [
|
|
1133
|
+
5760,
|
|
1134
|
+
6158,
|
|
1135
|
+
8192,
|
|
1136
|
+
8193,
|
|
1137
|
+
8194,
|
|
1138
|
+
8195,
|
|
1139
|
+
8196,
|
|
1140
|
+
8197,
|
|
1141
|
+
8198,
|
|
1142
|
+
8199,
|
|
1143
|
+
8200,
|
|
1144
|
+
8201,
|
|
1145
|
+
8202,
|
|
1146
|
+
8239,
|
|
1147
|
+
8287,
|
|
1148
|
+
12288,
|
|
1149
|
+
65279,
|
|
1150
|
+
10,
|
|
1151
|
+
13,
|
|
1152
|
+
8232,
|
|
1153
|
+
8233,
|
|
1154
|
+
32,
|
|
1155
|
+
9,
|
|
1156
|
+
11,
|
|
1157
|
+
12,
|
|
1158
|
+
160
|
|
1159
|
+
].includes(ch);
|
|
1160
|
+
};
|
|
1161
|
+
//#endregion
|
|
1162
|
+
//#region src/parser/skipSpaces.ts
|
|
1163
|
+
/**
|
|
1164
|
+
* Points the parser to the next character in the
|
|
1165
|
+
* path string every time it encounters any kind of
|
|
1166
|
+
* space character.
|
|
1167
|
+
*
|
|
1168
|
+
* @param path - The PathParser instance
|
|
1169
|
+
*/
|
|
1170
|
+
const skipSpaces = (path) => {
|
|
1171
|
+
const { pathValue, max } = path;
|
|
1172
|
+
while (path.index < max && isSpace(pathValue.charCodeAt(path.index))) path.index += 1;
|
|
1173
|
+
};
|
|
1174
|
+
//#endregion
|
|
1175
|
+
//#region src/parser/isPathCommand.ts
|
|
1176
|
+
/**
|
|
1177
|
+
* Checks if the character is a path command.
|
|
1178
|
+
*
|
|
1179
|
+
* @param code the character to check
|
|
1180
|
+
* @returns check result
|
|
1181
|
+
*/
|
|
1182
|
+
const isPathCommand = (code) => {
|
|
1183
|
+
switch (code | 32) {
|
|
1184
|
+
case 109:
|
|
1185
|
+
case 122:
|
|
1186
|
+
case 108:
|
|
1187
|
+
case 104:
|
|
1188
|
+
case 118:
|
|
1189
|
+
case 99:
|
|
1190
|
+
case 115:
|
|
1191
|
+
case 113:
|
|
1192
|
+
case 116:
|
|
1193
|
+
case 97: return true;
|
|
1194
|
+
default: return false;
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
//#endregion
|
|
1198
|
+
//#region src/parser/isDigitStart.ts
|
|
1199
|
+
/**
|
|
1200
|
+
* Checks if the character is or belongs to a number.
|
|
1201
|
+
* [0-9]|+|-|.
|
|
1202
|
+
*
|
|
1203
|
+
* @param code the character to check
|
|
1204
|
+
* @returns check result
|
|
1205
|
+
*/
|
|
1206
|
+
const isDigitStart = (code) => {
|
|
1207
|
+
return isDigit(code) || code === 43 || code === 45 || code === 46;
|
|
1208
|
+
};
|
|
1209
|
+
//#endregion
|
|
1210
|
+
//#region src/parser/isArcCommand.ts
|
|
1211
|
+
/**
|
|
1212
|
+
* Checks if the character is an A (arc-to) path command.
|
|
1213
|
+
*
|
|
1214
|
+
* @param code the character to check
|
|
1215
|
+
* @returns check result
|
|
1216
|
+
*/
|
|
1217
|
+
const isArcCommand = (code) => {
|
|
1218
|
+
return (code | 32) === 97;
|
|
1219
|
+
};
|
|
1220
|
+
//#endregion
|
|
1221
|
+
//#region src/parser/isMoveCommand.ts
|
|
1222
|
+
/**
|
|
1223
|
+
* Checks if the character is a MoveTo command.
|
|
1224
|
+
*
|
|
1225
|
+
* @param code the character to check
|
|
1226
|
+
* @returns check result
|
|
1227
|
+
*/
|
|
1228
|
+
const isMoveCommand = (code) => {
|
|
1229
|
+
switch (code | 32) {
|
|
1230
|
+
case 109:
|
|
1231
|
+
case 77: return true;
|
|
1232
|
+
default: return false;
|
|
1233
|
+
}
|
|
1234
|
+
};
|
|
1235
|
+
//#endregion
|
|
1236
|
+
//#region src/parser/scanSegment.ts
|
|
1237
|
+
/**
|
|
1238
|
+
* Scans every character in the path string to determine
|
|
1239
|
+
* where a segment starts and where it ends.
|
|
1240
|
+
*
|
|
1241
|
+
* @param path - The PathParser instance
|
|
1242
|
+
*/
|
|
1243
|
+
const scanSegment = (path) => {
|
|
1244
|
+
const { max, pathValue, index, segments } = path;
|
|
1245
|
+
const cmdCode = pathValue.charCodeAt(index);
|
|
1246
|
+
const reqParams = paramsCounts[pathValue[index].toLowerCase()];
|
|
1247
|
+
path.segmentStart = index;
|
|
1248
|
+
if (!isPathCommand(cmdCode)) {
|
|
1249
|
+
path.err = `${error}: ${invalidPathValue} "${pathValue[index]}" is not a path command at index ${index}`;
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const lastSegment = segments[segments.length - 1];
|
|
1253
|
+
if (!isMoveCommand(cmdCode) && lastSegment?.[0]?.toLocaleLowerCase() === "z") {
|
|
1254
|
+
path.err = `${error}: ${invalidPathValue} "${pathValue[index]}" is not a MoveTo path command at index ${index}`;
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
path.index += 1;
|
|
1258
|
+
skipSpaces(path);
|
|
1259
|
+
path.data = [];
|
|
1260
|
+
if (!reqParams) {
|
|
1261
|
+
finalizeSegment(path);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
for (;;) {
|
|
1265
|
+
for (let i = reqParams; i > 0; i -= 1) {
|
|
1266
|
+
if (isArcCommand(cmdCode) && (i === 3 || i === 4)) scanFlag(path);
|
|
1267
|
+
else scanParam(path);
|
|
1268
|
+
if (path.err.length) return;
|
|
1269
|
+
path.data.push(path.param);
|
|
1270
|
+
skipSpaces(path);
|
|
1271
|
+
if (path.index < max && pathValue.charCodeAt(path.index) === 44) {
|
|
1272
|
+
path.index += 1;
|
|
1273
|
+
skipSpaces(path);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (path.index >= path.max) break;
|
|
1277
|
+
if (!isDigitStart(pathValue.charCodeAt(path.index))) break;
|
|
1278
|
+
}
|
|
1279
|
+
finalizeSegment(path);
|
|
1280
|
+
};
|
|
1281
|
+
//#endregion
|
|
1282
|
+
//#region src/parser/pathParser.ts
|
|
1283
|
+
/**
|
|
1284
|
+
* The `PathParser` is used by the `parsePathString` static method
|
|
1285
|
+
* to generate a `pathArray`.
|
|
1286
|
+
*
|
|
1287
|
+
* @param pathString - The SVG path string to parse
|
|
1288
|
+
*/
|
|
1289
|
+
var PathParser = class {
|
|
1290
|
+
constructor(pathString) {
|
|
1291
|
+
this.segments = [];
|
|
1292
|
+
this.pathValue = pathString;
|
|
1293
|
+
this.max = pathString.length;
|
|
1294
|
+
this.index = 0;
|
|
1295
|
+
this.param = 0;
|
|
1296
|
+
this.segmentStart = 0;
|
|
1297
|
+
this.data = [];
|
|
1298
|
+
this.err = "";
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
//#endregion
|
|
1302
|
+
//#region src/parser/parsePathString.ts
|
|
1303
|
+
/**
|
|
1304
|
+
* Parses a path string value and returns an array
|
|
1305
|
+
* of segments we like to call `PathArray`.
|
|
1306
|
+
*
|
|
1307
|
+
* If parameter value is already a `PathArray`,
|
|
1308
|
+
* return a clone of it.
|
|
1309
|
+
|
|
1310
|
+
* @example
|
|
1311
|
+
* parsePathString("M 0 0L50 50")
|
|
1312
|
+
* // => [["M",0,0],["L",50,50]]
|
|
1313
|
+
*
|
|
1314
|
+
* @param pathInput the string to be parsed
|
|
1315
|
+
* @returns the resulted `pathArray` or error string
|
|
1316
|
+
*/
|
|
1317
|
+
const parsePathString = (pathInput) => {
|
|
1318
|
+
if (typeof pathInput !== "string") return pathInput.slice(0);
|
|
1319
|
+
const path = new PathParser(pathInput);
|
|
1320
|
+
skipSpaces(path);
|
|
1321
|
+
while (path.index < path.max && !path.err.length) scanSegment(path);
|
|
1322
|
+
if (!path.err.length) {
|
|
1323
|
+
if (path.segments.length)
|
|
1324
|
+
/**
|
|
1325
|
+
* force absolute first M
|
|
1326
|
+
* getPathBBox calculation requires first segment to be absolute
|
|
1327
|
+
* @see https://github.com/thednp/svg-path-commander/pull/49
|
|
1328
|
+
*/
|
|
1329
|
+
path.segments[0][0] = "M";
|
|
1330
|
+
} else throw TypeError(path.err);
|
|
1331
|
+
return path.segments;
|
|
1332
|
+
};
|
|
1333
|
+
//#endregion
|
|
1334
|
+
//#region src/process/absolutizeSegment.ts
|
|
1335
|
+
/**
|
|
1336
|
+
* Returns an absolute segment of a `PathArray` object.
|
|
1337
|
+
*
|
|
1338
|
+
* @param segment the segment object
|
|
1339
|
+
* @param index the segment index
|
|
1340
|
+
* @param lastX the last known X value
|
|
1341
|
+
* @param lastY the last known Y value
|
|
1342
|
+
* @returns the absolute segment
|
|
1343
|
+
*/
|
|
1344
|
+
const absolutizeSegment = (segment, index, lastX, lastY) => {
|
|
1345
|
+
const [pathCommand] = segment;
|
|
1346
|
+
const absCommand = pathCommand.toUpperCase();
|
|
1347
|
+
if (index === 0 || absCommand === pathCommand) return segment;
|
|
1348
|
+
if (absCommand === "A") return [
|
|
1349
|
+
absCommand,
|
|
1350
|
+
segment[1],
|
|
1351
|
+
segment[2],
|
|
1352
|
+
segment[3],
|
|
1353
|
+
segment[4],
|
|
1354
|
+
segment[5],
|
|
1355
|
+
segment[6] + lastX,
|
|
1356
|
+
segment[7] + lastY
|
|
1357
|
+
];
|
|
1358
|
+
else if (absCommand === "V") return [absCommand, segment[1] + lastY];
|
|
1359
|
+
else if (absCommand === "H") return [absCommand, segment[1] + lastX];
|
|
1360
|
+
else if (absCommand === "L") return [
|
|
1361
|
+
absCommand,
|
|
1362
|
+
segment[1] + lastX,
|
|
1363
|
+
segment[2] + lastY
|
|
1364
|
+
];
|
|
1365
|
+
else {
|
|
1366
|
+
const absValues = [];
|
|
1367
|
+
const seglen = segment.length;
|
|
1368
|
+
for (let j = 1; j < seglen; j += 1) absValues.push(segment[j] + (j % 2 ? lastX : lastY));
|
|
1369
|
+
return [absCommand].concat(absValues);
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
//#endregion
|
|
1373
|
+
//#region src/process/iterate.ts
|
|
1374
|
+
/**
|
|
1375
|
+
* Iterates over a `PathArray`, executing a callback for each segment.
|
|
1376
|
+
* The callback can:
|
|
1377
|
+
* - Read current position (`x`, `y`)
|
|
1378
|
+
* - Modify the segment (return new segment)
|
|
1379
|
+
* - Stop early (return `false`)
|
|
1380
|
+
*
|
|
1381
|
+
* The iterator maintains accurate current point (`x`, `y`) and subpath start (`mx`, `my`)
|
|
1382
|
+
* while correctly handling relative/absolute commands, including H/V and Z.
|
|
1383
|
+
*
|
|
1384
|
+
* **Important**: If the callback returns a new segment with more coordinates (e.g., Q → C),
|
|
1385
|
+
* the path length may increase, and iteration will continue over new segments.
|
|
1386
|
+
*
|
|
1387
|
+
* @template T - Specific PathArray type (e.g., CurveArray, PolylineArray)
|
|
1388
|
+
* @param path - The source `PathArray` to iterate over
|
|
1389
|
+
* @param iterator - Callback function for each segment
|
|
1390
|
+
* @param iterator.segment - Current path segment
|
|
1391
|
+
* @param iterator.index - Index of current segment
|
|
1392
|
+
* @param iterator.x - Current X position (after applying relative offset)
|
|
1393
|
+
* @param iterator.y - Current Y position (after applying relative offset)
|
|
1394
|
+
* @returns The modified `path` (or original if no changes)
|
|
1395
|
+
*
|
|
1396
|
+
* @example
|
|
1397
|
+
* iterate(path, (seg, i, x, y) => {
|
|
1398
|
+
* if (seg[0] === 'L') return ['C', x, y, seg[1], seg[2], seg[1], seg[2]];
|
|
1399
|
+
* });
|
|
1400
|
+
*/
|
|
1401
|
+
const iterate = (path, iterator) => {
|
|
1402
|
+
let x = 0;
|
|
1403
|
+
let y = 0;
|
|
1404
|
+
let mx = 0;
|
|
1405
|
+
let my = 0;
|
|
1406
|
+
let i = 0;
|
|
1407
|
+
while (i < path.length) {
|
|
1408
|
+
const segment = path[i];
|
|
1409
|
+
const [pathCommand] = segment;
|
|
1410
|
+
const absCommand = pathCommand.toUpperCase();
|
|
1411
|
+
const isRelative = absCommand !== pathCommand;
|
|
1412
|
+
const iteratorResult = iterator(segment, i, x, y);
|
|
1413
|
+
if (iteratorResult === false) break;
|
|
1414
|
+
if (absCommand === "Z") {
|
|
1415
|
+
x = mx;
|
|
1416
|
+
y = my;
|
|
1417
|
+
} else if (absCommand === "H") x = segment[1] + (isRelative ? x : 0);
|
|
1418
|
+
else if (absCommand === "V") y = segment[1] + (isRelative ? y : 0);
|
|
1419
|
+
else {
|
|
1420
|
+
const segLen = segment.length;
|
|
1421
|
+
x = segment[segLen - 2] + (isRelative ? x : 0);
|
|
1422
|
+
y = segment[segLen - 1] + (isRelative ? y : 0);
|
|
1423
|
+
if (absCommand === "M") {
|
|
1424
|
+
mx = x;
|
|
1425
|
+
my = y;
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (iteratorResult) path[i] = iteratorResult;
|
|
1429
|
+
i += 1;
|
|
1430
|
+
}
|
|
1431
|
+
return path;
|
|
1432
|
+
};
|
|
1433
|
+
//#endregion
|
|
1434
|
+
//#region src/convert/pathToAbsolute.ts
|
|
1435
|
+
/**
|
|
1436
|
+
* Parses a path string value or object and returns an array
|
|
1437
|
+
* of segments, all converted to absolute values.
|
|
1438
|
+
*
|
|
1439
|
+
* @param pathInput - The path string or PathArray
|
|
1440
|
+
* @returns The resulted PathArray with absolute values
|
|
1441
|
+
*
|
|
1442
|
+
* @example
|
|
1443
|
+
* ```ts
|
|
1444
|
+
* pathToAbsolute('M10 10l80 80')
|
|
1445
|
+
* // => [['M', 10, 10], ['L', 90, 90]]
|
|
1446
|
+
* ```
|
|
1447
|
+
*/
|
|
1448
|
+
const pathToAbsolute = (pathInput) => {
|
|
1449
|
+
return iterate(parsePathString(pathInput), absolutizeSegment);
|
|
1450
|
+
};
|
|
1451
|
+
//#endregion
|
|
1452
|
+
//#region src/process/relativizeSegment.ts
|
|
1453
|
+
/**
|
|
1454
|
+
* Returns a relative segment of a `PathArray` object.
|
|
1455
|
+
*
|
|
1456
|
+
* @param segment the segment object
|
|
1457
|
+
* @param index the segment index
|
|
1458
|
+
* @param lastX the last known X value
|
|
1459
|
+
* @param lastY the last known Y value
|
|
1460
|
+
* @returns the relative segment
|
|
1461
|
+
*/
|
|
1462
|
+
const relativizeSegment = (segment, index, lastX, lastY) => {
|
|
1463
|
+
const [pathCommand] = segment;
|
|
1464
|
+
const relCommand = pathCommand.toLowerCase();
|
|
1465
|
+
if (index === 0 || pathCommand === relCommand) return segment;
|
|
1466
|
+
if (relCommand === "a") return [
|
|
1467
|
+
relCommand,
|
|
1468
|
+
segment[1],
|
|
1469
|
+
segment[2],
|
|
1470
|
+
segment[3],
|
|
1471
|
+
segment[4],
|
|
1472
|
+
segment[5],
|
|
1473
|
+
segment[6] - lastX,
|
|
1474
|
+
segment[7] - lastY
|
|
1475
|
+
];
|
|
1476
|
+
else if (relCommand === "v") return [relCommand, segment[1] - lastY];
|
|
1477
|
+
else if (relCommand === "h") return [relCommand, segment[1] - lastX];
|
|
1478
|
+
else if (relCommand === "l") return [
|
|
1479
|
+
relCommand,
|
|
1480
|
+
segment[1] - lastX,
|
|
1481
|
+
segment[2] - lastY
|
|
1482
|
+
];
|
|
1483
|
+
else {
|
|
1484
|
+
const relValues = [];
|
|
1485
|
+
const seglen = segment.length;
|
|
1486
|
+
for (let j = 1; j < seglen; j += 1) relValues.push(segment[j] - (j % 2 ? lastX : lastY));
|
|
1487
|
+
return [relCommand].concat(relValues);
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
//#endregion
|
|
1491
|
+
//#region src/convert/pathToRelative.ts
|
|
1492
|
+
/**
|
|
1493
|
+
* Parses a path string value or object and returns an array
|
|
1494
|
+
* of segments, all converted to relative values.
|
|
1495
|
+
*
|
|
1496
|
+
* @param pathInput - The path string or PathArray
|
|
1497
|
+
* @returns The resulted PathArray with relative values
|
|
1498
|
+
*
|
|
1499
|
+
* @example
|
|
1500
|
+
* ```ts
|
|
1501
|
+
* pathToRelative('M10 10L90 90')
|
|
1502
|
+
* // => [['M', 10, 10], ['l', 80, 80]]
|
|
1503
|
+
* ```
|
|
1504
|
+
*/
|
|
1505
|
+
const pathToRelative = (pathInput) => {
|
|
1506
|
+
return iterate(parsePathString(pathInput), relativizeSegment);
|
|
1507
|
+
};
|
|
1508
|
+
//#endregion
|
|
1509
|
+
//#region src/process/arcToCubic.ts
|
|
1510
|
+
/**
|
|
1511
|
+
* Converts A (arc-to) segments to C (cubic-bezier-to).
|
|
1512
|
+
*
|
|
1513
|
+
* For more information of where this math came from visit:
|
|
1514
|
+
* http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes
|
|
1515
|
+
*
|
|
1516
|
+
* @param X1 the starting x position
|
|
1517
|
+
* @param Y1 the starting y position
|
|
1518
|
+
* @param RX x-radius of the arc
|
|
1519
|
+
* @param RY y-radius of the arc
|
|
1520
|
+
* @param angle x-axis-rotation of the arc
|
|
1521
|
+
* @param LAF large-arc-flag of the arc
|
|
1522
|
+
* @param SF sweep-flag of the arc
|
|
1523
|
+
* @param X2 the ending x position
|
|
1524
|
+
* @param Y2 the ending y position
|
|
1525
|
+
* @param recursive the parameters needed to split arc into 2 segments
|
|
1526
|
+
* @returns the resulting cubic-bezier segment(s)
|
|
1527
|
+
*/
|
|
1528
|
+
const arcToCubic = (X1, Y1, RX, RY, angle, LAF, SF, X2, Y2, recursive) => {
|
|
1529
|
+
let x1 = X1;
|
|
1530
|
+
let y1 = Y1;
|
|
1531
|
+
let rx = RX;
|
|
1532
|
+
let ry = RY;
|
|
1533
|
+
let x2 = X2;
|
|
1534
|
+
let y2 = Y2;
|
|
1535
|
+
const d120 = Math.PI * 120 / 180;
|
|
1536
|
+
const rad = Math.PI / 180 * (+angle || 0);
|
|
1537
|
+
let res = [];
|
|
1538
|
+
let xy;
|
|
1539
|
+
let f1;
|
|
1540
|
+
let f2;
|
|
1541
|
+
let cx;
|
|
1542
|
+
let cy;
|
|
1543
|
+
if (!recursive) {
|
|
1544
|
+
xy = rotateVector(x1, y1, -rad);
|
|
1545
|
+
x1 = xy.x;
|
|
1546
|
+
y1 = xy.y;
|
|
1547
|
+
xy = rotateVector(x2, y2, -rad);
|
|
1548
|
+
x2 = xy.x;
|
|
1549
|
+
y2 = xy.y;
|
|
1550
|
+
const x = (x1 - x2) / 2;
|
|
1551
|
+
const y = (y1 - y2) / 2;
|
|
1552
|
+
let h = x * x / (rx * rx) + y * y / (ry * ry);
|
|
1553
|
+
if (h > 1) {
|
|
1554
|
+
h = Math.sqrt(h);
|
|
1555
|
+
rx *= h;
|
|
1556
|
+
ry *= h;
|
|
1557
|
+
}
|
|
1558
|
+
const rx2 = rx * rx;
|
|
1559
|
+
const ry2 = ry * ry;
|
|
1560
|
+
const k = (LAF === SF ? -1 : 1) * Math.sqrt(Math.abs((rx2 * ry2 - rx2 * y * y - ry2 * x * x) / (rx2 * y * y + ry2 * x * x)));
|
|
1561
|
+
cx = k * rx * y / ry + (x1 + x2) / 2;
|
|
1562
|
+
cy = k * -ry * x / rx + (y1 + y2) / 2;
|
|
1563
|
+
f1 = Math.asin(((y1 - cy) / ry * 10 ** 9 >> 0) / 10 ** 9);
|
|
1564
|
+
f2 = Math.asin(((y2 - cy) / ry * 10 ** 9 >> 0) / 10 ** 9);
|
|
1565
|
+
f1 = x1 < cx ? Math.PI - f1 : f1;
|
|
1566
|
+
f2 = x2 < cx ? Math.PI - f2 : f2;
|
|
1567
|
+
if (f1 < 0) f1 = Math.PI * 2 + f1;
|
|
1568
|
+
if (f2 < 0) f2 = Math.PI * 2 + f2;
|
|
1569
|
+
if (SF && f1 > f2) f1 -= Math.PI * 2;
|
|
1570
|
+
if (!SF && f2 > f1) f2 -= Math.PI * 2;
|
|
1571
|
+
} else [f1, f2, cx, cy] = recursive;
|
|
1572
|
+
let df = f2 - f1;
|
|
1573
|
+
if (Math.abs(df) > d120) {
|
|
1574
|
+
const f2old = f2;
|
|
1575
|
+
const x2old = x2;
|
|
1576
|
+
const y2old = y2;
|
|
1577
|
+
f2 = f1 + d120 * (SF && f2 > f1 ? 1 : -1);
|
|
1578
|
+
x2 = cx + rx * Math.cos(f2);
|
|
1579
|
+
y2 = cy + ry * Math.sin(f2);
|
|
1580
|
+
res = arcToCubic(x2, y2, rx, ry, angle, 0, SF, x2old, y2old, [
|
|
1581
|
+
f2,
|
|
1582
|
+
f2old,
|
|
1583
|
+
cx,
|
|
1584
|
+
cy
|
|
1585
|
+
]);
|
|
1586
|
+
}
|
|
1587
|
+
df = f2 - f1;
|
|
1588
|
+
const c1 = Math.cos(f1);
|
|
1589
|
+
const s1 = Math.sin(f1);
|
|
1590
|
+
const c2 = Math.cos(f2);
|
|
1591
|
+
const s2 = Math.sin(f2);
|
|
1592
|
+
const t = Math.tan(df / 4);
|
|
1593
|
+
const hx = 4 / 3 * rx * t;
|
|
1594
|
+
const hy = 4 / 3 * ry * t;
|
|
1595
|
+
const m1 = [x1, y1];
|
|
1596
|
+
const m2 = [x1 + hx * s1, y1 - hy * c1];
|
|
1597
|
+
const m3 = [x2 + hx * s2, y2 - hy * c2];
|
|
1598
|
+
const m4 = [x2, y2];
|
|
1599
|
+
m2[0] = 2 * m1[0] - m2[0];
|
|
1600
|
+
m2[1] = 2 * m1[1] - m2[1];
|
|
1601
|
+
if (recursive) return [
|
|
1602
|
+
m2[0],
|
|
1603
|
+
m2[1],
|
|
1604
|
+
m3[0],
|
|
1605
|
+
m3[1],
|
|
1606
|
+
m4[0],
|
|
1607
|
+
m4[1]
|
|
1608
|
+
].concat(res);
|
|
1609
|
+
res = [
|
|
1610
|
+
m2[0],
|
|
1611
|
+
m2[1],
|
|
1612
|
+
m3[0],
|
|
1613
|
+
m3[1],
|
|
1614
|
+
m4[0],
|
|
1615
|
+
m4[1]
|
|
1616
|
+
].concat(res);
|
|
1617
|
+
const newres = [];
|
|
1618
|
+
for (let i = 0, ii = res.length; i < ii; i += 1) newres[i] = i % 2 ? rotateVector(res[i - 1], res[i], rad).y : rotateVector(res[i], res[i + 1], rad).x;
|
|
1619
|
+
return newres;
|
|
1620
|
+
};
|
|
1621
|
+
//#endregion
|
|
1622
|
+
//#region src/process/quadToCubic.ts
|
|
1623
|
+
/**
|
|
1624
|
+
* Converts a Q (quadratic-bezier) segment to C (cubic-bezier).
|
|
1625
|
+
*
|
|
1626
|
+
* @param x1 curve start x
|
|
1627
|
+
* @param y1 curve start y
|
|
1628
|
+
* @param qx control point x
|
|
1629
|
+
* @param qy control point y
|
|
1630
|
+
* @param x2 curve end x
|
|
1631
|
+
* @param y2 curve end y
|
|
1632
|
+
* @returns the cubic-bezier segment
|
|
1633
|
+
*/
|
|
1634
|
+
const quadToCubic = (x1, y1, qx, qy, x2, y2) => {
|
|
1635
|
+
const r13 = 1 / 3;
|
|
1636
|
+
const r23 = 2 / 3;
|
|
1637
|
+
return [
|
|
1638
|
+
r13 * x1 + r23 * qx,
|
|
1639
|
+
r13 * y1 + r23 * qy,
|
|
1640
|
+
r13 * x2 + r23 * qx,
|
|
1641
|
+
r13 * y2 + r23 * qy,
|
|
1642
|
+
x2,
|
|
1643
|
+
y2
|
|
1644
|
+
];
|
|
1645
|
+
};
|
|
1646
|
+
//#endregion
|
|
1647
|
+
//#region src/process/lineToCubic.ts
|
|
1648
|
+
/**
|
|
1649
|
+
* Converts an L (line-to) segment to C (cubic-bezier).
|
|
1650
|
+
*
|
|
1651
|
+
* @param x1 line start x
|
|
1652
|
+
* @param y1 line start y
|
|
1653
|
+
* @param x2 line end x
|
|
1654
|
+
* @param y2 line end y
|
|
1655
|
+
* @returns the cubic-bezier segment
|
|
1656
|
+
*/
|
|
1657
|
+
const lineToCubic = (x1, y1, x2, y2) => {
|
|
1658
|
+
const c1 = midPoint([x1, y1], [x2, y2], 1 / 3);
|
|
1659
|
+
const c2 = midPoint([x1, y1], [x2, y2], 2 / 3);
|
|
1660
|
+
return [
|
|
1661
|
+
c1[0],
|
|
1662
|
+
c1[1],
|
|
1663
|
+
c2[0],
|
|
1664
|
+
c2[1],
|
|
1665
|
+
x2,
|
|
1666
|
+
y2
|
|
1667
|
+
];
|
|
1668
|
+
};
|
|
1669
|
+
//#endregion
|
|
1670
|
+
//#region src/process/segmentToCubic.ts
|
|
1671
|
+
/**
|
|
1672
|
+
* Converts any segment to C (cubic-bezier).
|
|
1673
|
+
*
|
|
1674
|
+
* @param segment the source segment
|
|
1675
|
+
* @param params the source segment parameters
|
|
1676
|
+
* @returns the cubic-bezier segment
|
|
1677
|
+
*/
|
|
1678
|
+
const segmentToCubic = (segment, params) => {
|
|
1679
|
+
const pathCommand = segment[0];
|
|
1680
|
+
const values = segment.slice(1).map(Number);
|
|
1681
|
+
const [x, y] = values;
|
|
1682
|
+
const { x1: px1, y1: py1 } = params;
|
|
1683
|
+
if (!"TQ".includes(pathCommand)) {
|
|
1684
|
+
params.qx = null;
|
|
1685
|
+
params.qy = null;
|
|
1686
|
+
}
|
|
1687
|
+
if (pathCommand === "M") {
|
|
1688
|
+
params.mx = x;
|
|
1689
|
+
params.my = y;
|
|
1690
|
+
params.x = x;
|
|
1691
|
+
params.y = y;
|
|
1692
|
+
return segment;
|
|
1693
|
+
} else if (pathCommand === "A") return ["C"].concat(arcToCubic(px1, py1, values[0], values[1], values[2], values[3], values[4], values[5], values[6]));
|
|
1694
|
+
else if (pathCommand === "Q") {
|
|
1695
|
+
params.qx = x;
|
|
1696
|
+
params.qy = y;
|
|
1697
|
+
return ["C"].concat(quadToCubic(px1, py1, values[0], values[1], values[2], values[3]));
|
|
1698
|
+
} else if (pathCommand === "L") return ["C"].concat(lineToCubic(px1, py1, x, y));
|
|
1699
|
+
else if (pathCommand === "Z") return ["C"].concat(lineToCubic(px1, py1, params.mx, params.my));
|
|
1700
|
+
return segment;
|
|
1701
|
+
};
|
|
1702
|
+
//#endregion
|
|
1703
|
+
//#region src/process/normalizeSegment.ts
|
|
1704
|
+
/**
|
|
1705
|
+
* Normalizes a single segment of a `pathArray` object.
|
|
1706
|
+
*
|
|
1707
|
+
* @param segment the segment object
|
|
1708
|
+
* @param params the normalization parameters
|
|
1709
|
+
* @returns the normalized segment
|
|
1710
|
+
*/
|
|
1711
|
+
const normalizeSegment = (segment, params) => {
|
|
1712
|
+
const [pathCommand] = segment;
|
|
1713
|
+
const absCommand = pathCommand.toUpperCase();
|
|
1714
|
+
const isRelative = pathCommand !== absCommand;
|
|
1715
|
+
const { x1: px1, y1: py1, x2: px2, y2: py2, x, y } = params;
|
|
1716
|
+
const values = segment.slice(1);
|
|
1717
|
+
let absValues = values.map((n, j) => n + (isRelative ? j % 2 ? y : x : 0));
|
|
1718
|
+
if (!"TQ".includes(absCommand)) {
|
|
1719
|
+
params.qx = null;
|
|
1720
|
+
params.qy = null;
|
|
1721
|
+
}
|
|
1722
|
+
if (absCommand === "A") {
|
|
1723
|
+
absValues = values.slice(0, -2).concat(values[5] + (isRelative ? x : 0), values[6] + (isRelative ? y : 0));
|
|
1724
|
+
return ["A"].concat(absValues);
|
|
1725
|
+
} else if (absCommand === "H") return [
|
|
1726
|
+
"L",
|
|
1727
|
+
segment[1] + (isRelative ? x : 0),
|
|
1728
|
+
py1
|
|
1729
|
+
];
|
|
1730
|
+
else if (absCommand === "V") return [
|
|
1731
|
+
"L",
|
|
1732
|
+
px1,
|
|
1733
|
+
segment[1] + (isRelative ? y : 0)
|
|
1734
|
+
];
|
|
1735
|
+
else if (absCommand === "L") return [
|
|
1736
|
+
"L",
|
|
1737
|
+
segment[1] + (isRelative ? x : 0),
|
|
1738
|
+
segment[2] + (isRelative ? y : 0)
|
|
1739
|
+
];
|
|
1740
|
+
else if (absCommand === "M") return [
|
|
1741
|
+
"M",
|
|
1742
|
+
segment[1] + (isRelative ? x : 0),
|
|
1743
|
+
segment[2] + (isRelative ? y : 0)
|
|
1744
|
+
];
|
|
1745
|
+
else if (absCommand === "C") return ["C"].concat(absValues);
|
|
1746
|
+
else if (absCommand === "S") {
|
|
1747
|
+
const x1 = px1 * 2 - px2;
|
|
1748
|
+
const y1 = py1 * 2 - py2;
|
|
1749
|
+
params.x1 = x1;
|
|
1750
|
+
params.y1 = y1;
|
|
1751
|
+
return [
|
|
1752
|
+
"C",
|
|
1753
|
+
x1,
|
|
1754
|
+
y1
|
|
1755
|
+
].concat(absValues);
|
|
1756
|
+
} else if (absCommand === "T") {
|
|
1757
|
+
const qx = px1 * 2 - (params.qx ? params.qx : 0);
|
|
1758
|
+
const qy = py1 * 2 - (params.qy ? params.qy : 0);
|
|
1759
|
+
params.qx = qx;
|
|
1760
|
+
params.qy = qy;
|
|
1761
|
+
return [
|
|
1762
|
+
"Q",
|
|
1763
|
+
qx,
|
|
1764
|
+
qy
|
|
1765
|
+
].concat(absValues);
|
|
1766
|
+
} else if (absCommand === "Q") {
|
|
1767
|
+
const [nqx, nqy] = absValues;
|
|
1768
|
+
params.qx = nqx;
|
|
1769
|
+
params.qy = nqy;
|
|
1770
|
+
return ["Q"].concat(absValues);
|
|
1771
|
+
} else if (absCommand === "Z") return ["Z"];
|
|
1772
|
+
return segment;
|
|
1773
|
+
};
|
|
1774
|
+
//#endregion
|
|
1775
|
+
//#region src/parser/paramsParser.ts
|
|
1776
|
+
/**
|
|
1777
|
+
* Default parser parameters object used to track position state
|
|
1778
|
+
* while iterating through path segments.
|
|
1779
|
+
*/
|
|
1780
|
+
const paramsParser = {
|
|
1781
|
+
mx: 0,
|
|
1782
|
+
my: 0,
|
|
1783
|
+
x1: 0,
|
|
1784
|
+
y1: 0,
|
|
1785
|
+
x2: 0,
|
|
1786
|
+
y2: 0,
|
|
1787
|
+
x: 0,
|
|
1788
|
+
y: 0,
|
|
1789
|
+
qx: null,
|
|
1790
|
+
qy: null
|
|
1791
|
+
};
|
|
1792
|
+
//#endregion
|
|
1793
|
+
//#region src/convert/pathToCurve.ts
|
|
1794
|
+
/**
|
|
1795
|
+
* Parses a path string or PathArray and returns a new one
|
|
1796
|
+
* in which all segments are converted to cubic-bezier.
|
|
1797
|
+
*
|
|
1798
|
+
* @param pathInput - The path string or PathArray
|
|
1799
|
+
* @returns The resulted CurveArray with all segments as cubic beziers
|
|
1800
|
+
*
|
|
1801
|
+
* @example
|
|
1802
|
+
* ```ts
|
|
1803
|
+
* pathToCurve('M10 50q15 -25 30 0')
|
|
1804
|
+
* // => [['M', 10, 50], ['C', 25, 25, 40, 50, 40, 50]]
|
|
1805
|
+
* ```
|
|
1806
|
+
*/
|
|
1807
|
+
const pathToCurve = (pathInput) => {
|
|
1808
|
+
const params = { ...paramsParser };
|
|
1809
|
+
const path = parsePathString(pathInput);
|
|
1810
|
+
return iterate(path, (seg, index, lastX, lastY) => {
|
|
1811
|
+
params.x = lastX;
|
|
1812
|
+
params.y = lastY;
|
|
1813
|
+
const normalSegment = normalizeSegment(seg, params);
|
|
1814
|
+
if (normalSegment[0] === "M") {
|
|
1815
|
+
params.mx = normalSegment[1];
|
|
1816
|
+
params.my = normalSegment[2];
|
|
1817
|
+
}
|
|
1818
|
+
let result = segmentToCubic(normalSegment, params);
|
|
1819
|
+
if (result[0] === "C" && result.length > 7) {
|
|
1820
|
+
path.splice(index + 1, 0, ["C"].concat(result.slice(7)));
|
|
1821
|
+
result = result.slice(0, 7);
|
|
1822
|
+
}
|
|
1823
|
+
const seglen = result.length;
|
|
1824
|
+
params.x1 = +result[seglen - 2];
|
|
1825
|
+
params.y1 = +result[seglen - 1];
|
|
1826
|
+
params.x2 = +result[seglen - 4] || params.x1;
|
|
1827
|
+
params.y2 = +result[seglen - 3] || params.y1;
|
|
1828
|
+
return result;
|
|
1829
|
+
});
|
|
1830
|
+
};
|
|
1831
|
+
//#endregion
|
|
1832
|
+
//#region src/convert/pathToString.ts
|
|
1833
|
+
/**
|
|
1834
|
+
* Returns a valid `d` attribute string value created
|
|
1835
|
+
* by rounding values and concatenating the PathArray segments.
|
|
1836
|
+
*
|
|
1837
|
+
* @param path - The PathArray object
|
|
1838
|
+
* @param roundOption - Amount of decimals to round values to, or "off"
|
|
1839
|
+
* @returns The concatenated path string
|
|
1840
|
+
*
|
|
1841
|
+
* @example
|
|
1842
|
+
* ```ts
|
|
1843
|
+
* pathToString([['M', 10, 10], ['L', 90, 90]], 2)
|
|
1844
|
+
* // => 'M10 10L90 90'
|
|
1845
|
+
* ```
|
|
1846
|
+
*/
|
|
1847
|
+
const pathToString = (path, roundOption) => {
|
|
1848
|
+
const pathLen = path.length;
|
|
1849
|
+
let { round } = defaultOptions;
|
|
1850
|
+
let segment = path[0];
|
|
1851
|
+
let result = "";
|
|
1852
|
+
round = roundOption === "off" ? roundOption : typeof roundOption === "number" && roundOption >= 0 ? roundOption : typeof round === "number" && round >= 0 ? round : "off";
|
|
1853
|
+
for (let i = 0; i < pathLen; i += 1) {
|
|
1854
|
+
segment = path[i];
|
|
1855
|
+
const [pathCommand] = segment;
|
|
1856
|
+
const values = segment.slice(1);
|
|
1857
|
+
result += pathCommand;
|
|
1858
|
+
if (round === "off") result += values.join(" ");
|
|
1859
|
+
else {
|
|
1860
|
+
let j = 0;
|
|
1861
|
+
const valLen = values.length;
|
|
1862
|
+
while (j < valLen) {
|
|
1863
|
+
result += roundTo(values[j], round);
|
|
1864
|
+
if (j !== valLen - 1) result += " ";
|
|
1865
|
+
j += 1;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return result;
|
|
1870
|
+
};
|
|
1871
|
+
//#endregion
|
|
1872
|
+
//#region src/util/getPathBBox.ts
|
|
1873
|
+
/**
|
|
1874
|
+
* Calculates the bounding box of a path.
|
|
1875
|
+
*
|
|
1876
|
+
* @param pathInput - The path string or PathArray
|
|
1877
|
+
* @returns An object with width, height, x, y, x2, y2, cx, cy, cz properties
|
|
1878
|
+
*
|
|
1879
|
+
* @example
|
|
1880
|
+
* ```ts
|
|
1881
|
+
* getPathBBox('M0 0L100 0L100 100L0 100Z')
|
|
1882
|
+
* // => { x: 0, y: 0, width: 100, height: 100, x2: 100, y2: 100, cx: 50, cy: 50, cz: 150 }
|
|
1883
|
+
* ```
|
|
1884
|
+
*/
|
|
1885
|
+
const getPathBBox = (pathInput) => {
|
|
1886
|
+
if (!pathInput) return {
|
|
1887
|
+
x: 0,
|
|
1888
|
+
y: 0,
|
|
1889
|
+
width: 0,
|
|
1890
|
+
height: 0,
|
|
1891
|
+
x2: 0,
|
|
1892
|
+
y2: 0,
|
|
1893
|
+
cx: 0,
|
|
1894
|
+
cy: 0,
|
|
1895
|
+
cz: 0
|
|
1896
|
+
};
|
|
1897
|
+
const path = parsePathString(pathInput);
|
|
1898
|
+
let pathCommand = "M";
|
|
1899
|
+
let mx = 0;
|
|
1900
|
+
let my = 0;
|
|
1901
|
+
const { max, min } = Math;
|
|
1902
|
+
let xMin = Infinity;
|
|
1903
|
+
let yMin = Infinity;
|
|
1904
|
+
let xMax = -Infinity;
|
|
1905
|
+
let yMax = -Infinity;
|
|
1906
|
+
let minX = 0;
|
|
1907
|
+
let minY = 0;
|
|
1908
|
+
let maxX = 0;
|
|
1909
|
+
let maxY = 0;
|
|
1910
|
+
let paramX1 = 0;
|
|
1911
|
+
let paramY1 = 0;
|
|
1912
|
+
let paramX2 = 0;
|
|
1913
|
+
let paramY2 = 0;
|
|
1914
|
+
let paramQX = 0;
|
|
1915
|
+
let paramQY = 0;
|
|
1916
|
+
iterate(path, (seg, index, lastX, lastY) => {
|
|
1917
|
+
[pathCommand] = seg;
|
|
1918
|
+
const absCommand = pathCommand.toUpperCase();
|
|
1919
|
+
const absoluteSegment = absCommand !== pathCommand ? absolutizeSegment(seg, index, lastX, lastY) : seg.slice(0);
|
|
1920
|
+
const normalSegment = absCommand === "V" ? [
|
|
1921
|
+
"L",
|
|
1922
|
+
lastX,
|
|
1923
|
+
absoluteSegment[1]
|
|
1924
|
+
] : absCommand === "H" ? [
|
|
1925
|
+
"L",
|
|
1926
|
+
absoluteSegment[1],
|
|
1927
|
+
lastY
|
|
1928
|
+
] : absoluteSegment;
|
|
1929
|
+
[pathCommand] = normalSegment;
|
|
1930
|
+
if (!"TQ".includes(absCommand)) {
|
|
1931
|
+
paramQX = 0;
|
|
1932
|
+
paramQY = 0;
|
|
1933
|
+
}
|
|
1934
|
+
if (pathCommand === "M") {
|
|
1935
|
+
[, mx, my] = normalSegment;
|
|
1936
|
+
minX = mx;
|
|
1937
|
+
minY = my;
|
|
1938
|
+
maxX = mx;
|
|
1939
|
+
maxY = my;
|
|
1940
|
+
} else if (pathCommand === "L") [minX, minY, maxX, maxY] = getLineBBox(lastX, lastY, normalSegment[1], normalSegment[2]);
|
|
1941
|
+
else if (pathCommand === "A") [minX, minY, maxX, maxY] = getArcBBox(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4], normalSegment[5], normalSegment[6], normalSegment[7]);
|
|
1942
|
+
else if (pathCommand === "S") {
|
|
1943
|
+
const cp1x = paramX1 * 2 - paramX2;
|
|
1944
|
+
const cp1y = paramY1 * 2 - paramY2;
|
|
1945
|
+
[minX, minY, maxX, maxY] = getCubicBBox(lastX, lastY, cp1x, cp1y, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4]);
|
|
1946
|
+
} else if (pathCommand === "C") [minX, minY, maxX, maxY] = getCubicBBox(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4], normalSegment[5], normalSegment[6]);
|
|
1947
|
+
else if (pathCommand === "T") {
|
|
1948
|
+
paramQX = paramX1 * 2 - paramQX;
|
|
1949
|
+
paramQY = paramY1 * 2 - paramQY;
|
|
1950
|
+
[minX, minY, maxX, maxY] = getQuadBBox(lastX, lastY, paramQX, paramQY, normalSegment[1], normalSegment[2]);
|
|
1951
|
+
} else if (pathCommand === "Q") {
|
|
1952
|
+
paramQX = normalSegment[1];
|
|
1953
|
+
paramQY = normalSegment[2];
|
|
1954
|
+
[minX, minY, maxX, maxY] = getQuadBBox(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4]);
|
|
1955
|
+
} else if (pathCommand === "Z") [minX, minY, maxX, maxY] = getLineBBox(lastX, lastY, mx, my);
|
|
1956
|
+
xMin = min(minX, xMin);
|
|
1957
|
+
yMin = min(minY, yMin);
|
|
1958
|
+
xMax = max(maxX, xMax);
|
|
1959
|
+
yMax = max(maxY, yMax);
|
|
1960
|
+
[paramX1, paramY1] = pathCommand === "Z" ? [mx, my] : normalSegment.slice(-2);
|
|
1961
|
+
[paramX2, paramY2] = pathCommand === "C" ? [normalSegment[3], normalSegment[4]] : pathCommand === "S" ? [normalSegment[1], normalSegment[2]] : [paramX1, paramY1];
|
|
1962
|
+
});
|
|
1963
|
+
const width = xMax - xMin;
|
|
1964
|
+
const height = yMax - yMin;
|
|
1965
|
+
return {
|
|
1966
|
+
width,
|
|
1967
|
+
height,
|
|
1968
|
+
x: xMin,
|
|
1969
|
+
y: yMin,
|
|
1970
|
+
x2: xMax,
|
|
1971
|
+
y2: yMax,
|
|
1972
|
+
cx: xMin + width / 2,
|
|
1973
|
+
cy: yMin + height / 2,
|
|
1974
|
+
cz: Math.max(width, height) + Math.min(width, height) / 2
|
|
1975
|
+
};
|
|
1976
|
+
};
|
|
1977
|
+
//#endregion
|
|
1978
|
+
//#region src/util/getTotalLength.ts
|
|
1979
|
+
/**
|
|
1980
|
+
* Returns the total length of a path, equivalent to `shape.getTotalLength()`.
|
|
1981
|
+
*
|
|
1982
|
+
* @param pathInput - The target path string or PathArray
|
|
1983
|
+
* @returns The total length of the path
|
|
1984
|
+
*
|
|
1985
|
+
* @example
|
|
1986
|
+
* ```ts
|
|
1987
|
+
* getTotalLength('M0 0L100 0L100 100L0 100Z')
|
|
1988
|
+
* // => 300
|
|
1989
|
+
* ```
|
|
1990
|
+
*/
|
|
1991
|
+
const getTotalLength = (pathInput) => {
|
|
1992
|
+
const path = parsePathString(pathInput);
|
|
1993
|
+
let paramX1 = 0;
|
|
1994
|
+
let paramY1 = 0;
|
|
1995
|
+
let paramX2 = 0;
|
|
1996
|
+
let paramY2 = 0;
|
|
1997
|
+
let paramQX = 0;
|
|
1998
|
+
let paramQY = 0;
|
|
1999
|
+
let pathCommand = "M";
|
|
2000
|
+
let mx = 0;
|
|
2001
|
+
let my = 0;
|
|
2002
|
+
let totalLength = 0;
|
|
2003
|
+
iterate(path, (seg, index, lastX, lastY) => {
|
|
2004
|
+
[pathCommand] = seg;
|
|
2005
|
+
const absCommand = pathCommand.toUpperCase();
|
|
2006
|
+
const absoluteSegment = absCommand !== pathCommand ? absolutizeSegment(seg, index, lastX, lastY) : seg.slice(0);
|
|
2007
|
+
const normalSegment = absCommand === "V" ? [
|
|
2008
|
+
"L",
|
|
2009
|
+
lastX,
|
|
2010
|
+
absoluteSegment[1]
|
|
2011
|
+
] : absCommand === "H" ? [
|
|
2012
|
+
"L",
|
|
2013
|
+
absoluteSegment[1],
|
|
2014
|
+
lastY
|
|
2015
|
+
] : absoluteSegment;
|
|
2016
|
+
[pathCommand] = normalSegment;
|
|
2017
|
+
if (!"TQ".includes(absCommand)) {
|
|
2018
|
+
paramQX = 0;
|
|
2019
|
+
paramQY = 0;
|
|
2020
|
+
}
|
|
2021
|
+
if (pathCommand === "M") [, mx, my] = normalSegment;
|
|
2022
|
+
else if (pathCommand === "L") totalLength += getLineLength(lastX, lastY, normalSegment[1], normalSegment[2]);
|
|
2023
|
+
else if (pathCommand === "A") totalLength += getArcLength(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4], normalSegment[5], normalSegment[6], normalSegment[7]);
|
|
2024
|
+
else if (pathCommand === "S") {
|
|
2025
|
+
const cp1x = paramX1 * 2 - paramX2;
|
|
2026
|
+
const cp1y = paramY1 * 2 - paramY2;
|
|
2027
|
+
totalLength += getCubicLength(lastX, lastY, cp1x, cp1y, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4]);
|
|
2028
|
+
} else if (pathCommand === "C") totalLength += getCubicLength(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4], normalSegment[5], normalSegment[6]);
|
|
2029
|
+
else if (pathCommand === "T") {
|
|
2030
|
+
paramQX = paramX1 * 2 - paramQX;
|
|
2031
|
+
paramQY = paramY1 * 2 - paramQY;
|
|
2032
|
+
totalLength += getQuadLength(lastX, lastY, paramQX, paramQY, normalSegment[1], normalSegment[2]);
|
|
2033
|
+
} else if (pathCommand === "Q") {
|
|
2034
|
+
paramQX = normalSegment[1];
|
|
2035
|
+
paramQY = normalSegment[2];
|
|
2036
|
+
totalLength += getQuadLength(lastX, lastY, normalSegment[1], normalSegment[2], normalSegment[3], normalSegment[4]);
|
|
2037
|
+
} else if (pathCommand === "Z") totalLength += getLineLength(lastX, lastY, mx, my);
|
|
2038
|
+
[paramX1, paramY1] = pathCommand === "Z" ? [mx, my] : normalSegment.slice(-2);
|
|
2039
|
+
[paramX2, paramY2] = pathCommand === "C" ? [normalSegment[3], normalSegment[4]] : pathCommand === "S" ? [normalSegment[1], normalSegment[2]] : [paramX1, paramY1];
|
|
2040
|
+
});
|
|
2041
|
+
return totalLength;
|
|
2042
|
+
};
|
|
2043
|
+
//#endregion
|
|
2044
|
+
//#region src/util/distanceEpsilon.ts
|
|
2045
|
+
/** Small threshold value used for floating-point distance comparisons in path calculations. */
|
|
2046
|
+
const DISTANCE_EPSILON = 1e-5;
|
|
2047
|
+
//#endregion
|
|
2048
|
+
//#region src/process/normalizePath.ts
|
|
2049
|
+
/**
|
|
2050
|
+
* Parses a path string or PathArray, then iterates the result for:
|
|
2051
|
+
* * converting segments to absolute values
|
|
2052
|
+
* * converting shorthand commands to their non-shorthand notation
|
|
2053
|
+
*
|
|
2054
|
+
* @param pathInput - The path string or PathArray
|
|
2055
|
+
* @returns The normalized PathArray
|
|
2056
|
+
*
|
|
2057
|
+
* @example
|
|
2058
|
+
* ```ts
|
|
2059
|
+
* normalizePath('M10 90s20 -80 40 -80s20 80 40 80')
|
|
2060
|
+
* // => [['M', 10, 90], ['C', 30, 90, 25, 10, 50, 10], ['C', 75, 10, 70, 90, 90, 90]]
|
|
2061
|
+
* ```
|
|
2062
|
+
*/
|
|
2063
|
+
const normalizePath = (pathInput) => {
|
|
2064
|
+
const path = parsePathString(pathInput);
|
|
2065
|
+
const params = { ...paramsParser };
|
|
2066
|
+
return iterate(path, (seg, _, lastX, lastY) => {
|
|
2067
|
+
params.x = lastX;
|
|
2068
|
+
params.y = lastY;
|
|
2069
|
+
const result = normalizeSegment(seg, params);
|
|
2070
|
+
const seglen = result.length;
|
|
2071
|
+
params.x1 = +result[seglen - 2];
|
|
2072
|
+
params.y1 = +result[seglen - 1];
|
|
2073
|
+
params.x2 = +result[seglen - 4] || params.x1;
|
|
2074
|
+
params.y2 = +result[seglen - 3] || params.y1;
|
|
2075
|
+
return result;
|
|
2076
|
+
});
|
|
2077
|
+
};
|
|
2078
|
+
//#endregion
|
|
2079
|
+
//#region src/util/getPointAtLength.ts
|
|
2080
|
+
/**
|
|
2081
|
+
* Returns [x,y] coordinates of a point at a given length along a path.
|
|
2082
|
+
*
|
|
2083
|
+
* @param pathInput - The PathArray or path string to look into
|
|
2084
|
+
* @param distance - The distance along the path
|
|
2085
|
+
* @returns The requested {x, y} point coordinates
|
|
2086
|
+
*
|
|
2087
|
+
* @example
|
|
2088
|
+
* ```ts
|
|
2089
|
+
* getPointAtLength('M0 0L100 0L100 100Z', 50)
|
|
2090
|
+
* // => { x: 50, y: 0 }
|
|
2091
|
+
* ```
|
|
2092
|
+
*/
|
|
2093
|
+
const getPointAtLength = (pathInput, distance) => {
|
|
2094
|
+
const path = normalizePath(pathInput);
|
|
2095
|
+
let isM = false;
|
|
2096
|
+
let data = [];
|
|
2097
|
+
let x = 0;
|
|
2098
|
+
let y = 0;
|
|
2099
|
+
let [mx, my] = path[0].slice(1);
|
|
2100
|
+
const distanceIsNumber = typeof distance === "number";
|
|
2101
|
+
let point = {
|
|
2102
|
+
x: mx,
|
|
2103
|
+
y: my
|
|
2104
|
+
};
|
|
2105
|
+
let length = 0;
|
|
2106
|
+
let POINT = point;
|
|
2107
|
+
let totalLength = 0;
|
|
2108
|
+
if (!distanceIsNumber || distance < 1e-5) return point;
|
|
2109
|
+
iterate(path, (seg, _, lastX, lastY) => {
|
|
2110
|
+
const pathCommand = seg[0];
|
|
2111
|
+
isM = pathCommand === "M";
|
|
2112
|
+
data = !isM ? [lastX, lastY].concat(seg.slice(1)) : data;
|
|
2113
|
+
if (isM) {
|
|
2114
|
+
[, mx, my] = seg;
|
|
2115
|
+
point = {
|
|
2116
|
+
x: mx,
|
|
2117
|
+
y: my
|
|
2118
|
+
};
|
|
2119
|
+
length = 0;
|
|
2120
|
+
} else if (pathCommand === "L") {
|
|
2121
|
+
point = getPointAtLineLength(data[0], data[1], data[2], data[3], distance - totalLength);
|
|
2122
|
+
length = getLineLength(data[0], data[1], data[2], data[3]);
|
|
2123
|
+
} else if (pathCommand === "A") {
|
|
2124
|
+
point = getPointAtArcLength(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], distance - totalLength);
|
|
2125
|
+
length = getArcLength(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8]);
|
|
2126
|
+
} else if (pathCommand === "C") {
|
|
2127
|
+
point = getPointAtCubicLength(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7], distance - totalLength);
|
|
2128
|
+
length = getCubicLength(data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7]);
|
|
2129
|
+
} else if (pathCommand === "Q") {
|
|
2130
|
+
point = getPointAtQuadLength(data[0], data[1], data[2], data[3], data[4], data[5], distance - totalLength);
|
|
2131
|
+
length = getQuadLength(data[0], data[1], data[2], data[3], data[4], data[5]);
|
|
2132
|
+
} else if (pathCommand === "Z") {
|
|
2133
|
+
data = [
|
|
2134
|
+
lastX,
|
|
2135
|
+
lastY,
|
|
2136
|
+
mx,
|
|
2137
|
+
my
|
|
2138
|
+
];
|
|
2139
|
+
point = {
|
|
2140
|
+
x: mx,
|
|
2141
|
+
y: my
|
|
2142
|
+
};
|
|
2143
|
+
length = getLineLength(data[0], data[1], data[2], data[3]);
|
|
2144
|
+
}
|
|
2145
|
+
[x, y] = data.slice(-2);
|
|
2146
|
+
if (totalLength < distance) POINT = point;
|
|
2147
|
+
else return false;
|
|
2148
|
+
totalLength += length;
|
|
2149
|
+
});
|
|
2150
|
+
if (distance > totalLength - 1e-5) return {
|
|
2151
|
+
x,
|
|
2152
|
+
y
|
|
2153
|
+
};
|
|
2154
|
+
return POINT;
|
|
2155
|
+
};
|
|
2156
|
+
//#endregion
|
|
2157
|
+
//#region src/util/getPropertiesAtLength.ts
|
|
2158
|
+
/**
|
|
2159
|
+
* Returns the segment, its index and length as well as
|
|
2160
|
+
* the length to that segment at a given length in a path.
|
|
2161
|
+
*
|
|
2162
|
+
* @param pathInput target `pathArray`
|
|
2163
|
+
* @param distance the given length
|
|
2164
|
+
* @returns the requested properties
|
|
2165
|
+
*/
|
|
2166
|
+
const getPropertiesAtLength = (pathInput, distance) => {
|
|
2167
|
+
const pathArray = parsePathString(pathInput);
|
|
2168
|
+
let pathTemp = pathArray.slice(0);
|
|
2169
|
+
let pathLength = getTotalLength(pathTemp);
|
|
2170
|
+
let index = pathTemp.length - 1;
|
|
2171
|
+
let lengthAtSegment = 0;
|
|
2172
|
+
let length = 0;
|
|
2173
|
+
let segment = pathArray[0];
|
|
2174
|
+
if (index <= 0 || !distance || !Number.isFinite(distance)) return {
|
|
2175
|
+
segment,
|
|
2176
|
+
index: 0,
|
|
2177
|
+
length,
|
|
2178
|
+
lengthAtSegment
|
|
2179
|
+
};
|
|
2180
|
+
if (distance >= pathLength) {
|
|
2181
|
+
pathTemp = pathArray.slice(0, -1);
|
|
2182
|
+
lengthAtSegment = getTotalLength(pathTemp);
|
|
2183
|
+
length = pathLength - lengthAtSegment;
|
|
2184
|
+
segment = pathArray[index];
|
|
2185
|
+
return {
|
|
2186
|
+
segment,
|
|
2187
|
+
index,
|
|
2188
|
+
length,
|
|
2189
|
+
lengthAtSegment
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
const segments = [];
|
|
2193
|
+
while (index > 0) {
|
|
2194
|
+
segment = pathTemp[index];
|
|
2195
|
+
pathTemp = pathTemp.slice(0, -1);
|
|
2196
|
+
lengthAtSegment = getTotalLength(pathTemp);
|
|
2197
|
+
length = pathLength - lengthAtSegment;
|
|
2198
|
+
pathLength = lengthAtSegment;
|
|
2199
|
+
segments.push({
|
|
2200
|
+
segment,
|
|
2201
|
+
index,
|
|
2202
|
+
length,
|
|
2203
|
+
lengthAtSegment
|
|
2204
|
+
});
|
|
2205
|
+
index -= 1;
|
|
2206
|
+
}
|
|
2207
|
+
return segments.find(({ lengthAtSegment: l }) => l <= distance);
|
|
2208
|
+
};
|
|
2209
|
+
//#endregion
|
|
2210
|
+
//#region src/util/getPropertiesAtPoint.ts
|
|
2211
|
+
/**
|
|
2212
|
+
* Returns the point and segment in path closest to a given point as well as
|
|
2213
|
+
* the distance to the path stroke.
|
|
2214
|
+
*
|
|
2215
|
+
* @see https://bl.ocks.org/mbostock/8027637
|
|
2216
|
+
*
|
|
2217
|
+
* @param pathInput target `pathArray`
|
|
2218
|
+
* @param point the given point
|
|
2219
|
+
* @returns the requested properties
|
|
2220
|
+
*/
|
|
2221
|
+
const getPropertiesAtPoint = (pathInput, point) => {
|
|
2222
|
+
const path = parsePathString(pathInput);
|
|
2223
|
+
const normalPath = normalizePath(path);
|
|
2224
|
+
const pathLength = getTotalLength(normalPath);
|
|
2225
|
+
const distanceTo = (p) => {
|
|
2226
|
+
const dx = p.x - point.x;
|
|
2227
|
+
const dy = p.y - point.y;
|
|
2228
|
+
return dx * dx + dy * dy;
|
|
2229
|
+
};
|
|
2230
|
+
let precision = 8;
|
|
2231
|
+
let scan;
|
|
2232
|
+
let closest = {
|
|
2233
|
+
x: 0,
|
|
2234
|
+
y: 0
|
|
2235
|
+
};
|
|
2236
|
+
let scanDistance = 0;
|
|
2237
|
+
let bestLength = 0;
|
|
2238
|
+
let bestDistance = Infinity;
|
|
2239
|
+
for (let scanLength = 0; scanLength <= pathLength; scanLength += precision) {
|
|
2240
|
+
scan = getPointAtLength(normalPath, scanLength);
|
|
2241
|
+
scanDistance = distanceTo(scan);
|
|
2242
|
+
if (scanDistance < bestDistance) {
|
|
2243
|
+
closest = scan;
|
|
2244
|
+
bestLength = scanLength;
|
|
2245
|
+
bestDistance = scanDistance;
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
precision /= 2;
|
|
2249
|
+
let before;
|
|
2250
|
+
let after;
|
|
2251
|
+
let beforeLength = 0;
|
|
2252
|
+
let afterLength = 0;
|
|
2253
|
+
let beforeDistance = 0;
|
|
2254
|
+
let afterDistance = 0;
|
|
2255
|
+
while (precision > 1e-6) {
|
|
2256
|
+
beforeLength = bestLength - precision;
|
|
2257
|
+
before = getPointAtLength(normalPath, beforeLength);
|
|
2258
|
+
beforeDistance = distanceTo(before);
|
|
2259
|
+
afterLength = bestLength + precision;
|
|
2260
|
+
after = getPointAtLength(normalPath, afterLength);
|
|
2261
|
+
afterDistance = distanceTo(after);
|
|
2262
|
+
if (beforeLength >= 0 && beforeDistance < bestDistance) {
|
|
2263
|
+
closest = before;
|
|
2264
|
+
bestLength = beforeLength;
|
|
2265
|
+
bestDistance = beforeDistance;
|
|
2266
|
+
} else if (afterLength <= pathLength && afterDistance < bestDistance) {
|
|
2267
|
+
closest = after;
|
|
2268
|
+
bestLength = afterLength;
|
|
2269
|
+
bestDistance = afterDistance;
|
|
2270
|
+
} else precision /= 2;
|
|
2271
|
+
if (precision < 1e-5) break;
|
|
2272
|
+
}
|
|
2273
|
+
const segment = getPropertiesAtLength(path, bestLength);
|
|
2274
|
+
return {
|
|
2275
|
+
closest,
|
|
2276
|
+
distance: Math.sqrt(bestDistance),
|
|
2277
|
+
segment
|
|
2278
|
+
};
|
|
2279
|
+
};
|
|
2280
|
+
//#endregion
|
|
2281
|
+
//#region src/util/getClosestPoint.ts
|
|
2282
|
+
/**
|
|
2283
|
+
* Returns the point in path closest to a given point.
|
|
2284
|
+
*
|
|
2285
|
+
* @param pathInput target `pathArray`
|
|
2286
|
+
* @param point the given point
|
|
2287
|
+
* @returns the best match
|
|
2288
|
+
*/
|
|
2289
|
+
const getClosestPoint = (pathInput, point) => {
|
|
2290
|
+
return getPropertiesAtPoint(pathInput, point).closest;
|
|
2291
|
+
};
|
|
2292
|
+
//#endregion
|
|
2293
|
+
//#region src/util/getPathArea.ts
|
|
2294
|
+
/**
|
|
2295
|
+
* Returns the area of a single cubic-bezier segment.
|
|
2296
|
+
*
|
|
2297
|
+
* http://objectmix.com/graphics/133553-area-closed-bezier-curve.html
|
|
2298
|
+
*
|
|
2299
|
+
* @param x1 the starting point X
|
|
2300
|
+
* @param y1 the starting point Y
|
|
2301
|
+
* @param c1x the first control point X
|
|
2302
|
+
* @param c1y the first control point Y
|
|
2303
|
+
* @param c2x the second control point X
|
|
2304
|
+
* @param c2y the second control point Y
|
|
2305
|
+
* @param x2 the ending point X
|
|
2306
|
+
* @param y2 the ending point Y
|
|
2307
|
+
* @returns the area of the cubic-bezier segment
|
|
2308
|
+
*/
|
|
2309
|
+
const getCubicSegArea = (x1, y1, c1x, c1y, c2x, c2y, x2, y2) => {
|
|
2310
|
+
return 3 * ((y2 - y1) * (c1x + c2x) - (x2 - x1) * (c1y + c2y) + c1y * (x1 - c2x) - c1x * (y1 - c2y) + y2 * (c2x + x1 / 3) - x2 * (c2y + y1 / 3)) / 20;
|
|
2311
|
+
};
|
|
2312
|
+
/**
|
|
2313
|
+
* Returns the signed area of a shape.
|
|
2314
|
+
*
|
|
2315
|
+
* @author Jürg Lehni & Jonathan Puckey
|
|
2316
|
+
*
|
|
2317
|
+
* @see https://github.com/paperjs/paper.js/blob/develop/src/path/Path.js
|
|
2318
|
+
*
|
|
2319
|
+
* @param path - The shape PathArray
|
|
2320
|
+
* @returns The signed area of the shape (positive for clockwise, negative for counter-clockwise)
|
|
2321
|
+
*
|
|
2322
|
+
* @example
|
|
2323
|
+
* ```ts
|
|
2324
|
+
* getPathArea([['M', 0, 0], ['L', 100, 0], ['L', 100, 100], ['L', 0, 100], ['Z']])
|
|
2325
|
+
* // => -10000 (counter-clockwise square)
|
|
2326
|
+
* ```
|
|
2327
|
+
*/
|
|
2328
|
+
const getPathArea = (path) => {
|
|
2329
|
+
let x = 0;
|
|
2330
|
+
let y = 0;
|
|
2331
|
+
let len = 0;
|
|
2332
|
+
return pathToCurve(path).map((seg) => {
|
|
2333
|
+
switch (seg[0]) {
|
|
2334
|
+
case "M":
|
|
2335
|
+
[, x, y] = seg;
|
|
2336
|
+
return 0;
|
|
2337
|
+
default:
|
|
2338
|
+
len = getCubicSegArea(x, y, seg[1], seg[2], seg[3], seg[4], seg[5], seg[6]);
|
|
2339
|
+
[x, y] = seg.slice(-2);
|
|
2340
|
+
return len;
|
|
2341
|
+
}
|
|
2342
|
+
}).reduce((a, b) => a + b, 0);
|
|
2343
|
+
};
|
|
2344
|
+
//#endregion
|
|
2345
|
+
//#region src/util/getDrawDirection.ts
|
|
2346
|
+
/**
|
|
2347
|
+
* Check if a path is drawn clockwise and returns true if so,
|
|
2348
|
+
* false otherwise.
|
|
2349
|
+
*
|
|
2350
|
+
* @param path the path string or `pathArray`
|
|
2351
|
+
* @returns true when clockwise or false if not
|
|
2352
|
+
*/
|
|
2353
|
+
const getDrawDirection = (path) => {
|
|
2354
|
+
return getPathArea(pathToCurve(path)) >= 0;
|
|
2355
|
+
};
|
|
2356
|
+
//#endregion
|
|
2357
|
+
//#region src/util/getSegmentAtLength.ts
|
|
2358
|
+
/**
|
|
2359
|
+
* Returns the segment at a given length.
|
|
2360
|
+
*
|
|
2361
|
+
* @param pathInput the target `pathArray`
|
|
2362
|
+
* @param distance the distance in path to look at
|
|
2363
|
+
* @returns the requested segment
|
|
2364
|
+
*/
|
|
2365
|
+
const getSegmentAtLength = (pathInput, distance) => {
|
|
2366
|
+
return getPropertiesAtLength(pathInput, distance).segment;
|
|
2367
|
+
};
|
|
2368
|
+
//#endregion
|
|
2369
|
+
//#region src/util/getSegmentOfPoint.ts
|
|
2370
|
+
/**
|
|
2371
|
+
* Returns the path segment which contains a given point.
|
|
2372
|
+
*
|
|
2373
|
+
* @param path the `pathArray` to look into
|
|
2374
|
+
* @param point the point of the shape to look for
|
|
2375
|
+
* @returns the requested segment
|
|
2376
|
+
*/
|
|
2377
|
+
const getSegmentOfPoint = (path, point) => {
|
|
2378
|
+
return getPropertiesAtPoint(path, point).segment;
|
|
2379
|
+
};
|
|
2380
|
+
//#endregion
|
|
2381
|
+
//#region src/util/isPathArray.ts
|
|
2382
|
+
/**
|
|
2383
|
+
* Iterates an array to check if it's an actual `pathArray`.
|
|
2384
|
+
*
|
|
2385
|
+
* @param path the `pathArray` to be checked
|
|
2386
|
+
* @returns iteration result
|
|
2387
|
+
*/
|
|
2388
|
+
const isPathArray = (path) => {
|
|
2389
|
+
return Array.isArray(path) && path.every((seg) => {
|
|
2390
|
+
const lk = seg[0].toLowerCase();
|
|
2391
|
+
return paramsCounts[lk] === seg.length - 1 && "achlmqstvz".includes(lk) && seg.slice(1).every(Number.isFinite);
|
|
2392
|
+
}) && path.length > 0;
|
|
2393
|
+
};
|
|
2394
|
+
//#endregion
|
|
2395
|
+
//#region src/util/isAbsoluteArray.ts
|
|
2396
|
+
/**
|
|
2397
|
+
* Iterates an array to check if it's a `pathArray`
|
|
2398
|
+
* with all absolute values.
|
|
2399
|
+
*
|
|
2400
|
+
* @param path the `pathArray` to be checked
|
|
2401
|
+
* @returns iteration result
|
|
2402
|
+
*/
|
|
2403
|
+
const isAbsoluteArray = (path) => {
|
|
2404
|
+
return isPathArray(path) && path.every(([x]) => x === x.toUpperCase());
|
|
2405
|
+
};
|
|
2406
|
+
//#endregion
|
|
2407
|
+
//#region src/util/isNormalizedArray.ts
|
|
2408
|
+
/**
|
|
2409
|
+
* Iterates an array to check if it's a `pathArray`
|
|
2410
|
+
* with all segments in non-shorthand notation
|
|
2411
|
+
* with absolute values.
|
|
2412
|
+
*
|
|
2413
|
+
* @param path - the array to be checked
|
|
2414
|
+
* @returns true if the array is a normalized path array
|
|
2415
|
+
*/
|
|
2416
|
+
const isNormalizedArray = (path) => {
|
|
2417
|
+
return isAbsoluteArray(path) && path.every(([pc]) => "ACLMQZ".includes(pc));
|
|
2418
|
+
};
|
|
2419
|
+
//#endregion
|
|
2420
|
+
//#region src/util/isPolygonArray.ts
|
|
2421
|
+
/**
|
|
2422
|
+
* Checks if a path is a polygon (only M, L, H, V, Z commands).
|
|
2423
|
+
* @param pathArray PathArray (pre-normalize if needed)
|
|
2424
|
+
* @returns boolean
|
|
2425
|
+
*/
|
|
2426
|
+
const isPolygonArray = (path) => {
|
|
2427
|
+
return isNormalizedArray(path) && path.every(([pc]) => "MLVHZ".includes(pc));
|
|
2428
|
+
};
|
|
2429
|
+
//#endregion
|
|
2430
|
+
//#region src/util/isCurveArray.ts
|
|
2431
|
+
/**
|
|
2432
|
+
* Iterates an array to check if it's a `pathArray`
|
|
2433
|
+
* with all C (cubic bezier) segments.
|
|
2434
|
+
*
|
|
2435
|
+
* @param path the `Array` to be checked
|
|
2436
|
+
* @returns iteration result
|
|
2437
|
+
*/
|
|
2438
|
+
const isCurveArray = (path) => {
|
|
2439
|
+
return isNormalizedArray(path) && path.every(([pc]) => "MC".includes(pc));
|
|
2440
|
+
};
|
|
2441
|
+
//#endregion
|
|
2442
|
+
//#region src/util/isPointInStroke.ts
|
|
2443
|
+
/**
|
|
2444
|
+
* Checks if a given point is in the stroke of a path.
|
|
2445
|
+
*
|
|
2446
|
+
* @param pathInput target path
|
|
2447
|
+
* @param point the given `{x,y}` point
|
|
2448
|
+
* @returns the query result
|
|
2449
|
+
*/
|
|
2450
|
+
const isPointInStroke = (pathInput, point) => {
|
|
2451
|
+
const { distance } = getPropertiesAtPoint(pathInput, point);
|
|
2452
|
+
return Math.abs(distance) < DISTANCE_EPSILON;
|
|
2453
|
+
};
|
|
2454
|
+
//#endregion
|
|
2455
|
+
//#region src/util/isRelativeArray.ts
|
|
2456
|
+
/**
|
|
2457
|
+
* Iterates an array to check if it's a `pathArray`
|
|
2458
|
+
* with relative values.
|
|
2459
|
+
*
|
|
2460
|
+
* @param path the `pathArray` to be checked
|
|
2461
|
+
* @returns iteration result
|
|
2462
|
+
*/
|
|
2463
|
+
const isRelativeArray = (path) => {
|
|
2464
|
+
return isPathArray(path) && path.slice(1).every(([pc]) => pc === pc.toLowerCase());
|
|
2465
|
+
};
|
|
2466
|
+
//#endregion
|
|
2467
|
+
//#region src/util/isValidPath.ts
|
|
2468
|
+
/**
|
|
2469
|
+
* Parses a path string value to determine its validity
|
|
2470
|
+
* then returns true if it's valid or false otherwise.
|
|
2471
|
+
*
|
|
2472
|
+
* @param pathString the path string to be parsed
|
|
2473
|
+
* @returns the path string validity
|
|
2474
|
+
*/
|
|
2475
|
+
const isValidPath = (pathString) => {
|
|
2476
|
+
if (typeof pathString !== "string" || !pathString.length) return false;
|
|
2477
|
+
const path = new PathParser(pathString);
|
|
2478
|
+
skipSpaces(path);
|
|
2479
|
+
while (path.index < path.max && !path.err.length) scanSegment(path);
|
|
2480
|
+
return !path.err.length && "mM".includes(path.segments[0][0]);
|
|
2481
|
+
};
|
|
2482
|
+
//#endregion
|
|
2483
|
+
//#region src/morph/samplePolygon.ts
|
|
2484
|
+
/**
|
|
2485
|
+
* Samples points from a path to form a polygon approximation.
|
|
2486
|
+
* Collects endpoints of each segment (M start + ends of L/C/etc).
|
|
2487
|
+
*
|
|
2488
|
+
* If `sampleSize` parameter is provided, it will return a polygon
|
|
2489
|
+
* equivalent to the original `PathArray`.
|
|
2490
|
+
* @param path `PolygonPathArray` or `CurvePathArray`
|
|
2491
|
+
* @returns Array of [x, y] points
|
|
2492
|
+
*/
|
|
2493
|
+
function samplePolygon(path) {
|
|
2494
|
+
const points = [];
|
|
2495
|
+
let [mx, my] = [0, 0];
|
|
2496
|
+
iterate(path, (seg) => {
|
|
2497
|
+
const cmd = seg[0];
|
|
2498
|
+
if (cmd === "M") {
|
|
2499
|
+
[mx, my] = [seg[1], seg[2]];
|
|
2500
|
+
points.push([mx, my]);
|
|
2501
|
+
} else if (cmd === "L") points.push([seg[1], seg[2]]);
|
|
2502
|
+
else if (cmd === "C") points.push([seg[5], seg[6]]);
|
|
2503
|
+
else if (cmd === "A") points.push([seg[6], seg[7]]);
|
|
2504
|
+
else if (cmd === "Z") points.push([mx, my]);
|
|
2505
|
+
else throw new TypeError(`${error}: path command "${cmd}" is not supported`);
|
|
2506
|
+
});
|
|
2507
|
+
return points;
|
|
2508
|
+
}
|
|
2509
|
+
//#endregion
|
|
2510
|
+
//#region src/util/shapeParams.ts
|
|
2511
|
+
/**
|
|
2512
|
+
* Supported shapes and their specific parameters.
|
|
2513
|
+
*/
|
|
2514
|
+
const shapeParams = {
|
|
2515
|
+
line: [
|
|
2516
|
+
"x1",
|
|
2517
|
+
"y1",
|
|
2518
|
+
"x2",
|
|
2519
|
+
"y2"
|
|
2520
|
+
],
|
|
2521
|
+
circle: [
|
|
2522
|
+
"cx",
|
|
2523
|
+
"cy",
|
|
2524
|
+
"r"
|
|
2525
|
+
],
|
|
2526
|
+
ellipse: [
|
|
2527
|
+
"cx",
|
|
2528
|
+
"cy",
|
|
2529
|
+
"rx",
|
|
2530
|
+
"ry"
|
|
2531
|
+
],
|
|
2532
|
+
rect: [
|
|
2533
|
+
"width",
|
|
2534
|
+
"height",
|
|
2535
|
+
"x",
|
|
2536
|
+
"y",
|
|
2537
|
+
"rx",
|
|
2538
|
+
"ry"
|
|
2539
|
+
],
|
|
2540
|
+
polygon: ["points"],
|
|
2541
|
+
polyline: ["points"],
|
|
2542
|
+
glyph: ["d"]
|
|
2543
|
+
};
|
|
2544
|
+
//#endregion
|
|
2545
|
+
//#region src/util/isElement.ts
|
|
2546
|
+
/**
|
|
2547
|
+
* Checks if a value is a DOM Element.
|
|
2548
|
+
*
|
|
2549
|
+
* @param node - The value to check
|
|
2550
|
+
* @returns True if the value is a DOM Element (nodeType === 1)
|
|
2551
|
+
*/
|
|
2552
|
+
const isElement = (node) => node !== void 0 && node !== null && typeof node === "object" && node.nodeType === 1;
|
|
2553
|
+
//#endregion
|
|
2554
|
+
//#region src/util/shapeToPathArray.ts
|
|
2555
|
+
/**
|
|
2556
|
+
* Returns a new PathArray from line attributes.
|
|
2557
|
+
*
|
|
2558
|
+
* @param attr - Shape configuration with x1, y1, x2, y2
|
|
2559
|
+
* @returns A new line PathArray
|
|
2560
|
+
*
|
|
2561
|
+
* @example
|
|
2562
|
+
* ```ts
|
|
2563
|
+
* getLinePath({ x1: 0, y1: 0, x2: 100, y2: 100 })
|
|
2564
|
+
* // => [['M', 0, 0], ['L', 100, 100]]
|
|
2565
|
+
* ```
|
|
2566
|
+
*/
|
|
2567
|
+
const getLinePath = (attr) => {
|
|
2568
|
+
let { x1, y1, x2, y2 } = attr;
|
|
2569
|
+
[x1, y1, x2, y2] = [
|
|
2570
|
+
x1,
|
|
2571
|
+
y1,
|
|
2572
|
+
x2,
|
|
2573
|
+
y2
|
|
2574
|
+
].map((a) => +a);
|
|
2575
|
+
return [[
|
|
2576
|
+
"M",
|
|
2577
|
+
x1,
|
|
2578
|
+
y1
|
|
2579
|
+
], [
|
|
2580
|
+
"L",
|
|
2581
|
+
x2,
|
|
2582
|
+
y2
|
|
2583
|
+
]];
|
|
2584
|
+
};
|
|
2585
|
+
/**
|
|
2586
|
+
* Returns a new PathArray from polyline/polygon attributes.
|
|
2587
|
+
*
|
|
2588
|
+
* @param attr - Shape configuration with points string
|
|
2589
|
+
* @returns A new polygon/polyline PathArray
|
|
2590
|
+
*
|
|
2591
|
+
* @example
|
|
2592
|
+
* ```ts
|
|
2593
|
+
* getPolyPath({ type: 'polygon', points: '0,0 100,0 100,100 0,100' })
|
|
2594
|
+
* // => [['M', 0, 0], ['L', 100, 0], ['L', 100, 100], ['L', 0, 100], ['z']]
|
|
2595
|
+
* ```
|
|
2596
|
+
*/
|
|
2597
|
+
const getPolyPath = (attr) => {
|
|
2598
|
+
const pathArray = [];
|
|
2599
|
+
const points = (attr.points || "").trim().split(/[\s|,]/).map((a) => +a);
|
|
2600
|
+
let index = 0;
|
|
2601
|
+
while (index < points.length) {
|
|
2602
|
+
pathArray.push([
|
|
2603
|
+
index ? "L" : "M",
|
|
2604
|
+
points[index],
|
|
2605
|
+
points[index + 1]
|
|
2606
|
+
]);
|
|
2607
|
+
index += 2;
|
|
2608
|
+
}
|
|
2609
|
+
return attr.type === "polygon" ? [...pathArray, ["z"]] : pathArray;
|
|
2610
|
+
};
|
|
2611
|
+
/**
|
|
2612
|
+
* Returns a new PathArray from circle attributes.
|
|
2613
|
+
*
|
|
2614
|
+
* @param attr - Shape configuration with cx, cy, r
|
|
2615
|
+
* @returns A circle PathArray
|
|
2616
|
+
*
|
|
2617
|
+
* @example
|
|
2618
|
+
* ```ts
|
|
2619
|
+
* getCirclePath({ cx: 50, cy: 50, r: 25 })
|
|
2620
|
+
* // => [['M', 25, 50], ['a', 25, 25, 0, 1, 0, 50, 0], ['a', 25, 25, 0, 1, 0, -50, 0]]
|
|
2621
|
+
* ```
|
|
2622
|
+
*/
|
|
2623
|
+
const getCirclePath = (attr) => {
|
|
2624
|
+
let { cx, cy, r } = attr;
|
|
2625
|
+
[cx, cy, r] = [
|
|
2626
|
+
cx,
|
|
2627
|
+
cy,
|
|
2628
|
+
r
|
|
2629
|
+
].map((a) => +a);
|
|
2630
|
+
return [
|
|
2631
|
+
[
|
|
2632
|
+
"M",
|
|
2633
|
+
cx - r,
|
|
2634
|
+
cy
|
|
2635
|
+
],
|
|
2636
|
+
[
|
|
2637
|
+
"a",
|
|
2638
|
+
r,
|
|
2639
|
+
r,
|
|
2640
|
+
0,
|
|
2641
|
+
1,
|
|
2642
|
+
0,
|
|
2643
|
+
2 * r,
|
|
2644
|
+
0
|
|
2645
|
+
],
|
|
2646
|
+
[
|
|
2647
|
+
"a",
|
|
2648
|
+
r,
|
|
2649
|
+
r,
|
|
2650
|
+
0,
|
|
2651
|
+
1,
|
|
2652
|
+
0,
|
|
2653
|
+
-2 * r,
|
|
2654
|
+
0
|
|
2655
|
+
]
|
|
2656
|
+
];
|
|
2657
|
+
};
|
|
2658
|
+
/**
|
|
2659
|
+
* Returns a new PathArray from ellipse attributes.
|
|
2660
|
+
*
|
|
2661
|
+
* @param attr - Shape configuration with cx, cy, rx, ry
|
|
2662
|
+
* @returns An ellipse PathArray
|
|
2663
|
+
*
|
|
2664
|
+
* @example
|
|
2665
|
+
* ```ts
|
|
2666
|
+
* getEllipsePath({ cx: 50, cy: 50, rx: 30, ry: 20 })
|
|
2667
|
+
* // => [['M', 20, 50], ['a', 30, 20, 0, 1, 0, 60, 0], ['a', 30, 20, 0, 1, 0, -60, 0]]
|
|
2668
|
+
* ```
|
|
2669
|
+
*/
|
|
2670
|
+
const getEllipsePath = (attr) => {
|
|
2671
|
+
let { cx, cy } = attr;
|
|
2672
|
+
let rx = attr.rx || 0;
|
|
2673
|
+
let ry = attr.ry || rx;
|
|
2674
|
+
[cx, cy, rx, ry] = [
|
|
2675
|
+
cx,
|
|
2676
|
+
cy,
|
|
2677
|
+
rx,
|
|
2678
|
+
ry
|
|
2679
|
+
].map((a) => +a);
|
|
2680
|
+
return [
|
|
2681
|
+
[
|
|
2682
|
+
"M",
|
|
2683
|
+
cx - rx,
|
|
2684
|
+
cy
|
|
2685
|
+
],
|
|
2686
|
+
[
|
|
2687
|
+
"a",
|
|
2688
|
+
rx,
|
|
2689
|
+
ry,
|
|
2690
|
+
0,
|
|
2691
|
+
1,
|
|
2692
|
+
0,
|
|
2693
|
+
2 * rx,
|
|
2694
|
+
0
|
|
2695
|
+
],
|
|
2696
|
+
[
|
|
2697
|
+
"a",
|
|
2698
|
+
rx,
|
|
2699
|
+
ry,
|
|
2700
|
+
0,
|
|
2701
|
+
1,
|
|
2702
|
+
0,
|
|
2703
|
+
-2 * rx,
|
|
2704
|
+
0
|
|
2705
|
+
]
|
|
2706
|
+
];
|
|
2707
|
+
};
|
|
2708
|
+
/**
|
|
2709
|
+
* Returns a new PathArray from rect attributes.
|
|
2710
|
+
*
|
|
2711
|
+
* @param attr - Object with x, y, width, height, and optional rx/ry
|
|
2712
|
+
* @returns A new PathArray from `<rect>` attributes
|
|
2713
|
+
*
|
|
2714
|
+
* @example
|
|
2715
|
+
* ```ts
|
|
2716
|
+
* getRectanglePath({ x: 0, y: 0, width: 100, height: 50, ry: 10 })
|
|
2717
|
+
* // => [['M', 10, 0], ['h', 80], ['a', 10, 10, 0, 0, 1, 10, 10], ...]
|
|
2718
|
+
* ```
|
|
2719
|
+
*/
|
|
2720
|
+
const getRectanglePath = (attr) => {
|
|
2721
|
+
const x = +attr.x || 0;
|
|
2722
|
+
const y = +attr.y || 0;
|
|
2723
|
+
const w = +attr.width;
|
|
2724
|
+
const h = +attr.height;
|
|
2725
|
+
let rx = +(attr.rx || 0);
|
|
2726
|
+
let ry = +(attr.ry || rx);
|
|
2727
|
+
if (rx || ry) {
|
|
2728
|
+
if (rx * 2 > w) rx -= (rx * 2 - w) / 2;
|
|
2729
|
+
if (ry * 2 > h) ry -= (ry * 2 - h) / 2;
|
|
2730
|
+
return [
|
|
2731
|
+
[
|
|
2732
|
+
"M",
|
|
2733
|
+
x + rx,
|
|
2734
|
+
y
|
|
2735
|
+
],
|
|
2736
|
+
["h", w - rx * 2],
|
|
2737
|
+
[
|
|
2738
|
+
"s",
|
|
2739
|
+
rx,
|
|
2740
|
+
0,
|
|
2741
|
+
rx,
|
|
2742
|
+
ry
|
|
2743
|
+
],
|
|
2744
|
+
["v", h - ry * 2],
|
|
2745
|
+
[
|
|
2746
|
+
"s",
|
|
2747
|
+
0,
|
|
2748
|
+
ry,
|
|
2749
|
+
-rx,
|
|
2750
|
+
ry
|
|
2751
|
+
],
|
|
2752
|
+
["h", -w + rx * 2],
|
|
2753
|
+
[
|
|
2754
|
+
"s",
|
|
2755
|
+
-rx,
|
|
2756
|
+
0,
|
|
2757
|
+
-rx,
|
|
2758
|
+
-ry
|
|
2759
|
+
],
|
|
2760
|
+
["v", -h + ry * 2],
|
|
2761
|
+
[
|
|
2762
|
+
"s",
|
|
2763
|
+
0,
|
|
2764
|
+
-ry,
|
|
2765
|
+
rx,
|
|
2766
|
+
-ry
|
|
2767
|
+
]
|
|
2768
|
+
];
|
|
2769
|
+
}
|
|
2770
|
+
return [
|
|
2771
|
+
[
|
|
2772
|
+
"M",
|
|
2773
|
+
x,
|
|
2774
|
+
y
|
|
2775
|
+
],
|
|
2776
|
+
["h", w],
|
|
2777
|
+
["v", h],
|
|
2778
|
+
["H", x],
|
|
2779
|
+
["Z"]
|
|
2780
|
+
];
|
|
2781
|
+
};
|
|
2782
|
+
/**
|
|
2783
|
+
* Returns a new `pathArray` created from attributes of a `<line>`, `<polyline>`,
|
|
2784
|
+
* `<polygon>`, `<rect>`, `<ellipse>`, `<circle>`, <path> or `<glyph>`.
|
|
2785
|
+
*
|
|
2786
|
+
* It can also work with an options object, see the type below
|
|
2787
|
+
* @see ShapeOps
|
|
2788
|
+
*
|
|
2789
|
+
* @param element target shape
|
|
2790
|
+
* @returns the newly created `<path>` element
|
|
2791
|
+
*/
|
|
2792
|
+
const shapeToPathArray = (element) => {
|
|
2793
|
+
const supportedShapes = Object.keys(shapeParams);
|
|
2794
|
+
const targetIsElement = isElement(element);
|
|
2795
|
+
const tagName = targetIsElement ? element.tagName : null;
|
|
2796
|
+
if (tagName && [...supportedShapes, "path"].every((s) => tagName !== s)) throw TypeError(`${error}: "${tagName}" is not SVGElement`);
|
|
2797
|
+
const type = targetIsElement ? tagName : element.type;
|
|
2798
|
+
const shapeAttrs = shapeParams[type];
|
|
2799
|
+
const config = { type };
|
|
2800
|
+
if (targetIsElement) shapeAttrs.forEach((p) => {
|
|
2801
|
+
config[p] = element.getAttribute(p);
|
|
2802
|
+
});
|
|
2803
|
+
else Object.assign(config, element);
|
|
2804
|
+
let pathArray = [];
|
|
2805
|
+
if (type === "circle") pathArray = getCirclePath(config);
|
|
2806
|
+
else if (type === "ellipse") pathArray = getEllipsePath(config);
|
|
2807
|
+
else if (["polyline", "polygon"].includes(type)) pathArray = getPolyPath(config);
|
|
2808
|
+
else if (type === "rect") pathArray = getRectanglePath(config);
|
|
2809
|
+
else if (type === "line") pathArray = getLinePath(config);
|
|
2810
|
+
else if (["glyph", "path"].includes(type)) pathArray = parsePathString(targetIsElement ? element.getAttribute("d") || "" : element.d || "");
|
|
2811
|
+
if (isPathArray(pathArray) && pathArray.length) return pathArray;
|
|
2812
|
+
return false;
|
|
2813
|
+
};
|
|
2814
|
+
//#endregion
|
|
2815
|
+
//#region src/util/shapeToPath.ts
|
|
2816
|
+
/**
|
|
2817
|
+
* Returns a new `<path>` element created from attributes of a `<line>`, `<polyline>`,
|
|
2818
|
+
* `<polygon>`, `<rect>`, `<ellipse>`, `<circle>` or `<glyph>`. If `replace` parameter
|
|
2819
|
+
* is `true`, it will replace the target. The default `ownerDocument` is your current
|
|
2820
|
+
* `document` browser page, if you want to use in server-side using `jsdom`, you can
|
|
2821
|
+
* pass the `jsdom` `document` to `ownDocument`.
|
|
2822
|
+
*
|
|
2823
|
+
* It can also work with an options object, see the type below
|
|
2824
|
+
* @see ShapeOps
|
|
2825
|
+
*
|
|
2826
|
+
* The newly created `<path>` element keeps all non-specific
|
|
2827
|
+
* attributes like `class`, `fill`, etc.
|
|
2828
|
+
*
|
|
2829
|
+
* @param element - Target shape element or shape options object
|
|
2830
|
+
* @param replace - Option to replace target element
|
|
2831
|
+
* @param ownerDocument - Document for creating the element
|
|
2832
|
+
* @returns The newly created `<path>` element, or false if the path is invalid
|
|
2833
|
+
*
|
|
2834
|
+
* @example
|
|
2835
|
+
* ```ts
|
|
2836
|
+
* const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
|
|
2837
|
+
* circle.setAttribute('cx', '50')
|
|
2838
|
+
* circle.setAttribute('cy', '50')
|
|
2839
|
+
* circle.setAttribute('r', '25')
|
|
2840
|
+
* const path = shapeToPath(circle)
|
|
2841
|
+
* path.getAttribute('d')
|
|
2842
|
+
* // => 'M50 25A25 25 0 1 1 50 75A25 25 0 1 1 50 25Z'
|
|
2843
|
+
* ```
|
|
2844
|
+
*/
|
|
2845
|
+
const shapeToPath = (element, replace, ownerDocument) => {
|
|
2846
|
+
const doc = ownerDocument || document;
|
|
2847
|
+
const supportedShapes = Object.keys(shapeParams);
|
|
2848
|
+
const targetIsElement = isElement(element);
|
|
2849
|
+
const tagName = targetIsElement ? element.tagName : null;
|
|
2850
|
+
if (tagName === "path") throw TypeError(`${error}: "${tagName}" is already SVGPathElement`);
|
|
2851
|
+
if (tagName && supportedShapes.every((s) => tagName !== s)) throw TypeError(`${error}: "${tagName}" is not SVGElement`);
|
|
2852
|
+
const path = doc.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
2853
|
+
const type = targetIsElement ? tagName : element.type;
|
|
2854
|
+
const shapeAttrs = shapeParams[type];
|
|
2855
|
+
const config = { type };
|
|
2856
|
+
const round = defaultOptions.round;
|
|
2857
|
+
const pathArray = shapeToPathArray(element);
|
|
2858
|
+
const description = pathArray && pathArray.length ? pathToString(pathArray, round) : "";
|
|
2859
|
+
if (targetIsElement) {
|
|
2860
|
+
shapeAttrs.forEach((p) => {
|
|
2861
|
+
config[p] = element.getAttribute(p);
|
|
2862
|
+
});
|
|
2863
|
+
for (let i = 0; i < element.attributes.length; i++) {
|
|
2864
|
+
const attr = element.attributes[i];
|
|
2865
|
+
if (attr && !shapeAttrs.includes(attr.name)) path.setAttribute(attr.name, attr.value);
|
|
2866
|
+
}
|
|
2867
|
+
} else {
|
|
2868
|
+
Object.assign(config, element);
|
|
2869
|
+
Object.keys(config).forEach((k) => {
|
|
2870
|
+
if (!shapeAttrs.includes(k) && k !== "type") path.setAttribute(k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`), config[k]);
|
|
2871
|
+
});
|
|
2872
|
+
}
|
|
2873
|
+
if (isValidPath(description)) {
|
|
2874
|
+
path.setAttribute("d", description);
|
|
2875
|
+
if (replace && targetIsElement) {
|
|
2876
|
+
element.before(path, element);
|
|
2877
|
+
element.remove();
|
|
2878
|
+
}
|
|
2879
|
+
return path;
|
|
2880
|
+
}
|
|
2881
|
+
return false;
|
|
2882
|
+
};
|
|
2883
|
+
//#endregion
|
|
2884
|
+
//#region src/util/isMultiPath.ts
|
|
2885
|
+
/**
|
|
2886
|
+
* Determines if an SVG path contains multiple subpaths.
|
|
2887
|
+
* Accepts path string or PathArray.
|
|
2888
|
+
* @param path - 'M10,10 L20,20 Z M30,30 L40,40' → true
|
|
2889
|
+
* @returns boolean
|
|
2890
|
+
*/
|
|
2891
|
+
const isMultiPath = (path) => {
|
|
2892
|
+
if (typeof path === "string") {
|
|
2893
|
+
const matches = path.match(/[Mm]/g);
|
|
2894
|
+
return matches ? matches.length > 1 : false;
|
|
2895
|
+
}
|
|
2896
|
+
if (isPathArray(path)) {
|
|
2897
|
+
let moveCount = 0;
|
|
2898
|
+
for (const segment of path) if (segment[0].toUpperCase() === "M") {
|
|
2899
|
+
moveCount++;
|
|
2900
|
+
if (moveCount > 1) return true;
|
|
2901
|
+
}
|
|
2902
|
+
return false;
|
|
2903
|
+
}
|
|
2904
|
+
throw new TypeError(error + ": expected string or PathArray");
|
|
2905
|
+
};
|
|
2906
|
+
//#endregion
|
|
2907
|
+
//#region src/util/isPolylineArray.ts
|
|
2908
|
+
/**
|
|
2909
|
+
* Checks if a path is a polyline (only M, L, H, V commands).
|
|
2910
|
+
* @param pathArray PathArray (pre-normalize if needed)
|
|
2911
|
+
* @returns boolean
|
|
2912
|
+
*/
|
|
2913
|
+
function isPolylineArray(path) {
|
|
2914
|
+
return isNormalizedArray(path) && path.every(([pc]) => "MLVH".includes(pc));
|
|
2915
|
+
}
|
|
2916
|
+
//#endregion
|
|
2917
|
+
//#region src/util/isClosedPath.ts
|
|
2918
|
+
/**
|
|
2919
|
+
* Check if a PathArray is closed, which means its last segment is a Z.
|
|
2920
|
+
* @param path
|
|
2921
|
+
* @returns true if the path is closed
|
|
2922
|
+
*/
|
|
2923
|
+
const isClosedPath = (path) => {
|
|
2924
|
+
return path[path.length - 1][0].toUpperCase() === "Z";
|
|
2925
|
+
};
|
|
2926
|
+
//#endregion
|
|
2927
|
+
//#region src/process/shortenSegment.ts
|
|
2928
|
+
/**
|
|
2929
|
+
* Shorten a single segment of a `pathArray` object.
|
|
2930
|
+
*
|
|
2931
|
+
* @param segment the `absoluteSegment` object
|
|
2932
|
+
* @param normalSegment the `normalSegment` object
|
|
2933
|
+
* @param params the coordinates of the previous segment
|
|
2934
|
+
* @param prevCommand the path command of the previous segment
|
|
2935
|
+
* @returns the shortened segment
|
|
2936
|
+
*/
|
|
2937
|
+
const shortenSegment = (segment, normalSegment, params, prevCommand) => {
|
|
2938
|
+
const [pathCommand] = segment;
|
|
2939
|
+
const { round: defaultRound } = defaultOptions;
|
|
2940
|
+
const round = typeof defaultRound === "number" ? defaultRound : 4;
|
|
2941
|
+
const normalValues = normalSegment.slice(1);
|
|
2942
|
+
const { x1, y1, x2, y2, x, y } = params;
|
|
2943
|
+
const [nx, ny] = normalValues.slice(-2);
|
|
2944
|
+
const result = segment;
|
|
2945
|
+
if (!"TQ".includes(pathCommand)) {
|
|
2946
|
+
params.qx = null;
|
|
2947
|
+
params.qy = null;
|
|
2948
|
+
}
|
|
2949
|
+
if (pathCommand === "L") {
|
|
2950
|
+
if (roundTo(x, round) === roundTo(nx, round)) return ["V", ny];
|
|
2951
|
+
else if (roundTo(y, round) === roundTo(ny, round)) return ["H", nx];
|
|
2952
|
+
} else if (pathCommand === "C") {
|
|
2953
|
+
const [nx1, ny1] = normalValues;
|
|
2954
|
+
params.x1 = nx1;
|
|
2955
|
+
params.y1 = ny1;
|
|
2956
|
+
if ("CS".includes(prevCommand) && (roundTo(nx1, round) === roundTo(x1 * 2 - x2, round) && roundTo(ny1, round) === roundTo(y1 * 2 - y2, round) || roundTo(x1, round) === roundTo(x2 * 2 - x, round) && roundTo(y1, round) === roundTo(y2 * 2 - y, round))) return [
|
|
2957
|
+
"S",
|
|
2958
|
+
normalValues[2],
|
|
2959
|
+
normalValues[3],
|
|
2960
|
+
normalValues[4],
|
|
2961
|
+
normalValues[5]
|
|
2962
|
+
];
|
|
2963
|
+
} else if (pathCommand === "Q") {
|
|
2964
|
+
const [qx, qy] = normalValues;
|
|
2965
|
+
params.qx = qx;
|
|
2966
|
+
params.qy = qy;
|
|
2967
|
+
if ("QT".includes(prevCommand) && roundTo(qx, round) === roundTo(x1 * 2 - x2, round) && roundTo(qy, round) === roundTo(y1 * 2 - y2, round)) return [
|
|
2968
|
+
"T",
|
|
2969
|
+
normalValues[2],
|
|
2970
|
+
normalValues[3]
|
|
2971
|
+
];
|
|
2972
|
+
}
|
|
2973
|
+
return result;
|
|
2974
|
+
};
|
|
2975
|
+
//#endregion
|
|
2976
|
+
//#region src/process/roundSegment.ts
|
|
2977
|
+
/**
|
|
2978
|
+
* Rounds the numeric values of a path segment to the specified precision.
|
|
2979
|
+
*
|
|
2980
|
+
* @param segment - The path segment to round
|
|
2981
|
+
* @param roundOption - Number of decimal places
|
|
2982
|
+
* @returns The rounded segment
|
|
2983
|
+
*/
|
|
2984
|
+
const roundSegment = (segment, roundOption) => {
|
|
2985
|
+
const values = segment.slice(1).map((n) => roundTo(n, roundOption));
|
|
2986
|
+
return [segment[0]].concat(values);
|
|
2987
|
+
};
|
|
2988
|
+
//#endregion
|
|
2989
|
+
//#region src/process/optimizePath.ts
|
|
2990
|
+
/**
|
|
2991
|
+
* Optimizes a PathArray:
|
|
2992
|
+
* * converts segments to shorthand if possible
|
|
2993
|
+
* * selects shortest representation from absolute and relative forms
|
|
2994
|
+
*
|
|
2995
|
+
* @param pathInput - A path string or PathArray
|
|
2996
|
+
* @param roundOption - Number of decimal places for rounding
|
|
2997
|
+
* @returns The optimized PathArray
|
|
2998
|
+
*
|
|
2999
|
+
* @example
|
|
3000
|
+
* ```ts
|
|
3001
|
+
* optimizePath('M10 10L10 10L90 90', 2)
|
|
3002
|
+
* // => [['M', 10, 10], ['l', 0, 0], ['l', 80, 80]]
|
|
3003
|
+
* ```
|
|
3004
|
+
*/
|
|
3005
|
+
const optimizePath = (pathInput, roundOption) => {
|
|
3006
|
+
const path = pathToAbsolute(pathInput);
|
|
3007
|
+
const round = typeof roundOption === "number" && roundOption >= 0 ? roundOption : 2;
|
|
3008
|
+
const optimParams = { ...paramsParser };
|
|
3009
|
+
const allPathCommands = [];
|
|
3010
|
+
let pathCommand = "M";
|
|
3011
|
+
let prevCommand = "Z";
|
|
3012
|
+
return iterate(path, (seg, i, lastX, lastY) => {
|
|
3013
|
+
optimParams.x = lastX;
|
|
3014
|
+
optimParams.y = lastY;
|
|
3015
|
+
const normalizedSegment = normalizeSegment(seg, optimParams);
|
|
3016
|
+
let result = seg;
|
|
3017
|
+
pathCommand = seg[0];
|
|
3018
|
+
allPathCommands[i] = pathCommand;
|
|
3019
|
+
if (i) {
|
|
3020
|
+
prevCommand = allPathCommands[i - 1];
|
|
3021
|
+
const shortSegment = shortenSegment(seg, normalizedSegment, optimParams, prevCommand);
|
|
3022
|
+
const absSegment = roundSegment(shortSegment, round);
|
|
3023
|
+
const absString = absSegment.join("");
|
|
3024
|
+
const relSegment = roundSegment(relativizeSegment(shortSegment, i, lastX, lastY), round);
|
|
3025
|
+
const relString = relSegment.join("");
|
|
3026
|
+
result = absString.length < relString.length ? absSegment : relSegment;
|
|
3027
|
+
}
|
|
3028
|
+
const seglen = normalizedSegment.length;
|
|
3029
|
+
optimParams.x1 = +normalizedSegment[seglen - 2];
|
|
3030
|
+
optimParams.y1 = +normalizedSegment[seglen - 1];
|
|
3031
|
+
optimParams.x2 = +normalizedSegment[seglen - 4] || optimParams.x1;
|
|
3032
|
+
optimParams.y2 = +normalizedSegment[seglen - 3] || optimParams.y1;
|
|
3033
|
+
return result;
|
|
3034
|
+
});
|
|
3035
|
+
};
|
|
3036
|
+
//#endregion
|
|
3037
|
+
//#region src/process/reversePath.ts
|
|
3038
|
+
/**
|
|
3039
|
+
* Reverses all segments of a PathArray and returns a new PathArray
|
|
3040
|
+
* with absolute values.
|
|
3041
|
+
*
|
|
3042
|
+
* @param pathInput - The source PathArray
|
|
3043
|
+
* @returns The reversed PathArray
|
|
3044
|
+
*
|
|
3045
|
+
* @example
|
|
3046
|
+
* ```ts
|
|
3047
|
+
* reversePath([['M', 0, 0], ['L', 100, 0], ['L', 100, 100], ['L', 0, 100], ['Z']])
|
|
3048
|
+
* // => [['M', 0, 100], ['L', 0, 0], ['L', 100, 0], ['L', 100, 100], ['Z']]
|
|
3049
|
+
* ```
|
|
3050
|
+
*/
|
|
3051
|
+
const reversePath = (pathInput) => {
|
|
3052
|
+
const absolutePath = pathToAbsolute(pathInput);
|
|
3053
|
+
const normalizedPath = normalizePath(absolutePath);
|
|
3054
|
+
const pLen = absolutePath.length;
|
|
3055
|
+
const isClosed = absolutePath[pLen - 1][0] === "Z";
|
|
3056
|
+
const reversedPath = iterate(absolutePath, (segment, i) => {
|
|
3057
|
+
const normalizedSegment = normalizedPath[i];
|
|
3058
|
+
const prevSeg = i && absolutePath[i - 1];
|
|
3059
|
+
const prevCommand = prevSeg && prevSeg[0];
|
|
3060
|
+
const nextSeg = absolutePath[i + 1];
|
|
3061
|
+
const nextCommand = nextSeg && nextSeg[0];
|
|
3062
|
+
const pathCommand = segment[0];
|
|
3063
|
+
const [x, y] = normalizedPath[i ? i - 1 : pLen - 1].slice(-2);
|
|
3064
|
+
let result = segment;
|
|
3065
|
+
switch (pathCommand) {
|
|
3066
|
+
case "M":
|
|
3067
|
+
result = isClosed ? ["Z"] : [
|
|
3068
|
+
pathCommand,
|
|
3069
|
+
x,
|
|
3070
|
+
y
|
|
3071
|
+
];
|
|
3072
|
+
break;
|
|
3073
|
+
case "A":
|
|
3074
|
+
result = [
|
|
3075
|
+
pathCommand,
|
|
3076
|
+
segment[1],
|
|
3077
|
+
segment[2],
|
|
3078
|
+
segment[3],
|
|
3079
|
+
segment[4],
|
|
3080
|
+
segment[5] === 1 ? 0 : 1,
|
|
3081
|
+
x,
|
|
3082
|
+
y
|
|
3083
|
+
];
|
|
3084
|
+
break;
|
|
3085
|
+
case "C":
|
|
3086
|
+
if (nextSeg && nextCommand === "S") result = [
|
|
3087
|
+
"S",
|
|
3088
|
+
segment[1],
|
|
3089
|
+
segment[2],
|
|
3090
|
+
x,
|
|
3091
|
+
y
|
|
3092
|
+
];
|
|
3093
|
+
else result = [
|
|
3094
|
+
pathCommand,
|
|
3095
|
+
segment[3],
|
|
3096
|
+
segment[4],
|
|
3097
|
+
segment[1],
|
|
3098
|
+
segment[2],
|
|
3099
|
+
x,
|
|
3100
|
+
y
|
|
3101
|
+
];
|
|
3102
|
+
break;
|
|
3103
|
+
case "S":
|
|
3104
|
+
if (prevCommand && "CS".includes(prevCommand) && (!nextSeg || nextCommand !== "S")) result = [
|
|
3105
|
+
"C",
|
|
3106
|
+
normalizedSegment[3],
|
|
3107
|
+
normalizedSegment[4],
|
|
3108
|
+
normalizedSegment[1],
|
|
3109
|
+
normalizedSegment[2],
|
|
3110
|
+
x,
|
|
3111
|
+
y
|
|
3112
|
+
];
|
|
3113
|
+
else result = [
|
|
3114
|
+
pathCommand,
|
|
3115
|
+
normalizedSegment[1],
|
|
3116
|
+
normalizedSegment[2],
|
|
3117
|
+
x,
|
|
3118
|
+
y
|
|
3119
|
+
];
|
|
3120
|
+
break;
|
|
3121
|
+
case "Q":
|
|
3122
|
+
if (nextSeg && nextCommand === "T") result = [
|
|
3123
|
+
"T",
|
|
3124
|
+
x,
|
|
3125
|
+
y
|
|
3126
|
+
];
|
|
3127
|
+
else result = [
|
|
3128
|
+
pathCommand,
|
|
3129
|
+
segment[1],
|
|
3130
|
+
segment[2],
|
|
3131
|
+
x,
|
|
3132
|
+
y
|
|
3133
|
+
];
|
|
3134
|
+
break;
|
|
3135
|
+
case "T":
|
|
3136
|
+
if (prevCommand && "QT".includes(prevCommand) && (!nextSeg || nextCommand !== "T")) result = [
|
|
3137
|
+
"Q",
|
|
3138
|
+
normalizedSegment[1],
|
|
3139
|
+
normalizedSegment[2],
|
|
3140
|
+
x,
|
|
3141
|
+
y
|
|
3142
|
+
];
|
|
3143
|
+
else result = [
|
|
3144
|
+
pathCommand,
|
|
3145
|
+
x,
|
|
3146
|
+
y
|
|
3147
|
+
];
|
|
3148
|
+
break;
|
|
3149
|
+
case "Z":
|
|
3150
|
+
result = [
|
|
3151
|
+
"M",
|
|
3152
|
+
x,
|
|
3153
|
+
y
|
|
3154
|
+
];
|
|
3155
|
+
break;
|
|
3156
|
+
case "H":
|
|
3157
|
+
result = [pathCommand, x];
|
|
3158
|
+
break;
|
|
3159
|
+
case "V":
|
|
3160
|
+
result = [pathCommand, y];
|
|
3161
|
+
break;
|
|
3162
|
+
default: result = [pathCommand].concat(segment.slice(1, -2), x, y);
|
|
3163
|
+
}
|
|
3164
|
+
return result;
|
|
3165
|
+
});
|
|
3166
|
+
return isClosed ? reversedPath.reverse() : [reversedPath[0]].concat(reversedPath.slice(1).reverse());
|
|
3167
|
+
};
|
|
3168
|
+
//#endregion
|
|
3169
|
+
//#region src/process/splitPath.ts
|
|
3170
|
+
/**
|
|
3171
|
+
* Split a path string or PathArray into an array of sub-paths.
|
|
3172
|
+
*
|
|
3173
|
+
* In the process, values are converted to absolute
|
|
3174
|
+
* for visual consistency.
|
|
3175
|
+
*
|
|
3176
|
+
* @param pathInput - The source path string or PathArray
|
|
3177
|
+
* @returns An array of sub-path PathArrays
|
|
3178
|
+
*
|
|
3179
|
+
* @example
|
|
3180
|
+
* ```ts
|
|
3181
|
+
* splitPath('M0 0L100 0ZM200 0L300 0Z')
|
|
3182
|
+
* // => [
|
|
3183
|
+
* // [['M', 0, 0], ['L', 100, 0], ['Z']],
|
|
3184
|
+
* // [['M', 200, 0], ['L', 300, 0], ['Z']]
|
|
3185
|
+
* // ]
|
|
3186
|
+
* ```
|
|
3187
|
+
*/
|
|
3188
|
+
const splitPath = (pathInput) => {
|
|
3189
|
+
const composite = [];
|
|
3190
|
+
const parsedPath = parsePathString(pathInput);
|
|
3191
|
+
let path = [];
|
|
3192
|
+
let pi = -1;
|
|
3193
|
+
let x = 0;
|
|
3194
|
+
let y = 0;
|
|
3195
|
+
let mx = 0;
|
|
3196
|
+
let my = 0;
|
|
3197
|
+
iterate(parsedPath, (seg, _, prevX, prevY) => {
|
|
3198
|
+
const cmd = seg[0];
|
|
3199
|
+
const absCommand = cmd.toUpperCase();
|
|
3200
|
+
const isRelative = cmd === cmd.toLowerCase();
|
|
3201
|
+
const values = seg.slice(1);
|
|
3202
|
+
if (absCommand === "M") {
|
|
3203
|
+
pi += 1;
|
|
3204
|
+
[x, y] = values;
|
|
3205
|
+
x += isRelative ? prevX : 0;
|
|
3206
|
+
y += isRelative ? prevY : 0;
|
|
3207
|
+
mx = x;
|
|
3208
|
+
my = y;
|
|
3209
|
+
path = [isRelative ? [
|
|
3210
|
+
absCommand,
|
|
3211
|
+
mx,
|
|
3212
|
+
my
|
|
3213
|
+
] : seg];
|
|
3214
|
+
} else {
|
|
3215
|
+
if (absCommand === "Z") {
|
|
3216
|
+
x = mx;
|
|
3217
|
+
y = my;
|
|
3218
|
+
} else if (absCommand === "H") {
|
|
3219
|
+
[, x] = seg;
|
|
3220
|
+
x += isRelative ? prevX : 0;
|
|
3221
|
+
} else if (absCommand === "V") {
|
|
3222
|
+
[, y] = seg;
|
|
3223
|
+
y += isRelative ? prevY : 0;
|
|
3224
|
+
} else {
|
|
3225
|
+
[x, y] = seg.slice(-2);
|
|
3226
|
+
x += isRelative ? prevX : 0;
|
|
3227
|
+
y += isRelative ? prevY : 0;
|
|
3228
|
+
}
|
|
3229
|
+
path.push(seg);
|
|
3230
|
+
}
|
|
3231
|
+
composite[pi] = path;
|
|
3232
|
+
});
|
|
3233
|
+
return composite;
|
|
3234
|
+
};
|
|
3235
|
+
//#endregion
|
|
3236
|
+
//#region src/process/getSVGMatrix.ts
|
|
3237
|
+
/**
|
|
3238
|
+
* Returns a transformation matrix to apply to `<path>` elements.
|
|
3239
|
+
*
|
|
3240
|
+
* @see TransformObjectValues
|
|
3241
|
+
*
|
|
3242
|
+
* @param transform the `transformObject`
|
|
3243
|
+
* @returns a new transformation matrix
|
|
3244
|
+
*/
|
|
3245
|
+
const getSVGMatrix = (transform) => {
|
|
3246
|
+
let matrix = new CSSMatrix();
|
|
3247
|
+
const { origin } = transform;
|
|
3248
|
+
const [originX, originY] = origin;
|
|
3249
|
+
const { translate } = transform;
|
|
3250
|
+
const { rotate } = transform;
|
|
3251
|
+
const { skew } = transform;
|
|
3252
|
+
const { scale } = transform;
|
|
3253
|
+
if (Array.isArray(translate) && translate.length >= 2 && translate.every((x) => !Number.isNaN(+x)) && translate.some((x) => x !== 0)) matrix = matrix.translate(...translate);
|
|
3254
|
+
else if (typeof translate === "number" && !Number.isNaN(translate)) matrix = matrix.translate(translate);
|
|
3255
|
+
if (rotate || skew || scale) {
|
|
3256
|
+
matrix = matrix.translate(originX, originY);
|
|
3257
|
+
if (Array.isArray(rotate) && rotate.length >= 2 && rotate.every((x) => !Number.isNaN(+x)) && rotate.some((x) => x !== 0)) matrix = matrix.rotate(...rotate);
|
|
3258
|
+
else if (typeof rotate === "number" && !Number.isNaN(rotate)) matrix = matrix.rotate(rotate);
|
|
3259
|
+
if (Array.isArray(skew) && skew.length === 2 && skew.every((x) => !Number.isNaN(+x)) && skew.some((x) => x !== 0)) {
|
|
3260
|
+
matrix = skew[0] ? matrix.skewX(skew[0]) : matrix;
|
|
3261
|
+
matrix = skew[1] ? matrix.skewY(skew[1]) : matrix;
|
|
3262
|
+
} else if (typeof skew === "number" && !Number.isNaN(skew)) matrix = matrix.skewX(skew);
|
|
3263
|
+
if (Array.isArray(scale) && scale.length >= 2 && scale.every((x) => !Number.isNaN(+x)) && scale.some((x) => x !== 1)) matrix = matrix.scale(...scale);
|
|
3264
|
+
else if (typeof scale === "number" && !Number.isNaN(scale)) matrix = matrix.scale(scale);
|
|
3265
|
+
matrix = matrix.translate(-originX, -originY);
|
|
3266
|
+
}
|
|
3267
|
+
return matrix;
|
|
3268
|
+
};
|
|
3269
|
+
//#endregion
|
|
3270
|
+
//#region src/process/projection2d.ts
|
|
3271
|
+
/**
|
|
3272
|
+
* Transforms a specified point using a matrix, returning a new
|
|
3273
|
+
* Tuple *Object* comprising of the transformed point.
|
|
3274
|
+
* Neither the matrix nor the original point are altered.
|
|
3275
|
+
*
|
|
3276
|
+
* @copyright thednp © 2021
|
|
3277
|
+
*
|
|
3278
|
+
* @param cssm CSSMatrix instance
|
|
3279
|
+
* @param v Tuple
|
|
3280
|
+
* @returns the resulting Tuple
|
|
3281
|
+
*/
|
|
3282
|
+
const translatePoint = (cssm, v) => {
|
|
3283
|
+
let m = CSSMatrix.Translate(v[0], v[1], v[2]);
|
|
3284
|
+
[, , , m.m44] = v;
|
|
3285
|
+
m = cssm.multiply(m);
|
|
3286
|
+
return [
|
|
3287
|
+
m.m41,
|
|
3288
|
+
m.m42,
|
|
3289
|
+
m.m43,
|
|
3290
|
+
m.m44
|
|
3291
|
+
];
|
|
3292
|
+
};
|
|
3293
|
+
/**
|
|
3294
|
+
* Returns the [x,y] projected coordinates for a given an [x,y] point
|
|
3295
|
+
* and an [x,y,z] perspective origin point.
|
|
3296
|
+
*
|
|
3297
|
+
* Equation found here =>
|
|
3298
|
+
* http://en.wikipedia.org/wiki/3D_projection#Diagram
|
|
3299
|
+
* Details =>
|
|
3300
|
+
* https://stackoverflow.com/questions/23792505/predicted-rendering-of-css-3d-transformed-pixel
|
|
3301
|
+
*
|
|
3302
|
+
* @param m the transformation matrix
|
|
3303
|
+
* @param point2D the initial [x,y] coordinates
|
|
3304
|
+
* @param origin the [x,y,z] transform origin
|
|
3305
|
+
* @returns the projected [x,y] coordinates
|
|
3306
|
+
*/
|
|
3307
|
+
const projection2d = (m, point2D, origin) => {
|
|
3308
|
+
const [originX, originY, originZ] = origin;
|
|
3309
|
+
const [x, y, z] = translatePoint(m, [
|
|
3310
|
+
point2D[0],
|
|
3311
|
+
point2D[1],
|
|
3312
|
+
0,
|
|
3313
|
+
1
|
|
3314
|
+
]);
|
|
3315
|
+
const relativePositionX = x - originX;
|
|
3316
|
+
const relativePositionY = y - originY;
|
|
3317
|
+
const relativePositionZ = z - originZ;
|
|
3318
|
+
return [relativePositionX * (Math.abs(originZ) / Math.abs(relativePositionZ) || 1) + originX, relativePositionY * (Math.abs(originZ) / Math.abs(relativePositionZ) || 1) + originY];
|
|
3319
|
+
};
|
|
3320
|
+
//#endregion
|
|
3321
|
+
//#region src/process/transformPath.ts
|
|
3322
|
+
/**
|
|
3323
|
+
* Apply a 2D / 3D transformation to a PathArray.
|
|
3324
|
+
*
|
|
3325
|
+
* Since SVGElement doesn't support 3D transformation, this function
|
|
3326
|
+
* creates a 2D projection of the path element.
|
|
3327
|
+
*
|
|
3328
|
+
* @param pathInput - The PathArray or path string to transform
|
|
3329
|
+
* @param transform - The transform functions object (translate, rotate, skew, scale, origin)
|
|
3330
|
+
* @returns The transformed PathArray
|
|
3331
|
+
*
|
|
3332
|
+
* @example
|
|
3333
|
+
* ```ts
|
|
3334
|
+
* transformPath('M0 0L100 0L100 100L0 100Z', { translate: [10, 20], scale: 2 })
|
|
3335
|
+
* // => [['M', 10, 20], ['L', 210, 20], ['L', 210, 220], ['L', 10, 220], ['Z']]
|
|
3336
|
+
* ```
|
|
3337
|
+
*/
|
|
3338
|
+
const transformPath = (pathInput, transform) => {
|
|
3339
|
+
let x = 0;
|
|
3340
|
+
let y = 0;
|
|
3341
|
+
let lx = 0;
|
|
3342
|
+
let ly = 0;
|
|
3343
|
+
let j = 0;
|
|
3344
|
+
let jj = 0;
|
|
3345
|
+
const path = parsePathString(pathInput);
|
|
3346
|
+
const transformProps = transform && Object.keys(transform);
|
|
3347
|
+
if (!transform || transformProps && !transformProps.length) return path.slice(0);
|
|
3348
|
+
if (!transform.origin) Object.assign(transform, { origin: defaultOptions.origin });
|
|
3349
|
+
const origin = transform.origin;
|
|
3350
|
+
const matrixInstance = getSVGMatrix(transform);
|
|
3351
|
+
if (matrixInstance.isIdentity) return path.slice(0);
|
|
3352
|
+
return iterate(path, (seg, index, lastX, lastY) => {
|
|
3353
|
+
let [pathCommand] = seg;
|
|
3354
|
+
const absCommand = pathCommand.toUpperCase();
|
|
3355
|
+
const absoluteSegment = absCommand !== pathCommand ? absolutizeSegment(seg, index, lastX, lastY) : seg.slice(0);
|
|
3356
|
+
let result = absCommand === "A" ? ["C"].concat(arcToCubic(lastX, lastY, absoluteSegment[1], absoluteSegment[2], absoluteSegment[3], absoluteSegment[4], absoluteSegment[5], absoluteSegment[6], absoluteSegment[7])) : absCommand === "V" ? [
|
|
3357
|
+
"L",
|
|
3358
|
+
lastX,
|
|
3359
|
+
absoluteSegment[1]
|
|
3360
|
+
] : absCommand === "H" ? [
|
|
3361
|
+
"L",
|
|
3362
|
+
absoluteSegment[1],
|
|
3363
|
+
lastY
|
|
3364
|
+
] : absoluteSegment;
|
|
3365
|
+
pathCommand = result[0];
|
|
3366
|
+
const isLongArc = pathCommand === "C" && result.length > 7;
|
|
3367
|
+
const tempSegment = isLongArc ? result.slice(0, 7) : result.slice(0);
|
|
3368
|
+
if (isLongArc) {
|
|
3369
|
+
path.splice(index + 1, 0, ["C"].concat(result.slice(7)));
|
|
3370
|
+
result = tempSegment;
|
|
3371
|
+
}
|
|
3372
|
+
if (pathCommand === "L") {
|
|
3373
|
+
[lx, ly] = projection2d(matrixInstance, [result[1], result[2]], origin);
|
|
3374
|
+
if (x !== lx && y !== ly) result = [
|
|
3375
|
+
"L",
|
|
3376
|
+
lx,
|
|
3377
|
+
ly
|
|
3378
|
+
];
|
|
3379
|
+
else if (y === ly) result = ["H", lx];
|
|
3380
|
+
else if (x === lx) result = ["V", ly];
|
|
3381
|
+
} else for (j = 1, jj = result.length; j < jj; j += 2) {
|
|
3382
|
+
[lx, ly] = projection2d(matrixInstance, [+result[j], +result[j + 1]], origin);
|
|
3383
|
+
result[j] = lx;
|
|
3384
|
+
result[j + 1] = ly;
|
|
3385
|
+
}
|
|
3386
|
+
x = lx;
|
|
3387
|
+
y = ly;
|
|
3388
|
+
return result;
|
|
3389
|
+
});
|
|
3390
|
+
};
|
|
3391
|
+
//#endregion
|
|
3392
|
+
//#region src/process/reverseCurve.ts
|
|
3393
|
+
/**
|
|
3394
|
+
* Reverses all segments of a `pathArray`
|
|
3395
|
+
* which consists of only C (cubic-bezier) path commands.
|
|
3396
|
+
*
|
|
3397
|
+
* @param path the source `pathArray`
|
|
3398
|
+
* @returns the reversed `pathArray`
|
|
3399
|
+
*/
|
|
3400
|
+
const reverseCurve = (path) => {
|
|
3401
|
+
const rotatedCurve = path.slice(1).map((x, i, curveOnly) => !i ? path[0].slice(1).concat(x.slice(1)) : curveOnly[i - 1].slice(-2).concat(x.slice(1))).map((x) => x.map((_, i) => x[x.length - i - 2 * (1 - i % 2)])).reverse();
|
|
3402
|
+
return [["M"].concat(rotatedCurve[0].slice(0, 2))].concat(rotatedCurve.map((x) => ["C"].concat(x.slice(2))));
|
|
3403
|
+
};
|
|
3404
|
+
//#endregion
|
|
3405
|
+
//#region src/process/roundPath.ts
|
|
3406
|
+
/**
|
|
3407
|
+
* Rounds the values of a `pathArray` instance to
|
|
3408
|
+
* a specified amount of decimals and returns it.
|
|
3409
|
+
*
|
|
3410
|
+
* @param path the source `pathArray`
|
|
3411
|
+
* @param roundOption the amount of decimals to round numbers to
|
|
3412
|
+
* @returns the resulted `pathArray` with rounded values
|
|
3413
|
+
*/
|
|
3414
|
+
const roundPath = (path, roundOption) => {
|
|
3415
|
+
let { round } = defaultOptions;
|
|
3416
|
+
round = roundOption === "off" ? roundOption : typeof roundOption === "number" && roundOption >= 0 ? roundOption : typeof round === "number" && round >= 0 ? round : "off";
|
|
3417
|
+
if (round === "off") return path.slice(0);
|
|
3418
|
+
return iterate(path, (segment) => {
|
|
3419
|
+
return roundSegment(segment, round);
|
|
3420
|
+
});
|
|
3421
|
+
};
|
|
3422
|
+
//#endregion
|
|
3423
|
+
//#region src/morph/fixPath.ts
|
|
3424
|
+
/**
|
|
3425
|
+
* Checks a `PathArray` for an unnecessary `Z` segment
|
|
3426
|
+
* and returns a new `PathArray` without it.
|
|
3427
|
+
* In short, if the segment before `Z` extends to `M`,
|
|
3428
|
+
* the `Z` segment must be removed.
|
|
3429
|
+
*
|
|
3430
|
+
* The `pathInput` must be a single path, without
|
|
3431
|
+
* sub-paths. For multi-path `<path>` elements,
|
|
3432
|
+
* use `splitPath` first and apply this utility on each
|
|
3433
|
+
* sub-path separately.
|
|
3434
|
+
*
|
|
3435
|
+
* @param pathInput the `pathArray` source
|
|
3436
|
+
* @returns void
|
|
3437
|
+
*/
|
|
3438
|
+
const fixPath = (pathInput) => {
|
|
3439
|
+
const pathArray = parsePathString(pathInput);
|
|
3440
|
+
if (isClosedPath(pathArray)) {
|
|
3441
|
+
const normalArray = normalizePath(pathArray);
|
|
3442
|
+
const length = pathArray.length;
|
|
3443
|
+
const segBeforeZ = length - 2;
|
|
3444
|
+
const [mx, my] = normalArray[0].slice(1);
|
|
3445
|
+
const [x, y] = normalArray[segBeforeZ].slice(-2);
|
|
3446
|
+
if (mx === x && my === y) pathArray.splice(length - 1, 1);
|
|
3447
|
+
}
|
|
3448
|
+
};
|
|
3449
|
+
//#endregion
|
|
3450
|
+
//#region src/morph/splitCubicSegment.ts
|
|
3451
|
+
/**
|
|
3452
|
+
* Split a cubic Bézier into two cubics at parameter t [0–1].
|
|
3453
|
+
*
|
|
3454
|
+
* @param x1 - Start point X
|
|
3455
|
+
* @param y1 - Start point Y
|
|
3456
|
+
* @param x2 - First control point X
|
|
3457
|
+
* @param y2 - First control point Y
|
|
3458
|
+
* @param x3 - Second control point X
|
|
3459
|
+
* @param y3 - Second control point Y
|
|
3460
|
+
* @param x4 - End point X
|
|
3461
|
+
* @param y4 - End point Y
|
|
3462
|
+
* @param t - Parameter in range [0, 1] at which to split
|
|
3463
|
+
* @returns Array of two cubic segments, each as [x1,y1, x2,y2, x3,y3, x4,y4]
|
|
3464
|
+
*/
|
|
3465
|
+
function splitCubicSegment(x1, y1, x2, y2, x3, y3, x4, y4, t) {
|
|
3466
|
+
const [px01, py01] = midPoint([x1, y1], [x2, y2], t);
|
|
3467
|
+
const [px12, py12] = midPoint([x2, y2], [x3, y3], t);
|
|
3468
|
+
const [px23, py23] = midPoint([x3, y3], [x4, y4], t);
|
|
3469
|
+
const [cx0, cy0] = midPoint([px01, py01], [px12, py12], t);
|
|
3470
|
+
const [cx1, cy1] = midPoint([px12, py12], [px23, py23], t);
|
|
3471
|
+
const [px, py] = midPoint([cx0, cy0], [cx1, cy1], t);
|
|
3472
|
+
return [[
|
|
3473
|
+
x1,
|
|
3474
|
+
y1,
|
|
3475
|
+
px01,
|
|
3476
|
+
py01,
|
|
3477
|
+
cx0,
|
|
3478
|
+
cy0,
|
|
3479
|
+
px,
|
|
3480
|
+
py
|
|
3481
|
+
], [
|
|
3482
|
+
px,
|
|
3483
|
+
py,
|
|
3484
|
+
cx1,
|
|
3485
|
+
cy1,
|
|
3486
|
+
px23,
|
|
3487
|
+
py23,
|
|
3488
|
+
x4,
|
|
3489
|
+
y4
|
|
3490
|
+
]];
|
|
3491
|
+
}
|
|
3492
|
+
//#endregion
|
|
3493
|
+
//#region src/morph/pathToPolyline.ts
|
|
3494
|
+
/**
|
|
3495
|
+
* Converts any `PolyLineArray`/`PolygonArray` path (closed or open) to an explicit polyline (M + L*).
|
|
3496
|
+
* If the path is closed (has Z), the Z is replaced with an explicit L back to the initial M point.
|
|
3497
|
+
* This allows uniform processing without special-casing Z.
|
|
3498
|
+
*
|
|
3499
|
+
* @param path string or PathArray
|
|
3500
|
+
* @returns PolylineArray (M + L*) — never contains Z
|
|
3501
|
+
*/
|
|
3502
|
+
const pathToPolyline = (path) => {
|
|
3503
|
+
const normal = normalizePath(path);
|
|
3504
|
+
if (!isPolygonArray(normal) && !isPolylineArray(normal)) throw TypeError(`${error}: pathValue is not a polyline/polygon`);
|
|
3505
|
+
if (!isClosedPath(normal)) return normal;
|
|
3506
|
+
const result = [normal[0]];
|
|
3507
|
+
const [mx, my] = normal[0].slice(1);
|
|
3508
|
+
for (let i = 1; i < normal.length; i++) {
|
|
3509
|
+
const seg = normal[i];
|
|
3510
|
+
if (seg[0].toUpperCase() === "Z") result.push([
|
|
3511
|
+
"L",
|
|
3512
|
+
mx,
|
|
3513
|
+
my
|
|
3514
|
+
]);
|
|
3515
|
+
else result.push(seg);
|
|
3516
|
+
}
|
|
3517
|
+
return result;
|
|
3518
|
+
};
|
|
3519
|
+
//#endregion
|
|
3520
|
+
//#region src/morph/splitLineToCount.ts
|
|
3521
|
+
/**
|
|
3522
|
+
* Split a line segment into `count` smaller segments of equal length
|
|
3523
|
+
* using the same repeated front-cutting strategy as splitCubicToCount.
|
|
3524
|
+
*
|
|
3525
|
+
* Does NOT mutate input.
|
|
3526
|
+
*
|
|
3527
|
+
* @param x1 - Start point X
|
|
3528
|
+
* @param y1 - Start point Y
|
|
3529
|
+
* @param x2 - End point X
|
|
3530
|
+
* @param y2 - End point Y
|
|
3531
|
+
* @param count - Number of segments to split into
|
|
3532
|
+
* @returns Array of `count` line segments, each as [x1, y1, x2, y2]
|
|
3533
|
+
*/
|
|
3534
|
+
function splitLineToCount(x1, y1, x2, y2, count) {
|
|
3535
|
+
if (count <= 1) return [[
|
|
3536
|
+
x1,
|
|
3537
|
+
y1,
|
|
3538
|
+
x2,
|
|
3539
|
+
y2
|
|
3540
|
+
]];
|
|
3541
|
+
const result = [];
|
|
3542
|
+
const dx = x2 - x1;
|
|
3543
|
+
const dy = y2 - y1;
|
|
3544
|
+
let currentX = x1;
|
|
3545
|
+
let currentY = y1;
|
|
3546
|
+
let i = 0;
|
|
3547
|
+
while (i < count) {
|
|
3548
|
+
const t = 1 / (count - i);
|
|
3549
|
+
const nextX = x1 + t * dx;
|
|
3550
|
+
const nextY = y1 + t * dy;
|
|
3551
|
+
result.push([
|
|
3552
|
+
currentX,
|
|
3553
|
+
currentY,
|
|
3554
|
+
nextX,
|
|
3555
|
+
nextY
|
|
3556
|
+
]);
|
|
3557
|
+
currentX = nextX;
|
|
3558
|
+
currentY = nextY;
|
|
3559
|
+
i++;
|
|
3560
|
+
}
|
|
3561
|
+
return result;
|
|
3562
|
+
}
|
|
3563
|
+
//#endregion
|
|
3564
|
+
//#region src/morph/getPathSplits.ts
|
|
3565
|
+
/**
|
|
3566
|
+
* Determine the right amount of splits for each segment in a given PathArray
|
|
3567
|
+
* and for a target total amount of sub-segments.
|
|
3568
|
+
* For a triangle path "M0,0 L600,300 L0,600 Z" we have 3 equal lines,
|
|
3569
|
+
* we can easily do 4 splits per line and go to town, however, most triangles
|
|
3570
|
+
* are not even so we need to take side lengths into account.
|
|
3571
|
+
* @param path The target PathArray
|
|
3572
|
+
* @param target The total amount of sub-segments
|
|
3573
|
+
* @returns an array of numbers reprezenting the sub-segment count for each segment
|
|
3574
|
+
*/
|
|
3575
|
+
function getPathSplits(path, target) {
|
|
3576
|
+
if (target <= 1) throw new TypeError(`${error}: target must be >= 2`);
|
|
3577
|
+
const totalLength = getTotalLength(path);
|
|
3578
|
+
if (totalLength === 0) return Array(path.length).fill(1);
|
|
3579
|
+
const idealSegLen = totalLength / target;
|
|
3580
|
+
const isPoly = isPolylineArray(path);
|
|
3581
|
+
const splits = [1];
|
|
3582
|
+
const lengths = [0];
|
|
3583
|
+
iterate(path, (seg, i, prevX, prevY) => {
|
|
3584
|
+
if (i > 0) {
|
|
3585
|
+
const [endX, endY] = seg.slice(-2);
|
|
3586
|
+
const segLen = isPoly ? getLineLength(prevX, prevY, endX, endY) : getCubicLength(prevX, prevY, seg[1], seg[2], seg[3], seg[4], seg[5], seg[6]);
|
|
3587
|
+
lengths.push(segLen);
|
|
3588
|
+
splits.push(1);
|
|
3589
|
+
}
|
|
3590
|
+
});
|
|
3591
|
+
let totalAllocated = 1;
|
|
3592
|
+
for (let i = 1; i < lengths.length; i++) {
|
|
3593
|
+
const segLen = lengths[i];
|
|
3594
|
+
const desired = segLen > idealSegLen ? Math.round(segLen / idealSegLen) : 1;
|
|
3595
|
+
splits[i] = desired;
|
|
3596
|
+
totalAllocated += desired;
|
|
3597
|
+
}
|
|
3598
|
+
let diff = target - totalAllocated;
|
|
3599
|
+
if (diff !== 0) {
|
|
3600
|
+
const candidates = [];
|
|
3601
|
+
for (let i = 1; i < lengths.length; i++) if (lengths[i] > 0) candidates.push([i, lengths[i]]);
|
|
3602
|
+
const cLen = candidates.length;
|
|
3603
|
+
if (diff < 0) {
|
|
3604
|
+
candidates.sort((a, b) => a[1] - b[1]);
|
|
3605
|
+
for (let i = 0; i < cLen; i++) {
|
|
3606
|
+
const idx = candidates[i][0];
|
|
3607
|
+
if (splits[idx] > 1 && candidates[i][1] > 0) {
|
|
3608
|
+
splits[idx]--;
|
|
3609
|
+
diff++;
|
|
3610
|
+
}
|
|
3611
|
+
if (diff === 0) break;
|
|
3612
|
+
else if (i === cLen - 1) i = 0;
|
|
3613
|
+
}
|
|
3614
|
+
} else if (diff > 0) {
|
|
3615
|
+
candidates.sort((a, b) => b[1] - a[1]);
|
|
3616
|
+
for (let i = 0; i < cLen; i++) {
|
|
3617
|
+
const idx = candidates[i][0];
|
|
3618
|
+
if (candidates[i][1] > 0) {
|
|
3619
|
+
splits[idx]++;
|
|
3620
|
+
diff--;
|
|
3621
|
+
}
|
|
3622
|
+
if (diff === 0) break;
|
|
3623
|
+
else if (i === cLen - 1) i = 0;
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
return splits;
|
|
3628
|
+
}
|
|
3629
|
+
//#endregion
|
|
3630
|
+
//#region src/morph/splitLinePathToCount.ts
|
|
3631
|
+
/**
|
|
3632
|
+
* Splits a PolylineArray so that it has exactly `target` line segments.
|
|
3633
|
+
*
|
|
3634
|
+
* @param path - The polyline array to split
|
|
3635
|
+
* @param target - The desired number of line segments
|
|
3636
|
+
* @returns The modified polyline array with the target segment count
|
|
3637
|
+
*/
|
|
3638
|
+
function splitLinePathToCount(path, target) {
|
|
3639
|
+
if (path.length < 2 || target <= 1) return path;
|
|
3640
|
+
const splits = getPathSplits(path, target);
|
|
3641
|
+
let totalAdded = 0;
|
|
3642
|
+
const newPath = [path[0]];
|
|
3643
|
+
const pathLen = path.length;
|
|
3644
|
+
let currentX = path[0][1];
|
|
3645
|
+
let currentY = path[0][2];
|
|
3646
|
+
for (let i = 1; i < pathLen; i++) {
|
|
3647
|
+
const [endX, endY] = path[i].slice(1);
|
|
3648
|
+
const count = splits[i];
|
|
3649
|
+
if (count >= 1) {
|
|
3650
|
+
const subLines = splitLineToCount(currentX, currentY, endX, endY, count);
|
|
3651
|
+
for (const sub of subLines) {
|
|
3652
|
+
newPath.push([
|
|
3653
|
+
"L",
|
|
3654
|
+
sub[2],
|
|
3655
|
+
sub[3]
|
|
3656
|
+
]);
|
|
3657
|
+
totalAdded++;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
currentX = endX;
|
|
3661
|
+
currentY = endY;
|
|
3662
|
+
}
|
|
3663
|
+
if (newPath.length !== target) console.warn(`${error}: requested ${target} segments, got ${newPath.length}. Adjusted on last segment.`);
|
|
3664
|
+
return newPath;
|
|
3665
|
+
}
|
|
3666
|
+
//#endregion
|
|
3667
|
+
//#region src/morph/splitCubicToCount.ts
|
|
3668
|
+
/**
|
|
3669
|
+
* Split a cubic Bézier into `count` segments of roughly equal parameter length.
|
|
3670
|
+
* Does NOT mutate input parameters.
|
|
3671
|
+
*
|
|
3672
|
+
* @param x1 - Start point X
|
|
3673
|
+
* @param y1 - Start point Y
|
|
3674
|
+
* @param x2 - First control point X
|
|
3675
|
+
* @param y2 - First control point Y
|
|
3676
|
+
* @param x3 - Second control point X
|
|
3677
|
+
* @param y3 - Second control point Y
|
|
3678
|
+
* @param x4 - End point X
|
|
3679
|
+
* @param y4 - End point Y
|
|
3680
|
+
* @param count - Number of segments to split into
|
|
3681
|
+
* @returns Array of `count` cubic segments, each as [x1,y1,x2,y2,x3,y3,x4,y4]
|
|
3682
|
+
*/
|
|
3683
|
+
function splitCubicToCount(x1, y1, x2, y2, x3, y3, x4, y4, count) {
|
|
3684
|
+
if (count <= 1) return [[
|
|
3685
|
+
x1,
|
|
3686
|
+
y1,
|
|
3687
|
+
x2,
|
|
3688
|
+
y2,
|
|
3689
|
+
x3,
|
|
3690
|
+
y3,
|
|
3691
|
+
x4,
|
|
3692
|
+
y4
|
|
3693
|
+
]];
|
|
3694
|
+
const result = [];
|
|
3695
|
+
let cx1 = x1;
|
|
3696
|
+
let cy1 = y1;
|
|
3697
|
+
let cx2 = x2;
|
|
3698
|
+
let cy2 = y2;
|
|
3699
|
+
let cx3 = x3;
|
|
3700
|
+
let cy3 = y3;
|
|
3701
|
+
let cx4 = x4;
|
|
3702
|
+
let cy4 = y4;
|
|
3703
|
+
let i = 0;
|
|
3704
|
+
while (i < count) {
|
|
3705
|
+
const t = 1 / (count - i);
|
|
3706
|
+
const [first, second] = splitCubicSegment(cx1, cy1, cx2, cy2, cx3, cy3, cx4, cy4, t);
|
|
3707
|
+
result.push(first);
|
|
3708
|
+
[cx1, cy1, cx2, cy2, cx3, cy3, cx4, cy4] = second;
|
|
3709
|
+
i++;
|
|
3710
|
+
}
|
|
3711
|
+
return result;
|
|
3712
|
+
}
|
|
3713
|
+
//#endregion
|
|
3714
|
+
//#region src/morph/splitCurvePathToCount.ts
|
|
3715
|
+
/**
|
|
3716
|
+
* Splits a CurveArray so that it has exactly `target` cubic segments.
|
|
3717
|
+
*
|
|
3718
|
+
* @param path - The curve array to split
|
|
3719
|
+
* @param target - The desired number of cubic segments
|
|
3720
|
+
* @returns The modified curve array with the target segment count
|
|
3721
|
+
*/
|
|
3722
|
+
function splitCurvePathToCount(path, target) {
|
|
3723
|
+
if (path.length < 2 || target <= 1) return path;
|
|
3724
|
+
const splits = getPathSplits(path, target);
|
|
3725
|
+
let totalAdded = 0;
|
|
3726
|
+
const newPath = [path[0]];
|
|
3727
|
+
const pathLen = path.length;
|
|
3728
|
+
let currentX = path[0][1];
|
|
3729
|
+
let currentY = path[0][2];
|
|
3730
|
+
for (let i = 1; i < pathLen; i++) {
|
|
3731
|
+
const seg = path[i];
|
|
3732
|
+
const [endX, endY] = seg.slice(-2);
|
|
3733
|
+
const count = splits[i];
|
|
3734
|
+
if (count >= 1) {
|
|
3735
|
+
const subs = splitCubicToCount(currentX, currentY, seg[1], seg[2], seg[3], seg[4], seg[5], seg[6], count);
|
|
3736
|
+
for (const sub of subs) {
|
|
3737
|
+
newPath.push([
|
|
3738
|
+
"C",
|
|
3739
|
+
sub[2],
|
|
3740
|
+
sub[3],
|
|
3741
|
+
sub[4],
|
|
3742
|
+
sub[5],
|
|
3743
|
+
sub[6],
|
|
3744
|
+
sub[7]
|
|
3745
|
+
]);
|
|
3746
|
+
totalAdded++;
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
currentX = endX;
|
|
3750
|
+
currentY = endY;
|
|
3751
|
+
}
|
|
3752
|
+
if (newPath.length !== target) console.warn(`${error}: requested ${target} segments, got ${newPath.length}.`);
|
|
3753
|
+
return newPath;
|
|
3754
|
+
}
|
|
3755
|
+
//#endregion
|
|
3756
|
+
//#region src/morph/getRotatedPath.ts
|
|
3757
|
+
/**
|
|
3758
|
+
* Returns all possible rotations of a path (line or curve) by shifting the start point.
|
|
3759
|
+
* Each rotation is a new PathArray starting at a different original segment.
|
|
3760
|
+
*
|
|
3761
|
+
* @param path PathArray (M + L/C + optional Z) — must be single subpath, normalized
|
|
3762
|
+
* @returns PathArray[] — array of all possible rotations
|
|
3763
|
+
*/
|
|
3764
|
+
function getRotations(a) {
|
|
3765
|
+
const pathLen = a.length;
|
|
3766
|
+
const pointCount = pathLen - 1;
|
|
3767
|
+
let path;
|
|
3768
|
+
const result = [];
|
|
3769
|
+
for (let idx = 0; idx < pathLen; idx++) {
|
|
3770
|
+
path = [];
|
|
3771
|
+
for (let i = 0; i < pathLen; i++) {
|
|
3772
|
+
let oldSegIdx = idx + i;
|
|
3773
|
+
let seg;
|
|
3774
|
+
if (i === 0 || a[oldSegIdx] && a[oldSegIdx][0] === "M") {
|
|
3775
|
+
seg = a[oldSegIdx];
|
|
3776
|
+
path.push(["M", ...seg.slice(-2)]);
|
|
3777
|
+
continue;
|
|
3778
|
+
}
|
|
3779
|
+
if (oldSegIdx >= pathLen) oldSegIdx -= pointCount;
|
|
3780
|
+
path.push(a[oldSegIdx]);
|
|
3781
|
+
}
|
|
3782
|
+
result.push(path);
|
|
3783
|
+
}
|
|
3784
|
+
return result;
|
|
3785
|
+
}
|
|
3786
|
+
/**
|
|
3787
|
+
* Finds the best rotation of pathA to match pathB by minimizing sum of squared distances
|
|
3788
|
+
* between corresponding endpoints.
|
|
3789
|
+
*
|
|
3790
|
+
* Works with both polygon (L) and curve (C) arrays.
|
|
3791
|
+
*
|
|
3792
|
+
* @param pathA PathArray to rotate (will be modified in rotation options)
|
|
3793
|
+
* @param pathB PathArray reference (fixed)
|
|
3794
|
+
* @returns PathArray — best rotation of pathA
|
|
3795
|
+
*/
|
|
3796
|
+
function getRotatedPath(pathA, pathB, computedRotations) {
|
|
3797
|
+
const rotations = computedRotations || getRotations(pathA);
|
|
3798
|
+
if (pathA.length !== pathB.length) throw new TypeError(error + ": paths must have the same number of segments after equalization");
|
|
3799
|
+
let bestIndex = 0;
|
|
3800
|
+
let minDistanceSq = Infinity;
|
|
3801
|
+
for (let ri = 0; ri < rotations.length; ri++) {
|
|
3802
|
+
const rotation = rotations[ri];
|
|
3803
|
+
const rLen = rotation.length;
|
|
3804
|
+
let sumDistSq = 0;
|
|
3805
|
+
for (let i = 0; i < rLen; i++) {
|
|
3806
|
+
const segA = rotation[i];
|
|
3807
|
+
const segB = pathB[i];
|
|
3808
|
+
const endA = segA.slice(-2);
|
|
3809
|
+
const endB = segB.slice(-2);
|
|
3810
|
+
const dx = endA[0] - endB[0];
|
|
3811
|
+
const dy = endA[1] - endB[1];
|
|
3812
|
+
sumDistSq += dx * dx + dy * dy;
|
|
3813
|
+
}
|
|
3814
|
+
if (sumDistSq < minDistanceSq) {
|
|
3815
|
+
minDistanceSq = sumDistSq;
|
|
3816
|
+
bestIndex = ri;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
return rotations[bestIndex];
|
|
3820
|
+
}
|
|
3821
|
+
//#endregion
|
|
3822
|
+
//#region src/morph/equalizeSegments.ts
|
|
3823
|
+
const equalizeSegmentsDefaults = {
|
|
3824
|
+
mode: "auto",
|
|
3825
|
+
sampleSize: 10,
|
|
3826
|
+
roundValues: 4,
|
|
3827
|
+
reverse: true,
|
|
3828
|
+
close: false,
|
|
3829
|
+
target: void 0
|
|
3830
|
+
};
|
|
3831
|
+
/**
|
|
3832
|
+
* Equalizes two paths for morphing (single subpath only).
|
|
3833
|
+
*
|
|
3834
|
+
* @see https://minus-ze.ro/posts/morphing-arbitrary-paths-in-svg/
|
|
3835
|
+
* @param path1 - First path string or PathArray
|
|
3836
|
+
* @param path2 - Second path string or PathArray
|
|
3837
|
+
* @param initialCfg - Equalization options
|
|
3838
|
+
* @returns Tuple of two equalized MorphPathArrays
|
|
3839
|
+
*
|
|
3840
|
+
* @example
|
|
3841
|
+
* ```ts
|
|
3842
|
+
* const [eq1, eq2] = equalizeSegments('M0 0L100 0L50 100Z', 'M0 0L100 0L100 100L0 100Z')
|
|
3843
|
+
* // eq1.length === eq2.length
|
|
3844
|
+
* ```
|
|
3845
|
+
*/
|
|
3846
|
+
const equalizeSegments = (path1, path2, initialCfg = {}) => {
|
|
3847
|
+
const { close, mode, reverse, roundValues, target: initialTarget } = Object.assign(equalizeSegmentsDefaults, initialCfg);
|
|
3848
|
+
let p1 = normalizePath(path1);
|
|
3849
|
+
let p2 = normalizePath(path2);
|
|
3850
|
+
fixPath(p1);
|
|
3851
|
+
fixPath(p2);
|
|
3852
|
+
let bothPoly = (isPolygonArray(p1) || isPolylineArray(p1)) && (isPolygonArray(p2) || isPolylineArray(p2));
|
|
3853
|
+
if (bothPoly && mode === "auto") {
|
|
3854
|
+
p1 = pathToPolyline(p1);
|
|
3855
|
+
p2 = pathToPolyline(p2);
|
|
3856
|
+
} else {
|
|
3857
|
+
bothPoly = false;
|
|
3858
|
+
p1 = pathToCurve(p1);
|
|
3859
|
+
p2 = pathToCurve(p2);
|
|
3860
|
+
}
|
|
3861
|
+
const area1 = polygonArea(samplePolygon(p1));
|
|
3862
|
+
const area2 = polygonArea(samplePolygon(p2));
|
|
3863
|
+
if (reverse !== false && Math.sign(area1) !== Math.sign(area2)) p2 = reversePath(p2);
|
|
3864
|
+
const segCount1 = p1.length;
|
|
3865
|
+
const segCount2 = p2.length;
|
|
3866
|
+
const minTarget = Math.max(segCount1, segCount2);
|
|
3867
|
+
let target = minTarget;
|
|
3868
|
+
if (typeof initialTarget !== "number") {
|
|
3869
|
+
const avgLen = (getTotalLength(p1) + getTotalLength(p2)) / 2;
|
|
3870
|
+
const avgSegLen = avgLen / Math.max(segCount1, segCount2);
|
|
3871
|
+
const idealSegCount = Math.max(minTarget, Math.round(avgLen / Math.max(avgSegLen, 1)));
|
|
3872
|
+
target = Math.min(idealSegCount, Math.max(segCount1, segCount2) * 3);
|
|
3873
|
+
} else if (initialTarget >= minTarget) target = initialTarget;
|
|
3874
|
+
else console.warn("equalizeSegments \"target\" option: " + initialTarget + ", expected >= " + minTarget);
|
|
3875
|
+
let equalP1 = p1;
|
|
3876
|
+
let equalP2 = p2;
|
|
3877
|
+
if (bothPoly) {
|
|
3878
|
+
equalP1 = splitLinePathToCount(p1, target);
|
|
3879
|
+
equalP2 = splitLinePathToCount(p2, target);
|
|
3880
|
+
} else {
|
|
3881
|
+
equalP1 = splitCurvePathToCount(p1, target);
|
|
3882
|
+
equalP2 = splitCurvePathToCount(p2, target);
|
|
3883
|
+
}
|
|
3884
|
+
equalP2 = getRotatedPath(equalP2, equalP1);
|
|
3885
|
+
if (typeof roundValues === "number" && roundValues !== 4) {
|
|
3886
|
+
equalP1 = roundPath(equalP1, roundValues);
|
|
3887
|
+
equalP2 = roundPath(equalP2, roundValues);
|
|
3888
|
+
}
|
|
3889
|
+
if (close) {
|
|
3890
|
+
equalP1.push(["Z"]);
|
|
3891
|
+
equalP2.push(["Z"]);
|
|
3892
|
+
}
|
|
3893
|
+
return [equalP1, equalP2];
|
|
3894
|
+
};
|
|
3895
|
+
//#endregion
|
|
3896
|
+
//#region src/util/pathIntersection.ts
|
|
3897
|
+
const intersect = (x1, y1, x2, y2, x3, y3, x4, y4) => {
|
|
3898
|
+
if (Math.max(x1, x2) < Math.min(x3, x4) || Math.min(x1, x2) > Math.max(x3, x4) || Math.max(y1, y2) < Math.min(y3, y4) || Math.min(y1, y2) > Math.max(y3, y4)) return;
|
|
3899
|
+
const nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4), ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4), denominator = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
|
3900
|
+
if (!denominator) return;
|
|
3901
|
+
const px = nx / denominator, py = ny / denominator, px2 = roundTo(px, 2), py2 = roundTo(py, 2);
|
|
3902
|
+
if (px2 < roundTo(Math.min(x1, x2), 2) || px2 > roundTo(Math.max(x1, x2), 2) || px2 < roundTo(Math.min(x3, x4), 2) || px2 > roundTo(Math.max(x3, x4), 2) || py2 < roundTo(Math.min(y1, y2), 2) || py2 > roundTo(Math.max(y1, y2), 2) || py2 < roundTo(Math.min(y3, y4), 2) || py2 > roundTo(Math.max(y3, y4), 2)) return;
|
|
3903
|
+
return {
|
|
3904
|
+
x: px,
|
|
3905
|
+
y: py
|
|
3906
|
+
};
|
|
3907
|
+
};
|
|
3908
|
+
/**
|
|
3909
|
+
* Checks if a point is inside a bounding box.
|
|
3910
|
+
*
|
|
3911
|
+
* @param bbox - The bounding box as [minX, minY, maxX, maxY]
|
|
3912
|
+
* @param point - The point as [x, y]
|
|
3913
|
+
* @returns True if the point is inside or on the edge of the bounding box
|
|
3914
|
+
*/
|
|
3915
|
+
const isPointInsideBBox = (bbox, [x, y]) => {
|
|
3916
|
+
const [minX, minY, maxX, maxY] = bbox;
|
|
3917
|
+
return x >= minX && x <= maxX && y >= minY && y <= maxY;
|
|
3918
|
+
};
|
|
3919
|
+
/**
|
|
3920
|
+
* Checks if two bounding boxes intersect.
|
|
3921
|
+
*
|
|
3922
|
+
* @param a - First bounding box as [minX, minY, maxX, maxY]
|
|
3923
|
+
* @param b - Second bounding box as [minX, minY, maxX, maxY]
|
|
3924
|
+
* @returns True if the bounding boxes overlap
|
|
3925
|
+
*/
|
|
3926
|
+
const boundingBoxIntersect = (a, b) => {
|
|
3927
|
+
const [ax1, ay1, ax2, ay2] = a;
|
|
3928
|
+
const [bx1, by1, bx2, by2] = b;
|
|
3929
|
+
return isPointInsideBBox(b, [ax1, ay1]) || isPointInsideBBox(b, [ax2, ay1]) || isPointInsideBBox(b, [ax1, ay2]) || isPointInsideBBox(b, [ax2, ay2]) || isPointInsideBBox(a, [bx1, by1]) || isPointInsideBBox(a, [bx2, by1]) || isPointInsideBBox(a, [bx1, by2]) || isPointInsideBBox(a, [bx2, by2]) || (ax1 < bx2 && ax1 > bx1 || bx1 < ax2 && bx1 > ax1) && (ay1 < by2 && ay1 > by1 || by1 < ay2 && by1 > ay1);
|
|
3930
|
+
};
|
|
3931
|
+
const interHelper = (bez1, bez2, config) => {
|
|
3932
|
+
const bbox1 = getCubicBBox(...bez1);
|
|
3933
|
+
const bbox2 = getCubicBBox(...bez2);
|
|
3934
|
+
const { justCount, epsilon } = Object.assign({
|
|
3935
|
+
justCount: true,
|
|
3936
|
+
epsilon: DISTANCE_EPSILON
|
|
3937
|
+
}, config);
|
|
3938
|
+
if (!boundingBoxIntersect(bbox1, bbox2)) return justCount ? 0 : [];
|
|
3939
|
+
const l1 = getCubicLength(...bez1), l2 = getCubicLength(...bez2), n1 = Math.max(l1 / 5 >> 0, 1), n2 = Math.max(l2 / 5 >> 0, 1), points1 = [], points2 = [], xy = {};
|
|
3940
|
+
let res = justCount ? 0 : [];
|
|
3941
|
+
for (let i = 0; i < n1 + 1; i++) {
|
|
3942
|
+
const p = getPointAtCubicLength(...bez1, i / n1 * l1);
|
|
3943
|
+
points1.push({
|
|
3944
|
+
x: p.x,
|
|
3945
|
+
y: p.y,
|
|
3946
|
+
t: i / n1
|
|
3947
|
+
});
|
|
3948
|
+
}
|
|
3949
|
+
for (let i = 0; i < n2 + 1; i++) {
|
|
3950
|
+
const p = getPointAtCubicLength(...bez2, i / n2 * l2);
|
|
3951
|
+
points2.push({
|
|
3952
|
+
x: p.x,
|
|
3953
|
+
y: p.y,
|
|
3954
|
+
t: i / n2
|
|
3955
|
+
});
|
|
3956
|
+
}
|
|
3957
|
+
for (let i = 0; i < n1; i++) for (let j = 0; j < n2; j++) {
|
|
3958
|
+
const maxLimit = 1 + epsilon, di = points1[i], di1 = points1[i + 1], dj = points2[j], dj1 = points2[j + 1], ci = Math.abs(di1.x - di.x) < .001 ? "y" : "x", cj = Math.abs(dj1.x - dj.x) < .001 ? "y" : "x", is = intersect(di.x, di.y, di1.x, di1.y, dj.x, dj.y, dj1.x, dj1.y);
|
|
3959
|
+
if (is) {
|
|
3960
|
+
if (xy[is.x.toFixed(4)] == is.y.toFixed(4)) continue;
|
|
3961
|
+
xy[is.x.toFixed(4)] = is.y.toFixed(4);
|
|
3962
|
+
const t1 = di.t + Math.abs((is[ci] - di[ci]) / (di1[ci] - di[ci])) * (di1.t - di.t), t2 = dj.t + Math.abs((is[cj] - dj[cj]) / (dj1[cj] - dj[cj])) * (dj1.t - dj.t);
|
|
3963
|
+
if (t1 >= 0 && t1 <= maxLimit && t2 >= 0 && t2 <= maxLimit) if (justCount) res++;
|
|
3964
|
+
else res.push({
|
|
3965
|
+
x: is.x,
|
|
3966
|
+
y: is.y,
|
|
3967
|
+
t1: Math.min(t1, 1),
|
|
3968
|
+
t2: Math.min(t2, 1)
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
return res;
|
|
3973
|
+
};
|
|
3974
|
+
/**
|
|
3975
|
+
* Finds intersection points between two paths.
|
|
3976
|
+
*
|
|
3977
|
+
* @param pathInput1 - First path string or PathArray
|
|
3978
|
+
* @param pathInput2 - Second path string or PathArray
|
|
3979
|
+
* @param justCount - If true, returns the count of intersections; if false, returns the intersection points
|
|
3980
|
+
* @returns The number of intersections (when justCount is true) or an array of IntersectionPoint objects
|
|
3981
|
+
*
|
|
3982
|
+
* @example
|
|
3983
|
+
* ```ts
|
|
3984
|
+
* pathsIntersection('M0 50C0 0,100 0,100 50', 'M50 0C100 0,100 100,50 100', true)
|
|
3985
|
+
* // => 1
|
|
3986
|
+
* pathsIntersection('M0 50C0 0,100 0,100 50', 'M50 0C100 0,100 100,50 100', false)
|
|
3987
|
+
* // => [{ x: 50, y: 25, t1: 0.5, t2: 0.5 }]
|
|
3988
|
+
* ```
|
|
3989
|
+
*/
|
|
3990
|
+
const pathsIntersection = (pathInput1, pathInput2, justCount = true) => {
|
|
3991
|
+
const path1 = pathToCurve(pathInput1);
|
|
3992
|
+
const path2 = pathToCurve(pathInput2);
|
|
3993
|
+
let x1 = 0, y1 = 0, x2 = 0, y2 = 0, x1m = 0, y1m = 0, x2m = 0, y2m = 0, bez1 = [
|
|
3994
|
+
x1,
|
|
3995
|
+
y1,
|
|
3996
|
+
x1,
|
|
3997
|
+
y1,
|
|
3998
|
+
x1m,
|
|
3999
|
+
y1m,
|
|
4000
|
+
x1m,
|
|
4001
|
+
y1m
|
|
4002
|
+
], bez2 = [
|
|
4003
|
+
x2,
|
|
4004
|
+
y2,
|
|
4005
|
+
x2,
|
|
4006
|
+
y2,
|
|
4007
|
+
x2m,
|
|
4008
|
+
y2m,
|
|
4009
|
+
x2m,
|
|
4010
|
+
y2m
|
|
4011
|
+
], countResult = 0;
|
|
4012
|
+
const pointsResult = [];
|
|
4013
|
+
const pathLen1 = path1.length;
|
|
4014
|
+
const pathLen2 = path2.length;
|
|
4015
|
+
for (let i = 0; i < pathLen1; i++) {
|
|
4016
|
+
const seg1 = path1[i];
|
|
4017
|
+
if (seg1[0] == "M") {
|
|
4018
|
+
x1 = seg1[1];
|
|
4019
|
+
y1 = seg1[2];
|
|
4020
|
+
x1m = x1;
|
|
4021
|
+
y1m = y1;
|
|
4022
|
+
} else {
|
|
4023
|
+
if (seg1[0] == "C") {
|
|
4024
|
+
bez1 = [
|
|
4025
|
+
x1,
|
|
4026
|
+
y1,
|
|
4027
|
+
seg1[1],
|
|
4028
|
+
seg1[2],
|
|
4029
|
+
seg1[3],
|
|
4030
|
+
seg1[4],
|
|
4031
|
+
seg1[5],
|
|
4032
|
+
seg1[6]
|
|
4033
|
+
];
|
|
4034
|
+
x1 = bez1[6];
|
|
4035
|
+
y1 = bez1[7];
|
|
4036
|
+
} else {
|
|
4037
|
+
bez1 = [
|
|
4038
|
+
x1,
|
|
4039
|
+
y1,
|
|
4040
|
+
x1,
|
|
4041
|
+
y1,
|
|
4042
|
+
x1m,
|
|
4043
|
+
y1m,
|
|
4044
|
+
x1m,
|
|
4045
|
+
y1m
|
|
4046
|
+
];
|
|
4047
|
+
x1 = x1m;
|
|
4048
|
+
y1 = y1m;
|
|
4049
|
+
}
|
|
4050
|
+
for (let j = 0; j < pathLen2; j++) {
|
|
4051
|
+
const seg2 = path2[j];
|
|
4052
|
+
if (seg2[0] == "M") {
|
|
4053
|
+
x2 = seg2[1];
|
|
4054
|
+
y2 = seg2[2];
|
|
4055
|
+
x2m = x2;
|
|
4056
|
+
y2m = y2;
|
|
4057
|
+
} else if (seg2[0] == "C") {
|
|
4058
|
+
bez2 = [
|
|
4059
|
+
x2,
|
|
4060
|
+
y2,
|
|
4061
|
+
seg2[1],
|
|
4062
|
+
seg2[2],
|
|
4063
|
+
seg2[3],
|
|
4064
|
+
seg2[4],
|
|
4065
|
+
seg2[5],
|
|
4066
|
+
seg2[6]
|
|
4067
|
+
];
|
|
4068
|
+
x2 = bez2[6];
|
|
4069
|
+
y2 = bez2[7];
|
|
4070
|
+
}
|
|
4071
|
+
const intr = interHelper(bez1, bez2, { justCount });
|
|
4072
|
+
if (justCount) countResult += intr;
|
|
4073
|
+
else pointsResult.push(...intr);
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
}
|
|
4077
|
+
return justCount ? countResult : pointsResult;
|
|
4078
|
+
};
|
|
4079
|
+
//#endregion
|
|
4080
|
+
//#region src/morph/createPlaceholder.ts
|
|
4081
|
+
/**
|
|
4082
|
+
* Create a degenerate `PathArray` at a given coordinate
|
|
4083
|
+
* to serve as a pair for another `PathArray`.
|
|
4084
|
+
* @param param0 An [x, y] tuple for the coordinate
|
|
4085
|
+
* @returns A new degenerate `PathArray`
|
|
4086
|
+
*/
|
|
4087
|
+
const createPlaceholder = ([atx, aty]) => {
|
|
4088
|
+
const r = .001;
|
|
4089
|
+
return [
|
|
4090
|
+
[
|
|
4091
|
+
"M",
|
|
4092
|
+
atx,
|
|
4093
|
+
aty
|
|
4094
|
+
],
|
|
4095
|
+
[
|
|
4096
|
+
"L",
|
|
4097
|
+
atx + r,
|
|
4098
|
+
aty
|
|
4099
|
+
],
|
|
4100
|
+
[
|
|
4101
|
+
"L",
|
|
4102
|
+
atx + r,
|
|
4103
|
+
aty + r
|
|
4104
|
+
],
|
|
4105
|
+
[
|
|
4106
|
+
"L",
|
|
4107
|
+
atx,
|
|
4108
|
+
aty + r
|
|
4109
|
+
],
|
|
4110
|
+
[
|
|
4111
|
+
"L",
|
|
4112
|
+
atx,
|
|
4113
|
+
aty
|
|
4114
|
+
],
|
|
4115
|
+
["Z"]
|
|
4116
|
+
];
|
|
4117
|
+
};
|
|
4118
|
+
//#endregion
|
|
4119
|
+
//#region src/morph/matchPaths.ts
|
|
4120
|
+
function getBestMatch(target, candidates) {
|
|
4121
|
+
const targetBBox = target.bbox;
|
|
4122
|
+
const potentialCandidates = [];
|
|
4123
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
4124
|
+
const { bbox, size } = candidates[i];
|
|
4125
|
+
const dx = targetBBox.cx - bbox.cx;
|
|
4126
|
+
const dy = targetBBox.cy - bbox.cy;
|
|
4127
|
+
const centeredDistance = Math.sqrt(dx * dx + dy * dy);
|
|
4128
|
+
const sizeDifference = Math.abs(target.size - size) / Math.max(target.size, size, 1e-6);
|
|
4129
|
+
const hasOverlap = isPointInsideBBox([
|
|
4130
|
+
targetBBox.x,
|
|
4131
|
+
targetBBox.y,
|
|
4132
|
+
targetBBox.x2,
|
|
4133
|
+
targetBBox.y2
|
|
4134
|
+
], [bbox.cx, bbox.cy]) || isPointInsideBBox([
|
|
4135
|
+
bbox.x,
|
|
4136
|
+
bbox.y,
|
|
4137
|
+
bbox.x2,
|
|
4138
|
+
bbox.y2
|
|
4139
|
+
], [targetBBox.cx, targetBBox.cy]);
|
|
4140
|
+
const boxIntersect = boundingBoxIntersect([
|
|
4141
|
+
targetBBox.x,
|
|
4142
|
+
targetBBox.y,
|
|
4143
|
+
targetBBox.x2,
|
|
4144
|
+
targetBBox.y2
|
|
4145
|
+
], [
|
|
4146
|
+
bbox.x,
|
|
4147
|
+
bbox.y,
|
|
4148
|
+
bbox.x2,
|
|
4149
|
+
bbox.y2
|
|
4150
|
+
]);
|
|
4151
|
+
potentialCandidates.push({
|
|
4152
|
+
index: i,
|
|
4153
|
+
hasOverlap,
|
|
4154
|
+
boxIntersect,
|
|
4155
|
+
sizeDifference,
|
|
4156
|
+
centeredDistance
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
4159
|
+
const overlaping = potentialCandidates.filter((c) => c.hasOverlap && c.boxIntersect);
|
|
4160
|
+
if (overlaping.length > 0) {
|
|
4161
|
+
let best = overlaping[0];
|
|
4162
|
+
for (let i = 1; i < overlaping.length; i++) if (overlaping[i].centeredDistance < best.centeredDistance) best = overlaping[i];
|
|
4163
|
+
return candidates.splice(best.index, 1)[0];
|
|
4164
|
+
}
|
|
4165
|
+
return null;
|
|
4166
|
+
}
|
|
4167
|
+
/**
|
|
4168
|
+
* Matches paths from two sets by proximity and size similarity.
|
|
4169
|
+
* Unmatched paths receive placeholder paths at their centroid.
|
|
4170
|
+
*
|
|
4171
|
+
* @param fromPaths - Source path features to match from
|
|
4172
|
+
* @param toPaths - Target path features to match to
|
|
4173
|
+
* @returns Array of paired NormalArrays [from, to]
|
|
4174
|
+
*/
|
|
4175
|
+
function matchPaths(fromPaths, toPaths) {
|
|
4176
|
+
const pairs = [];
|
|
4177
|
+
fromPaths.sort((a, b) => b.size - a.size);
|
|
4178
|
+
toPaths.sort((a, b) => b.size - a.size);
|
|
4179
|
+
while (fromPaths.length > 0) {
|
|
4180
|
+
const from = fromPaths.shift();
|
|
4181
|
+
const bestTo = getBestMatch(from, toPaths);
|
|
4182
|
+
if (bestTo) pairs.push([from.path, bestTo.path]);
|
|
4183
|
+
else {
|
|
4184
|
+
const fromCentroid = [from.bbox.cx, from.bbox.cy];
|
|
4185
|
+
pairs.push([from.path, createPlaceholder(fromCentroid)]);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
while (toPaths.length > 0) {
|
|
4189
|
+
const to = toPaths.shift();
|
|
4190
|
+
const toCentroid = [to.bbox.cx, to.bbox.cy];
|
|
4191
|
+
pairs.push([createPlaceholder(toCentroid), to.path]);
|
|
4192
|
+
}
|
|
4193
|
+
return pairs;
|
|
4194
|
+
}
|
|
4195
|
+
//#endregion
|
|
4196
|
+
//#region src/morph/classifyPaths.ts
|
|
4197
|
+
/**
|
|
4198
|
+
* Classifies paths into outer (containing) and inner (hole) paths.
|
|
4199
|
+
*
|
|
4200
|
+
* @param paths - Array of normalized path arrays to classify
|
|
4201
|
+
* @returns Object with `outers` (containing shapes) and `inners` (holes)
|
|
4202
|
+
*/
|
|
4203
|
+
const classifyPaths = (paths) => {
|
|
4204
|
+
const outers = [];
|
|
4205
|
+
const inners = [];
|
|
4206
|
+
for (const path of paths) {
|
|
4207
|
+
const signedArea = polygonArea(samplePolygon(path));
|
|
4208
|
+
const bbox = getPathBBox(path);
|
|
4209
|
+
const feature = {
|
|
4210
|
+
isPoly: isPolygonArray(path) || isPolylineArray(path),
|
|
4211
|
+
size: bbox.width * bbox.height,
|
|
4212
|
+
path,
|
|
4213
|
+
signedArea,
|
|
4214
|
+
area: Math.abs(signedArea),
|
|
4215
|
+
bbox
|
|
4216
|
+
};
|
|
4217
|
+
if (signedArea > 0) outers.push(feature);
|
|
4218
|
+
else inners.push(feature);
|
|
4219
|
+
}
|
|
4220
|
+
return {
|
|
4221
|
+
outers,
|
|
4222
|
+
inners
|
|
4223
|
+
};
|
|
4224
|
+
};
|
|
4225
|
+
//#endregion
|
|
4226
|
+
//#region src/morph/equalizePaths.ts
|
|
4227
|
+
const equalizePathsDefaults = {
|
|
4228
|
+
mode: "auto",
|
|
4229
|
+
roundValues: 4,
|
|
4230
|
+
close: false,
|
|
4231
|
+
sampleSize: 10
|
|
4232
|
+
};
|
|
4233
|
+
/**
|
|
4234
|
+
* Equalizes two paths for morphing (single/multi subpath).
|
|
4235
|
+
*
|
|
4236
|
+
* @see https://minus-ze.ro/posts/morphing-arbitrary-paths-in-svg/
|
|
4237
|
+
* @param pathInput1 - First path string or PathArray
|
|
4238
|
+
* @param pathInput2 - Second path string or PathArray
|
|
4239
|
+
* @param initialCfg - Configuration options for equalization
|
|
4240
|
+
* @returns Tuple of two equalized MorphPathArrays
|
|
4241
|
+
*
|
|
4242
|
+
* @example
|
|
4243
|
+
* ```ts
|
|
4244
|
+
* const [eq1, eq2] = equalizePaths('M0 0L100 0L50 100Z', 'M0 0L100 0L100 100L0 100Z')
|
|
4245
|
+
* // eq1.length === eq2.length — ready for morphing
|
|
4246
|
+
* ```
|
|
4247
|
+
*/
|
|
4248
|
+
const equalizePaths = (pathInput1, pathInput2, initialCfg = {}) => {
|
|
4249
|
+
const cfg = Object.assign(equalizePathsDefaults, initialCfg);
|
|
4250
|
+
const p1 = normalizePath(pathInput1);
|
|
4251
|
+
const p2 = normalizePath(pathInput2);
|
|
4252
|
+
const multi1 = isMultiPath(p1);
|
|
4253
|
+
const multi2 = isMultiPath(p2);
|
|
4254
|
+
if (!multi1 && !multi2) return equalizeSegments(p1, p2, cfg);
|
|
4255
|
+
const globalArea1 = polygonArea(samplePolygon(p1));
|
|
4256
|
+
const globalArea2 = polygonArea(samplePolygon(p2));
|
|
4257
|
+
let path1 = p1;
|
|
4258
|
+
let path2 = p2;
|
|
4259
|
+
if (Math.sign(globalArea1) < 0) path1 = reversePath(path1);
|
|
4260
|
+
if (Math.sign(globalArea2) < 0) path2 = reversePath(path2);
|
|
4261
|
+
const multiPath1 = splitPath(path1);
|
|
4262
|
+
const multiPath2 = splitPath(path2);
|
|
4263
|
+
const { outers: outers1, inners: inners1 } = classifyPaths(multiPath1);
|
|
4264
|
+
const { outers: outers2, inners: inners2 } = classifyPaths(multiPath2);
|
|
4265
|
+
const outerPairs = matchPaths(outers1, outers2);
|
|
4266
|
+
const innerPairs = matchPaths(inners1, inners2);
|
|
4267
|
+
const equalizedPairs = [];
|
|
4268
|
+
for (const [from, to] of [...outerPairs, ...innerPairs]) {
|
|
4269
|
+
const [eqFrom, eqTo] = equalizeSegments(from, to, {
|
|
4270
|
+
...cfg,
|
|
4271
|
+
reverse: false
|
|
4272
|
+
});
|
|
4273
|
+
equalizedPairs.push([eqFrom, eqTo]);
|
|
4274
|
+
}
|
|
4275
|
+
return [equalizedPairs.map((p) => p[0]).flat(), equalizedPairs.map((p) => p[1]).flat()];
|
|
4276
|
+
};
|
|
4277
|
+
//#endregion
|
|
4278
|
+
//#region src/main.ts
|
|
4279
|
+
/**
|
|
4280
|
+
* Creates a new SVGPathCommander instance with the following properties:
|
|
4281
|
+
* * segments: `pathArray`
|
|
4282
|
+
* * round: number
|
|
4283
|
+
* * origin: [number, number, number?]
|
|
4284
|
+
*
|
|
4285
|
+
* @class
|
|
4286
|
+
* @author thednp <https://github.com/thednp/svg-path-commander>
|
|
4287
|
+
* @returns a new SVGPathCommander instance
|
|
4288
|
+
*/
|
|
4289
|
+
var SVGPathCommander = class {
|
|
4290
|
+
/**
|
|
4291
|
+
* @constructor
|
|
4292
|
+
* @param pathValue the path string
|
|
4293
|
+
* @param config instance options
|
|
4294
|
+
*/
|
|
4295
|
+
constructor(pathValue, config) {
|
|
4296
|
+
const instanceOptions = config || {};
|
|
4297
|
+
const undefPath = typeof pathValue === "undefined";
|
|
4298
|
+
if (undefPath || !pathValue.length) throw TypeError(`${error}: "pathValue" is ${undefPath ? "undefined" : "empty"}`);
|
|
4299
|
+
this.segments = parsePathString(pathValue);
|
|
4300
|
+
const { round: roundOption, origin: originOption } = instanceOptions;
|
|
4301
|
+
let round;
|
|
4302
|
+
if (Number.isInteger(roundOption) || roundOption === "off") round = roundOption;
|
|
4303
|
+
else round = defaultOptions.round;
|
|
4304
|
+
let origin = defaultOptions.origin;
|
|
4305
|
+
if (Array.isArray(originOption) && originOption.length >= 2) {
|
|
4306
|
+
const [originX, originY, originZ] = originOption.map(Number);
|
|
4307
|
+
origin = [
|
|
4308
|
+
!Number.isNaN(originX) ? originX : 0,
|
|
4309
|
+
!Number.isNaN(originY) ? originY : 0,
|
|
4310
|
+
!Number.isNaN(originZ) ? originZ : 0
|
|
4311
|
+
];
|
|
4312
|
+
}
|
|
4313
|
+
this.round = round;
|
|
4314
|
+
this.origin = origin;
|
|
4315
|
+
return this;
|
|
4316
|
+
}
|
|
4317
|
+
get bbox() {
|
|
4318
|
+
return getPathBBox(this.segments);
|
|
4319
|
+
}
|
|
4320
|
+
get length() {
|
|
4321
|
+
return getTotalLength(this.segments);
|
|
4322
|
+
}
|
|
4323
|
+
/**
|
|
4324
|
+
* Returns the path bounding box, equivalent to native `path.getBBox()`.
|
|
4325
|
+
*
|
|
4326
|
+
* @public
|
|
4327
|
+
* @returns the pathBBox
|
|
4328
|
+
*/
|
|
4329
|
+
getBBox() {
|
|
4330
|
+
return this.bbox;
|
|
4331
|
+
}
|
|
4332
|
+
/**
|
|
4333
|
+
* Returns the total path length, equivalent to native `path.getTotalLength()`.
|
|
4334
|
+
*
|
|
4335
|
+
* @public
|
|
4336
|
+
* @returns the path total length
|
|
4337
|
+
*/
|
|
4338
|
+
getTotalLength() {
|
|
4339
|
+
return this.length;
|
|
4340
|
+
}
|
|
4341
|
+
/**
|
|
4342
|
+
* Returns an `{x,y}` point in the path stroke at a given length,
|
|
4343
|
+
* equivalent to the native `path.getPointAtLength()`.
|
|
4344
|
+
*
|
|
4345
|
+
* @public
|
|
4346
|
+
* @param length the length
|
|
4347
|
+
* @returns the requested point
|
|
4348
|
+
*/
|
|
4349
|
+
getPointAtLength(length) {
|
|
4350
|
+
return getPointAtLength(this.segments, length);
|
|
4351
|
+
}
|
|
4352
|
+
/**
|
|
4353
|
+
* Convert path to absolute values.
|
|
4354
|
+
*
|
|
4355
|
+
* @example
|
|
4356
|
+
* ```ts
|
|
4357
|
+
* new SVGPathCommander('M10 10l80 80').toAbsolute().toString()
|
|
4358
|
+
* // => 'M10 10L90 90'
|
|
4359
|
+
* ```
|
|
4360
|
+
*
|
|
4361
|
+
* @returns this for chaining
|
|
4362
|
+
* @public
|
|
4363
|
+
*/
|
|
4364
|
+
toAbsolute() {
|
|
4365
|
+
const { segments } = this;
|
|
4366
|
+
this.segments = pathToAbsolute(segments);
|
|
4367
|
+
return this;
|
|
4368
|
+
}
|
|
4369
|
+
/**
|
|
4370
|
+
* Convert path to relative values.
|
|
4371
|
+
*
|
|
4372
|
+
* @example
|
|
4373
|
+
* ```ts
|
|
4374
|
+
* new SVGPathCommander('M10 10L90 90').toRelative().toString()
|
|
4375
|
+
* // => 'M10 10l80 80'
|
|
4376
|
+
* ```
|
|
4377
|
+
*
|
|
4378
|
+
* @returns this for chaining
|
|
4379
|
+
* @public
|
|
4380
|
+
*/
|
|
4381
|
+
toRelative() {
|
|
4382
|
+
const { segments } = this;
|
|
4383
|
+
this.segments = pathToRelative(segments);
|
|
4384
|
+
return this;
|
|
4385
|
+
}
|
|
4386
|
+
/**
|
|
4387
|
+
* Convert path to cubic-bezier values. In addition, un-necessary `Z`
|
|
4388
|
+
* segment is removed if previous segment extends to the `M` segment.
|
|
4389
|
+
*
|
|
4390
|
+
* @example
|
|
4391
|
+
* ```ts
|
|
4392
|
+
* new SVGPathCommander('M10 50q15 -25 30 0').toCurve().toString()
|
|
4393
|
+
* // => 'M10 50C25 25 40 50 40 50'
|
|
4394
|
+
* ```
|
|
4395
|
+
*
|
|
4396
|
+
* @returns this for chaining
|
|
4397
|
+
* @public
|
|
4398
|
+
*/
|
|
4399
|
+
toCurve() {
|
|
4400
|
+
const { segments } = this;
|
|
4401
|
+
this.segments = pathToCurve(segments);
|
|
4402
|
+
return this;
|
|
4403
|
+
}
|
|
4404
|
+
/**
|
|
4405
|
+
* Reverse the order of the segments and their values.
|
|
4406
|
+
*
|
|
4407
|
+
* @example
|
|
4408
|
+
* ```ts
|
|
4409
|
+
* new SVGPathCommander('M0 0L100 0L100 100L0 100Z').reverse().toString()
|
|
4410
|
+
* // => 'M0 100L0 0L100 0L100 100Z'
|
|
4411
|
+
* ```
|
|
4412
|
+
*
|
|
4413
|
+
* @param onlySubpath - option to reverse all sub-paths except first
|
|
4414
|
+
* @returns this for chaining
|
|
4415
|
+
* @public
|
|
4416
|
+
*/
|
|
4417
|
+
reverse(onlySubpath) {
|
|
4418
|
+
const { segments } = this;
|
|
4419
|
+
const split = splitPath(segments);
|
|
4420
|
+
const subPath = split.length > 1 ? split : false;
|
|
4421
|
+
const absoluteMultiPath = subPath ? subPath.map((x, i) => {
|
|
4422
|
+
if (onlySubpath) return i ? reversePath(x) : x.slice(0);
|
|
4423
|
+
return reversePath(x);
|
|
4424
|
+
}) : segments.slice(0);
|
|
4425
|
+
let path = [];
|
|
4426
|
+
if (subPath) path = absoluteMultiPath.flat(1);
|
|
4427
|
+
else path = onlySubpath ? segments : reversePath(segments);
|
|
4428
|
+
this.segments = path.slice(0);
|
|
4429
|
+
return this;
|
|
4430
|
+
}
|
|
4431
|
+
/**
|
|
4432
|
+
* Normalize path in 2 steps:
|
|
4433
|
+
* * convert `pathArray`(s) to absolute values
|
|
4434
|
+
* * convert shorthand notation to standard notation
|
|
4435
|
+
*
|
|
4436
|
+
* @example
|
|
4437
|
+
* ```ts
|
|
4438
|
+
* new SVGPathCommander('M10 90s20 -80 40 -80s20 80 40 80').normalize().toString()
|
|
4439
|
+
* // => 'M10 90C30 90 25 10 50 10C75 10 70 90 90 90'
|
|
4440
|
+
* ```
|
|
4441
|
+
*
|
|
4442
|
+
* @returns this for chaining
|
|
4443
|
+
* @public
|
|
4444
|
+
*/
|
|
4445
|
+
normalize() {
|
|
4446
|
+
const { segments } = this;
|
|
4447
|
+
this.segments = normalizePath(segments);
|
|
4448
|
+
return this;
|
|
4449
|
+
}
|
|
4450
|
+
/**
|
|
4451
|
+
* Optimize `pathArray` values:
|
|
4452
|
+
* * convert segments to absolute and/or relative values
|
|
4453
|
+
* * select segments with shortest resulted string
|
|
4454
|
+
* * round values to the specified `decimals` option value
|
|
4455
|
+
*
|
|
4456
|
+
* @example
|
|
4457
|
+
* ```ts
|
|
4458
|
+
* new SVGPathCommander('M10 10L10 10L90 90').optimize().toString()
|
|
4459
|
+
* // => 'M10 10l0 0 80 80'
|
|
4460
|
+
* ```
|
|
4461
|
+
*
|
|
4462
|
+
* @returns this for chaining
|
|
4463
|
+
* @public
|
|
4464
|
+
*/
|
|
4465
|
+
optimize() {
|
|
4466
|
+
const { segments } = this;
|
|
4467
|
+
this.segments = optimizePath(segments, this.round === "off" ? 2 : this.round);
|
|
4468
|
+
return this;
|
|
4469
|
+
}
|
|
4470
|
+
/**
|
|
4471
|
+
* Transform path using values from an `Object` defined as `transformObject`.
|
|
4472
|
+
*
|
|
4473
|
+
* @see TransformObject for a quick reference
|
|
4474
|
+
*
|
|
4475
|
+
* @param source a `transformObject` as described above
|
|
4476
|
+
* @returns this for chaining
|
|
4477
|
+
* @public
|
|
4478
|
+
*/
|
|
4479
|
+
transform(source) {
|
|
4480
|
+
if (!source || typeof source !== "object" || typeof source === "object" && ![
|
|
4481
|
+
"translate",
|
|
4482
|
+
"rotate",
|
|
4483
|
+
"skew",
|
|
4484
|
+
"scale"
|
|
4485
|
+
].some((x) => x in source)) return this;
|
|
4486
|
+
const { segments, origin: [cx, cy, cz] } = this;
|
|
4487
|
+
const transform = {};
|
|
4488
|
+
for (const [k, v] of Object.entries(source)) if (k === "skew" && Array.isArray(v)) transform[k] = v.map(Number);
|
|
4489
|
+
else if ((k === "rotate" || k === "translate" || k === "origin" || k === "scale") && Array.isArray(v)) transform[k] = v.map(Number);
|
|
4490
|
+
else if (k !== "origin" && typeof Number(v) === "number") transform[k] = Number(v);
|
|
4491
|
+
const { origin } = transform;
|
|
4492
|
+
if (Array.isArray(origin) && origin.length >= 2) {
|
|
4493
|
+
const [originX, originY, originZ] = origin.map(Number);
|
|
4494
|
+
transform.origin = [
|
|
4495
|
+
!Number.isNaN(originX) ? originX : cx,
|
|
4496
|
+
!Number.isNaN(originY) ? originY : cy,
|
|
4497
|
+
originZ || cz
|
|
4498
|
+
];
|
|
4499
|
+
} else transform.origin = [
|
|
4500
|
+
cx,
|
|
4501
|
+
cy,
|
|
4502
|
+
cz
|
|
4503
|
+
];
|
|
4504
|
+
this.segments = transformPath(segments, transform);
|
|
4505
|
+
return this;
|
|
4506
|
+
}
|
|
4507
|
+
/**
|
|
4508
|
+
* Rotate path 180deg vertically.
|
|
4509
|
+
*
|
|
4510
|
+
* @example
|
|
4511
|
+
* ```ts
|
|
4512
|
+
* const path = new SVGPathCommander('M0 0L100 0L100 100L0 100Z')
|
|
4513
|
+
* path.flipX().toString()
|
|
4514
|
+
* ```
|
|
4515
|
+
*
|
|
4516
|
+
* @returns this for chaining
|
|
4517
|
+
* @public
|
|
4518
|
+
*/
|
|
4519
|
+
flipX() {
|
|
4520
|
+
const { cx, cy } = this.bbox;
|
|
4521
|
+
this.transform({
|
|
4522
|
+
rotate: [
|
|
4523
|
+
0,
|
|
4524
|
+
180,
|
|
4525
|
+
0
|
|
4526
|
+
],
|
|
4527
|
+
origin: [
|
|
4528
|
+
cx,
|
|
4529
|
+
cy,
|
|
4530
|
+
0
|
|
4531
|
+
]
|
|
4532
|
+
});
|
|
4533
|
+
return this;
|
|
4534
|
+
}
|
|
4535
|
+
/**
|
|
4536
|
+
* Rotate path 180deg horizontally.
|
|
4537
|
+
*
|
|
4538
|
+
* @example
|
|
4539
|
+
* ```ts
|
|
4540
|
+
* const path = new SVGPathCommander('M0 0L100 0L100 100L0 100Z')
|
|
4541
|
+
* path.flipY().toString()
|
|
4542
|
+
* ```
|
|
4543
|
+
*
|
|
4544
|
+
* @returns this for chaining
|
|
4545
|
+
* @public
|
|
4546
|
+
*/
|
|
4547
|
+
flipY() {
|
|
4548
|
+
const { cx, cy } = this.bbox;
|
|
4549
|
+
this.transform({
|
|
4550
|
+
rotate: [
|
|
4551
|
+
180,
|
|
4552
|
+
0,
|
|
4553
|
+
0
|
|
4554
|
+
],
|
|
4555
|
+
origin: [
|
|
4556
|
+
cx,
|
|
4557
|
+
cy,
|
|
4558
|
+
0
|
|
4559
|
+
]
|
|
4560
|
+
});
|
|
4561
|
+
return this;
|
|
4562
|
+
}
|
|
4563
|
+
/**
|
|
4564
|
+
* Export the current path to be used
|
|
4565
|
+
* for the `d` (description) attribute.
|
|
4566
|
+
*
|
|
4567
|
+
* @public
|
|
4568
|
+
* @returns the path string
|
|
4569
|
+
*/
|
|
4570
|
+
toString() {
|
|
4571
|
+
return pathToString(this.segments, this.round);
|
|
4572
|
+
}
|
|
4573
|
+
/**
|
|
4574
|
+
* Remove the instance.
|
|
4575
|
+
*
|
|
4576
|
+
* @public
|
|
4577
|
+
* @returns void
|
|
4578
|
+
*/
|
|
4579
|
+
dispose() {
|
|
4580
|
+
Object.keys(this).forEach((key) => delete this[key]);
|
|
4581
|
+
}
|
|
4582
|
+
static options = defaultOptions;
|
|
4583
|
+
static CSSMatrix = CSSMatrix;
|
|
4584
|
+
static arcTools = arcTools;
|
|
4585
|
+
static bezierTools = bezierTools;
|
|
4586
|
+
static cubicTools = cubicTools;
|
|
4587
|
+
static lineTools = lineTools;
|
|
4588
|
+
static polygonTools = polygonTools;
|
|
4589
|
+
static quadTools = quadTools;
|
|
4590
|
+
static pathToAbsolute = pathToAbsolute;
|
|
4591
|
+
static pathToRelative = pathToRelative;
|
|
4592
|
+
static pathToCurve = pathToCurve;
|
|
4593
|
+
static pathToString = pathToString;
|
|
4594
|
+
static distanceSquareRoot = distanceSquareRoot;
|
|
4595
|
+
static midPoint = midPoint;
|
|
4596
|
+
static rotateVector = rotateVector;
|
|
4597
|
+
static roundTo = roundTo;
|
|
4598
|
+
static parsePathString = parsePathString;
|
|
4599
|
+
static finalizeSegment = finalizeSegment;
|
|
4600
|
+
static invalidPathValue = invalidPathValue;
|
|
4601
|
+
static isArcCommand = isArcCommand;
|
|
4602
|
+
static isDigit = isDigit;
|
|
4603
|
+
static isDigitStart = isDigitStart;
|
|
4604
|
+
static isMoveCommand = isMoveCommand;
|
|
4605
|
+
static isPathCommand = isPathCommand;
|
|
4606
|
+
static isSpace = isSpace;
|
|
4607
|
+
static paramsCount = paramsCounts;
|
|
4608
|
+
static paramsParser = paramsParser;
|
|
4609
|
+
static PathParser = PathParser;
|
|
4610
|
+
static scanFlag = scanFlag;
|
|
4611
|
+
static scanParam = scanParam;
|
|
4612
|
+
static scanSegment = scanSegment;
|
|
4613
|
+
static skipSpaces = skipSpaces;
|
|
4614
|
+
static distanceEpsilon = DISTANCE_EPSILON;
|
|
4615
|
+
static fixPath = fixPath;
|
|
4616
|
+
static getClosestPoint = getClosestPoint;
|
|
4617
|
+
static getDrawDirection = getDrawDirection;
|
|
4618
|
+
static getPathArea = getPathArea;
|
|
4619
|
+
static getPathBBox = getPathBBox;
|
|
4620
|
+
static getPointAtLength = getPointAtLength;
|
|
4621
|
+
static getPropertiesAtLength = getPropertiesAtLength;
|
|
4622
|
+
static getPropertiesAtPoint = getPropertiesAtPoint;
|
|
4623
|
+
static getSegmentAtLength = getSegmentAtLength;
|
|
4624
|
+
static getSegmentOfPoint = getSegmentOfPoint;
|
|
4625
|
+
static getTotalLength = getTotalLength;
|
|
4626
|
+
static isAbsoluteArray = isAbsoluteArray;
|
|
4627
|
+
static isCurveArray = isCurveArray;
|
|
4628
|
+
static isPolygonArray = isPolygonArray;
|
|
4629
|
+
static isNormalizedArray = isNormalizedArray;
|
|
4630
|
+
static isPathArray = isPathArray;
|
|
4631
|
+
static isPointInStroke = isPointInStroke;
|
|
4632
|
+
static isRelativeArray = isRelativeArray;
|
|
4633
|
+
static isValidPath = isValidPath;
|
|
4634
|
+
static samplePolygon = samplePolygon;
|
|
4635
|
+
static shapeParams = shapeParams;
|
|
4636
|
+
static shapeToPath = shapeToPath;
|
|
4637
|
+
static shapeToPathArray = shapeToPathArray;
|
|
4638
|
+
static absolutizeSegment = absolutizeSegment;
|
|
4639
|
+
static arcToCubic = arcToCubic;
|
|
4640
|
+
static getSVGMatrix = getSVGMatrix;
|
|
4641
|
+
static iterate = iterate;
|
|
4642
|
+
static lineToCubic = lineToCubic;
|
|
4643
|
+
static normalizePath = normalizePath;
|
|
4644
|
+
static normalizeSegment = normalizeSegment;
|
|
4645
|
+
static optimizePath = optimizePath;
|
|
4646
|
+
static projection2d = projection2d;
|
|
4647
|
+
static quadToCubic = quadToCubic;
|
|
4648
|
+
static relativizeSegment = relativizeSegment;
|
|
4649
|
+
static reverseCurve = reverseCurve;
|
|
4650
|
+
static reversePath = reversePath;
|
|
4651
|
+
static roundPath = roundPath;
|
|
4652
|
+
static roundSegment = roundSegment;
|
|
4653
|
+
static segmentToCubic = segmentToCubic;
|
|
4654
|
+
static shortenSegment = shortenSegment;
|
|
4655
|
+
static splitPath = splitPath;
|
|
4656
|
+
static equalizePaths = equalizePaths;
|
|
4657
|
+
static equalizeSegments = equalizeSegments;
|
|
4658
|
+
static splitCubicSegment = splitCubicSegment;
|
|
4659
|
+
static transformPath = transformPath;
|
|
4660
|
+
static isPointInsideBBox = isPointInsideBBox;
|
|
4661
|
+
static pathsIntersection = pathsIntersection;
|
|
4662
|
+
static boundingBoxIntersect = boundingBoxIntersect;
|
|
4663
|
+
static isMultiPath = isMultiPath;
|
|
4664
|
+
static isClosedPath = isClosedPath;
|
|
4665
|
+
static isPolylineArray = isPolylineArray;
|
|
4666
|
+
static version = version;
|
|
4667
|
+
};
|
|
4668
|
+
//#endregion
|
|
4669
|
+
//#region src/index.ts
|
|
4670
|
+
var src_default = SVGPathCommander;
|
|
4671
|
+
//#endregion
|
|
4672
|
+
export { src_default as default };
|
|
4673
|
+
|
|
4674
|
+
//# sourceMappingURL=index.js.map
|