# Animation craft

## Timeline rules that keep frame-by-frame rendering exact

The renderer calls `window.seek(t)` for every frame, and stills jump around in time. So:

- Set every initial state with `gsap.set` before building the timeline, then use `.to()` tweens
  and `.set()` at explicit times. `.from()` and a plain `.fromTo()` render immediately at build
  time and fight earlier tweens on the same property; a `.fromTo()` is fine with
  `immediateRender: false`.
- Anything the timeline can't tween (a video or image-sequence frame, text typed into a field, a
  canvas, which one of several states is showing) is computed per frame inside `window.seek`, from
  the time alone, never from the previous frame.
- No callbacks, timers or CSS animations/transitions; anything not driven by the timeline will
  not be at the right state when a frame is captured.
- Put beat start times in one table (`T`) and write tween times relative to it, so cutting or
  lengthening a beat is a one-line change.
- End the timeline with an empty tween at `DURATION - 0.01` so the last frames exist.

## Things that look broken on video (each cost a review round)

- **Slow zooms on text or UI shimmer**: every frame re-rasterizes, glyphs snap between pixels and the
  picture seems to wobble. Enlarge with a static transform, move fast (≤0.6 s) or translate only.
- **Two states at once** (a status line showing "Thinking" under "Speaking"): competing tweens on
  the same element. Compute mutually exclusive states per frame from a list of changes.
- **Hidden things showing**: a child with `visibility: visible` ignores its hidden parent (use
  `inherit`); a hidden headline still casts its `text-shadow` outside its mask (hide the layer).
- **Wrong colours on overlay text**: class names like `.a` collide with an embedded mock page's CSS.
  Prefix your own classes.
- **Text that pops on or off**: always rise, roll or type text in, and animate its background in
  and out; a hard cut away from a text slide reads as a glitch.
- **Captions too fast to read**: word-by-word text needs 0.16–0.22 s per word and a 1–2 s hold.

## Headlines

- Each line is its own mask (`overflow: hidden`) and the text rises into it. Give the mask
  `padding-bottom: 0.24em; margin-bottom: -0.24em`, otherwise g, y, p and q are clipped. This
  bites every headline, not just one.
- Hidden state `yPercent: 140`, exit `yPercent: -130`: far enough that no ascender peeks.
- Lines enter over 0.8 s with a 0.12 s stagger and exit over 0.45 s with `power2.in`, 0.1 s before
  the next beat, so two headlines never share the screen.

## Transitions without double exposure

Two semi-transparent layers on top of each other (a phone fading out over a window fading in,
two different menus cross-fading inside a phone) look like a mistake. Instead:

- Exit the old element fast (≈0.38 s, `power2.in`) and start the new one when it is gone.
- Swap different content inside a device with a wipe (clip-path + a thin accent beam), not a
  cross-fade. Cross-fades are only for the *same* screen with one value changed.
- Reset hidden state (scroll positions, transforms) with `.set()` while the element is invisible.

## Pointing at things

- **Cursor + click ripple** for desktop UI: move 0.6 s `power2.inOut`, press by scaling the cursor
  to 0.86 for 0.08 s, ripple scales 0.5 → 1.7 while fading.
- **Rings** (accent border, soft outer glow) for fields and values, drawn after the click.
- **Callout with a caret** when a value changes: a white card with a label, the old value struck
  through (the strike line scales in), an arrow and the new value in the accent. Put the callout
  in the phone's own coordinate space so it moves and scales with the phone; the caret tip sits
  on the bottom centre of the value's ring.
- **Pulse** the ring once (scale 1 → 1.18 → 1) at the moment the value changes.

## Composition

- 16:9: copy column on the left (x 140–900), visual on the right in a soft grey panel; the UI
  window may bleed off the right edge but the part that matters (the clicked element, the
  highlighted value) must stay in frame.
- 4:5: copy on top, visual below; phone and window bleed off the bottom/right edge so they can
  be larger. Drop the corner brand mark; give the headline the space.
- Walls of outputs: 7 × 4 cards, `rotateX(26deg) rotateZ(-13deg)` with perspective, drifting
  slowly; fade the side (16:9) or top (4:5) behind the copy to the canvas colour.
- End card: brand-colour circle wipe from the last focal point, then logo, headline, chip,
  button; a cursor presses the button at the same 0.96 scale the site's buttons use.

## Legibility math

On-screen size = canvas px × (display width ÷ canvas width). A 16:9 video shown 960 px wide
halves every size; on a 390 px phone it is ×0.18, so 92 px headlines become 17 px and 24 px pills
become 4 px. That is why phones get the 4:5 cut: at 350 px wide it is ×0.32, 104 px headlines
become 34 px and 34 px pills 11 px.

## Stills review checklist

- [ ] Descenders complete on every headline (g, y, p, q, j).
- [ ] No headline, pill or card overlaps the visual or another beat's text.
- [ ] Frames just after each beat change show one scene, not two half-faded ones.
- [ ] No placeholder left (striped boxes with labels).
- [ ] The cursor's tip lands on the element it clicks; rings and the callout caret sit on target.
- [ ] Nothing important is cut by a bleed edge.
- [ ] Readable at the real display size: shrink a still to that width and look again.
- [ ] Colours, font and logo match the product; the end card uses the brand colour.
