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.d.cts CHANGED
@@ -249,6 +249,20 @@ declare class Transform2D {
249
249
  destroy(): void;
250
250
  }
251
251
 
252
+ type BooleanOp = 'union' | 'intersection' | 'difference' | 'xor';
253
+ /**
254
+ * A flat ring: `[x0, y0, x1, y1, …]` (the same layout {@link Curve.getAdaptiveVertices} produces).
255
+ * A path is described as an array of such rings (one per sub-path).
256
+ */
257
+ type FlatRing = number[];
258
+ /**
259
+ * Boolean operation between two ring soups, returning the result as flat rings (shells wound
260
+ * counter-clockwise, holes clockwise — so a nonzero fill renders them correctly). Powered by the
261
+ * Martinez–Rueda sweep-line clipper (`polygon-clipping`); because the inputs are sampled outlines,
262
+ * the result is a polygonal approximation of curved paths.
263
+ */
264
+ declare function polygonBoolean(op: BooleanOp, ringsA: FlatRing[], ringsB: FlatRing[]): FlatRing[];
265
+
252
266
  declare function catmullRom(t: number, p0: number, p1: number, p2: number, p3: number): number;
253
267
 
254
268
  interface CssFunctionArg {
@@ -375,6 +389,12 @@ interface LineStyle {
375
389
  cap: LineCap;
376
390
  miterLimit: number;
377
391
  }
392
+ /**
393
+ * Derive a triangulator {@link LineStyle} from a (partial) {@link Path2DStyle}. This is what
394
+ * makes `path.strokeTriangulate()` honor `style.strokeWidth` / `strokeLinejoin` / `strokeLinecap`
395
+ * / `strokeMiterlimit` instead of silently falling back to a 1px miter hairline.
396
+ */
397
+ declare function resolveLineStyle(style?: Partial<Path2DStyle>): LineStyle;
378
398
  declare function strokeTriangulate(points: number[], options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
379
399
 
380
400
  interface IsPointInFillOptions {
@@ -413,6 +433,13 @@ declare abstract class Curve {
413
433
  getPointAt(u: number, output?: Vector2): Vector2;
414
434
  isClockwise(): boolean;
415
435
  getControlPointRefs(): Vector2[];
436
+ /**
437
+ * Reverse the traversal direction in place (start ↔ end, same geometry). The base
438
+ * implementation reverses the order of the control-point *values*, which is correct for
439
+ * line / Bézier / spline primitives whose {@link getControlPointRefs} order matches their
440
+ * parametric order. {@link RoundCurve} (angle-based) and composites (child order) override it.
441
+ */
442
+ reverse(): this;
416
443
  applyTransform(transform: Transform2D | ((point: Vector2) => void)): this;
417
444
  getUnevenVertices(count?: number, output?: number[]): number[];
418
445
  getSpacedVertices(count?: number, output?: number[]): number[];
@@ -428,6 +455,16 @@ declare abstract class Curve {
428
455
  getUToTMapping(u: number, distance?: number): number;
429
456
  getTangent(t: number, output?: Vector2): Vector2;
430
457
  getTangentAt(u: number, output?: Vector2): Vector2;
458
+ /**
459
+ * PathKit-style sample at an absolute arc-length `distance` along the curve: the point, the unit
460
+ * tangent, and the tangent `angle` in radians. `distance` is clamped to `[0, getLength()]`, so
461
+ * passing `0`/`getLength()` always yields the endpoints. See {@link PathMeasure} for a wrapper.
462
+ */
463
+ getPosTan(distance: number): {
464
+ position: Vector2;
465
+ tangent: Vector2;
466
+ angle: number;
467
+ };
431
468
  getNormal(t: number, output?: Vector2): Vector2;
432
469
  getNormalAt(u: number, output?: Vector2): Vector2;
433
470
  getTForPoint(target: Vector2Like, epsilon?: number): number;
@@ -464,6 +501,13 @@ declare abstract class Curve {
464
501
  contains(x: number, y: number, options?: IsPointInFillOptions): boolean;
465
502
  getFillVertices(_options?: FillTriangulateOptions): number[];
466
503
  fillTriangulate(options?: FillTriangulateOptions): FillTriangulatedResult;
504
+ /**
505
+ * Whether this curve forms a closed loop (its outline should be stroked without end caps,
506
+ * stitching the last vertex back to the first). The base test is purely geometric — the first
507
+ * sampled vertex coincides with the last. Curves that close without a duplicated endpoint
508
+ * (a full-revolution {@link RoundCurve}, rectangles, polygons) override this.
509
+ */
510
+ isClosed(): boolean;
467
511
  strokeTriangulate(options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
468
512
  toCommands(): Path2DCommand[];
469
513
  toData(): Path2DData;
@@ -494,6 +538,13 @@ declare class RoundCurve extends Curve {
494
538
  set dy(val: number);
495
539
  constructor(_center?: Vector2, _radius?: Vector2, _diff?: Vector2, rotate?: number, startAngle?: number, endAngle?: number, clockwise?: boolean);
496
540
  isClockwise(): boolean;
541
+ /**
542
+ * A circle/ellipse arc is closed when it sweeps (at least) a full revolution — the sampled
543
+ * outline does not duplicate the start vertex, so the geometric first==last test in the base
544
+ * class would wrongly report a full circle as open and leave a seam gap in the stroke.
545
+ */
546
+ isClosed(): boolean;
547
+ reverse(): this;
497
548
  protected _getDeltaAngle(): number;
498
549
  getPoint(t: number, output?: Vector2): Vector2;
499
550
  /**
@@ -541,7 +592,14 @@ declare class CompositeCurve<T extends Curve = Curve> extends Curve {
541
592
  protected _removeNextPointIfEqualPrevPoint(output: number[], offset: number): number[];
542
593
  getSpacedVertices(count?: number, output?: number[]): number[];
543
594
  getAdaptiveVertices(output?: number[]): number[];
595
+ /**
596
+ * A composite is closed when its single child is closed (e.g. a lone full-circle arc), or when
597
+ * its assembled outline returns to its start (rectangles, polygons, multi-segment loops).
598
+ */
599
+ isClosed(): boolean;
544
600
  strokeTriangulate(options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
601
+ /** Reverse the sub-curve order and reverse each sub-curve, so the whole outline runs backwards. */
602
+ reverse(): this;
545
603
  getFillVertices(options?: FillTriangulateOptions): number[];
546
604
  applyTransform(transform: Transform2D | ((point: Vector2) => void)): this;
547
605
  getMinMax(min?: Vector2, max?: Vector2): {
@@ -564,6 +622,7 @@ declare class CubicBezierCurve extends Curve {
564
622
  getPoint(t: number, output?: Vector2): Vector2;
565
623
  getAdaptiveVertices(output?: number[]): number[];
566
624
  getControlPointRefs(): Vector2[];
625
+ reverse(): this;
567
626
  protected _solveQuadratic(a: number, b: number, c: number): number[];
568
627
  getMinMax(min?: Vector2, max?: Vector2): {
569
628
  min: Vector2;
@@ -589,6 +648,7 @@ declare class LineCurve extends Curve {
589
648
  getTangent(_t: number, output?: Vector2): Vector2;
590
649
  getTangentAt(u: number, output?: Vector2): Vector2;
591
650
  getControlPointRefs(): Vector2[];
651
+ reverse(): this;
592
652
  getAdaptiveVertices(output?: number[]): number[];
593
653
  getMinMax(min?: Vector2, max?: Vector2): {
594
654
  min: Vector2;
@@ -621,6 +681,7 @@ declare class QuadraticBezierCurve extends Curve {
621
681
  constructor(p1?: Vector2, cp?: Vector2, p2?: Vector2);
622
682
  getPoint(t: number, output?: Vector2): Vector2;
623
683
  getControlPointRefs(): Vector2[];
684
+ reverse(): this;
624
685
  getAdaptiveVertices(output?: number[]): number[];
625
686
  getMinMax(min?: Vector2, max?: Vector2): {
626
687
  min: Vector2;
@@ -665,6 +726,7 @@ declare class SplineCurve extends Curve {
665
726
  constructor(points?: Vector2[]);
666
727
  getPoint(t: number, output?: Vector2): Vector2;
667
728
  getControlPointRefs(): Vector2[];
729
+ reverse(): this;
668
730
  copyFrom(source: SplineCurve): this;
669
731
  }
670
732
 
@@ -676,6 +738,13 @@ declare class CurvePath extends CompositeCurve {
676
738
  addPoints(points: Vector2[]): this;
677
739
  addCommands(commands: Path2DCommand[]): this;
678
740
  addData(data: string): this;
741
+ /**
742
+ * A sub-path is closed if it was explicitly closed (`autoClose`, i.e. a `Z`/`closePath`), or if
743
+ * it forms a geometric loop / wraps a single closed primitive (handled by the base class).
744
+ */
745
+ isClosed(): boolean;
746
+ /** Reverse direction, then refresh the {@link startPoint}/{@link currentPoint} cursors. */
747
+ reverse(): this;
679
748
  protected _closeVertices(output: number[]): number[];
680
749
  getUnevenVertices(count?: number, output?: number[]): number[];
681
750
  getSpacedVertices(count?: number, output?: number[]): number[];
@@ -760,6 +829,23 @@ declare class Path2D<T = any> extends CompositeCurve<CurvePath> {
760
829
  /** Per-sub-path sampled rings, cached for repeated hit tests. */
761
830
  protected _getRings(): number[][];
762
831
  isPointInFill(point: Vector2Like, options?: IsPointInFillOptions): boolean;
832
+ /** Build a `Path2D` from flat rings (`[x0,y0,…]` per sub-path); closed-and-filled as sub-paths. */
833
+ static fromRings(rings: number[][], style?: Partial<Path2DStyle>): Path2D;
834
+ /**
835
+ * Boolean (path) operation against another path, returning a NEW `Path2D` whose outline is the
836
+ * polygonal result. Curves are sampled before clipping, so the result is a polygonal
837
+ * approximation (see {@link polygonBoolean}). The result inherits this path's `style` unless
838
+ * overridden via `style`. Holes are emitted as oppositely-wound sub-paths (nonzero fill).
839
+ */
840
+ booleanOp(op: BooleanOp, other: Path2D, style?: Partial<Path2DStyle>): Path2D;
841
+ /** `this ∪ other` — the combined filled area. */
842
+ union(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
843
+ /** `this ∩ other` — only the overlapping area. */
844
+ intersection(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
845
+ /** `this − other` — this path with `other` cut away. */
846
+ difference(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
847
+ /** `this ⊕ other` — areas covered by exactly one of the two paths. */
848
+ xor(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
763
849
  /**
764
850
  * Test whether a point lies on this path's stroke. A hit on any sub-path counts.
765
851
  *
@@ -833,6 +919,40 @@ declare class Path2DSet<T = any> {
833
919
  }>): HTMLCanvasElement;
834
920
  }
835
921
 
922
+ interface PosTan {
923
+ position: Vector2;
924
+ tangent: Vector2;
925
+ angle: number;
926
+ }
927
+ /**
928
+ * PathKit/Skia-style arc-length measurement over any {@link Curve} (including `CurvePath` and
929
+ * `Path2D`). Wraps the curve's existing arc-length cache, so repeated queries are cheap.
930
+ *
931
+ * ```ts
932
+ * const measure = new PathMeasure(path)
933
+ * const { position, angle } = measure.getPosTan(measure.getLength() * progress)
934
+ * ```
935
+ */
936
+ declare class PathMeasure {
937
+ curve: Curve;
938
+ constructor(curve: Curve);
939
+ /** Total arc length of the path. */
940
+ getLength(): number;
941
+ /** Whether the path forms a closed loop (see {@link Curve.isClosed}). */
942
+ isClosed(): boolean;
943
+ /** Point + unit tangent + tangent angle at an absolute arc-length `distance` (clamped). */
944
+ getPosTan(distance: number): PosTan;
945
+ /** Point at an absolute arc-length `distance` (clamped to `[0, getLength()]`). */
946
+ getPosition(distance: number): Vector2;
947
+ /** Point + tangent at a normalized progress `t ∈ [0, 1]` along the path. */
948
+ getPosTanAtProgress(t: number): PosTan;
949
+ /**
950
+ * Evenly sample the path into `count + 1` {@link PosTan} entries (arc-length spaced), e.g. to
951
+ * lay glyphs along a path or drive an `animate(progress)`-style traversal.
952
+ */
953
+ sample(count?: number): PosTan[];
954
+ }
955
+
836
956
  declare class FFDControlGrid {
837
957
  rows: number;
838
958
  cols: number;
@@ -875,5 +995,5 @@ declare function svgToDom(svg: string | SVGElement): SVGElement;
875
995
 
876
996
  declare function svgToPath2DSet(svg: string | SVGElement): Path2DSet;
877
997
 
878
- 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 };
879
- export type { CssFunction, CssFunctionArg, DrawPointOptions, FillRule, FillTriangulateOptions, FillTriangulatedResult, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };
998
+ 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 };
999
+ export type { BooleanOp, CssFunction, CssFunctionArg, DrawPointOptions, FillRule, FillTriangulateOptions, FillTriangulatedResult, FlatRing, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, PosTan, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };
package/dist/index.d.mts CHANGED
@@ -249,6 +249,20 @@ declare class Transform2D {
249
249
  destroy(): void;
250
250
  }
251
251
 
252
+ type BooleanOp = 'union' | 'intersection' | 'difference' | 'xor';
253
+ /**
254
+ * A flat ring: `[x0, y0, x1, y1, …]` (the same layout {@link Curve.getAdaptiveVertices} produces).
255
+ * A path is described as an array of such rings (one per sub-path).
256
+ */
257
+ type FlatRing = number[];
258
+ /**
259
+ * Boolean operation between two ring soups, returning the result as flat rings (shells wound
260
+ * counter-clockwise, holes clockwise — so a nonzero fill renders them correctly). Powered by the
261
+ * Martinez–Rueda sweep-line clipper (`polygon-clipping`); because the inputs are sampled outlines,
262
+ * the result is a polygonal approximation of curved paths.
263
+ */
264
+ declare function polygonBoolean(op: BooleanOp, ringsA: FlatRing[], ringsB: FlatRing[]): FlatRing[];
265
+
252
266
  declare function catmullRom(t: number, p0: number, p1: number, p2: number, p3: number): number;
253
267
 
254
268
  interface CssFunctionArg {
@@ -375,6 +389,12 @@ interface LineStyle {
375
389
  cap: LineCap;
376
390
  miterLimit: number;
377
391
  }
392
+ /**
393
+ * Derive a triangulator {@link LineStyle} from a (partial) {@link Path2DStyle}. This is what
394
+ * makes `path.strokeTriangulate()` honor `style.strokeWidth` / `strokeLinejoin` / `strokeLinecap`
395
+ * / `strokeMiterlimit` instead of silently falling back to a 1px miter hairline.
396
+ */
397
+ declare function resolveLineStyle(style?: Partial<Path2DStyle>): LineStyle;
378
398
  declare function strokeTriangulate(points: number[], options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
379
399
 
380
400
  interface IsPointInFillOptions {
@@ -413,6 +433,13 @@ declare abstract class Curve {
413
433
  getPointAt(u: number, output?: Vector2): Vector2;
414
434
  isClockwise(): boolean;
415
435
  getControlPointRefs(): Vector2[];
436
+ /**
437
+ * Reverse the traversal direction in place (start ↔ end, same geometry). The base
438
+ * implementation reverses the order of the control-point *values*, which is correct for
439
+ * line / Bézier / spline primitives whose {@link getControlPointRefs} order matches their
440
+ * parametric order. {@link RoundCurve} (angle-based) and composites (child order) override it.
441
+ */
442
+ reverse(): this;
416
443
  applyTransform(transform: Transform2D | ((point: Vector2) => void)): this;
417
444
  getUnevenVertices(count?: number, output?: number[]): number[];
418
445
  getSpacedVertices(count?: number, output?: number[]): number[];
@@ -428,6 +455,16 @@ declare abstract class Curve {
428
455
  getUToTMapping(u: number, distance?: number): number;
429
456
  getTangent(t: number, output?: Vector2): Vector2;
430
457
  getTangentAt(u: number, output?: Vector2): Vector2;
458
+ /**
459
+ * PathKit-style sample at an absolute arc-length `distance` along the curve: the point, the unit
460
+ * tangent, and the tangent `angle` in radians. `distance` is clamped to `[0, getLength()]`, so
461
+ * passing `0`/`getLength()` always yields the endpoints. See {@link PathMeasure} for a wrapper.
462
+ */
463
+ getPosTan(distance: number): {
464
+ position: Vector2;
465
+ tangent: Vector2;
466
+ angle: number;
467
+ };
431
468
  getNormal(t: number, output?: Vector2): Vector2;
432
469
  getNormalAt(u: number, output?: Vector2): Vector2;
433
470
  getTForPoint(target: Vector2Like, epsilon?: number): number;
@@ -464,6 +501,13 @@ declare abstract class Curve {
464
501
  contains(x: number, y: number, options?: IsPointInFillOptions): boolean;
465
502
  getFillVertices(_options?: FillTriangulateOptions): number[];
466
503
  fillTriangulate(options?: FillTriangulateOptions): FillTriangulatedResult;
504
+ /**
505
+ * Whether this curve forms a closed loop (its outline should be stroked without end caps,
506
+ * stitching the last vertex back to the first). The base test is purely geometric — the first
507
+ * sampled vertex coincides with the last. Curves that close without a duplicated endpoint
508
+ * (a full-revolution {@link RoundCurve}, rectangles, polygons) override this.
509
+ */
510
+ isClosed(): boolean;
467
511
  strokeTriangulate(options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
468
512
  toCommands(): Path2DCommand[];
469
513
  toData(): Path2DData;
@@ -494,6 +538,13 @@ declare class RoundCurve extends Curve {
494
538
  set dy(val: number);
495
539
  constructor(_center?: Vector2, _radius?: Vector2, _diff?: Vector2, rotate?: number, startAngle?: number, endAngle?: number, clockwise?: boolean);
496
540
  isClockwise(): boolean;
541
+ /**
542
+ * A circle/ellipse arc is closed when it sweeps (at least) a full revolution — the sampled
543
+ * outline does not duplicate the start vertex, so the geometric first==last test in the base
544
+ * class would wrongly report a full circle as open and leave a seam gap in the stroke.
545
+ */
546
+ isClosed(): boolean;
547
+ reverse(): this;
497
548
  protected _getDeltaAngle(): number;
498
549
  getPoint(t: number, output?: Vector2): Vector2;
499
550
  /**
@@ -541,7 +592,14 @@ declare class CompositeCurve<T extends Curve = Curve> extends Curve {
541
592
  protected _removeNextPointIfEqualPrevPoint(output: number[], offset: number): number[];
542
593
  getSpacedVertices(count?: number, output?: number[]): number[];
543
594
  getAdaptiveVertices(output?: number[]): number[];
595
+ /**
596
+ * A composite is closed when its single child is closed (e.g. a lone full-circle arc), or when
597
+ * its assembled outline returns to its start (rectangles, polygons, multi-segment loops).
598
+ */
599
+ isClosed(): boolean;
544
600
  strokeTriangulate(options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
601
+ /** Reverse the sub-curve order and reverse each sub-curve, so the whole outline runs backwards. */
602
+ reverse(): this;
545
603
  getFillVertices(options?: FillTriangulateOptions): number[];
546
604
  applyTransform(transform: Transform2D | ((point: Vector2) => void)): this;
547
605
  getMinMax(min?: Vector2, max?: Vector2): {
@@ -564,6 +622,7 @@ declare class CubicBezierCurve extends Curve {
564
622
  getPoint(t: number, output?: Vector2): Vector2;
565
623
  getAdaptiveVertices(output?: number[]): number[];
566
624
  getControlPointRefs(): Vector2[];
625
+ reverse(): this;
567
626
  protected _solveQuadratic(a: number, b: number, c: number): number[];
568
627
  getMinMax(min?: Vector2, max?: Vector2): {
569
628
  min: Vector2;
@@ -589,6 +648,7 @@ declare class LineCurve extends Curve {
589
648
  getTangent(_t: number, output?: Vector2): Vector2;
590
649
  getTangentAt(u: number, output?: Vector2): Vector2;
591
650
  getControlPointRefs(): Vector2[];
651
+ reverse(): this;
592
652
  getAdaptiveVertices(output?: number[]): number[];
593
653
  getMinMax(min?: Vector2, max?: Vector2): {
594
654
  min: Vector2;
@@ -621,6 +681,7 @@ declare class QuadraticBezierCurve extends Curve {
621
681
  constructor(p1?: Vector2, cp?: Vector2, p2?: Vector2);
622
682
  getPoint(t: number, output?: Vector2): Vector2;
623
683
  getControlPointRefs(): Vector2[];
684
+ reverse(): this;
624
685
  getAdaptiveVertices(output?: number[]): number[];
625
686
  getMinMax(min?: Vector2, max?: Vector2): {
626
687
  min: Vector2;
@@ -665,6 +726,7 @@ declare class SplineCurve extends Curve {
665
726
  constructor(points?: Vector2[]);
666
727
  getPoint(t: number, output?: Vector2): Vector2;
667
728
  getControlPointRefs(): Vector2[];
729
+ reverse(): this;
668
730
  copyFrom(source: SplineCurve): this;
669
731
  }
670
732
 
@@ -676,6 +738,13 @@ declare class CurvePath extends CompositeCurve {
676
738
  addPoints(points: Vector2[]): this;
677
739
  addCommands(commands: Path2DCommand[]): this;
678
740
  addData(data: string): this;
741
+ /**
742
+ * A sub-path is closed if it was explicitly closed (`autoClose`, i.e. a `Z`/`closePath`), or if
743
+ * it forms a geometric loop / wraps a single closed primitive (handled by the base class).
744
+ */
745
+ isClosed(): boolean;
746
+ /** Reverse direction, then refresh the {@link startPoint}/{@link currentPoint} cursors. */
747
+ reverse(): this;
679
748
  protected _closeVertices(output: number[]): number[];
680
749
  getUnevenVertices(count?: number, output?: number[]): number[];
681
750
  getSpacedVertices(count?: number, output?: number[]): number[];
@@ -760,6 +829,23 @@ declare class Path2D<T = any> extends CompositeCurve<CurvePath> {
760
829
  /** Per-sub-path sampled rings, cached for repeated hit tests. */
761
830
  protected _getRings(): number[][];
762
831
  isPointInFill(point: Vector2Like, options?: IsPointInFillOptions): boolean;
832
+ /** Build a `Path2D` from flat rings (`[x0,y0,…]` per sub-path); closed-and-filled as sub-paths. */
833
+ static fromRings(rings: number[][], style?: Partial<Path2DStyle>): Path2D;
834
+ /**
835
+ * Boolean (path) operation against another path, returning a NEW `Path2D` whose outline is the
836
+ * polygonal result. Curves are sampled before clipping, so the result is a polygonal
837
+ * approximation (see {@link polygonBoolean}). The result inherits this path's `style` unless
838
+ * overridden via `style`. Holes are emitted as oppositely-wound sub-paths (nonzero fill).
839
+ */
840
+ booleanOp(op: BooleanOp, other: Path2D, style?: Partial<Path2DStyle>): Path2D;
841
+ /** `this ∪ other` — the combined filled area. */
842
+ union(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
843
+ /** `this ∩ other` — only the overlapping area. */
844
+ intersection(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
845
+ /** `this − other` — this path with `other` cut away. */
846
+ difference(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
847
+ /** `this ⊕ other` — areas covered by exactly one of the two paths. */
848
+ xor(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
763
849
  /**
764
850
  * Test whether a point lies on this path's stroke. A hit on any sub-path counts.
765
851
  *
@@ -833,6 +919,40 @@ declare class Path2DSet<T = any> {
833
919
  }>): HTMLCanvasElement;
834
920
  }
835
921
 
922
+ interface PosTan {
923
+ position: Vector2;
924
+ tangent: Vector2;
925
+ angle: number;
926
+ }
927
+ /**
928
+ * PathKit/Skia-style arc-length measurement over any {@link Curve} (including `CurvePath` and
929
+ * `Path2D`). Wraps the curve's existing arc-length cache, so repeated queries are cheap.
930
+ *
931
+ * ```ts
932
+ * const measure = new PathMeasure(path)
933
+ * const { position, angle } = measure.getPosTan(measure.getLength() * progress)
934
+ * ```
935
+ */
936
+ declare class PathMeasure {
937
+ curve: Curve;
938
+ constructor(curve: Curve);
939
+ /** Total arc length of the path. */
940
+ getLength(): number;
941
+ /** Whether the path forms a closed loop (see {@link Curve.isClosed}). */
942
+ isClosed(): boolean;
943
+ /** Point + unit tangent + tangent angle at an absolute arc-length `distance` (clamped). */
944
+ getPosTan(distance: number): PosTan;
945
+ /** Point at an absolute arc-length `distance` (clamped to `[0, getLength()]`). */
946
+ getPosition(distance: number): Vector2;
947
+ /** Point + tangent at a normalized progress `t ∈ [0, 1]` along the path. */
948
+ getPosTanAtProgress(t: number): PosTan;
949
+ /**
950
+ * Evenly sample the path into `count + 1` {@link PosTan} entries (arc-length spaced), e.g. to
951
+ * lay glyphs along a path or drive an `animate(progress)`-style traversal.
952
+ */
953
+ sample(count?: number): PosTan[];
954
+ }
955
+
836
956
  declare class FFDControlGrid {
837
957
  rows: number;
838
958
  cols: number;
@@ -875,5 +995,5 @@ declare function svgToDom(svg: string | SVGElement): SVGElement;
875
995
 
876
996
  declare function svgToPath2DSet(svg: string | SVGElement): Path2DSet;
877
997
 
878
- 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 };
879
- export type { CssFunction, CssFunctionArg, DrawPointOptions, FillRule, FillTriangulateOptions, FillTriangulatedResult, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };
998
+ 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 };
999
+ export type { BooleanOp, CssFunction, CssFunctionArg, DrawPointOptions, FillRule, FillTriangulateOptions, FillTriangulatedResult, FlatRing, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, PosTan, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };
package/dist/index.d.ts CHANGED
@@ -249,6 +249,20 @@ declare class Transform2D {
249
249
  destroy(): void;
250
250
  }
251
251
 
252
+ type BooleanOp = 'union' | 'intersection' | 'difference' | 'xor';
253
+ /**
254
+ * A flat ring: `[x0, y0, x1, y1, …]` (the same layout {@link Curve.getAdaptiveVertices} produces).
255
+ * A path is described as an array of such rings (one per sub-path).
256
+ */
257
+ type FlatRing = number[];
258
+ /**
259
+ * Boolean operation between two ring soups, returning the result as flat rings (shells wound
260
+ * counter-clockwise, holes clockwise — so a nonzero fill renders them correctly). Powered by the
261
+ * Martinez–Rueda sweep-line clipper (`polygon-clipping`); because the inputs are sampled outlines,
262
+ * the result is a polygonal approximation of curved paths.
263
+ */
264
+ declare function polygonBoolean(op: BooleanOp, ringsA: FlatRing[], ringsB: FlatRing[]): FlatRing[];
265
+
252
266
  declare function catmullRom(t: number, p0: number, p1: number, p2: number, p3: number): number;
253
267
 
254
268
  interface CssFunctionArg {
@@ -375,6 +389,12 @@ interface LineStyle {
375
389
  cap: LineCap;
376
390
  miterLimit: number;
377
391
  }
392
+ /**
393
+ * Derive a triangulator {@link LineStyle} from a (partial) {@link Path2DStyle}. This is what
394
+ * makes `path.strokeTriangulate()` honor `style.strokeWidth` / `strokeLinejoin` / `strokeLinecap`
395
+ * / `strokeMiterlimit` instead of silently falling back to a 1px miter hairline.
396
+ */
397
+ declare function resolveLineStyle(style?: Partial<Path2DStyle>): LineStyle;
378
398
  declare function strokeTriangulate(points: number[], options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
379
399
 
380
400
  interface IsPointInFillOptions {
@@ -413,6 +433,13 @@ declare abstract class Curve {
413
433
  getPointAt(u: number, output?: Vector2): Vector2;
414
434
  isClockwise(): boolean;
415
435
  getControlPointRefs(): Vector2[];
436
+ /**
437
+ * Reverse the traversal direction in place (start ↔ end, same geometry). The base
438
+ * implementation reverses the order of the control-point *values*, which is correct for
439
+ * line / Bézier / spline primitives whose {@link getControlPointRefs} order matches their
440
+ * parametric order. {@link RoundCurve} (angle-based) and composites (child order) override it.
441
+ */
442
+ reverse(): this;
416
443
  applyTransform(transform: Transform2D | ((point: Vector2) => void)): this;
417
444
  getUnevenVertices(count?: number, output?: number[]): number[];
418
445
  getSpacedVertices(count?: number, output?: number[]): number[];
@@ -428,6 +455,16 @@ declare abstract class Curve {
428
455
  getUToTMapping(u: number, distance?: number): number;
429
456
  getTangent(t: number, output?: Vector2): Vector2;
430
457
  getTangentAt(u: number, output?: Vector2): Vector2;
458
+ /**
459
+ * PathKit-style sample at an absolute arc-length `distance` along the curve: the point, the unit
460
+ * tangent, and the tangent `angle` in radians. `distance` is clamped to `[0, getLength()]`, so
461
+ * passing `0`/`getLength()` always yields the endpoints. See {@link PathMeasure} for a wrapper.
462
+ */
463
+ getPosTan(distance: number): {
464
+ position: Vector2;
465
+ tangent: Vector2;
466
+ angle: number;
467
+ };
431
468
  getNormal(t: number, output?: Vector2): Vector2;
432
469
  getNormalAt(u: number, output?: Vector2): Vector2;
433
470
  getTForPoint(target: Vector2Like, epsilon?: number): number;
@@ -464,6 +501,13 @@ declare abstract class Curve {
464
501
  contains(x: number, y: number, options?: IsPointInFillOptions): boolean;
465
502
  getFillVertices(_options?: FillTriangulateOptions): number[];
466
503
  fillTriangulate(options?: FillTriangulateOptions): FillTriangulatedResult;
504
+ /**
505
+ * Whether this curve forms a closed loop (its outline should be stroked without end caps,
506
+ * stitching the last vertex back to the first). The base test is purely geometric — the first
507
+ * sampled vertex coincides with the last. Curves that close without a duplicated endpoint
508
+ * (a full-revolution {@link RoundCurve}, rectangles, polygons) override this.
509
+ */
510
+ isClosed(): boolean;
467
511
  strokeTriangulate(options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
468
512
  toCommands(): Path2DCommand[];
469
513
  toData(): Path2DData;
@@ -494,6 +538,13 @@ declare class RoundCurve extends Curve {
494
538
  set dy(val: number);
495
539
  constructor(_center?: Vector2, _radius?: Vector2, _diff?: Vector2, rotate?: number, startAngle?: number, endAngle?: number, clockwise?: boolean);
496
540
  isClockwise(): boolean;
541
+ /**
542
+ * A circle/ellipse arc is closed when it sweeps (at least) a full revolution — the sampled
543
+ * outline does not duplicate the start vertex, so the geometric first==last test in the base
544
+ * class would wrongly report a full circle as open and leave a seam gap in the stroke.
545
+ */
546
+ isClosed(): boolean;
547
+ reverse(): this;
497
548
  protected _getDeltaAngle(): number;
498
549
  getPoint(t: number, output?: Vector2): Vector2;
499
550
  /**
@@ -541,7 +592,14 @@ declare class CompositeCurve<T extends Curve = Curve> extends Curve {
541
592
  protected _removeNextPointIfEqualPrevPoint(output: number[], offset: number): number[];
542
593
  getSpacedVertices(count?: number, output?: number[]): number[];
543
594
  getAdaptiveVertices(output?: number[]): number[];
595
+ /**
596
+ * A composite is closed when its single child is closed (e.g. a lone full-circle arc), or when
597
+ * its assembled outline returns to its start (rectangles, polygons, multi-segment loops).
598
+ */
599
+ isClosed(): boolean;
544
600
  strokeTriangulate(options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
601
+ /** Reverse the sub-curve order and reverse each sub-curve, so the whole outline runs backwards. */
602
+ reverse(): this;
545
603
  getFillVertices(options?: FillTriangulateOptions): number[];
546
604
  applyTransform(transform: Transform2D | ((point: Vector2) => void)): this;
547
605
  getMinMax(min?: Vector2, max?: Vector2): {
@@ -564,6 +622,7 @@ declare class CubicBezierCurve extends Curve {
564
622
  getPoint(t: number, output?: Vector2): Vector2;
565
623
  getAdaptiveVertices(output?: number[]): number[];
566
624
  getControlPointRefs(): Vector2[];
625
+ reverse(): this;
567
626
  protected _solveQuadratic(a: number, b: number, c: number): number[];
568
627
  getMinMax(min?: Vector2, max?: Vector2): {
569
628
  min: Vector2;
@@ -589,6 +648,7 @@ declare class LineCurve extends Curve {
589
648
  getTangent(_t: number, output?: Vector2): Vector2;
590
649
  getTangentAt(u: number, output?: Vector2): Vector2;
591
650
  getControlPointRefs(): Vector2[];
651
+ reverse(): this;
592
652
  getAdaptiveVertices(output?: number[]): number[];
593
653
  getMinMax(min?: Vector2, max?: Vector2): {
594
654
  min: Vector2;
@@ -621,6 +681,7 @@ declare class QuadraticBezierCurve extends Curve {
621
681
  constructor(p1?: Vector2, cp?: Vector2, p2?: Vector2);
622
682
  getPoint(t: number, output?: Vector2): Vector2;
623
683
  getControlPointRefs(): Vector2[];
684
+ reverse(): this;
624
685
  getAdaptiveVertices(output?: number[]): number[];
625
686
  getMinMax(min?: Vector2, max?: Vector2): {
626
687
  min: Vector2;
@@ -665,6 +726,7 @@ declare class SplineCurve extends Curve {
665
726
  constructor(points?: Vector2[]);
666
727
  getPoint(t: number, output?: Vector2): Vector2;
667
728
  getControlPointRefs(): Vector2[];
729
+ reverse(): this;
668
730
  copyFrom(source: SplineCurve): this;
669
731
  }
670
732
 
@@ -676,6 +738,13 @@ declare class CurvePath extends CompositeCurve {
676
738
  addPoints(points: Vector2[]): this;
677
739
  addCommands(commands: Path2DCommand[]): this;
678
740
  addData(data: string): this;
741
+ /**
742
+ * A sub-path is closed if it was explicitly closed (`autoClose`, i.e. a `Z`/`closePath`), or if
743
+ * it forms a geometric loop / wraps a single closed primitive (handled by the base class).
744
+ */
745
+ isClosed(): boolean;
746
+ /** Reverse direction, then refresh the {@link startPoint}/{@link currentPoint} cursors. */
747
+ reverse(): this;
679
748
  protected _closeVertices(output: number[]): number[];
680
749
  getUnevenVertices(count?: number, output?: number[]): number[];
681
750
  getSpacedVertices(count?: number, output?: number[]): number[];
@@ -760,6 +829,23 @@ declare class Path2D<T = any> extends CompositeCurve<CurvePath> {
760
829
  /** Per-sub-path sampled rings, cached for repeated hit tests. */
761
830
  protected _getRings(): number[][];
762
831
  isPointInFill(point: Vector2Like, options?: IsPointInFillOptions): boolean;
832
+ /** Build a `Path2D` from flat rings (`[x0,y0,…]` per sub-path); closed-and-filled as sub-paths. */
833
+ static fromRings(rings: number[][], style?: Partial<Path2DStyle>): Path2D;
834
+ /**
835
+ * Boolean (path) operation against another path, returning a NEW `Path2D` whose outline is the
836
+ * polygonal result. Curves are sampled before clipping, so the result is a polygonal
837
+ * approximation (see {@link polygonBoolean}). The result inherits this path's `style` unless
838
+ * overridden via `style`. Holes are emitted as oppositely-wound sub-paths (nonzero fill).
839
+ */
840
+ booleanOp(op: BooleanOp, other: Path2D, style?: Partial<Path2DStyle>): Path2D;
841
+ /** `this ∪ other` — the combined filled area. */
842
+ union(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
843
+ /** `this ∩ other` — only the overlapping area. */
844
+ intersection(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
845
+ /** `this − other` — this path with `other` cut away. */
846
+ difference(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
847
+ /** `this ⊕ other` — areas covered by exactly one of the two paths. */
848
+ xor(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
763
849
  /**
764
850
  * Test whether a point lies on this path's stroke. A hit on any sub-path counts.
765
851
  *
@@ -833,6 +919,40 @@ declare class Path2DSet<T = any> {
833
919
  }>): HTMLCanvasElement;
834
920
  }
835
921
 
922
+ interface PosTan {
923
+ position: Vector2;
924
+ tangent: Vector2;
925
+ angle: number;
926
+ }
927
+ /**
928
+ * PathKit/Skia-style arc-length measurement over any {@link Curve} (including `CurvePath` and
929
+ * `Path2D`). Wraps the curve's existing arc-length cache, so repeated queries are cheap.
930
+ *
931
+ * ```ts
932
+ * const measure = new PathMeasure(path)
933
+ * const { position, angle } = measure.getPosTan(measure.getLength() * progress)
934
+ * ```
935
+ */
936
+ declare class PathMeasure {
937
+ curve: Curve;
938
+ constructor(curve: Curve);
939
+ /** Total arc length of the path. */
940
+ getLength(): number;
941
+ /** Whether the path forms a closed loop (see {@link Curve.isClosed}). */
942
+ isClosed(): boolean;
943
+ /** Point + unit tangent + tangent angle at an absolute arc-length `distance` (clamped). */
944
+ getPosTan(distance: number): PosTan;
945
+ /** Point at an absolute arc-length `distance` (clamped to `[0, getLength()]`). */
946
+ getPosition(distance: number): Vector2;
947
+ /** Point + tangent at a normalized progress `t ∈ [0, 1]` along the path. */
948
+ getPosTanAtProgress(t: number): PosTan;
949
+ /**
950
+ * Evenly sample the path into `count + 1` {@link PosTan} entries (arc-length spaced), e.g. to
951
+ * lay glyphs along a path or drive an `animate(progress)`-style traversal.
952
+ */
953
+ sample(count?: number): PosTan[];
954
+ }
955
+
836
956
  declare class FFDControlGrid {
837
957
  rows: number;
838
958
  cols: number;
@@ -875,5 +995,5 @@ declare function svgToDom(svg: string | SVGElement): SVGElement;
875
995
 
876
996
  declare function svgToPath2DSet(svg: string | SVGElement): Path2DSet;
877
997
 
878
- 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 };
879
- export type { CssFunction, CssFunctionArg, DrawPointOptions, FillRule, FillTriangulateOptions, FillTriangulatedResult, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };
998
+ 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 };
999
+ export type { BooleanOp, CssFunction, CssFunctionArg, DrawPointOptions, FillRule, FillTriangulateOptions, FillTriangulatedResult, FlatRing, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, PosTan, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };