@shotstack/cli 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/shotstack.js +50 -50
- package/package.json +1 -1
- package/skills/shotstack/SKILL.md +12 -8
- package/skills/shotstack/references/asset-library.md +2 -0
- package/skills/shotstack/references/beat-sync.md +78 -0
- package/skills/shotstack/references/generation.md +3 -1
- package/skills/shotstack/references/html5-effects.md +109 -0
- package/skills/shotstack/references/html5-snippets.md +20 -19
- package/skills/shotstack/references/html5.md +53 -12
- package/skills/shotstack/references/motion.md +60 -96
- package/skills/shotstack/references/onboarding.md +2 -2
- package/skills/shotstack/shared/agent-core.md +75 -27
package/package.json
CHANGED
|
@@ -15,16 +15,16 @@ This skill loads in **terminal-based AI agents** (Claude Code, Cursor, Codex CLI
|
|
|
15
15
|
|
|
16
16
|
## Check versions once per session
|
|
17
17
|
|
|
18
|
-
This skill is version **0.
|
|
18
|
+
This skill is version **0.11.0** and needs `@shotstack/cli` **0.11.0** or later. Before the first `shotstack` command in a session:
|
|
19
19
|
|
|
20
|
-
1. Run `shotstack --version`. If it is older than 0.
|
|
20
|
+
1. Run `shotstack --version`. If it is older than 0.11.0, tell the user: what this skill documents, such as `analyze`, is missing until they run `npm install -g @shotstack/cli@latest`.
|
|
21
21
|
2. Run `npm view @shotstack/cli version`. If it is newer than this skill's version, tell the user an update is available and ask whether to install it. Only on a yes, run `npx skills update shotstack -y` and `npm install -g @shotstack/cli@latest`. Never update without asking. `npx skills check` is not a read-only check: it installs the update.
|
|
22
22
|
|
|
23
23
|
If a check fails (no network, no npm, a sandbox), skip it and carry on.
|
|
24
24
|
|
|
25
25
|
**Open-ended first render? Ask before composing.** If the ask doesn't state what the video is (subject, format or destination) — e.g. "render my first video", "make me a video", "set up Shotstack" — read [`references/onboarding.md`](references/onboarding.md) and ask its discovery questions, then WAIT for answers. Do not write an Edit first. An open-ended ask is not a specification, and a stray JSON file in the project doesn't make the user experienced. Compose without asking only when the user has stated what the video is, or explicitly says to just show anything.
|
|
26
26
|
|
|
27
|
-
Commands for the Shotstack video rendering API. `render` submits an Edit JSON and returns a render ID; `status` polls a render until done. `ingest` uploads your own local files (or fetches remote URLs) and hosts them so they can be referenced from an Edit. `validate` lints an Edit — run it before every render. `studio` opens an edit in the browser editor for a human to preview, tweak, and render. `template list|get|create|update|delete` reads and writes templates saved to the account: `list` shows saved templates (id + name); `get <id>` fetches a saved template's Edit JSON (e.g. one the user designed in Studio) so you can render or remix it; `create <file> --name <name>` saves an edit as a new template; `update <id> <file>` overwrites one; `delete <id>` removes one. `models` lists the generation models for prompt-driven assets and whether this key can use each one; `models <id>` prints the options a model accepts. `generate <model> "<prompt>"` generates one asset on its own and prints its URL.
|
|
27
|
+
Commands for the Shotstack video rendering API. `render` submits an Edit JSON and returns a render ID; `status` polls a render until done. `ingest` uploads your own local files (or fetches remote URLs) and hosts them so they can be referenced from an Edit. `validate` lints an Edit — run it before every render. `studio` opens an edit in the browser editor for a human to preview, tweak, and render. `template list|get|create|update|delete` reads and writes templates saved to the account: `list` shows saved templates (id + name); `get <id>` fetches a saved template's Edit JSON (e.g. one the user designed in Studio) so you can render or remix it; `create <file> --name <name>` saves an edit as a new template; `update <id> <file>` overwrites one; `delete <id>` removes one. `models` lists the generation models for prompt-driven assets and whether this key can use each one; `models <id>` prints the options a model accepts. `generate <model> "<prompt>"` generates one asset on its own and prints its URL. `analyze <url>` measures a music track's tempo, beats, bars, drops and sections, free and without a key.
|
|
28
28
|
|
|
29
29
|
**Default loop — no API key required:** compose → `validate` (offline lint) → `studio` (browser preview). Only the API-backed commands (`render`, `status`, `ingest`, `models`, `generate`) need a key. Reach for `render` when you specifically need a final MP4 exported in the cloud; reach for `studio` for everything else — it is the cheapest way to see an edit and the right default for any creative a human will review.
|
|
30
30
|
|
|
@@ -117,6 +117,8 @@ shotstack generate seedance-2.0-text-to-video "Waves break over black sand at du
|
|
|
117
117
|
|
|
118
118
|
**Generation costs credits on stage as well as v1.** Before generating, run the same command with `--quote`, tell the user the model and the credits it prints (it says "up to" when the final charge can be lower), and wait for a yes. Identical requests share one job and its cached result: repeating a request while it runs, or after it finishes, returns the same job without charging again. That is also how to pick up a job started without `--watch`. Sending a failed generation again retries it, and that is charged.
|
|
119
119
|
|
|
120
|
+
**Cutting or animating to music?** Measure the track with `shotstack analyze <url> --output json` and follow [`references/beat-sync.md`](references/beat-sync.md): write every cut and keyframe in bars from the map, never from the BPM you asked for.
|
|
121
|
+
|
|
120
122
|
**Read [`references/generation.md`](references/generation.md) before writing a prompt.** It covers what each model responds to, when to put text in a `rich-text` layer instead of the image, and how to size and chain generations inside an edit.
|
|
121
123
|
|
|
122
124
|
## Default workflow: preview in Studio (no key, no credits)
|
|
@@ -148,7 +150,7 @@ If the share API is unreachable, the command falls back to the inline base64url
|
|
|
148
150
|
|
|
149
151
|
## REQUIRED: Read `shared/agent-core.md` before composing any Edit JSON
|
|
150
152
|
|
|
151
|
-
|
|
153
|
+
Edit JSON does **not** follow CSS or web conventions, least of all the `rich-text` asset's styling fields (an `html5` asset's `css` is ordinary CSS). Composing Edit JSON from training-data instinct will produce silently invalid renders. **Read [`shared/agent-core.md`](shared/agent-core.md) first**, every time. The most-failed conversions:
|
|
152
154
|
|
|
153
155
|
| You'd write (wrong) | API requires (right) |
|
|
154
156
|
|---|---|
|
|
@@ -160,9 +162,9 @@ The Shotstack schema does **not** match CSS or web conventions. Composing Edit J
|
|
|
160
162
|
| `fit: "cover"` (CSS instinct) | `fit: "crop"` |
|
|
161
163
|
| `tracks[0]` is the bottom (z-index instinct) | `tracks[0]` is the **TOP** layer |
|
|
162
164
|
|
|
163
|
-
The full ruleset (asset types, fonts, smart-string clip values, top-5 mistakes) lives in `shared/agent-core.md`.
|
|
165
|
+
The full ruleset (asset types, fonts, smart-string clip values, top-5 mistakes) lives in `shared/agent-core.md`.
|
|
164
166
|
|
|
165
|
-
**
|
|
167
|
+
**Motion is yours to design.** Built-in motion (transitions, effects, keyframes, rich-text animation) takes fixed options from the schema; `html5` takes any seekable animation. [`references/motion.md`](references/motion.md) covers what each layer can animate (transitions, effects, keyframes, rich-text animation, `html5`), the easing available and recipes; the *Motion* section of `agent-core.md` summarises it.
|
|
166
168
|
|
|
167
169
|
## Exit codes
|
|
168
170
|
|
|
@@ -193,11 +195,13 @@ This skill ships sub-references for the gnarly bits:
|
|
|
193
195
|
- [`references/positioning.md`](references/positioning.md) — coordinate model, `position`/`offset` (fraction of frame, +y up), clip bounding box & `fit`, text sizing, transform order
|
|
194
196
|
- [`references/caption.md`](references/caption.md) — sizing per resolution, default style, the 5 named presets, alias pattern (asset type: `rich-caption`)
|
|
195
197
|
- [`references/svg.md`](references/svg.md) — required attrs, supported elements
|
|
196
|
-
- [`references/motion.md`](references/motion.md) —
|
|
197
|
-
- [`references/html5.md`](references/html5.md) — HTML5 asset: fields, preloaded libs (
|
|
198
|
+
- [`references/motion.md`](references/motion.md) — what moves on each layer (transitions, effects, keyframes, rich-text animation, `html5`), the easing available, GSAP/CSS recipes, and the brand kit through merge fields
|
|
199
|
+
- [`references/html5.md`](references/html5.md) — HTML5 asset: fields, preloaded libs (GSAP and its plugins, d3, anime, lottie), browser harness and seek rules, 3D, sizing, worked examples
|
|
200
|
+
- [`references/html5-effects.md`](references/html5-effects.md) — **the creative palette**: every effect an `html5` clip can do, by common motion-design name (per-character text, stroke write-ons, mattes, blend modes, noise displacement, speed ramps, 2.5D camera, particles), with one-line recipes and what to avoid. Read before designing an `html5` clip.
|
|
198
201
|
- [`references/html5-snippets.md`](references/html5-snippets.md) — copy-paste motion-graphic clips: kinetic headline, value reveal, shine sweep, pulsing CTA, film grain
|
|
199
202
|
- [`references/fonts.md`](references/fonts.md) — built-in fonts, Google Fonts URL pattern, custom-font workflow
|
|
200
203
|
- [`references/generation.md`](references/generation.md) — per-model prompting craft for AI-generated video, images, speech and music, and what a timeline adds to a prompt. Read before writing any `prompt`.
|
|
204
|
+
- [`references/beat-sync.md`](references/beat-sync.md) — cutting and animating to music: get the track first, analyse it, write every cue in bars, drive `html5` from the map, check frames at bar lines
|
|
201
205
|
- [`references/asset-library.md`](references/asset-library.md) — placeholder videos, images, music
|
|
202
206
|
- [`references/troubleshooting.md`](references/troubleshooting.md) — common errors and fixes
|
|
203
207
|
|
|
@@ -32,6 +32,8 @@ https://shotstack-assets.s3.amazonaws.com/images/waterfall.jpeg
|
|
|
32
32
|
|
|
33
33
|
Royalty-free tracks from the Unminus library, hosted on Shotstack's CDN. Use as `audio` assets on a dedicated track. (`timeline.soundtrack` is deprecated — use `audio` with `length: "end"` instead.)
|
|
34
34
|
|
|
35
|
+
These are the fallback, not the default: the same few tracks sit under every video that uses them. When the user has no track, offer to generate one first (see *Music* in [`shared/agent-core.md`](../shared/agent-core.md)).
|
|
36
|
+
|
|
35
37
|
```
|
|
36
38
|
https://shotstack-assets.s3.amazonaws.com/music/unminus/white.mp3
|
|
37
39
|
https://shotstack-assets.s3.amazonaws.com/music/unminus/waveform.mp3
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Beat sync
|
|
2
|
+
|
|
3
|
+
Cut and animate to a track by measuring it, not by trusting the tempo you asked for. A generated track keeps a stated BPM but puts its drops and sections where it likes, and a tempo found by ear or by a general-purpose library is often half a beat off the kick. A cut that lands a quarter-beat late reads as a mistake.
|
|
4
|
+
|
|
5
|
+
## 1. Get the track before you compose
|
|
6
|
+
|
|
7
|
+
Analysis needs a file. A `prompt` on the timeline's `audio` asset only generates at render time, so there is nothing to measure while you write the edit.
|
|
8
|
+
|
|
9
|
+
- **The user's track:** use its hosted URL.
|
|
10
|
+
- **A library track:** use its URL as is.
|
|
11
|
+
- **A generated track:** generate it on its own first, then use the URL it returns as a plain `audio` `src`. Still state the BPM in the prompt; measure the result anyway.
|
|
12
|
+
|
|
13
|
+
<!-- surface:cli -->
|
|
14
|
+
Host a local file with `shotstack ingest upload <file> --watch`. Generate a track with `shotstack generate <music-model> "<prompt>" --length <seconds> --watch`, after quoting it with `--quote`.
|
|
15
|
+
<!-- /surface -->
|
|
16
|
+
|
|
17
|
+
## 2. Analyse it
|
|
18
|
+
|
|
19
|
+
Analysis is free, takes 15–30 s and reads the first 10 minutes of the track.
|
|
20
|
+
|
|
21
|
+
It needs no API key. The `src` must be a direct, public link to an audio or video file: share pages and redirects are refused.
|
|
22
|
+
<!-- surface:cli -->
|
|
23
|
+
```sh
|
|
24
|
+
shotstack analyze https://shotstack-assets.s3.amazonaws.com/music/unminus/lit.mp3 --output json > map.json
|
|
25
|
+
```
|
|
26
|
+
<!-- /surface -->
|
|
27
|
+
|
|
28
|
+
## 3. Read the map
|
|
29
|
+
|
|
30
|
+
| Field | Meaning |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `steady` | `true`: the tempo holds, and `bpm`, `beat`, `bar`, `meter` and `origin` describe a grid. `false`: use `beats[]` and `downbeats[]` instead. |
|
|
33
|
+
| `bpm`, `beat`, `bar` | Tempo, and the beat and bar lengths in seconds. |
|
|
34
|
+
| `meter` | Beats per bar. |
|
|
35
|
+
| `origin` | Time of the first downbeat. Bar `n` starts at `origin + n × bar`. |
|
|
36
|
+
| `bars.count`, `bars.energy[]` | Number of bars, and loudness per bar from 0 to 1. |
|
|
37
|
+
| `drops[]` | `{ bar, time }` where the low end comes in hard. |
|
|
38
|
+
| `sections[]` | `{ bar, time, change }`, where `change` is `rise` or `fall`. |
|
|
39
|
+
| `downbeat.confidence` | How sure the bar phase is, 0 to 1. |
|
|
40
|
+
| `precision` | How far to trust each part: `grid` is `frame` or `beat`, `sections` is `bar`, `downbeat` is `estimate`. |
|
|
41
|
+
| `beats[]`, `downbeats[]` | Beat and bar times, only when `steady` is `false`. |
|
|
42
|
+
|
|
43
|
+
## 4. Write every cue in bars
|
|
44
|
+
|
|
45
|
+
- **Cuts on bar lines.** A shot holds a whole number of bars. Compute each `start` and `length` from the map and keep three decimals; never round to whole seconds.
|
|
46
|
+
- **Let energy set the pace.** Hold two or four bars where `bars.energy` is low and cut every bar where it is high.
|
|
47
|
+
- **Spend the big moment on a drop.** Put the reveal, the logo or the hardest cut at a `drops[].time`.
|
|
48
|
+
- **Change scene at sections.** A `rise` opens a new idea; a `fall` is where to breathe.
|
|
49
|
+
- **Accents on beats.** A pulse, flash or kick-on-the-beat sits at `origin + k × beat`.
|
|
50
|
+
- **No grid?** When `steady` is `false`, cut on `downbeats[]` and accent on `beats[]`.
|
|
51
|
+
- **Downbeat off by one?** The bar phase is an estimate. If `downbeat.confidence` is low, look at the first drop: if it lands mid-bar, shift `origin` by a beat.
|
|
52
|
+
|
|
53
|
+
## 5. Drive `html5` motion from the map
|
|
54
|
+
|
|
55
|
+
An `html5` clip's time is local to the clip. Put the map's numbers and the clip's own `start` in the page and place every tween from them, on a timeline the harness seeks (not `paused`):
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const ORIGIN = 0.062, BAR = 1.9998, BEAT = BAR / 4, CLIP_START = 8.062; // from the map and this clip
|
|
59
|
+
const at = (bar, beat = 0) => ORIGIN + bar * BAR + beat * BEAT - CLIP_START;
|
|
60
|
+
const tl = gsap.timeline();
|
|
61
|
+
tl.from(".title", { yPercent: 110, duration: BEAT, ease: "power3.out" }, at(4))
|
|
62
|
+
.to(".title", { scale: 1.06, duration: BEAT / 2, yoyo: true, repeat: 1 }, at(5))
|
|
63
|
+
.to(".title", { opacity: 0, duration: BEAT }, at(7, 3));
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Built-in motion works the same way: a clip whose `start` is a bar line makes its `transition` land on the beat.
|
|
67
|
+
|
|
68
|
+
## 6. Keep the music a plain audio clip
|
|
69
|
+
|
|
70
|
+
Put the track on its own track as an `audio` asset at `start: 0` with `length: "end"`; a fade (`effect: "fadeOut"`) is fine. Anything that moves the audio moves the grid with it. If you trim the track or start it later, add that offset to every time: timeline time = map time − `trim` + the audio clip's `start`.
|
|
71
|
+
|
|
72
|
+
## 7. Check frames at bar lines
|
|
73
|
+
|
|
74
|
+
Before calling it done, look at a few cuts and the first drop at their bar times: the new shot should be on screen at the bar line, not a frame or two after it.
|
|
75
|
+
|
|
76
|
+
<!-- surface:cli -->
|
|
77
|
+
Preview with `shotstack studio`, or render on stage and pull a frame at a bar time with `ffmpeg -ss <time> -i out.mp4 -frames:v 1 bar.png`.
|
|
78
|
+
<!-- /surface -->
|
|
@@ -64,7 +64,9 @@ Speech length is not knowable in advance. Set the clip's `length` to `"auto"` an
|
|
|
64
64
|
|
|
65
65
|
**Eleven Music.** A prompt answers genre, mood, instrumentation, tempo and production era whether or not you intend it to. Whatever you leave out, the model chooses.
|
|
66
66
|
|
|
67
|
-
**Production vocabulary does real work** — "dusty", "vinyl crackle", "sparse", "ethereal".
|
|
67
|
+
**Production vocabulary does real work** — "dusty", "vinyl crackle", "sparse", "ethereal". Naming a key helps, less reliably.
|
|
68
|
+
|
|
69
|
+
**State a BPM, then measure.** The model holds a stated tempo closely, but it places drops and sections where it likes, whatever the prompt says. Before timing cuts or motion to a generated track, analyse it: see [`beat-sync.md`](beat-sync.md).
|
|
68
70
|
|
|
69
71
|
**Naming a musician or band is refused**, as is quoting copyrighted lyrics. The error includes a suggested alternative prompt. Describe the era and production style instead: "1990s trip-hop with detuned Rhodes and a heavy swung break".
|
|
70
72
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# HTML5 effects palette
|
|
2
|
+
|
|
3
|
+
What you can build inside one `html5` clip, listed by the effect you'd reach for in a motion-graphics or editing tool. Every recipe renders the same in Studio's preview and the cloud render, on the CPU renderer, unless its note says otherwise. Timing and easing come from [`motion.md`](motion.md); the rules every recipe obeys are in [`html5.md`](html5.md). Snippets assume a GSAP timeline `tl = gsap.timeline()`.
|
|
4
|
+
|
|
5
|
+
**Match** means preview and render agree. **Caveat** names what differs. **Not available** gives the alternative.
|
|
6
|
+
|
|
7
|
+
## Text
|
|
8
|
+
|
|
9
|
+
| Effect | Recipe | Status |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Per-character animation | `const s = SplitText.create("#t", { type: "chars" }); tl.from(s.chars, { y: 40, opacity: 0, rotation: -15, stagger: 0.04 })` | Match |
|
|
12
|
+
| Per word / per line | `type: "words"` or `type: "lines"`. For lines, embed the font and build the tween in `onSplit` with `autoSplit: true`, so lines are measured with the real font | Match |
|
|
13
|
+
| Tracking in / out | `tl.fromTo("#t", { letterSpacing: "0.6em" }, { letterSpacing: "0.12em", duration: 2 })` | Match |
|
|
14
|
+
| Typewriter | `tl.to("#t", { text: "Final copy", duration: 1.5, ease: "none" })` | Match |
|
|
15
|
+
| Text decoder | `tl.to("#t", { scrambleText: { text: "DECODED", chars: "01" }, duration: 1.2 })` | Caveat: random glyphs; only the final text matches |
|
|
16
|
+
| Text on a path | SVG `<text><textPath href="#curve" id="tp">…</textPath></text>`, then `tl.to("#tp", { attr: { startOffset: "30%" } })` | Match |
|
|
17
|
+
| Stroke, gradient fill, glow | `-webkit-text-stroke: 2px #fff` · `background: linear-gradient(…); -webkit-background-clip: text; color: transparent` · `text-shadow: 0 0 12px #0ff` | Match, glow edges soften slightly in preview |
|
|
18
|
+
|
|
19
|
+
## Shapes and paths
|
|
20
|
+
|
|
21
|
+
| Effect | Recipe | Status |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| Stroke write-on | `tl.fromTo("#path", { drawSVG: "0%" }, { drawSVG: "100%" })` | Match |
|
|
24
|
+
| Travelling stroke segment | `tl.fromTo("#path", { drawSVG: "0% 10%" }, { drawSVG: "90% 100%" })` | Match |
|
|
25
|
+
| Shape morph | `tl.to("#shapeA", { morphSVG: "#shapeB" })` | Match |
|
|
26
|
+
| Motion path, auto-orient | `tl.to("#dot", { motionPath: { path: "#track", align: "#track", autoRotate: true, alignOrigin: [0.5, 0.5] } })`, or CSS `offset-path: path("M…")` with `tl.to(el, { offsetDistance: "100%" })` | Match |
|
|
27
|
+
| Repeated copies of a shape | Generate N copies in JS, then `tl.from(copies, { rotation: (i) => i * 30, opacity: 0, stagger: 0.05 })` | Match |
|
|
28
|
+
| Boolean path operations, path offset | Not available. Draw the merged outline as one SVG path | — |
|
|
29
|
+
|
|
30
|
+
## Masks and mattes
|
|
31
|
+
|
|
32
|
+
| Effect | Recipe | Status |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| Animated mask (wipe, reveal) | `tl.fromTo(el, { clipPath: "inset(0% 100% 0% 0%)" }, { clipPath: "inset(0% 0% 0% 0%)" })` or a `polygon(…)` with the same point count | Match |
|
|
35
|
+
| Mask feather | `mask-image: linear-gradient(90deg, #000 60%, transparent)` | Match |
|
|
36
|
+
| Alpha matte (fill through text) | SVG `<mask id="m"><text fill="#fff">MATTE</text></mask>` on a gradient or image; animate what's behind | Match |
|
|
37
|
+
| Luma matte | `<mask style="mask-type: luminance">` containing a gradient | Match |
|
|
38
|
+
| Inverted matte | In the SVG mask, a white rect with the shape drawn in black | Match |
|
|
39
|
+
| HTML content as a matte | Not available. The matte must be SVG content or an image | — |
|
|
40
|
+
|
|
41
|
+
## Blend modes and adjustment layers
|
|
42
|
+
|
|
43
|
+
| Effect | Recipe | Status |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| Multiply, screen, overlay, difference, colour dodge, add | `mix-blend-mode: multiply` (also `plus-lighter` for add) | Match |
|
|
46
|
+
| Linear burn, vivid or pin light, hard mix, subtract, divide, dissolve | Not available. Nearest: `color-burn`, `hard-light`, `difference` | — |
|
|
47
|
+
| Adjustment layer | An element over the layers it affects with `backdrop-filter: blur(10px) saturate(1.4)` | Match. Caveat: costly over large areas |
|
|
48
|
+
| Blending isolated to a group | `isolation: isolate` on the group | Match |
|
|
49
|
+
|
|
50
|
+
## Effects
|
|
51
|
+
|
|
52
|
+
| Effect | Recipe | Status |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Gaussian blur | `filter: blur(8px)`; animate with `tl.to(el, { filter: "blur(0px)" })` | Match |
|
|
55
|
+
| Directional blur | SVG `<filter><feGaussianBlur stdDeviation="14 0"/></filter>` (x only) | Match |
|
|
56
|
+
| Glow | `filter: drop-shadow(0 0 12px #22d3ee) drop-shadow(0 0 28px #a855f7)` | Caveat: halo slightly softer in preview |
|
|
57
|
+
| Drop shadow | `filter: drop-shadow(0 12px 24px rgba(0,0,0,.5))` | Match |
|
|
58
|
+
| Noise displacement, wave warp | `<feTurbulence id="t" type="fractalNoise" baseFrequency="0.02" numOctaves="3"/>` → `<feDisplacementMap id="d" in="SourceGraphic" scale="30"/>`; animate `tl.to("#t", { attr: { baseFrequency: 0.05 } })` and `#d` `scale` | Match |
|
|
59
|
+
| Fractal noise | `<feTurbulence type="turbulence">` then `<feColorMatrix>` to tint | Match. Caveat: about 75 ms per full 1080p frame |
|
|
60
|
+
| Curves, levels, hue/saturation, tint | `<feComponentTransfer>` (`table`, `gamma`), `<feColorMatrix type="hueRotate">` (animate `attr: { values }`), or CSS `saturate() contrast() hue-rotate()` | Match |
|
|
61
|
+
| Bevel, light sweep | `<feSpecularLighting>` with `<fePointLight id="p">`; animate `tl.to("#p", { attr: { x: 1920 } })` | Match |
|
|
62
|
+
| Lens glow, anamorphic streak | Radial or linear gradients with `mix-blend-mode: screen` | Match |
|
|
63
|
+
| Vignette | Overlay `radial-gradient(ellipse, transparent 55%, rgba(0,0,0,.6))` | Match |
|
|
64
|
+
| Film grain | Snippet 6 in [`html5-snippets.md`](html5-snippets.md) | Match |
|
|
65
|
+
| Motion blur | Not available. Fake it on fast moves with a short directional blur | — |
|
|
66
|
+
|
|
67
|
+
## Motion and time
|
|
68
|
+
|
|
69
|
+
| Effect | Recipe | Status |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| Custom easing curve | `CustomEase.create("hop", "M0,0 C0.3,1 0.7,1 1,0")`, then `ease: "hop"` | Match |
|
|
72
|
+
| Bounce with squash | `CustomBounce.create("bnc", { strength: 0.6, squash: 2, squashID: "bnc-sq" })`; position uses `"bnc"`, scale uses `"bnc-sq"` | Match |
|
|
73
|
+
| Wiggle, random shake | `CustomWiggle.create("wig", { wiggles: 7, type: "easeOut" })`, then `ease: "wig"`. Never `type: "random"` | Match |
|
|
74
|
+
| Loop, ping-pong loop | `repeat: -1`, plus `yoyo: true` for ping-pong | Match |
|
|
75
|
+
| Nested scenes | Each scene is its own timeline on one master: `gsap.timeline().add(sceneA, 0).add(sceneB, 2.5)` | Match |
|
|
76
|
+
| Speed ramp, time remapping | `const scene = gsap.timeline({ paused: true }).to(…); gsap.globalTimeline.remove(scene); tl.to(scene, { progress: 1, duration: 3, ease: "power3.inOut" })`. The `remove` line is required | Match |
|
|
77
|
+
| Throw, fall, bounce | `tl.to(el, { physics2D: { velocity: 600, angle: -60, gravity: 900 }, duration: 2 })` | Match |
|
|
78
|
+
| Momentum glide to a stop | `tl.to(el, { inertia: { x: { velocity: 600, end: 300 } } })` | Match |
|
|
79
|
+
| Layout morph | `const st = Flip.getState(".item")`; change classes or order; `tl.add(Flip.from(st, { duration: 1 }))` | Match |
|
|
80
|
+
| Seeded randomness | `const rand = d3.randomLcg(42)`; never `Math.random()` | Match |
|
|
81
|
+
|
|
82
|
+
## 3D and camera
|
|
83
|
+
|
|
84
|
+
| Effect | Recipe | Status |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| 3D layers and camera move (2.5D) | `perspective: 1600px` on a stage, `transform-style: preserve-3d` on a world, each layer at `translateZ(z) scale((p − z) / p)`; move the world with `tl.to("#world", { z: 150, y: 20, rotationY: -3 })` | Match. Order layers back to front |
|
|
87
|
+
| Depth of field, rack focus | Tween a `focus` object and set each layer's `blur(|z − focus| / 120 px)` in its `onUpdate` | Match. Caveat: six blurred full-frame layers cost about 340 ms per 1080p frame |
|
|
88
|
+
| Card flip, tilt | `perspective` and `tl.to(el, { rotationY: 180 })` | Match |
|
|
89
|
+
| Cube, carousel, crossing planes | Correct in the cloud render; Studio's preview paints faces in DOM order. Avoid | Caveat |
|
|
90
|
+
| 3D models, lights, materials, WebGL | Not available. Fake depth with layered 2.5D, gradients and SVG lighting | — |
|
|
91
|
+
|
|
92
|
+
## Generative and data
|
|
93
|
+
|
|
94
|
+
| Use | Recipe | Status |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| Particles, generative art | Canvas 2D drawn from `window.__shotstackSeek = (ms) => { … }` with seeded positions; or DOM dots with `physics2D` | Match |
|
|
97
|
+
| Charts and data viz | D3 builds SVG, GSAP animates it (see the bar-chart example in [`html5.md`](html5.md)) | Match |
|
|
98
|
+
| Lottie animation | `lottie.loadAnimation({ container, renderer: "svg", autoplay: false, loop: false, animationData })`. Shapes, stroke write-ons, masks, mattes and nested compositions work | Caveat: expressions don't run; convert them to keyframes first |
|
|
99
|
+
|
|
100
|
+
## Not available
|
|
101
|
+
|
|
102
|
+
- **WebGL, shaders, 3D models:** use CSS 2.5D and SVG filters.
|
|
103
|
+
- **Video or audio inside the clip:** put footage and sound on other timeline tracks; `html5` is the graphics layer.
|
|
104
|
+
- **Scroll, drag or pointer-driven animation:** capture is time-driven only.
|
|
105
|
+
- **Remote fonts, images, scripts:** inline them as `data:` URIs.
|
|
106
|
+
|
|
107
|
+
## Cost
|
|
108
|
+
|
|
109
|
+
Most recipes cost 20–40 ms per 1080p frame on the CPU renderer. The expensive ones are full-frame blur or `backdrop-filter` (about 90 ms), fractal noise (about 75 ms) and stacks of blurred depth layers (about 340 ms). Keep heavy filters on small elements, and blur only the layers that need it.
|
|
@@ -4,36 +4,33 @@ Copy-paste `html5` clips that "pop" — kinetic type, value reveals, shine sweep
|
|
|
4
4
|
pulsing CTAs, grain. Each is a **single clip**: paste it into a track's
|
|
5
5
|
`clips[]`, set `start`/`length`, and position with `offset`.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
makes a set of these clips feel like one production. **When you adapt a snippet,
|
|
12
|
-
keep the tokens** — change the words, the colours and the canvas size, not the
|
|
13
|
-
easings and durations. Read [`motion.md`](motion.md) for the why and the full recipe set.
|
|
7
|
+
These are examples to remix. They share one palette (ink `#141414`, accent `#D96B82`)
|
|
8
|
+
so they combine cleanly as they are; change anything — timing, easing, travel, colour,
|
|
9
|
+
layout. [`motion.md`](motion.md) covers what each layer can animate and the easing
|
|
10
|
+
available.
|
|
14
11
|
|
|
15
12
|
Read [`html5.md`](html5.md) for the rules these obey. The non-negotiables:
|
|
16
13
|
|
|
17
14
|
- **Seekable animation only.** GSAP timelines, GSAP tweens, anime.js, Lottie, or CSS `@keyframes`. **Never** `setTimeout`/`setInterval`/`requestAnimationFrame`/`Date.now()`/`gsap.call()` — the renderer seeks by absolute time, it doesn't play.
|
|
18
|
-
-
|
|
15
|
+
- **Animate CSS properties; keep callbacks pure.** `onUpdate` runs on every seek, so it may only set values derived from the tween's progress. Never use `paused: true` timelines or `gsap.call()`: neither follows the renderer's seeks. Bake final values into the HTML at generation time and reveal them with opacity/transform tweens.
|
|
19
16
|
- **Size the clip to the content, not the canvas.** `html, body` pinned to the clip's `width`/`height`; place with `offset` (`{x:0,y:0}` is centred, `y` positive is up). Use **px**, never `vw`/`vh`/`%`.
|
|
20
17
|
- **The body is transparent by default** — the clip composites over the layers below (only set an opaque background if you want one).
|
|
21
|
-
- **
|
|
22
|
-
- **No network.** gsap
|
|
18
|
+
- **Canvas only from the seek hook.** Draw canvas 2D inside `window.__shotstackSeek(ms)`, never in a `requestAnimationFrame` loop. WebGL is blank in the cloud render. SVG or positioned DOM is usually lighter.
|
|
19
|
+
- **No network.** gsap (with its plugins), anime, d3 and lottie are preloaded; external `<script src>`, `fetch`, remote `<img>`, and remote fonts are all CSP-blocked — inline everything as `data:` URIs.
|
|
23
20
|
- **Fonts:** `timeline.fonts[]` does **not** reach `html5` and remote `@font-face` is blocked. These snippets name a display font first but render in the `system-ui` fallback unless you inline the font as a `data:` `@font-face`.
|
|
24
21
|
- **Use unique element IDs for GSAP targets, not `:nth-child` or compound CSS selectors.** Studio preview doesn't resolve `.parent:nth-child(n) .child` patterns; `#id` works everywhere. When animating repeated elements (list rows, bars, cards), generate a unique ID per item (`id="row0"`, `id="bar0"`) rather than relying on structural selectors.
|
|
25
22
|
|
|
26
23
|
Coordinates below assume a **1080×1920 vertical** canvas; adjust `offset` for other sizes.
|
|
27
24
|
|
|
28
|
-
> **Continuous
|
|
25
|
+
> **Continuous motion.** Looping or drifting effects (the pulse, the sweep, the grain) are *ambient*, not entrances — they use `ease-in-out` / `linear` and their own loop durations.
|
|
29
26
|
|
|
30
27
|
---
|
|
31
28
|
|
|
32
29
|
## 1. Blur reveal — calm text entrance
|
|
33
30
|
|
|
34
|
-
**Category** entrances · **Use when**
|
|
31
|
+
**Category** entrances · **Use when** a calm text reveal for a title or line; for energy, use snippet 2 · **Canvas** 900×300 · **Tags** text, reveal, entrance, blur
|
|
35
32
|
|
|
36
|
-
The reference entrance: opacity, blur and a 16 px rise settle **together** off one tween (one progress, many channels) on
|
|
33
|
+
The reference entrance: opacity, blur and a 16 px rise settle **together** off one tween (one progress, many channels) on `power3.out`. Quietly cinematic.
|
|
37
34
|
|
|
38
35
|
```json
|
|
39
36
|
{
|
|
@@ -59,7 +56,7 @@ The reference entrance: opacity, blur and a 16 px rise settle **together** off o
|
|
|
59
56
|
|
|
60
57
|
**Category** entrances · **Use when** a headline needs energy — a hype/hero title where each word punches up in sequence · **Canvas** 980×420 · **Tags** text, reveal, entrance, stagger, hero
|
|
61
58
|
|
|
62
|
-
Each word springs up in sequence. GSAP timeline, fully seekable,
|
|
59
|
+
Each word springs up in sequence. GSAP timeline, fully seekable, words `0.13 s` apart.
|
|
63
60
|
|
|
64
61
|
```json
|
|
65
62
|
{
|
|
@@ -77,7 +74,7 @@ Each word springs up in sequence. GSAP timeline, fully seekable, on the house st
|
|
|
77
74
|
}
|
|
78
75
|
```
|
|
79
76
|
|
|
80
|
-
Each word starts `translateY(120%)`, opacity 0; `0.6 s` rise on `power3.out`, words `0.13 s` apart
|
|
77
|
+
Each word starts `translateY(120%)`, opacity 0; `0.6 s` rise on `power3.out`, words `0.13 s` apart; a trailing empty tween holds the title still. **Punchy variant:** swap `ease:'power3.out'` for `ease:'back.out(1.4)'` for an overshoot, or tighten the stagger for faster delivery. `Anton` falls back to `system-ui` unless inlined.
|
|
81
78
|
|
|
82
79
|
---
|
|
83
80
|
|
|
@@ -137,7 +134,7 @@ A specular highlight slides across text — premium product gloss. *Continuous*
|
|
|
137
134
|
|
|
138
135
|
**Category** graphics (emphasis) · **Use when** drawing the eye to a CTA on an end card · **Canvas** 620×170 · **Tags** cta, button, pulse, loop · **Accent** `#D96B82`
|
|
139
136
|
|
|
140
|
-
A "SHOP NOW" pill with a soft breathing glow. *Ambient loop* — `ease-in-out`, its own 1.6 s cycle
|
|
137
|
+
A "SHOP NOW" pill with a soft breathing glow. *Ambient loop* — `ease-in-out`, its own 1.6 s cycle. CSS keyframes seek cleanly.
|
|
141
138
|
|
|
142
139
|
```json
|
|
143
140
|
{
|
|
@@ -155,7 +152,7 @@ A "SHOP NOW" pill with a soft breathing glow. *Ambient loop* — `ease-in-out`,
|
|
|
155
152
|
}
|
|
156
153
|
```
|
|
157
154
|
|
|
158
|
-
The accent (`#D96B82`)
|
|
155
|
+
The accent (`#D96B82`) carries the glow. The clip is wider/taller than the pill so the glow has room. For a non-looping single pop, replace `infinite` with `1`.
|
|
159
156
|
|
|
160
157
|
---
|
|
161
158
|
|
|
@@ -197,12 +194,16 @@ The snippets share one palette (ink `#141414`, accent `#D96B82`) so a set alread
|
|
|
197
194
|
]
|
|
198
195
|
```
|
|
199
196
|
|
|
200
|
-
Then in any snippet's CSS, swap the literal hex for the token: `color:{{ink}}`, `background:{{accent}}`, `font-family:{{font}}`. One edit re-skins every clip.
|
|
197
|
+
Then in any snippet's CSS, swap the literal hex for the token: `color:{{ink}}`, `background:{{accent}}`, `font-family:{{font}}`. One edit re-skins every clip. (If you ship a snippet *without* a matching `merge[]` entry, leave the literal hex in — an undefined `{{token}}` renders as invalid CSS.)
|
|
201
198
|
|
|
202
199
|
## Composing these
|
|
203
200
|
|
|
204
201
|
- Each snippet is one clip on its own track. Layer order is top-track-first (see `agent-core.md`) — grain and shine go in **early** tracks, backgrounds in **late** ones.
|
|
202
|
+
<!-- surface:cli -->
|
|
205
203
|
- They don't overlap on a single track, so `shotstack validate <file>` stays clean. Run it before rendering.
|
|
204
|
+
<!-- /surface -->
|
|
206
205
|
- Reuse text via top-level `merge[]` (`{{title}}` in the HTML) — see the lower-third example in `html5.md`.
|
|
207
|
-
- Mix calm and punchy
|
|
206
|
+
- Mix calm and punchy: a `blur-reveal` title, a `kinetic-headline` hero line, a `value-reveal` stat, a `shine` on the product, a pulsing CTA.
|
|
207
|
+
<!-- surface:cli -->
|
|
208
208
|
- Heavier motion = longer render. Preview in `shotstack studio <file>` before spending credits.
|
|
209
|
+
<!-- /surface -->
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The `html5` asset type renders a self-contained HTML/CSS/JS page inside a video clip. Use it for animated overlays, data visualisations, motion graphics, and anything you'd build as a tiny single-page web app.
|
|
4
4
|
|
|
5
|
+
For what you can make with it, effect by effect, see [`html5-effects.md`](html5-effects.md).
|
|
6
|
+
|
|
5
7
|
This is the **modern replacement for the deprecated `html` asset.** `html5` runs in a real browser iframe with a JS runtime, library preloads, and deterministic frame capture — the old `html` asset should never be used.
|
|
6
8
|
|
|
7
9
|
## Contents
|
|
@@ -11,6 +13,7 @@ This is the **modern replacement for the deprecated `html` asset.** `html5` runs
|
|
|
11
13
|
- The browser harness (deterministic auto-seek)
|
|
12
14
|
- Sandbox restrictions: no network, inline everything (incl. fonts)
|
|
13
15
|
- Sizing
|
|
16
|
+
- 3D: what renders in preview and render
|
|
14
17
|
- Worked example: animated lower-third (GSAP)
|
|
15
18
|
- Worked example: animated bar chart (D3 + GSAP)
|
|
16
19
|
- Worked example: 10-second countdown (pure CSS)
|
|
@@ -45,20 +48,44 @@ Clip-level `width` and `height` set the iframe's pixel dimensions. They default
|
|
|
45
48
|
|
|
46
49
|
## Preloaded libraries
|
|
47
50
|
|
|
48
|
-
Four libraries are always available — no `<script src=>` tags needed:
|
|
51
|
+
Four libraries and GSAP's plugins are always available — no `<script src=>` tags and no `gsap.registerPlugin()` call needed:
|
|
49
52
|
|
|
50
53
|
- **GSAP** (`window.gsap`) — primary animation library. Use timelines (`gsap.timeline()`) over loose tweens; the harness seeks timelines correctly.
|
|
51
54
|
- **anime.js** (`window.anime`) — alternative animation library.
|
|
52
|
-
- **D3** (`window.d3`) — for data binding, scales, and SVG/DOM construction. Pair with GSAP for the actual animation; D3's transitions work too but GSAP is more reliable under seek.
|
|
53
|
-
- **Lottie** (`window.lottie`) — Bodymovin JSON player (SVG-renderer build; `renderer: "canvas"` is unavailable).
|
|
55
|
+
- **D3** (`window.d3`) — for data binding, scales, and SVG/DOM construction. Pair with GSAP for the actual animation; D3's transitions work too but GSAP is more reliable under seek. `d3.randomLcg(seed)` is the seeded random source.
|
|
56
|
+
- **Lottie** (`window.lottie`) — Bodymovin JSON player (SVG-renderer build; `renderer: "canvas"` is unavailable). Expressions in the file (`wiggle`, `loopOut`) don't run: a layer that carries one doesn't render. Bake expressions into keyframes before export (After Effects: *Animation → Keyframe Assistant → Convert Expression to Keyframes*).
|
|
57
|
+
|
|
58
|
+
GSAP 3.15 plugins, already registered:
|
|
59
|
+
|
|
60
|
+
| Plugin | Use it for |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `SplitText` | Per-character, word or line animation. `SplitText.create("#title", { type: "chars" })` |
|
|
63
|
+
| `TextPlugin` | Typewriter text: `gsap.to(el, { text: "Hello", duration: 1 })` |
|
|
64
|
+
| `ScrambleTextPlugin` | Decoding text. Picks random glyphs, so only its final text matches between preview and render |
|
|
65
|
+
| `DrawSVGPlugin` | Stroke write-ons and travelling stroke segments: `{ drawSVG: "0%" } → { drawSVG: "100%" }` |
|
|
66
|
+
| `MorphSVGPlugin` | Morph one SVG path into another |
|
|
67
|
+
| `MotionPathPlugin` | Move along an SVG path, with `autoRotate` |
|
|
68
|
+
| `Flip` | Animate between two layouts: `Flip.getState`, change the layout, `Flip.from(state)` |
|
|
69
|
+
| `CustomEase`, `CustomBounce`, `CustomWiggle`, `EasePack` | Graph-editor curves, bounce, wiggle and slow-mo eases. Avoid `CustomWiggle` `type: "random"` and `RoughEase`: they're random |
|
|
70
|
+
| `Physics2DPlugin`, `PhysicsPropsPlugin`, `InertiaPlugin` | Velocity, gravity and friction motion |
|
|
54
71
|
|
|
55
|
-
|
|
72
|
+
ScrollTrigger, ScrollSmoother, Observer and Draggable aren't bundled: capture never scrolls or takes input.
|
|
73
|
+
|
|
74
|
+
**You can't load other libraries via `<script src=>`** — the iframe's CSP blocks all external scripts and network access (see *Sandbox restrictions* below).
|
|
56
75
|
|
|
57
76
|
## The browser harness (deterministic auto-seek)
|
|
58
77
|
|
|
59
78
|
Frames are captured by **seeking** the animation to each timestamp, not by playing in real time — so your animation must be **seekable**. GSAP (timelines or tweens), anime.js, Lottie, and CSS (`@keyframes`, transitions, `Element.animate()`) are all driven automatically. Anything time-driven that isn't seekable gives a frozen or wrong frame: never use `setTimeout`, `setInterval`, `requestAnimationFrame` loops, `Date.now()` / `performance.now()`, or `gsap.call()`. For "different content at different times" (countdowns, tickers, scene swaps) use the staggered-CSS pattern (see the countdown example) or bake values into the HTML and animate their visibility (see the value-reveal snippet).
|
|
60
79
|
|
|
61
|
-
|
|
80
|
+
**Callbacks.** `onUpdate` runs on every seek for tweens and timelines the harness drives, so it can set what GSAP can't tween directly (a blur computed from depth, a counter's text), provided it depends only on the tween's own progress. It does **not** run for timelines created with `paused: true`, and `gsap.call()` fires when the playhead passes it but isn't undone when the renderer seeks back. Avoid both. Prefer animating CSS properties (opacity, transform, filter) and baking values into the HTML.
|
|
81
|
+
|
|
82
|
+
**Scenes as nested timelines.** Build each scene as its own timeline and place it on one master: `gsap.timeline().add(sceneA, 0).add(sceneB, 2.5)`. Nested timelines and `delay` play at their offsets, the way nested compositions sit on an editing timeline.
|
|
83
|
+
|
|
84
|
+
**Randomness must be seeded.** Preview and render must draw the same values, so never use `Math.random()`. Use `const rand = d3.randomLcg(42)` and generate positions, colours and velocities from it at load.
|
|
85
|
+
|
|
86
|
+
**Initial state in CSS or `from()`.** Put each element's starting state in its CSS or in a `from()` / `fromTo()`. A `gsap.set()` made before the timeline can miss the first frame of the cloud render.
|
|
87
|
+
|
|
88
|
+
**Custom drawing.** For anything the libraries don't drive (canvas 2D, SVG built in code), define `window.__shotstackSeek = function (ms) { … }` and redraw the whole frame for `ms` from scratch. The harness calls it on every frame, after GSAP.
|
|
62
89
|
|
|
63
90
|
**Duration comes from the clip's `length`** — there's no animation-duration auto-detection. Size your animation to run within (or fill) the clip's `length`.
|
|
64
91
|
|
|
@@ -80,7 +107,7 @@ The iframe renders under a strict Content-Security-Policy (`default-src 'none'`)
|
|
|
80
107
|
@font-face { font-family: 'Brand'; src: url('data:font/woff2;base64,<…>') format('woff2'); }
|
|
81
108
|
.title { font-family: 'Brand', sans-serif; }
|
|
82
109
|
```
|
|
83
|
-
2. **Use a system family** (`system-ui`, `Arial`, `Georgia`, …) — resolves in the render
|
|
110
|
+
2. **Use a system family** (`system-ui`, `Arial`, `Georgia`, …) — resolves with no load, but each machine picks its own font, so text in Studio's preview won't match the cloud render exactly. Embed a font whenever the text must match.
|
|
84
111
|
|
|
85
112
|
An unresolved family silently falls back to the browser default — the render won't fail, but the text won't be your font. For a single styled line, a `rich-text` asset (which *does* use `timeline.fonts[]`; verified catalogue in `shared/agent-core.md`) is simpler than a `data:` font embed.
|
|
86
113
|
|
|
@@ -118,6 +145,19 @@ The clip's `width` / `height` are the iframe's natural pixel dimensions — matc
|
|
|
118
145
|
- The harness already applies a reset — `margin:0; padding:0; box-sizing:border-box; overflow:hidden`, and `body { background: transparent }` — so you mainly need to pin the explicit `width`/`height` on `html, body`. The body is transparent by default; it composites over the track below.
|
|
119
146
|
- The iframe doesn't know about the clip — every internal coordinate is in iframe-pixel space.
|
|
120
147
|
|
|
148
|
+
## 3D: what renders in preview and render
|
|
149
|
+
|
|
150
|
+
| Look | How | Preview and render |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| 2.5D camera move (layers at depth, dolly, rack focus) | `perspective` on a stage, `transform-style: preserve-3d` on a world, each layer at `translateZ(…)`. GSAP moves the world (`z`, `y`, `rotationY`) as the camera. Depth of field: `onUpdate` sets each layer's `blur()` from its distance to a tweened focus value | Match |
|
|
153
|
+
| Card flip, tilted plane | `perspective` + `rotationY` / `rotationX` | Match |
|
|
154
|
+
| Cubes, carousels, planes that cross | `preserve-3d` with several faces | Correct in the cloud render; Studio's preview paints faces in DOM order, so hidden faces show. Avoid |
|
|
155
|
+
| WebGL (three.js `WebGLRenderer`, shaders) | `<canvas>` with a WebGL context | Blank in the cloud render: no WebGL there |
|
|
156
|
+
|
|
157
|
+
For 2.5D, order layers back to front in the DOM and scale each far layer up so it still fills the frame (`scale = (perspective − z) / perspective`).
|
|
158
|
+
|
|
159
|
+
**Watch the cost.** Blur and `backdrop-filter` on full-frame layers are the expensive part on the CPU renderer: six blurred depth layers cost about 340 ms per 1080p frame. Keep blurred layers small, or blur only the layers that need it.
|
|
160
|
+
|
|
121
161
|
## Worked example: animated lower-third (GSAP)
|
|
122
162
|
|
|
123
163
|
Slide-in name + role bar with subtle accent. **Clip sized to the bar (560×120), positioned via `offset`.** 5 seconds.
|
|
@@ -227,18 +267,19 @@ The same pattern scales to scene transitions (each scene is a `<section>` with i
|
|
|
227
267
|
|
|
228
268
|
## Common mistakes
|
|
229
269
|
|
|
230
|
-
1. **`<canvas>`
|
|
270
|
+
1. **`<canvas>` drawn in a loop.** Canvas 2D drawing renders in preview and render, but only when it's drawn from `window.__shotstackSeek(ms)`: a `requestAnimationFrame` loop never advances under seek. A WebGL canvas is blank in the cloud render. For charts and shapes, SVG is still lighter:
|
|
231
271
|
|
|
232
|
-
| Want |
|
|
272
|
+
| Want | Use |
|
|
233
273
|
|---|---|
|
|
234
|
-
| Particles / generative graphics |
|
|
274
|
+
| Particles / generative graphics | Canvas 2D from `__shotstackSeek` with seeded positions, or SVG `<circle>`s |
|
|
235
275
|
| Charts (line / bar / area) | D3 → `<svg>` (see the bar-chart worked example) |
|
|
236
276
|
| Pixel-level effects | CSS filters (`filter: blur(...) hue-rotate(...)`), `<feFilter>` in SVG |
|
|
237
277
|
| Free-form drawings | SVG `<path>` |
|
|
238
|
-
|
|
239
|
-
If you're building something that genuinely cannot be expressed without canvas, render it as a `<video>` or `<image>` asset instead of an `html5` clip.
|
|
240
278
|
2. **Mismatched dimensions.** If `clip.width = 1920` and your CSS sets `body { width: 1280px }`, content gets cropped or stretched. Pin the iframe's `html, body` dimensions to the clip dimensions.
|
|
241
|
-
3. **JS syntax or runtime errors produce a blank clip with no render error.** If `asset.js` throws (syntax error or uncaught runtime error), the entire clip renders as a blank frame. The render still reports `status: "done"` with no error — there is no feedback loop.
|
|
279
|
+
3. **JS syntax or runtime errors produce a blank clip with no render error.** If `asset.js` throws (syntax error or uncaught runtime error), the entire clip renders as a blank frame. The render still reports `status: "done"` with no error — there is no feedback loop. Runtime errors (e.g. referencing a DOM element that doesn't exist) are silent: wrap suspect code in `try/catch` to surface the error.
|
|
280
|
+
<!-- surface:cli -->
|
|
281
|
+
`shotstack validate` catches JS syntax errors offline. If a clip is blank, check the JS first: run `node --check` on the string.
|
|
282
|
+
<!-- /surface -->
|
|
242
283
|
|
|
243
284
|
## When to use `html5` vs `rich-text`/`svg`
|
|
244
285
|
|