Skip to content

Paint, tour and play animations - #1483

Merged
milanb17 merged 7 commits into
mainfrom
mb/animations-component-redesign-e33a0d
Nov 4, 2026
Merged

milanb17 merged 7 commits into
mainfrom
mb/animations-component-redesign-e33a0d

Conversation

@milanb17

@milanb17 milanb17 commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Six commits that rework how agents author and readers experience the animation block, and add its sibling, clip.

  1. Reference animations and a browser harness. Eight worked animations under packages/review/instructions/animations, each a draft folder with a README. The authoring instructions end with a References section; a browser test mounts every reference and steps every frame (VITE_ANIMATION_SHOTS=1 saves a screenshot per frame).
  2. Authoring guidance fixes. Hand-made layouts, moving elements between frames, keyframe start values, and selector vs card id wording in the typings.
  3. Code tour. An animation whose frames carry code gets a tour. Expand becomes Tour, the panel and the frames move together both ways, and inline code opens at the frame showing.
  4. Paint-in on landing. A streamed animation no longer pops in behind the generic wipe: the draw queue gets a painting phase, the stage protocol gets paint/painted, and a default painter brings parts in over ~1.5s. Authors can order, exclude or restyle parts, or replace the painter. Cold loads, history and reduced motion do not paint.
  5. The clip block. One continuous timeline the reader plays, pauses and jumps around by markers. New schema type (gated with animations), runtime API, protocol messages, a shared host shell over a frame player and a clip player, drafts as markers.json, and a reference clip.
  6. Live-board fixes. A streamed clip's paint-in survives its first marker jump, the draw queue keeps its head under StrictMode, and stages under the system theme follow the reader's preference.

Verification

  • pnpm lint, pnpm format:check, canvas app typecheck: clean.
  • Node tests (tour, draw queue) and browser tests (animation folder, blocks, draw queue): 119 passed.
  • Phases 3–6 checked in the browser harness and on the hosted cloudboard viewer (paint-in and clip timing over a live stream).
  • Not yet checked in an isolated Desktop: themes, narrow layout, keyboard navigation and reduced motion for the tour overlay and the clip player.

Eight worked animations under instructions/animations, each a draft folder
plus a README: a request across layers, a state machine, a cache filling
and evicting, a bounded queue with workers, retry with backoff, a two-lane
race, a structure before and after, and a log replay. The animation
instructions end with a References section naming each one and its
read-only copy beside the drafts, written by the MCP client where the
agent's files live; a reference that fails to parse is left out there and
fails the tests, which insert every reference through the API.

The canvas harness mounts each reference on a board, steps every frame of
every scenario, and with VITE_ANIMATION_SHOTS=1 saves a screenshot per
frame, so an author sees the result without Desktop.

Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f
The eight reference animations turned up where the guidance misled their
authors. The animation instructions now name the third way to draw, a
hand-made layout with arrows, tokens, travels and timelines declared while
the script first runs, and the lifecycle attributes that need no script;
they say how to move an element between frames, that every keyframe needs
its start value for a jump to land, and that whiteboard.frames follows the
active scenario. The API reference says where a string is a selector and
where it is a card id, and the class guide names the card's head and dot.

Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f
An animation whose frames list code now has a tour: one stop per frame of
the path showing, a frame's code as the stop's source and its caption as
an explanation stop otherwise. The header's Expand becomes the diagrams'
Tour control, which opens the fullscreen tour overlay with the figure as
the stage and the guided tour panel beside it; an animation with no code
keeps its plain Expand. The panel and the frames move together both ways:
a stop picked in the panel shows its frame, and a frame reached by the
controls, playback, the stage or a scenario switch moves the panel, which
stays open on a scenario switch at the frame showing. Inline, a drawn
element's code opens in the column with the tour at the frame showing,
and while the column shows this tour it follows the frames and its own
steps move them.

Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f
An animation the agent writes used to pop onto the board behind the generic
landing wipe. Now the block arrives from the stream with its stage built but
unseen; when the draw queue reaches it the host says "paint", the stage
brings the first frame in part by part, and the queue holds on the block
until the stage reports it painted, or until a cap.

- Draw queue: a "painting" phase for animation inserts, ended early by
  painted(), with the slot ring but no content wipe on the figure.
- Stage protocol: StageInit.paint, a "paint" host message, a "painted"
  stage message, and a long fallback so a stage never stays blank.
- Default painter for the kit's parts in document order within about a
  second and a half: boxes settle in, arrows draw along their path.
- data-wb-paint orders a part or excludes it; data-wb-paint-style picks
  fade, draw, wipe or none.
- whiteboard.on("paint") and whiteboard.timeline("paint") replace the
  default painter; the hook also runs instantly on a cold load.

Cold loads, history and reduced motion do not paint.

Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f
An animation is frames the reader steps through. A clip is its sibling:
one continuous timeline the reader plays, pauses, and jumps around by
named markers, for motion where the timing itself is the point. The two
share the sandboxed stage, the runtime, the draft checkout, the host
component, the code tour and the paint-in.

- Schema: clip {title, html, markers: {key, title, caption?, code?}[],
  scenarios?: {key, label}[], code, height, pins}; the Animations
  preference gates it with the animation.
- Runtime: whiteboard.clip(scenario?) builds a timeline with .marker();
  marker handlers, whiteboard.markers and whiteboard.marker; the frame
  API throws in a clip with a pointer to markers. Lifecycle attributes
  follow the marker last reached. Reduced motion steps marker to marker.
- Protocol: mode, markers and scenario in the init; play, pause, seek and
  scenario from the host; clip, time, marker and ended from the stage.
- Host: the block is now a shell over a frame player and a clip player
  with shared controls; a Markers strip, a progress line with a tick per
  marker, Space to play, arrows between markers; the tour's stops are
  the markers; a rebooted stage resumes at its marker.
- Drafts check a clip out as markers.json; a reference clip,
  request-timing, runs in the harness; the authoring guide and typings
  cover clips.

Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f
A clip jumps to its first marker as it activates, without motion, and
that jump finished every animation on the page a frame later, including
the paint-in a streamed clip had just begun: on a live board a new clip
appeared at once. Paint animations now sit out that finish, as they do a
frame shown without motion.

The draw queue lost its head on a live board too. An animation released
the queue when it unmounted, which under StrictMode's simulated unmount
ran at mount; and updaters told the time themselves, so one React ran
again ended a step early. The release on unmount goes (the cap covers a
block that leaves mid-paint), every updater reads the clock before it is
queued, and a control that would change nothing queues nothing.

A stage under the system theme read "light dark" as dark on a light
page; it now follows the reader's preference.

Agent-Session: b315a4f8-1333-461c-9be6-366ceb231b7f
@milanb17 milanb17 changed the title Rethink the animation block: references, code tour, paint-in and the clip block Rework the animation block and add clips Nov 2, 2026
@milanb17 milanb17 changed the title Rework the animation block and add clips Paint, tour and play animations Nov 2, 2026
@milanb17
milanb17 added this pull request to stack #1505 November 4, 2026 02:36
…nt-redesign-e33a0d

Agent-Session: 62100749-c77c-4d19-ad48-c6d4ba1e867b
Agent-Session: cbacb871-2558-4fe4-9272-77524bb21683
Agent-Session: ffed49d6-ca86-4940-b93c-e8eb7e0de440
Agent-Session: 7bc797d9-b46c-48dc-b281-4fce85dc15ad
@milanb17
milanb17 merged commit 1b8b76b into main Nov 4, 2026
3 checks passed
@milanb17
milanb17 deleted the mb/animations-component-redesign-e33a0d branch November 4, 2026 04:55
milanb17 added a commit that referenced this pull request Nov 4, 2026
Stacked on #1483 (the references and the scene API live there).

An animation's `whiteboard.scene` draws every card as the same box, so a
reader cannot tell a database from the service in front of it, or a
third-party API from one of ours, without reading each title. This gives
cards three presentational options and leaves the kinds out of the API:
the typings and the authoring instructions say which form fits what, and
two references draw their cards that way.

![request-across-layers: a window for the client, bars for the API and
service, a teal cylinder for
Postgres](https://raw.githubusercontent.com/devdotfast/whiteboard/pr-assets/scene-card-forms/scene-request-across-layers.png)

![cache-fill-evict: the cache as a teal drum holding its items, Postgres
as a
cylinder](https://raw.githubusercontent.com/devdotfast/whiteboard/pr-assets/scene-card-forms/scene-cache-fill-evict.png)

## The options

On `SceneCardOptions` (actor, store, table), and as `data-form`,
`data-outline` and `data-color` on a hand-made `wb-node`:

| Option | Values | Drawn as |
| --- | --- | --- |
| `form` | `box` (default), `pill`, `cut`, `cylinder`, `drum`, `stack`,
`window`, `bars` | border radii, `corner-shape: bevel`, a lid
pseudo-element for the cylinder and the drum, a box-shadow stack, a
title-bar rule, side rules |
| `outline` | `solid` (default), `dashed`, `dotted` | border style |
| `color` | `ink`, `accent`, `teal`, `violet` | border colour at rest
(and the card's dot); a frame's `tone` takes over while set |

`teal` and `violet` join the stage theme as `--wb-teal` (the software
map's storage colour) and `--wb-violet` (the merged purple), so both
follow light and dark. Highlights, tones, `enter`/`exit`/`only` and the
card's content work unchanged on every form; the stack keeps its echoes
under the highlight ring.

The option is called `form` rather than shape because lint reserves
"shape" in identifiers.

## Suggested defaults, not a schema

`whiteboard.d.ts` and the scene section of `animation.md` suggest:
`window` for a client, `bars` for a service or worker of this codebase,
`cylinder` for a database, `drum` for a cache, `stack` for a queue or
topic, `pill` for a start or end, `cut` for a branch, `outline:
"dashed"` for a third-party service, `teal` for stores and `violet` for
queues. Nothing in the runtime knows those words.

The request-across-layers and cache-fill-evict references use them;
their READMEs say so.

## Verification

- Every animation browser test, including the references harness
stepping all nine references through every frame: 30 passed. The stage
kit variables test passes with the two new colours.
- `pnpm lint`, `pnpm format` and the canvas typecheck (which holds
`whiteboard.d.ts` to the runtime's types) are clean.
- The screenshots are frames the harness saved
(`VITE_ANIMATION_SHOTS=1`), dark theme. Not checked: light theme and an
isolated Desktop. The screenshots live on `pr-assets/scene-card-forms`,
as `pr-assets/home-pr-pills` did; delete that branch with this PR.


## Second commit: a frame's remaining time along its step underline

While an animation plays, the marker under the step showing is drawn
left to right over the frame's duration, on a wash of the full
underline, so the one mark that says "this step" also says how much of
it is left. Frames without labels sweep a ring around the current dot.
Designed first on the "Play timer" page of the Animations Scratchpad
Paper file.

- Only while playing: a frame reached by hand draws its underline whole,
as before.
- Pause holds the line where it is and Play resumes from there; a jump
to another frame forgets it; Replay starts over.
- Time up with the stage still settling holds the line full.
- The strip and dots draw it with a CSS animation from inline custom
properties (`--frame-timer-fill`, `--frame-timer-ms`,
`--frame-timer-rest`), so nothing ticks in React. Reduced motion shows
the whole underline at once, as today.
- Two browser tests in `blocks.browser.test.tsx`: play runs the clock on
the current step, pause holds it with a fraction between 0 and 1, a hand
step drops it; and an unlabelled animation sweeps its current dot.


## Third commit: steady height, ported from #1459

#1459 could not merge onto the animations branch mechanically, since the
block it changed had split into a frame player, a clip player and a
paint-in. Its changes are ported here by hand: the hidden measuring copy
(`measure` / `slotSizes` on init, `measured` back), `contentBottom` and
`measureFrames` in the runtime, the block's `measuredHeight` rule with
its one-second fallback and remeasure triggers (stage, scenario, theme,
width, slot heights), and the caption row keeping its tallest caption,
now in `StopCaption` for both players. Clips are not measured. Its tests
come along; the animation and block browser suites pass (97). #1459 can
be closed once this merges.


## Fourth commit: an expanded animation scales up

Expand gave the stage the overlay's width but never made the drawing
bigger. Now the stage keeps its inline width fullscreen and the block
scales the whole canvas, code peeks included, to fit the room the
controls leave, at most 2x, centred; a room smaller than the inline
stage scrolls as before. The measured-height logic is untouched because
the stage still lays out at one width. One browser test expands a block
in a wide host and checks the stage scales above 1x, keeps its inline
layout width, and its footprint grows with it.

Seen while checking, not from these commits: a scene with four declared
columns folded to two rows at a 614px stage keeps a 150px stage height,
so the second row is clipped, inline and expanded alike. Reproduced on
the commit before the steady-height port. Worth its own look.


## Fifth to seventh commits: scene layout, pacing, groups, change marks
and a legend

- **Cards size to their content**, 140 to 300px, centred in their
columns, so a folded scene no longer stretches a card into a slab around
a line of text.
- **Edges are straight and level**: a fanned pair's offset moves the
whole line, and scene rows centre their cards so an edge between a short
card and a tall one runs level.
- **Play replays a still frame**: a new stage's first frame, or the
frame after a jump or Replay, is shown again with its motion when Play
reaches it. A send's chip travels at half its old speed, 900 to 2400 ms,
after a short lead. One browser test covers the replay.
- **`s.group({ id, at, title, color })`** draws a washed, titled box
behind the cards in its cells; hidden when the scene folds.
- **`change: "added" | "modified" | "removed"`** on a card marks what
the review did to that part: dot and left stripe in the change colour,
struck title when removed. A frame's tone still colours the card over
it.
- **`s.legend([...])`** draws one row under the scene, a card-shaped
glyph with the entry's marks and a label each.
- The typings and `animation.md` describe all of it;
request-across-layers groups its server, marks the API as modified and
carries a legend. The cylinder form's text sits centred in its body.


## Later commits: layout room, routing, the state-only dot, an
architecture reference

- Storage-form labels centre in their drum or cylinder; the card dot
shows only with a tone or change mark.
- A new reference, `video-platform-architecture`: four grouped layers,
forms, a dashed CDN and a legend, with upload and playback scenarios.
- An edge bows clear of a card by the card's reach across the line, not
its longest side, and a card near the line is passed toward the stage's
middle; a frame hides other frames' chips.
- A grouped scene opens its rows and columns to 72px so the layers stand
apart and cross-layer edges can be followed.
- Fullscreen, the stage always lays out at its inline width and scales
to fit, down to 0.4x beside the tour pane, so it never folds, loses its
groups, or measures a folded height.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants