FasterMotion
API ReferencePath Animation

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

CommandNameDescription
M/mMoveToStart new subpath
L/lLineToStraight line
H/hHorizontalHorizontal line
V/vVerticalVertical line
C/cCurveToCubic bezier
S/sSmooth CurveSmooth cubic bezier
Q/qQuadraticQuadratic bezier
T/tSmooth QuadSmooth quadratic
A/aArcElliptical arc
Z/zClosePathClose 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 segments

toPathData()

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

OptionTypeDefaultDescription
intervalnumber5Target distance between baked points
minPointsnumber2Minimum number of points
includeTangentsbooleantrueCalculate 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

  1. Parse SVG path into bezier segments
  2. Tessellate beziers using recursive subdivision (Godot algorithm)
  3. Resample at even intervals for consistent spacing
  4. Calculate tangent vectors from point differences
  5. 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 pixels

Try 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 start

getClosestPoint()

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 point

getSamples()

Get N evenly-spaced samples:

const samples = PathSampler.getSamples(baked, 20);
// Returns 20 evenly-spaced SampleResult objects

slice()

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 path

getNormal()

Get perpendicular vector to tangent:

const sample = PathSampler.sampleAt(baked, 0.5);
const normal = PathSampler.getNormal(sample);
// Returns { x, y } perpendicular unit vector

SampleResult 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

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

IntervalPointsMemoryAccuracy
1pxManyHighMaximum
5px (default)ModerateLowGood
10pxFewMinimalApproximate
// 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

On this page