greenlight-motion 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -12
- package/THIRD_PARTY_LICENSES.md +2 -1
- package/cli/index.mjs +83 -2
- package/dist/bundle-inputs.json +5 -0
- package/dist/greenlight-motion-script.mjs +16754 -0
- package/dist/greenlight-motion.js +895 -677
- package/dist/greenlight-motion.mjs +40001 -38558
- package/dist/script-inputs.json +34 -0
- package/package.json +2 -1
- package/schema/capabilities.json +1 -1
- package/skill/SKILL.md +26 -11
- package/skill/references/scene-script.md +111 -0
- package/skill/references/video-3d.md +75 -6
- package/skill/references/video-camera.md +1 -0
- package/skill/references/video-effects.md +6 -6
- package/skill/references/video-keyframes.md +1 -1
- package/skill/references/video-particles.md +5 -5
- package/skill/references/video-projects.md +39 -5
- package/skill/references/video-text-presets.md +47 -7
- package/skill/references/video-text.md +6 -4
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.
|
|
21
|
+
npx greenlight-motion@0.1.1 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.1 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.
|
|
44
|
+
1. `npx greenlight-motion@0.1.1 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.
|
|
49
|
+
3. `npx greenlight-motion@0.1.1 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.
|
|
51
|
+
4. `npx greenlight-motion@0.1.1 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/` — 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.
|
|
116
|
+
Machine-readable lists: `npx greenlight-motion@0.1.1 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.
|
|
163
|
+
`npx greenlight-motion@0.1.1 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.
|
|
179
|
+
- **They review on this computer:** run `npx greenlight-motion@0.1.1 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.
|
|
208
|
+
changes saves a `.glvideo`). `npx greenlight-motion@0.1.1 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.
|
|
212
|
+
`init` makes, with the options the author ticked. `npx greenlight-motion@0.1.1 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,7 +231,7 @@ 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
|
|
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.
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
- **One idea per scene**, 3–6 scenes, 2–3 s each; at most ~8 words on screen at once, held ≥ 1 s (people read ~3 words a second).
|
|
62
|
+
- **Hierarchy**: one headline per scene; supporting text much smaller; one accent colour; strong contrast.
|
|
63
|
+
- **Timing**: stagger related elements 0.06–0.12 s; one focal move at a time — supporting elements move less and later; exits faster than entrances.
|
|
64
|
+
- **Text animation** moves words or letters, never whole lines (the presets do this). Keep one family per video; use calm presets (glide-text,
|
|
65
|
+
classic-fade-up, classic-rise-mask) for long lines, punchy ones (swing-pop, whip, letter-vortex) for 1–3 words. glide-continuous and
|
|
66
|
+
glide-soft-stop never let the text rest: only when the user asks for drifting text.
|
|
67
|
+
- **Transitions**: vary two or three kinds across the video; the camera stays subtle.
|
|
68
|
+
- **Sound**: auto is a good base; add `data-1` / `selection-data-click` for UI-like pops, `cinematic-transition-13` for a big reveal.
|
|
69
|
+
- **9:16**: bigger text, stacked vertically, everything in the middle 80 %.
|
|
70
|
+
|
|
71
|
+
## Catalogues
|
|
72
|
+
|
|
73
|
+
**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
|
|
74
|
+
|
|
75
|
+
**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
|
|
76
|
+
|
|
77
|
+
**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
|
|
78
|
+
|
|
79
|
+
**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
|
|
80
|
+
|
|
81
|
+
**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`
|
|
82
|
+
|
|
83
|
+
**Layer effects** (`effects`): `mockupFrame`, ★ `runningGlow`, `depthMap`, `depth3d`, `depthScan`, `fresnelRim`, ★ `motionBlur`, ★ `motionTrail`, `autoFill`, ★ `repetile`, `regionDistort`, `bend`, `cornerPin`, `meshWarp`, `bezierWarp`, `rig`, ★ `droste`, `pryzm`, ★ `glass`, `chromaKey`, `layerMask`
|
|
84
|
+
|
|
85
|
+
Effect params: references/video-effects.md.
|
|
86
|
+
|
|
87
|
+
**Sounds** (`sound`, `sounds[].sfx`):
|
|
88
|
+
- Ambient Transition: ambient-transition-22 (5s)
|
|
89
|
+
- Bass Drop: cinematic-transition-13 (2.5s), sd-low-06 (8s), sd-low-hi-04 (8s)
|
|
90
|
+
- Bleep: digital-bleep-01 (0.286s)
|
|
91
|
+
- Bubble: bubble-07 (1.565s), bubble-08 (2.061s), bubble-10 (0.906s), bubble-23 (1.071s)
|
|
92
|
+
- Camera: camera-focus-and-shutter (1.022s), camera-focus-beep (0.283s), camera-power-switch (0.48s)
|
|
93
|
+
- Cartoon Sound: cartoon-sound-17 (4s)
|
|
94
|
+
- Click: keyboard-click-02 (0.294s), selection-data-click (0.68s)
|
|
95
|
+
- Crystal: crystal-swoosh (2.715s)
|
|
96
|
+
- Cyber Transition: cyber-transition-20 (1.353s)
|
|
97
|
+
- 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)
|
|
98
|
+
- 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)
|
|
99
|
+
- Foley & Ambience: flash (0.967s)
|
|
100
|
+
- Full Screen Transition: big-transition-whoosh (2.465s)
|
|
101
|
+
- Gears (single): small-gears-13 (1.5s), small-gears-16 (1.022s)
|
|
102
|
+
- Glitch: glitch-transition-06 (0.806s)
|
|
103
|
+
- 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)
|
|
104
|
+
- SFX Transition: cinematic-rewind-short (1.056s), sfx-message-03 (0.174s), simple-transition-08 (3.557s), simple-transition-13 (3s)
|
|
105
|
+
- Strobe: strobe-transition-22 (1s)
|
|
106
|
+
- Swoosh: soft-swoosh (1.848s), standard-swoosh (1.335s)
|
|
107
|
+
- Text: keyboard-writing-02 (2.904s), machine-writing-01 (3.924s), pen-writing-tick (0.429s)
|
|
108
|
+
- Tiles: tiles-07 (1.473s), tiles-08 (0.979s)
|
|
109
|
+
- 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)
|
|
110
|
+
- Whoosh / Additional: whoosh-05 (2.116s)
|
|
111
|
+
|
|
@@ -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 `
|
|
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,
|
|
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
|
|
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
|
-
**`
|
|
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
|
{
|
|
@@ -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
|
-
####
|
|
309
|
+
#### Mosaic
|
|
310
310
|
|
|
311
|
-
`pixelate` (**
|
|
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
|
-
|
|
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 (`
|
|
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 (
|
|
561
|
-
- 3D-only values (hidden on a flat entry; turning **Render in 3D** off resets them): Colorama `input` `5` Height / `6` Depth / `7` Facing;
|
|
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
|
|
|
@@ -93,7 +93,7 @@ Rules:
|
|
|
93
93
|
}
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
**`solver`** — the box the particles live in. Its transform *is* the layer's, so moving/rotating/scaling/parenting the item moves the whole volume; V1 simulates in local space (`space` is always `"local"`), so particles travel with it.
|
|
96
|
+
**`solver`** — the box the particles live in. Its transform *is* the layer's, so moving/rotating/scaling/parenting the item moves the whole volume; V1 simulates in local space (`space` is always `"local"`), so particles travel with it. The box is centred on the layer's position (`panX`/`panY`/`panZ` and the PRS keyframes) in the same world as every other layer and is filmed once by the project camera, so a solver given a Shape's exact transform fills that Shape's rect under any camera — pan, zoom, roll, tilt, dolly, keyed moves and a Blender picture-plane world thousands of px down a page alike (versions up to 15.0.21 drew an off-centre solver at twice its offset, often out of frame under a moving camera). A camera-locked channel keeps it in frame space. To snow onto a rect, size `bounds.size`/the emitter to the rect and put the layer at its centre.
|
|
97
97
|
|
|
98
98
|
| Field | Range | Meaning |
|
|
99
99
|
| --- | --- | --- |
|
|
@@ -116,7 +116,7 @@ Rules:
|
|
|
116
116
|
| `stir` / `detach` | `{strength 0–200, radius px}` / bool | v3: how hard emission stirs the fluid; `detach: true` makes emission deposit material only and hands the field's momentum to `forceEmitters`; `stir.strength: 0` then makes the emitter deposit nothing at all, which is how you get a layer whose only smoke comes from `kind: "smoke"` force emitters. |
|
|
117
117
|
| `position` / `size` | px vec3 | Placement and extent inside the box (`size.x` is the diameter for `sphere`/`cone`). |
|
|
118
118
|
| `rate` | 0–20000 /s | Continuous emission. |
|
|
119
|
-
| `bursts` | ≤8 × `{time, count}` | One-shot pops on top of the rate
|
|
119
|
+
| `bursts` | ≤8 × `{time, count}` | One-shot pops on top of the rate. This is how a shockwave / explosion / puff is built: a burst emits its whole count in one frame, `speed` throws the front outward (with `shape: "ring"` + `dirMode: "outward"` for a ring blast) and `forces.drag` is what makes it slam to a halt. Several entries = several pulses; ignored when `emit: "static"`. |
|
|
120
120
|
| `start` / `end` | s | Emission window inside the clip. |
|
|
121
121
|
| `lifetime` | `{value 0.05–600, random}` | Seconds, ± per particle. |
|
|
122
122
|
| `speed` | `{value, random}` | Launch speed in composition px/s. |
|
|
@@ -166,7 +166,7 @@ Distribution is part of the simulation, so changing it re-bakes (a `mode` of `ra
|
|
|
166
166
|
|
|
167
167
|
#### The fluid field (v3) — fluid and smoke physics, Particular-style
|
|
168
168
|
|
|
169
|
-
`forces.fluid` turns on a **simulated** force field: an incompressible velocity grid the size of the Solver Box that emission stirs and particles ride. Unlike turbulence (a formula in position and time) it has **memory** — a plume meets the eddies it left behind, smoke curls back on itself, ink mushrooms. It is stepped at 1/60 s with the particles and thrown away after the bake; only particle snapshots are cached. The fluid supplies **movement** — particles carried, swirled and lifted by a medium. It is not rendered itself: there is no volumetric or dye render mode, and particles are always drawn as particles. **Cost:** the grid steps 360× for a 6 s clip — `low` ≈ 4 ms/step, `medium` ≈ 10, `high` ≈ 30, `ultra`/`flat-hd` ≈ 80, `volume-hd` ≈ 250 — so a `flat-hd` preset bakes for ~30 s (in a worker, while the live GPU preview already shows the edit — see **Live preview** below
|
|
169
|
+
`forces.fluid` turns on a **simulated** force field: an incompressible velocity grid the size of the Solver Box that emission stirs and particles ride. Unlike turbulence (a formula in position and time) it has **memory** — a plume meets the eddies it left behind, smoke curls back on itself, ink mushrooms. It is stepped at 1/60 s with the particles and thrown away after the bake; only particle snapshots are cached. The fluid supplies **movement** — particles carried, swirled and lifted by a medium. It is not rendered itself: there is no volumetric or dye render mode, and particles are always drawn as particles. **Cost:** the grid steps 360× for a 6 s clip — `low` ≈ 4 ms/step, `medium` ≈ 10, `high` ≈ 30, `ultra`/`flat-hd` ≈ 80, `volume-hd` ≈ 250 — so a `flat-hd` preset bakes for ~30 s (in a worker, while the live GPU preview already shows the edit — see **Live preview** below).
|
|
170
170
|
|
|
171
171
|
**Live preview vs. the bake (10.4.37+).** In the desktop editor the whole solver — field and particles — also runs **live on WebGPU** (`frontend/src/sim/particles/gpuEngine.js`), so a change to any of these controls shows within a frame while the user edits; the bake above re-runs in the background after the edit and, once complete, is what the editor scrubs (any time is one read) and what **exports render**. Live is also the fallback-free path when the bake is not there yet; the bake is the fallback when live is not possible (Smoke Dye and Grow Points are bake-only) or when the live step cannot hold 60 fps on that machine. The bake itself is cancellable, debounced and streams in chunks, so even the fallback appears progressively. None of this changes the JSON: the bake is a pure function of the system and the live engine is only a preview of the same inputs. Route "smoke", "fluid", "ink in water", "cigarette smoke", "steam", "particles flowing like liquid" here. Ready-made presets: `smoke` (Rising Smoke), `cigarette`, `ink`, `ember`, `shockwave` (a ring blast every 1.5 s that punches the fluid). A `text-cage` preset id in older projects is just a label; the system it carries still bakes.
|
|
172
172
|
|
|
@@ -182,7 +182,7 @@ Distribution is part of the simulation, so changing it re-bakes (a `mode` of `ra
|
|
|
182
182
|
| `fluid.bounds` | `closed` \| `open` | Walls (a plume pools under the top) or outflow (it leaves the box). |
|
|
183
183
|
| `fluid.smoke` | `{buoyancy −200–600, weight 0–400, heat 0–100, cooling 0–4, dissipate 0–4}` | Smoke model only. Emission deposits hot dye; heat lifts, weight pulls. |
|
|
184
184
|
| `forces.draft` | `{strength 0–300, direction 0–360°, gust 0–1}` | A gusting wind on the field. Its variation is in time only, so it reads as weather, never as a pattern. |
|
|
185
|
-
| `forceEmitters[]` | ≤3 × `{id, kind jet\|vortex\|smoke\|burst, mode vortex\|orbit\|twist\|attract\|repel, lift, pulseStart, pulseEvery, position, radius, strength 0–300, push px/s, direction °, spin ccw\|cw, density 0–8, heat 0–100, motion hold\|orbit\|sweep, motionSpeed, motionRadius}` | Forces placed in the box, with no particles of their own. `kind: "jet"` is a **Blower** in the UI — a fan: everything inside `radius` is blended toward `push` px/s along `direction` (0° = right, 90° = up) at a rate set by `strength`; `kind: "vortex"` is a whirlpool (`spin`). With `forces.fluid` on they drive the field and the particles ride it (the emitter's own stir keeps working unless `emitters[0].detach`); with the field off blowers and vortices act on the particles directly. `kind: "smoke"` is a **source**, not a push: it injects material (`density` per second, at `heat`) into the field at its point, so a layer can have several plumes coming from places you choose rather than one from the emitter — it needs `forces.fluid.model: "smoke"`, and `strength: 0` makes it pure material lifted by buoyancy. `kind: "burst"` is a **pulse force**: a radial blast from its point at `pulseStart`, repeating every `pulseEvery` seconds (0 = once), throwing what is inside `radius` outward at `push` px/s — a shockwave with no particles of its own. A `vortex`-kind emitter takes any of
|
|
185
|
+
| `forceEmitters[]` | ≤3 × `{id, kind jet\|vortex\|smoke\|burst, mode vortex\|orbit\|twist\|attract\|repel, lift, pulseStart, pulseEvery, position, radius, strength 0–300, push px/s, direction °, spin ccw\|cw, density 0–8, heat 0–100, motion hold\|orbit\|sweep, motionSpeed, motionRadius}` | Forces placed in the box, with no particles of their own. `kind: "jet"` is a **Blower** in the UI — a fan: everything inside `radius` is blended toward `push` px/s along `direction` (0° = right, 90° = up) at a rate set by `strength`; `kind: "vortex"` is a whirlpool (`spin`). With `forces.fluid` on they drive the field and the particles ride it (the emitter's own stir keeps working unless `emitters[0].detach`); with the field off blowers and vortices act on the particles directly. `kind: "smoke"` is a **source**, not a push: it injects material (`density` per second, at `heat`) into the field at its point, so a layer can have several plumes coming from places you choose rather than one from the emitter — it needs `forces.fluid.model: "smoke"`, and `strength: 0` makes it pure material lifted by buoyancy. `kind: "burst"` is a **pulse force**: a radial blast from its point at `pulseStart`, repeating every `pulseEvery` seconds (0 = once), throwing what is inside `radius` outward at `push` px/s — a shockwave with no particles of its own. A `vortex`-kind emitter takes any of the five `forces.vortex` `mode`s, centred on itself. |
|
|
186
186
|
| `pathFollow` | `{enabled, attract 0–600, flow 0–600, proximity px, swirl 0–400, anchors[]}` | Particular's "move along path": attract toward the spline, flow along its tangent, braid around it, falling off past `proximity`. `anchors` are in the box's unit space; empty = the emitter's own `path`. |
|
|
187
187
|
| `collision` | `{source, faces outer\|inner, bounce 0–1, depth px, size px, position {x,y}}` | A layer's artwork (same `source` snapshot as a particle object) as a signed-distance collider extruded `depth` px. `outer` = obstacle (particles bounce, the flow routes around it); `inner` = container (emission is clipped to it, particles stay inside). A text source gives "particles inside letters". **`follow`** (default false, needs `fromItemId`) makes the collider track that layer's live ARTWORK and its position, scale and rotation every step — keyframes and parenting included — and turns `size`/`position`/`rotation` into the FALLBACK for a source that has been deleted. It also follows the layer's place on the TIMELINE: outside that clip's span, or while its channel is hidden, there is no collider at all (particles pass through and a container stops holding them). Without it the collider stands wherever `size`/`position` put it whatever the layer does, and those are filled in only at pick time. Two limits while following: a moving collider re-simulates every time its pose changes, and it steers the PARTICLES only — the fluid field's obstacle mask is a fixed grid over the Solver Box, so the flow passes straight through. **`releaseOutside`** (default false, container only) stops the container gathering particles that are already outside it — turn it on when the collider arrives part-way through a shot, or it inhales the cloud around it. **`open`** `{side none\|top\|bottom\|left\|right, spread 1–180°, at s, fade s}` (container only) lets particles OUT through one side at a chosen time — `at` is when it starts, `fade` how long it takes to open, `spread` how much of that wall becomes a doorway. It gates on the contact NORMAL, so `bottom` opens the floor of every stroke of a letter rather than cutting the shape in half, and what leaves stays out; sides are the FRAME's, so `bottom` is the bottom of the screen even under a rotating source. `side: 'none'` contributes nothing to the sim hash. Authoring note: the fixed collider's SDF is a 384x216 grid scaled from the box WIDTH, so keep `solver.bounds.size` near 16:9 or the field is distorted along the short axis. |
|
|
188
188
|
| `colliders[]` | up to **3** more, each the SAME shape as `collision` | The colliders past the first. `collision` stays the canonical FIRST one, so a project with a single collider is unchanged; four total. A particle stays OUTSIDE every obstacle and INSIDE every container, and emission is clipped to the INTERSECTION of the containers. Resolution is sequential rather than a joint solve, so two containers that do not overlap are unsatisfiable — visible on screen rather than averaged away. Only the first FIXED collider shapes the fluid field (one flow grid), and a system with more than one always previews from the bake. Because a `follow: true` collider stops existing outside its layer's span, two colliders on two spans give you DIFFERENT COLLISIONS OVER TIME with no keyframes. |
|
|
@@ -246,7 +246,7 @@ The other fluid presets differ from these only in numbers: *Cigarette* is Rising
|
|
|
246
246
|
| `colorOverLife` | ≤16 stops `{at 0–1, color}` | Colours are stored `#rrggbbaa` (`rgba()` is accepted and normalised). **Alpha always comes from this ramp**, whatever `colorMode` says — end stops must be transparent or particles pop in/out. |
|
|
247
247
|
| `colorMode` | `age` \| `source` \| `velocity` | Where RGB comes from. `source` needs a `layer` emitter. (v1's `colorFrom: 'ramp'\|'source'` migrates to `age`/`source`.) |
|
|
248
248
|
| `velocityRamp` / `velocityRange` | ramp / `{min, max}` px/s | The `velocity` gradient and the speeds that read its ends (`max` is forced above `min`). |
|
|
249
|
-
| `twinkle` / `stretch` / `opacity` | 0–1 / 0–4 / 0–1 | Per-particle flicker (a ~1 Hz opacity pulse);
|
|
249
|
+
| `twinkle` / `stretch` / `opacity` | 0–1 / 0–4 / 0–1 | Per-particle flicker (a ~1 Hz opacity pulse); how far a particle is smeared along its own velocity, 0 a dot and high values a streak; layer opacity. |
|
|
250
250
|
| `onKill` | `{fade 0–10 s, scale 0–1}` | How a particle **leaves**. Only possible because the clip is baked in advance: a particle knows a wall is coming and starts fading/shrinking before it goes. `scale` needs a non-zero `fade`. |
|
|
251
251
|
|
|
252
252
|
#### Emitting from a layer (`shape: "layer"`) — dissolve a picture into its own pixels
|