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.
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.
Whether no command has been appended yet.
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.
Optionalcounterclockwise: booleanA copy of this path, sharing its (readonly) command objects.
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.
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.
Optionaltolerance: numberAppends 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.
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);
Optionalcounterclockwise: booleanFlattens 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.
Optionaltolerance: number
Flattening tolerance, as Path.flatten.
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.
Optionaltolerance: number
Greatest allowed distance from the true curve, in world units. Must be finite and positive.
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.
Optionaltolerance: numberAppends a straight segment from the current point to (x, y) (§51 "line").
Starts a new subpath at (x, y) (§51 "move").
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.
Optionaltolerance: numberThe 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.
Optionaltolerance: numberPath.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.
Optionaltolerance: number
Flattening tolerance, as Path.flatten.
Appends a quadratic Bézier from the current point through control point
(controlX, controlY) to (x, y) (§51 "quadratic Bézier").
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).
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.
Optionaltolerance: number
Greatest distance a removed vertex may be from its replacement segment. Must be finite and positive.
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).
Optionaltimes: number
How many halvings, 0 … 16. Each curve becomes 2^times
curves, so the ceiling is where a command list stops being the right
representation.
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.
Optionaltolerance: numberApplies 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.
A 2D path: an ordered list of §51's segment commands, built through a fluent API and read through the operations §51 requires.
Mutation, and what returns what
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 newPathand leaves the receiver untouched. That split is deliberate: a path is source data that other objects hold references to, and an in-placetransformwould 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.