multiplane 0.3.3 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +327 -326
- package/README.md +327 -326
- package/package.json +92 -92
- package/skills/multiplane/SKILL.md +215 -213
- package/src/capture.mjs +16 -6
- package/src/events.mjs +5 -2
- package/src/hosted.mjs +1 -1
- package/src/post.mjs +59 -26
- package/src/render-pool.mjs +17 -7
- package/src/render-worker.mjs +6 -4
- package/src/render.mjs +5 -5
- package/src/server.mjs +7 -1
- package/src/stills.mjs +5 -4
- package/src/three/index.js +36 -14
- package/src/verify.mjs +4 -2
- package/src/workers.mjs +5 -1
|
@@ -1,213 +1,215 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: multiplane
|
|
3
|
-
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.'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Multiplane
|
|
7
|
-
|
|
8
|
-
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.
|
|
9
|
-
|
|
10
|
-
Your job has two parts: **write a page that follows the contract**, then **check and render it with the CLI**.
|
|
11
|
-
|
|
12
|
-
## Workflow
|
|
13
|
-
|
|
14
|
-
1. Write the shot page (see "Shot contract" below).
|
|
15
|
-
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.
|
|
16
|
-
3. Run `multiplane stills shot.html --at 0,1,2.5 --json`: capture a few stills to inspect the images.
|
|
17
|
-
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.
|
|
18
|
-
5. Run `multiplane render shot.html --json`: produce the video.
|
|
19
|
-
|
|
20
|
-
`stills`, `verify`, and `render` automatically run lint before starting. Errors end the command with `lint_failed`, avoiding wasted rendering time.
|
|
21
|
-
|
|
22
|
-
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.
|
|
23
|
-
|
|
24
|
-
## Set up a machine
|
|
25
|
-
|
|
26
|
-
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.
|
|
27
|
-
|
|
28
|
-
- `multiplane config get --json` reads the user config; `data.overrides` shows environment variables that currently take priority.
|
|
29
|
-
- `multiplane config set ffmpeg <path> --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 <key>` restores the default.
|
|
30
|
-
- `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`.
|
|
31
|
-
- When only the desktop app is installed, `multiplane` may not be on PATH. The app ships its own command: on Windows `<install directory>\multiplane.cmd`, on macOS `/Applications/Multiplane.app/Contents/Resources/bin/multiplane`, and on Linux `<path to the AppImage> --multiplane-cli`. The app's Environment page shows the exact path.
|
|
32
|
-
|
|
33
|
-
## Shot contract
|
|
34
|
-
|
|
35
|
-
Minimal shot:
|
|
36
|
-
|
|
37
|
-
```html
|
|
38
|
-
<!doctype html>
|
|
39
|
-
<html lang="en"><meta charset="utf-8"><title>Example</title>
|
|
40
|
-
<link rel="stylesheet" href="/multiplane/runtime/shot.css">
|
|
41
|
-
<style>
|
|
42
|
-
#stage { width: 1920px; height: 1080px; background: #111; }
|
|
43
|
-
#box { position: absolute; left: 0; top: 490px; width: 100px; height: 100px; background: #eee; }
|
|
44
|
-
</style>
|
|
45
|
-
<div id="stage"><div id="box"></div></div>
|
|
46
|
-
<script src="/multiplane/runtime/shot.js"></script>
|
|
47
|
-
<script>
|
|
48
|
-
const box = document.querySelector('#box');
|
|
49
|
-
Shot.define({
|
|
50
|
-
duration: 4, // seconds
|
|
51
|
-
fps: 30, // optional, default 30
|
|
52
|
-
render(t) { // draw the frame at time t; nothing else may influence it
|
|
53
|
-
const k = Shot.ease.inOutCubic(Shot.ease.prog(t, 0.5, 3));
|
|
54
|
-
box.style.transform = `translateX(${Shot.ease.lerp(0, 1820, k)}px)`;
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
</script></html>
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Stage
|
|
61
|
-
|
|
62
|
-
- 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.
|
|
63
|
-
- 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).
|
|
64
|
-
- 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).
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
| `
|
|
99
|
-
|
|
|
100
|
-
| `
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
"three
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
<
|
|
129
|
-
<
|
|
130
|
-
<
|
|
131
|
-
<script
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
- Use `
|
|
148
|
-
- Use `layer
|
|
149
|
-
-
|
|
150
|
-
-
|
|
151
|
-
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
|
160
|
-
|
|
161
|
-
| `multiplane
|
|
162
|
-
| `multiplane
|
|
163
|
-
| `multiplane
|
|
164
|
-
| `multiplane
|
|
165
|
-
| `multiplane
|
|
166
|
-
| `multiplane
|
|
167
|
-
| `multiplane
|
|
168
|
-
| `multiplane
|
|
169
|
-
| `multiplane
|
|
170
|
-
| `multiplane
|
|
171
|
-
| `multiplane
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
-
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
- Only
|
|
207
|
-
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
1
|
+
---
|
|
2
|
+
name: multiplane
|
|
3
|
+
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.'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Multiplane
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
Your job has two parts: **write a page that follows the contract**, then **check and render it with the CLI**.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Write the shot page (see "Shot contract" below).
|
|
15
|
+
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.
|
|
16
|
+
3. Run `multiplane stills shot.html --at 0,1,2.5 --json`: capture a few stills to inspect the images.
|
|
17
|
+
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.
|
|
18
|
+
5. Run `multiplane render shot.html --json`: produce the video.
|
|
19
|
+
|
|
20
|
+
`stills`, `verify`, and `render` automatically run lint before starting. Errors end the command with `lint_failed`, avoiding wasted rendering time.
|
|
21
|
+
|
|
22
|
+
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.
|
|
23
|
+
|
|
24
|
+
## Set up a machine
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
- `multiplane config get --json` reads the user config; `data.overrides` shows environment variables that currently take priority.
|
|
29
|
+
- `multiplane config set ffmpeg <path> --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 <key>` restores the default.
|
|
30
|
+
- `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`.
|
|
31
|
+
- When only the desktop app is installed, `multiplane` may not be on PATH. The app ships its own command: on Windows `<install directory>\multiplane.cmd`, on macOS `/Applications/Multiplane.app/Contents/Resources/bin/multiplane`, and on Linux `<path to the AppImage> --multiplane-cli`. The app's Environment page shows the exact path.
|
|
32
|
+
|
|
33
|
+
## Shot contract
|
|
34
|
+
|
|
35
|
+
Minimal shot:
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<!doctype html>
|
|
39
|
+
<html lang="en"><meta charset="utf-8"><title>Example</title>
|
|
40
|
+
<link rel="stylesheet" href="/multiplane/runtime/shot.css">
|
|
41
|
+
<style>
|
|
42
|
+
#stage { width: 1920px; height: 1080px; background: #111; }
|
|
43
|
+
#box { position: absolute; left: 0; top: 490px; width: 100px; height: 100px; background: #eee; }
|
|
44
|
+
</style>
|
|
45
|
+
<div id="stage"><div id="box"></div></div>
|
|
46
|
+
<script src="/multiplane/runtime/shot.js"></script>
|
|
47
|
+
<script>
|
|
48
|
+
const box = document.querySelector('#box');
|
|
49
|
+
Shot.define({
|
|
50
|
+
duration: 4, // seconds
|
|
51
|
+
fps: 30, // optional, default 30
|
|
52
|
+
render(t) { // draw the frame at time t; nothing else may influence it
|
|
53
|
+
const k = Shot.ease.inOutCubic(Shot.ease.prog(t, 0.5, 3));
|
|
54
|
+
box.style.transform = `translateX(${Shot.ease.lerp(0, 1820, k)}px)`;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
</script></html>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Stage
|
|
61
|
+
|
|
62
|
+
- 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.
|
|
63
|
+
- 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).
|
|
64
|
+
- 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).
|
|
65
|
+
- `--scale` sets the browser's `devicePixelRatio`. DOM, SVG and text redraw at that resolution; a `<canvas>` 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`.
|
|
66
|
+
|
|
67
|
+
### `Shot.define(spec)`
|
|
68
|
+
|
|
69
|
+
| Field | Required | Meaning |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `duration` | Yes | Duration in seconds. |
|
|
72
|
+
| `render(t)` | Yes | Draw the image at second `t`. Use a **synchronous function**; do not return a Promise. |
|
|
73
|
+
| `fps` | No | Frame rate, default 30. |
|
|
74
|
+
| `fonts` | No | Fonts to wait for: `[['48px MyFont', 'text used'], ...]`. |
|
|
75
|
+
| `setup()` | No | One-time preparation (load images, create a 3D scene, etc.). May return a Promise; the renderer waits for it before starting. |
|
|
76
|
+
| `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. |
|
|
77
|
+
| `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. |
|
|
78
|
+
| `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. |
|
|
79
|
+
| `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. |
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
### Resources
|
|
84
|
+
|
|
85
|
+
- 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.
|
|
86
|
+
- Treat paths starting with `/multiplane/` as reserved for the renderer's runtime libraries.
|
|
87
|
+
- **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.
|
|
88
|
+
- Finish loading images, fonts, and other assets in `setup()` / `ready`. Do not start loading in `render(t)`.
|
|
89
|
+
|
|
90
|
+
## Determinism constraints (most important)
|
|
91
|
+
|
|
92
|
+
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.**
|
|
93
|
+
|
|
94
|
+
Do not use the following in `render(t)` or the code it calls:
|
|
95
|
+
|
|
96
|
+
| Do not use | Reason | Use instead |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| `Date.now()`, `new Date()`, `performance.now()` | Wall-clock time changes on every call | Use only the parameter `t` |
|
|
99
|
+
| `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 |
|
|
100
|
+
| CSS `animation`, `transition`, `@keyframes`, Web Animations API | The browser advances them by wall-clock time | Set styles directly in `render(t)` |
|
|
101
|
+
| `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 |
|
|
102
|
+
| `<video>`, `<audio>` playback, animated images such as GIFs | Playback position is not controlled by `t` | Extract individual frames in advance and select a frame by `t` |
|
|
103
|
+
| State that depends on the previous call (accumulated positions, per-frame particle integration, "if the previous frame was...") | With out-of-order or parallel calls, the previous call is not the previous frame | Write a closed-form function of `t`; for simulations, precompute the full result with fixed time steps in `setup()` and only look up values in `render(t)` |
|
|
104
|
+
| An `async` `render`, waiting for loading in `render` | The image may be incomplete when captured | Put all loading in `setup()` / `ready` |
|
|
105
|
+
|
|
106
|
+
The runtime uses wall-clock time for its own preview playback. You do not write that part, and it does not affect rendering.
|
|
107
|
+
|
|
108
|
+
`lint` detects most direct patterns in the table above, but not indirect calls (such as `const now = Date.now; now()`) or accumulated state (such as `x += 1`). So **only a passing `verify` counts** (a pass with `gpu_raster_jitter` warnings still counts).
|
|
109
|
+
|
|
110
|
+
If a prohibited pattern is necessary and you have confirmed that it cannot affect the image (for example, a log only in `setup()`), suppress it with a comment on the line **immediately above** it. Include the rule code:
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
// multiplane-disable-next-line wall_clock
|
|
114
|
+
console.log('setup took', performance.now() - started);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use `/* multiplane-disable-next-line css_animation */` in CSS and `<!-- multiplane-disable-next-line media_playback -->` in HTML. Do not use suppression to bypass issues that actually affect the image. Also, pixel equality holds only on the same machine, with the same browser version, fonts, and assets. Changing the GPU or browser version may produce small differences.
|
|
118
|
+
|
|
119
|
+
## 3D layer (optional)
|
|
120
|
+
|
|
121
|
+
Use `multiplane/three` when you need 3D cards or depth of field. When the renderer is the desktop app's bundled command, three.js 0.180.0 is already included. With the npm package, install `three@0.180.0` in the project (`npm install three@0.180.0`). `multiplane doctor` reports whether three.js is available, its version and where it comes from.
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<script type="importmap">{"imports":{
|
|
125
|
+
"three": "/multiplane/vendor/three/build/three.module.js",
|
|
126
|
+
"three/addons/": "/multiplane/vendor/three/examples/jsm/"
|
|
127
|
+
}}</script>
|
|
128
|
+
<link rel="stylesheet" href="/multiplane/runtime/shot.css">
|
|
129
|
+
<style>#stage { width: 1920px; height: 1080px; }</style>
|
|
130
|
+
<div id="stage"></div>
|
|
131
|
+
<script src="/multiplane/runtime/shot.js"></script>
|
|
132
|
+
<script type="module">
|
|
133
|
+
import { createThreeLayer } from '/multiplane/three.js';
|
|
134
|
+
const layer = createThreeLayer({ aperture: 0.12, focusDistance: 10 });
|
|
135
|
+
const card = layer.card({ src: './assets/card.png', width: 2.5, height: 2.5, position: [0, 0, 0] }); // your own local image
|
|
136
|
+
Shot.define({
|
|
137
|
+
duration: 3,
|
|
138
|
+
setup: () => layer.ready, // wait for textures
|
|
139
|
+
render(t) {
|
|
140
|
+
card.rotation.y = 0.3 * t;
|
|
141
|
+
layer.render({ t, focusDistance: 10 - t }); // always pass t
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
</script>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- Use `createThreeLayer(options)` to add a transparent WebGL canvas inside `#stage`, layered with ordinary HTML elements (`zIndex` controls stacking). Common options: `fov`, `focusDistance`, `aperture` (0 keeps everything sharp), `samples` / `maxSamples` (depth-of-field sample count, adaptive to the amount of blur by default), `quality` (`'final'` uses accumulated depth of field and is the default; `'preview'` is a fast approximation for drafts only).
|
|
148
|
+
- Use `style` on `createThreeLayer` for layer-wide defaults, or on a card to override them: `annotationFontFamily`, `annotationColor` (lines), `annotationTextColor`, `textureBackground`, `browserFrameColor`, `browserFrameFontFamily`. Font options take a single family name; the renderer waits for it to load and fails with `resource_failed` instead of falling back. A layer-wide `textureBackground` only fills cards that have `annotations` or `browserFrame`; plain cards keep their image's transparency unless the card's own `style` sets `textureBackground`.
|
|
149
|
+
- Use `layer.card({ src, width, height, position, rotation, radius, border, shadow, annotations, style, lodBias })` to get a three.js `Group`. Change its position and rotation in `render(t)`.
|
|
150
|
+
- `lodBias` (default 0) sharpens a card that is shown smaller than its texture, such as small text on a card facing the camera at about 0.6× size: try `-1`. Leave it at 0 for cards seen at a steep angle with fine repeating patterns, where a negative bias causes moiré.
|
|
151
|
+
- Call `layer.render({ t, ... })` inside every `render(t)` call and pass the same `t`. The renderer handles motion blur subsamples automatically.
|
|
152
|
+
- Use `layer.scene` and `layer.camera` as ordinary three.js objects and add your own meshes. Their state must likewise depend only on `t`.
|
|
153
|
+
- Use `apertureForBlurRadius({ radiusPixels, depth, focusDistance })` to convert a desired blur radius in pixels at a given depth into an aperture.
|
|
154
|
+
|
|
155
|
+
## Commands
|
|
156
|
+
|
|
157
|
+
Use `--json` with any command. Resolve shot paths relative to `--root` (the current directory by default).
|
|
158
|
+
|
|
159
|
+
| Command | Purpose | Common options |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `multiplane lint <shot>` | Static code checks without a browser | `--root` |
|
|
162
|
+
| `multiplane stills <shot>` | PNG stills at given times and a contact sheet; also for final images | `--at 0,1.5,3` (seconds) or `--every 0.5`; `--out` |
|
|
163
|
+
| `multiplane verify <shot>` | Determinism check: pixel comparisons out of order, after reload, and with single/multiple processes | `--at` (must fall on frames), `--workers` (default 2) |
|
|
164
|
+
| `multiplane render <shot>` | Render video | `--out`, `--from` / `--to` (seconds, end excluded), `--fps`, `--scale`, `--workers`, `--quality`, `--motion-blur`, `--max-motion-blur`, `--shutter`, `--grain`, `--no-post`, `--draft`, `--encoder`, `--audio`, `--no-static-reuse` |
|
|
165
|
+
| `multiplane inspect <shot>` | Read duration, frames, stage size, cuts and `meta` without rendering | `--fps`, `--root` |
|
|
166
|
+
| `multiplane serve` | Serve shots on 127.0.0.1 so the user can open them in a browser; stop it when done | `--root`, `--port` |
|
|
167
|
+
| `multiplane benchmark` | Measure the fastest worker count on this machine (2–3 minutes); renders then use it automatically | `--raster` |
|
|
168
|
+
| `multiplane doctor` | Check the environment | — |
|
|
169
|
+
| `multiplane config get\|set\|unset` | Read or change the user config | see "Set up a machine" |
|
|
170
|
+
| `multiplane skill status\|install\|remove` | Manage this skill for Claude Code and Codex | `--host`, `--force` |
|
|
171
|
+
| `multiplane status <job>` | View job status | — |
|
|
172
|
+
| `multiplane cancel <job>` | Stop a job | `--wait` (seconds) |
|
|
173
|
+
| `multiplane wait <job>` | Wait for a job started earlier and print its events | `--timeout` (seconds) |
|
|
174
|
+
|
|
175
|
+
Use `--out`, `--overwrite`, and `--no-lint` (skip automatic lint) with `stills`, `verify`, or `render`. Run `multiplane <command> --help` to list all options for that command.
|
|
176
|
+
|
|
177
|
+
Each run has its own job directory, `out/jobs/<jobId>/`. Without `--out`, artifacts are written there; use `artifacts` in the result for the paths. If `--out` points to an existing file, the command fails with `output_exists`. Add `--overwrite` when you intend to replace it (replacement happens only after the new artifact is fully written; on failure, the old file stays untouched).
|
|
178
|
+
|
|
179
|
+
FFmpeg is not bundled. It is found in this order: `--ffmpeg <path>`, the environment variable `MULTIPLANE_FFMPEG`, `ffmpeg` in the user config `config.json`, then the system PATH. It must support libx264. To save a path for every project, use `multiplane config set ffmpeg <path>` with the user's approval (see "Set up a machine").
|
|
180
|
+
|
|
181
|
+
Motion blur quality: the shot decides whether there is motion blur (`post.motionBlur` above 1, the starting number of samples); `--quality` decides how many samples fast frames get. The default `standard` compares adjacent samples block by block and starts at `motionBlur` but at most 4 (an explicit `--motion-blur` still sets the start; a limit no larger than the start keeps the count fixed), and doubles the count wherever something moved more than about 2 pixels, up to 32; `fast` is the same with a limit of 16; `high` starts at `motionBlur`, uses about 1 pixel and goes up to 256. Static frames drop to 1 sample. Use the default `standard` for iterating and for the final render: on real shots `high` takes several times longer for a difference visible only when zoomed in. Use `fast` when the user asks for speed over quality (quick reviews of long shots, renders that are not the final deliverable); it renders sooner and very fast motion shows slightly more visible steps. Use `high` only when the user asks, for stills that will be viewed up close, or when very fast thin motion still strobes under `standard`. Do not raise `motionBlur` for the whole shot to fix strobing; use `high`, or `--max-motion-blur` for a custom limit. `result.data.blurSamples` (`{ min, max, mean, histogram }`) shows the counts used. A shot with its own `blurAt(t)` keeps full control of the count.
|
|
182
|
+
|
|
183
|
+
When the user wants an image rather than a video (a cover, poster, thumbnail or illustration), write a shot and use `stills` for the final output: each PNG gets the same supersampling (`--scale 2`), motion blur, depth of field and `--alpha` as video. Pick a time during motion if the still should show motion blur. Hand over the `t-<seconds>.png` file from `result.data.stills`, not the contact sheet.
|
|
184
|
+
|
|
185
|
+
Use `--draft` with `render` or `stills` while iterating: it captures at a scale of at most 0.5 and upscales, with no motion blur and no grain, so it is faster and visibly lower in quality. Options you pass explicitly override the preset. Render the final video without `--draft`; `result.data.draft` is `true` when a draft was produced, so do not hand a draft over as the final result.
|
|
186
|
+
|
|
187
|
+
## Transparent output
|
|
188
|
+
|
|
189
|
+
When the result must be laid over other footage, render with `--alpha` and `--format webm` (VP9) or `--format mov` (ProRes 4444); MP4 cannot carry alpha and `--alpha` with MP4 fails with `invalid_option`. The page background is transparent; do not give `#stage` (or a full-size element) a background colour if the layer should be transparent. Declare `post: { alpha: true }` in the shot when transparency is its normal output. Check transparent shots with `verify --alpha` and `stills --alpha`. Do not judge alpha by playing the WebM in a browser or a generic player; the renderer has already decoded and checked it.
|
|
190
|
+
|
|
191
|
+
## Read results
|
|
192
|
+
|
|
193
|
+
With `--json`, stdout contains one JSON event per line. **The last line is always `type: "result"`**. Use that line to determine success or failure:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{"schema":"multiplane/1","type":"result","command":"render","ts":"2026-10-11T09:30:09.120Z","ok":true,"code":null,"message":null,"artifacts":[{"path":"/abs/out/jobs/20261011-093000-shot-3f2a/shot.mp4","kind":"video","bytes":812345}],"elapsedMs":9120,"jobId":"20261011-093000-shot-3f2a","data":{"output":"/abs/out/jobs/20261011-093000-shot-3f2a/shot.mp4","frames":120,"fps":30,"width":1920,"height":1080}}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- When `ok: false`, use `code` to decide what to do next and `hint` for a suggested fix. Do not parse `message` to make decisions.
|
|
200
|
+
- Interpret exit codes as follows: 0 success; 1 shot or check failure (fix the shot); 2 invalid arguments (fix the command); 3 environment problem (run `doctor`); 4 renderer internal error (retry once, then report if it still fails); 130 cancelled.
|
|
201
|
+
- Before a video is published, the renderer reads the encoded file back and compares frame count, duration, size and frame rate with what it rendered (`result.data.outputCheck`). On a mismatch the command ends with `output_invalid` (exit code 4) and nothing is published; an existing file at the output path is left untouched. Retry once and report it if it repeats. A `warning` with `code: "output_unverified"` means the file was published without this check; inspect it before relying on it.
|
|
202
|
+
- `result.data.resources.peakMemoryBytes` is the sampled peak memory of the whole process tree. If jobs fail with `browser_crashed` or `worker_crashed` and this value is close to the machine's memory, lower `--workers` or `--scale`.
|
|
203
|
+
- If the browser process closes unexpectedly, the renderer relaunches it and renders the affected frame again. You will see a `warning` event with `code: "browser_restarted"`; pixels are unaffected, so no action is needed. If it keeps failing, the command ends with `browser_crashed` (exit code 4): this is not a problem in the shot. Retry once, then try `--raster software`, and report it if it still fails.
|
|
204
|
+
- Read each `lint` issue as a `finding`: `code` is the rule code (corresponding to the constraints above, such as `wall_clock`, `css_animation`, `remote_resource`), `location` provides `file`, `line`, and `column`, `details.snippet` is the source line, and `hint` explains what to use instead. Fix every `severity: "error"`. A `"warning"` (such as `timer_driven` or `font_not_awaited`) does not block rendering but usually indicates a problem.
|
|
205
|
+
- When `verify` fails, `code` is `nondeterministic_output`. Read the preceding `finding` events for the failed check (`reorder` for out-of-order calls / `reload` for page reloads / `workers:<n>` for parallel processes / `range` for consecutive frames), time `t`, changed pixel count, changed region `bbox`, and paths to the reference frame, actual frame, and diff image. Inspect the diff image and `bbox` to locate the element, then use the constraint table above to find the cause. Common patterns:
|
|
206
|
+
- Only `reorder` or `workers` fails: `render(t)` depends on state from the previous call.
|
|
207
|
+
- `reload` fails: random numbers or wall-clock time are used, or assets did not finish loading before `ready`.
|
|
208
|
+
- Only text regions fail: fonts are missing from `fonts`, or remote fonts are used.
|
|
209
|
+
- With `--json`, human-readable messages go to stderr. Do not merge them with stdout when parsing.
|
|
210
|
+
|
|
211
|
+
## Long renders
|
|
212
|
+
|
|
213
|
+
Rendering may take a long time. Each run creates a job directory, `out/jobs/<jobId>/` (`jobId` appears in both the `start` event and the `result`). Read `status.json` for the latest progress snapshot and `events.jsonl` for the full event stream. To stop a job, run `multiplane cancel <jobId>` or create a file named `stop` in its job directory.
|
|
214
|
+
|
|
215
|
+
If the user turned on hosting in the desktop app, the app runs the render and it keeps going after your session ends. Nothing changes in how you call the command; stderr says the job was handed over, and `status.json` may show `queued` while another hosted job runs. When a session resumes and you started a job earlier but never saw its `result`, run `multiplane wait <jobId> --json` to get its events and exit code instead of starting it again. Use `--no-hosted` only when the user asks for a render that should not go through the app.
|
package/src/capture.mjs
CHANGED
|
@@ -119,10 +119,10 @@ async function openShotOnce(shot, scale = 1, options = {}) {
|
|
|
119
119
|
}
|
|
120
120
|
if (alpha && cdp) await cdp.send('Emulation.setDefaultBackgroundColorOverride', { color: { r: 0, g: 0, b: 0, a: 0 } });
|
|
121
121
|
const timings = { renderCallMs: 0, screenshotMs: 0 };
|
|
122
|
-
let closing, sampleContextUsed = false;
|
|
123
|
-
const draw = async (t, { motionBlur = 1 } = {}) => {
|
|
122
|
+
let closing, sampleContextUsed = false, motionProbeUsed = false, dofSamples = 0;
|
|
123
|
+
const draw = async (t, { motionBlur = 1, motionBlurBase = motionBlur, motionProbeTime, dofMotionSamples } = {}) => {
|
|
124
124
|
const renderStart = performance.now();
|
|
125
|
-
const changed = await page.evaluate(({ t, motionBlur, alpha }) => {
|
|
125
|
+
const changed = await page.evaluate(({ t, motionBlur, motionBlurBase, motionProbeTime, dofMotionSamples, alpha }) => {
|
|
126
126
|
globalThis.__multiplaneStatic?.suspend();
|
|
127
127
|
if (alpha) document.documentElement.style.setProperty('--multiplane-page-background', 'transparent');
|
|
128
128
|
const stage = document.querySelector('#stage');
|
|
@@ -135,17 +135,23 @@ async function openShotOnce(shot, scale = 1, options = {}) {
|
|
|
135
135
|
globalThis.__multiplaneStatic?.resume();
|
|
136
136
|
const previous = SHOT.sampleContext;
|
|
137
137
|
let contextUsed = false;
|
|
138
|
-
|
|
138
|
+
// TODO(design): Document motionBlurBase, stable across nested refinements.
|
|
139
|
+
SHOT.sampleContext = { time: t, motionBlurBase, get motionBlur() { contextUsed = true; return motionBlur; } };
|
|
139
140
|
let failure;
|
|
141
|
+
// Private renderer/layer handshake; never part of the shot contract.
|
|
142
|
+
const probe = globalThis.__multiplaneMotionProbe = { time: motionProbeTime, used: false, motionSamples: dofMotionSamples, samples: 0 };
|
|
140
143
|
try { SHOT.render(t); } catch (error) { failure = String(error?.message ?? error); } finally {
|
|
144
|
+
delete globalThis.__multiplaneMotionProbe;
|
|
141
145
|
if (previous === undefined) delete SHOT.sampleContext;
|
|
142
146
|
else SHOT.sampleContext = previous;
|
|
143
147
|
}
|
|
144
148
|
stage.getBoundingClientRect();
|
|
145
149
|
const changed = globalThis.__multiplaneStatic?.read() ?? true;
|
|
146
|
-
return { failure, contextUsed, changed, reason: globalThis.__multiplaneStatic?.reason };
|
|
147
|
-
}, { t, motionBlur, alpha }).then(({ failure, changed, contextUsed, reason }) => {
|
|
150
|
+
return { failure, contextUsed, probeUsed: probe.used, dofSamples: probe.samples, changed, reason: globalThis.__multiplaneStatic?.reason };
|
|
151
|
+
}, { t, motionBlur, motionBlurBase, motionProbeTime, dofMotionSamples, alpha }).then(({ failure, changed, contextUsed, probeUsed, dofSamples: count, reason }) => {
|
|
148
152
|
sampleContextUsed = contextUsed;
|
|
153
|
+
motionProbeUsed = probeUsed;
|
|
154
|
+
dofSamples = count;
|
|
149
155
|
const index = observationReasons.indexOf(reason);
|
|
150
156
|
if (index >= 0) Atomics.add(reuseState(options), index + 4, 1);
|
|
151
157
|
if (failure !== undefined) throw new MultiplaneError('render_threw', undefined, { details: { t, reason: failure } });
|
|
@@ -171,6 +177,8 @@ async function openShotOnce(shot, scale = 1, options = {}) {
|
|
|
171
177
|
return {
|
|
172
178
|
page, browser, server, ...metadata, alpha, beginFrame, timings,
|
|
173
179
|
get sampleContextUsed() { return sampleContextUsed; },
|
|
180
|
+
get motionProbeUsed() { return motionProbeUsed; },
|
|
181
|
+
get dofSamples() { return dofSamples; },
|
|
174
182
|
...(reuseEnabled(options) ? { observe: draw } : {}),
|
|
175
183
|
isCrash: error => browserClosed(error, browser, page, crashed),
|
|
176
184
|
...(metadata.hasBlurAt ? { blurAt: async t => {
|
|
@@ -212,6 +220,8 @@ export async function openShot(shot, scale = 1, options = {}) {
|
|
|
212
220
|
get page() { return current.page; },
|
|
213
221
|
get browser() { return current.browser; },
|
|
214
222
|
get sampleContextUsed() { return current.sampleContextUsed; },
|
|
223
|
+
get motionProbeUsed() { return current.motionProbeUsed; },
|
|
224
|
+
get dofSamples() { return current.dofSamples; },
|
|
215
225
|
...(current.observe ? { observe: (...args) => current.observe(...args) } : {}),
|
|
216
226
|
beginFrame: (...args) => current.beginFrame(...args),
|
|
217
227
|
frame: (...args) => current.frame(...args),
|
package/src/events.mjs
CHANGED
|
@@ -221,14 +221,17 @@ export async function runCommand(command, shot, options, action, job = true) {
|
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
223
|
const normalized = { ...Object.fromEntries(Object.keys(explicit).filter(key => key !== '_hostedCLI').map(key => [key, options[key]])), draft: true };
|
|
224
|
-
Object.assign(events.options, await applyDraft(command, shot, normalized, options, {
|
|
224
|
+
Object.assign(events.options, await applyDraft(command, shot, normalized, options, {
|
|
225
225
|
check: events.check,
|
|
226
226
|
onBrowser: browser => {
|
|
227
227
|
const unsubscribe = events.onCancel(() => browser.close());
|
|
228
228
|
browser.once('disconnected', unsubscribe);
|
|
229
229
|
events.check();
|
|
230
230
|
},
|
|
231
|
-
}));
|
|
231
|
+
}));
|
|
232
|
+
// Draft merges explicit options again; retain the absolute output path
|
|
233
|
+
// required by atomic directory publication and artifact relocation.
|
|
234
|
+
events.options.out = path.resolve(events.options.out);
|
|
232
235
|
}
|
|
233
236
|
events.start();
|
|
234
237
|
for (const warning of user.warnings) events.warning(warning);
|