modern-path2d 1.7.0 → 1.8.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.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 {
@@ -274,6 +288,25 @@ declare function parseCssArg(name: string, value: string, context?: ParseCssFunc
274
288
 
275
289
  declare function cubicBezier(t: number, p0: number, p1: number, p2: number, p3: number): number;
276
290
 
291
+ interface EvenoddFillRuleResult {
292
+ index: number;
293
+ /** Containment depth: 0/2/4… are filled shells, 1/3/5… are holes. */
294
+ depth: number;
295
+ /** Immediate enclosing ring (deepest container), or -1 for a top-level ring. */
296
+ parentIndex: number;
297
+ }
298
+ /**
299
+ * Even-odd nesting of a ring soup: classify each ring by how many other rings contain it.
300
+ * Even depth → a filled shell; odd depth → a hole. Each ring also gets its immediate parent
301
+ * (the deepest container), so a shell can collect exactly the holes one level inside it — and
302
+ * an island inside a hole becomes its own shell again (nested donuts work).
303
+ *
304
+ * Mirrors {@link nonzeroFillRule}'s output shape, but uses containment parity (even-odd) instead
305
+ * of winding. Used by `Path2D.fillTriangulate` for `fillRule: 'evenodd'` so WebGL/triangulated
306
+ * fills get real holes instead of solid overlapping rings.
307
+ */
308
+ declare function evenoddFillRule(paths: number[][]): EvenoddFillRuleResult[];
309
+
277
310
  interface FillTriangulateOptions {
278
311
  holes?: number[];
279
312
  vertices?: number[];
@@ -375,6 +408,12 @@ interface LineStyle {
375
408
  cap: LineCap;
376
409
  miterLimit: number;
377
410
  }
411
+ /**
412
+ * Derive a triangulator {@link LineStyle} from a (partial) {@link Path2DStyle}. This is what
413
+ * makes `path.strokeTriangulate()` honor `style.strokeWidth` / `strokeLinejoin` / `strokeLinecap`
414
+ * / `strokeMiterlimit` instead of silently falling back to a 1px miter hairline.
415
+ */
416
+ declare function resolveLineStyle(style?: Partial<Path2DStyle>): LineStyle;
378
417
  declare function strokeTriangulate(points: number[], options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
379
418
 
380
419
  interface IsPointInFillOptions {
@@ -413,6 +452,13 @@ declare abstract class Curve {
413
452
  getPointAt(u: number, output?: Vector2): Vector2;
414
453
  isClockwise(): boolean;
415
454
  getControlPointRefs(): Vector2[];
455
+ /**
456
+ * Reverse the traversal direction in place (start ↔ end, same geometry). The base
457
+ * implementation reverses the order of the control-point *values*, which is correct for
458
+ * line / Bézier / spline primitives whose {@link getControlPointRefs} order matches their
459
+ * parametric order. {@link RoundCurve} (angle-based) and composites (child order) override it.
460
+ */
461
+ reverse(): this;
416
462
  applyTransform(transform: Transform2D | ((point: Vector2) => void)): this;
417
463
  getUnevenVertices(count?: number, output?: number[]): number[];
418
464
  getSpacedVertices(count?: number, output?: number[]): number[];
@@ -428,6 +474,16 @@ declare abstract class Curve {
428
474
  getUToTMapping(u: number, distance?: number): number;
429
475
  getTangent(t: number, output?: Vector2): Vector2;
430
476
  getTangentAt(u: number, output?: Vector2): Vector2;
477
+ /**
478
+ * PathKit-style sample at an absolute arc-length `distance` along the curve: the point, the unit
479
+ * tangent, and the tangent `angle` in radians. `distance` is clamped to `[0, getLength()]`, so
480
+ * passing `0`/`getLength()` always yields the endpoints. See {@link PathMeasure} for a wrapper.
481
+ */
482
+ getPosTan(distance: number): {
483
+ position: Vector2;
484
+ tangent: Vector2;
485
+ angle: number;
486
+ };
431
487
  getNormal(t: number, output?: Vector2): Vector2;
432
488
  getNormalAt(u: number, output?: Vector2): Vector2;
433
489
  getTForPoint(target: Vector2Like, epsilon?: number): number;
@@ -464,6 +520,13 @@ declare abstract class Curve {
464
520
  contains(x: number, y: number, options?: IsPointInFillOptions): boolean;
465
521
  getFillVertices(_options?: FillTriangulateOptions): number[];
466
522
  fillTriangulate(options?: FillTriangulateOptions): FillTriangulatedResult;
523
+ /**
524
+ * Whether this curve forms a closed loop (its outline should be stroked without end caps,
525
+ * stitching the last vertex back to the first). The base test is purely geometric — the first
526
+ * sampled vertex coincides with the last. Curves that close without a duplicated endpoint
527
+ * (a full-revolution {@link RoundCurve}, rectangles, polygons) override this.
528
+ */
529
+ isClosed(): boolean;
467
530
  strokeTriangulate(options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
468
531
  toCommands(): Path2DCommand[];
469
532
  toData(): Path2DData;
@@ -494,6 +557,13 @@ declare class RoundCurve extends Curve {
494
557
  set dy(val: number);
495
558
  constructor(_center?: Vector2, _radius?: Vector2, _diff?: Vector2, rotate?: number, startAngle?: number, endAngle?: number, clockwise?: boolean);
496
559
  isClockwise(): boolean;
560
+ /**
561
+ * A circle/ellipse arc is closed when it sweeps (at least) a full revolution — the sampled
562
+ * outline does not duplicate the start vertex, so the geometric first==last test in the base
563
+ * class would wrongly report a full circle as open and leave a seam gap in the stroke.
564
+ */
565
+ isClosed(): boolean;
566
+ reverse(): this;
497
567
  protected _getDeltaAngle(): number;
498
568
  getPoint(t: number, output?: Vector2): Vector2;
499
569
  /**
@@ -541,7 +611,14 @@ declare class CompositeCurve<T extends Curve = Curve> extends Curve {
541
611
  protected _removeNextPointIfEqualPrevPoint(output: number[], offset: number): number[];
542
612
  getSpacedVertices(count?: number, output?: number[]): number[];
543
613
  getAdaptiveVertices(output?: number[]): number[];
614
+ /**
615
+ * A composite is closed when its single child is closed (e.g. a lone full-circle arc), or when
616
+ * its assembled outline returns to its start (rectangles, polygons, multi-segment loops).
617
+ */
618
+ isClosed(): boolean;
544
619
  strokeTriangulate(options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
620
+ /** Reverse the sub-curve order and reverse each sub-curve, so the whole outline runs backwards. */
621
+ reverse(): this;
545
622
  getFillVertices(options?: FillTriangulateOptions): number[];
546
623
  applyTransform(transform: Transform2D | ((point: Vector2) => void)): this;
547
624
  getMinMax(min?: Vector2, max?: Vector2): {
@@ -564,6 +641,7 @@ declare class CubicBezierCurve extends Curve {
564
641
  getPoint(t: number, output?: Vector2): Vector2;
565
642
  getAdaptiveVertices(output?: number[]): number[];
566
643
  getControlPointRefs(): Vector2[];
644
+ reverse(): this;
567
645
  protected _solveQuadratic(a: number, b: number, c: number): number[];
568
646
  getMinMax(min?: Vector2, max?: Vector2): {
569
647
  min: Vector2;
@@ -589,6 +667,7 @@ declare class LineCurve extends Curve {
589
667
  getTangent(_t: number, output?: Vector2): Vector2;
590
668
  getTangentAt(u: number, output?: Vector2): Vector2;
591
669
  getControlPointRefs(): Vector2[];
670
+ reverse(): this;
592
671
  getAdaptiveVertices(output?: number[]): number[];
593
672
  getMinMax(min?: Vector2, max?: Vector2): {
594
673
  min: Vector2;
@@ -621,6 +700,7 @@ declare class QuadraticBezierCurve extends Curve {
621
700
  constructor(p1?: Vector2, cp?: Vector2, p2?: Vector2);
622
701
  getPoint(t: number, output?: Vector2): Vector2;
623
702
  getControlPointRefs(): Vector2[];
703
+ reverse(): this;
624
704
  getAdaptiveVertices(output?: number[]): number[];
625
705
  getMinMax(min?: Vector2, max?: Vector2): {
626
706
  min: Vector2;
@@ -665,6 +745,7 @@ declare class SplineCurve extends Curve {
665
745
  constructor(points?: Vector2[]);
666
746
  getPoint(t: number, output?: Vector2): Vector2;
667
747
  getControlPointRefs(): Vector2[];
748
+ reverse(): this;
668
749
  copyFrom(source: SplineCurve): this;
669
750
  }
670
751
 
@@ -676,6 +757,13 @@ declare class CurvePath extends CompositeCurve {
676
757
  addPoints(points: Vector2[]): this;
677
758
  addCommands(commands: Path2DCommand[]): this;
678
759
  addData(data: string): this;
760
+ /**
761
+ * A sub-path is closed if it was explicitly closed (`autoClose`, i.e. a `Z`/`closePath`), or if
762
+ * it forms a geometric loop / wraps a single closed primitive (handled by the base class).
763
+ */
764
+ isClosed(): boolean;
765
+ /** Reverse direction, then refresh the {@link startPoint}/{@link currentPoint} cursors. */
766
+ reverse(): this;
679
767
  protected _closeVertices(output: number[]): number[];
680
768
  getUnevenVertices(count?: number, output?: number[]): number[];
681
769
  getSpacedVertices(count?: number, output?: number[]): number[];
@@ -760,6 +848,23 @@ declare class Path2D<T = any> extends CompositeCurve<CurvePath> {
760
848
  /** Per-sub-path sampled rings, cached for repeated hit tests. */
761
849
  protected _getRings(): number[][];
762
850
  isPointInFill(point: Vector2Like, options?: IsPointInFillOptions): boolean;
851
+ /** Build a `Path2D` from flat rings (`[x0,y0,…]` per sub-path); closed-and-filled as sub-paths. */
852
+ static fromRings(rings: number[][], style?: Partial<Path2DStyle>): Path2D;
853
+ /**
854
+ * Boolean (path) operation against another path, returning a NEW `Path2D` whose outline is the
855
+ * polygonal result. Curves are sampled before clipping, so the result is a polygonal
856
+ * approximation (see {@link polygonBoolean}). The result inherits this path's `style` unless
857
+ * overridden via `style`. Holes are emitted as oppositely-wound sub-paths (nonzero fill).
858
+ */
859
+ booleanOp(op: BooleanOp, other: Path2D, style?: Partial<Path2DStyle>): Path2D;
860
+ /** `this ∪ other` — the combined filled area. */
861
+ union(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
862
+ /** `this ∩ other` — only the overlapping area. */
863
+ intersection(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
864
+ /** `this − other` — this path with `other` cut away. */
865
+ difference(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
866
+ /** `this ⊕ other` — areas covered by exactly one of the two paths. */
867
+ xor(other: Path2D, style?: Partial<Path2DStyle>): Path2D;
763
868
  /**
764
869
  * Test whether a point lies on this path's stroke. A hit on any sub-path counts.
765
870
  *
@@ -833,6 +938,40 @@ declare class Path2DSet<T = any> {
833
938
  }>): HTMLCanvasElement;
834
939
  }
835
940
 
941
+ interface PosTan {
942
+ position: Vector2;
943
+ tangent: Vector2;
944
+ angle: number;
945
+ }
946
+ /**
947
+ * PathKit/Skia-style arc-length measurement over any {@link Curve} (including `CurvePath` and
948
+ * `Path2D`). Wraps the curve's existing arc-length cache, so repeated queries are cheap.
949
+ *
950
+ * ```ts
951
+ * const measure = new PathMeasure(path)
952
+ * const { position, angle } = measure.getPosTan(measure.getLength() * progress)
953
+ * ```
954
+ */
955
+ declare class PathMeasure {
956
+ curve: Curve;
957
+ constructor(curve: Curve);
958
+ /** Total arc length of the path. */
959
+ getLength(): number;
960
+ /** Whether the path forms a closed loop (see {@link Curve.isClosed}). */
961
+ isClosed(): boolean;
962
+ /** Point + unit tangent + tangent angle at an absolute arc-length `distance` (clamped). */
963
+ getPosTan(distance: number): PosTan;
964
+ /** Point at an absolute arc-length `distance` (clamped to `[0, getLength()]`). */
965
+ getPosition(distance: number): Vector2;
966
+ /** Point + tangent at a normalized progress `t ∈ [0, 1]` along the path. */
967
+ getPosTanAtProgress(t: number): PosTan;
968
+ /**
969
+ * Evenly sample the path into `count + 1` {@link PosTan} entries (arc-length spaced), e.g. to
970
+ * lay glyphs along a path or drive an `animate(progress)`-style traversal.
971
+ */
972
+ sample(count?: number): PosTan[];
973
+ }
974
+
836
975
  declare class FFDControlGrid {
837
976
  rows: number;
838
977
  cols: number;
@@ -875,5 +1014,5 @@ declare function svgToDom(svg: string | SVGElement): SVGElement;
875
1014
 
876
1015
  declare function svgToPath2DSet(svg: string | SVGElement): Path2DSet;
877
1016
 
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 };
1017
+ 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, evenoddFillRule, 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 };
1018
+ export type { BooleanOp, CssFunction, CssFunctionArg, DrawPointOptions, EvenoddFillRuleResult, FillRule, FillTriangulateOptions, FillTriangulatedResult, FlatRing, IsPointInFillOptions, IsPointInStrokeOptions, LineCap, LineJoin, LineStyle, ParseCssFunctionContext, Path2DCommand, Path2DData, Path2DDrawStyle, Path2DStyle, PosTan, StrokeLinecap, StrokeLinejoin, StrokeTriangulateOptions, StrokeTriangulatedResult, TransformableObject, TriangulatedResult, Vector2Like };