motion-lab

The piece contract

Every piece in this repository follows the same contract. The tools in tools/ depend on nothing else, so a piece that honours it renders, verifies and shows up in the README gallery without touching any tool.

Start from template/index.html: it already contains the player block, the helpers and a working __motion object.

1. One folder, one file

projects/NN-slug/index.html

2. The canvas

3. window.__motion

window.__motion = {
  title: 'Fifteen-second showreel',        // short, English, shown in the README
  description: 'Kinetic type, shape morphs and a synth beat.',  // one sentence
  fps: 30, width: 1920, height: 1080,
  frames: 540,                             // 450..900 (15..30 s)
  poster: 270,                             // optional: frame used for the README still (default frames/2)
  gifStart: 4,                             // optional: second where the README GIF starts (default 0)
  seek(i) {},                              // draws frame i, synchronously
  async audio() {},                        // resolves to the whole soundtrack as an AudioBuffer
};

seek(i) — the only way a frame is drawn

audio() — the whole soundtrack, offline

4. The player block

Copy it from the template unchanged. It gives every piece the same behaviour in a browser:

The player is the only place a clock is allowed.

5. Style rules

6. Definition of done

node tools/render.mjs NN-slug     # out/NN-slug.mp4
node tools/still.mjs  NN-slug     # out/NN-slug.png (poster frame)
node tools/gif.mjs    NN-slug     # out/NN-slug.gif (960 px, 6-8 s, <= 3 MB)
node tools/verify.mjs NN-slug     # exits 0

or all four at once: node tools/all.mjs NN-slug. Then look at a few frames (node tools/still.mjs NN-slug 30 200 400 → out/stills/) before calling it finished. If verify fails, the piece is fixed — never the tool.

7. Variants (optional): the vertical cut

A piece may also carry a 9:16 cut for Reels, Shorts and TikTok. It is an optional sub-object; a piece without it renders and verifies exactly as before.

window.__motion = {
  /* ...the landscape piece as above... */
  variants: {
    vertical: {
      width: 1080, height: 1920,
      frames: 453,                   // 360..600 (12..20 s)
      canvas: verticalCanvas,        // its own 1080x1920 canvas (required)
      poster: 300, gifStart: 7,      // optional, as above
      seek(i) {},                    // same rules as the piece's seek: synchronous, pure in i
      async audio() {},              // 2 ch, 48 kHz, exactly frames / fps seconds
    },
  },
};
node tools/all.mjs NN-slug --variant vertical