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.
projects/NN-slug/index.html
NN is a two-digit number, slug is lowercase kebab-case (01-showreel).<script src>, no stylesheet, no image, no font file, no
fetch. A page that makes any request other than data: / blob: fails
verify.system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif
(and ui-monospace, "Cascadia Code", Consolas, Menlo, monospace for code).
Glyphs drawn as paths inside the file are fine.<title> that matches __motion.title (a — motion-lab suffix is fine).<canvas width="1920" height="1080">. It is the first <canvas> in
the document, or the one you expose as __motion.canvas.{ preserveDrawingBuffer: true } and draw
inside seek(i); the time uniform comes from i, never from a clock.ctx.drawImage(glCanvas, 0, 0)) inside seek(i), so the captured canvas
is complete.window.__motionwindow.__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 drawnseek(i) returns, frame i is on the canvas.i. The result must not depend on which frame was drawn before.
Frames are rendered in parallel, out of order, and re-drawn during
verification. verify draws random frames twice with other frames in between
and requires identical pixels.Math.random, Date.now,
performance.now and new Date() are forbidden inside seek (verify
counts calls to them). Use a seeded PRNG — mulberry32(seed) is in the
template — and re-create it from a fixed seed (or from i) every time you
need it.seek call, never by leaving the
previous frame on the canvas.fillRect of your
background). Reset ctx.setTransform, globalAlpha, filter,
globalCompositeOperation and shadowBlur before you return.i is an integer in [0, frames - 1]; clamp it defensively.verify flags a frame whose luma is uniform or near-black). A fade to
black for the loop should stop just short of pure black or keep a texture.audio() — the whole soundtrack, offlinePromise<AudioBuffer>: 2 channels, 48 000 Hz, exactly
frames / fps seconds (Math.round(48000 * frames / fps) samples), built
with new OfflineAudioContext(2, length, 48000) and startRendering().DynamicsCompressorNode, convolution from a
generated impulse. No sample files, no licensed audio, no network.at = frame / fps), so sound and picture cannot drift.verify fails a buffer
that clips or is silent.Copy it from the template unchanged. It gives every piece the same behaviour in a browser:
requestAnimationFrame → seek);Space pauses; ?frame=N shows a single frame (handy for stills);?variant=vertical plays the piece’s vertical cut instead (see 7);window.__MOTION_RENDER__ = true before the page
loads, the player does nothing except seek(0).The player is the only place a clock is allowed.
ctx.font, ctx.letterSpacing) so it is
kerned and anti-aliased. Nothing a viewer must read under 28 px.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.
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
},
},
};
verify --variant vertical checks this on 7 frames: no
run of 3 pixels brighter than luma 70 may lie outside.drawn in code · bytepatterns.com.--variant vertical and writes out/NN-slug.vertical.mp4,
.png and .gif (540 px wide, ≤ 2 MB):node tools/all.mjs NN-slug --variant vertical