greenlight-motion 0.3.4 → 0.4.0

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/skill/SKILL.md CHANGED
@@ -18,7 +18,7 @@ You never write UI or animation code. If something is not in the format, it is n
18
18
  ## Setup (once per machine)
19
19
 
20
20
  ```
21
- npx greenlight-motion@0.3.4 doctor
21
+ npx greenlight-motion@0.4.0 doctor
22
22
  ```
23
23
 
24
24
  It checks Node 18+, Chrome or Edge, WebGPU and H.264 encoding, and prints a fix for anything missing. Do not install
@@ -39,7 +39,7 @@ animation presets (the same converter the GreenLight Motion connector uses in Cl
39
39
  keyframes rarely look as good. The format, the design rules and the catalogues: `skill/references/scene-script.md`.
40
40
 
41
41
  ```
42
- npx greenlight-motion@0.3.4 script <folder> --from scenes.json # or write <folder>/script.json, then: script <folder>
42
+ npx greenlight-motion@0.4.0 script <folder> --from scenes.json # or write <folder>/script.json, then: script <folder>
43
43
  ```
44
44
 
45
45
  Then `check` and `stills` (below), fix the SCRIPT and run `script` again — it rewrites `project.json`. A scene's
@@ -85,14 +85,14 @@ What the engine does there, so you don't learn it from a broken frame:
85
85
 
86
86
  ## Workflow
87
87
 
88
- 1. `npx greenlight-motion@0.3.4 init <folder>` — starts from a working example (title, subtitle, accent line, four
88
+ 1. `npx greenlight-motion@0.4.0 init <folder>` — starts from a working example (title, subtitle, accent line, four
89
89
  options). `--from file.json` starts from a project you have (for example one exported from GreenLight Dash).
90
90
  2. Edit `<folder>/project.json` — for changes to an EXISTING project prefer `apply` (below): it checks every edit and
91
91
  applies all or nothing. Put pictures, video, sound and fonts in `<folder>/assets/` and refer to them as
92
92
  `"asset:<file>"` (e.g. `"url": "asset:product.png"`).
93
- 3. `npx greenlight-motion@0.3.4 check <folder>` — fix everything it lists (missing files, template mistakes, blank
93
+ 3. `npx greenlight-motion@0.4.0 check <folder>` — fix everything it lists (missing files, template mistakes, blank
94
94
  frames, page errors), and read its notes: text the engine wraps where you did not break it. `--json` for machine output.
95
- 4. `npx greenlight-motion@0.3.4 stills <folder>` — renders frames and **a labelled contact sheet**. Open and look at
95
+ 4. `npx greenlight-motion@0.4.0 stills <folder>` — renders frames and **a labelled contact sheet**. Open and look at
96
96
  it before you go on: layout, readability, timing. Fix and repeat. `--at 0.5,1.2,3` for exact times.
97
97
  5. Hand it over: tell the user to open `<folder>/index.html` (Chrome or Edge), or `pack` it into one file, or
98
98
  `render` the MP4 yourself (`--fps 60 --motion-blur` for fast motion).
@@ -157,7 +157,7 @@ editor's own normalizer), so write only what you mean.
157
157
  Full references (generated from the editor's own docs; ignore the `/api/` routes they mention):
158
158
  `skill/references/` — video-design, scene-script, video-projects, video-text, video-text-kinetic, video-text-presets, video-shapes, video-keyframes,
159
159
  video-camera, video-3d, video-effects, video-transitions, video-particles, video-paths, video-backgrounds, video-grid.
160
- Machine-readable lists: `npx greenlight-motion@0.3.4 schema effects | transitions | easings | paths | expressions`.
160
+ Machine-readable lists: `npx greenlight-motion@0.4.0 schema effects | transitions | easings | paths | expressions`.
161
161
 
162
162
  ## Template
163
163
 
@@ -204,7 +204,7 @@ an absolute moment; `align` `sync` (default) / `start` / `end`; `offset` seconds
204
204
 
205
205
  The library is 77 hand-picked sounds in categories (whooshes and swooshes, transitions, clicks, pop-ups and
206
206
  notifications, data blips, typing and writing, foley, bubbles, impacts and bass drops, camera, gears, voice clips).
207
- `npx greenlight-motion@0.3.4 schema sounds` lists every one with its `category`, `use` (what it is for), `duration`
207
+ `npx greenlight-motion@0.4.0 schema sounds` lists every one with its `category`, `use` (what it is for), `duration`
208
208
  and `sync`. Pick by `category` and `use`, then by name.
209
209
 
210
210
  - **Where the files come from:** the GreenLight sound CDN. `build` downloads each sound a cue uses into
@@ -220,7 +220,7 @@ The page has **Notes**: the user marks what should change right on the frame (a
220
220
  and writes a note for each mark. Every note carries the moment (seconds and frame), the mark in composition pixels
221
221
  and the layers under it (item ids in project.json), so you change exactly that and nothing else.
222
222
 
223
- - **They review on this computer:** run `npx greenlight-motion@0.3.4 review <folder>` and give them the URL it prints
223
+ - **They review on this computer:** run `npx greenlight-motion@0.4.0 review <folder>` and give them the URL it prints
224
224
  (it keeps running; `--open` opens their browser, only when they ask). Their notes land in `<folder>/review/`: read
225
225
  `review/NOTES.md`, look at the `at-<t>s.png` pictures (the marks are numbered), make the changes (`apply`), `build`
226
226
  again and tell them to reload the page.
@@ -249,11 +249,11 @@ that fails (`plugin`); look at `stills` for the look. A package that carries its
249
249
 
250
250
  The same project opens in GreenLight Dash (the desktop editor this engine comes from) for full editing: the page's
251
251
  edit note offers **Open in GreenLight Dash** (it imports the folder as a new project on the open board; a page with
252
- changes saves a `.glvideo`). `npx greenlight-motion@0.3.4 pack <folder> --glvideo` writes that file yourself: ONE
252
+ changes saves a `.glvideo`). `npx greenlight-motion@0.4.0 pack <folder> --glvideo` writes that file yourself: ONE
253
253
  `.glvideo` (the project + its files) that opens in GreenLight Dash on a double-click, or dropped on its board. Hand it
254
254
  over when the user wants to keep editing in the app. A
255
255
  GreenLight Dash project comes the other way through the app's **Export for agents** — a ready folder like the one
256
- `init` makes, with the options the author ticked. `npx greenlight-motion@0.3.4 init <folder> --from <its project.json>`
256
+ `init` makes, with the options the author ticked. `npx greenlight-motion@0.4.0 init <folder> --from <its project.json>`
257
257
  starts from one too. Never tell the user they must install it: the page plays, changes options and renders on its own.
258
258
 
259
259
  ## Files
@@ -112,7 +112,7 @@ the first version and check every version against it.
112
112
 
113
113
  ## Catalogues
114
114
 
115
- **Text animations** (`textAnimation`; ★ = preferred): ★ `glide-text` Glide Text · `glide-continuous` Glide Continuous · `smart-overshoot` Smart Overshoot · `glide-soft-stop` Glide Soft Stop · ★ `ember-type` Ember Type · `molten-edge` Molten Edge · `focus-type` Focus Type · `tumble-type` Tumble Type · `flap-down` Flap Down · `drop-swell` Drop Swell · `rain-type` Rain Type · `counter-sweep` Counter Sweep · `sunset-shrink` Sunset Shrink · `boomerang` Boomerang · ★ `swing-pop` Swing Pop · ★ `word-swing` Word Swing · ★ `letter-vortex` Letter Vortex · `center-burst` Center Burst · ★ `pendulum` Pendulum · `spin-pop` Spin Pop · `domino` Domino · `flip-pop` Flip Pop · `card-turn` Card Turn · `slam-zoom` Slam Zoom · `orbit` Orbit · ★ `fan-deal` Fan Deal · ★ `whip` Whip · `rubber-letters` Rubber Letters · `pop-letters` Pop Letters · `drop-swing` Drop Swing · `coin-spin` Coin Spin · `converge` Converge · `roll-in` Roll In · `door-swing` Door Swing · `pill-type` Pill Type · `card-type` Card Type · `word-bars` Word Bars · `line-stack` Word Stack · `marker-type` Marker Type · `card-erase` Card Erase · `colour-sweep` Colour Sweep · `glow-pass` Glow Pass · `wave-pass` Wave Pass · `pop-through` Pop Through · `shine` Shine · ★ `marker` Marker · ★ `outline-pass` Outline Pass · ★ `neon-flow` Neon Flow · `rainbow` Rainbow · `gradient-flow` Gradient Flow · `jitter` Jitter · `float` Float · `flicker` Flicker · `breathe` Breathing Wave · `shimmer` Shimmer · `decode` Decode · `binary` Binary Decode · `alphabet-roll` Alphabet Roll · ★ `glitch` Glitch · `classic-fade-up` Fade Up · `classic-fade-down` Fade Down · `classic-rise-mask` Rise (Mask) · `classic-blur-in` Blur In · `classic-slide-in` Slide In · `classic-side-mask` Side Mask · `classic-tilt-drop` Tilt Drop · `classic-flip-up` Flip Up · `classic-typewriter` Typewriter · `classic-wave-rise` Wave Rise · `classic-bounce-letters` Bounce Letters · `classic-materialize` Materialize · `classic-neon-runner` Neon Runner · `classic-ink-highlight` Ink Highlight · `classic-flash-type` Flash Type · `classic-karaoke-sweep` Karaoke Sweep · `classic-chase-loop` Chase Loop · `classic-sung-words` Sung Words · `classic-marker-lines` Marker Words · `classic-pill-grow` Pill Grow · `classic-lower-third` Lower Third · `classic-drop-bar` Drop Bar · `classic-outline-tag` Outline Tag · `classic-prompt-field` Prompt Field · `classic-random-pop` Random Pop · `classic-chaos-letters` Chaos Letters · `classic-random-rise` Random Rise · `classic-ransom-note` Ransom Note · `classic-editorial-typer` Editorial Typer
115
+ **Text animations** (`textAnimation`; ★ = preferred): ★ `glide-text` Glide Text · `glide-continuous` Glide Continuous · `smart-overshoot` Smart Overshoot · `glide-soft-stop` Glide Soft Stop · ★ `ember-type` Ember Type · `molten-edge` Molten Edge · `focus-type` Focus Type · `tumble-type` Tumble Type · `flap-down` Flap Down · `drop-swell` Drop Swell · `rain-type` Rain Type · `counter-sweep` Counter Sweep · `sunset-shrink` Sunset Shrink · `boomerang` Boomerang · ★ `swing-pop` Swing Pop · ★ `word-swing` Word Swing · ★ `letter-vortex` Letter Vortex · `center-burst` Center Burst · ★ `pendulum` Pendulum · `spin-pop` Spin Pop · `domino` Domino · `flip-pop` Flip Pop · `card-turn` Card Turn · `slam-zoom` Slam Zoom · `orbit` Orbit · ★ `fan-deal` Fan Deal · ★ `whip` Whip · `rubber-letters` Rubber Letters · `pop-letters` Pop Letters · `drop-swing` Drop Swing · `coin-spin` Coin Spin · `converge` Converge · `roll-in` Roll In · `door-swing` Door Swing · `title-push-3d` Title Push 3D · `perspective-type` Perspective Type · `rack-focus` Rack Focus · `stand-up` Stand Up · `strata` Strata · `spotlight` Spotlight · `ripple` Ripple · `pill-type` Pill Type · `card-type` Card Type · `word-bars` Word Bars · `line-stack` Word Stack · `marker-type` Marker Type · `card-erase` Card Erase · `colour-sweep` Colour Sweep · `glow-pass` Glow Pass · `wave-pass` Wave Pass · `pop-through` Pop Through · `shine` Shine · ★ `marker` Marker · ★ `outline-pass` Outline Pass · ★ `neon-flow` Neon Flow · `rainbow` Rainbow · `gradient-flow` Gradient Flow · `jitter` Jitter · `float` Float · `flicker` Flicker · `breathe` Breathing Wave · `shimmer` Shimmer · `decode` Decode · `binary` Binary Decode · `alphabet-roll` Alphabet Roll · ★ `glitch` Glitch · `classic-fade-up` Fade Up · `classic-fade-down` Fade Down · `classic-rise-mask` Rise (Mask) · `classic-blur-in` Blur In · `classic-slide-in` Slide In · `classic-side-mask` Side Mask · `classic-tilt-drop` Tilt Drop · `classic-flip-up` Flip Up · `classic-typewriter` Typewriter · `classic-wave-rise` Wave Rise · `classic-bounce-letters` Bounce Letters · `classic-materialize` Materialize · `classic-neon-runner` Neon Runner · `classic-ink-highlight` Ink Highlight · `classic-flash-type` Flash Type · `classic-karaoke-sweep` Karaoke Sweep · `classic-chase-loop` Chase Loop · `classic-sung-words` Sung Words · `classic-marker-lines` Marker Words · `classic-pill-grow` Pill Grow · `classic-lower-third` Lower Third · `classic-drop-bar` Drop Bar · `classic-outline-tag` Outline Tag · `classic-prompt-field` Prompt Field · `classic-random-pop` Random Pop · `classic-chaos-letters` Chaos Letters · `classic-random-rise` Random Rise · `classic-ransom-note` Ransom Note · `classic-editorial-typer` Editorial Typer
116
116
 
117
117
  **Entrances** (`enter`): `slide` Slide · direction · `glide` Glide (in and out) · direction · `glide-stop` Glide Stop (in and out) · direction · `glide-floaty` Glide Floaty (in and out) · direction · `glide-endless` Glide Endless (in and out) · direction · `glide-diagonal` Glide Diagonal (in and out) · direction · `glide-arc` Glide Arc (in and out) · direction · `glide-close-pass` Glide Close Pass (in and out) · direction · `glide-fade-drift` Glide Fade Drift (in and out) · direction · `glide-bank` Glide Bank (in and out) · direction · `glide-swing-3d` Glide Swing 3D (in and out) · direction · `glide-linger` Glide Linger (in and out) · direction · `glide-soft-entry` Glide Soft Entry (in and out) · direction · `glide-blur` Glide Blur (in and out) · direction · `glide-spin-through` Glide Spin Through (in and out) · direction · `glide-depth-pass` Glide Depth Pass (in and out) · direction · `glide-tilt` Glide Tilt (in and out) · direction · `zoom-in` Zoom In · `mockup-close-up` Mockup Close-Up · `mockup-orbit-left` Orbit Left · `deck-drift` Deck Drift · `hero-punch` Hero Punch · `dark-spotlight` Dark Spotlight · `phone-app-swipe` Phone Swipe Reveal · `phone-app-punch` Phone App Punch In · `phone-app-tilt-drift` Phone Tilt Drift · `phone-app-cta-hold` CTA Hold Focus · `button-press` Button Press · `button-tap-flash` Tap Flash · `pop-overshoot` Pop Overshoot · `spring-in` Spring In · `rubber-wobble` Rubber Wobble · `tap-nudge` Tap Nudge · `whip-swing` Whip Swing · `flick-drop` Flick Drop · `jelly-shake` Jelly Shake · `heartbeat` Heartbeat · `impact-punch` Impact Punch · `pendulum-swing` Pendulum Swing · `loose-wiggle` Loose Wiggle · `shiver` Shiver · `float` Float · `flicker-in` Flicker In · `earthquake` Earthquake · `tumble-drop` Tumble Drop · `rubber-band` Rubber Band · `spring-twist` Spring Twist · `slingshot` Slingshot · `pendulum-in` Pendulum In · `smooth-overshoot-rise` Smooth Overshoot Rise · `spin-in` Spin In · `twist-in` Twist In · `roll-in` Roll In · `tilt-settle` Tilt Settle · `wind-up-spin` Wind-Up Spin
118
118
 
@@ -126,7 +126,7 @@ the first version and check every version against it.
126
126
 
127
127
  **Zoom feels** (`camera.feel`): `studio` Studio · `calm` Calm · `precise` Precise · `lively` Lively · `follow` Follow. Full camera layers (paths, Smart Zoom on a recording's clicks, the drawn cursor): references/video-camera.md.
128
128
 
129
- **Overlay effects** (`effects`): `glow`, ★ `bloom`, `auraBlur`, `gradientBeam`, ★ `gradientSweep`, `gradientGlow`, `fractalNoise`, ★ `rgbSplit`, `glitch`, `scanlines`, `flashBlink`, `vhs`, `fisheye`, `opticsCompensation`, `vignette`, ★ `echo`, `timeshift`, `radialBlur`, ★ `radialFastBlur`, `sharpen`, `mirror`, `waveWarp`, `warpStyles`, `shockwave`, `waveLines`, `waveRipple`, `fill`, `shapeRulers`, `copyColor`, `curves`, `turbulentDisplace`, `alphaChoker`, `extract`, ★ `colorama`, `posterize`, ★ `halftone`, ★ `fastCameraWiggle`, ★ `lightRays`, `longShadow`, `outlineBands`, `kaleidoscope`, `crtMatrix`, `pixelate`, `vectorFieldWarp`, `pixelStretch`, `neonGlow`, `edgeDetect`, `doubleExposure`, ★ `filmStock`, `fastBlur`, `gridLines`, `strip`, `ascii`, `objectDither`, `objectIsolines`, `objectWindow`, `objectSlitScan`, `objectCurtain`, `objectGrid`, `objectContour`, `objectNodes`, `objectLinks`, `objectFacets`, `objectMeasure`
129
+ **Overlay effects** (`effects`): `glow`, ★ `bloom`, `auraBlur`, `gradientBeam`, ★ `gradientSweep`, `gradientGlow`, `fractalNoise`, ★ `rgbSplit`, `glitch`, `scanlines`, `flashBlink`, `vhs`, `fisheye`, `opticsCompensation`, `vignette`, ★ `echo`, `timeshift`, `radialBlur`, ★ `radialFastBlur`, `sharpen`, `mirror`, `waveWarp`, `warpStyles`, `shockwave`, `waveLines`, `waveRipple`, `fill`, `shapeRulers`, `copyColor`, `curves`, `turbulentDisplace`, `alphaChoker`, `gooey`, `extract`, ★ `colorama`, `posterize`, ★ `halftone`, ★ `fastCameraWiggle`, ★ `lightRays`, `lightSweep`, `longShadow`, `outlineBands`, `kaleidoscope`, `crtMatrix`, `pixelate`, `vectorFieldWarp`, `pixelStretch`, `neonGlow`, `edgeDetect`, `doubleExposure`, ★ `filmStock`, `fastBlur`, `gridLines`, `strip`, `ascii`, `objectDither`, `objectIsolines`, `objectWindow`, `objectSlitScan`, `objectCurtain`, `objectGrid`, `objectContour`, `objectNodes`, `objectLinks`, `objectFacets`, `objectMeasure`
130
130
 
131
131
  **Layer effects** (`effects`): `mockupFrame`, ★ `runningGlow`, `depthMap`, `depth3d`, `depthScan`, `fresnelRim`, ★ `motionBlur`, `posterizeTime`, ★ `motionTrail`, `autoFill`, ★ `repetile`, `regionDistort`, `bend`, `cornerPin`, `meshWarp`, `bezierWarp`, `rig`, ★ `droste`, `pryzm`, ★ `glass`, `chromaKey`, `layerMask`
132
132
 
@@ -10,32 +10,34 @@
10
10
 
11
11
  ## Contents
12
12
 
13
- - [3D model layers (`mediaType: "model3d"`) — a live GLB/GLTF/OBJ rendered per frame](video-3d.md#3d-model-layers-mediatype-model3d--a-live-glbgltfobj-rendered-per-frame)
14
- - [Scene Rig — one set for every 3D layer](video-3d.md#scene-rig--one-set-for-every-3d-layer)
15
- - [Flat layers in the 3D scene — `item.shadowPlate`](video-3d.md#flat-layers-in-the-3d-scene--itemshadowplate)
13
+ - [3D model layers (`mediaType: "model3d"`) — a live GLB/GLTF/OBJ rendered per frame](#3d-model-layers-mediatype-model3d--a-live-glbgltfobj-rendered-per-frame)
14
+ - [Scene Rig — one set for every 3D layer](#scene-rig--one-set-for-every-3d-layer)
15
+ - [Turning flat layers in 3D — pivot and stacking](#turning-flat-layers-in-3d--pivot-and-stacking)
16
+ - [Flat layers in the 3D scene — `item.shadowPlate`](#flat-layers-in-the-3d-scene--itemshadowplate)
16
17
 
17
18
  ### 3D model layers (`mediaType: "model3d"`) — a live GLB/GLTF/OBJ rendered per frame
18
19
 
19
- Unlike a device mockup (pre-baked to a video file), a `model3d` layer is a **real 3D scene rendered live every frame** by the Player (three.js, its own depth-buffered pass), then composited like any other layer. The model can be one of the bundled device presets or a custom uploaded asset, and it participates in the project camera, keyframes, layer parenting and effects like any layer.
20
+ Unlike a device mockup (pre-baked to a video file), a `model3d` layer is a **real 3D scene rendered live every frame** by the Player (three.js, its own depth-buffered pass), then composited like any other layer. The model can be one of the bundled device presets, a primitive stand-in or a custom uploaded asset. It takes the project camera, keyframes and layer parenting like any layer. Effects work on it only in part: see [Real 3D on 3D model layers](video-effects.md#real-3d-on-3d-model-layers); layer effects marked `disabledWhen3d` in capabilities (Motion Blur, Rig (Puppet), the warps, Layer Mask…) do not run on it. It takes no transitions ([video-transitions.md](video-transitions.md)).
20
21
 
21
- **⚠ The clip `url` must NEVER be empty — an empty url silently deletes the layer.** `sanitizeClip` (VideoEditor.jsx) treats a `model3d` clip like a video clip: no `url` → the clip is dropped on load/render, and every timeline item referencing it is dropped with it. Nothing errors — the project saves fine and a headless render completes "successfully" with the model simply missing from every frame. This happens even when the item carries a complete, valid `model3dConfig`. The two valid url forms:
22
+ **Never leave the clip `url` empty: the layer disappears without a warning.** A `model3d` clip is loaded like a video clip, so a clip with no `url` is dropped on load and render, and every timeline item that uses it goes with it — even one carrying a complete, valid `model3dConfig`. The project still saves and a headless render still "succeeds", with the model missing from every frame. The valid url forms:
22
23
 
23
24
  - **Preset device:** `"url": "model-preset:<presetId>"` — a stable sentinel, NOT a fetchable path. (The bundled .glb's real URL is a content-hashed build artifact that changes every release; the sentinel survives app updates.)
24
25
  - **Primitive shape:** `"url": "model-primitive:<kind>"` — a stand-in built in code, no asset file. Pair it with `"source": "primitive"` and `"primitive": { "kind", "color", "text" }` in `model3dConfig` (see below).
25
26
  - **Custom model:** a real uploaded asset URL ending in `.glb`, `.gltf` or `.obj` (upload via the media API, then use the returned `/media/...` url).
27
+ - **Gaussian splat:** a `.ply`, `.splat` or `.spz` url. Only a 3D Scene draws splats (world3d.md (GreenLight Dash only)).
26
28
 
27
29
  **Clip + item pair (preset device):**
28
30
 
29
31
  ```json
30
32
  // clip — url is the preset sentinel, source REMOTE, no proxy/thumbnail
31
33
  { "id": "m1", "source": "REMOTE", "mediaType": "model3d",
32
- "url": "model-preset:iphone_17", "proxyUrl": null, "name": "iPhone 17", "duration": 4 }
34
+ "url": "model-preset:iphone_pro_max", "proxyUrl": null, "name": "iPhone 17 Pro Max", "duration": 4 }
33
35
  // item — model3dConfig holds everything about fit, look and animation
34
36
  { "id": "mi1", "clipId": "m1", "trimStart": 0, "trimEnd": 4,
35
37
  "model3dConfig": {
36
- "source": "preset", "preset": "iphone_17",
38
+ "source": "preset", "preset": "iphone_pro_max",
37
39
  "fit": "height", "fitValue": 60, "anchor": "center", "placeOnRigFloor": true,
38
- "baseRotation": { "x": 0, "y": 0, "z": 0 },
40
+ "baseRotation": { "x": 0, "y": 180, "z": 0 },
39
41
  "env": { "hdr": "potsdamer_platz_1k", "intensity": 1, "exposure": 1, "rotation": 0 },
40
42
  "lights": { "key": 1, "fill": 0.45, "rim": 0.35 },
41
43
  "animation": { "clip": null, "speed": 1, "loop": "loop", "offset": 0 },
@@ -65,14 +67,13 @@ For a custom model use `"source": "custom", "preset": null` and point the clip `
65
67
  | `primitive.color` | any CSS colour, ≤ 64 chars | `#d9d9d9` | Tints the whole shape (the car's glass and tyres keep their own colour). |
66
68
  | `primitive.text` | string, ≤ 40 chars | `TEXT` | Only read by `kind: "text"` (extruded letters). |
67
69
 
68
- On a floor-bearing rig, `placeOnRigFloor: true` stands the shape on the cyclorama; `panX` (% of comp width) and `panZ` (px, + toward the camera) place it on the floor. Fit values the editor uses for a set at comp scale: cube/sphere 24, card 26, torus 22, cone/cylinder 28, human 58, car 20, text 14. Keep `source`/`preset` consistent with the clip url: the renderer resolves the asset from `model3dConfig` first (`source: "preset"` + a valid `preset` id wins), then falls back to the clip's sentinel/url — but the clip url is what keeps the clip **alive** through sanitization, so always set both sides to the same device.
70
+ On a floor-bearing rig, `placeOnRigFloor: true` stands the shape on the cyclorama; `panX` (% of comp width) and `panZ` (px, + toward the camera) place it on the floor. Fit values the editor uses for a set at comp scale: cube/sphere 24, card 26, torus 22, cone/cylinder 28, human 58, car 20, text 14. Keep `source`/`preset` consistent with the clip url: the renderer resolves the asset from `model3dConfig` first (`source: "preset"` + a valid `preset` id wins), then falls back to the clip's sentinel/url — but the clip url is what keeps the clip **alive** on load, so always set both sides to the same device.
69
71
 
70
- **Bundled device presets** (`MODEL_PRESETS` in `frontend/src/components/videoEditor/model3dPresets.js`):
72
+ **Bundled device presets:**
71
73
 
72
74
  | `preset` id | Device | Screen artwork | `baseRotation.y` to face the camera |
73
75
  | --- | --- | --- | --- |
74
76
  | `iphone_pro_max` | iPhone 17 Pro Max | yes | `180` |
75
- | `iphone_17` | iPhone 17 | yes | `0` |
76
77
  | `iphone_air` | iPhone Air | yes | `0` |
77
78
  | `double_iphone` | Two iPhones | yes | `180` |
78
79
  | `ipad` | iPad | yes | `0` |
@@ -81,18 +82,21 @@ On a floor-bearing rig, `placeOnRigFloor: true` stands the shape on the cycloram
81
82
  | `macbook_air` | MacBook Air | yes | `0` |
82
83
  | `imac` | iMac | yes | `180` |
83
84
 
84
- Some GLBs are authored facing away from the viewer — for those, set `baseRotation.y: 180` (the last column; the editor's Add-Layer flow applies this automatically via `presetBaseRotation`). A wrong value shows the device's back.
85
+ `iphone_17` is an alias of `iphone_pro_max`: it loads the same model, so it also needs `baseRotation.y: 180`. Write `iphone_pro_max`.
85
86
 
86
- **`model3dConfig` fields** (sanitized by `sanitizeModel3dConfig` in `editorState.js`; defaults in `MODEL_CONFIG_DEFAULTS` in `types.js` — out-of-range values are clamped, bad enums fall back):
87
+ Some GLBs are authored facing away from the viewer — for those, set `baseRotation.y: 180` (the last column; the editor sets it when it adds the layer). A wrong value shows the device's back.
88
+
89
+ **`model3dConfig` fields** (out-of-range values are clamped, unknown enum values fall back to the default):
87
90
 
88
91
  | Field | Values / range | Default | Meaning |
89
92
  | --- | --- | --- | --- |
90
- | `source` | `"preset"` \| `"custom"` | `custom` | Where the geometry comes from. Anything else coerces to `custom`. |
93
+ | `source` | `"preset"` \| `"primitive"` \| `"custom"` | `custom` | Where the geometry comes from. Anything else coerces to `custom`. |
91
94
  | `preset` | preset id string | `null` | Required when `source: "preset"`. An unknown id falls through to the clip url. |
92
95
  | `fit` | `"height"` \| `"width"` \| `"unit"` | `height` | How `fitValue` sizes the model: % of frame height, % of frame width, or raw pixels-per-model-unit. |
93
96
  | `fitValue` | `0.01–10000` | `60` | With `fit: "height"`, `60` = the model's bounding box spans 60 % of the frame height. |
94
97
  | `anchor` | `"center"` \| `"base"` \| `"origin"` | `center` | Pivot: bbox centre, bbox floor (for objects standing on a surface), or the asset's own origin — no recentring, so a GLB exported in scene coordinates (Blender → GLEA) lands exactly where its scene put it, whatever its rest pose. |
95
98
  | `placeOnRigFloor` | bool | `true` | With a floor-bearing Scene Rig, place the model's real post-transform lower bound on the cyclorama floor. The ordinary Y transform becomes an offset from that floor. Set `false` for free/flying placement. |
99
+ | `physics` | `"off"` \| `"falls"` \| `"solid"` | `off` | Rigid-body physics, only in a 3D Scene (Rigid bodies (GreenLight Dash only)): `falls` makes the model a rigid body that falls, hits and stacks from where the layer stands; `solid` a fixed one others hit. The classic renderer ignores it. |
96
100
  | `castShadow` | bool | `true` | With a Scene Rig casting real shadows: the model is in the shadow map. `false` = it casts nothing (a huge set piece that should only receive). Blended parts of a model (glTF BLEND — a baked contact-shadow card, a glow) never cast either way. Only stored when `false`. |
97
101
  | `receiveShadow` | bool | `true` | With a Scene Rig: rig shadows darken the model. `false` = they never land on it. Only stored when `false`. |
98
102
  | `baseRotation` | `{x,y,z}` deg, `±360` | `{0,0,0}` | Static orientation applied before keyframed rotation. |
@@ -101,7 +105,7 @@ Some GLBs are authored facing away from the viewer — for those, set `baseRotat
101
105
  | `env.exposure` | `0.05–8` | `1` | Tone-mapping exposure of the model pass. |
102
106
  | `env.rotation` | `±360` deg | `0` | Spins the environment (moves reflections). |
103
107
  | `lights.key/fill/rim` | `0–10` each | `1 / 0.45 / 0.35` | The three directional lights' intensities. **Accepted, normalised on read** — see "Scene Rig" below. Still the right shape to emit when a project has no rig layer; ignored on every model once one exists. |
104
- | `animation.clip` | glTF animation name \| `"*"` \| `null` | `null` | Plays the named animation baked into the asset; `"*"` plays EVERY clip together, each on its own length (Blender's exporter writes one clip per animated object); `null` = static. Names must match the glTF exactly. `loop: "once"` holds the last frame. A machine running or a device coming apart is made as such a GLB: video-mechanisms.md (GreenLight Dash only). |
108
+ | `animation.clip` | glTF animation name \| `"*"` \| `null` | `null` | Plays the named animation baked into the asset; `"*"` plays EVERY clip together, each on its own length (Blender's exporter writes one clip per animated object); `null` = static. Names must match the glTF exactly. `loop: "once"` holds the last frame. A machine running or a device coming apart is made as such a GLB: mechanisms.md (GreenLight Dash only). |
105
109
  | `animation.speed` | `-8–8` | `1` | Playback rate (negative = reversed). |
106
110
  | `animation.loop` | `"once"` \| `"loop"` \| `"pingpong"` | `loop` | Loop mode. |
107
111
  | `animation.offset` | `±600` s | `0` | Start offset into the animation. |
@@ -131,17 +135,19 @@ Some GLBs are authored facing away from the viewer — for those, set `baseRotat
131
135
  }]
132
136
  ```
133
137
 
134
- A `screen`-shaded mesh shows the media in **its own colours**: its pixels skip the 3D tone curve (a depth-tested coverage mask, so anything in front still hides it), so a UI on a slab matches the same file as a 2D layer. Being self-lit it takes no light — and so no shadow: a character walking on a page or a UI looks as if it floats. Set `receiveShadow: true` on such a screen and the Scene Rig's real shadows darken it where they land (by the casting lights' share of the set's light), while every other pixel keeps the media's exact colours. Use it for any screen something stands on or passes over. Videos are driven by the layer clock and are **frame-exact in exports and stills** for MP4/MOV (WebCodecs decoder lanes, settled before each frame is encoded; the device `screen` uses the same path). WebM falls back to a seeked `<video>` (best effort). The inspector's SCREEN section lists the entries and removes them; agents write them.
138
+ A `screen`-shaded mesh shows the media in **its own colours**: its pixels skip the 3D tone curve (a depth-tested coverage mask, so anything in front still hides it), so a UI on a slab matches the same file as a 2D layer. Being self-lit it takes no light — and so no shadow: a character walking on a page or a UI looks as if it floats. Set `receiveShadow: true` on such a screen and the Scene Rig's real shadows darken it where they land (by the casting lights' share of the set's light), while every other pixel keeps the media's exact colours. Use it for any screen something stands on or passes over. Videos are driven by the layer clock and are **frame-exact in exports and stills** for MP4/MOV (the device `screen` uses the same path). WebM falls back to a seeked `<video>` (best effort).
135
139
 
136
- **Transforms & keyframes.** The item's ordinary transform fields and `propertyKeyframes` (`scale`, `xPercent`/`yPercent`, `rotateX`/`rotateY`/`rotateZ`, `panZ`, opacity, …) are applied **inside the 3D pass** — a keyframed `rotateY` really orbits the object in 3D rather than spinning a flat card. The project camera films model layers like everything else (`cameraLocked` on the channel opts out).
140
+ **Transforms & keyframes.** The item's ordinary transform fields and `propertyKeyframes` (`scale`, `xPercent`/`yPercent`, `translateZ`, `rotateX`/`rotateY`/`rotate`, `opacity`, …) are applied **inside the 3D pass** — a keyframed `rotateY` really orbits the object in 3D rather than spinning a flat card. The project camera films model layers like everything else (`cameraLocked` on the channel opts out).
137
141
 
138
- **Look.** The 3D pass is tone-mapped with **AgX** (the curve Blender renders with; three.js r183's AgXToneMapping) plus a GLEA look — ×1.35 gain so mid-grey sits where the earlier ACES default had it, and 1.2 saturation — in its own pass, so models, the Scene Rig, World View and real-3D effects share one filmic curve while 2D layers are untouched. It replaced the earlier ACES fit as the default.
142
+ **Look.** The 3D pass is tone-mapped with **AgX** (the curve Blender renders with) plus a GLEA look (×1.35 gain and 1.2 saturation) in its own pass, so models, the Scene Rig and real-3D effects share one filmic curve while 2D layers are untouched. A 3D Scene takes its film look from `rig.look` and `rig.exposure` (world3d.md (GreenLight Dash only)).
139
143
 
140
144
  **Rendering.** Model layers render in the editor preview, the desktop Download, **`/render-onserver`** / **`/render-onserver-job`**, and **`/render-still`**. The fast `/render` does **not** support them (video/image only). Very heavy assets (≳900 k triangles) render slowly enough that an export can look hung — prefer decimated models.
141
145
 
142
146
  ### Scene Rig — one set for every 3D layer
143
147
 
144
- A **rig** clip carries the project's lights, backdrop and environment. It is a singleton, like the camera: `mediaType: "rig"` on a channel with `kind: "rig"`, and the item carries `item.rig`. **When a rig exists it lights every model layer** and `model3dConfig.lights` is ignored; with no rig, each model keeps its own three lights exactly as before. Emitting a rig is therefore additive — it never changes an existing project until you add one.
148
+ > **Every Rig is a 3D Scene**, drawn by the world renderer unless `rig.renderer` is `"classic"` (or the machine has no WebGPU, below): the backdrop, the Rig's lights and environment and the model layers become one lit, shadowed world, path traced in Final 3D, and you can build in it yourself, with no Blender, by writing `rig.scene` (world3d.md (GreenLight Dash only)). The fields below hold in both; where the world reads them differently, the section says so.
149
+
150
+ A **rig** clip carries the project's lights, backdrop and environment. It is a singleton, like the camera: `mediaType: "rig"` on a channel with `kind: "rig"`, and the item carries `item.rig`. **When a rig exists it lights every model layer** and `model3dConfig.lights` is ignored; with no rig, each model keeps its own three lights. Adding a rig is therefore additive — it never changes an existing project until you add one.
145
151
 
146
152
  ```jsonc
147
153
  "rig": {
@@ -150,7 +156,7 @@ A **rig** clip carries the project's lights, backdrop and environment. It is a s
150
156
  "width": 4, "height": 2.25, "depth": 3, // MULTIPLES OF COMP HEIGHT, not px
151
157
  "curve": 0.35, // floor→wall sweep, as a fraction of height
152
158
  "x": 0, "y": 0, "z": 0, "rotX": 0, "rotY": 0, "rotZ": 0, "scale": 1,
153
- "material": { "mode": "solid", // solid | shadowCatcher
159
+ "material": { "mode": "solid", // solid | shadowCatcher | image (a picture, `imageUrl`)
154
160
  "color": "#d8d8d8", "roughness": 0.8,
155
161
  "metalness": 0, "opacity": 1,
156
162
  "receiveShadow": true, "castShadow": false }
@@ -168,17 +174,22 @@ A **rig** clip carries the project's lights, backdrop and environment. It is a s
168
174
  }
169
175
  ```
170
176
 
171
- **Shadows.** `mode: "map"` gives real shadow maps: a model casts onto the cyclorama and
177
+ **Which renderer draws it.** On a machine with WebGPU, every Rig is drawn by the **world renderer** (world3d.md (GreenLight Dash only)) unless `rig.renderer` is `"classic"`; without WebGPU it falls back to the classic Rig passes. Both read the same `rig`, with these differences:
178
+
179
+ - The world renderer ignores the `shadows` block (`mode`, `maxCasters`) and a light's `shadowBias`, `shadowNormalBias` and `shadowMapSize`: a light casts when its own `castShadow` is true. The shadow-map paragraphs below (**Shadows**, **The shadow map follows the camera**, the area light's companion caster) describe the classic renderer.
180
+ - `shadowSoftness` is the light's size in the world renderer (see the light fields below); in the classic renderer it widens the shadow-map penumbra.
181
+
182
+ **Shadows (classic renderer).** `mode: "map"` gives real shadow maps: a model casts onto the cyclorama and
172
183
  onto other models. A light only casts when it ALSO has `castShadow: true` — the mode
173
184
  alone does nothing. `maxCasters` (0-4, default 2) caps how many casting lights are
174
185
  honoured, because each one is a full extra render of the set every frame; lights past
175
186
  the cap are silently not casting, in array order. The cyclorama receives by default
176
187
  (`cyclorama.material.receiveShadow`) and does not cast. `mode: "contact"` keeps only
177
188
  the cheap soft blob under each model standing on the rig floor (`placeOnRigFloor`).
178
- A Scene Rig added in the editor starts with `mode: "map"` and only the Key light
179
- (`k1`) casting; a rig you write yourself gets exactly what you write.
189
+ A rig you write gets exactly what you write. In a 3D Scene none of this applies: every light with
190
+ `castShadow: true` casts (see **Which renderer draws it** above).
180
191
 
181
- **The shadow map follows the camera.** Each frame a casting light's map is fitted to
192
+ **The shadow map follows the camera (classic renderer).** Each frame a casting light's map is fitted to
182
193
  what the camera actually sees of the set (the frustum clipped to the receivers, in the
183
194
  casters' reach), in 1/8-octave size steps with a texel-snapped centre so a moving
184
195
  camera does not make shadow edges crawl. A small character on a long set (a ten-metre
@@ -189,38 +200,76 @@ override it with `shadowBias` / `shadowNormalBias` (0–20, in texels of its map
189
200
  them out for automatic — raise them if a surface still shows stripes, lower them if a
190
201
  shadow looks detached).
191
202
 
192
- **One shared world, but only when a rig exists.** With a rig, every 3D layer is placed
193
- in one scene so the shadow map can contain every caster; each layer still renders in
194
- its own pass, so opacity, blend modes, masks, effects and DOF are unchanged. With NO
195
- rig, each model keeps its own scene and its own `model3dConfig.env` — which is why
196
- adding a rig to an existing project is additive and never re-lights it by surprise.
197
- With a rig, the rig's `env` (and exposure) is the one in effect for every model;
198
- key `sc:env:intensity` to change reflections over time (e.g. only once a metal
199
- object takes the stage).
200
-
201
- **Camera depth of field is per pixel on 3D passes.** A model, a cyclorama or an
202
- occluding shadow plate hands the compositor its depth buffer, and every pixel blurs by
203
- the CoC of its OWN distance (`camAperture · |camFocusDistance − d| / d · camBlurLevel/100`,
204
- capped at 64 px, still held off inside the focus area) — a character in focus stays
205
- sharp in the same model as a wall metres behind it. The gather is depth-aware: a
206
- blurred background never smears over a sharp foreground edge, a sharp foreground
207
- leaves no halo on the blurred background, and a foreground in front of the focus plane
208
- blurs out over what is behind it and past its own silhouette. Taps read the picture
209
- prefiltered (mip level of the tap spacing), so thin bright detail under a wide blur —
210
- lettering far behind the focus — averages instead of sparkling; 2D layers get the same.
211
- A whole set (backdrop included) can therefore live in one model layer. Flat 2D layers
212
- keep one CoC per layer — the depth of their plane.
213
-
214
- **Light types and their fields.** `directional` / `point` / `spot` / `area` take `az` (±360°), `elev` (±90°), `dist` (world px); `ambient` and `hemisphere` take neither. `spot` adds `angle` (1–89°) and `penumbra` (0–1); `spot` and `area` aim at `targetX` / `targetY` / `targetZ` (world px, default the rig origin); `hemisphere` adds `groundColor`. Fields a type does not have are dropped by the sanitizer.
215
-
216
- **`area` — a softbox.** A lit rectangle of `width` × `height` world px (default 800 × 800) at its spherical position, facing its target: broad, soft highlights and soft wrap-around light, the way a large source lights in Blender or a studio — use it for keys and fills on products, glossy and metal surfaces. Bigger at the same `intensity` = the same light on the subject, only softer. It has no shadow map of its own: with `castShadow: true` a companion light from the same place carries half its light and casts the shadow (a lighter, softer shadow, as from a large source), and counts against `shadows.maxCasters`. Set the softness to match the box: `shadowSoftness` ≈ (box size / distance to the target) ÷ 0.22.
217
-
218
- **`intensity` means the same on every light: what the set receives.** A `point` or `spot` of `2` at `dist: 2400` (or an `area` of `2` at any size, at its target) lights the subject at the centre of the set like a `directional` of `2`, then falls off physically around it (closer is brighter, farther darker). A casting light's `shadowSoftness` (0–10, default 1) widens its penumbra, scaled by the light's distance — contact points stay sharp and the shadow softens away from them.
203
+ **One shared world, but only when a rig exists.** With `renderer: "classic"`, every 3D
204
+ layer is placed in one scene so the shadow map can contain every caster, and each layer
205
+ still renders in its own pass, so opacity, blend modes, masks, effects and DOF are
206
+ unchanged. In a 3D Scene the world draws the models, and a model it draws takes no
207
+ `opacity` or `blendMode`. With NO rig, each model keeps its own scene and its own
208
+ `model3dConfig.env`, which is why adding a rig to an existing project is additive and
209
+ never re-lights it by surprise. With a rig, the rig's `env` (and exposure) is the one in
210
+ effect for every model; key `sc:env:intensity` to change reflections over time (e.g.
211
+ only once a metal object takes the stage). A 3D Scene turns its environment by `rotY`
212
+ alone.
219
213
 
220
- ### Flat layers in the 3D scene — `item.shadowPlate`
214
+ **Camera depth of field is per pixel on 3D passes** (a model, the cyclorama, an occluding shadow plate), so a whole set, backdrop included, can live in one model layer; each pixel blurs by a radius in px: [video-camera.md](video-camera.md#depth-of-field).
215
+
216
+ **Light types and their fields.** Every light has `lid`, `type`, `name`, `on`, `color` and `intensity` (0–100, default 1). `directional` / `point` / `spot` / `area` also take `az` (±360°), `elev` (±90°), `dist` (0–100000 world px, default 3000), `castShadow` (default false; obeyed in Draft and Final on every kind of light: leave it off on a fill, or it throws a second shadow) and `shadowSoftness` (0–10, default 1); `ambient` and `hemisphere` take neither. `spot` adds `angle` (1–89°, default 32) and `penumbra` (0–1, default 0.5); `point` adds `range` (world px, default 0) and `decay` (0–4, default 2, the falloff exponent); `area` adds `width` × `height` (world px, default 800 × 800) and `spread` (below); `spot` and `area` aim at `targetX` / `targetY` / `targetZ` (world px, default the rig origin); `hemisphere` adds `groundColor`. Fields a type does not have are dropped on load.
217
+
218
+ - **`castShadow`** — a fill leaves it off, or it throws a second shadow.
219
+ - **`shadowSoftness`** — in the world renderer it is the light's size, and the subject stays as bright: a directional light becomes a sun `1 + 2 × shadowSoftness` degrees wide, an area light's panel is scaled by it, and a spot or point lamp grows with it and with its distance. In the classic renderer a casting light's `shadowSoftness` widens its penumbra, scaled by the light's distance — contact points stay sharp and the shadow softens away from them.
220
+ - **`spread`** (area only, 1–180, default 180) narrows a panel to a pool of light, like a grid on a soft box. The subject stays as bright. Stored only below 180.
221
+ - **`texture`** (on `directional`, `spot` and `area`) is what the light shines through: `{ "pattern": "window" }` (or `blinds`, `leaves`), or an image of the user's: `{ "url": "/media/…/gobo.png", "name": "gobo.png" }`. A spot projects it in colour, in Draft and Final. A directional and an area light cast the pattern from something out of frame. An image on an area light sits on the panel itself, so it shows in reflections in Final and tints the set in Draft.
222
+
223
+ **`area` — a softbox.** A lit rectangle of `width` × `height` world px at its spherical position, facing its target: broad, soft highlights and soft wrap-around light, the way a large source lights in Blender or a studio — use it for keys and fills on products, glossy and metal surfaces. Bigger at the same `intensity` = the same light on the subject, only softer. In the classic renderer it has no shadow map of its own: with `castShadow: true` a companion light from the same place carries half its light and casts the shadow (a lighter, softer shadow, as from a large source), and counts against `shadows.maxCasters`; match the softness to the box there with `shadowSoftness` ≈ (box size / distance to the target) ÷ 0.22.
224
+
225
+ **`intensity` means the same on every light: what the set receives.** A `point` or `spot` of `2` at `dist: 2400` (or an `area` of `2` at any size, at its target) lights the subject at the centre of the set like a `directional` of `2`, then falls off physically around it (closer is brighter, farther darker).
226
+
227
+ **`lid`** matches `/^[A-Za-z0-9_-]{1,12}$/` and must be unique within the rig — it is the key keyframe tracks address.
228
+
229
+ **Sizes are multiples of composition height**, so a rig authored at 1080p is correct at 4K with no migration. `width: 4` on a 1080-tall comp is 4320 world px.
230
+
231
+ **Switch a light off with `intensity: 0` or `on: false`, never by removing it.** The light count is a compiled shader constant, so changing it recompiles every material in the scene. `on: false` turns the light off the same way and keeps its `intensity` for later.
232
+
233
+ **Cyclorama shape `"none"`** draws nothing: use it for a rig that only carries lights. Any other shape with `material.mode: "solid"` writes depth, so layers behind it are cut by its surface.
234
+
235
+ **`material.mode: "image"`** with `material.imageUrl` (a media url) puts a picture on the backdrop in the world renderer; the picture is multiplied by `color`, so leave `color` white to show its own colours. The classic renderer does not read `imageUrl`.
236
+
237
+ **`material.mode: "shadowCatcher"`** keeps only the shadows the set receives and is transparent everywhere else, for models standing on the composition `background`. It writes no depth, so it never cuts a layer. Its shadows use `material.shadowColor` (default `#000000`), not `color` — `color` stays the Solid surface's colour, so a paper set switched to shadows does not paint them in the paper's own colour. `opacity` is the shadow strength (0-1), and the backdrop colour track `scc:cyc:col` drives `shadowColor` in this mode.
238
+
239
+ **Rig keyframes** live on the rig item's `propertyKeyframes` under two prefixes (`sc:` numeric, `scc:` colour — the prefix alone decides numeric vs RGBA interpolation):
240
+
241
+ | Key | Range |
242
+ | --- | --- |
243
+ | `sc:<lid>:intensity` | 0–100 |
244
+ | `sc:<lid>:az` \| `elev` \| `dist` \| `shadowSoftness` | per the fields above; not on `ambient` / `hemisphere` |
245
+ | `sc:<lid>:angle` \| `penumbra` | spot only |
246
+ | `sc:<lid>:width` \| `height` \| `spread` | area only (world px; spread in degrees) |
247
+ | `scc:<lid>:col` | colour |
248
+ | `sc:cyc:x` \| `y` \| `z` \| `rotX` \| `rotY` \| `rotZ` \| `scale` \| `opacity` | — |
249
+ | `scc:cyc:col` | colour |
250
+ | `sc:env:intensity` \| `exposure` \| `rotX` \| `rotY` \| `rotZ` | — |
251
+
252
+ **Refused, and dropped on load:** light `type`, adding/removing a light, `castShadow`, `shadowMapSize`, cyclorama `shape` and dimensions, `env.source`/`preset`/`url`. Each is a shader recompile or an environment re-bake, not a value — animating one would rebuild every frame. A track naming a `lid` that no longer exists is dropped on the next save.
253
+
254
+ **The playhead outside every rig clip does NOT mean darkness.** The nearest rig still lights the scene, and a project with no rig falls back to the default three directional lights.
255
+
256
+ **24 built-in presets**, each a **complete** rig — applying one replaces `item.rig` wholesale:
257
+
258
+ | Category | Preset ids |
259
+ | --- | --- |
260
+ | Studio | `threePoint` (the same lights as a project with no rig — use it to match one), `studioRoom`, `clamshellBeauty`, `highKeyWhite` |
261
+ | Product | `productSoftbox`, `topDownProduct`, `turntable`, `pastelSweep`, `pianoBlack`, `cosmeticGlow`, `windowFlatlay` |
262
+ | Dramatic | `rimLitDark`, `filmNoir`, `stageSpotlight`, `campfireGlow`, `rimHero` |
263
+ | Daylight | `sunsetWindow`, `overcastExterior`, `desertNoon`, `blueHour` |
264
+ | Location | `warehouse`, `sciFiRoom` |
265
+ | Stylized | `neonTwoTone`, `duotonePop` |
266
+
267
+ **Other rig-item fields the editor writes.** `item.rigLook = { id, name, hash }` records which look was applied (a preset id, or `saved:<id>` for a look the user saved) and a hash of the rig at that moment, so the editor can mark the look as edited once the rig drifts from it. It is informational — omit it when emitting a rig; a hand-made `hash` never matches, so the look would show as edited from the start. `item.rigAi` holds the AI video state — the prerender used as the source, the takes and which one is placed on the timeline. It is written by the editor from real render and generation results; do not write it yourself.
221
268
 
222
- **Turning a flat layer in 3D — the pivot.** Every flat layer (image, video, shape, text, html clip) turns in 3D
223
- with `rotateX` / `rotateY` / `rotateZ`, static fields and `propertyKeyframes` tracks alike: a static angle and the
269
+ ### Turning flat layers in 3D — pivot and stacking
270
+
271
+ **The pivot.** Every flat layer (image, video, shape, text, html clip) turns in 3D
272
+ with the static `rotateX` / `rotateY` / `rotateZ` fields and the `rotateX` / `rotateY` / `rotate` keyframe tracks alike: a static angle and the
224
273
  same angle reached by a key give the same pose, and mixing static and keyed channels never moves the layer. The
225
274
  pivot is `keyframeTransformOrigin` (`"center"` default; `"top"`, `"bottom"`, `"left"`, `"right"`, the four
226
275
  corners such as `"bottom left"`, or CSS-style `"50% 100%"`): rotation and the keyed `scale` / `scaleX` / `scaleY`
@@ -235,6 +284,8 @@ in 3D is ordered against every flat layer it does not cross by which side of tha
235
284
  a card standing on a page or floor layer always draws over it, wherever the camera looks. Two layers that cut
236
285
  through each other still need `shadowPlate.occludes` (below) to intersect per pixel.
237
286
 
287
+ ### Flat layers in the 3D scene — `item.shadowPlate`
288
+
238
289
  `text`, `shape`, `image` and `video` items can join the 3D scene without becoming
239
290
  solids. The layer stays flat and keeps its normal renderer, so text animators, shape and
240
291
  video animation stay native and its alpha updates every frame.
@@ -255,53 +306,9 @@ ordinary 2D layer. The field is ignored on any other media type.
255
306
  - **`castShadow`** — the layer's live silhouette is added to the rig's shadow map, so it
256
307
  drops a shadow on models and on the cyclorama. Needs a rig with
257
308
  `shadows.mode: "map"` and at least one light with `castShadow: true`. The layer's own
258
- pixels are untouched.
309
+ pixels are untouched. A 3D Scene's world takes no flat silhouette, so there a scene
310
+ object or a model layer has to cast the shadow.
259
311
  - **`receiveShadow`** — the set's shadows fall across the layer. This is the only one
260
312
  that re-films the layer inside the lit scene, so **Motion Blur, Rig (Puppet) and 3D
261
313
  Depth are unavailable on that layer** while it is on; the other two leave them
262
314
  working.
263
-
264
- **`lid`** matches `/^[A-Za-z0-9_-]{1,12}$/` and must be unique within the rig — it is the key keyframe tracks address.
265
-
266
- **Sizes are multiples of composition height**, so a rig authored at 1080p is correct at 4K with no migration. `width: 4` on a 1080-tall comp is 4320 world px.
267
-
268
- **A light is switched off with `intensity: 0`, never by removing it** — the light count is a compiled shader constant, so changing it recompiles every material in the scene. `on: false` is presentation only and does the same thing.
269
-
270
- **Cyclorama shape `"none"`** draws nothing: use it for a rig that only carries lights. Any other shape with `material.mode: "solid"` writes depth, so layers behind it are cut by its surface.
271
-
272
- **`material.mode: "shadowCatcher"`** keeps only the shadows the set receives and is transparent everywhere else, for models standing on the composition `background`. It writes no depth, so it never cuts a layer. Its shadows use `material.shadowColor` (default `#000000`), not `color` — `color` stays the Solid surface's colour, so a paper set switched to shadows does not paint them in the paper's own colour. `opacity` is the shadow strength (0-1), and the backdrop colour track `scc:cyc:col` drives `shadowColor` in this mode.
273
-
274
- **Rig keyframes** live on the rig item's `propertyKeyframes` under two prefixes (`sc:` numeric, `scc:` colour — the prefix alone decides numeric vs RGBA interpolation):
275
-
276
- In the editor, the stopwatch beside an animatable rig field promotes that exact
277
- property into a flat 0%/100% track seeded from the field's current authored value.
278
- Once any rig track exists, moving a selected light in the 3D world view auto-keys
279
- its `az`, `elev` and `dist` tracks at the playhead instead of overwriting the base
280
- rig. API-authored projects use the same track shape directly.
281
-
282
- | Key | Range |
283
- | --- | --- |
284
- | `sc:<lid>:intensity` \| `az` \| `elev` \| `dist` | per the light's type |
285
- | `sc:<lid>:angle` \| `penumbra` | spot only |
286
- | `sc:<lid>:width` \| `height` | area only (world px) |
287
- | `scc:<lid>:col` | colour |
288
- | `sc:cyc:x` \| `y` \| `z` \| `rotX` \| `rotY` \| `rotZ` \| `scale` \| `opacity` | — |
289
- | `scc:cyc:col` | colour |
290
- | `sc:env:intensity` \| `exposure` \| `rotX` \| `rotY` \| `rotZ` | — |
291
-
292
- **Refused, and the sanitizer drops them:** light `type`, adding/removing a light, `castShadow`, `shadowMapSize`, cyclorama `shape` and dimensions, `env.source`/`preset`/`url`. Each is a shader recompile or an environment re-bake, not a value — animating one would rebuild every frame. A track naming a `lid` that no longer exists is dropped on the next save.
293
-
294
- **The playhead outside every rig clip does NOT mean darkness.** The nearest rig still lights the scene, and a project with no rig falls back to the legacy three directionals.
295
-
296
- **21 built-in presets** (`rigPresets.js`), each a **complete** rig — applying one replaces `item.rig` wholesale:
297
-
298
- | Category | Preset ids |
299
- | --- | --- |
300
- | Studio | `threePoint` (the exact legacy default — use it to match a pre-rig project), `studioRoom`, `clamshellBeauty`, `highKeyWhite` |
301
- | Product | `productSoftbox`, `topDownProduct`, `turntable`, `pastelSweep`, `pianoBlack` |
302
- | Dramatic | `rimLitDark`, `filmNoir`, `stageSpotlight`, `campfireGlow` |
303
- | Daylight | `sunsetWindow`, `overcastExterior`, `desertNoon`, `blueHour` |
304
- | Location | `warehouse`, `sciFiRoom` |
305
- | Stylized | `neonTwoTone`, `duotonePop` |
306
-
307
- **Other rig-item fields the editor writes.** `item.rigLook = { id, name, hash }` records which look was applied (a preset id, or `saved:<id>` for a look the user saved) and a hash of the rig at that moment, so the editor can mark the look as edited once the rig drifts from it. It is informational — omit it when emitting a rig; a hand-made `hash` never matches, so the look would show as edited from the start. `item.rigAi` holds the AI video state — the prerender used as the source, the takes and which one is placed on the timeline. It is written by the editor from real render and generation results; do not write it yourself.