Documentation
    Preparing search index...

    A 2D path: an ordered list of §51's segment commands, built through a fluent API and read through the operations §51 requires.

    const path = new Path()
    .moveTo(0, 0)
    .lineTo(100, 0)
    .quadraticCurveTo(150, 50, 100, 100)
    .cubicCurveTo(75, 125, 25, 125, 0, 100)
    .arc(0, 50, 25, 0, Math.PI)
    .close();

    path.length(); // world units along the path
    path.flatten(0.05); // one polyline per subpath
    path.fillRings(0.05); // outline/hole groups, per the fill rule
    path.union(other); // §51 Boolean, flatten-then-clip

    The builder methods mutate the path and return this, so they chain. Every operation that produces a different shape — Path.transform, Path.reverse, Path.subdivide, Path.simplify, Path.clone, Path.union and the other §51 Booleans — returns a new Path and leaves the receiver untouched. That split is deliberate: a path is source data that other objects hold references to, and an in-place transform would be the kind of silent shared-state rewrite §85 exists to prevent.

    The command objects themselves are deeply readonly and are shared, not copied, between a path and its clone.

    Index
    fillRule: FillRule

    How this path's subpaths combine into a filled region (§51 "Fill rules"). Read by Path.fillRings; carried by every operation that returns a new path.

    • get commands(): readonly PathCommand[]

      The command list, in append order (§51's model, readable).

      Live, not a copy: appending to the path is visible here immediately. The commands are readonly objects, so reading them cannot corrupt the path.

      Returns readonly PathCommand[]

    • get isEmpty(): boolean

      Whether no command has been appended yet.

      Returns boolean

    • Appends a circular arc centred on (centerX, centerY) (§51 "circular … arc").

      path.arc(0, 50, 25, 0, Math.PI); // §51's own example
      

      Angles are radians (§7b) measured from the +X axis, and a positive sweep turns counter-clockwise in the Y-up plane (§7a). counterclockwise selects the other way round the circle.

      The sweep is measured in the requested direction and wrapped into one turn, the Canvas rule: arc(…, 0, 4π) is a whole circle, arc(…, 0, 2π) is a whole circle, and arc(…, 0, 2π, true) is nothing at all — going clockwise from 0 to 2π arrives immediately. A clockwise whole circle is arc(…, 0, −2π, true).

      If the path has a current point that is not the arc's first point, the arc contributes an implicit straight segment to it. If the path has no current point, an explicit moveTo the arc's first point is appended first, so the command list stays well formed on its own terms.

      Parameters

      • centerX: number
      • centerY: number
      • radius: number
      • startAngle: number
      • endAngle: number
      • counterclockwise: boolean = false

      Returns this

    • A copy of this path, sharing its (readonly) command objects.

      Returns Path

    • Closes the current subpath with an implicit straight segment back to its first point (§51 "close path"), leaving the current point there.

      Refuses (§85) when no subpath is open — a close that closes nothing is a mistake in the caller, and Canvas's silent no-op is exactly the kind of swallowed error this repository does not do.

      Returns this

    • The closest point on the path to point (§51 "closest-point query"), with its distance and its normalized arc-length position.

      Measured on the flattening: every edge is tested, the point is projected onto it and clamped to its ends, and the winner is chosen by squared distance — exact arithmetic, ties broken by the earlier edge, so the answer is cross-platform even though the reported distance takes a square root.

      Linear in the number of edges. A path sampled by many queries wants a spatial index; that is a caching layer over this method, not a different answer.

      Parameters

      • point: Point2D
      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

      Returns PathClosestPoint

    • Appends a cubic Bézier from the current point through control points (control1X, control1Y) and (control2X, control2Y) to (x, y) (§51 "cubic Bézier").

      Named as §51 names it — cubicCurveTo, not Canvas's bezierCurveTo.

      Parameters

      • control1X: number
      • control1Y: number
      • control2X: number
      • control2Y: number
      • x: number
      • y: number

      Returns this

    • Appends an elliptical arc (§51 "… and elliptical arc") — Path.arc with two semi-axes and a rotation of the ellipse's local frame.

      path.ellipse(0, 0, 40, 20, Math.PI / 4, 0, Math.PI);
      

      Parameters

      • centerX: number
      • centerY: number
      • radiusX: number
      • radiusY: number
      • rotation: number
      • startAngle: number
      • endAngle: number
      • counterclockwise: boolean = false

      Returns this

    • Flattens this path and sorts the resulting rings into filled regions and the holes cut out of them, according to Path.fillRule (§51 "Fill rules") — the input triangulatePolygon takes.

      const letterO = new Path()
      .moveTo(-2, -3).lineTo(2, -3).lineTo(2, 3).lineTo(-2, 3).close()
      .moveTo(-1, -2).lineTo(-1, 2).lineTo(1, 2).lineTo(1, -2).close();
      const [region] = letterO.fillRings();
      region.holes.length; // 1 — the counter, under either fill rule

      Ring bounds reject impossible containment first; remaining candidates are probed by winding number, which gives both rules at once: under even-odd a ring is filled when an even number of rings contain it, under nonzero when the winding just inside it — the winding of the containing rings plus its own — is not zero. Each unfilled ring is then attached to the innermost ring containing it, which is always a filled one (if a container were unfilled, everything it contains would see winding zero from outside and be filled by its own turn alone).

      That grouping is what makes an island — a filled ring inside a hole, the middle of a letter "e" drawn as three rings — expressible: it comes back as its own region rather than as a hole inside a hole, which is a configuration §52's tessellator documents that it refuses.

      Rings of fewer than three points, and rings enclosing exactly zero signed area, contribute nothing to any fill and are left out of the result. The path is untouched: this method answers "what does filling this path produce", and a degenerate ring's honest answer is "nothing". Open subpaths are filled as if closed, the SVG and Canvas rule — so an open subpath that happened to end where it began loses that final point, which under the closing rule is the implicit edge drawn a second time.

      Under nonzero, a ring nested inside another with the same winding is filled in its own right, so the inner region is covered by two groups. For an opaque fill that is invisible; for a translucent one it double-blends. The exact answer is a boolean union (booleanOp); reporting the overlap honestly beats dropping a ring the caller drew.

      Parameters

      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

        Flattening tolerance, as Path.flatten.

      Returns PathFillRings[]

    • Flattens every subpath into a polyline no further than tolerance world units from the true curve (§51 "flatten"), one point array per subpath, in command order.

      const [ring] = new Path()
      .moveTo(0, 0).lineTo(2, 0).lineTo(2, 2).lineTo(0, 2).close()
      .flatten();
      ring.length; // 4 — the closing edge is implicit, the first point is not repeated

      Fresh arrays of fresh points on every call; the caller owns them. A closed subpath comes back as a ring whose last point is not the first — the shape triangulatePolygon documents, so the output feeds §52 directly. An open subpath comes back as its polyline; nothing is appended to close it, because whether an open subpath is filled as if closed is a fill question (Path.fillRings answers it) and not a flattening one.

      Two consecutive points are never exactly equal: a duplicate would be a zero-length edge, which §52's tessellator refuses and which no stroke can find a direction for. Points that are merely close are left alone — moving them is Path.simplify's job and its caller's decision (§85). One consequence is worth knowing: a hand-written lineTo aimed at the coordinates of an arc's first point will usually miss it by an ulp, since that point is centre + r·(cos θ, sin θ), and the pair survives as a hairline spike that §52 then refuses by name. Let the arc's implicit connecting segment draw that edge instead — which is how a rounded rectangle is written anyway, as a moveTo and four arcs.

      A subpath that received no segment (a lone moveTo) comes back as a single point rather than being dropped: for a fill it is nothing, but for a round cap it is a dot, and this method does not get to decide which one the caller meant.

      Béziers are split at their midpoint until a flatness test passes: the distance of the control points from the chord, compared against tolerance — for a cubic, (d₁ + d₂)² ≤ tolerance² · |chord|², which bounds the true deviation by ¾ · tolerance since a cubic stays within ¾ max(d₁, d₂) of its chord. Straight stretches of a curve therefore cost two points and tight corners cost many, which is what "adaptive" buys over a fixed segment count (§52's "adaptive curve subdivision", delegated here by that module). Arcs are sampled at a uniform angular step chosen so the sagitta of each chord is within tolerance.

      Parameters

      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

        Greatest allowed distance from the true curve, in world units. Must be finite and positive.

      Returns Point2D[][]

    • §51 intersection of the filled regions. See booleanOp.

      Parameters

      • other: Path
      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

      Returns Path

    • Total length of the path in world units (§51 "compute length"), measured on the flattening at tolerance — closing segments of closed subpaths included, the gaps between subpaths excluded.

      A flattened measurement, and therefore an under-estimate of a curve's true length (a chord is shorter than its arc) bounded by the same tolerance that bounds the flattening. Same-runtime tier (§33): one Math.sqrt per edge.

      Parameters

      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

      Returns number

    • Appends a straight segment from the current point to (x, y) (§51 "line").

      Parameters

      • x: number
      • y: number

      Returns this

    • Starts a new subpath at (x, y) (§51 "move").

      Parameters

      • x: number
      • y: number

      Returns this

    • The unit normal at normalized arc-length position t (§51 "evaluate … normal") — the tangent turned a quarter turn counter-clockwise, so it points to the left of the direction of travel in the Y-up plane (§7a).

      For a counter-clockwise ring that is the inward normal; reverse the path (or negate the result) for the other side. Stating the convention is the point: a stroke offsets along it, and a sign flip is a stroke drawn on the wrong side.

      Parameters

      • t: number
      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

      Returns Point2D

    • The point at normalized arc-length position t along the path (§51 "evaluate point").

      t = 0 is the first point of the first subpath and t = 1 the last point of the last, with the parameter advancing in proportion to length, which is what makes it the right parameter to lay text or ticks along. Where one subpath ends and the next begins the position jumps: the gap between them has no length and is not part of the path.

      Refuses (§85) a t outside [0, 1] and a path of zero length — neither has a defensible answer, and clamping would hide a caller's bug.

      Parameters

      • t: number
      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

      Returns Point2D

    • Path.flatten, keeping the one bit a Point2D[] cannot carry: whether each subpath closes back on its first point (§52 stroke expansion).

      const [outline] = new Path()
      .moveTo(0, 0).lineTo(2, 0).lineTo(2, 2).close()
      .polylines();
      outline.closed; // true — the closing edge is implicit
      outline.points.length; // 3 — the first point is not repeated

      The arrays and points are the same fresh, caller-owned ones Path.flatten hands back, under exactly the same rules; this is not a different flattening, it is the same one reported with its closed flag intact.

      It exists because a stroke is the one consumer for which the flag is load-bearing: a closed ring is joined at every vertex and capped at none, an open one is capped at both ends, and the difference is not recoverable from the points (a polyline that happens to end where it began is not a ring — Path.flatten's own note about the ulp trap is the reason a coordinate comparison cannot be made to answer it). fillRings needs the opposite — an open subpath fills as if closed — which is why the fill path does not take this route.

      Parameters

      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

        Flattening tolerance, as Path.flatten.

      Returns Polyline2D[]

    • Appends a quadratic Bézier from the current point through control point (controlX, controlY) to (x, y) (§51 "quadratic Bézier").

      Parameters

      • controlX: number
      • controlY: number
      • x: number
      • y: number

      Returns this

    • Reverses the direction the path is traversed in (§51 "reverse"), returning a new path: subpaths come back in reverse order, each walked from its last point to its first.

      The visible consequence is winding: every ring's signed area flips sign, which under the nonzero fill rule is how a subpath is turned from a solid into a hole. Closed subpaths stay closed; the implicit closing segment is traversed in its new direction like any other.

      Exact for every segment kind (a cubic's two control points swap, an arc's sweep negates), so cross-platform (§33).

      Returns Path

    • Removes vertices that a straight run does not need (§51 "simplify"), returning a new path in which no removed vertex lies further than tolerance from the segment that replaced it.

      Two things are removed: a lineTo that lands within tolerance of the current point (a zero-length segment), and a vertex between two lineTos that is within tolerance of the chord joining its neighbours. Every vertex removed from a run is re-checked against the growing chord, so the guarantee holds for the run and not just for one step.

      Curves are never touched. Refitting a Bézier to a tolerance is curve fitting, a different algorithm with a different failure mode, and a simplify that quietly replaced a designer's cubic with a different cubic would be the silent rewrite §85 forbids. Vertex removal on the polyline runs is what ships; curve refitting, and the globally optimal Douglas–Peucker pass that removes fewer vertices than this greedy one misses, are the staged upgrades.

      Cross-platform (§33): squared distances and a cross product, no square roots.

      Parameters

      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

        Greatest distance a removed vertex may be from its replacement segment. Must be finite and positive.

      Returns Path

    • Splits every curve segment at its midpoint, times times, returning a new path of the same shape (§51 "subdivide").

      const finer = path.subdivide(); // every curve becomes two of the same kind
      

      Not to be confused with Path.flatten, which throws the curves away. This is the exact operation: a cubic becomes two cubics through de Casteljau at t = ½, a quadratic two quadratics, an arc two arcs of half the sweep; lines, moves and closes pass through untouched. It is what an editor's "add a point" does, what a subdivision-based intersection search needs, and — because halving a double is exact and (a + b) × 0.5 rounds once — it is cross-platform for every segment kind, arcs included (a curve that follows an arc needs that arc's end point, which is the one same-runtime number in the operation; see this module's header).

      Parameters

      • times: number = 1

        How many halvings, 0 … 16. Each curve becomes 2^times curves, so the ceiling is where a command list stops being the right representation.

      Returns Path

    • §51 subtraction: this path's fill minus other's. See booleanOp.

      Parameters

      • other: Path
      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

      Returns Path

    • The unit tangent at normalized arc-length position t (§51 "evaluate … tangent"), pointing in the direction of travel.

      On the flattening, so it is the direction of the polyline edge containing t and is piecewise constant: at a vertex shared by two edges the edge starting there wins, which is a rule rather than a coin toss (§33). Same-runtime tier: one Math.sqrt.

      Parameters

      • t: number
      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

      Returns Point2D

    • Applies a 2D affine transform to every command (§51 "transform"), returning a new path.

      matrix is read as @fourjs/math's column-major 3×3 with the translation in its third column: a point maps to (e0·x + e3·y + e6, e1·x + e4·y + e7).

      Move, line and Bézier commands are matrix-vector products — +, -, * only, so cross-platform (§33) and exact for any affine matrix.

      An arc is stored as a centre, two radii, a rotation and a sweep, and a general affine map takes that ellipse to a different ellipse whose axes and rotation require an eigen decomposition to recover. This method therefore handles the case that is both common and exactly expressible — a similarity: uniform scale, rotation, translation, optionally with a reflection, which maps radii by the scale, adds (or mirrors) the rotation, and leaves the sweep alone (negating it under a reflection). Recovering that scale and angle costs one Math.sqrt and one Math.atan2, so transforming a path containing an arc is same-runtime tier while transforming one without is cross-platform.

      A non-similarity matrix applied to a path containing an arc is refused (§85), naming the operation that would fix it. Silently squashing the circle into another circle, or converting it to Béziers behind the caller's back, are both worse answers than saying so.

      A projective matrix — a third row that is not (0, 0, 1) — is refused for any path: a path model has no perspective divide.

      Parameters

      Returns Path

    • §51 union of the filled regions. See booleanOp.

      Parameters

      • other: Path
      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

      Returns Path

    • §51 exclusive-or of the filled regions. See booleanOp.

      Parameters

      • other: Path
      • tolerance: number = DEFAULT_FLATTEN_TOLERANCE

      Returns Path