๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.๐Ÿšง Under construction โ€” I'm migrating this site from Framer to Next.js and publishing it early for testing, so a lot of the content is still in flux.
Open menu
Switch to Darkhello@product.inc
Writing

Motion as data: keyframes as a source file

ยท Alex Zapadenko

Describe motion as data in a source file rather than producing it by hand in a motion tool or capturing it off a screen. A scene becomes an object with an id, the screen it animates, a loop duration and a block of keyframes โ€” and the clip is rendered from that description rather than recorded. The clips on this site are made that way: 24 scenes across 13 case studies, 88 keyframe blocks in total.

What a scene actually is

The whole definition of a piece of motion is a small object that lives next to the study it belongs to:

{
  id: 'gridfill',
  screen: 'recons',
  period: 4.6,
  name: 'Reconciliations โ€” grid fills in',
  css: `
    .fchip{animation:blChip 4.6s ease-out infinite}
    @keyframes blChip{0%{opacity:0;transform:translateY(-6px)}
                      7%{opacity:1;transform:translateY(0)} ... }`,
}

There is no binary, no timeline file, no export step from another application. Changing when the filter chips land is editing a percentage in that block, and it shows up in review as a one-line diff with a comment next to it explaining the intent. That is the main practical difference from every motion tool I've used: the reasoning survives, because it can sit in the file as a comment rather than in someone's memory of why the easing was chosen.

How it gets rendered

The renderer opens the screen in a headless browser, then does something that sounds obvious and mostly isn't: it pauses every animation on the page and sets the timeline position explicitly for each frame.

document.getAnimations().forEach((a) => a.pause());
// then, per frame i of N:
const t = (i / frames) * period * 1000;
document.getAnimations().forEach((a) => { a.currentTime = t; });

Each frame is screenshotted at that exact time, and the sequence is encoded in one pass with ffmpeg โ€” H.264, CRF 19, preset slow, yuv420p, +faststart. Frame count is simply period ร— 30.

Three things follow from that, and they're the reasons to do it this way.

The loop is seamless by construction. Time steps across exactly one period, so the last frame lands immediately before the first one comes round again. Nobody trims the end of a recording by eye to hide a jump, because there is no jump to hide.

Resolution stops depending on your display. The page is rendered at a device scale factor of 3, so the output is three times the artboard regardless of what monitor the person running it happens to own. A screen recording is bounded by the hardware it was captured on, and it carries whatever the compositor did to the pixels on the way.

The clip inherits system changes. Because the motion is described against the same screens and the same token contract as everything else, changing a brand's colour and re-running the renderer produces the same movement in the new palette. A recording made in October is a fossil of October's design; this is a function of the current one.

What it costs

It only covers motion you can express in CSS and the Web Animations API. Interface motion โ€” things appearing, moving, growing, settling โ€” fits comfortably. Character animation, illustration, anything with real craft in the curves, does not, and pretending otherwise would produce worse work than hiring someone who owns a motion tool.

You also give up direct manipulation. Scrubbing a timeline and feeling when an ease is wrong is genuinely faster than editing percentages and re-rendering, and I don't think there's a good answer to that beyond keeping the render loop quick enough that the difference stays tolerable.

And it presumes the screens are code. If your design lives in a design tool and gets handed to engineering afterwards, none of this is available to you, because there's nothing to step through.

One practical wrinkle worth knowing if you build something similar: every frame has to come out the same size or the encoder rejects the sequence. Studies whose pages genuinely scroll are taller than the device box, so the artboard height gets measured once before the loop and the viewport is grown to match โ€” which is only safe because these animations move opacity and transforms rather than layout.

When to reach for it

If motion is a handful of recurring interface behaviours that need to stay consistent across a lot of surfaces, describing them as data pays for itself quickly. If motion is the product's craft โ€” a title sequence, a mascot, anything where the curve itself is the work โ€” it doesn't, and the tool that lets someone feel their way to the right timing is the correct instrument.

Related: why generation touches tokens, never screens, how I work.