svg-path-commander 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -5
- package/dist/svg-path-commander.cjs +1 -1
- package/dist/svg-path-commander.cjs.map +1 -1
- package/dist/svg-path-commander.d.ts +119 -33
- package/dist/svg-path-commander.js +1 -1
- package/dist/svg-path-commander.js.map +1 -1
- package/dist/svg-path-commander.mjs +798 -828
- package/dist/svg-path-commander.mjs.map +1 -1
- package/package.json +3 -3
- package/src/convert/pathToAbsolute.ts +16 -70
- package/src/convert/pathToCurve.ts +36 -28
- package/src/convert/pathToRelative.ts +33 -62
- package/src/index.ts +13 -19
- package/src/interface.ts +1 -1
- package/src/math/arcTools.ts +248 -71
- package/src/math/bezier.ts +19 -27
- package/src/math/cubicTools.ts +67 -26
- package/src/math/distanceSquareRoot.ts +3 -1
- package/src/math/lineTools.ts +41 -26
- package/src/math/midPoint.ts +3 -1
- package/src/math/polygonArea.ts +3 -1
- package/src/math/polygonLength.ts +2 -1
- package/src/math/quadTools.ts +45 -26
- package/src/parser/parsePathString.ts +4 -4
- package/src/process/absolutizeSegment.ts +58 -0
- package/src/process/iterate.ts +33 -0
- package/src/process/normalizePath.ts +34 -28
- package/src/process/normalizeSegment.ts +8 -9
- package/src/process/projection2d.ts +2 -1
- package/src/process/relativizeSegment.ts +61 -0
- package/src/process/reversePath.ts +1 -1
- package/src/process/roundPath.ts +8 -10
- package/src/process/segmentToCubic.ts +1 -1
- package/src/process/shortenSegment.ts +3 -3
- package/src/process/splitCubic.ts +8 -7
- package/src/process/splitPath.ts +38 -4
- package/src/process/transformPath.ts +80 -73
- package/src/types.ts +35 -1
- package/src/util/getPathArea.ts +3 -3
- package/src/util/getPathBBox.ts +86 -19
- package/src/util/getPointAtLength.ts +98 -4
- package/src/util/getPropertiesAtLength.ts +2 -2
- package/src/util/getTotalLength.ts +71 -4
- package/test/class.test.ts +15 -14
- package/test/fixtures/shapes.js +27 -27
- package/test/fixtures/simpleShapes.js +18 -18
- package/test/static.test.ts +37 -17
- package/cypress.config.ts +0 -29
- package/src/process/fixArc.ts +0 -23
- package/src/process/replaceArc.ts +0 -52
- package/src/util/pathFactory.ts +0 -130
package/src/math/arcTools.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getPointAtLineLength } from './lineTools';
|
|
2
2
|
import type { Point } from '../types';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -19,48 +19,26 @@ const ellipticalArcLength = (rx: number, ry: number, theta: number) => {
|
|
|
19
19
|
};
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @param
|
|
24
|
-
* @param
|
|
25
|
-
* @param
|
|
26
|
-
* @param
|
|
27
|
-
* @param
|
|
28
|
-
* @param
|
|
29
|
-
* @
|
|
30
|
-
* @see https://stackoverflow.com/questions/87734/how-do-you-calculate-the-axis-aligned-bounding-box-of-an-ellipse
|
|
22
|
+
* Compute point on ellipse from angle around ellipse (theta);
|
|
23
|
+
* @param theta the arc sweep angle
|
|
24
|
+
* @param cx the center X
|
|
25
|
+
* @param cy the center Y
|
|
26
|
+
* @param rx the radius X
|
|
27
|
+
* @param ry the radius Y
|
|
28
|
+
* @param alpha the angle
|
|
29
|
+
* @returns a point around ellipse
|
|
31
30
|
*/
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
endAngle: number,
|
|
40
|
-
) => {
|
|
41
|
-
const { cos, sin, min, max } = Math;
|
|
42
|
-
const cosRotation = cos(rotation);
|
|
43
|
-
const sinRotation = sin(rotation);
|
|
44
|
-
|
|
45
|
-
// Rotate parametric equations
|
|
46
|
-
const xRotated = (t: number) => {
|
|
47
|
-
return x + rx * cos(t) * cosRotation - ry * sin(t) * sinRotation;
|
|
48
|
-
};
|
|
49
|
-
const yRotated = (t: number) => {
|
|
50
|
-
return y + ry * sin(t) * cosRotation + rx * cos(t) * sinRotation;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Evaluate at start and end angles
|
|
54
|
-
const startX = xRotated(startAngle);
|
|
55
|
-
const startY = yRotated(startAngle);
|
|
56
|
-
const endX = xRotated(endAngle);
|
|
57
|
-
const endY = yRotated(endAngle);
|
|
31
|
+
const arc = (theta: number, cx: number, cy: number, rx: number, ry: number, alpha: number) => {
|
|
32
|
+
// theta is angle in radians around arc
|
|
33
|
+
// alpha is angle of rotation of ellipse in radians
|
|
34
|
+
const cos = Math.cos(alpha);
|
|
35
|
+
const sin = Math.sin(alpha);
|
|
36
|
+
const x = rx * Math.cos(theta);
|
|
37
|
+
const y = ry * Math.sin(theta);
|
|
58
38
|
|
|
59
|
-
// Find minimum and maximum x and y values
|
|
60
|
-
// Return AABB
|
|
61
39
|
return {
|
|
62
|
-
|
|
63
|
-
|
|
40
|
+
x: cx + cos * x - sin * y,
|
|
41
|
+
y: cy + sin * x + cos * y,
|
|
64
42
|
};
|
|
65
43
|
};
|
|
66
44
|
|
|
@@ -82,20 +60,21 @@ const angleBetween = (v0: Point, v1: Point) => {
|
|
|
82
60
|
};
|
|
83
61
|
|
|
84
62
|
/**
|
|
85
|
-
* Returns properties for an Arc segment
|
|
63
|
+
* Returns the following properties for an Arc segment: center, start angle
|
|
64
|
+
* and radiuses on X and Y coordinates.
|
|
86
65
|
*
|
|
87
66
|
* @param x1 the starting point X
|
|
88
67
|
* @param y1 the starting point Y
|
|
89
|
-
* @param
|
|
90
|
-
* @param
|
|
91
|
-
* @param
|
|
92
|
-
* @param
|
|
68
|
+
* @param RX the radius on X axis
|
|
69
|
+
* @param RY the radius on Y axis
|
|
70
|
+
* @param angle the ellipse rotation in degrees
|
|
71
|
+
* @param LAF the large arc flag
|
|
72
|
+
* @param SF the sweep flag
|
|
93
73
|
* @param x2 the ending point X
|
|
94
74
|
* @param y2 the ending point Y
|
|
95
|
-
* @
|
|
96
|
-
* @returns properties specific to Arc segmentas well as the segment length, point at length and the bounding box
|
|
75
|
+
* @returns properties specific to Arc segments
|
|
97
76
|
*/
|
|
98
|
-
const
|
|
77
|
+
const getArcProps = (
|
|
99
78
|
x1: number,
|
|
100
79
|
y1: number,
|
|
101
80
|
RX: number,
|
|
@@ -105,7 +84,6 @@ const getSegmentProperties = (
|
|
|
105
84
|
SF: number,
|
|
106
85
|
x: number,
|
|
107
86
|
y: number,
|
|
108
|
-
distance?: number,
|
|
109
87
|
) => {
|
|
110
88
|
const { abs, sin, cos, sqrt, PI } = Math;
|
|
111
89
|
let rx = abs(RX);
|
|
@@ -115,14 +93,22 @@ const getSegmentProperties = (
|
|
|
115
93
|
|
|
116
94
|
if (x1 === x && y1 === y) {
|
|
117
95
|
return {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
96
|
+
rx,
|
|
97
|
+
ry,
|
|
98
|
+
startAngle: 0,
|
|
99
|
+
endAngle: 0,
|
|
100
|
+
center: { x, y },
|
|
121
101
|
};
|
|
122
102
|
}
|
|
123
103
|
|
|
124
104
|
if (rx === 0 || ry === 0) {
|
|
125
|
-
return
|
|
105
|
+
return {
|
|
106
|
+
rx,
|
|
107
|
+
ry,
|
|
108
|
+
startAngle: 0,
|
|
109
|
+
endAngle: 0,
|
|
110
|
+
center: { x, y },
|
|
111
|
+
};
|
|
126
112
|
}
|
|
127
113
|
|
|
128
114
|
const dx = (x1 - x) / 2;
|
|
@@ -144,6 +130,7 @@ const getSegmentProperties = (
|
|
|
144
130
|
const cSquareRootDenom = rx ** 2 * transformedPoint.y ** 2 + ry ** 2 * transformedPoint.x ** 2;
|
|
145
131
|
|
|
146
132
|
let cRadicand = cSquareNumerator / cSquareRootDenom;
|
|
133
|
+
/* istanbul ignore next @preserve */
|
|
147
134
|
cRadicand = cRadicand < 0 ? 0 : cRadicand;
|
|
148
135
|
const cCoef = (LAF !== SF ? 1 : -1) * sqrt(cRadicand);
|
|
149
136
|
const transformedCenter = {
|
|
@@ -176,15 +163,7 @@ const getSegmentProperties = (
|
|
|
176
163
|
}
|
|
177
164
|
sweepAngle %= 2 * PI;
|
|
178
165
|
|
|
179
|
-
const alpha = startAngle + sweepAngle * (distance || 0);
|
|
180
166
|
const endAngle = startAngle + sweepAngle;
|
|
181
|
-
const ellipseComponentX = rx * cos(alpha);
|
|
182
|
-
const ellipseComponentY = ry * sin(alpha);
|
|
183
|
-
|
|
184
|
-
const point = {
|
|
185
|
-
x: cos(xRotRad) * ellipseComponentX - sin(xRotRad) * ellipseComponentY + center.x,
|
|
186
|
-
y: sin(xRotRad) * ellipseComponentX + cos(xRotRad) * ellipseComponentY + center.y,
|
|
187
|
-
};
|
|
188
167
|
|
|
189
168
|
// to be used later
|
|
190
169
|
// point.ellipticalArcStartAngle = startAngle;
|
|
@@ -198,20 +177,218 @@ const getSegmentProperties = (
|
|
|
198
177
|
// point.box = minmax(center.x, center.y, rx, ry, xRotRad, startAngle, startAngle + sweepAngle);
|
|
199
178
|
|
|
200
179
|
return {
|
|
201
|
-
point,
|
|
202
180
|
center,
|
|
203
|
-
angle: alpha,
|
|
204
181
|
startAngle,
|
|
205
182
|
endAngle,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
183
|
+
rx,
|
|
184
|
+
ry,
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Returns the length of an Arc segment.
|
|
190
|
+
*
|
|
191
|
+
* @param x1 the starting point X
|
|
192
|
+
* @param y1 the starting point Y
|
|
193
|
+
* @param c1x the first control point X
|
|
194
|
+
* @param c1y the first control point Y
|
|
195
|
+
* @param c2x the second control point X
|
|
196
|
+
* @param c2y the second control point Y
|
|
197
|
+
* @param x2 the ending point X
|
|
198
|
+
* @param y2 the ending point Y
|
|
199
|
+
* @returns the length of the Arc segment
|
|
200
|
+
*/
|
|
201
|
+
export const getArcLength = (
|
|
202
|
+
x1: number,
|
|
203
|
+
y1: number,
|
|
204
|
+
RX: number,
|
|
205
|
+
RY: number,
|
|
206
|
+
angle: number,
|
|
207
|
+
LAF: number,
|
|
208
|
+
SF: number,
|
|
209
|
+
x: number,
|
|
210
|
+
y: number,
|
|
211
|
+
) => {
|
|
212
|
+
const { rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
|
|
213
|
+
return ellipticalArcLength(rx, ry, endAngle - startAngle);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Returns a point along an Arc segment at a given distance.
|
|
218
|
+
*
|
|
219
|
+
* @param x1 the starting point X
|
|
220
|
+
* @param y1 the starting point Y
|
|
221
|
+
* @param RX the radius on X axis
|
|
222
|
+
* @param RY the radius on Y axis
|
|
223
|
+
* @param angle the ellipse rotation in degrees
|
|
224
|
+
* @param LAF the large arc flag
|
|
225
|
+
* @param SF the sweep flag
|
|
226
|
+
* @param x2 the ending point X
|
|
227
|
+
* @param y2 the ending point Y
|
|
228
|
+
* @param distance a [0-1] ratio
|
|
229
|
+
* @returns a point along the Arc segment
|
|
230
|
+
*/
|
|
231
|
+
export const getPointAtArcLength = (
|
|
232
|
+
x1: number,
|
|
233
|
+
y1: number,
|
|
234
|
+
RX: number,
|
|
235
|
+
RY: number,
|
|
236
|
+
angle: number,
|
|
237
|
+
LAF: number,
|
|
238
|
+
SF: number,
|
|
239
|
+
x: number,
|
|
240
|
+
y: number,
|
|
241
|
+
distance?: number,
|
|
242
|
+
) => {
|
|
243
|
+
let point = { x: x1, y: y1 };
|
|
244
|
+
const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
|
|
245
|
+
const length = ellipticalArcLength(rx, ry, endAngle - startAngle);
|
|
246
|
+
|
|
247
|
+
/* istanbul ignore else @preserve */
|
|
248
|
+
if (typeof distance === 'number') {
|
|
249
|
+
if (distance <= 0) {
|
|
250
|
+
point = { x: x1, y: y1 };
|
|
251
|
+
} else if (distance >= length) {
|
|
252
|
+
point = { x, y };
|
|
253
|
+
} else {
|
|
254
|
+
/* istanbul ignore next @preserve */
|
|
255
|
+
if (x1 === x && y1 === y) {
|
|
256
|
+
return { x, y };
|
|
257
|
+
}
|
|
258
|
+
/* istanbul ignore next @preserve */
|
|
259
|
+
if (rx === 0 || ry === 0) {
|
|
260
|
+
return getPointAtLineLength(x1, y1, x, y, distance);
|
|
261
|
+
}
|
|
262
|
+
const { PI, cos, sin } = Math;
|
|
263
|
+
const sweepAngle = endAngle - startAngle;
|
|
264
|
+
const xRot = ((angle % 360) + 360) % 360;
|
|
265
|
+
const xRotRad = xRot * (PI / 180);
|
|
266
|
+
const alpha = startAngle + sweepAngle * (distance / length);
|
|
267
|
+
const ellipseComponentX = rx * cos(alpha);
|
|
268
|
+
const ellipseComponentY = ry * sin(alpha);
|
|
269
|
+
|
|
270
|
+
point = {
|
|
271
|
+
x: cos(xRotRad) * ellipseComponentX - sin(xRotRad) * ellipseComponentY + center.x,
|
|
272
|
+
y: sin(xRotRad) * ellipseComponentX + cos(xRotRad) * ellipseComponentY + center.y,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return point;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Returns the bounding box for an Arc segment.
|
|
282
|
+
* @see https://github.com/herrstrietzel/svg-pathdata-getbbox
|
|
283
|
+
*
|
|
284
|
+
* @param x1 the starting point X
|
|
285
|
+
* @param y1 the starting point Y
|
|
286
|
+
* @param RX the radius on X axis
|
|
287
|
+
* @param RY the radius on Y axis
|
|
288
|
+
* @param angle the ellipse rotation in degrees
|
|
289
|
+
* @param LAF the large arc flag
|
|
290
|
+
* @param SF the sweep flag
|
|
291
|
+
* @param x2 the ending point X
|
|
292
|
+
* @param y2 the ending point Y
|
|
293
|
+
* @returns the extrema of the Arc segment
|
|
294
|
+
*
|
|
295
|
+
*/
|
|
296
|
+
export const getArcBBox = (
|
|
297
|
+
x1: number,
|
|
298
|
+
y1: number,
|
|
299
|
+
RX: number,
|
|
300
|
+
RY: number,
|
|
301
|
+
angle: number,
|
|
302
|
+
LAF: number,
|
|
303
|
+
SF: number,
|
|
304
|
+
x: number,
|
|
305
|
+
y: number,
|
|
306
|
+
) => {
|
|
307
|
+
const { center, rx, ry, startAngle, endAngle } = getArcProps(x1, y1, RX, RY, angle, LAF, SF, x, y);
|
|
308
|
+
const deltaAngle = endAngle - startAngle;
|
|
309
|
+
|
|
310
|
+
// final on path point
|
|
311
|
+
const p = { x, y };
|
|
312
|
+
|
|
313
|
+
// circle/elipse center coordinates
|
|
314
|
+
const [cx, cy] = [center.x, center.y];
|
|
315
|
+
|
|
316
|
+
// collect extreme points – add end point
|
|
317
|
+
const extremes = [p];
|
|
318
|
+
|
|
319
|
+
// rotation to radians
|
|
320
|
+
const alpha = (angle * Math.PI) / 180;
|
|
321
|
+
const tan = Math.tan(alpha);
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* find min/max from zeroes of directional derivative along x and y
|
|
325
|
+
* along x axis
|
|
326
|
+
*/
|
|
327
|
+
const theta = Math.atan2(-ry * tan, rx);
|
|
328
|
+
const angle1 = theta;
|
|
329
|
+
const angle2 = theta + Math.PI;
|
|
330
|
+
const angle3 = Math.atan2(ry, rx * tan);
|
|
331
|
+
const angle4 = angle3 + Math.PI;
|
|
332
|
+
|
|
333
|
+
// inner bounding box
|
|
334
|
+
const xArr = [x1, x];
|
|
335
|
+
const yArr = [y1, y];
|
|
336
|
+
const xMin = Math.min(...xArr);
|
|
337
|
+
const xMax = Math.max(...xArr);
|
|
338
|
+
const yMin = Math.min(...yArr);
|
|
339
|
+
const yMax = Math.max(...yArr);
|
|
340
|
+
|
|
341
|
+
// on path point close after start
|
|
342
|
+
const angleAfterStart = endAngle - deltaAngle * 0.001;
|
|
343
|
+
const pP2 = arc(angleAfterStart, cx, cy, rx, ry, alpha);
|
|
344
|
+
|
|
345
|
+
// on path point close before end
|
|
346
|
+
const angleBeforeEnd = endAngle - deltaAngle * 0.999;
|
|
347
|
+
const pP3 = arc(angleBeforeEnd, cx, cy, rx, ry, alpha);
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* expected extremes
|
|
351
|
+
* if leaving inner bounding box
|
|
352
|
+
* (between segment start and end point)
|
|
353
|
+
* otherwise exclude elliptic extreme points
|
|
354
|
+
*/
|
|
355
|
+
|
|
356
|
+
// right
|
|
357
|
+
// istanbul ignore if @preserve
|
|
358
|
+
if (pP2.x > xMax || pP3.x > xMax) {
|
|
359
|
+
// get point for this theta
|
|
360
|
+
extremes.push(arc(angle1, cx, cy, rx, ry, alpha));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// left
|
|
364
|
+
// istanbul ignore if @preserve
|
|
365
|
+
if (pP2.x < xMin || pP3.x < xMin) {
|
|
366
|
+
// get anti-symmetric point
|
|
367
|
+
extremes.push(arc(angle2, cx, cy, rx, ry, alpha));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// top
|
|
371
|
+
// istanbul ignore if @preserve
|
|
372
|
+
if (pP2.y < yMin || pP3.y < yMin) {
|
|
373
|
+
// get anti-symmetric point
|
|
374
|
+
extremes.push(arc(angle4, cx, cy, rx, ry, alpha));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// bottom
|
|
378
|
+
// istanbul ignore if @preserve
|
|
379
|
+
if (pP2.y > yMax || pP3.y > yMax) {
|
|
380
|
+
// get point for this theta
|
|
381
|
+
extremes.push(arc(angle3, cx, cy, rx, ry, alpha));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
min: {
|
|
386
|
+
x: Math.min(...extremes.map(n => n.x)),
|
|
387
|
+
y: Math.min(...extremes.map(n => n.y)),
|
|
210
388
|
},
|
|
211
|
-
|
|
212
|
-
|
|
389
|
+
max: {
|
|
390
|
+
x: Math.max(...extremes.map(n => n.x)),
|
|
391
|
+
y: Math.max(...extremes.map(n => n.y)),
|
|
213
392
|
},
|
|
214
393
|
};
|
|
215
394
|
};
|
|
216
|
-
|
|
217
|
-
export default getSegmentProperties;
|
package/src/math/bezier.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type {
|
|
2
|
+
PointTuple,
|
|
3
|
+
DerivedPoint,
|
|
4
|
+
QuadPoints,
|
|
5
|
+
CubicPoints,
|
|
6
|
+
DerivedQuadPoints,
|
|
7
|
+
DerivedCubicPoints,
|
|
8
|
+
QuadCoordinates,
|
|
9
|
+
CubicCoordinates,
|
|
10
|
+
DeriveCallback,
|
|
11
|
+
} from '../types';
|
|
2
12
|
|
|
3
13
|
/**
|
|
4
14
|
* Tools from bezier.js by Mike 'Pomax' Kamermans
|
|
@@ -37,25 +47,6 @@ const Cvalues = [
|
|
|
37
47
|
0.0123412297999871995468056670700372915759, 0.0123412297999871995468056670700372915759,
|
|
38
48
|
];
|
|
39
49
|
|
|
40
|
-
type DerivedPoint = Point & { t: number };
|
|
41
|
-
type QuadPoints = [Point, Point, Point, Point, Point, Point];
|
|
42
|
-
type CubicPoints = [Point, Point, Point, Point, Point, Point, Point, Point];
|
|
43
|
-
type DerivedQuadPoints = [DerivedPoint, DerivedPoint, DerivedPoint, DerivedPoint, DerivedPoint, DerivedPoint];
|
|
44
|
-
type DerivedCubicPoints = [
|
|
45
|
-
DerivedPoint,
|
|
46
|
-
DerivedPoint,
|
|
47
|
-
DerivedPoint,
|
|
48
|
-
DerivedPoint,
|
|
49
|
-
DerivedPoint,
|
|
50
|
-
DerivedPoint,
|
|
51
|
-
DerivedPoint,
|
|
52
|
-
DerivedPoint,
|
|
53
|
-
];
|
|
54
|
-
export type QuadCoordinates = [number, number, number, number, number, number];
|
|
55
|
-
export type CubicCoordinates = [number, number, number, number, number, number, number, number];
|
|
56
|
-
|
|
57
|
-
type DeriveCallback = (t: number) => Point;
|
|
58
|
-
|
|
59
50
|
/**
|
|
60
51
|
*
|
|
61
52
|
* @param points
|
|
@@ -195,15 +186,15 @@ export const minmaxQ = (A: [number, number, number]) => {
|
|
|
195
186
|
const min = Math.min(A[0], A[2]);
|
|
196
187
|
const max = Math.max(A[0], A[2]);
|
|
197
188
|
|
|
198
|
-
/* istanbul ignore
|
|
189
|
+
/* istanbul ignore next @preserve */
|
|
199
190
|
if (A[1] >= A[0] ? A[2] >= A[1] : A[2] <= A[1]) {
|
|
200
191
|
// if no extremum in ]0,1[
|
|
201
|
-
return [min, max] as
|
|
192
|
+
return [min, max] as PointTuple;
|
|
202
193
|
}
|
|
203
194
|
|
|
204
195
|
// check if the extremum E is min or max
|
|
205
196
|
const E = (A[0] * A[2] - A[1] * A[1]) / (A[0] - 2 * A[1] + A[2]);
|
|
206
|
-
return (E < min ? [E, max] : [min, E]) as
|
|
197
|
+
return (E < min ? [E, max] : [min, E]) as PointTuple;
|
|
207
198
|
};
|
|
208
199
|
|
|
209
200
|
/**
|
|
@@ -215,11 +206,11 @@ export const minmaxC = (A: [number, number, number, number]) => {
|
|
|
215
206
|
const K = A[0] - 3 * A[1] + 3 * A[2] - A[3];
|
|
216
207
|
|
|
217
208
|
// if the polynomial is (almost) quadratic and not cubic
|
|
218
|
-
/* istanbul ignore
|
|
209
|
+
/* istanbul ignore next @preserve */
|
|
219
210
|
if (Math.abs(K) < CBEZIER_MINMAX_EPSILON) {
|
|
220
211
|
if (A[0] === A[3] && A[0] === A[1]) {
|
|
221
212
|
// no curve, point targeting same location
|
|
222
|
-
return [A[0], A[3]] as
|
|
213
|
+
return [A[0], A[3]] as PointTuple;
|
|
223
214
|
}
|
|
224
215
|
|
|
225
216
|
return minmaxQ([A[0], -0.5 * A[0] + 1.5 * A[1], A[0] - 3 * A[1] + 3 * A[2]]);
|
|
@@ -230,7 +221,7 @@ export const minmaxC = (A: [number, number, number, number]) => {
|
|
|
230
221
|
|
|
231
222
|
// if the polynomial is monotone in [0,1]
|
|
232
223
|
if (T <= 0) {
|
|
233
|
-
return [Math.min(A[0], A[3]), Math.max(A[0], A[3])] as
|
|
224
|
+
return [Math.min(A[0], A[3]), Math.max(A[0], A[3])] as PointTuple;
|
|
234
225
|
}
|
|
235
226
|
const S = Math.sqrt(T);
|
|
236
227
|
|
|
@@ -241,6 +232,7 @@ export const minmaxC = (A: [number, number, number, number]) => {
|
|
|
241
232
|
const L = A[0] - 2 * A[1] + A[2];
|
|
242
233
|
// check local extrema
|
|
243
234
|
for (let R = (L + S) / K, i = 1; i <= 2; R = (L - S) / K, i++) {
|
|
235
|
+
// istanbul ignore next @preserve
|
|
244
236
|
if (R > 0 && R < 1) {
|
|
245
237
|
// if the extrema is for R in [0,1]
|
|
246
238
|
const Q =
|
|
@@ -257,5 +249,5 @@ export const minmaxC = (A: [number, number, number, number]) => {
|
|
|
257
249
|
}
|
|
258
250
|
}
|
|
259
251
|
|
|
260
|
-
return [min, max] as
|
|
252
|
+
return [min, max] as PointTuple;
|
|
261
253
|
};
|
package/src/math/cubicTools.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { length, minmaxC
|
|
1
|
+
import { length, minmaxC } from './bezier';
|
|
2
|
+
import { type CubicCoordinates } from '../types';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Returns a {x,y} point at a given length of a CubicBezier segment.
|
|
@@ -14,7 +15,7 @@ import { length, minmaxC, type CubicCoordinates } from './bezier';
|
|
|
14
15
|
* @param t a [0-1] ratio
|
|
15
16
|
* @returns the point at cubic-bezier segment length
|
|
16
17
|
*/
|
|
17
|
-
const getPointAtCubicSegmentLength = ([x1, y1, c1x, c1y, c2x, c2y, x2, y2]: CubicCoordinates, t: number) => {
|
|
18
|
+
export const getPointAtCubicSegmentLength = ([x1, y1, c1x, c1y, c2x, c2y, x2, y2]: CubicCoordinates, t: number) => {
|
|
18
19
|
const t1 = 1 - t;
|
|
19
20
|
return {
|
|
20
21
|
x: t1 ** 3 * x1 + 3 * t1 ** 2 * t * c1x + 3 * t1 * t ** 2 * c2x + t ** 3 * x2,
|
|
@@ -23,7 +24,7 @@ const getPointAtCubicSegmentLength = ([x1, y1, c1x, c1y, c2x, c2y, x2, y2]: Cubi
|
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
* Returns the
|
|
27
|
+
* Returns the length of a CubicBezier segment.
|
|
27
28
|
*
|
|
28
29
|
* @param x1 the starting point X
|
|
29
30
|
* @param y1 the starting point Y
|
|
@@ -33,10 +34,36 @@ const getPointAtCubicSegmentLength = ([x1, y1, c1x, c1y, c2x, c2y, x2, y2]: Cubi
|
|
|
33
34
|
* @param c2y the second control point Y
|
|
34
35
|
* @param x2 the ending point X
|
|
35
36
|
* @param y2 the ending point Y
|
|
36
|
-
* @
|
|
37
|
-
* @returns the segment length, point at length and the bounding box
|
|
37
|
+
* @returns the CubicBezier segment length
|
|
38
38
|
*/
|
|
39
|
-
const
|
|
39
|
+
export const getCubicLength = (
|
|
40
|
+
x1: number,
|
|
41
|
+
y1: number,
|
|
42
|
+
c1x: number,
|
|
43
|
+
c1y: number,
|
|
44
|
+
c2x: number,
|
|
45
|
+
c2y: number,
|
|
46
|
+
x2: number,
|
|
47
|
+
y2: number,
|
|
48
|
+
) => {
|
|
49
|
+
return length([x1, y1, c1x, c1y, c2x, c2y, x2, y2]);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns the point along a CubicBezier segment at a given distance.
|
|
54
|
+
*
|
|
55
|
+
* @param x1 the starting point X
|
|
56
|
+
* @param y1 the starting point Y
|
|
57
|
+
* @param c1x the first control point X
|
|
58
|
+
* @param c1y the first control point Y
|
|
59
|
+
* @param c2x the second control point X
|
|
60
|
+
* @param c2y the second control point Y
|
|
61
|
+
* @param x2 the ending point X
|
|
62
|
+
* @param y2 the ending point Y
|
|
63
|
+
* @param distance the distance to look at
|
|
64
|
+
* @returns the point at CubicBezier length
|
|
65
|
+
*/
|
|
66
|
+
export const getPointAtCubicLength = (
|
|
40
67
|
x1: number,
|
|
41
68
|
y1: number,
|
|
42
69
|
c1x: number,
|
|
@@ -48,34 +75,48 @@ const getSegmentProperties = (
|
|
|
48
75
|
distance?: number,
|
|
49
76
|
) => {
|
|
50
77
|
const distanceIsNumber = typeof distance === 'number';
|
|
51
|
-
let
|
|
52
|
-
|
|
53
|
-
|
|
78
|
+
let point = { x: x1, y: y1 };
|
|
79
|
+
/* istanbul ignore else @preserve */
|
|
54
80
|
if (distanceIsNumber) {
|
|
55
|
-
const currentLength =
|
|
81
|
+
const currentLength = length([x1, y1, c1x, c1y, c2x, c2y, x2, y2]);
|
|
56
82
|
if (distance <= 0) {
|
|
57
83
|
// first point already defined
|
|
58
84
|
} else if (distance >= currentLength) {
|
|
59
|
-
|
|
85
|
+
point = { x: x2, y: y2 };
|
|
60
86
|
} else {
|
|
61
|
-
|
|
87
|
+
point = getPointAtCubicSegmentLength([x1, y1, c1x, c1y, c2x, c2y, x2, y2], distance / currentLength);
|
|
62
88
|
}
|
|
63
89
|
}
|
|
90
|
+
return point;
|
|
91
|
+
};
|
|
64
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Returns the boundig box of a CubicBezier segment.
|
|
95
|
+
*
|
|
96
|
+
* @param x1 the starting point X
|
|
97
|
+
* @param y1 the starting point Y
|
|
98
|
+
* @param c1x the first control point X
|
|
99
|
+
* @param c1y the first control point Y
|
|
100
|
+
* @param c2x the second control point X
|
|
101
|
+
* @param c2y the second control point Y
|
|
102
|
+
* @param x2 the ending point X
|
|
103
|
+
* @param y2 the ending point Y
|
|
104
|
+
* @returns the point at CubicBezier length
|
|
105
|
+
*/
|
|
106
|
+
export const getCubicBBox = (
|
|
107
|
+
x1: number,
|
|
108
|
+
y1: number,
|
|
109
|
+
c1x: number,
|
|
110
|
+
c1y: number,
|
|
111
|
+
c2x: number,
|
|
112
|
+
c2y: number,
|
|
113
|
+
x2: number,
|
|
114
|
+
y2: number,
|
|
115
|
+
) => {
|
|
116
|
+
const cxMinMax = minmaxC([x1, c1x, c2x, x2]);
|
|
117
|
+
const cyMinMax = minmaxC([y1, c1y, c2y, y2]);
|
|
65
118
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return getLength();
|
|
69
|
-
},
|
|
70
|
-
get bbox() {
|
|
71
|
-
const cxMinMax = minmaxC([x1, c1x, c2x, x2]);
|
|
72
|
-
const cyMinMax = minmaxC([y1, c1y, c2y, y2]);
|
|
73
|
-
return {
|
|
74
|
-
min: { x: cxMinMax[0], y: cyMinMax[0] },
|
|
75
|
-
max: { x: cxMinMax[1], y: cyMinMax[1] },
|
|
76
|
-
};
|
|
77
|
-
},
|
|
119
|
+
min: { x: cxMinMax[0], y: cyMinMax[0] },
|
|
120
|
+
max: { x: cxMinMax[1], y: cyMinMax[1] },
|
|
78
121
|
};
|
|
79
122
|
};
|
|
80
|
-
|
|
81
|
-
export default getSegmentProperties;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type PointTuple } from '../types';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Returns the square root of the distance
|
|
3
5
|
* between two given points.
|
|
@@ -6,7 +8,7 @@
|
|
|
6
8
|
* @param b the second point coordinates
|
|
7
9
|
* @returns the distance value
|
|
8
10
|
*/
|
|
9
|
-
const distanceSquareRoot = (a:
|
|
11
|
+
const distanceSquareRoot = (a: PointTuple, b: PointTuple): number => {
|
|
10
12
|
return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]));
|
|
11
13
|
};
|
|
12
14
|
|