Agent progress

When to use
Use when the feature is an AI agent doing several steps for the viewer: it reads, traces, patches and tests, and the proof is visible labour. A card springs in, four rows rise 70 ms apart, an arc turns beside a status line that swaps whole states, and the rows flip from numbered outlines to solid check circles one by one while the bar advances. The film ends mid-list, with the last row still turning, so the work reads as ongoing. No prompt is typed and no cursor is needed. Clip: agent-progress.mp4. Demo: demo/agent-progress.html.
The code
.badge { position: relative; width: 6.4vh; height: 6.4vh; }
.badge > * { position: absolute; inset: 0; }
.done { border-radius: 50%; background: var(--done); opacity: 0; scale: 0.4; } /* the solid circle, hidden until its check */
.done path { stroke-dasharray: 24; stroke-dashoffset: 24; } /* the tick draws on one dash */
.lab i { position: absolute; inset: 54% 0 auto; height: 0.35vh; background: var(--muted); transform: scaleX(0); transform-origin: 0 50%; }Easing and timing
The clip runs 3.2 s. The look is daylight.
The clip uses 4 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.
| Ease | Leaving a key | Arriving at a key |
|---|---|---|
land | 12% influence, speed 4.8 | 75% influence, speed 0 |
landSoft | 20% influence, speed 2.5 | 75% influence, speed 0 |
launch | 33% influence, speed 0 | 25% influence, speed 3 |
pop | 12% influence, speed 4.8 | 35% influence, speed -1.5 |
Notes
import { EASE } from '../../core/motion/presets.js';
const at = { rows: 420, checks: [1250, 1800, 2350], swaps: [420, 1250, 2000] }; // ms
rows.forEach((r, i) => r.animate([{ translate: '0 1.8vh', opacity: 0 }, { translate: '0 0', opacity: 1 }], { duration: 380, delay: at.rows + i * 70, easing: EASE.landSoft, fill: 'both' }));
spin.animate([{ rotate: '0deg' }, { rotate: '900deg' }], { duration: 3200, easing: 'linear', fill: 'both' }); // finite, never infinite
// a slot swaps whole states: the old one leaves in 120 ms, the next rises in 260 ms
slot.forEach((el, i) => {
el.animate([{ opacity: 0, translate: '0 0.9vh' }, { opacity: 1, translate: '0 0' }], { duration: 260, delay: at.swaps[i], easing: EASE.landSoft, fill: 'both' });
if (at.swaps[i + 1]) el.animate([{ opacity: 1 }, { opacity: 0, translate: '0 -0.9vh' }], { duration: 160, delay: at.swaps[i + 1] - 160, easing: EASE.launch, fill: 'forwards' });
});
// a check lands: outline out, circle pops with overshoot, tick draws, label strikes and dims
at.checks.forEach((k, i) => {
num[i].animate([{ opacity: 1, scale: 1 }, { opacity: 0, scale: 0.6 }], { duration: 160, delay: k, easing: EASE.launch, fill: 'forwards' });
done[i].animate([{ opacity: 0, scale: 0.4 }, { opacity: 1, scale: 1 }], { duration: 380, delay: k, easing: EASE.pop, fill: 'forwards' });
tick[i].animate([{ strokeDashoffset: 24 }, { strokeDashoffset: 0 }], { duration: 260, delay: k + 100, easing: EASE.landSoft, fill: 'forwards' });
strike[i].animate([{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }], { duration: 300, delay: k + 60, easing: EASE.landSoft, fill: 'forwards' });
});The state change is the point: the viewer watches rows turn from a number into a result. Give the checks uneven gaps (550 ms here, then 550) and start them after the last row has arrived, so arrival and work never overlap. The status line and the counter swap whole states, never a crossfade of two half visible words. The bar advances in the same frames as the check, on the same curve. Keep the last row unchecked: a full list ends the work and the eye stops, an unfinished one keeps it. Use one working accent for the machine (arc, bar) and a second colour only for done; strike and dim the label to --muted, never to --faint, so it still reads.
Looks (ground, type, radius, accent) live in demo/demo.css; this snippet keeps neutral tokens.
Sound
Sound: pluck at 1.25 s into the move, when the first check lands (default gain); no tick per check.





