Gesso
Brand

Beat machine

Place a style library's objects on a grid, snapshot beats, and play the story back on the relief stage.

The stage already knows how to tell a story: a beat is a state, and the maths in between draws the travel. What it lacked was somewhere to author one. The beat machine is that place. You pick a style library, put its objects on a cell grid, pose them, set the camera, and snapshot beats in a row. Clicking through them shows each one with the neighbours ghosted in, so a move reads as a move before it ever plays.

The output is a stage. Nothing here is a second renderer, and a story authored in the machine plays back through the same ReliefStage the marketing site and the app use.

The same machine runs full screen at /beat-machine, on the same saved document, so a story authored there shows up here.

The model

PieceWhat it is
LibraryKinds of object plus five animation presets. Relief, Markers, Characters and Lit iso ship; a document can place objects from every library that is loaded.
KindA generator and the handful of numbers that drive it. Every kind builds deterministically from its params and a seed.
ObjectOne kind, its params, and a home cell.
BeatA state: which objects are shown, where each one is posed, the camera, one line of subtext, and how long to hold.
TransitionHow a beat is entered from the one before: duration, ease, a preset, and an optional stagger.
PresetThe personality of a transition, as data. Where an object enters from, where it leaves to, and whether a move overshoots or bumps.

Edits land on the beat you are looking at

Click a beat and you are editing it. Move an object, change the camera, pick a preset, and that beat carries the change. Drag an object across the grid with the pointer and it snaps cell by cell, staying on the level it is already on. Snapshot copies the current beat in right after itself and selects the copy, so the story keeps going from where you are. Onion skin draws the beat before and the beat after at a fraction of their opacity, which is what makes a move legible while you set it.

An object added while beat three is selected exists from beat three on. Earlier beats do not know about it, so it enters with that beat's preset rather than having always been there.

The grid

Ground is a lattice of cells, columns and rows on each side of the origin, and levels climb from it: five above and five below by default. Relief's stage space has z as up, so "level" is a cell's z. Cells are placement, not bounds. An object wider than a cell simply overlaps its neighbours, and a beat can nudge one with at in world units if a cell is too coarse.

With a grid present the camera frames the lattice rather than whatever happens to be visible, which keeps the framing still while objects move. Zoom and pan per beat do the rest.

Four libraries, one stage

Pass every library the rail should offer, and a document can mix them. Relief is the landscape; Markers are the things that point at it; Characters are figures that walk the grid rather than being extruded from it; Lit iso is the first richer look lane — shaded volumes on the same tip camera. Brands bring their own libraries, and Mark in motion is the Ramson mark as two more. A document names one library as its own, which sets the grid defaults and lets its presets go by their bare ids, and any object from another library carries that library's id with it. Presets from the other libraries are picked by library:id, so a marker can drop in with markers:drop while a fox arrives with characters:arrive and a hall rises with lit-iso:rise.

Markers stand up. A pin, a flag's banner and a plate's card sit on a right face, so each is a thin plate seen edge on, with brand colour on its edges and never as a fill.

KindWhat it is
PinA map pin standing on its tip.
ArrowFlat on the ground, pointing along x.
HaloRings hovering over a spot, stacked by altitude.
FlagA banner on a post.
PlateA callout card on a short post.
ChevronsA run of chevrons that fans apart when exploded.

Its five presets: Drop falls in from three levels up with a small bounce, Hoist rises from below while it grows, Point swings in with a turn, Pulse pops from nothing and swells on a move, and Wipe slides in from the left.

Characters, not landscapes

Use an extruded library (Relief, Markers) when the object is the drawing: stacked plates, a pin, a flag. Use Characters when the object needs authored clips — a walk cycle, an idle, a short burst — that extrusion cannot pose. The figure still sits on a cell, still takes a level, still eases with the beat's preset. The isometric camera does not change. What changes is the geometry path: assembly.mesh instead of extruded blocks, and an AnimationMixer that plays named clips.

Beat motion picks a role, and the role looks up a clip name on the kind:

RoleFox clipWhen
enterWalkThe object becomes visible on this beat
leaveSurveyThe object is leaving; opacity already fades
emphasiseRunThe object is named in the beat's emphasise list
walkWalkThe pose moves between two visible beats
idleSurveyHeld, nothing else asking

The seeded story on this page (and at /beat-machine) places one Fox on the grid. It is hidden on the opening beats, walks in on someone, runs when that beat emphasises it, and idles as it leaves if you loop back to the start. Add another from the Characters rail the same way you add a pin. The last beat, place, brings a lit-iso hall and plus onto the same grid.

The Khronos Fox sample is CC-BY 4.0: model by PixelMannen (CC0), rigging and animation by tomkranis, glTF conversion by AsoboStudio and scurest. It lives at /beat-machine/fox.glb. If that file is missing, the stage keeps a block figure with the same clip names, so the mixer and the beat map still run. A second kind, Figure, is that stand-in on purpose — useful when you want the hybrid without fetching a mesh.

Point assembly.mesh.src at any other glTF and keep the clip map in sync with that file's animation names. There is no retargeter and no clip editor; this spike stops at mixer plus beat triggers.

Its five presets: Arrive walks in from the left, Bound drops from two levels up, Turn spins in, Hush fades in place, and Dart overshoots a move.

Look lanes

Relief, lit-iso, and later soft-3D and flat, are a family of look lanes. They share the cell grid, the beats, the libraries and the JSON. They differ in how an object is drawn. Characters sit beside that family, not inside it: a mesh kind opts out of extrusion with assembly.mesh and plays clips.

PathReads asWhat the kind returns
ReliefExtruded plates: outline, solid, or wireblocks and placements
Lit isoShaded volumes under a soft keylook: "lit-iso" and volumes
CharactersA figure with authored clipsassembly.mesh
Soft-3D / flatNext siblingsNot this spike

Lit iso is the first wedge past plates. Same orthographic tip camera, same stage, richer materials: MeshStandardMaterial with roughness, a height mix toward surfaceSunken so stacks read grounded, and a hemisphere-plus-key rig on the same scene. Outline plates use unlit materials, so the extra lights do not wash them. The canvas stays on-demand — lit-iso objects are static volumes, so they do not start the mixer loop Characters need.

Placing a lit-iso object on /beat-machine

The seeded story adds a Hall and a Plus on the last beat. From the Lit iso rail you can add a Tower or another Pin the same way you add a slab. Pick a cell with the arrow keys, snapshot, and play. lit-iso:rise and lit-iso:pop are the presets that beat uses; Unfold fans floors and the plus bar apart because volumes carry explode offsets.

Albedo follows kit surface roles, mixed toward ink, because paper fills sit at white and a lit white volume has no shade. Hemisphere sky and ground tints follow surfaceRaised and surfaceSunken. Accent is still not a fill — windows mix a little into emissive, because --primary is often ink and would black out a volume if used as the whole emissive. Roughness, glow intensity, and the key's direction are authored numbers; they do not retune with CSS. Selecting a lit-iso object tints toward accent rather than replacing the fill, for the same reason.

KindWhat it is
HallA civic block: plinth, storeys, pyramidal roof, door and two windows.
TowerFloors that step in, capped with a drum.
PinA standing drop — cone and sphere, not the Markers plate.
PlusA pictogram plus that stands on the cell.

Its five presets: Rise comes up from two levels down, Unfold arrives exploded and seals, Slide comes in from the left, Turn spins in, and Pop grows from nothing.

Five presets

Every library ships five. Relief's:

PresetEntersMovesLeaves
SettleRises from two levels down, fading up, with an ease out.Eases out.Sinks two levels and fades.
UnfoldArrives exploded and seals.Fans the plates apart mid-way, then seals.Explodes as it goes.
SweepSlides in from three cells to the left and overshoots a little.Overshoots a little.Slides out three cells to the right.
SpinA quarter turn in while it grows and fades up.Slight overshoot.A quarter turn out.
BloomGrows from nothing with a pop.Swells a touch mid-way.Shrinks to nothing.

A beat names one preset for everything that changes, and any object can override it. With no preset, objects fade and moves ease, which is the quiet default.

Playback

Each beat eases in over its transition and then holds. The channel above the beat strip is that timeline: each beat's slice is its card, with the transition drawn lighter than the hold, and the playhead runs down over the cards as the story plays. Drag anywhere in the channel to scrub, or focus it and use the arrow keys. Play runs the whole story, and Loop wraps it. Under prefers-reduced-motion playback cuts to each beat at the start of its transition, so the story still lands beat by beat with none of the travel.

Export video records the story once through, off the live canvas, and downloads a WebM (or an MP4 where WebM is not available). It records the authored motion even under reduced motion, because the artifact is a file rather than the screen.

JSON, in and out

The document is JSON and round-trips. Copy document puts the whole thing on the clipboard; Paste loads one back; Download saves the file. Copy beats only gives you just the grid, camera and beats, which is the part a hand-written stage wants.

Pass storageKey and the machine autosaves to localStorage, so a reload picks the session up where it was. Reset drops back to the document you started with.

To play a story without the machine, resolve the document to a stage:

import { ReliefStage } from '@/components/relief/stage';
import { createAssemblyCache, parse, toStage } from '@/components/beat-machine/lib/document';
import { charactersLibrary } from '@/components/beat-machine/libraries/characters';
import { litIsoLibrary } from '@/components/beat-machine/libraries/lit-iso';
import { markersLibrary } from '@/components/beat-machine/libraries/markers';
import { reliefLibrary } from '@/components/beat-machine/libraries/relief';

const stage = toStage(
  parse(json),
  [reliefLibrary, markersLibrary, charactersLibrary, litIsoLibrary],
  createAssemblyCache(),
);

<ReliefStage stage={stage} beat={2} />

Keyboard

Click the stage to focus it, then:

KeysDoes
← → ↑ ↓Move the selected object one cell.
⇧ ↑ / ⇧ ↓Raise or lower it one level.
SpacePlay or pause.
[ ]Previous or next beat.
NSnapshot.
DeleteRemove the selected object.
⌘ Z / ⇧ ⌘ ZUndo and redo, anywhere in the machine.

With the channel focused, ← → scrub a tenth of a second (a second with ⇧), Home and End go to either end, and PageUp PageDown step the beats.

Writing a library

A library is data plus one function per kind:

import type { StyleLibrary } from '@/components/beat-machine/lib/types';

export const paperLibrary: StyleLibrary = {
  id: 'paper',
  label: 'Paper',
  grid: { cell: 100, columns: 6, rows: 3, levels: 5 },
  presets: [/* five AnimationPresets */],
  kinds: [
    {
      id: 'card',
      label: 'Card',
      description: 'A folded sheet.',
      fields: [{ key: 'width', label: 'Width', min: 80, max: 400, step: 10, default: 240 }],
      create: (params) => ({ id: 'card', blocks: [/* … */], placements: [/* … */] }),
    },
  ],
};

create returns a relief Assembly. Extruded kinds fill blocks and placements. A mesh kind (a character) returns an assembly with empty blocks and a mesh spec instead — same object on the grid, a different geometry path on the stage. A lit-iso kind sets look: "lit-iso" and fills volumes (boxes, cylinders, cones, spheres, or an evenodd outline stood up as extrude) that the stage shades instead of drawing as plates. Presets are still plain AnimationPreset data, which is why a document plays back the same anywhere.

Usage

import { BeatMachine } from '@/components/beat-machine';
import { charactersLibrary } from '@/components/beat-machine/libraries/characters';
import { litIsoLibrary } from '@/components/beat-machine/libraries/lit-iso';
import { markersLibrary } from '@/components/beat-machine/libraries/markers';
import { reliefLibrary } from '@/components/beat-machine/libraries/relief';

<div className="h-[720px]">
  <BeatMachine
    libraries={[reliefLibrary, markersLibrary, charactersLibrary, litIsoLibrary]}
    storageKey="my-story"
  />
</div>

library takes a single one instead. The first library listed is the one a new document belongs to.

The machine fills its container, so give it a height. Below about 880px of width it stacks the rail, stage and inspector rather than sitting them side by side.

Accessibility

  • The stage is a keyboard surface: click or tab to it, and the arrow keys, Space, brackets and N do what the strip and transport buttons do. Every shortcut has a button, so nothing is reachable only by key. Dragging is pointer-only; the arrow keys and the inspector's steppers are the keyboard path to the same placement.
  • Beats announce as they change through a polite live region, using the beat's subtext, which is also the stage's accessible description.
  • Onion skins are decorative and never announced. Thumbnails are decorative too.
  • Under prefers-reduced-motion the transport cuts between beats instead of easing, and selecting a beat cuts to it.
  • Every control is a real button, switch, select or range with a label. The rail and the strip use aria-pressed for the current selection.

Install

shadcn add @gesso/beat-machine