vaweGitHub

MovesReveal a title

Echo trail

look: dusk2.6 secho-trail.md

Echo trail: final frame of the text reveal animation
Tap the clip to play it.

When to use

Use when one element flies in fast and stops on a beat, and you want the After Effects Echo effect: five time-offset copies trail it with falling opacity, then fold into it on the hit frame. Clip: echo-trail.mp4. Demo: demo/echo-trail.html.

Use on the one moving object of the beat. Copies are the same element shown a few frames in the past, never hand-placed, so the trail bends wherever the path bends.

The code

js
import '../../core/engine/page-api.js';
import { easeFn } from '../../core/motion/presets.js';
const HIT = 1.05, FLY = 0.85, N = 5, LAG = 0.035, SPREAD = 0.22;   // copy i shows the lead's frame i * LAG ago
const copies = Array.from({ length: N }, () => {
  const c = lead.cloneNode(true);
  c.removeAttribute('id'); c.setAttribute('aria-hidden', 'true');
  c.textContent = '';                                         // copies carry the tile shape only, never the glyph
  return c;
});
[...copies].reverse().forEach((c) => lead.before(c));         // the oldest copy sits lowest, the lead on top
const ease = easeFn('carry');                                 // accelerates into the hit
const pos = (t) => {                                          // the one path, a pure function of time
  const u = ease(Math.min(1, Math.max(0, (t - (HIT - FLY)) / FLY)));
  return { x: (1 - u) * -620, y: (1 - u) * 260 };
};
const squash = (t) => (t < HIT ? 0 : Math.exp(-14 * (t - HIT)) * Math.cos(34 * (t - HIT)));
vawe.onFrame((t) => {
  const collapse = Math.min(1, Math.max(0, (t - (HIT - SPREAD)) / SPREAD));   // 0 to 1 over the last SPREAD before the hit
  copies.forEach((c, i) => {
    const p = pos(t - (i + 1) * LAG * (1 - collapse));        // the offset in time closes to zero on the hit frame
    c.style.translate = `${p.x}px ${p.y}px`;
    c.style.opacity = 0.6 * (1 - i / N) * (1 - collapse);     // the oldest copy is the faintest
  });
  const p = pos(t), s = squash(t);
  lead.style.translate = `${p.x}px ${p.y}px`;
  lead.style.scale = `${1 - 0.18 * s} ${1 + 0.18 * s}`;       // a short squash recoil on the hit
});

Easing and timing

The clip runs 2.6 s. The look is dusk.

The clip uses one named ease 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
carry33% influence, speed 012% influence, speed 4.8

Notes

  • The squash after the hit stays a damped cosine: it rings several times, which no EASE segment does.
  • Each copy calls the lead's own pos(t - offset). For a path written as WAAPI keyframes, give each copy the same animation with a later delay (i * 35 ms) instead: the copy then lags the same way.
  • The trail is widest where the lead is fastest, so the ease accelerates into the stop. A slow ease leaves the copies stacked on the lead and there is nothing to see.
  • Keep N * LAG divided by SPREAD under 1 (5 x 0.035 over 0.22 is 0.8). Above 1 a copy would run backwards in time while it closes up. Close the gap late and fast, in the last 0.22 s.
  • Opacity falls per copy (0.6 down to 0.12) and all copies reach 0 on the hit, so no ghost outlives it.
  • The copies are clones of the lead with the glyph removed (textContent = ''): a trail of bare tile shapes reads as motion, where cloned letters read as ghost text. Insert them oldest first so the nearest copy sits just under the lead and the lead, fully opaque, hides them on the hit frame.

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

Sound

Sound: a thud or pluck at the hit second, 1.05 s into the move (default gain).