FasterMotion
API ReferencePath Animation

TrimPath Advanced

Stagger animations, sequential mode, and canvas integration

Advanced TrimPath features for complex animations including stagger effects, multi-path sequential mode, and canvas/fmtion integration.

Stagger Animations

Animate multiple paths with staggered timing.

TrimPath.stagger()

import { TrimPath } from 'faster-motion';

TrimPath.stagger('.stroke-paths', {
  start: 0,
  end: 100,
  duration: 1000,
  stagger: 150  // 150ms delay between each
});

Try it: Staggered Drawing

TrimPath.stagger('.line', {
  start: 0,
  end: 100,
  duration: 800,
  ease: 'easeOut',
  stagger: {
    each: 100,
    from: 'center'  // Start from middle elements
  }
});

StaggerOptions

OptionTypeDescription
eachnumberDelay between each element (ms)
from'start' | 'end' | 'center' | 'edges' | numberStarting position
grid[rows, cols]2D grid stagger pattern
// Stagger from center outward
TrimPath.stagger('.paths', {
  end: 100,
  stagger: { each: 100, from: 'center' }
});

// Stagger from specific index
TrimPath.stagger('.paths', {
  end: 100,
  stagger: { each: 50, from: 3 }  // Start from 4th element
});

// Grid stagger (for path grids)
TrimPath.stagger('.grid-paths', {
  end: 100,
  stagger: { each: 50, grid: [4, 6] }  // 4 rows, 6 columns
});

Parallel vs Sequential Mode

When animating multiple paths together, there are two modes:

Parallel Mode (Default)

Each path is trimmed independently using its own length.

// Both paths animate 0-100% simultaneously
// but each based on their own individual lengths
TrimPath.stagger([path1, path2], {
  start: 0,
  end: 100,
  duration: 1000
});

Sequential Mode

All paths are treated as one continuous path. Trim values are distributed based on relative lengths.

Try it: Sequential vs Parallel

// Sequential mode - paths draw one after another
// If 3 equal-length paths:
// - At 33% progress: First path fully drawn
// - At 66% progress: First + second paths drawn
// - At 100% progress: All three paths drawn

How Sequential Mode Works:

With 3 paths of lengths 100px, 120px, 100px (total: 320px):

Global EndPath 1 (31%)Path 2 (38%)Path 3 (31%)
0.2580% visible0% visible0% visible
0.50100% visible50% visible0% visible
0.75100% visible100% visible35% visible
1.00100% visible100% visible100% visible

This matches Lottie's "Trim Paths" behavior:

  • m=1 → Parallel (simultaneous)
  • m=2 → Sequential

Offset Wrap-Around

When the offset value causes the visible region to wrap around the path:

// Offset causes wrap-around
TrimPath.to('#path', {
  start: 0,
  end: 30,
  offset: 80,  // 80% offset
  duration: 1000
});
// Visible region: 80-100% AND 0-10%

The dash pattern is automatically adjusted to show two segments when wrap-around occurs.

Canvas TrimPath

For canvas-based animations using .fmtion files.

MorphablePath Trim Properties

Canvas path objects support trim via direct properties:

// In canvas context
path.trimStart = 0.25;  // 0-1 value
path.trimEnd = 0.75;    // 0-1 value
path.trimOffset = 0;    // 0-1 value (0 = 0°, 1 = 360°)

fmtion TrimPathTrack Schema

interface TrimPathTrack {
  id: string;
  name?: string;
  target: string;           // "canvas://object/path-id"
  simultaneous?: boolean;   // true = parallel, false = sequential
  enabled?: boolean;
  keyframes: TrimPathKeyframe[];
}

interface TrimPathKeyframe {
  time: number;      // Milliseconds
  start: number;     // 0-1
  end: number;       // 0-1
  offset: number;    // 0-1
  easing?: string;
}

Example .fmtion Track

{
  "trimPathTracks": [{
    "id": "trim-1",
    "target": "canvas://object/signature-path",
    "simultaneous": true,
    "keyframes": [
      { "time": 0, "start": 0, "end": 0, "offset": 0 },
      { "time": 1000, "start": 0, "end": 1, "offset": 0, "easing": "easeOut" }
    ]
  }]
}

Sequential Mode Utilities

FasterMotion exports utilities for calculating sequential trim values:

calculateSequentialPathInfo

Build path info with cumulative offsets.

import { calculateSequentialPathInfo } from 'faster-motion';

const pathLengths = [
  { id: 'path1', length: 100 },
  { id: 'path2', length: 150 },
  { id: 'path3', length: 50 }
];

const pathInfos = calculateSequentialPathInfo(pathLengths);
// Returns:
// [
//   { id: 'path1', length: 100, startOffset: 0 },
//   { id: 'path2', length: 150, startOffset: 0.333 },
//   { id: 'path3', length: 50, startOffset: 0.833 }
// ]

calculateSequentialTrimValues

Distribute global trim values to individual paths.

import { calculateSequentialTrimValues } from 'faster-motion';

const trimValues = calculateSequentialTrimValues(
  0.25,       // globalStart
  0.75,       // globalEnd
  0,          // globalOffset
  pathInfos
);

// Returns Map<string, { localStart: number, localEnd: number }>
trimValues.get('path1');  // { localStart: 0.75, localEnd: 1.0 }
trimValues.get('path2');  // { localStart: 0, localEnd: 0.83 }
trimValues.get('path3');  // { localStart: 0, localEnd: 0 }

calculateLocalTrim

Calculate local trim for a single path segment.

import { calculateLocalTrim } from 'faster-motion';

const { localStart, localEnd } = calculateLocalTrim(
  0.25,   // globalStart
  0.75,   // globalEnd
  0.333,  // pathStartOffset
  0.833   // pathEndOffset
);

Performance Tips

Pre-prepare Elements

For frequently animated paths, prepare them once:

// On page load
document.querySelectorAll('.animated-path').forEach(el => {
  TrimPath.prepare(el);
});

// Later, animate without setup overhead
TrimPath.to('.animated-path', { end: 100, duration: 500 });

Cache Path Lengths

For sequential mode with many paths:

// Calculate once
const pathInfos = calculateSequentialPathInfo(
  paths.map(p => ({ id: p.id, length: p.getTotalLength() }))
);

// Reuse for multiple animations
function animateSequential(endProgress) {
  const values = calculateSequentialTrimValues(0, endProgress, 0, pathInfos);
  // Apply values...
}

Avoid Re-measuring

Don't call getTotalLength() every frame - it's expensive:

// Bad - measures every update
TrimPath.to('#path', {
  onUpdate: () => {
    const length = path.getTotalLength();  // Expensive!
  }
});

// Good - measure once
const length = path.getTotalLength();
TrimPath.to('#path', {
  onUpdate: (state) => {
    const visibleLength = (state.end - state.start) * length;
  }
});

Common Patterns

Sequential Reveal of Logo Parts

const logoPaths = document.querySelectorAll('.logo-path');
const pathInfos = calculateSequentialPathInfo(
  Array.from(logoPaths).map((p, i) => ({
    id: `path-${i}`,
    length: p.getTotalLength()
  }))
);

// Animate as one continuous drawing
const proxy = { progress: 0 };
FasterMotion.dom({
  target: proxy,
  to: { progress: 1 },
  duration: 3000,
  onUpdate: () => {
    const values = calculateSequentialTrimValues(0, proxy.progress, 0, pathInfos);
    logoPaths.forEach((p, i) => {
      const trim = values.get(`path-${i}`);
      // Apply trim values to path
    });
  }
});

Infinite Loading Animation

TrimPath.to('#loader', {
  start: 0,
  end: 30,
  offset: 720,  // Two full rotations
  duration: 2000,
  ease: 'linear',
  repeat: -1
});

TypeScript Support

import {
  TrimPath,
  calculateSequentialPathInfo,
  calculateSequentialTrimValues,
  SequentialPathInfo,
  LocalTrimResult
} from 'faster-motion';

const pathInfos: SequentialPathInfo[] = calculateSequentialPathInfo([
  { id: 'p1', length: 100 },
  { id: 'p2', length: 200 }
]);

const values: Map<string, LocalTrimResult> = calculateSequentialTrimValues(
  0, 0.5, 0, pathInfos
);

See Also

On this page