svg-path-kit
v1.0.2
Published
Build precise, complex SVG paths with smooth curves and circular or elliptical arcs using a fluent API.
Maintainers
Readme
SVG Path Kit
A small, composable TypeScript library for building complex SVG path strings using a high‑level, geometry‑oriented API.
Instead of manually concatenating SVG path commands, you work with Point2D, Vector2D, Bézier helpers, and a fluent PathBuilder that outputs a valid d attribute for <path> elements.
Installation
npm install svg-path-kit
# or
yarn add svg-path-kit
# or
pnpm add svg-path-kitThe library is written in TypeScript and ships with type definitions.
Core Concepts
1. Points and Vectors
The library models 2D geometry explicitly:
Point2D: an absolute position in 2D spaceVector2D: a displacement with direction and magnitude
Key ideas:
- A
Point2Dcan be translated by aVector2Dto produce a new point. - A
Vector2Dcan be added, subtracted, scaled, rotated, and converted to a unit vector. - This lets you build paths using clear geometric operations instead of raw numbers.
Point2D
- Factory:
Point2D.of(x: number, y: number): Point2D - Methods:
add(vector: Vector2D): Point2D– translate the point by a vector.toVector(): Vector2D– convert the point to a vector from the origin.
Vector2D
Static factories:
Vector2D.of(x: number, y: number)– create a vector from coordinates.Vector2D.ofAngle(angle: number)– unit vector at angle (radians) from the x‑axis.Vector2D.from(initial: Point2D, terminal: Point2D)– vector from one point to another.
Properties:
x,y– componentsmagnitude– vector lengthslope–y / x
Static constant:
Vector2D.NULL_VECTOR– zero vector(0, 0)
Methods (immutable unless noted):
add(v: Vector2D): Vector2D– vector addition.subtract(v: Vector2D): Vector2D– vector subtraction.multiply(scalar: number): Vector2D– scalar multiplication.dotProduct(v: Vector2D): number– dot product.crossProduct(v: Vector2D): number– 2D cross product (scalar z‑component).unit(): Vector2D– normalized vector (orNULL_VECTORif magnitude is 0).perpendicular(direction?: RotationDirection): Vector2D– perpendicular vector, clockwise or counterclockwise.opposite(): Vector2D– negated vector.clone(): Vector2D– copy.scale(s: number): void– in‑place scaling.rotate(angle: number): void– in‑place rotation.toPoint(): Point2D– interpret as a point from the origin.
RotationDirectionenum:RotationDirection.CLOCKWISERotationDirection.COUNTERCLOCKWISE
These primitives are the foundation for all higher‑level path operations.
Path Model
2. Commands and Paths
The library models an SVG path as a list of strongly‑typed command objects.
Command(abstract)- Base class for individual SVG commands.
- Each command has a mode:
'relative'or'absolute'. - Methods:
toString(): string– SVG path segment (e.g."c 10 0 20 10 30 0").getEndPoint(): Point2D– absolute end point of the command.
Concrete command types:
MoveCommand–M/mLineCommand–L/lQuadraticBezierCurveCommand–Q/qCubicBezierCurveCommand–C/cEllipticalArcCommand–A/aClosePathCommand–Z/z
You usually don’t instantiate these directly; you build them via PathBuilder.
Path
new Path(commands: Command[])– wrap an array of commands.toString(): string– join command strings into a full SVGdattribute.
Example of consuming a built path:
import { PathBuilder } from 'svg-path-kit';
import { Point2D, Vector2D } from 'svg-path-kit';
const pathBuilder = PathBuilder.M(Point2D.of(0, 0))
.l(Vector2D.of(100, 0))
.l(Vector2D.of(0, 100))
.z();
const d = pathBuilder.toString();
// <path d={d} /> in your SVGPathBuilder: Fluent SVG Path Construction
PathBuilder is the main entry point for building complex SVG paths. It manages the current point, stacks of open subpaths, and provides high‑level methods for lines, curves, arcs, and auto‑controlled Béziers.
Creating a builder
- Relative start:
PathBuilder.m(vector: Vector2D)– start a path with a relative move from(0, 0). - Absolute start:
PathBuilder.M(point: Point2D)– start a path at an absolute point.
Internally both static methods call a private constructor and push a MoveCommand.
State accessors
currentPosition: Point2D– absolute current endpoint (origin if no commands yet).lastCommand: Command | null– last appended command ornull.
currentPosition is especially useful when you have built up parts of the path with relative commands and you want to:
- Capture the absolute position you have reached, and/or
- Later construct new segments relative to that stored point, even when you are no longer adjacent to it in the command sequence.
Example – capture an absolute point from relative movement and reuse it later:
import { PathBuilder, Point2D, Vector2D } from 'svg-path-kit';
const builder = PathBuilder.M(Point2D.of(10, 10))
.l(Vector2D.of(50, 0)) // move relatively
.l(Vector2D.of(0, 40)); // move relatively again
const corner = builder.currentPosition; // absolute Point2D(60, 50)
// ... build more of the path elsewhere ...
builder.l(Vector2D.of(20, 0));
// later, build something relative to `corner`, even though
// the currentPosition has moved on:
const offset = Vector2D.of(0, -20);
const target = corner.add(offset);
builder.L(target); // absolute line back near that stored corner
const d = builder.toString();Because currentPosition is a Point2D, you can combine it with Vector2D operations (Vector2D.from, add, subtract, rotate, etc.) to derive new absolute points and then use L, CForCircularArc, CAutoControl, or other absolute commands relative to those computed positions.
Move commands
m(point: Vector2D): this– relativemfromcurrentPosition.M(point: Point2D): this– absoluteMto a point, computed as a vector fromcurrentPosition.
Every m/M opens a new subpath; the first move in each is remembered so you can close it later with z().
Line commands
l(point: Vector2D): this– relativelfrom current point.L(point: Point2D): this– absoluteL.
This is the simplest way to build polylines.
Quadratic Bézier commands
q(controlPoint: Vector2D, endPoint: Vector2D): this– relativeq.Q(controlPoint: Point2D, endPoint: Point2D): this– absoluteQ(automatically converted into relative vectors internally).
Cubic Bézier commands
c(firstControlPoint: Vector2D, secondControlPoint: Vector2D, endPoint: Vector2D): this– relativec.C(firstControlPoint: Point2D, secondControlPoint: Point2D, endPoint: Point2D): this– absoluteC.
You typically use these when you know exact control points. For smoother authoring, see auto‑control curves below.
Elliptical arc commands
Direct wrappers around SVG A / a commands:
- Relative:
a(xRadius, yRadius, xAxisRotation, largeArcFlag, sweepFlag, endPoint: Vector2D) - Absolute:
A(xRadius, yRadius, xAxisRotation, largeArcFlag, sweepFlag, endPoint: Point2D)
These generate true SVG elliptical arcs.
High‑Level Geometry Helpers
Beyond raw commands, the library provides geometry‑aware helpers that turn arcs and tangents into high‑quality cubic Bézier segments.
1. Circular Arc as Cubic Bézier
Use these when you want the visual shape of a circular arc but prefer cubic Bézier segments (e.g. for uniform curve handling or animation).
In PathBuilder:
Relative
cForCircularArc(overloads):cForCircularArc(angle: number, endingPoint: Vector2D): PathBuildercForCircularArc(center: Vector2D, angle: number): PathBuilder
Absolute
CForCircularArc(overloads):CForCircularArc(angle: number, endingPoint: Point2D): PathBuilderCForCircularArc(center: Point2D, angle: number): PathBuilder
Example – build a path that contains a circular arc segment:
import { PathBuilder, Point2D, Vector2D } from 'svg-path-kit';
const d = PathBuilder.M(Point2D.of(0, 0))
.l(Vector2D.of(50, 0))
// 90° circular arc, expressed as a cubic Bézier
.cForCircularArc(Math.PI / 2, Vector2D.of(50, 50))
.l(Vector2D.of(0, 50))
.z()
.toString();Under the hood these helpers call the lower‑level cubicBezierCurveForCircularArc utility, which computes control points so the Bézier approximates the exact circular arc.
2. Elliptical Arc as Cubic Bézier
When you want an elliptical arc with separate radii and rotation, use:
Low‑level:
cubicBezierCurveForEllipticalArc(center: Point2D, startingPoint: Point2D, centralAngle: number, ratio: number, phi: number): CubicBezierCurveratiois the axis ratio (a/b) for the ellipse.phiis the rotation of the ellipse in radians.
PathBuilder integration:
- Relative:
cForEllipticalArc(center: Vector2D, angle: number, axisRatio: number, ellipseRotation?: number) - Absolute:
CForEllipticalArc(center: Point2D, angle: number, axisRatio: number, ellipseRotation?: number)
- Relative:
These compute a cubic Bézier curve that follows the given elliptical arc, using parametric ellipse math and derivatives for smooth tangents.
3. Auto‑Controlled Cubic Bézier (Smooth Connections)
To avoid manually guessing control points for smooth joins, the library offers an auto‑control helper.
High‑level: PathBuilder.cAutoControl / CAutoControl
These wrap the lower‑level cubicBezierAutoControl helper and integrate with the builder state:
Relative:
cAutoControl( endingPoint: Vector2D, startAngle?: number, endAngle?: number, curvatureA: number = 1/3, curvatureB: number = curvatureA )Absolute:
CAutoControl( endingPoint: Point2D, startAngle?: number, endAngle?: number, curvatureA: number = 1/3, curvatureB: number = curvatureA )
Behavior:
- Angles to tangents: if
startAngle/endAngleare provided, they’re converted to unit direction vectors. - Automatic start direction: if
startAngleis omitted, the start direction is inferred from:- the last line segment’s direction, or
- the last cubic/quadratic segment’s tangent into its endpoint.
- Automatic end direction: if
endAngleis omitted, the direction is inferred based on the chord between current point and target.
This allows you to write code like:
const d = PathBuilder.M(Point2D.of(0, 0))
.l(Vector2D.of(50, 0))
.cAutoControl(Vector2D.of(50, 50)) // smooth curve continuing from the line
.cAutoControl(Vector2D.of(0, 50)) // another smooth curve
.toString();You get visually smooth transitions without hand‑tuning control points.
Working with CubicBezierCurve
The CubicBezierCurve class (used by the helpers above) exposes utilities for geometry‑based workflows:
- Constructor:
new CubicBezierCurve(startingPoint, firstControlPoint, secondControlPoint, endingPoint)
getPointAt(t: number): Point2D– position on the curve for (0 \le t \le 1).getTangentAt(t: number): Vector2D– tangent vector at that parameter.splitAt(t: number, side: 'left' | 'right' = 'left'): CubicBezierCurve– split the curve using De Casteljau’s algorithm:'left'– returns the first segment from (t=0) to given (t).'right'– returns the trailing segment, translated so its start is at the origin.
These methods are useful for:
- Parametric animation along the curve
- Collision or hit‑testing
- Splitting and trimming path segments
Closing Paths and Exporting
After constructing your commands with PathBuilder:
z()– close the current subpath by emitting aClosePathCommandthat connects back to the last move (m/M) in the open stack.toPath()– wrap the internal command list into aPathinstance.toString()– stringify to an SVGdstring.
Example:
const builder = PathBuilder.M(Point2D.of(10, 10))
.l(Vector2D.of(100, 0))
.l(Vector2D.of(0, 50))
.z();
const d = builder.toString();
// d might look like: "M 10 10 l 100 0 l 0 50 z"You can use d anywhere SVG expects a path definition.
Practical Usage Patterns
Here are some general patterns you can build with this library, independent of any specific example:
- Smooth strokes and ribbons
- Use
cAutoControl/CAutoControlto create flowing, connected segments where the tangent direction is inferred from the previous segment.
- Use
- Arcs and circular motion
- Replace raw SVG
Acommands withcForCircularArc/CForCircularArcif you want everything expressed as cubic Béziers (easier to analyze, animate, or sample).
- Replace raw SVG
- Elliptical shapes and orbits
- Use
cForEllipticalArc/CForEllipticalArcfor elliptical trajectories with configurable axis ratios and rotation.
- Use
- Geometric constructions
- Compose
Point2D+Vector2Doperations (add, rotate, scale, perpendiculars) to express geometry declaratively before converting it into commands.
- Compose
- Animation over time
- Drive input angles, lengths, and positions from an external interpolation system, then rebuild paths each frame using the same
PathBuildercalls to get smooth, time‑varying shapes.
- Drive input angles, lengths, and positions from an external interpolation system, then rebuild paths each frame using the same
TypeScript Support
- All public classes and functions ship with
.d.tsdeclarations. - Works smoothly in TS projects; you get autocomplete for:
Point2D,Vector2D,RotationDirectionPathBuilderand its full fluent APICubicBezierCurveand the various arc / auto‑control helpers
Summary of Public API
Geometry
Point2D.of(x, y)Vector2D.of(x, y)Vector2D.ofAngle(angle)Vector2D.from(initial, terminal)RotationDirectionenum
Path construction
PathBuilder.m(vector)/PathBuilder.M(point)- Instance methods:
m,M,l,L,q,Q,c,C,a,AcForCircularArc,CForCircularArccForEllipticalArc,CForEllipticalArccAutoControl,CAutoControlz,toPath,toString
Cubic Bézier utilities
CubicBezierCurvegetPointAt(t)getTangentAt(t)splitAt(t, side?)
cubicBezierCurveForCircularArc(...)cubicBezierCurveForEllipticalArc(...)cubicBezierAutoControl(...)
This is the toolkit you use to build precise, smooth, and expressive SVG paths in a fully programmatic way.
