vaweGitHub

MovesReveal a title

Text on path

look: signal3 stext-on-path.md

Text on path: final frame of the text reveal animation
Tap the clip to play it.

When to use

Use when a line must travel and settle on a curve: words ride an SVG path like beads on a wire, the lead word first, and stop where the line reads. The path is a rail, so the move gives a long S-curve to a title without a camera. Clip: text-on-path.mp4. Demo: demo/text-on-path.html.

startOffset is an attribute, not a CSS property, so Web Animations cannot drive it. Set it from window.seek(t), a pure function of the time. Each word is its own <text> on the same path.

The code

html
<svg viewBox="0 0 1920 1080">
  <path id="p" fill="none" stroke="var(--line)" d="M -260 820 C 260 820, 520 330, 1000 400 S 1560 740, 2200 380"/>
  <text><textPath href="#p">curve</textPath></text>   <!-- one text per word -->
</svg>

Easing and timing

The clip runs 3 s. The look is signal.

The clip uses 2 named eases from core/motion/presets.js. Each is a pair of After Effects handles: influence is how far along the segment the handle reaches, and speed is how fast the value moves at the key, as a multiple of the segment's average speed. Motion in the docs explains both.

EaseLeaving a keyArriving at a key
land12% influence, speed 4.875% influence, speed 0
launch33% influence, speed 025% influence, speed 3

Notes

js
await document.fonts.load('800 150px "Big Shoulders"');        // measure after the face is in
const widths = els.map((tp) => tp.parentNode.getComputedTextLength());
let x = 420 + (pathLen - 560 - run) / 2;                       // run = widths + gaps: the settled train, centred
const REST = widths.map((w) => { const s = x; x += w + GAP; return s; });
const land = easeFn('land'), arrive = (p) => land(clamp01(p));   // easeFn, not EASE: this is seek, not an easing string
const launch = easeFn('launch'), leave = (p) => launch(clamp01(p));
window.seek = (t) => els.forEach((tp, i) => {
  const lead = WORDS.length - 1 - i;                           // the front word goes first, so the train never overlaps
  const a = arrive((t - 0.15 - lead * 0.14) / 1.15), l = leave((t - 2.55 - lead * 0.04) / 0.3);
  tp.setAttribute('startOffset', (REST[i] - (1 - a) * 1500 + l * 1500).toFixed(2));
});
window.seek(0);

The numbers that make it look expensive

  • 1.15 s on EASE.land, 1500 units of travel: most of the distance is covered in the first quarter, and the last words glide in.
  • The front word starts first and the others follow 140 ms apart, so the gaps open as they enter and close as they settle, and no two words ever overlap.
  • The exit is 0.3 s on EASE.launch, in the same direction along the rail, again front word first and 40 ms apart: faster than the arrival.
  • Keep the slope under about 30 degrees where the line settles, or it will not read. The settled line sits in the flattest stretch of the path.
  • The rail is a hairline in --line, a guide rather than a device; drop it if the film already has a rule.
  • One word in the accent; contrast of every word is 4.5:1 or more on the ground.

The common mistake

Driving startOffset with a CSS animation or WAAPI: it does nothing, because the attribute is not animatable that way. Also a single <text> for the whole line moves all words as one rigid train and cannot stagger. Text beyond the end of the path is not drawn: start words off the path, not past it.

Looks (ground, type, radius, accent) live in demo/demo.css; this snippet keeps neutral tokens.

Sound

Sound: none; or a rising swish across 0.15 to 1.3 s, ending on the settle.