greenlight-motion 0.1.0 → 0.1.2

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,22 +18,37 @@ 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.1.0 doctor
21
+ npx greenlight-motion@0.1.2 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
25
25
  anything globally without asking the user. Always pin the version in every command you run.
26
26
 
27
+ ## The fast way: a scene script
28
+
29
+ For a new video, start with a **scene script** instead of a whole project: a short JSON file — scenes, their texts and
30
+ shapes, preset names, transitions, sounds — that `script` turns into `project.json` with the Video Editor's own
31
+ animation presets (the same converter the GreenLight Motion connector uses in Claude and ChatGPT). Hand-written
32
+ keyframes rarely look as good. The format, the design rules and the catalogues: `skill/references/scene-script.md`.
33
+
34
+ ```
35
+ npx greenlight-motion@0.1.2 script <folder> --from scenes.json # or write <folder>/script.json, then: script <folder>
36
+ ```
37
+
38
+ Then `check` and `stills` (below), fix the SCRIPT and run `script` again — it rewrites `project.json`. For what a
39
+ script can't say (3D, particles, motion paths, masks, text animators by hand), go on from `project.json` with the
40
+ workflow below and stop running `script` (it refuses to overwrite a project edited since, unless `--force`).
41
+
27
42
  ## Workflow
28
43
 
29
- 1. `npx greenlight-motion@0.1.0 init <folder>` — starts from a working example (title, subtitle, accent line, four
44
+ 1. `npx greenlight-motion@0.1.2 init <folder>` — starts from a working example (title, subtitle, accent line, four
30
45
  options). `--from file.json` starts from a project you have (for example one exported from GreenLight Dash).
31
46
  2. Edit `<folder>/project.json` — for changes to an EXISTING project prefer `apply` (below): it checks every edit and
32
47
  applies all or nothing. Put pictures, video, sound and fonts in `<folder>/assets/` and refer to them as
33
48
  `"asset:<file>"` (e.g. `"url": "asset:product.png"`).
34
- 3. `npx greenlight-motion@0.1.0 check <folder>` — fix everything it lists (missing files, template mistakes, blank
49
+ 3. `npx greenlight-motion@0.1.2 check <folder>` — fix everything it lists (missing files, template mistakes, blank
35
50
  frames, page errors). `--json` for machine output.
36
- 4. `npx greenlight-motion@0.1.0 stills <folder>` — renders frames and **a labelled contact sheet**. Open and look at
51
+ 4. `npx greenlight-motion@0.1.2 stills <folder>` — renders frames and **a labelled contact sheet**. Open and look at
37
52
  it before you go on: layout, readability, timing. Fix and repeat. `--at 0.5,1.2,3` for exact times.
38
53
  5. Hand it over: tell the user to open `<folder>/index.html` (Chrome or Edge), or `pack` it into one file, or
39
54
  `render` the MP4 yourself (`--fps 60 --motion-blur` for fast motion).
@@ -96,9 +111,9 @@ editor's own normalizer), so write only what you mean.
96
111
  - Not here: MotionFast templates (`mediaType: "motion"`) live only in GreenLight Dash — build the look from layers.
97
112
 
98
113
  Full references (generated from the editor's own docs; ignore the `/api/` routes they mention):
99
- `skill/references/` — video-projects, video-text, video-text-kinetic, video-text-presets, video-shapes, video-keyframes,
114
+ `skill/references/` — video-design, scene-script, video-projects, video-text, video-text-kinetic, video-text-presets, video-shapes, video-keyframes,
100
115
  video-camera, video-3d, video-effects, video-transitions, video-particles, video-paths, video-backgrounds, video-grid.
101
- Machine-readable lists: `npx greenlight-motion@0.1.0 schema effects | transitions | easings | paths | expressions`.
116
+ Machine-readable lists: `npx greenlight-motion@0.1.2 schema effects | transitions | easings | paths | expressions`.
102
117
 
103
118
  ## Template
104
119
 
@@ -145,7 +160,7 @@ an absolute moment; `align` `sync` (default) / `start` / `end`; `offset` seconds
145
160
 
146
161
  The library is 77 hand-picked sounds in categories (whooshes and swooshes, transitions, clicks, pop-ups and
147
162
  notifications, data blips, typing and writing, foley, bubbles, impacts and bass drops, camera, gears, voice clips).
148
- `npx greenlight-motion@0.1.0 schema sounds` lists every one with its `category`, `use` (what it is for), `duration`
163
+ `npx greenlight-motion@0.1.2 schema sounds` lists every one with its `category`, `use` (what it is for), `duration`
149
164
  and `sync`. Pick by `category` and `use`, then by name.
150
165
 
151
166
  - **Where the files come from:** the GreenLight sound CDN. `build` downloads each sound a cue uses into
@@ -161,7 +176,7 @@ The page has **Notes**: the user marks what should change right on the frame (a
161
176
  and writes a note for each mark. Every note carries the moment (seconds and frame), the mark in composition pixels
162
177
  and the layers under it (item ids in project.json), so you change exactly that and nothing else.
163
178
 
164
- - **They review on this computer:** run `npx greenlight-motion@0.1.0 review <folder>` and give them the URL it prints
179
+ - **They review on this computer:** run `npx greenlight-motion@0.1.2 review <folder>` and give them the URL it prints
165
180
  (it keeps running; `--open` opens their browser, only when they ask). Their notes land in `<folder>/review/`: read
166
181
  `review/NOTES.md`, look at the `at-<t>s.png` pictures (the marks are numbered), make the changes (`apply`), `build`
167
182
  again and tell them to reload the page.
@@ -190,11 +205,11 @@ that fails (`plugin`); look at `stills` for the look. A package that carries its
190
205
 
191
206
  The same project opens in GreenLight Dash (the desktop editor this engine comes from) for full editing: the page's
192
207
  edit note offers **Open in GreenLight Dash** (it imports the folder as a new project on the open board; a page with
193
- changes saves a `.glvideo`). `npx greenlight-motion@0.1.0 pack <folder> --glvideo` writes that file yourself: ONE
208
+ changes saves a `.glvideo`). `npx greenlight-motion@0.1.2 pack <folder> --glvideo` writes that file yourself: ONE
194
209
  `.glvideo` (the project + its files) that opens in GreenLight Dash on a double-click, or dropped on its board. Hand it
195
210
  over when the user wants to keep editing in the app. A
196
211
  GreenLight Dash project comes the other way through the app's **Export for agents** — a ready folder like the one
197
- `init` makes, with the options the author ticked. `npx greenlight-motion@0.1.0 init <folder> --from <its project.json>`
212
+ `init` makes, with the options the author ticked. `npx greenlight-motion@0.1.2 init <folder> --from <its project.json>`
198
213
  starts from one too. Never tell the user they must install it: the page plays, changes options and renders on its own.
199
214
 
200
215
  ## Files
@@ -216,20 +231,13 @@ When the user has not named one, choose from these first. Each still has to suit
216
231
  - **Layer Effects** (`layerEffects[]`): `runningGlow`, `motionBlur`, `motionTrail`, `autoFill`, `repetile`,
217
232
  `regionDistort`, `cornerPin`, `rig`, `droste`, `glass`, `chromaKey`, `layerMask`.
218
233
  - **Leave out unless asked:** `grain`, `lensPrism`, `linesDraw`, `sdfLensBlur`.
219
- - **Text animation presets** (`references/video-text-presets.md`, `references/video-text-kinetic.md`): Glide Soft,
234
+ - **Text animation presets** (`references/video-text-presets.md`, `references/video-text-kinetic.md`): Glide Text,
220
235
  Ember Type, Marker, Outline Pass, Neon Flow, Glitch; Kinetic: Swing Pop, Pendulum, Fan Deal, Whip, Word Swing,
221
236
  Letter Vortex; Voice: Spoken Box, Spoken Pop, Spoken Glow, Karaoke Wipe, Word Pop, Rise on Voice,
222
237
  Typed as Spoken. Pick the category first (what the text should do), then prefer these within it.
223
238
 
224
239
  ## Motion design that looks made, not generated
225
240
 
226
- - **Timing.** Entrances 0.4–0.8 s, exits faster than entrances. Stagger related elements by 0.06–0.12 s. Give the eye
227
- a held moment (≥ 1 s) on anything the viewer must read: about 3 words per second.
228
- - **Easing by intent.** Entrances: `AE Out` or `Expo Out` (fast start, soft landing). Camera and big moves: `AE Smooth`. A layer that passes through (in, glide, out): `Smooth Glide` with the `glide` ƒx Soft on that track, keys ±100 % → ∓150 % with the last key at 80 % (the Glide presets; see video-keyframes.md).
229
- Playful arrivals: `Back Out` or `Overshoot`. Exits: `Ease In`. `Linear` only for constant motion (tickers, rotation).
230
- The full list with the real curves: `schema easings`.
231
- - **One focal move at a time.** If everything moves, nothing reads. Secondary elements move less and later.
232
- - **Hierarchy through size and weight**, not colour alone; keep text inside the safe area (|panX|, |panY| ≤ 40).
233
- - **Camera** moves are slow and small unless they are the point. Fast motion at 60 fps with motion blur.
234
- - **Review the contact sheet** every round: blank frames, collisions, unreadable text, and anything off-frame are
235
- your bugs to fix before the user sees them.
241
+ Read `skill/references/video-design.md` before the first version and check every version against it: one idea per
242
+ scene, words on screen, hierarchy, timing, easing by intent, text animation picks, transitions, camera, sound and what
243
+ to look for in the contact sheet. It is the same text GreenLight Dash's agents and the chat connector use.
@@ -0,0 +1,104 @@
1
+ # The scene script — the fast way to a video that looks made
2
+
3
+ A short JSON script — scenes, their texts and shapes, preset names, transitions, sounds — that
4
+ `npx greenlight-motion script <folder>` turns into a full project (`<folder>/project.json`) with the GreenLight Video
5
+ Editor's own animation presets: the same converter the GreenLight Motion connector's build_video uses in Claude and
6
+ ChatGPT. Write it as `<folder>/script.json`, run `script`, then `check` and `stills` and look at the contact sheet.
7
+ Change the SCRIPT and run `script` again (it rewrites project.json). For what a script can't say (3D, particles, motion
8
+ paths, masks, text animators by hand), go on from project.json with `apply` and the other references — and stop
9
+ running `script`, or it replaces those edits.
10
+
11
+ Pictures, video and music: put the files in `<folder>/assets/` and name them `asset:<file name>`; an https link
12
+ works too (it has to stay reachable). SVG you write: `{ "type": "image", "svg": "<svg …>…</svg>" }`.
13
+
14
+ ## The scene script
15
+
16
+ ```json
17
+ {
18
+ "title": "Bean There — opening day",
19
+ "format": "16:9",
20
+ "style": { "background": "linear-gradient(160deg, #2b1a12, #0e0806)", "colors": { "text": "#f6efe7", "accent": "#d9a066", "muted": "#b8a594" } },
21
+ "scenes": [
22
+ { "name": "Hook", "duration": 2.6, "camera": "push-in",
23
+ "elements": [
24
+ { "type": "text", "text": "Bean There", "role": "headline", "textAnimation": "glide-text" },
25
+ { "type": "line", "at": [-180, 92], "to": [180, 92], "start": 0.7 },
26
+ { "type": "text", "text": "Coffee worth the detour", "role": "subhead", "at": [0, 150], "start": 0.9, "enter": "glide-soft-entry", "direction": "up" } ] },
27
+ { "name": "Offer", "duration": 2.4, "transition": "Camera Whip", "camera": "drift-left",
28
+ "elements": [
29
+ { "type": "pill", "w": 560, "h": 116, "color": "accent", "start": 0.15 },
30
+ { "type": "text", "text": "Opening day: 2 for 1", "role": "body", "color": "#1b120d", "start": 0.3, "enter": "pop-overshoot", "sound": "data-1" } ] },
31
+ { "name": "Close", "duration": 2.4, "transition": "Gradient Glow Stack", "background": { "shader": "silk", "look": "sunset" },
32
+ "elements": [
33
+ { "type": "text", "text": "12 Elm Street", "role": "headline", "textAnimation": "word-swing" },
34
+ { "type": "text", "text": "OPEN 7 – 7", "role": "label", "at": [0, 130], "start": 0.8, "letterSpacing": 6 } ] }
35
+ ]
36
+ }
37
+ ```
38
+
39
+ - **format** 16:9 · 9:16 · 1:1 · 4:5 (16:9 = 1920×1080, 9:16 = 1080×1920, 1:1 = 1080×1080, 4:5 = 1080×1350). Sizes and positions are in those pixels.
40
+ - **at**: `center` `top` `bottom` `left` `right` `top-left` … or `[x, y]` px from the centre (+y is down). Keep text inside the middle 80 %.
41
+ - **text**: `role` headline | subhead | body | label sets size and weight (override with `size`, `weight`); `color` text | accent | muted | #hex |
42
+ a CSS gradient; `width` (px, wraps), `align`, `uppercase`, `letterSpacing`, `lineHeight`. A headline gets `glide-text` unless you set `textAnimation`.
43
+ - **rect · pill · circle**: `w`, `h`, `color` (or a CSS gradient), `radius`, `border` {width, color}, `shadow`: true. They enter with `pop-overshoot` unless you set `enter`.
44
+ - **line**: from `at` to `to`, `width`, drawn on (`draw`: false to skip).
45
+ - **image · video**: `src` (a file in the folder's assets/ as `asset:<name>`, or an https link) or, for an image, `svg` (SVG markup you write); `w` or `h`
46
+ (px; the other follows the file's shape) or `fit` cover | contain
47
+ (default: a video covers the frame, a picture sits at 70 % of contain), `rotate` (deg), `opacity` (0–1). A video also takes
48
+ `startAt` (s into the file) and `volume` (0–100, default muted). No entrance unless you set `enter`.
49
+ - **Keyframes on any element** — your own motion: `"keyframes": { "x": [[0, -400], [0.8, 0, "Smooth Glide"]], "scale": [[0, 0.9], [2, 1.05]] }`
50
+ — tracks x, y (px offsets from `at`), scale, rotate (deg), opacity (0–1); each key [seconds into the element, value, easing of the
51
+ segment after it — Smooth Glide, AE Smooth, Expo Out, Back Out, Elastic, Linear …]. They replace an entrance's keys on the same track.
52
+ - **Every element**: `start` / `end` (s within its scene; default: staggered 0.12 s, to the scene's end), `enter` / `exit` (keyframe presets, `direction` up | down | left | right
53
+ for directional ones), `effects`: [{ "id", "params"? }], `sound` (played at its start), `id`, `label` (its name in the page's options).
54
+ - **Scene**: `duration` (s), `transition` into it (default Cross Fade, 0.6 s; `transitionDuration`), `camera` none | push-in | pull-out | drift-left | drift-right | drift-up (default push-in),
55
+ `background` (overrides style), `sounds`: [{ "sfx", "at" }]. Script-level `sound`: auto (default: a whoosh on each transition and on the first headline) | none.
56
+ - **background**: a colour, a CSS gradient, { "shader": id, "look"? } — shaders: silk, glowblobs, waves, plasma, mesh, fire, spacetravel, glitters, blueplanet, auroraridge, emberpool, neonvignette, electricvignette, spectrumdrift, emberfacet, silkdrift, eclipseprism, glassreeds, staticfringe, ripplefold, nightaurora, pixelbeams, petalgrid, amberblinds, icehalo, windowhaze (looks: references/video-backgrounds.md) — or { "image": src } / { "video": src } (covers the frame; b-roll behind the scene).
57
+ - **music** (script level): `"music": { "src": file, "volume": 80, "fadeIn": 0.3, "fadeOut": 1.2, "startAt": 0 }` — one track under the video.
58
+
59
+ ## What makes it look made, not generated
60
+
61
+ references/video-design.md — the same design rules GreenLight Dash's agents and the chat connector use. Read it before
62
+ the first version and check every version against it.
63
+
64
+ ## Catalogues
65
+
66
+ **Text animations** (`textAnimation`; ★ = preferred): ★ `glide-text` Glide Text · `glide-continuous` Glide Continuous · `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
67
+
68
+ **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
69
+
70
+ **Exits** (`exit`): `slide` Slide Out · `zoom-in` Zoom Out · `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 Out · `spring-in` Spring Out · `rubber-wobble` Rubber Wobble · `tap-nudge` Tap Nudge · `whip-swing` Whip Out · `flick-drop` Flick Up · `jelly-shake` Jelly Shake · `heartbeat` Heartbeat · `impact-punch` Impact Out · `pendulum-swing` Pendulum Swing · `loose-wiggle` Loose Wiggle Out · `shiver` Shiver Out · `float` Float Away · `flicker-in` Flicker Out · `earthquake` Earthquake Out · `tumble-drop` Tumble Up · `rubber-band` Rubber Band Out · `spring-twist` Spring Untwist · `slingshot` Slingshot Out · `pendulum-in` Pendulum Out · `smooth-overshoot-rise` Smooth Overshoot Drop · `spin-in` Spin Out · `twist-in` Twist Out · `roll-in` Roll Out · `tilt-settle` Tilt Away · `wind-up-spin` Wind-Up Spin Out
71
+
72
+ **Transitions** (★ = preferred): None · ★ Cross Fade · Push · Parallax · Wipe · Zoom In · Zoom Out · ★ Advanced Zoom · ★ Advanced Movement · 3D Carousel · Louver Flip · ★ Circular Wipe · ★ Pixel Blocks · ★ Camera Whip · ★ Zoom Ruler · ★ Gradient Glow Stack · ★ Gradient Map · Scale Swap · Side by Side · Dip to Black · Radial Wipe
73
+
74
+ **Overlay effects** (`effects`): `glow`, ★ `bloom`, `auraBlur`, `gradientBeam`, ★ `gradientSweep`, `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`
75
+
76
+ **Layer effects** (`effects`): `mockupFrame`, ★ `runningGlow`, `depthMap`, `depth3d`, `depthScan`, `fresnelRim`, ★ `motionBlur`, ★ `motionTrail`, `autoFill`, ★ `repetile`, `regionDistort`, `bend`, `cornerPin`, `meshWarp`, `bezierWarp`, `rig`, ★ `droste`, `pryzm`, ★ `glass`, `chromaKey`, `layerMask`
77
+
78
+ Effect params: references/video-effects.md.
79
+
80
+ **Sounds** (`sound`, `sounds[].sfx`):
81
+ - Ambient Transition: ambient-transition-22 (5s)
82
+ - Bass Drop: cinematic-transition-13 (2.5s), sd-low-06 (8s), sd-low-hi-04 (8s)
83
+ - Bleep: digital-bleep-01 (0.286s)
84
+ - Bubble: bubble-07 (1.565s), bubble-08 (2.061s), bubble-10 (0.906s), bubble-23 (1.071s)
85
+ - Camera: camera-focus-and-shutter (1.022s), camera-focus-beep (0.283s), camera-power-switch (0.48s)
86
+ - Cartoon Sound: cartoon-sound-17 (4s)
87
+ - Click: keyboard-click-02 (0.294s), selection-data-click (0.68s)
88
+ - Crystal: crystal-swoosh (2.715s)
89
+ - Cyber Transition: cyber-transition-20 (1.353s)
90
+ - Data: data-1 (0.454s), data-6 (0.388s), data-8 (2.841s), data-9 (1.716s), data-11 (2.848s), data-12 (2.072s), data-14 (2.233s)
91
+ - Foley: disk-mechanism-close (0.939s), mechanical-insert (0.491s), metal-movement-low (1.022s), paper-flap-01 (1.278s), paper-flap-02 (0.929s), paper-turn-01 (0.672s), rustle-whoosh (0.971s), stretched-leather-02 (1.5s), tiny-mechanism-assembling-fast (0.789s)
92
+ - Foley & Ambience: flash (0.967s)
93
+ - Full Screen Transition: big-transition-whoosh (2.465s)
94
+ - Gears (single): small-gears-13 (1.5s), small-gears-16 (1.022s)
95
+ - Glitch: glitch-transition-06 (0.806s)
96
+ - Notifications and Buttons: notifications-and-buttons-16 (1s), sfx-confirmation (4s), sfx-failure (0.265s), sfx-message-01 (0.159s), sfx-message-05 (0.75s), sfx-notification-01 (2.4s), sfx-notification-08 (2.3s), sfx-payment (1.323s)
97
+ - SFX Transition: cinematic-rewind-short (1.056s), sfx-message-03 (0.174s), simple-transition-08 (3.557s), simple-transition-13 (3s)
98
+ - Strobe: strobe-transition-22 (1s)
99
+ - Swoosh: soft-swoosh (1.848s), standard-swoosh (1.335s)
100
+ - Text: keyboard-writing-02 (2.904s), machine-writing-01 (3.924s), pen-writing-tick (0.429s)
101
+ - Tiles: tiles-07 (1.473s), tiles-08 (0.979s)
102
+ - Whoosh: airy-whoosh-02 (2.944s), liquid-transition-whoosh (1.76s), resonant-light-whoosh (3.203s), whoosh-3 (0.469s), whoosh-13 (1.99s), whoosh-38 (1.263s)
103
+ - Whoosh / Additional: whoosh-05 (2.116s)
104
+
@@ -81,7 +81,7 @@ On a floor-bearing rig, `placeOnRigFloor: true` stands the shape on the cycloram
81
81
 
82
82
  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.
83
83
 
84
- **`model3dConfig` fields** (sanitized by `sanitizeModel3dConfig` in `VideoEditor.jsx`; defaults in `MODEL_CONFIG_DEFAULTS` in `types.js` — out-of-range values are clamped, bad enums fall back):
84
+ **`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):
85
85
 
86
86
  | Field | Values / range | Default | Meaning |
87
87
  | --- | --- | --- | --- |
@@ -89,15 +89,17 @@ Some GLBs are authored facing away from the viewer — for those, set `baseRotat
89
89
  | `preset` | preset id string | `null` | Required when `source: "preset"`. An unknown id falls through to the clip url. |
90
90
  | `fit` | `"height"` \| `"width"` \| `"unit"` | `height` | How `fitValue` sizes the model: % of frame height, % of frame width, or raw pixels-per-model-unit. |
91
91
  | `fitValue` | `0.01–10000` | `60` | With `fit: "height"`, `60` = the model's bounding box spans 60 % of the frame height. |
92
- | `anchor` | `"center"` \| `"base"` | `center` | Pivot: bbox centre, or bbox floor (for objects standing on a surface). |
92
+ | `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. |
93
93
  | `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. |
94
+ | `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`. |
95
+ | `receiveShadow` | bool | `true` | With a Scene Rig: rig shadows darken the model. `false` = they never land on it. Only stored when `false`. |
94
96
  | `baseRotation` | `{x,y,z}` deg, `±360` | `{0,0,0}` | Static orientation applied before keyframed rotation. |
95
97
  | `env.hdr` | see list below | `potsdamer_platz_1k` | Image-based lighting environment (reflections + ambient). |
96
98
  | `env.intensity` | `0–5` | `1` | Environment light strength. |
97
99
  | `env.exposure` | `0.05–8` | `1` | Tone-mapping exposure of the model pass. |
98
100
  | `env.rotation` | `±360` deg | `0` | Spins the environment (moves reflections). |
99
101
  | `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. |
100
- | `animation.clip` | glTF animation name \| `null` | `null` | Plays the named animation baked into the asset; `null` = static. Names must match the glTF exactly. |
102
+ | `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. |
101
103
  | `animation.speed` | `-8–8` | `1` | Playback rate (negative = reversed). |
102
104
  | `animation.loop` | `"once"` \| `"loop"` \| `"pingpong"` | `loop` | Loop mode. |
103
105
  | `animation.offset` | `±600` s | `0` | Start offset into the animation. |
@@ -108,10 +110,31 @@ Some GLBs are authored facing away from the viewer — for those, set `baseRotat
108
110
 
109
111
  `env.hdr` options: `potsdamer_platz_1k` (City), `studio_small_03_1k` (Studio), `lebombo_1k` (Apartment), `kiara_1_dawn_1k` (Dawn), `venice_sunset_1k` (Sunset), `dikhololo_night_1k` (Night), `forest_slope_1k` (Forest), `rooitou_park_1k` (Park), `st_fagans_interior_1k` (Lobby), `empty_warehouse_01_1k` (Warehouse).
110
112
 
111
- **Screen artwork (preset devices only).** `model3dConfig.screen` puts an image or video on the device's display, using the same pipeline as the Mockup builder: `{ "url": "<media url>", "scale": 1 (0.1–5), "offsetX": 0, "offsetY": 0 (±1), "rotation": 0 (±180°), "padding": 0 (0–0.45), "fitMode": "cover"|"contain", "borderRadius": 0 (0–0.5) }`. Only stored when `url` is set. Custom models have no known screen mesh, so `screen` is dropped when switching a layer to `source: "custom"`.
113
+ **Screen artwork (preset devices).** `model3dConfig.screen` puts an image or video on the device's display, using the same pipeline as the Mockup builder: `{ "url": "<media url>", "scale": 1 (0.1–5), "offsetX": 0, "offsetY": 0 (±1), "rotation": 0 (±180°), "padding": 0 (0–0.45), "fitMode": "cover"|"contain", "borderRadius": 0 (0–0.5) }`. Only stored when `url` is set; dropped when switching a layer to `source: "custom"`.
114
+
115
+ **Media on any model's own meshes — `model3dConfig.screens`.** Up to 8 entries, each putting an image or a video on the meshes whose node name (`node`) or first material name (`material`) matches — a slab showing a live UI, a TV, a poster, a phone inside a scene, on custom GLBs and presets alike:
116
+
117
+ ```jsonc
118
+ "screens": [{
119
+ "material": "UI_panel", // or "node": "Screen_mesh"
120
+ "url": "/media/admin/{board_id}/screen.mp4",
121
+ "fitMode": "stretch", // stretch (fill the mesh's own UVs — default) | cover | contain
122
+ "aspect": 1.7778, // canvas aspect for cover/contain (default 16:9)
123
+ "uv": "mesh", // mesh = the glTF UVs | planar = projected over the mesh's two largest axes
124
+ "shading": "screen", // screen = self-lit, exact colours | lit = takes the set's light, shadows, reflections and still glows
125
+ "receiveShadow": true, // screen only: Scene Rig shadows still fall on it (colours stay exact elsewhere). Default off
126
+ "offset": 0, "loop": true, // video: seconds into the media at the layer's first frame; loop or hold the last frame
127
+ "mirrorX": false, "flipY": false, // UV fixes for oddly authored meshes (glTF UV origin is top-left)
128
+ "scale": 1, "offsetX": 0, "offsetY": 0, "rotation": 0, "padding": 0, "borderRadius": 0
129
+ }]
130
+ ```
131
+
132
+ 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.
112
133
 
113
134
  **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).
114
135
 
136
+ **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.
137
+
115
138
  **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.
116
139
 
117
140
  ### Scene Rig — one set for every 3D layer
@@ -153,18 +176,63 @@ the cheap soft blob under each model standing on the rig floor (`placeOnRigFloor
153
176
  A Scene Rig added in the editor starts with `mode: "map"` and only the Key light
154
177
  (`k1`) casting; a rig you write yourself gets exactly what you write.
155
178
 
179
+ **The shadow map follows the camera.** Each frame a casting light's map is fitted to
180
+ what the camera actually sees of the set (the frustum clipped to the receivers, in the
181
+ casters' reach), in 1/8-octave size steps with a texel-snapped centre so a moving
182
+ camera does not make shadow edges crawl. A small character on a long set (a ten-metre
183
+ page) therefore gets shadows as crisp as on a small one — there is no need to move the
184
+ cast to a steeper light or to shrink the set. Shadow acne is handled automatically
185
+ (per-texel offsets plus receiver-plane depth bias in the soft-shadow filter); a light can
186
+ override it with `shadowBias` / `shadowNormalBias` (0–20, in texels of its map; leave
187
+ them out for automatic — raise them if a surface still shows stripes, lower them if a
188
+ shadow looks detached).
189
+
156
190
  **One shared world, but only when a rig exists.** With a rig, every 3D layer is placed
157
191
  in one scene so the shadow map can contain every caster; each layer still renders in
158
192
  its own pass, so opacity, blend modes, masks, effects and DOF are unchanged. With NO
159
193
  rig, each model keeps its own scene and its own `model3dConfig.env` — which is why
160
194
  adding a rig to an existing project is additive and never re-lights it by surprise.
195
+ With a rig, the rig's `env` (and exposure) is the one in effect for every model;
196
+ key `sc:env:intensity` to change reflections over time (e.g. only once a metal
197
+ object takes the stage).
198
+
199
+ **Camera depth of field is per pixel on 3D passes.** A model, a cyclorama or an
200
+ occluding shadow plate hands the compositor its depth buffer, and every pixel blurs by
201
+ the CoC of its OWN distance (`camAperture · |camFocusDistance − d| / d · camBlurLevel/100`,
202
+ capped at 64 px, still held off inside the focus area) — a character in focus stays
203
+ sharp in the same model as a wall metres behind it. The gather is depth-aware: a
204
+ blurred background never smears over a sharp foreground edge, a sharp foreground
205
+ leaves no halo on the blurred background, and a foreground in front of the focus plane
206
+ blurs out over what is behind it and past its own silhouette. Taps read the picture
207
+ prefiltered (mip level of the tap spacing), so thin bright detail under a wide blur —
208
+ lettering far behind the focus — averages instead of sparkling; 2D layers get the same.
209
+ A whole set (backdrop included) can therefore live in one model layer. Flat 2D layers
210
+ keep one CoC per layer — the depth of their plane.
161
211
 
162
- **Light types and their fields.** `directional` / `point` / `spot` take `az` (±360°), `elev` (±90°), `dist` (world px); `ambient` and `hemisphere` take neither. `spot` adds `angle` (1–89°) and `penumbra` (0–1); `hemisphere` adds `groundColor`. Fields a type does not have are dropped by the sanitizer.
212
+ **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.
163
213
 
164
- **`intensity` means the same on every light: what the set receives.** A `point` or `spot` of `2` at `dist: 2400` 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.
214
+ **`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.
215
+
216
+ **`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.
165
217
 
166
218
  ### Flat layers in the 3D scene — `item.shadowPlate`
167
219
 
220
+ **Turning a flat layer in 3D — the pivot.** Every flat layer (image, video, shape, text, html clip) turns in 3D
221
+ with `rotateX` / `rotateY` / `rotateZ`, static fields and `propertyKeyframes` tracks alike: a static angle and the
222
+ same angle reached by a key give the same pose, and mixing static and keyed channels never moves the layer. The
223
+ pivot is `keyframeTransformOrigin` (`"center"` default; `"top"`, `"bottom"`, `"left"`, `"right"`, the four
224
+ corners such as `"bottom left"`, or CSS-style `"50% 100%"`): rotation and the keyed `scale` / `scaleX` / `scaleY`
225
+ turn and grow about that point of the layer's own box (the drawn plate; for text, the laid-out block). `panX` /
226
+ `panY` / `panZ` keep placing the layer's centre as it rests unrotated, and the static `zoom` sizes it about that
227
+ centre. So a card that stands up off a page is placed **flat**, with `keyframeTransformOrigin: "bottom"` and
228
+ `rotateX` 0 → −90 keyed (or static −90): its bottom edge stays on the line it lay on, and a keyed `scaleY` 0 → 1
229
+ with the card already standing grows it up out of that line. No translate arc is needed.
230
+
231
+ **Stacking.** Without a model in the frame, flat layers composite back to front (painter's order). A layer turned
232
+ in 3D is ordered against every flat layer it does not cross by which side of that layer's plane it stands on, so
233
+ a card standing on a page or floor layer always draws over it, wherever the camera looks. Two layers that cut
234
+ through each other still need `shadowPlate.occludes` (below) to intersect per pixel.
235
+
168
236
  `text`, `shape`, `image` and `video` items can join the 3D scene without becoming
169
237
  solids. The layer stays flat and keeps its normal renderer, so text animators, shape and
170
238
  video animation stay native and its alpha updates every frame.
@@ -213,6 +281,7 @@ rig. API-authored projects use the same track shape directly.
213
281
  | --- | --- |
214
282
  | `sc:<lid>:intensity` \| `az` \| `elev` \| `dist` | per the light's type |
215
283
  | `sc:<lid>:angle` \| `penumbra` | spot only |
284
+ | `sc:<lid>:width` \| `height` | area only (world px) |
216
285
  | `scc:<lid>:col` | colour |
217
286
  | `sc:cyc:x` \| `y` \| `z` \| `rotX` \| `rotY` \| `rotZ` \| `scale` \| `opacity` | — |
218
287
  | `scc:cyc:col` | colour |
@@ -21,6 +21,7 @@ Rules:
21
21
  - The camera lives on a channel marked `"kind": "camera"`. **A project may have at most one camera channel, and it must be LAST in the `channels` array** (channel order is z-order; last renders highest). The editor re-pins it on every write and on load, so a project authored with the camera elsewhere is silently reordered — emit it last to avoid surprising diffs. Put several camera clips on it to chain moves; the clip under the playhead wins, and outside every camera clip the camera is neutral.
22
22
  - Camera clips are synthetic (no `url`) and are keyframed with their own property registry, using the ordinary `propertyKeyframes` shape (positions 0–100 % of the clip).
23
23
  - Any channel can opt out with `"cameraLocked": true` — it renders exactly as if there were no camera (watermarks, fixed captions).
24
+ - A camera-locked layer is a 2D layer, as in After Effects: it composites in STACK order and never depth-tests against 3D. Above a model layer in the stack it draws over the model (a flash, a title at the comp plane needs no pulling toward the camera); at the very bottom of the stack (a full-frame backdrop) it stays behind everything, the Scene Rig's cyclorama included. 3D layers on either side of a locked layer still occlude each other by depth.
24
25
 
25
26
  ```json
26
27
  {
@@ -0,0 +1,65 @@
1
+ # Motion design that looks made, not generated
2
+
3
+ What makes a video read as designed rather than generated. The rules hold however the video is built — a project
4
+ through the app's API, `project.json` with the engine's CLI, or a scene script — so read them before the first version
5
+ and check every version against them.
6
+
7
+ ## Story and words
8
+
9
+ - **One idea per scene**: 3–6 scenes of 2–3 s for a short video. A scene that says two things says neither.
10
+ - **At most ~8 words on screen at once, held at least 1 s.** People read about 3 words a second; text that leaves
11
+ before it is read is noise.
12
+ - **9:16**: bigger text, stacked vertically, everything in the middle 80 %.
13
+
14
+ ## Hierarchy
15
+
16
+ - One headline per scene; supporting text much smaller. Hierarchy comes from size and weight, not colour alone.
17
+ - One accent colour and strong contrast.
18
+ - Keep text inside the middle 80 % of the frame (in a project: |panX| and |panY| ≤ 40): players and feeds crop the
19
+ edges.
20
+
21
+ ## Timing
22
+
23
+ - Stagger related elements by 0.06–0.12 s.
24
+ - **One focal move at a time.** If everything moves, nothing reads: supporting elements move less and later.
25
+ - Shapes and pictures enter in 0.4–0.8 s; exits are faster than entrances.
26
+ - A text animation takes at least 0.8–1 s over the whole line, first unit in to last unit settled, unless the video
27
+ needs snap (a beat hit, a glitch flick): faster text flashes past before it is read. The arithmetic per kind of
28
+ animation: video-projects.md, Text animation pace.
29
+
30
+ ## Easing by intent
31
+
32
+ - Entrances: `AE Out` or `Expo Out` (fast start, soft landing). Camera and big moves: `AE Smooth`.
33
+ - A layer that passes through (in, glide, out): `Smooth Glide` with the `glide` expression — the Glide keyframe
34
+ presets build it (video-keyframes.md).
35
+ - Playful arrivals: `Back Out` or `Overshoot`. Exits: `Ease In`.
36
+ - `Linear` only for constant motion: tickers, rotation, progress along a path.
37
+
38
+ ## Text animation
39
+
40
+ - Animate words or letters, never a whole line at once — the text presets do this.
41
+ - One family per video. Calm presets for lines and captions: Glide Text (`glide-text`), Classic Fade Up
42
+ (`classic-fade-up`), Classic Rise (Mask) (`classic-rise-mask`); Focus Type (`focus-type`) for a title.
43
+ - Punchy presets that tilt or overshoot — Swing Pop (`swing-pop`), Whip (`whip`), Letter Vortex (`letter-vortex`) —
44
+ only on 1–3 words when the video wants energy. On a longer caption they read cheap.
45
+ - Glide Continuous (`glide-continuous`) and Glide Soft Stop (`glide-soft-stop`) never let the text rest: use them
46
+ only when the user asks for drifting text.
47
+ - On a coloured card, pick a preset without a colour band (Classic Rise (Mask), Classic Fade Up): a band's colour can
48
+ clash with the card.
49
+
50
+ ## Transitions and camera
51
+
52
+ - Vary two or three kinds of transition across a video: one kind everywhere looks like a template, a new one at
53
+ every cut looks random.
54
+ - Camera moves are slow and small unless they are the point. Fast motion looks best at 60 fps with motion blur.
55
+
56
+ ## Sound
57
+
58
+ - A soft whoosh on each transition and on the first headline is a good base. `data-1` or `selection-data-click` for
59
+ UI-like pops, `cinematic-transition-13` for one big reveal.
60
+
61
+ ## What to check in every version
62
+
63
+ Look at real frames across the video (a contact sheet) before the user sees it — the JSON doesn't show these:
64
+ blank or empty frames, overlapping or cut-off text, text too small or gone too fast, nothing moving or everything
65
+ moving at once, weak hierarchy, clashing colours. Fix them and look again; two rounds are usually enough.
@@ -306,9 +306,9 @@ Example, a 2.39:1 letterbox that slides in and out over everything below (on an
306
306
 
307
307
  `ascii` (**ASCII**, group Print, both scopes, free) rebuilds the picture from characters. Each cell's average tone picks a glyph from a ramp sorted by how much ink each character has (sparse = dark, dense = bright). `cellSize` (6–400 px at 1080p of the item's box, default 14) is the character height; the width follows the font. `charset`: `0` Standard ` .:-=+*#%@` (default) / `1` Detailed (70 characters) / `2` Blocks `░▒▓█` / `3` Binary / `4` Dots / `5` Matrix (half-width katakana) / `6` Custom, which uses `customChars`. `customChars` is a `type: "text"` param: a string of up to 80 characters, de-duplicated and auto-sorted by ink, with a space added for empty cells, so any word works. `font`: `0` Mono / `1` Mono Bold (default) / `2` Sans Bold / `3` Serif. `colorMode`: `0` Source (each glyph in its cell's colour, `colorBoost` 0–100 % pushes it toward full brightness) / `1` Single Color (`glyphColor`) / `2` Gradient (`glyphColor` for dark cells → `lightColor` for bright ones). `background`: `0` Solid (`backgroundColor`, default `#050505`) / `1` Source (the picture behind the glyphs at `sourceLevel` 0–100 %) / `2` None (transparent between glyphs). `contrast` (0–300 %, default 120) and `brightness` (−100…100) shape the tone before glyphs are picked. `invert` `1` maps dark areas to dense glyphs, the dark-ink-on-paper look (pair it with a light `backgroundColor`). `dither` (0–100 %, default 60) breaks up the contour lines a slow gradient would otherwise draw; it is fixed per cell, so video does not shimmer. `edgeLines` (0–100 %, default 0) swaps in `- / | \` along contours, following the edge direction. The character grid is printed on the item's box, so on a regular item it rides the layer; `clip` defaults on (the background is cut to the item's shape). Keyframable: `cellSize`, `colorBoost`, `sourceLevel`, `contrast`, `brightness`, `dither`, `edgeLines`, `mix`; the selects, colours and `customChars` are not. Presets in the card: Terminal, Color, Blocks, Binary, Matrix, Edges, Paper. Example: `{"id": "ascii", "mix": 100, "params": {"cellSize": 14, "charset": 6, "customChars": "GREENLIGHT", "font": 1, "colorMode": 1, "glyphColor": "#39ff7a", "background": 0, "backgroundColor": "#040806"}}`.
308
308
 
309
- #### Pixelate
309
+ #### Mosaic
310
310
 
311
- `pixelate` (**Pixelate**, group Display, both scopes, free) rebuilds the item from flat blocks. Use it for every pixelated look: pixel blocks, a mosaic, an 8-bit look, censor blocks, a pixelate transition. `crtMatrix` is a screen texture (CRT, LED and LCD sub-pixels), not pixelation.
311
+ `pixelate` (**Mosaic**, group Display, both scopes, free) rebuilds the item from flat blocks. Use it for every pixelated look: pixel blocks, a mosaic, an 8-bit look, censor blocks, a pixelate transition. `crtMatrix` is a screen texture (CRT, LED and LCD sub-pixels), not pixelation.
312
312
 
313
313
  - `sampling`: `0` Average (default) makes each block the exact mean colour of the pixels under it. The mean is premultiplied, so a half-covered block is half transparent and soft content turns into soft blocks. `1` Center takes the colour at the block centre: crisper and harder, but blocks flicker as detail moves under them.
314
314
  - `size` (1–400, default 24) is the block size in px at 1080p of the frame, the geometric mean of width and height. Below 1.5 the effect is off and costs nothing, so a `size` track that starts at 1 fades the blocks in cleanly.
@@ -394,14 +394,14 @@ To posterize everything below, put it on an adjustment item (channel order as in
394
394
 
395
395
  #### Motion Effects II notes
396
396
 
397
- Ten of them have non-obvious semantics: `directionalSmear`, `lightRays`, `longShadow`, `outlineBands`, `lensPrism`, `kaleidoscope`, `crtMatrix`, `vectorFieldWarp`, `pixelStretch`, `edgeDetect`. Non-obvious semantics:
397
+ Nine of them have non-obvious semantics: `lightRays`, `longShadow`, `outlineBands`, `lensPrism`, `kaleidoscope`, `crtMatrix`, `vectorFieldWarp`, `pixelStretch`, `edgeDetect`. Non-obvious semantics:
398
398
 
399
399
  - `longShadow` is **layer-scope only** — it paints outside the source silhouette, which an adjustment layer's opaque below-stack has no room for.
400
400
  - `outlineBands` grows contour bands from a distance field. On a regular layer the default `source: 0` (Alpha) uses the layer silhouette; on an adjustment item alpha is the opaque below-stack, so use `source: 1` (Luminance) or `2` (Edges) there. `params.ramp` is a CSS gradient string colouring the band field near → far.
401
401
  - `lightRays` in `source: 0` (Alpha) mode makes the layer silhouette emit; on an adjustment item prefer `1` (Luminance) or `2` (Highlights). `originX`/`originY` are in the layer's own 0–1 box and may go outside it (e.g. `originY: 1.2` for light from above). It averages 24–48 taps, so a thin line smeared over a long distance comes out dotted and faint — use `radialFastBlur` (Brightest) for that.
402
402
  - `pixelStretch` is deliberately a realtime pixel-**stretch** approximation, not an exact sort.
403
403
  - `edgeDetect` (**Edge Detect**, group Graphic, both scopes) turns the content into a styled edge overlay. `method` picks the detector: `0` Gradient (thick luminous outlines, `operator`/`threshold`/`knee`), `1` Thin Line/Canny (1px lines; `lowRatio` + `linkIter` control hysteresis), `2` Ink Sketch/XDoG (`dogSigma`/`dogK`/`dogTau`/`dogPhi`/`dogEps` — keeps hatching and shading), `3` Contour Bands (`bands` luminance level-sets). There is **no backdrop** — `sourceLevel` (0–100, keyframable) shows the original underneath: `0` is pure edges over transparency in layer scope, `100` is the full original with edges on top (use `blend: 1` Add for the glowing-outline social look). `colorMode`: `0` flat `edgeColor`, `1` source color, `2` hue from each edge's own gradient angle, `3` duo ramp `edgeColor2`→`edgeColor` by luminance. `animMode` drives itself from the clip clock: `1` Draw On and `2` Scanner sweep along `sweepAngle` (layer-space, rotates with the layer) across the clip's duration — the Scanner shows the untouched original ahead of the line; `3` Flicker, `4` Glow Pulse, `5` Line Boil (hand-drawn wobble) run on clip time. `animSpeed` (0.1–4×) scales all of it — the sweeps complete in 1/speed of the clip, the clock modes tick that much faster. Presets: Neon Wire / Outline Pop / Ink Sketch / HUD Scanner / Topographic / Prism Flow / Electric Ghost — all ship with `animMode: 0`; turn animation on deliberately.
404
- - Directions (`directionalSmear.direction`, `longShadow.direction`, prism axis) are authored in the layer's own frame and rotate with it.
404
+ - Directions (`longShadow.direction`, prism axis) are authored in the layer's own frame and rotate with it.
405
405
 
406
406
  `fastCameraWiggle` (**Fast Camera Wiggle**, group Motion, both scopes) mimics fast handheld camera shake with real shutter motion blur — built for shot cuts. fBm noise drives a screen affine of the plate (offset / roll / zoom pulse), and the blur is exact: `samples` sub-frame transforms across the `shutter` window (degrees at a 30fps reference, up to 720) are averaged in one pass. `amount` is the master (0–2000%; 100% ≈ 10% of frame height of peak excursion) and scales offsets, `rotation` and `zoomPulse` together — for a shot cut, **keyframe `fx:<eid>:amount`**: spike it at the cut frame and decay to 0 over ~10 frames. The default `frequency` (1 Hz) is a slow drift; cut shakes want ~8–17 Hz. `xWeight`/`yWeight` shape the direction (handheld reads best with Y below X), `roughness` is the noise persistence (high = jerkier), `seed` decorrelates instances. `phase`: `0` Trailing (streak behind the motion, default) / `1` Centered. `edges`: `0` Mirror (tiles in the layer's box — the full-frame default), `1` Zoom to Cover (auto punch-in computed from the amplitude bound, capped 4×, so it follows a keyframed amount), `2` Transparent (the isolated plate's real edges). The wiggle is a pure function of the clip-local clock, so scrubbing and renders are deterministic; two instances with the same seed shake identically. Pivot is the layer's own centre (frame centre on an adjustment item), so an off-centre layer shakes in place. It writes outside the silhouette by design; leave `clip` unset. Presets: Subtle Handheld / Fast Cut / Impact / Whip Chaos / Earthquake MAX.
407
407
 
@@ -557,8 +557,8 @@ On a `model3d` item, these Overlay Effects have a REAL 3D version that renders i
557
557
  - In `capabilities.json` these effects carry `real3dOnModels: true`; 3D-only params and options carry `model3d: true`.
558
558
  - The 3D version runs only when the entry says `"real3d": true` AND its id is in the list. The editor sets it when one of these is ADDED to a model layer (the picker marks them **3D**); the card's **Render in 3D** toggle switches an entry between the two. Any other effect on a model stays on the flat chain.
559
559
  - Same `params`, Mix, Active Range and `fx:<eid>:*` keyframes as the flat effect. `clip` and the Object family's tracking params are ignored in 3D — the model IS the subject.
560
- - What changes in 3D, in short: patterns (Pixelate Layer grid, Pixel Matrix, Halftone, ASCII, Dither, Fractal Noise) are locked to the SURFACE and turn with the model; lighting effects (Halftone dot size, Fill's shading, Colorama, Flash) use the model's real light and normals; RGB Split re-films the model from two shifted cameras (real parallax); Bloom thresholds the displayed light but blooms the HDR light; Beam / Sweep / Contour / Glitch / Slit Scan / Pixel Stretch run along the MODEL's own axes; Neon Glow, Lines Draw and Edge Detect trace its real outline, creases and depth steps; Light Rays' origin sits in the model's box and the body occludes its own rays; Shape Rulers draws its real 3D bounding box; Curves applies to the HDR light in tone space. Bloom's Threshold / Soft Knee are judged on the displayed light (same meaning as the flat slider), and what blooms is the HDR light, so a strong highlight glows more than a clipped one.
561
- - 3D-only values (hidden on a flat entry; turning **Render in 3D** off resets them): Colorama `input` `5` Height / `6` Depth / `7` Facing; Pixelate `grid` `2` **Voxels** (the model rebuilt from cubes on a grid in its own space — `size` is the cube size in px, the silhouette turns blocky too); Light Rays `source` `3` **Behind** (a light disc behind the body, rays escape round its silhouette); Flash Blink `lightAngle` (0–360°, default 135 — the side of the body the flash lands on).
560
+ - What changes in 3D, in short: patterns (Mosaic Layer grid, Pixel Matrix, Halftone, ASCII, Dither, Fractal Noise) are locked to the SURFACE and turn with the model; lighting effects (Halftone dot size, Fill's shading, Colorama, Flash) use the model's real light and normals; RGB Split re-films the model from two shifted cameras (real parallax); Bloom thresholds the displayed light but blooms the HDR light; Beam / Sweep / Contour / Glitch / Slit Scan / Pixel Stretch run along the MODEL's own axes; Neon Glow, Lines Draw and Edge Detect trace its real outline, creases and depth steps; Light Rays' origin sits in the model's box and the body occludes its own rays; Shape Rulers draws its real 3D bounding box; Curves applies to the HDR light in tone space. Bloom's Threshold / Soft Knee are judged on the displayed light (same meaning as the flat slider), and what blooms is the HDR light, so a strong highlight glows more than a clipped one.
561
+ - 3D-only values (hidden on a flat entry; turning **Render in 3D** off resets them): Colorama `input` `5` Height / `6` Depth / `7` Facing; Mosaic `grid` `2` **Voxels** (the model rebuilt from cubes on a grid in its own space — `size` is the cube size in px, the silhouette turns blocky too); Light Rays `source` `3` **Behind** (a light disc behind the body, rays escape round its silhouette); Flash Blink `lightAngle` (0–360°, default 135 — the side of the body the flash lands on).
562
562
  - Light that leaves the silhouette (halos, streaks, the ruler box, RGB Split's copies) is composited at the model's centre depth, so it is occluded by — and occludes — other 3D layers like the model itself.
563
563
  - Cost: nothing until a model carries one of these. The passes and their buffers load and allocate on first use.
564
564
 
@@ -17,7 +17,7 @@
17
17
 
18
18
  Every timeline item can animate its visual properties over the clip's duration. Each property gets its **own independent track** of keys under `propertyKeyframes` — an opacity fade on a different rhythm than a slow zoom, a rotation eased with a bounce while the scale stays smooth, etc. Tracks are always separate — there is no merged, all-properties-per-key form.
19
19
 
20
- Positions are always **0–100, percent of the clip's played duration** (not seconds). `value` is stored as a **string**. Easing values come from `GET /api/video-editor/capabilities` → `easings.keyframe.values` (e.g. `Linear`, `Smooth`, `Ease In`, `Ease Out`, `AE In`, `AE Smooth`, `AE Out`, `Power2 Smooth`, `Expo Out`, `Back Smooth`, `Bounce Out`, `Elastic`, …); an easing set on a key shapes the segment **after** that key. The `AE *` values are the After Effects-style bezier curves shared with MotionFast (`AE Smooth` is the classic slow-start/soft-stop AE feel — a good default for cinematic moves). Every easing still comes to a dead stop on its key. For more live energy — a move that lands soft and keeps drifting on, a shot that never sits still between keys — add the `glide` expression type on top of the track (`propertyKeyframeFx.<track> = {"type":"glide","params":{"fast":0.1,"slow":1,"weight":40,"delay":0}}`, see **Glide** under [Track expressions](video-keyframes.md#track-expressions-propertykeyframefx)). Two owner curves sit right after `Smooth`: **`Smooth Glide`** (`cubic-bezier(0.2111, 0.7721, 0.8541, 0.112)` — arrives fast, glides slowly through the middle, leaves fast) and **`Glide Easing Stop`** (`cubic-bezier(0, 0.9125, 1, -0.0338)` — jumps in, all but stops, jumps out). **In and Out pass (the Glide presets):** one track, `0 %` → off-frame on the entry side (`xPercent`/`yPercent` ±100) eased `Smooth Glide`, last key at **`80 %`** → ∓150 (`Linear`), plus `propertyKeyframeFx.<track> = {"type":"glide","params":{"fast":0.1,"slow":0.6,"weight":25,"delay":0}}` — the glide tail lags in seconds, so ±100 → ∓100 at 100 % would still be on screen at the layer's end; these keys make it leave (−130…−145 % over 1.5–4 s layers) with the slow middle near the centre. Five expressive curves from the MotionFast template catalogue are also available: `Natural` (punchy ease-in-out), `Slow Down` (pure decelerate), `Overshoot` (shoots past the target, settles slowly), `Impulse` (anticipation dip backwards before launching), and `Swing` (wind-up, then a springy arrival) — use these to reproduce classic template motion on any layer. A **single-key track holds its value constant** for the whole clip. Transform origin is one item-level field, `keyframeTransformOrigin`.
20
+ Positions are always **0–100, percent of the clip's played duration** (not seconds). `value` is stored as a **string**. Easing values come from `GET /api/video-editor/capabilities` → `easings.keyframe.values` (e.g. `Linear`, `Smooth`, `Ease In`, `Ease Out`, `AE In`, `AE Smooth`, `AE Out`, `Power2 Smooth`, `Expo Out`, `Back Smooth`, `Bounce Out`, `Elastic`, …); an easing set on a key shapes the segment **after** that key. The `AE *` values are the After Effects-style bezier curves shared with MotionFast (`AE Smooth` is the classic slow-start/soft-stop AE feel — a good default for cinematic moves). Every easing still comes to a dead stop on its key. For more live energy — a move that lands soft and keeps drifting on, a shot that never sits still between keys — add the `glide` expression type on top of the track (`propertyKeyframeFx.<track> = {"type":"glide","params":{"fast":0.1,"slow":1,"weight":40,"delay":0}}`, see **Glide** under [Track expressions](video-keyframes.md#track-expressions-propertykeyframefx)). Two owner curves sit right after `Smooth`: **`Smooth Glide`** (`cubic-bezier(0.2111, 0.7721, 0.8541, 0.112)` — arrives fast, glides slowly through the middle, leaves fast) and **`Glide Easing Stop`** (`cubic-bezier(0, 0.9125, 1, -0.0338)` — jumps in, all but stops, jumps out). **In and Out pass (the Glide presets):** one track, `0 %` → off-frame on the entry side (`xPercent`/`yPercent` ±100) eased `Smooth Glide`, last key at **`80 %`** → ∓150 (`Linear`), plus `propertyKeyframeFx.<track> = {"type":"glide","params":{"fast":0.1,"slow":0.6,"weight":25,"delay":0}}` — the glide tail lags in seconds, so ±100 → ∓100 at 100 % would still be on screen at the layer's end; these keys make it leave (−130…−145 % over 1.5–4 s layers) with the slow middle near the centre. Five expressive curves from the MotionFast template catalogue are also available: `Natural` (punchy ease-in-out), `Slow Down` (pure decelerate), `Overshoot` (shoots past the target, settles slowly), `Impulse` (anticipation dip backwards before launching), and `Swing` (wind-up, then a springy arrival) — use these to reproduce classic template motion on any layer. A **single-key track holds its value constant** for the whole clip. Transform origin is one item-level field, `keyframeTransformOrigin` (`"center"`, `"top"`, `"bottom"`, `"left"`, `"right"`, a corner such as `"bottom left"`, or `"x% y%"`): the point every rotation (static and keyed, 2D and 3D) and the keyed scale turn about, while `panX`/`panY`/`panZ` stay the layer's resting centre — see [Turning a flat layer in 3D](video-3d.md#flat-layers-in-the-3d-scene--itemshadowplate).
21
21
 
22
22
  **The easing catalogue.** `GET /api/video-editor/capabilities` → `easings` carries both vocabularies with the actual curve behind every name, so a look can be matched or carried into another tool without guessing. `easings.keyframe` covers keyframe and transition easings: `values` (all of them), `transitionValues` (the subset a transition accepts — `Custom` and `Hold` need per-key data and are rejected there), and `curves[]`. `easings.motionTemplate` covers the camelCase names a template's ease field takes (`aeSmooth`, `power2`, …). In `curves[]`, `kind` says which field IS the curve — `bezier` → `cubicBezier` `[x1,y1,x2,y2]` plus a ready-to-paste `css`, `sampled` → `samples`, the curve at t = 0, 0.01 … 1 (values past 0..1 are real overshoot; as CSS that is `linear(<samples>)`). `gsap` names the ease that plays, and `keyframeEquivalent` / `motionTemplateEquivalent` link the same curve across the two vocabularies — only where the curves genuinely match, so treat a missing link as "these are different curves that happen to share a name" (template `smooth` is not keyframe `Smooth`; template `power2` is keyframe `Ease Out`). A `sampled` entry's `cubicBezier` (when marked `bezierIsApproximate`) is only the closest cubic — `bezierMaxError` says how far off, up to ~0.19 for `Circ Out`; match against the samples, not the handles. Two easings carry no curve: `Custom` reads the key's own `bezier: [x1,y1,x2,y2]` (write both together or it falls back to Smooth), and `Hold` steps to the next key.
23
23