--- name: multiplane description: 'Use Multiplane to render HTML shots to video: write shot pages that follow the contract (`#stage` + `render(t)`), capture stills for inspection, run determinism checks, render MP4, and read machine-readable results. Use when asked to "make a video shot from a web page", "render HTML to video", "check whether a shot is deterministic", or "render / stills / verify with Multiplane". This skill covers usage and determinism constraints only, not visual design.' --- # Multiplane Multiplane is a local headless renderer: a shot is an ordinary HTML page whose image depends only on time `t`. Multiplane captures frames with headless Chromium, applies post-processing such as supersampling, motion blur, and film grain, and encodes video with FFmpeg. Your job has two parts: **write a page that follows the contract**, then **check and render it with the CLI**. ## Workflow 1. Write the shot page (see "Shot contract" below). 2. Run `multiplane lint shot.html --json`: it runs without a browser in under a second and flags patterns that break determinism or the contract, with line and column locations. Fix errors first. 3. Run `multiplane stills shot.html --at 0,1,2.5 --json`: capture a few stills to inspect the images. 4. Run `multiplane verify shot.html --json`: run a determinism check. **Do not render if it fails** (`ok: false`, code `nondeterministic_output`); fix the issues reported by `finding` first. A `gpu_raster_jitter` warning with `ok: true` is not a failure: GPU rasterization made a few scattered pixels differ by a level or two, which is invisible and not caused by the shot. Render as usual and mention it only if the user asks about determinism. 5. Run `multiplane render shot.html --json`: produce the video. `stills`, `verify`, and `render` automatically run lint before starting. Errors end the command with `lint_failed`, avoiding wasted rendering time. For environment problems (missing browser or FFmpeg), run `multiplane doctor --json` first. Each entry in `data.checks` has `ok` and a `detail`; when the command fails, the `result` also carries `code` and `hint`. Fix the failing checks before rendering. ## Set up a machine Use these commands when the user asks you to set up Multiplane, or approves a fix that `doctor` suggests. They change the user's machine-wide settings, so do not run `set`, `unset`, `install` or `remove` on your own initiative. - `multiplane config get --json` reads the user config; `data.overrides` shows environment variables that currently take priority. - `multiplane config set ffmpeg --json` saves the FFmpeg path and checks it at once: read `data.check.usable` and `data.check.reason`. An unusable path is still saved and produces a `warning`. Other keys: `raster` (`gpu` / `software`), `encoder` (`x264` / `nvenc-h264` / `nvenc-hevc`), `workers` (a positive integer, or `auto`), `jobsDir`. `multiplane config unset ` restores the default. - `multiplane skill status --json` reports this skill for Claude Code and Codex; `multiplane skill install --host claude|codex|all --json` installs or updates it. A copy the user has edited is reported as `modified` and is not replaced unless you pass `--force`, which backs it up first; ask the user before using `--force`. - When only the desktop app is installed, `multiplane` may not be on PATH. The app ships its own command: on Windows `\multiplane.cmd`, on macOS `/Applications/Multiplane.app/Contents/Resources/bin/multiplane`, and on Linux ` --multiplane-cli`. The app's Environment page shows the exact path. ## Shot contract Minimal shot: ```html Example
``` ### Stage - Include `#stage` in the page and draw everything inside it. Define the frame dimensions with **fixed CSS pixel width and height**. Do not let them vary with the viewport, time, or content. - Use only the width and height of `#stage` to define the frame dimensions; there are no separate width and height options. Common sizes: 1920×1080 (16:9), 1080×1920 (9:16), 1080×1080 (1:1), 1080×1350 (4:5). - Output pixels = logical dimensions × `--scale`, and must be positive even integers. For example, do not use `--scale 0.5` with 1080×1350 (it produces 675 rows). - `--scale` sets the browser's `devicePixelRatio`. DOM, SVG and text redraw at that resolution; a `` does not. Size every canvas by `devicePixelRatio` (`canvas.width = cssWidth * devicePixelRatio`, then `ctx.scale(devicePixelRatio, devicePixelRatio)`, CSS size unchanged), or it is only enlarged and looks soft at `--scale 2`. ### `Shot.define(spec)` | Field | Required | Meaning | |---|---|---| | `duration` | Yes | Duration in seconds. | | `render(t)` | Yes | Draw the image at second `t`. Use a **synchronous function**; do not return a Promise. | | `fps` | No | Frame rate, default 30. | | `fonts` | No | Fonts to wait for: `[['48px MyFont', 'text used'], ...]`. | | `setup()` | No | One-time preparation (load images, create a 3D scene, etc.). May return a Promise; the renderer waits for it before starting. | | `cuts` | No | Array of hard cut times (seconds). Motion blur time samples do not cross these times. **Declare every hard cut in the image**, or frames on either side of the cut will blur together. | | `post` | No | Post-processing defaults: `motionBlur` (time samples per frame; 1 disables it), `maxMotionBlur` (upper limit for adding samples where motion is fast; see below), `shutter` (shutter fraction of a frame; 0.5 means 180°), `grain` (film grain intensity; 0 disables it). Command-line options override these. | | `blurAt(t)` | No | Return the required number of time samples (a positive integer) at this time, using fewer samples for static sections and more for fast motion. | | `meta` | No | Any JSON data for tools that read the shot (beats, captions, notes), at most 1 MiB. Readable as `SHOT.meta`; returned by `inspect`; never affects pixels. | Use `Shot.ease` for `clamp`, `lerp`, `prog(t, start, dur)`, and several easing curves; use `Shot.size` for the stage's logical dimensions. You may also skip `Shot.define` and define `window.SHOT = { duration, fps, ready, render, cuts, post }` directly, where `ready` is a Promise that resolves when assets are ready. ### Resources - Keep the shot file inside the resource root (`--root`, the current directory by default). Relative paths in the page resolve from the shot file's location and must not point outside the root. - Treat paths starting with `/multiplane/` as reserved for the renderer's runtime libraries. - **Use local files for all resources**: point `@font-face` to local font files and list them in `fonts`. Do not reference remote fonts, CDN scripts, or remote images. - Finish loading images, fonts, and other assets in `setup()` / `ready`. Do not start loading in `render(t)`. ## Determinism constraints (most important) Multiplane calls `render(t)` out of order, repeatedly, and in parallel across multiple browser processes, then compares the pixels of each frame. **The same `t` must produce identical pixels, regardless of earlier calls or their count.** Do not use the following in `render(t)` or the code it calls: | Do not use | Reason | Use instead | |---|---|---| | `Date.now()`, `new Date()`, `performance.now()` | Wall-clock time changes on every call | Use only the parameter `t` | | `Math.random()`, `crypto.getRandomValues()` | Results change on every call | Use a fixed-seed pseudorandom generator (such as mulberry32), precompute in `setup()`, or derive values from `t` and the element index | | CSS `animation`, `transition`, `@keyframes`, Web Animations API | The browser advances them by wall-clock time | Set styles directly in `render(t)` | | `requestAnimationFrame`, `setTimeout`, `setInterval` driving the image | Same as above | Same as above; let the runtime handle preview playback, and do not write your own playback loop | | `