modern-path2d 1.7.0 → 1.8.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/dist/index.cjs +316 -11
- package/dist/index.d.cts +122 -2
- package/dist/index.d.mts +122 -2
- package/dist/index.d.ts +122 -2
- package/dist/index.js +3 -2
- package/dist/index.mjs +313 -12
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import earcut from 'earcut';
|
|
2
|
+
import polygonClipping from 'polygon-clipping';
|
|
2
3
|
|
|
3
4
|
function drawPoint(ctx, x, y, options = {}) {
|
|
4
5
|
const { radius = 1 } = options;
|
|
@@ -297,6 +298,63 @@ class BoundingBox {
|
|
|
297
298
|
}
|
|
298
299
|
}
|
|
299
300
|
|
|
301
|
+
function flatRingToPairs(r) {
|
|
302
|
+
const ring = [];
|
|
303
|
+
for (let i = 0; i < r.length; i += 2) {
|
|
304
|
+
ring.push([r[i], r[i + 1]]);
|
|
305
|
+
}
|
|
306
|
+
const first = ring[0];
|
|
307
|
+
const last = ring[ring.length - 1];
|
|
308
|
+
if (first && last && (first[0] !== last[0] || first[1] !== last[1])) {
|
|
309
|
+
ring.push([first[0], first[1]]);
|
|
310
|
+
}
|
|
311
|
+
return ring;
|
|
312
|
+
}
|
|
313
|
+
function ringsToGeom(rings) {
|
|
314
|
+
const valid = rings.filter((r) => r.length >= 6).map(flatRingToPairs);
|
|
315
|
+
if (!valid.length) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
let geom = [[valid[0]]];
|
|
319
|
+
for (let i = 1; i < valid.length; i++) {
|
|
320
|
+
geom = polygonClipping.xor(geom, [[valid[i]]]);
|
|
321
|
+
}
|
|
322
|
+
return geom;
|
|
323
|
+
}
|
|
324
|
+
function geomToRings(geom) {
|
|
325
|
+
const out = [];
|
|
326
|
+
for (const poly of geom) {
|
|
327
|
+
for (const ring of poly) {
|
|
328
|
+
const flat = [];
|
|
329
|
+
for (const [x, y] of ring) {
|
|
330
|
+
flat.push(x, y);
|
|
331
|
+
}
|
|
332
|
+
out.push(flat);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
337
|
+
function polygonBoolean(op, ringsA, ringsB) {
|
|
338
|
+
const a = ringsToGeom(ringsA);
|
|
339
|
+
const b = ringsToGeom(ringsB);
|
|
340
|
+
let res;
|
|
341
|
+
switch (op) {
|
|
342
|
+
case "union":
|
|
343
|
+
res = polygonClipping.union(a, b);
|
|
344
|
+
break;
|
|
345
|
+
case "intersection":
|
|
346
|
+
res = polygonClipping.intersection(a, b);
|
|
347
|
+
break;
|
|
348
|
+
case "difference":
|
|
349
|
+
res = polygonClipping.difference(a, b);
|
|
350
|
+
break;
|
|
351
|
+
case "xor":
|
|
352
|
+
res = polygonClipping.xor(a, b);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
return geomToRings(res);
|
|
356
|
+
}
|
|
357
|
+
|
|
300
358
|
function catmullRom(t, p0, p1, p2, p3) {
|
|
301
359
|
const v0 = (p2 - p0) * 0.5;
|
|
302
360
|
const v1 = (p3 - p1) * 0.5;
|
|
@@ -861,22 +919,35 @@ function quadraticBezier(t, p0, p1, p2) {
|
|
|
861
919
|
return quadraticBezierP0(t, p0) + quadraticBezierP1(t, p1) + quadraticBezierP2(t, p2);
|
|
862
920
|
}
|
|
863
921
|
|
|
922
|
+
function resolveLineJoin(join) {
|
|
923
|
+
switch (join) {
|
|
924
|
+
case "round":
|
|
925
|
+
case "bevel":
|
|
926
|
+
case "miter":
|
|
927
|
+
return join;
|
|
928
|
+
default:
|
|
929
|
+
return "miter";
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function resolveLineStyle(style) {
|
|
933
|
+
return {
|
|
934
|
+
width: style?.strokeWidth ?? 1,
|
|
935
|
+
alignment: 0.5,
|
|
936
|
+
join: resolveLineJoin(style?.strokeLinejoin),
|
|
937
|
+
cap: style?.strokeLinecap ?? "butt",
|
|
938
|
+
miterLimit: style?.strokeMiterlimit ?? 10
|
|
939
|
+
};
|
|
940
|
+
}
|
|
864
941
|
const closePointEps = 1e-4;
|
|
865
942
|
const curveEps = 1e-4;
|
|
866
943
|
function strokeTriangulate(points, options = {}) {
|
|
867
944
|
const {
|
|
868
945
|
vertices = [],
|
|
869
946
|
indices = [],
|
|
870
|
-
lineStyle = {
|
|
871
|
-
alignment: 0.5,
|
|
872
|
-
cap: "butt",
|
|
873
|
-
join: "miter",
|
|
874
|
-
width: 1,
|
|
875
|
-
miterLimit: 10
|
|
876
|
-
},
|
|
877
947
|
flipAlignment = false,
|
|
878
948
|
closed = true
|
|
879
949
|
} = options;
|
|
950
|
+
const lineStyle = options.lineStyle ?? resolveLineStyle(options.style);
|
|
880
951
|
const eps = closePointEps;
|
|
881
952
|
if (points.length === 0) {
|
|
882
953
|
return { vertices, indices };
|
|
@@ -2704,6 +2775,22 @@ class Curve {
|
|
|
2704
2775
|
getControlPointRefs() {
|
|
2705
2776
|
return [];
|
|
2706
2777
|
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Reverse the traversal direction in place (start ↔ end, same geometry). The base
|
|
2780
|
+
* implementation reverses the order of the control-point *values*, which is correct for
|
|
2781
|
+
* line / Bézier / spline primitives whose {@link getControlPointRefs} order matches their
|
|
2782
|
+
* parametric order. {@link RoundCurve} (angle-based) and composites (child order) override it.
|
|
2783
|
+
*/
|
|
2784
|
+
reverse() {
|
|
2785
|
+
const refs = this.getControlPointRefs();
|
|
2786
|
+
const n = refs.length;
|
|
2787
|
+
const snapshot = refs.map((p) => p.clone());
|
|
2788
|
+
for (let i = 0; i < n; i++) {
|
|
2789
|
+
refs[i].copyFrom(snapshot[n - 1 - i]);
|
|
2790
|
+
}
|
|
2791
|
+
this.invalidate();
|
|
2792
|
+
return this;
|
|
2793
|
+
}
|
|
2707
2794
|
applyTransform(transform) {
|
|
2708
2795
|
const isFunction = typeof transform === "function";
|
|
2709
2796
|
this.getControlPointRefs().forEach((p) => {
|
|
@@ -2831,6 +2918,22 @@ class Curve {
|
|
|
2831
2918
|
getTangentAt(u, output) {
|
|
2832
2919
|
return this.getTangent(this.getUToTMapping(u), output);
|
|
2833
2920
|
}
|
|
2921
|
+
/**
|
|
2922
|
+
* PathKit-style sample at an absolute arc-length `distance` along the curve: the point, the unit
|
|
2923
|
+
* tangent, and the tangent `angle` in radians. `distance` is clamped to `[0, getLength()]`, so
|
|
2924
|
+
* passing `0`/`getLength()` always yields the endpoints. See {@link PathMeasure} for a wrapper.
|
|
2925
|
+
*/
|
|
2926
|
+
getPosTan(distance) {
|
|
2927
|
+
const length = this.getLength();
|
|
2928
|
+
const u = length > 0 ? Math.min(Math.max(distance / length, 0), 1) : 0;
|
|
2929
|
+
const t = this.getUToTMapping(u);
|
|
2930
|
+
const tangent = this.getTangent(t);
|
|
2931
|
+
return {
|
|
2932
|
+
position: this.getPoint(t),
|
|
2933
|
+
tangent,
|
|
2934
|
+
angle: Math.atan2(tangent.y, tangent.x)
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2834
2937
|
getNormal(t, output = new Vector2()) {
|
|
2835
2938
|
this.getTangent(t, output);
|
|
2836
2939
|
return output.set(-output.y, output.x).normalize();
|
|
@@ -2926,10 +3029,28 @@ class Curve {
|
|
|
2926
3029
|
options
|
|
2927
3030
|
);
|
|
2928
3031
|
}
|
|
3032
|
+
/**
|
|
3033
|
+
* Whether this curve forms a closed loop (its outline should be stroked without end caps,
|
|
3034
|
+
* stitching the last vertex back to the first). The base test is purely geometric — the first
|
|
3035
|
+
* sampled vertex coincides with the last. Curves that close without a duplicated endpoint
|
|
3036
|
+
* (a full-revolution {@link RoundCurve}, rectangles, polygons) override this.
|
|
3037
|
+
*/
|
|
3038
|
+
isClosed() {
|
|
3039
|
+
const v = this._getCachedAdaptiveVertices();
|
|
3040
|
+
const len = v.length;
|
|
3041
|
+
if (len < 6) {
|
|
3042
|
+
return false;
|
|
3043
|
+
}
|
|
3044
|
+
const eps = 1e-4;
|
|
3045
|
+
return Math.abs(v[0] - v[len - 2]) < eps && Math.abs(v[1] - v[len - 1]) < eps;
|
|
3046
|
+
}
|
|
2929
3047
|
strokeTriangulate(options) {
|
|
2930
3048
|
return strokeTriangulate(
|
|
2931
3049
|
this.getAdaptiveVertices(),
|
|
2932
|
-
|
|
3050
|
+
{
|
|
3051
|
+
...options,
|
|
3052
|
+
closed: options?.closed ?? this.isClosed()
|
|
3053
|
+
}
|
|
2933
3054
|
);
|
|
2934
3055
|
}
|
|
2935
3056
|
toCommands() {
|
|
@@ -3024,6 +3145,22 @@ class RoundCurve extends Curve {
|
|
|
3024
3145
|
isClockwise() {
|
|
3025
3146
|
return this.clockwise;
|
|
3026
3147
|
}
|
|
3148
|
+
/**
|
|
3149
|
+
* A circle/ellipse arc is closed when it sweeps (at least) a full revolution — the sampled
|
|
3150
|
+
* outline does not duplicate the start vertex, so the geometric first==last test in the base
|
|
3151
|
+
* class would wrongly report a full circle as open and leave a seam gap in the stroke.
|
|
3152
|
+
*/
|
|
3153
|
+
isClosed() {
|
|
3154
|
+
return Math.abs(this.endAngle - this.startAngle) >= Math.PI * 2 - 1e-9 || super.isClosed();
|
|
3155
|
+
}
|
|
3156
|
+
reverse() {
|
|
3157
|
+
const { startAngle, endAngle } = this;
|
|
3158
|
+
this.startAngle = endAngle;
|
|
3159
|
+
this.endAngle = startAngle;
|
|
3160
|
+
this.clockwise = !this.clockwise;
|
|
3161
|
+
this.invalidate();
|
|
3162
|
+
return this;
|
|
3163
|
+
}
|
|
3027
3164
|
_getDeltaAngle() {
|
|
3028
3165
|
const PI_2 = Math.PI * 2;
|
|
3029
3166
|
let deltaAngle = this.endAngle - this.startAngle;
|
|
@@ -3280,9 +3417,25 @@ class RoundCurve extends Curve {
|
|
|
3280
3417
|
return output;
|
|
3281
3418
|
}
|
|
3282
3419
|
getAdaptiveVertices(output = []) {
|
|
3283
|
-
|
|
3420
|
+
const PI2 = Math.PI * 2;
|
|
3421
|
+
if (this.startAngle === 0 && this.endAngle === PI2) {
|
|
3284
3422
|
return this._getAdaptiveVerticesByCircle(output);
|
|
3285
3423
|
}
|
|
3424
|
+
if (Math.abs(this.endAngle - this.startAngle) >= PI2 - 1e-9) {
|
|
3425
|
+
const tmp = this._getAdaptiveVerticesByCircle([]);
|
|
3426
|
+
const n = tmp.length / 2;
|
|
3427
|
+
if (this.endAngle > this.startAngle) {
|
|
3428
|
+
for (let i = 0; i < tmp.length; i++) {
|
|
3429
|
+
output.push(tmp[i]);
|
|
3430
|
+
}
|
|
3431
|
+
} else {
|
|
3432
|
+
output.push(tmp[0], tmp[1]);
|
|
3433
|
+
for (let i = n - 1; i >= 1; i--) {
|
|
3434
|
+
output.push(tmp[i * 2], tmp[i * 2 + 1]);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
return output;
|
|
3438
|
+
}
|
|
3286
3439
|
return this._getAdaptiveVerticesByArc(output);
|
|
3287
3440
|
}
|
|
3288
3441
|
copyFrom(source) {
|
|
@@ -3492,6 +3645,15 @@ class LineCurve extends Curve {
|
|
|
3492
3645
|
getControlPointRefs() {
|
|
3493
3646
|
return [this.p1, this.p2];
|
|
3494
3647
|
}
|
|
3648
|
+
// Swap endpoint *references* (not values) so corner Vector2s shared with adjacent
|
|
3649
|
+
// segments stay intact and simply re-associate with the reversed segment.
|
|
3650
|
+
reverse() {
|
|
3651
|
+
const { p1, p2 } = this;
|
|
3652
|
+
this.p1 = p2;
|
|
3653
|
+
this.p2 = p1;
|
|
3654
|
+
this.invalidate();
|
|
3655
|
+
return this;
|
|
3656
|
+
}
|
|
3495
3657
|
getAdaptiveVertices(output = []) {
|
|
3496
3658
|
output.push(
|
|
3497
3659
|
this.p1.x,
|
|
@@ -3655,6 +3817,16 @@ class CompositeCurve extends Curve {
|
|
|
3655
3817
|
});
|
|
3656
3818
|
return output;
|
|
3657
3819
|
}
|
|
3820
|
+
/**
|
|
3821
|
+
* A composite is closed when its single child is closed (e.g. a lone full-circle arc), or when
|
|
3822
|
+
* its assembled outline returns to its start (rectangles, polygons, multi-segment loops).
|
|
3823
|
+
*/
|
|
3824
|
+
isClosed() {
|
|
3825
|
+
if (this.curves.length === 1) {
|
|
3826
|
+
return this.curves[0].isClosed();
|
|
3827
|
+
}
|
|
3828
|
+
return super.isClosed();
|
|
3829
|
+
}
|
|
3658
3830
|
strokeTriangulate(options) {
|
|
3659
3831
|
if (this.curves.length === 1) {
|
|
3660
3832
|
return this.curves[0].strokeTriangulate(options);
|
|
@@ -3662,6 +3834,13 @@ class CompositeCurve extends Curve {
|
|
|
3662
3834
|
return super.strokeTriangulate(options);
|
|
3663
3835
|
}
|
|
3664
3836
|
}
|
|
3837
|
+
/** Reverse the sub-curve order and reverse each sub-curve, so the whole outline runs backwards. */
|
|
3838
|
+
reverse() {
|
|
3839
|
+
this.curves.reverse();
|
|
3840
|
+
this.curves.forEach((curve) => curve.reverse());
|
|
3841
|
+
this.invalidate();
|
|
3842
|
+
return this;
|
|
3843
|
+
}
|
|
3665
3844
|
getFillVertices(options) {
|
|
3666
3845
|
if (this.curves.length === 1) {
|
|
3667
3846
|
return this.curves[0].getFillVertices(options);
|
|
@@ -3757,6 +3936,16 @@ class CubicBezierCurve extends Curve {
|
|
|
3757
3936
|
getControlPointRefs() {
|
|
3758
3937
|
return [this.p1, this.cp1, this.cp2, this.p2];
|
|
3759
3938
|
}
|
|
3939
|
+
// Swap endpoint and control-point references; keeps shared corner Vector2s intact.
|
|
3940
|
+
reverse() {
|
|
3941
|
+
const { p1, cp1, cp2, p2 } = this;
|
|
3942
|
+
this.p1 = p2;
|
|
3943
|
+
this.cp1 = cp2;
|
|
3944
|
+
this.cp2 = cp1;
|
|
3945
|
+
this.p2 = p1;
|
|
3946
|
+
this.invalidate();
|
|
3947
|
+
return this;
|
|
3948
|
+
}
|
|
3760
3949
|
_solveQuadratic(a, b, c) {
|
|
3761
3950
|
if (Math.abs(a) < 1e-12) {
|
|
3762
3951
|
if (Math.abs(b) < 1e-12)
|
|
@@ -3917,6 +4106,14 @@ class QuadraticBezierCurve extends Curve {
|
|
|
3917
4106
|
getControlPointRefs() {
|
|
3918
4107
|
return [this.p1, this.cp, this.p2];
|
|
3919
4108
|
}
|
|
4109
|
+
// Swap endpoint references (cp is symmetric); keeps shared corner Vector2s intact.
|
|
4110
|
+
reverse() {
|
|
4111
|
+
const { p1, p2 } = this;
|
|
4112
|
+
this.p1 = p2;
|
|
4113
|
+
this.p2 = p1;
|
|
4114
|
+
this.invalidate();
|
|
4115
|
+
return this;
|
|
4116
|
+
}
|
|
3920
4117
|
getAdaptiveVertices(output = []) {
|
|
3921
4118
|
return getAdaptiveQuadraticBezierCurvePoints(
|
|
3922
4119
|
this.p1.x,
|
|
@@ -4118,6 +4315,11 @@ class SplineCurve extends Curve {
|
|
|
4118
4315
|
getControlPointRefs() {
|
|
4119
4316
|
return this.points;
|
|
4120
4317
|
}
|
|
4318
|
+
reverse() {
|
|
4319
|
+
this.points.reverse();
|
|
4320
|
+
this.invalidate();
|
|
4321
|
+
return this;
|
|
4322
|
+
}
|
|
4121
4323
|
copyFrom(source) {
|
|
4122
4324
|
super.copyFrom(source);
|
|
4123
4325
|
this.points = [];
|
|
@@ -4154,6 +4356,22 @@ class CurvePath extends CompositeCurve {
|
|
|
4154
4356
|
this.addCommands(svgPathDataToCommands(data));
|
|
4155
4357
|
return this;
|
|
4156
4358
|
}
|
|
4359
|
+
/**
|
|
4360
|
+
* A sub-path is closed if it was explicitly closed (`autoClose`, i.e. a `Z`/`closePath`), or if
|
|
4361
|
+
* it forms a geometric loop / wraps a single closed primitive (handled by the base class).
|
|
4362
|
+
*/
|
|
4363
|
+
isClosed() {
|
|
4364
|
+
return this.autoClose || super.isClosed();
|
|
4365
|
+
}
|
|
4366
|
+
/** Reverse direction, then refresh the {@link startPoint}/{@link currentPoint} cursors. */
|
|
4367
|
+
reverse() {
|
|
4368
|
+
super.reverse();
|
|
4369
|
+
if (this.curves.length) {
|
|
4370
|
+
this.startPoint = this.getPoint(0);
|
|
4371
|
+
this.currentPoint = this.getPoint(1);
|
|
4372
|
+
}
|
|
4373
|
+
return this;
|
|
4374
|
+
}
|
|
4157
4375
|
_closeVertices(output) {
|
|
4158
4376
|
if (this.autoClose && output.length >= 4 && (output[0] !== output[output.length - 2] && output[1] !== output[output.length - 1])) {
|
|
4159
4377
|
output.push(output[0], output[1]);
|
|
@@ -4590,6 +4808,51 @@ class Path2D extends CompositeCurve {
|
|
|
4590
4808
|
const fillRule = options.fillRule ?? this.style.fillRule ?? "nonzero";
|
|
4591
4809
|
return pointInPolygons(point, this._getRings(), fillRule);
|
|
4592
4810
|
}
|
|
4811
|
+
/** Build a `Path2D` from flat rings (`[x0,y0,…]` per sub-path); closed-and-filled as sub-paths. */
|
|
4812
|
+
static fromRings(rings, style = {}) {
|
|
4813
|
+
const path = new Path2D(void 0, style);
|
|
4814
|
+
for (const ring of rings) {
|
|
4815
|
+
if (ring.length < 6) {
|
|
4816
|
+
continue;
|
|
4817
|
+
}
|
|
4818
|
+
let end = ring.length;
|
|
4819
|
+
if (ring[0] === ring[end - 2] && ring[1] === ring[end - 1]) {
|
|
4820
|
+
end -= 2;
|
|
4821
|
+
}
|
|
4822
|
+
path.moveTo(ring[0], ring[1]);
|
|
4823
|
+
for (let i = 2; i < end; i += 2) {
|
|
4824
|
+
path.lineTo(ring[i], ring[i + 1]);
|
|
4825
|
+
}
|
|
4826
|
+
path.closePath();
|
|
4827
|
+
}
|
|
4828
|
+
return path;
|
|
4829
|
+
}
|
|
4830
|
+
/**
|
|
4831
|
+
* Boolean (path) operation against another path, returning a NEW `Path2D` whose outline is the
|
|
4832
|
+
* polygonal result. Curves are sampled before clipping, so the result is a polygonal
|
|
4833
|
+
* approximation (see {@link polygonBoolean}). The result inherits this path's `style` unless
|
|
4834
|
+
* overridden via `style`. Holes are emitted as oppositely-wound sub-paths (nonzero fill).
|
|
4835
|
+
*/
|
|
4836
|
+
booleanOp(op, other, style) {
|
|
4837
|
+
const rings = polygonBoolean(op, this._getRings(), other._getRings());
|
|
4838
|
+
return Path2D.fromRings(rings, { ...this.style, ...style });
|
|
4839
|
+
}
|
|
4840
|
+
/** `this ∪ other` — the combined filled area. */
|
|
4841
|
+
union(other, style) {
|
|
4842
|
+
return this.booleanOp("union", other, style);
|
|
4843
|
+
}
|
|
4844
|
+
/** `this ∩ other` — only the overlapping area. */
|
|
4845
|
+
intersection(other, style) {
|
|
4846
|
+
return this.booleanOp("intersection", other, style);
|
|
4847
|
+
}
|
|
4848
|
+
/** `this − other` — this path with `other` cut away. */
|
|
4849
|
+
difference(other, style) {
|
|
4850
|
+
return this.booleanOp("difference", other, style);
|
|
4851
|
+
}
|
|
4852
|
+
/** `this ⊕ other` — areas covered by exactly one of the two paths. */
|
|
4853
|
+
xor(other, style) {
|
|
4854
|
+
return this.booleanOp("xor", other, style);
|
|
4855
|
+
}
|
|
4593
4856
|
/**
|
|
4594
4857
|
* Test whether a point lies on this path's stroke. A hit on any sub-path counts.
|
|
4595
4858
|
*
|
|
@@ -4689,7 +4952,7 @@ class Path2D extends CompositeCurve {
|
|
|
4689
4952
|
}
|
|
4690
4953
|
drawTo(ctx, style = {}) {
|
|
4691
4954
|
style = { ...this.style, ...style };
|
|
4692
|
-
const { fill = "#000", stroke = "none" } = style;
|
|
4955
|
+
const { fill = "#000", stroke = "none", fillRule = "nonzero" } = style;
|
|
4693
4956
|
ctx.beginPath();
|
|
4694
4957
|
ctx.save();
|
|
4695
4958
|
setCanvasContext(ctx, style);
|
|
@@ -4697,7 +4960,7 @@ class Path2D extends CompositeCurve {
|
|
|
4697
4960
|
path.drawTo(ctx);
|
|
4698
4961
|
});
|
|
4699
4962
|
if (fill !== "none") {
|
|
4700
|
-
ctx.fill();
|
|
4963
|
+
ctx.fill(fillRule);
|
|
4701
4964
|
}
|
|
4702
4965
|
if (stroke !== "none") {
|
|
4703
4966
|
ctx.stroke();
|
|
@@ -4903,6 +5166,44 @@ ${content}
|
|
|
4903
5166
|
}
|
|
4904
5167
|
}
|
|
4905
5168
|
|
|
5169
|
+
class PathMeasure {
|
|
5170
|
+
constructor(curve) {
|
|
5171
|
+
this.curve = curve;
|
|
5172
|
+
}
|
|
5173
|
+
/** Total arc length of the path. */
|
|
5174
|
+
getLength() {
|
|
5175
|
+
return this.curve.getLength();
|
|
5176
|
+
}
|
|
5177
|
+
/** Whether the path forms a closed loop (see {@link Curve.isClosed}). */
|
|
5178
|
+
isClosed() {
|
|
5179
|
+
return this.curve.isClosed();
|
|
5180
|
+
}
|
|
5181
|
+
/** Point + unit tangent + tangent angle at an absolute arc-length `distance` (clamped). */
|
|
5182
|
+
getPosTan(distance) {
|
|
5183
|
+
return this.curve.getPosTan(distance);
|
|
5184
|
+
}
|
|
5185
|
+
/** Point at an absolute arc-length `distance` (clamped to `[0, getLength()]`). */
|
|
5186
|
+
getPosition(distance) {
|
|
5187
|
+
return this.curve.getPosTan(distance).position;
|
|
5188
|
+
}
|
|
5189
|
+
/** Point + tangent at a normalized progress `t ∈ [0, 1]` along the path. */
|
|
5190
|
+
getPosTanAtProgress(t) {
|
|
5191
|
+
return this.curve.getPosTan(this.getLength() * t);
|
|
5192
|
+
}
|
|
5193
|
+
/**
|
|
5194
|
+
* Evenly sample the path into `count + 1` {@link PosTan} entries (arc-length spaced), e.g. to
|
|
5195
|
+
* lay glyphs along a path or drive an `animate(progress)`-style traversal.
|
|
5196
|
+
*/
|
|
5197
|
+
sample(count = 100) {
|
|
5198
|
+
const length = this.getLength();
|
|
5199
|
+
const out = [];
|
|
5200
|
+
for (let i = 0; i <= count; i++) {
|
|
5201
|
+
out.push(this.curve.getPosTan(length * i / count));
|
|
5202
|
+
}
|
|
5203
|
+
return out;
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
5206
|
+
|
|
4906
5207
|
class FFDControlGrid {
|
|
4907
5208
|
constructor(rows, cols, width = 1, height = 1) {
|
|
4908
5209
|
this.rows = rows;
|
|
@@ -4958,4 +5259,4 @@ function applyFFD(point, grid, width = grid.width, height = grid.height) {
|
|
|
4958
5259
|
point.set(x, y);
|
|
4959
5260
|
}
|
|
4960
5261
|
|
|
4961
|
-
export { ArcCurve, BoundingBox, CompositeCurve, CubicBezierCurve, Curve, CurvePath, EllipseCurve, EquilateralPolygonCurve, FFDControlGrid, LineCurve, PI, PI_2, Path2D, Path2DSet, PolygonCurve, QuadraticBezierCurve, RectangleCurve, RoundRectangleCurve, SplineCurve, Transform2D, Vector2, applyFFD, catmullRom, cubicBezier, drawPoint, fillTriangulate, getAdaptiveCubicBezierCurvePoints, getAdaptiveQuadraticBezierCurvePoints, getDirectedArea, getIntersectionPoint, nonzeroFillRule, parseArcCommand, parseCssArg, parseCssArgs, parseCssFunctions, parsePathDataArgs, pointInPolygon, pointInPolygons, pointToPolylineDistance, pointToSegmentDistance, quadraticBezier, setCanvasContext, strokeTriangulate, svgPathCommandsAddToPath2D, svgPathCommandsToData, svgPathDataToCommands, svgToDom, svgToPath2DSet, toKebabCase };
|
|
5262
|
+
export { ArcCurve, BoundingBox, CompositeCurve, CubicBezierCurve, Curve, CurvePath, EllipseCurve, EquilateralPolygonCurve, FFDControlGrid, LineCurve, PI, PI_2, Path2D, Path2DSet, PathMeasure, PolygonCurve, QuadraticBezierCurve, RectangleCurve, RoundRectangleCurve, SplineCurve, Transform2D, Vector2, applyFFD, catmullRom, cubicBezier, drawPoint, fillTriangulate, getAdaptiveCubicBezierCurvePoints, getAdaptiveQuadraticBezierCurvePoints, getDirectedArea, getIntersectionPoint, nonzeroFillRule, parseArcCommand, parseCssArg, parseCssArgs, parseCssFunctions, parsePathDataArgs, pointInPolygon, pointInPolygons, pointToPolylineDistance, pointToSegmentDistance, polygonBoolean, quadraticBezier, resolveLineStyle, setCanvasContext, strokeTriangulate, svgPathCommandsAddToPath2D, svgPathCommandsToData, svgPathDataToCommands, svgToDom, svgToPath2DSet, toKebabCase };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modern-path2d",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.8.0",
|
|
5
5
|
"packageManager": "pnpm@9.15.1",
|
|
6
6
|
"description": "A Path2D library, fully compatible with Web Path2D, with additional support for triangulate、animation、deformation etc.",
|
|
7
7
|
"author": "wxm",
|
|
@@ -56,7 +56,8 @@
|
|
|
56
56
|
"typecheck": "tsc --noEmit"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"earcut": "^3.0.2"
|
|
59
|
+
"earcut": "^3.0.2",
|
|
60
|
+
"polygon-clipping": "^0.15.7"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@antfu/eslint-config": "^8.2.0",
|