---
name: procedural_animation_canvas
description: Make real animation in HTML Canvas 2D with code: direct motion with timing and easing craft, prototype timing on hard stepped frames first, run a clean fixed-timestep engine, and export a working MP4 through the browser (captureStream + MediaRecorder with a frame-perfect stepping path and ffmpeg fallback). Use when someone wants programmatic 2D animation, motion graphics from code, bouncing/flying/squashing motion, or recording a canvas animation to a video file.
---

# Procedural Animation in Canvas 2D

## Purpose

Teach an agent to make Canvas 2D animation that feels directed, not generated:
motion with deliberate timing and easing, a cheap prototype loop for judging
timing before smoothing, a small engine that keeps simulation deterministic,
and an export path that produces a real, playable MP4. The engine lives in
`assets/engine.js`; the craft lives in the references.

## Workflow

1. **Write the move as data, not as drawing code.** Build a timeline of named
   channels with `makeTimeline().add('x', {from, to, duration, delay, ease})`.
   Duration is the feeling; set it before anything else. See
   `references/direction-guide.md` for how to choose timings, easings, and
   where anticipation/squash/arcs belong.
2. **Prototype on hard frames.** Before smoothing, judge the timing with
   `stepPlayback({fps: 6, duration, render})` or swap the real easing for
   `steps(6)`. Fix durations and delays while motion is still cheap. If the
   stepped version reads, smoothing will work; if it does not, no curve
   will save it.
3. **Write one `drawScene(t, ctx)` function.** It takes seconds, draws the
   whole frame, and touches nothing else. The timeline gives you values;
   `drawScene` turns them into pixels. This same function drives the live
   preview, the step preview, and the export, so they can never drift apart.
4. **Preview live.** `startLoop({canvas, update, render})` runs simulation on
   a fixed timestep (accumulator, spiral-of-death clamp) with interpolated
   rendering, so motion is deterministic at any display refresh rate.
5. **Export.** Frame-perfect path first: `recordFrames({width, height, fps,
   duration, renderFrame})` steps a hidden canvas through every frame and
   pushes each one manually with `requestFrame()`. If that is unavailable,
   `recordLive(canvas, {fps, duration})` records the live canvas. Both
   negotiate codecs via `pickMime()`: native MP4 on Chrome/Edge 126+ and
   Safari 14.1+, WebM on Firefox. WebM output gets transcoded with the
   ffmpeg command in `references/export-pipeline.md`. Open
   `assets/demo.html` (a working bounce: live, step, and record buttons)
   to see the whole loop end to end.

## Output Contract

- Deliverable: an MP4 (h264, yuv420p, +faststart) that plays everywhere, or
  a WebM plus the ffmpeg command that converts it.
- The animation is defined by a timeline and one `drawScene(t, ctx)`; live
  preview and export share that function, never two parallel implementations.
- Export resolution and fps are set explicitly in the export call, not read
  from the screen.

## Operating Rules

1. Never animate with raw per-frame increments (`x += 1`). Every move is a
   tween with a duration and an easing. Increments make motion frame-rate
   dependent and undirectable.
2. Ease out of starts, ease into stops. `linear` is for machines (wheels,
   conveyors), never for starts and stops.
3. One overshoot per scene. `backOut` and `easeOutElastic` fight each other
   if you stack them.
4. Prototype timing on hard frames (step 2) before committing to curves.
   Changing durations after the fact is a rewrite of the motion.
5. Never record the canvas the user interacts with. Export drives a hidden
   canvas; the visible one is a preview.
6. Never promise MP4 on a browser that cannot make it. `pickMime()` decides;
   Firefox exports WebM, and that is correct behavior, not a failure.
7. Engine math stays DOM-free. The easing/tween/timeline core in
   `assets/engine.js` has zero DOM dependencies and is covered by
   `assets/test-engine.js` (`node test-engine.js`); run it after any change.
