Path Sampling
Low-level utilities for parsing, baking, and sampling SVG paths
The path sampling system provides low-level utilities for parsing SVG paths, tessellating them into evenly-spaced points, and sampling positions with O(log n) performance. These are the building blocks for PathResource, TrimPath, and PathFollow.
Architecture Overview
SVG Path String ("M 0,0 C 50,0 50,100 100,100")
│
▼
┌──────────────┐
│ PathParser │ Parse SVG commands
│ │ Normalize to cubic beziers
└──────┬───────┘
│
▼
┌──────────────┐
│ PathBaker │ Tessellate curves
│ │ Create evenly-spaced points
└──────┬───────┘
│
▼
┌──────────────┐
│ PathSampler │ Binary search sampling
│ │ Position, tangent, closest point
└──────────────┘Most users should use PathResource instead of these utilities directly. Use these when you need fine-grained control over the parsing/baking process.
PathParser
Parse SVG path data into structured segments.
parse()
import { PathParser } from 'faster-motion';
const parsed = PathParser.parse('M 0,0 L 100,0 C 150,0 150,100 100,100 Z');
console.log(parsed.subPaths); // Array of subpaths
console.log(parsed.bounds); // { x, y, width, height }Supported Commands
| Command | Name | Description |
|---|---|---|
| M/m | MoveTo | Start new subpath |
| L/l | LineTo | Straight line |
| H/h | Horizontal | Horizontal line |
| V/v | Vertical | Vertical line |
| C/c | CurveTo | Cubic bezier |
| S/s | Smooth Curve | Smooth cubic bezier |
| Q/q | Quadratic | Quadratic bezier |
| T/t | Smooth Quad | Smooth quadratic |
| A/a | Arc | Elliptical arc |
| Z/z | ClosePath | Close subpath |
All commands are normalized to cubic beziers internally for uniform processing.
parseCommands()
Get raw command array without normalization:
const commands = PathParser.parseCommands('M 0,0 L 100,100');
// [
// { type: 'M', values: [0, 0] },
// { type: 'L', values: [100, 100] }
// ]Arc to Cubic Conversion
Elliptical arcs are automatically converted to cubic bezier curves:
const cubics = PathParser.arcToCubic(
0, 0, // Start point
50, 50, // Radii
0, // X-axis rotation
true, // Large arc flag
true, // Sweep flag
100, 0 // End point
);
// Returns array of cubic bezier segmentstoPathData()
Convert parsed path back to SVG string:
const parsed = PathParser.parse('M 0,0 Q 50,100 100,0');
const pathString = PathParser.toPathData(parsed);
// "M 0,0 C 33.33,66.67 66.67,66.67 100,0" (normalized to cubic)ParsedPath Structure
interface ParsedPath {
subPaths: SubPath[];
bounds: Bounds;
}
interface SubPath {
segments: PathSegment[];
closed: boolean;
}
type PathSegment =
| { type: 'M'; p0: Vec2 }
| { type: 'L'; p0: Vec2; p1: Vec2 }
| { type: 'C'; p0: Vec2; p1: Vec2; p2: Vec2; p3: Vec2 }
| { type: 'Z'; p0: Vec2; p1: Vec2 };PathBaker
Tessellate paths into evenly-spaced point arrays.
bake()
import { PathBaker } from 'faster-motion';
const baked = PathBaker.bake('M 0,0 C 50,0 50,100 100,100', {
interval: 5, // Distance between points (default: 5px)
minPoints: 2, // Minimum points (default: 2)
includeTangents: true // Include tangent vectors (default: true)
});BakeOptions
| Option | Type | Default | Description |
|---|---|---|---|
interval | number | 5 | Target distance between baked points |
minPoints | number | 2 | Minimum number of points |
includeTangents | boolean | true | Calculate tangent vectors |
bakeParsed()
Bake an already-parsed path:
const parsed = PathParser.parse(pathString);
const baked = PathBaker.bakeParsed(parsed, { interval: 3 });BakedPath Structure
interface BakedPath {
points: Float32Array; // [x0, y0, x1, y1, ...] flattened
tangents: Float32Array; // [tx0, ty0, tx1, ty1, ...] normalized
distances: Float32Array; // Cumulative distance at each point
totalLength: number; // Total path length
bounds: Bounds; // Bounding box
pointCount: number; // Number of baked points
closed: boolean; // Whether path is closed
}How Baking Works
- Parse SVG path into bezier segments
- Tessellate beziers using recursive subdivision (Godot algorithm)
- Resample at even intervals for consistent spacing
- Calculate tangent vectors from point differences
- Build cumulative distance array for binary search
PathSampler
High-performance sampling with O(log n) binary search.
bake()
Convenience method that combines parsing and baking:
import { PathSampler } from 'faster-motion';
const baked = PathSampler.bake('M 0,0 L 100,100', { interval: 5 });sampleAt()
Sample at normalized progress (0-1):
const sample = PathSampler.sampleAt(baked, 0.5);
console.log(sample.x, sample.y); // Position
console.log(sample.angle); // Tangent angle (radians)
console.log(sample.tangentX); // Normalized tangent X
console.log(sample.tangentY); // Normalized tangent Y
console.log(sample.progress); // 0.5
console.log(sample.distance); // Distance in pixelsTry it: Sampling Visualizer
const baked = PathSampler.bake(pathData);
// Sample at multiple positions
for (let t = 0; t <= 1; t += 0.1) {
const sample = PathSampler.sampleAt(baked, t);
// Draw point at sample.x, sample.y
// Draw tangent line using sample.angle
}sampleAtDistance()
Sample at absolute distance:
const sample = PathSampler.sampleAtDistance(baked, 50); // 50px from startgetClosestPoint()
Find the closest point on the path to any coordinate:
const result = PathSampler.getClosestPoint(baked, mouseX, mouseY);
console.log(result.x, result.y); // Point on path
console.log(result.progress); // 0-1 position
console.log(result.distanceToPoint); // Distance from query pointgetSamples()
Get N evenly-spaced samples:
const samples = PathSampler.getSamples(baked, 20);
// Returns 20 evenly-spaced SampleResult objectsslice()
Extract a portion of the path as SVG string:
const sliced = PathSampler.slice(baked, 0.25, 0.75);
// Returns SVG path data for middle 50%reverse()
Create a reversed baked path:
const reversed = PathSampler.reverse(baked);getOffsetPoint()
Calculate offset position from a sample:
const sample = PathSampler.sampleAt(baked, 0.5);
const offset = PathSampler.getOffsetPoint(sample, 0, 20);
// Returns { x, y } 20px perpendicular to pathgetNormal()
Get perpendicular vector to tangent:
const sample = PathSampler.sampleAt(baked, 0.5);
const normal = PathSampler.getNormal(sample);
// Returns { x, y } perpendicular unit vectorSampleResult Structure
interface SampleResult {
x: number; // Position X
y: number; // Position Y
angle: number; // Tangent angle in radians
tangentX: number; // Normalized tangent X (-1 to 1)
tangentY: number; // Normalized tangent Y (-1 to 1)
progress: number; // 0-1 progress along path
distance: number; // Absolute distance from start
}Performance Considerations
O(log n) Binary Search
Sampling uses binary search on the pre-computed distance array:
// This is O(log n) regardless of path complexity
const sample = PathSampler.sampleAt(baked, 0.5);Pre-Baking for Repeated Sampling
Bake once, sample many times:
// Bake once (O(n) operation)
const baked = PathSampler.bake(pathData);
// Sample repeatedly (O(log n) each)
for (let frame = 0; frame < 1000; frame++) {
const sample = PathSampler.sampleAt(baked, frame / 1000);
}When to Use PathResource
Use PathResource instead of direct sampling when:
- You need to share the path across animations
- You want serialization support
- You need a simpler API
// Direct sampling - more control
const baked = PathSampler.bake(pathData);
const sample = PathSampler.sampleAt(baked, 0.5);
// PathResource - simpler, shareable
const resource = new PathResource({ path: pathData });
const sample = resource.getPointAt(0.5);Bake Interval Tradeoffs
| Interval | Points | Memory | Accuracy |
|---|---|---|---|
| 1px | Many | High | Maximum |
| 5px (default) | Moderate | Low | Good |
| 10px | Few | Minimal | Approximate |
// High accuracy (more memory)
PathBaker.bake(path, { interval: 1 });
// Balanced (default)
PathBaker.bake(path, { interval: 5 });
// Low memory (approximate)
PathBaker.bake(path, { interval: 10 });TypeScript Support
import {
PathParser,
PathBaker,
PathSampler,
ParsedPath,
BakedPath,
BakeOptions,
SampleResult,
ClosestPointResult,
PathSegment,
SubPath
} from 'faster-motion';
const parsed: ParsedPath = PathParser.parse('M 0,0 L 100,100');
const options: BakeOptions = { interval: 5, includeTangents: true };
const baked: BakedPath = PathBaker.bakeParsed(parsed, options);
const sample: SampleResult = PathSampler.sampleAt(baked, 0.5);
const closest: ClosestPointResult = PathSampler.getClosestPoint(baked, 50, 50);See Also
- PathResource - High-level path API
- PathFollow - Animate elements along paths
- TrimPath - Animate path stroke visibility