modern-path2d 1.6.0 → 1.6.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/dist/index.cjs +237 -2
- package/dist/index.d.cts +129 -2
- package/dist/index.d.mts +129 -2
- package/dist/index.d.ts +129 -2
- package/dist/index.js +2 -2
- package/dist/index.mjs +234 -3
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -592,7 +592,7 @@ function getDirectedArea(vertices) {
|
|
|
592
592
|
function cross(ax, ay, bx, by, cx, cy) {
|
|
593
593
|
return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
|
|
594
594
|
}
|
|
595
|
-
function windingNumber(px, py, polygon) {
|
|
595
|
+
function windingNumber$1(px, py, polygon) {
|
|
596
596
|
const polygonLen = polygon.length;
|
|
597
597
|
let wn = 0;
|
|
598
598
|
for (let i = 0, j = polygonLen - 2; i < polygonLen; j = i, i += 2) {
|
|
@@ -697,7 +697,7 @@ function nonzeroFillRule(paths) {
|
|
|
697
697
|
const wnList = [];
|
|
698
698
|
for (let p = 0, pLen = testPoints.length; p < pLen; p++) {
|
|
699
699
|
const [x, y] = testPoints[p];
|
|
700
|
-
const winding = windingNumber(x, y, paths[j]);
|
|
700
|
+
const winding = windingNumber$1(x, y, paths[j]);
|
|
701
701
|
wnMap[winding] = (wnMap[winding] ?? 0) + 1;
|
|
702
702
|
wnList.push(winding);
|
|
703
703
|
}
|
|
@@ -720,6 +720,120 @@ function nonzeroFillRule(paths) {
|
|
|
720
720
|
return results;
|
|
721
721
|
}
|
|
722
722
|
|
|
723
|
+
function isLeft(ax, ay, bx, by, px, py) {
|
|
724
|
+
return (bx - ax) * (py - ay) - (px - ax) * (by - ay);
|
|
725
|
+
}
|
|
726
|
+
function windingNumber(px, py, vertices) {
|
|
727
|
+
const len = vertices.length;
|
|
728
|
+
let wn = 0;
|
|
729
|
+
for (let i = 0; i < len; i += 2) {
|
|
730
|
+
const ax = vertices[i];
|
|
731
|
+
const ay = vertices[i + 1];
|
|
732
|
+
const k = (i + 2) % len;
|
|
733
|
+
const bx = vertices[k];
|
|
734
|
+
const by = vertices[k + 1];
|
|
735
|
+
if (ay <= py) {
|
|
736
|
+
if (by > py && isLeft(ax, ay, bx, by, px, py) > 0) {
|
|
737
|
+
wn++;
|
|
738
|
+
}
|
|
739
|
+
} else {
|
|
740
|
+
if (by <= py && isLeft(ax, ay, bx, by, px, py) < 0) {
|
|
741
|
+
wn--;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return wn;
|
|
746
|
+
}
|
|
747
|
+
function crossingNumber(px, py, vertices) {
|
|
748
|
+
const len = vertices.length;
|
|
749
|
+
let cn = 0;
|
|
750
|
+
for (let i = 0; i < len; i += 2) {
|
|
751
|
+
const ax = vertices[i];
|
|
752
|
+
const ay = vertices[i + 1];
|
|
753
|
+
const k = (i + 2) % len;
|
|
754
|
+
const bx = vertices[k];
|
|
755
|
+
const by = vertices[k + 1];
|
|
756
|
+
if (ay <= py && by > py || ay > py && by <= py) {
|
|
757
|
+
const t = (py - ay) / (by - ay);
|
|
758
|
+
if (px < ax + t * (bx - ax)) {
|
|
759
|
+
cn++;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return cn;
|
|
764
|
+
}
|
|
765
|
+
function segmentDistance(px, py, ax, ay, bx, by) {
|
|
766
|
+
const dx = bx - ax;
|
|
767
|
+
const dy = by - ay;
|
|
768
|
+
const lenSq = dx * dx + dy * dy;
|
|
769
|
+
let t = lenSq === 0 ? 0 : ((px - ax) * dx + (py - ay) * dy) / lenSq;
|
|
770
|
+
if (t < 0) {
|
|
771
|
+
t = 0;
|
|
772
|
+
} else if (t > 1) {
|
|
773
|
+
t = 1;
|
|
774
|
+
}
|
|
775
|
+
const cx = ax + t * dx;
|
|
776
|
+
const cy = ay + t * dy;
|
|
777
|
+
return Math.hypot(px - cx, py - cy);
|
|
778
|
+
}
|
|
779
|
+
function pointInPolygon(point, vertices, fillRule = "nonzero") {
|
|
780
|
+
if (vertices.length < 6) {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
if (fillRule === "evenodd") {
|
|
784
|
+
return (crossingNumber(point.x, point.y, vertices) & 1) === 1;
|
|
785
|
+
}
|
|
786
|
+
return windingNumber(point.x, point.y, vertices) !== 0;
|
|
787
|
+
}
|
|
788
|
+
function pointInPolygons(point, polygons, fillRule = "nonzero") {
|
|
789
|
+
const { x, y } = point;
|
|
790
|
+
if (fillRule === "evenodd") {
|
|
791
|
+
let cn = 0;
|
|
792
|
+
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
793
|
+
const ring = polygons[i];
|
|
794
|
+
if (ring.length >= 6) {
|
|
795
|
+
cn += crossingNumber(x, y, ring);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return (cn & 1) === 1;
|
|
799
|
+
}
|
|
800
|
+
let wn = 0;
|
|
801
|
+
for (let i = 0, len = polygons.length; i < len; i++) {
|
|
802
|
+
const ring = polygons[i];
|
|
803
|
+
if (ring.length >= 6) {
|
|
804
|
+
wn += windingNumber(x, y, ring);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return wn !== 0;
|
|
808
|
+
}
|
|
809
|
+
function pointToSegmentDistance(point, a, b) {
|
|
810
|
+
return segmentDistance(point.x, point.y, a.x, a.y, b.x, b.y);
|
|
811
|
+
}
|
|
812
|
+
function pointToPolylineDistance(point, vertices, closed = false) {
|
|
813
|
+
const len = vertices.length;
|
|
814
|
+
if (len < 2) {
|
|
815
|
+
return Infinity;
|
|
816
|
+
}
|
|
817
|
+
const { x: px, y: py } = point;
|
|
818
|
+
if (len === 2) {
|
|
819
|
+
return Math.hypot(px - vertices[0], py - vertices[1]);
|
|
820
|
+
}
|
|
821
|
+
let min = Infinity;
|
|
822
|
+
for (let i = 0; i < len - 2; i += 2) {
|
|
823
|
+
const d = segmentDistance(px, py, vertices[i], vertices[i + 1], vertices[i + 2], vertices[i + 3]);
|
|
824
|
+
if (d < min) {
|
|
825
|
+
min = d;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (closed && len >= 6) {
|
|
829
|
+
const d = segmentDistance(px, py, vertices[len - 2], vertices[len - 1], vertices[0], vertices[1]);
|
|
830
|
+
if (d < min) {
|
|
831
|
+
min = d;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
return min;
|
|
835
|
+
}
|
|
836
|
+
|
|
723
837
|
function quadraticBezierP0(t, p) {
|
|
724
838
|
const k = 1 - t;
|
|
725
839
|
return k * k * p;
|
|
@@ -2707,6 +2821,40 @@ class Curve {
|
|
|
2707
2821
|
const { min, max } = this.getMinMax();
|
|
2708
2822
|
return new BoundingBox(min.x, min.y, max.x - min.x, max.y - min.y);
|
|
2709
2823
|
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Test whether a point lies inside the area enclosed by this curve.
|
|
2826
|
+
*
|
|
2827
|
+
* The curve is sampled via {@link getAdaptiveVertices} into a single implicitly closed
|
|
2828
|
+
* ring. This is purely geometric (it ignores any `fill`/`stroke` style), mirroring
|
|
2829
|
+
* `CanvasRenderingContext2D.isPointInPath`.
|
|
2830
|
+
*
|
|
2831
|
+
* Composites that hold multiple sub-paths (e.g. {@link Path2D}) override this so holes
|
|
2832
|
+
* are honored — a single `Curve` is always one ring.
|
|
2833
|
+
*/
|
|
2834
|
+
isPointInFill(point, options = {}) {
|
|
2835
|
+
return pointInPolygon(point, this.getAdaptiveVertices(), options.fillRule);
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Test whether a point lies on this curve's stroke, i.e. within `strokeWidth / 2 + tolerance`
|
|
2839
|
+
* of the sampled outline. The point must be in the same coordinate space as the curve.
|
|
2840
|
+
*
|
|
2841
|
+
* Options: `strokeWidth` (path units, default `1`), `tolerance` (extra hit slack in path
|
|
2842
|
+
* units, default `0` — useful for thin strokes; no coordinate scaling is assumed, so convert
|
|
2843
|
+
* pixel tolerance to path units upstream if your path is normalized), and `closed` (whether
|
|
2844
|
+
* to include the closing edge from the last vertex back to the first).
|
|
2845
|
+
*/
|
|
2846
|
+
isPointInStroke(point, options = {}) {
|
|
2847
|
+
const { strokeWidth = 1, tolerance = 0, closed = false } = options;
|
|
2848
|
+
const distance = pointToPolylineDistance(point, this.getAdaptiveVertices(), closed);
|
|
2849
|
+
return distance <= strokeWidth / 2 + tolerance;
|
|
2850
|
+
}
|
|
2851
|
+
/**
|
|
2852
|
+
* Concise PathKit-style fill containment test: `contains(x, y)` is shorthand for
|
|
2853
|
+
* {@link isPointInFill} with a `{ x, y }` point.
|
|
2854
|
+
*/
|
|
2855
|
+
contains(x, y, options = {}) {
|
|
2856
|
+
return this.isPointInFill({ x, y }, options);
|
|
2857
|
+
}
|
|
2710
2858
|
getFillVertices(_options) {
|
|
2711
2859
|
return this.getAdaptiveVertices();
|
|
2712
2860
|
}
|
|
@@ -3846,6 +3994,17 @@ class CurvePath extends CompositeCurve {
|
|
|
3846
3994
|
super.getFillVertices(options)
|
|
3847
3995
|
);
|
|
3848
3996
|
}
|
|
3997
|
+
/**
|
|
3998
|
+
* Same as {@link Curve.isPointInStroke}, but `closed` defaults to this sub-path's actual
|
|
3999
|
+
* closed-ness: explicitly `autoClose`, or geometrically closed (first vertex === last).
|
|
4000
|
+
*/
|
|
4001
|
+
isPointInStroke(point, options = {}) {
|
|
4002
|
+
const { strokeWidth = 1, tolerance = 0 } = options;
|
|
4003
|
+
const vertices = this.getAdaptiveVertices();
|
|
4004
|
+
const len = vertices.length;
|
|
4005
|
+
const closed = options.closed ?? (this.autoClose || len >= 6 && vertices[0] === vertices[len - 2] && vertices[1] === vertices[len - 1]);
|
|
4006
|
+
return pointToPolylineDistance(point, vertices, closed) <= strokeWidth / 2 + tolerance;
|
|
4007
|
+
}
|
|
3849
4008
|
_setCurrentPoint(point) {
|
|
3850
4009
|
this.currentPoint = new Vector2(point.x, point.y);
|
|
3851
4010
|
if (!this.startPoint) {
|
|
@@ -4209,6 +4368,40 @@ class Path2D extends CompositeCurve {
|
|
|
4209
4368
|
});
|
|
4210
4369
|
return this;
|
|
4211
4370
|
}
|
|
4371
|
+
/**
|
|
4372
|
+
* Test whether a point lies inside the filled area of this path.
|
|
4373
|
+
*
|
|
4374
|
+
* Each sub-path ({@link CurvePath}) is sampled into its own ring and all rings are
|
|
4375
|
+
* evaluated together via {@link pointInPolygons}, so holes (donut / hollow shapes) are
|
|
4376
|
+
* honored. This is purely geometric and ignores `style.fill` — for the `fill: 'none'`
|
|
4377
|
+
* fallback, gate the call upstream (see {@link Path2DSet.hitTest}).
|
|
4378
|
+
*
|
|
4379
|
+
* Defaults `fillRule` to `style.fillRule`, then `'nonzero'` (matching SVG/Canvas).
|
|
4380
|
+
*/
|
|
4381
|
+
isPointInFill(point, options = {}) {
|
|
4382
|
+
const fillRule = options.fillRule ?? this.style.fillRule ?? "nonzero";
|
|
4383
|
+
return pointInPolygons(
|
|
4384
|
+
point,
|
|
4385
|
+
this.curves.map((curve) => curve.getAdaptiveVertices()),
|
|
4386
|
+
fillRule
|
|
4387
|
+
);
|
|
4388
|
+
}
|
|
4389
|
+
/**
|
|
4390
|
+
* Test whether a point lies on this path's stroke. A hit on any sub-path counts.
|
|
4391
|
+
*
|
|
4392
|
+
* Defaults `strokeWidth` to this path's own {@link strokeWidth} (which is `0` when
|
|
4393
|
+
* `style.stroke` is `'none'`). Each sub-path infers its own closed-ness unless `closed`
|
|
4394
|
+
* is given explicitly.
|
|
4395
|
+
*/
|
|
4396
|
+
isPointInStroke(point, options = {}) {
|
|
4397
|
+
const strokeWidth = options.strokeWidth ?? this.strokeWidth;
|
|
4398
|
+
const { tolerance = 0, closed } = options;
|
|
4399
|
+
return this.curves.some((curve) => curve.isPointInStroke(point, {
|
|
4400
|
+
strokeWidth,
|
|
4401
|
+
tolerance,
|
|
4402
|
+
closed
|
|
4403
|
+
}));
|
|
4404
|
+
}
|
|
4212
4405
|
getMinMax(min = Vector2.MAX, max = Vector2.MIN, withStyle = true) {
|
|
4213
4406
|
const strokeWidth = this.strokeWidth;
|
|
4214
4407
|
this.curves.forEach((curve) => {
|
|
@@ -4386,6 +4579,44 @@ class Path2DSet {
|
|
|
4386
4579
|
this.paths = paths;
|
|
4387
4580
|
this.viewBox = viewBox;
|
|
4388
4581
|
}
|
|
4582
|
+
/**
|
|
4583
|
+
* Test whether a point lies inside the filled area of any path in this set.
|
|
4584
|
+
* Purely geometric (ignores `fill: 'none'`); use {@link hitTest} for style-aware hits.
|
|
4585
|
+
*/
|
|
4586
|
+
isPointInFill(point, options = {}) {
|
|
4587
|
+
return this.paths.some((path) => path.isPointInFill(point, options));
|
|
4588
|
+
}
|
|
4589
|
+
/**
|
|
4590
|
+
* Concise PathKit-style fill containment test across the whole set; shorthand for
|
|
4591
|
+
* {@link isPointInFill} with a `{ x, y }` point.
|
|
4592
|
+
*/
|
|
4593
|
+
contains(x, y, options = {}) {
|
|
4594
|
+
return this.isPointInFill({ x, y }, options);
|
|
4595
|
+
}
|
|
4596
|
+
/**
|
|
4597
|
+
* Find the topmost path hit by a point, or `undefined` if none.
|
|
4598
|
+
*
|
|
4599
|
+
* Paths are tested top-to-bottom (last drawn first). For each path a fill hit is checked
|
|
4600
|
+
* first (skipped when `style.fill` is `'none'`), then — if `stroke` is enabled — a stroke
|
|
4601
|
+
* hit (skipped when `style.stroke` is `'none'`). This honors the "fill: none falls back to
|
|
4602
|
+
* stroke" rule; the coordinate space of `point` must match the paths (no scaling assumed).
|
|
4603
|
+
*
|
|
4604
|
+
* Options: `stroke` (also test strokes, default `true`), `tolerance` (extra stroke hit slack
|
|
4605
|
+
* in path units, default `0`), and `fillRule` (overrides each path's own fill rule).
|
|
4606
|
+
*/
|
|
4607
|
+
hitTest(point, options = {}) {
|
|
4608
|
+
const { stroke = true, tolerance, fillRule } = options;
|
|
4609
|
+
for (let i = this.paths.length - 1; i >= 0; i--) {
|
|
4610
|
+
const path = this.paths[i];
|
|
4611
|
+
if ((path.style.fill ?? "#000") !== "none" && path.isPointInFill(point, { fillRule })) {
|
|
4612
|
+
return path;
|
|
4613
|
+
}
|
|
4614
|
+
if (stroke && (path.style.stroke ?? "none") !== "none" && path.isPointInStroke(point, { tolerance })) {
|
|
4615
|
+
return path;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4618
|
+
return void 0;
|
|
4619
|
+
}
|
|
4389
4620
|
getBoundingBox(withStyle = true) {
|
|
4390
4621
|
if (!this.paths.length) {
|
|
4391
4622
|
return void 0;
|
|
@@ -4541,4 +4772,4 @@ function applyFFD(point, grid, width = grid.width, height = grid.height) {
|
|
|
4541
4772
|
point.set(x, y);
|
|
4542
4773
|
}
|
|
4543
4774
|
|
|
4544
|
-
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, quadraticBezier, setCanvasContext, strokeTriangulate, svgPathCommandsAddToPath2D, svgPathCommandsToData, svgPathDataToCommands, svgToDom, svgToPath2DSet, toKebabCase };
|
|
4775
|
+
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 };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modern-path2d",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.1",
|
|
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",
|