modern-path2d 1.6.1 → 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.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 {
@@ -298,7 +312,12 @@ declare function getDirectedArea(vertices: number[]): number;
298
312
  declare const PI: number;
299
313
  declare const PI_2: number;
300
314
  declare function toKebabCase(str: string): string;
301
- declare function getIntersectionPoint(p1: Vector2, p2: Vector2, q1: Vector2, q2: Vector2): Vector2;
315
+ /**
316
+ * Intersection of line p1→p2 with line q1→q2, or `null` when the segments are parallel
317
+ * (`crossRS === 0`) or the intersection lies too far off p1→p2 (`|t| > 1`). Callers must
318
+ * handle `null` (e.g. `Path2D.bold` skips the join when there is no usable point).
319
+ */
320
+ declare function getIntersectionPoint(p1: Vector2, p2: Vector2, q1: Vector2, q2: Vector2): Vector2 | null;
302
321
 
303
322
  interface NonzeroFillRuleResult {
304
323
  index: number;
@@ -370,6 +389,12 @@ interface LineStyle {
370
389
  cap: LineCap;
371
390
  miterLimit: number;
372
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;
373
398
  declare function strokeTriangulate(points: number[], options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
374
399
 
375
400
  interface IsPointInFillOptions {
@@ -383,10 +408,38 @@ interface IsPointInStrokeOptions {
383
408
  declare abstract class Curve {
384
409
  arcLengthDivision: number;
385
410
  protected _lengths: number[];
411
+ protected _adaptiveCache?: number[];
412
+ /**
413
+ * Parent composite, set lazily when a composite caches its children. Lets
414
+ * {@link invalidate} propagate up so an ancestor's caches refresh too.
415
+ */
416
+ _owner?: Curve;
417
+ protected _invalidating: boolean;
386
418
  abstract getPoint(t: number, output?: Vector2): Vector2;
419
+ /**
420
+ * Drop cached arc lengths and the cached sampled outline used by hit testing, then
421
+ * bubble up to {@link _owner}. Called automatically by {@link applyTransform} and the
422
+ * `Path2D` mutators; call it manually after mutating control-point coordinates in place —
423
+ * the caches cannot observe such mutations.
424
+ */
425
+ invalidate(): this;
426
+ /** Clears this curve's own caches. Composites also clear their children (see override). */
427
+ protected _invalidateSelf(): void;
428
+ /**
429
+ * Sampled outline cached for repeated hit tests (read-only — do not mutate the result).
430
+ * Invalidated by {@link invalidate}.
431
+ */
432
+ protected _getCachedAdaptiveVertices(): number[];
387
433
  getPointAt(u: number, output?: Vector2): Vector2;
388
434
  isClockwise(): boolean;
389
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;
390
443
  applyTransform(transform: Transform2D | ((point: Vector2) => void)): this;
391
444
  getUnevenVertices(count?: number, output?: number[]): number[];
392
445
  getSpacedVertices(count?: number, output?: number[]): number[];
@@ -402,6 +455,16 @@ declare abstract class Curve {
402
455
  getUToTMapping(u: number, distance?: number): number;
403
456
  getTangent(t: number, output?: Vector2): Vector2;
404
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
+ };
405
468
  getNormal(t: number, output?: Vector2): Vector2;
406
469
  getNormalAt(u: number, output?: Vector2): Vector2;
407
470
  getTForPoint(target: Vector2Like, epsilon?: number): number;
@@ -438,6 +501,13 @@ declare abstract class Curve {
438
501
  contains(x: number, y: number, options?: IsPointInFillOptions): boolean;
439
502
  getFillVertices(_options?: FillTriangulateOptions): number[];
440
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;
441
511
  strokeTriangulate(options?: StrokeTriangulateOptions): StrokeTriangulatedResult;
442
512
  toCommands(): Path2DCommand[];
443
513
  toData(): Path2DData;
@@ -468,8 +538,30 @@ declare class RoundCurve extends Curve {
468
538
  set dy(val: number);
469
539
  constructor(_center?: Vector2, _radius?: Vector2, _diff?: Vector2, rotate?: number, startAngle?: number, endAngle?: number, clockwise?: boolean);
470
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;
471
548
  protected _getDeltaAngle(): number;
472
549
  getPoint(t: number, output?: Vector2): Vector2;
550
+ /**
551
+ * Point on the ellipse at an absolute angle (mirrors {@link getPoint}'s parameterization,
552
+ * ignoring `_diff`).
553
+ */
554
+ protected _pointAtAngle(angle: number, output: Vector2): Vector2;
555
+ /**
556
+ * Analytical bounds of the (elliptical) arc: the start/end points plus the per-axis
557
+ * extrema angles that fall within the swept interval. Matches {@link getPoint}, so it is
558
+ * exact for `ArcCurve`/`EllipseCurve`. The `_diff` offset (used only by the legacy
559
+ * `_getAdaptiveVerticesByCircle` path) is intentionally ignored here.
560
+ */
561
+ getMinMax(min?: Vector2, max?: Vector2): {
562
+ min: Vector2;
563
+ max: Vector2;
564
+ };
473
565
  toCommands(): Path2DCommand[];
474
566
  drawTo(ctx: CanvasRenderingContext2D): this;
475
567
  applyTransform(transform: Transform2D): this;
@@ -487,7 +579,10 @@ declare class ArcCurve extends RoundCurve {
487
579
 
488
580
  declare class CompositeCurve<T extends Curve = Curve> extends Curve {
489
581
  curves: T[];
582
+ protected _adaptiveCacheLen: number;
490
583
  constructor(curves?: T[]);
584
+ protected _invalidateSelf(): void;
585
+ protected _getCachedAdaptiveVertices(): number[];
491
586
  getFlatCurves(): Curve[];
492
587
  addCurve(curve: T): this;
493
588
  getPoint(t: number, output?: Vector2): Vector2;
@@ -497,7 +592,14 @@ declare class CompositeCurve<T extends Curve = Curve> extends Curve {
497
592
  protected _removeNextPointIfEqualPrevPoint(output: number[], offset: number): number[];
498
593
  getSpacedVertices(count?: number, output?: number[]): number[];
499
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;
500
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;
501
603
  getFillVertices(options?: FillTriangulateOptions): number[];
502
604
  applyTransform(transform: Transform2D | ((point: Vector2) => void)): this;
503
605
  getMinMax(min?: Vector2, max?: Vector2): {
@@ -520,6 +622,7 @@ declare class CubicBezierCurve extends Curve {
520
622
  getPoint(t: number, output?: Vector2): Vector2;
521
623
  getAdaptiveVertices(output?: number[]): number[];
522
624
  getControlPointRefs(): Vector2[];
625
+ reverse(): this;
523
626
  protected _solveQuadratic(a: number, b: number, c: number): number[];
524
627
  getMinMax(min?: Vector2, max?: Vector2): {
525
628
  min: Vector2;
@@ -545,6 +648,7 @@ declare class LineCurve extends Curve {
545
648
  getTangent(_t: number, output?: Vector2): Vector2;
546
649
  getTangentAt(u: number, output?: Vector2): Vector2;
547
650
  getControlPointRefs(): Vector2[];
651
+ reverse(): this;
548
652
  getAdaptiveVertices(output?: number[]): number[];
549
653
  getMinMax(min?: Vector2, max?: Vector2): {
550
654
  min: Vector2;
@@ -577,6 +681,7 @@ declare class QuadraticBezierCurve extends Curve {
577
681
  constructor(p1?: Vector2, cp?: Vector2, p2?: Vector2);
578
682
  getPoint(t: number, output?: Vector2): Vector2;
579
683
  getControlPointRefs(): Vector2[];
684
+ reverse(): this;
580
685
  getAdaptiveVertices(output?: number[]): number[];
581
686
  getMinMax(min?: Vector2, max?: Vector2): {
582
687
  min: Vector2;
@@ -599,7 +704,12 @@ declare class RectangleCurve extends PolygonCurve {
599
704
  copyFrom(source: RectangleCurve): this;
600
705
  }
601
706
 
602
- declare class RoundRectangleCurve extends RoundCurve {
707
+ /**
708
+ * A rounded rectangle, modelled as a real composite of 4 `LineCurve` edges + 4 quarter
709
+ * `ArcCurve` corners (like {@link RectangleCurve}). `getPoint`/`getLength`/`getMinMax`/
710
+ * `toCommands` therefore describe the actual rounded outline — not a bare ellipse.
711
+ */
712
+ declare class RoundRectangleCurve extends CompositeCurve {
603
713
  x: number;
604
714
  y: number;
605
715
  width: number;
@@ -616,6 +726,7 @@ declare class SplineCurve extends Curve {
616
726
  constructor(points?: Vector2[]);
617
727
  getPoint(t: number, output?: Vector2): Vector2;
618
728
  getControlPointRefs(): Vector2[];
729
+ reverse(): this;
619
730
  copyFrom(source: SplineCurve): this;
620
731
  }
621
732
 
@@ -627,6 +738,13 @@ declare class CurvePath extends CompositeCurve {
627
738
  addPoints(points: Vector2[]): this;
628
739
  addCommands(commands: Path2DCommand[]): this;
629
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;
630
748
  protected _closeVertices(output: number[]): number[];
631
749
  getUnevenVertices(count?: number, output?: number[]): number[];
632
750
  getSpacedVertices(count?: number, output?: number[]): number[];
@@ -668,6 +786,8 @@ declare class CurvePath extends CompositeCurve {
668
786
  */
669
787
  declare class Path2D<T = any> extends CompositeCurve<CurvePath> {
670
788
  protected _meta?: T;
789
+ protected _ringsCache?: number[][];
790
+ protected _ringsCacheLen: number;
671
791
  currentCurve: CurvePath;
672
792
  style: Partial<Path2DStyle>;
673
793
  get startPoint(): Vector2 | undefined;
@@ -705,7 +825,27 @@ declare class Path2D<T = any> extends CompositeCurve<CurvePath> {
705
825
  *
706
826
  * Defaults `fillRule` to `style.fillRule`, then `'nonzero'` (matching SVG/Canvas).
707
827
  */
828
+ protected _invalidateSelf(): void;
829
+ /** Per-sub-path sampled rings, cached for repeated hit tests. */
830
+ protected _getRings(): number[][];
708
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;
709
849
  /**
710
850
  * Test whether a point lies on this path's stroke. A hit on any sub-path counts.
711
851
  *
@@ -779,6 +919,40 @@ declare class Path2DSet<T = any> {
779
919
  }>): HTMLCanvasElement;
780
920
  }
781
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
+
782
956
  declare class FFDControlGrid {
783
957
  rows: number;
784
958
  cols: number;
@@ -821,5 +995,5 @@ declare function svgToDom(svg: string | SVGElement): SVGElement;
821
995
 
822
996
  declare function svgToPath2DSet(svg: string | SVGElement): Path2DSet;
823
997
 
824
- 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 };
825
- 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 };