@shotstack/cli 0.10.0 → 0.12.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.
@@ -1,119 +1,98 @@
1
- # Motion language — the house vocabulary
1
+ # Motion on Shotstack
2
2
 
3
- A small, closed set of timing, easing and stagger values that **every** animated clip should share. Composing motion from these tokens — instead of inventing a new easing and duration per clip — is what makes a multi-clip edit feel like one production instead of eight unrelated effects. Reach for these before hardcoding a `duration`, an `ease`, or a delay.
3
+ What can move in an edit, on which layer, and how much control each layer gives you. Timing, easing, travel and rhythm are design choices for the piece; this file covers the mechanics.
4
4
 
5
- > The values are video-paced for Shotstack's **second-based** timeline (most UI-motion guidance is frame-based). The GSAP/CSS easings below stand in for spring physics, which the render engine doesn't expose.
5
+ There are two kinds of motion. **Built-in motion** (clip transitions, effects and keyframes, and `rich-text` animation) takes fixed options from the schema and needs care when combined on an existing asset. **`html5`** takes any seekable animation.
6
6
 
7
- ## The tokens
7
+ ## Time
8
8
 
9
- ### Duration scale (seconds)
9
+ - **The timeline is in seconds.** Clip `start`/`length`, keyframe `start`/`length` and GSAP `duration` are all seconds. Convert frame-based specs with the output frame rate (`output.fps`): at 25 fps one frame is 0.04 s.
10
+ - **An `html5` clip captures its animation for the clip's `length`,** measured from the clip's start. Animation that runs past the end is cut; animation that finishes early holds its last state.
10
11
 
11
- | Token | Seconds | Use for |
12
- |---|---|---|
13
- | `instant` | 0.2 | micro shifts, near-imperceptible feedback |
14
- | `fast` | 0.33 | **exits**, small moves |
15
- | `base` | 0.6 | **the default entrance** — ~80% of reveals |
16
- | `slow` | 0.8 | large entrances, hero moves |
17
- | `slower` | 1.0 | full-scene transitions |
18
- | `hold` | 1.5 | minimum settled hold before a clip ends |
19
-
20
- Most entrances are `base` (0.6 s). Exits are deliberately faster than entrances — get out of the way faster than you came in.
12
+ ## What each layer can move
21
13
 
22
- ### Easing — one house curve, no overshoot by default
23
-
24
- | Where | Entrance / settle | Exit |
14
+ | Layer | What moves | Control |
25
15
  |---|---|---|
26
- | **html5 GSAP** | `ease: 'power3.out'` | `ease: 'power2.in'` |
27
- | **html5 CSS** | `cubic-bezier(0.16, 1, 0.3, 1)` | `cubic-bezier(0.3, 0, 0.8, 0.2)` |
28
-
29
- `power3.out` / `cubic-bezier(0.16,1,0.3,1)` is the **house ease** — a confident "fast then settle" that reads like a heavily-damped spring with **no bounce**. Use it for anything the eye tracks (position, scale, blur). Never use raw `linear` / `'none'` for motion the eye follows; `linear` is only correct for a continuous drift (grain, marquee, a constant-speed ticker).
30
-
31
- **Overshoot is opt-in, not default.** A gentle `back.out(1.4)` (GSAP) is reserved for **one** "hero" element per scene where a pop is intended — a logo sting, a headline punch. Calm is the default; energetic is fine when it *earns* it. Never sprinkle bounce across every element.
32
-
33
- ### Stagger — one value
34
-
35
- `0.13 s` (≈ 4 frames at 30 fps) between siblings in any cascade — words, list items, grouped reveals. One value everywhere; present but never frantic. In GSAP: `stagger: 0.13`. In CSS: `animation-delay` steps of `0.13s` (`0s, 0.13s, 0.26s, …`).
16
+ | Clip `transition` | the whole clip, in and out | `fade` `reveal` `wipe*` `slide*` `carousel*` `shuffle*` `zoom`, each with `Slow`/`Fast` variants. Timing and easing are engine-fixed. |
17
+ | Clip `effect` | a Ken Burns drift on an image or video | `zoomIn` `zoomOut` `slideLeft` `slideRight` `slideUp` `slideDown`, each with `Slow`/`Fast` |
18
+ | Clip keyframes | `scale`, `opacity`, `offset.x`/`.y`, `transform.rotate.angle`, `transform.skew.x`/`.y`, an asset's `volume`, a video's `speed` | Tween arrays with your own `start` and `length`; `interpolation` and `easing` from fixed lists (below) |
19
+ | `rich-text` `animation` | the text's entrance | `preset` + `duration` + `style` + `direction`; easing is engine-fixed |
20
+ | `html5` | anything inside the clip | any seekable GSAP, anime.js, Lottie or CSS animation, any easing, per-element timing. Rules in [`html5.md`](html5.md). |
36
21
 
37
- ### Travel & restraint
22
+ ### rich-text animation
38
23
 
39
- - **Travel small:** translate **12–24 px**, not 80. The eye should feel the motion, not be dragged by it. Blur reveals: `10 px → 0`. Scale reveals: `0.92 → 1` (below ~0.85 is dramatic-zoom territory — avoid unless intended).
40
- - **One focal element per moment.** If two things compete, stagger them. Don't animate everything at once.
41
- - **Let it settle.** After a reveal lands, hold it (a trailing `gsap.to({}, { duration: <hold> })` or enough clip `length`) before the clip ends. Never cut a reveal the instant it arrives.
24
+ | Preset | Example |
25
+ |---|---|
26
+ | fade in | `{ "preset": "fadeIn", "duration": 0.6 }` |
27
+ | rise + fade | `{ "preset": "ascend", "duration": 0.6, "direction": "up" }` |
28
+ | slide in | `{ "preset": "slideIn", "duration": 0.6, "direction": "up" }` |
29
+ | word-by-word cascade | `{ "preset": "shift", "duration": 0.6, "style": "word", "direction": "up" }` |
30
+ | typewriter | `{ "preset": "typewriter", "duration": 0.8, "style": "character" }` |
42
31
 
43
- ## The three motion layers
32
+ Schema constraints (`@shotstack/schemas` → `RichTextAnimation`): `preset` ∈ `fadeIn · slideIn · typewriter · ascend · shift · movingLetters`; `duration` 0.1–30 s; **`style` (`word`/`character`) applies only to `typewriter` and `shift`** (ignored on the others); `direction` is **required** for `slideIn`, `ascend`, `shift`, `movingLetters`. Rich-text `animation` is **entrance-only**: exits use the clip `transition.out`. For control over easing, per-character timing or exits, use `html5`.
44
33
 
45
- Shotstack exposes motion at three levels of control. Use the tokens at whichever level fits; reach down a level only when you need more control.
34
+ ### Clip keyframes
46
35
 
47
- | Layer | Control | Where the tokens apply |
48
- |---|---|---|
49
- | 1 — **clip `transition` / `effect`** | coarse (whole-clip in/out; only `Slow`/`Fast` variants) | pick `fade`, `slide*`, `zoom`; `Slow` ≈ `slow`, default ≈ `base`, `Fast` ≈ `fast` |
50
- | 2 — **`rich-text` `animation`** | medium (`preset` + `duration` + `style` + `direction`) | set `duration` from the scale; `style:"word"` gives the staggered cascade |
51
- | 3 — **`html5` GSAP / CSS** | full (any property, any easing) | the full vocabulary below — durations, the house ease, `0.13` stagger |
36
+ - `interpolation` is `linear` (the default), `bezier` or `constant`. `easing` takes effect with `bezier`.
37
+ - `easing` is `ease`, `easeIn`, `easeOut`, `easeInOut`, or `easeIn` / `easeOut` / `easeInOut` followed by `Quad`, `Cubic`, `Quart`, `Quint`, `Sine`, `Expo`, `Circ` or `Back` (for example `easeOutCubic`, `easeInOutBack`).
38
+ - To match an `html5` clip's GSAP ease: `easeOutQuad` ≈ `power1.out`, `easeOutCubic` ≈ `power2.out`, `easeOutQuart` ≈ `power3.out`.
52
39
 
53
- ### Layer 2 — rich-text animation, tokenised
40
+ ### Combining built-in motion on an existing asset
54
41
 
55
- `animation.preset` maps onto the choreography names; set `duration` from the scale:
42
+ - Keyframes move the whole rendered clip; a `rich-text` preset animates the text inside it. A slide preset plus an `offset` keyframe moves the text twice, so animate each property one way per clip.
43
+ - Keyframe times are relative to the clip's `start`, and a tween that runs past the clip's `length` is cut off.
44
+ - Footage, images and logos are scaled by `fit` first, and keyframe `scale` multiplies that, so a large scale on a low-resolution source shows its pixels.
56
45
 
57
- | Intent | `rich-text` animation |
58
- |---|---|
59
- | fade in | `{ "preset": "fadeIn", "duration": 0.6 }` |
60
- | rise + fade (the workhorse) | `{ "preset": "ascend", "duration": 0.6, "direction": "up" }` |
61
- | slide in | `{ "preset": "slideIn", "duration": 0.6, "direction": "up" }` |
62
- | word-by-word cascade | `{ "preset": "shift", "duration": 0.6, "style": "word", "direction": "up" }` |
63
- | typewriter | `{ "preset": "typewriter", "duration": 0.8, "style": "character" }` |
46
+ ## Easing available in html5
64
47
 
65
- Schema constraints (`@shotstack/schemas` → `RichTextAnimation`): `preset` ∈ `fadeIn · slideIn · typewriter · ascend · shift · movingLetters`; `duration` 0.1–30 s; **`style` (`word`/`character`) applies only to `typewriter` and `shift`** (ignored on the others); `direction` is **required** for `slideIn`, `ascend`, `shift`, `movingLetters`. Rich-text `animation` is **entrance-only** — exits use the clip `transition.out` (Layer 1). Easing on the presets is engine-fixed; for full easing control, drop to html5 (Layer 3).
48
+ - **GSAP:** `none`, `power1`–`power4`, `sine`, `circ`, `expo`, `back(overshoot)`, `elastic(amplitude, period)`, `bounce` and `steps(n)`, each as `.in`, `.out` or `.inOut`.
49
+ - **CSS:** any `cubic-bezier()`, `steps()` or `linear(…)` point list, on `@keyframes`, transitions and `Element.animate()`.
66
50
 
67
- ## Choreography recipes (html5 / GSAP)
51
+ ## Recipes
68
52
 
69
- Pure, seekable GSAP — obey [`html5.md`](html5.md) (no `setTimeout`/`rAF`/`Date.now`/`gsap.call()`; size the clip to the content). Each recipe is a named choreography helper you compose rather than reinventing.
53
+ Syntax starting points, deliberately different in feel. Build each scene on one top-level timeline and place tweens with GSAP's position parameter (`0`, `'<'`, `'-=0.2'`, labels) rather than `delay`.
70
54
 
71
55
  ```js
72
- // entryFadeRise — THE workhorse: rise 12px + fade, house ease, base duration
73
- gsap.from('.el', { opacity: 0, y: 12, duration: 0.6, ease: 'power3.out' });
56
+ const tl = gsap.timeline();
74
57
 
75
- // entryFade — presence only (overlays, avatars)
76
- gsap.from('.el', { opacity: 0, duration: 0.6, ease: 'power3.out' });
58
+ // settle: rise and fade into place
59
+ tl.from('#title', { opacity: 0, y: 24, duration: 0.6, ease: 'power3.out' }, 0);
77
60
 
78
- // entryScale — calm scale-up
79
- gsap.from('.el', { opacity: 0, scale: 0.92, duration: 0.6, ease: 'power3.out' });
61
+ // blur reveal: opacity, blur and rise off one tween
62
+ tl.from('#sub', { opacity: 0, y: 16, filter: 'blur(10px)', duration: 0.6, ease: 'power3.out' }, 0.3);
80
63
 
81
- // blurReveal — opacity + blur + rise off ONE tween (one progress, many channels)
82
- gsap.from('.el', { opacity: 0, y: 16, filter: 'blur(10px)', duration: 0.6, ease: 'power3.out' });
64
+ // pop: overshoot on arrival
65
+ tl.from('#badge', { opacity: 0, scale: 0.6, duration: 0.5, ease: 'back.out(2.2)' }, 0.8);
83
66
 
84
- // wordStagger — each word rises in sequence (wrap words in <span>)
85
- gsap.from('.word', { opacity: 0, yPercent: 120, duration: 0.6, ease: 'power3.out', stagger: 0.13 });
67
+ // slam: big travel, hard snap
68
+ tl.from('#hit', { opacity: 0, x: -600, skewX: -18, duration: 0.45, ease: 'expo.out' }, 1.2);
86
69
 
87
- // heroReveal — the ONE place a pop is allowed (≤1 per scene)
88
- gsap.from('.hero', { opacity: 0, y: 16, scale: 0.97, duration: 0.8, ease: 'back.out(1.4)' });
70
+ // character cascade: wrap each character in a <span class="c">
71
+ tl.from('#word .c', { opacity: 0, yPercent: 110, rotation: 12, duration: 0.5, ease: 'power4.out', stagger: 0.03 }, 1.6);
89
72
 
90
- // exitFadeFall — fade + drop, faster, ease-in (an exit doesn't settle)
91
- gsap.to('.el', { opacity: 0, y: 8, duration: 0.33, ease: 'power2.in' });
92
- ```
73
+ // elastic settle
74
+ tl.from('#price', { scale: 0, duration: 1.1, ease: 'elastic.out(1, 0.45)' }, 2.0);
93
75
 
94
- **Entrance → hold → exit, on one timeline:**
76
+ // exit, overlapping the next beat
77
+ tl.to('#title, #sub', { opacity: 0, y: -12, duration: 0.33, ease: 'power2.in' }, '-=0.1');
95
78
 
96
- ```js
97
- const tl = gsap.timeline();
98
- tl.from('.el', { opacity: 0, y: 12, duration: 0.6, ease: 'power3.out' }) // in (base)
99
- .to({}, { duration: 1.5 }) // hold
100
- .to('.el', { opacity: 0, y: 8, duration: 0.33, ease: 'power2.in' }); // out (fast)
79
+ // ambient loop: a drift that runs for the rest of the clip
80
+ tl.to('#glow', { opacity: 0.4, duration: 1.6, ease: 'sine.inOut', repeat: -1, yoyo: true }, 0);
101
81
  ```
102
82
 
103
83
  ### CSS equivalents (no JS)
104
84
 
105
85
  ```css
106
- /* house ease entrance */
107
86
  .el { animation: rise 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; }
108
- @keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
87
+ @keyframes rise { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: none; } }
109
88
 
110
- /* staggered siblings — 0.13s steps */
111
- .w:nth-child(1){animation-delay:0s}.w:nth-child(2){animation-delay:.13s}.w:nth-child(3){animation-delay:.26s}
89
+ /* staggered siblings */
90
+ .w:nth-child(1){animation-delay:0s}.w:nth-child(2){animation-delay:.08s}.w:nth-child(3){animation-delay:.16s}
112
91
  ```
113
92
 
114
- ## Brand kit — set the palette once, re-skin everything
93
+ ## Brand kit through merge fields
115
94
 
116
- Don't hardcode hex and font per snippet. Declare the brand as top-level `merge[]` fields and reference the tokens in every clip's `html`/`css`/`js` — one edit re-skins the whole video — brand tokens applied once, via Shotstack merge:
95
+ `merge` find/replace runs over every string in the edit, including `html5` `html`, `css` and `js`. Declare the brand once as top-level fields and reference them in every clip, and one edit re-skins the whole video:
117
96
 
118
97
  ```json
119
98
  "merge": [
@@ -129,23 +108,8 @@ Don't hardcode hex and font per snippet. Declare the brand as top-level `merge[]
129
108
  .cta { background: {{accent}}; }
130
109
  ```
131
110
 
132
- `merge` find/replace runs over the whole edit, including `html`/`css`/`js` strings, so `{{accent}}` resolves everywhere. Keep one accent and use it sparingly — a headline word, a number, a CTA, one glow — everything else neutral. **Colour is earned, never sprinkled.**
133
-
134
- ## Motion checklist
135
-
136
- Before rendering anything animated:
137
-
138
- 1. **Durations from the scale?** (`0.6` in, `0.33` out — not `0.45`, `0.7`, `1.2`.)
139
- 2. **House ease, not linear?** `power3.out` / `cubic-bezier(0.16,1,0.3,1)` for tracked motion.
140
- 3. **Overshoot only on one hero element**, if at all.
141
- 4. **Stagger is `0.13`**, applied via `stagger` / `animation-delay`.
142
- 5. **Travel 12–24 px; scale ≥ 0.92.** Restraint over flash.
143
- 6. **One focal element per moment;** competing elements staggered.
144
- 7. **It holds before it cuts** (settled `hold`, not an instant exit).
145
- 8. **Brand colours via `merge[]`**, accent used sparingly.
146
-
147
111
  ## See also
148
112
 
149
- - [`html5-snippets.md`](html5-snippets.md) — ready-made clips that already follow these tokens
150
- - [`html5.md`](html5.md) — the html5 sandbox rules these obey (seekable-only, sizing, fonts)
151
- - `shared/agent-core.md` → "Motion language" — the compact token summary (shared with the MCP server)
113
+ - [`html5.md`](html5.md) — the html5 sandbox: seekable animation only, sizing, fonts, preloaded libraries
114
+ - [`html5-snippets.md`](html5-snippets.md) — ready-made clips to remix
115
+ - `shared/agent-core.md` → "Motion" — the compact summary (shared with the MCP server)
@@ -42,8 +42,8 @@ e-commerce app full of product images answers question 2).
42
42
  (`references/asset-library.md`) and motion-graphic snippets
43
43
  (`references/html5-snippets.md`), matched to their stated use case.
44
44
  - Use their brand where it exists: colours, fonts and logos from the project
45
- or their site rather than an invented look. Follow the house motion
46
- language (`references/motion.md`).
45
+ or their site rather than an invented look. See `references/motion.md`
46
+ for what each layer can animate.
47
47
  - If they chose "many": add merge fields (`{{ placeholder }}`) for the parts
48
48
  that vary per video, and show one render with sample values.
49
49
  - Loop: compose, then `shotstack validate`, then `shotstack render edit.json
@@ -11,17 +11,17 @@ Don't invent property names or enum values. The Shotstack schema is published
11
11
  - <https://shotstack.io/docs/guide/llms-full.txt> — single-file LLM-friendly version of the full guide + reference.
12
12
  - <https://github.com/shotstack/oas-api-definition/tree/main/schemas> — raw OpenAPI YAML, source of truth.
13
13
 
14
- CSS naming conventions (`alignment`, `vertical: "center"`) **do not** apply. The spec uses precise names that often differ from web/CSS instincts:
14
+ The `rich-text` asset's styling fields look like CSS but aren't: CSS naming conventions (`alignment`, `vertical: "center"`) **do not** apply to them, or to clip properties. The spec uses precise names that often differ from web/CSS instincts. (An `html5` asset's `css` is ordinary CSS.)
15
15
 
16
- | You'd guess (wrong) | API uses (right) |
17
- |---|---|
18
- | `alignment` | `align` |
19
- | `align: "center"` (string on rich-text asset) | `align: { "horizontal": "center", "vertical": "middle" }` (object) |
20
- | `align.vertical: "center"` | `align.vertical: "middle"` |
21
- | `font.name` | `font.family` |
22
- | `duration` | `length` |
23
- | `transitions: [...]` (array) | `transition: { in, out }` (object) |
24
- | `fit: "cover"` (CSS instinct: scale+crop maintaining aspect) | `fit: "crop"` — Shotstack's `cover` STRETCHES without maintaining aspect ratio |
16
+ | You'd guess (wrong) | API uses (right) | On |
17
+ |---|---|---|
18
+ | `alignment` | `align` | `rich-text` |
19
+ | `align: "center"` (a string) | `align: { "horizontal": "center", "vertical": "middle" }` (object) | `rich-text` |
20
+ | `align.vertical: "center"` | `align.vertical: "middle"` | `rich-text` |
21
+ | `font.name` | `font.family` | `rich-text` |
22
+ | `duration` | `length` | clip |
23
+ | `transitions: [...]` (array) | `transition: { in, out }` (object) | clip |
24
+ | `fit: "cover"` (CSS instinct: scale+crop maintaining aspect) | `fit: "crop"` — Shotstack's `cover` STRETCHES without maintaining aspect ratio | clip |
25
25
 
26
26
  When the API rejects a property, the error message names the field — fix and retry. Don't guess twice.
27
27
 
@@ -89,11 +89,16 @@ Clips on the same track must not have overlapping `start`/`length` ranges — ov
89
89
 
90
90
  Sequential clips (one finishes, the next starts) **can** share a track — `"start": "auto"` chains them. A cross-fade needs two tracks with a small time overlap and a `transition` on each.
91
91
 
92
+ <!-- surface:cli -->
92
93
  **Validate before you render:** `shotstack validate <file>` catches same-track overlaps — plus unloaded fonts, non-public `src` URLs, and wrong property names/enums — offline, no API key, no credits.
94
+ <!-- /surface -->
93
95
 
94
96
  ## Clip motion & rich-text fields (cheatsheet)
95
97
 
96
- Compose from this rather than round-tripping the full schema — these are the values renders actually use. (`api.edit.json` stays authoritative; `shotstack validate` checks against it.)
98
+ Compose from this rather than round-tripping the full schema — these are the values renders actually use.
99
+ <!-- surface:cli -->
100
+ `api.edit.json` stays authoritative; `shotstack validate` checks against it.
101
+ <!-- /surface -->
97
102
 
98
103
  **Clip-level (wraps any asset):**
99
104
 
@@ -132,16 +137,17 @@ Compose from this rather than round-tripping the full schema — these are the v
132
137
 
133
138
  For motion beyond this (kinetic type, value reveals, shine sweeps, grain, pulsing CTAs) reach for `html5` — see [`references/html5-snippets.md`](../references/html5-snippets.md).
134
139
 
135
- ## Motion language (house tokens)
140
+ ## Motion
136
141
 
137
- Compose motion from a **closed set of tokens** so a multi-clip edit feels like one production, not eight unrelated effects. **Don't invent a new easing or duration per clip.** Full recipes (GSAP/CSS, choreography, brand kit) in [`references/motion.md`](../references/motion.md).
142
+ Timing, easing and rhythm are yours to design. Built-in motion takes fixed options from the schema; `html5` takes any seekable animation. Detail and recipes in [`references/motion.md`](../references/motion.md).
138
143
 
139
- - **Durations (seconds):** `instant 0.2 · fast 0.33 · base 0.6 · slow 0.8 · slower 1.0 · hold 1.5`. Entrances default to **`base` (0.6)**; **exits are faster** (`fast`).
140
- - **One house ease, no overshoot:** html5 GSAP `power3.out` (in) / `power2.in` (out); CSS `cubic-bezier(0.16, 1, 0.3, 1)`. Never raw `linear` for tracked motion. A gentle `back.out(1.4)` is reserved for **one** hero element per scene — calm is the default.
141
- - **One stagger:** `0.13s` between siblings (GSAP `stagger: 0.13`; CSS `animation-delay` steps).
142
- - **Restraint:** translate 12–24 px (not 80), scale ≥ 0.92, one focal element per moment, let a reveal settle (`hold`) before it cuts.
143
- - **rich-text `animation`** (entrance-only — exits use the clip `transition.out`): set `duration` from the scale (e.g. `0.6`); `preset:"ascend"` + `direction:"up"` is the rise-and-fade workhorse; for a word cascade use `preset:"shift"` + `style:"word"` + `direction:"up"` (`style` works only on `typewriter`/`shift`; `direction` is required for `slideIn`/`ascend`/`shift`/`movingLetters`).
144
- - **Brand once:** put palette/font in top-level `merge[]` (`{{ink}}`, `{{accent}}`, `{{font}}`) and reference them in every clip — one edit re-skins the whole video. Accent used sparingly.
144
+ - **Seconds, not frames:** convert frame-based specs with `output.fps`.
145
+ - **Clip level, fixed options:** `transition` in/out (engine-fixed timing, `Slow`/`Fast` variants), `effect` (Ken Burns drift) and keyframe tweens on `scale`, `opacity`, `offset`, `rotate`, `skew`, `volume` and `speed` (see Keyframes above). Keyframe `interpolation` is `linear`, `bezier` or `constant`, and `easing` comes from a fixed list (`easeOutCubic`, `easeInOutBack`, …).
146
+ - **Combining on an existing asset:** keyframes move the whole clip while a `rich-text` preset animates the text inside it, so animate each property one way per clip. Keyframe `scale` multiplies `fit`, so a large scale on a low-resolution source shows its pixels.
147
+ - **rich-text `animation`** is entrance-only (exits use the clip `transition.out`) with engine-fixed easing. `style` works only on `typewriter`/`shift`; `direction` is required for `slideIn`/`ascend`/`shift`/`movingLetters`.
148
+ - **`html5`** gives full control: any seekable GSAP, anime.js, Lottie or CSS animation, any easing, per-element timing.
149
+ - **To music:** measure the track before timing anything to it, then write cuts and motion in bars. Method in [`references/beat-sync.md`](../references/beat-sync.md).
150
+ - **Brand once:** palette and font as top-level `merge[]` fields (`{{ink}}`, `{{accent}}`, `{{font}}`) resolve inside `html5` `html`/`css`/`js` too, so one edit re-skins the whole video.
145
151
 
146
152
  ## Positioning & coordinates
147
153
 
@@ -208,7 +214,9 @@ Top-level `merge[]` (sibling of `timeline`/`output`, NOT a clip property) is a s
208
214
 
209
215
  **Checking merge results:** pass `?data=true&merged=true` on the status request to see the merged Edit JSON in the response — useful for debugging unresolved placeholders.
210
216
 
217
+ <!-- surface:cli -->
211
218
  **Validate caveat:** `shotstack validate` checks `html5` JS syntax **before** merge resolution. A `{{TOKEN}}` inside a JS string literal (e.g. `color: "{{ACCENT}}"`) is valid JS and passes. A `{{TOKEN}}` in JS *code* position (not inside a string) may fail validation pre-merge but work at render time post-merge — keep merge fields inside string literals to avoid false positives.
219
+ <!-- /surface -->
212
220
 
213
221
  ## Asset types
214
222
 
@@ -222,8 +230,8 @@ Use only the **current** asset types; the deprecated ones still parse but should
222
230
  | `image` | Static image — `jpg`, `png`, `webp`, `gif`, `bmp`, `tiff` — or AI-generated from a `prompt`. |
223
231
  | `audio` | Audio clip placed at a specific time on the timeline, or AI-generated speech or music from a `prompt`. |
224
232
  | `rich-text` | Styled text overlay with full typography control. **Use this instead of `text`/`html`/`title`.** |
225
- | `svg` | Vector graphics from raw SVG markup: `src` starts with `<svg`, never a URL or `data:` URI (the render fails). See `references/svg.md`. |
226
- | `html5` | Self-contained HTML/CSS/JS page rendered in an iframe (motion graphics, charts, animated overlays). Preloads gsap/d3/anime/lottie. See `references/html5.md`. **Never use the deprecated `html` asset.** |
233
+ | `svg` | Vector graphics from raw SVG markup: `src` starts with `<svg`, never a URL or `data:` URI (the render fails). See **`svg` assets** below. |
234
+ | `html5` | Self-contained HTML/CSS/JS page rendered in an iframe (motion graphics, charts, animated overlays). Preloads gsap with its plugins (SplitText, DrawSVG, MorphSVG, MotionPath, Flip and more), d3, anime and lottie. See `references/html5.md`. **Never use the deprecated `html` asset.** |
227
235
  | `rich-caption` | Word-level animated captions sourced from audio, video, or subtitle files. See `references/caption.md`. |
228
236
  | `luma` | Luma matte for masking effects. |
229
237
 
@@ -244,6 +252,29 @@ For background music, **use an `audio` asset on its own track** with `length: "e
244
252
  | `timeline.soundtrack` | `audio` asset on its own track with `length: "end"` |
245
253
  | `text-to-image`, `image-to-video`, `text-to-speech` | `image`, `video` or `audio` with `prompt` and `model` (below) |
246
254
 
255
+ ### `svg` assets
256
+
257
+ `src` **is the SVG markup itself**, starting with `<svg`. Never a URL and never a data URI (`data:image/svg+xml;…`, base64 or URL-encoded): those fail to parse, so Studio shows an empty placeholder and the rendered video leaves the shape out. Write colours as plain `#hex`, not `%23`. `data:` URIs belong inside `html5` assets only.
258
+
259
+ Every `src` must include `xmlns="http://www.w3.org/2000/svg"`, a `viewBox`, and `width` and `height` in pixels. Without them the renderer can't size or place the shape.
260
+
261
+ ```json
262
+ {
263
+ "asset": {
264
+ "type": "svg",
265
+ "src": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 200 200\" width=\"200\" height=\"200\"><circle cx=\"100\" cy=\"100\" r=\"90\" fill=\"#142033\" stroke=\"#F2C14E\" stroke-width=\"6\"/></svg>"
266
+ },
267
+ "start": 0,
268
+ "length": 3,
269
+ "width": 200,
270
+ "height": 200
271
+ }
272
+ ```
273
+
274
+ - **Supported:** `<path>`, `<rect>`, `<circle>`, `<ellipse>`, `<line>`, `<polygon>`, `<polyline>`, `<g>`, with `fill`, `fill-opacity`, `stroke`, `stroke-width`, `stroke-opacity` and `transform`.
275
+ - **Unsupported:** `<text>` (use `rich-text`), `<image>` (use `image`), `<animate>`/`<animateTransform>` (animate the clip instead; only the first frame renders), `<foreignObject>`, most `<filter>`s, and external `<use>` references.
276
+ - A plain coloured box behind text doesn't need SVG: `rich-text` has `background.color`, `opacity` and `borderRadius`. Use SVG for shapes rich-text can't draw, such as badges, rings, dividers and speech bubbles.
277
+
247
278
  ### AI-generated assets
248
279
 
249
280
  Set `prompt` and `model` on an `image`, `video` or `audio` asset and it is generated at render time. `options` configures the model, and a `src` you set is only a preview placeholder that the generated file replaces. For speech, the prompt is the words to speak.
@@ -252,10 +283,17 @@ Set `prompt` and `model` on an `image`, `video` or `audio` asset and it is gener
252
283
  { "asset": { "type": "video", "prompt": "Slow dolly across a rain-soaked neon street at night", "model": "seedance-2.0-text-to-video", "options": { "resolution": "720p" } }, "start": 0, "length": 5 }
253
284
  ```
254
285
 
255
- The models endpoint (`GET /models`) lists each model, whether the account can use it, and the JSON Schema for its `options`. `POST /generate/quote` takes the same body as `POST /generate` and returns its cost in credits without charging. Options outside that schema are rejected. To animate a still, use an image-to-video model and put the image URL in `options.startSrc` (`inputSrc` on the original image-to-video models).
286
+ The models endpoint (`GET /models`) lists each model, whether the account can use it, and the JSON Schema for its `options`. `POST /generate/quote` takes the same body as `POST /generate` and returns its cost in credits without charging.
287
+
288
+ Options outside that schema are rejected. To animate a still, use an image-to-video model and put the image URL in `options.startSrc` (`inputSrc` on the original image-to-video models).
256
289
 
257
290
  Generation is billed per asset **even on the stage endpoint**, which is otherwise free. Renders containing AI assets take longer, because the render waits for each generation.
258
291
 
292
+ **Music.** Use the user's own track when they have one. Otherwise offer a track generated for this video, written for its mood, tempo and length. The stock library is a dozen tracks shared by every video that falls back on it, so use it only when no music model is available or the user declines.
293
+ <!-- surface:cli -->
294
+ `shotstack models` shows whether this key can generate music; quote the track with `--quote` before generating it, as for any asset. Without a key, use a library track and tell the user once that after `shotstack login` you can generate music made for this video.
295
+ <!-- /surface -->
296
+
259
297
  ## The design ladder — escalate from rich-text to html5
260
298
 
261
299
  One overlay is a `rich-text` job. **Several videos "in different styles" is not** — making them all `rich-text` ships one look eight times. Match the asset to the ambition:
@@ -264,9 +302,9 @@ One overlay is a `rich-text` job. **Several videos "in different styles" is not*
264
302
  |---|---|---|
265
303
  | 1 — type & layout | `rich-text` | Titles, lower-thirds, kickers, captions, price/CTA pills. Fast and reliable; the right default for static styled text. |
266
304
  | 2 — shapes | `svg` | Colour panels, rules, badges, frames, geometric accents behind or around type. |
267
- | 3 — motion graphics | `html5` | Kinetic type, value reveals, shine sweeps, animated gradients, film grain, masked reveals, data-driven overlays — anything that should *move* beyond a `transition` or a Ken-Burns `effect`. gsap / anime / d3 / lottie are preloaded. See [`references/html5.md`](../references/html5.md) and the copy-paste clips in [`references/html5-snippets.md`](../references/html5-snippets.md). |
305
+ | 3 — motion graphics | `html5` | Kinetic type, value reveals, shine sweeps, animated gradients, film grain, masked reveals, data-driven overlays — anything that should *move* beyond a `transition` or a Ken-Burns `effect`. gsap and its plugins, anime, d3 and lottie are preloaded. See [`references/html5.md`](../references/html5.md), the effects palette in [`references/html5-effects.md`](../references/html5-effects.md), and the copy-paste clips in [`references/html5-snippets.md`](../references/html5-snippets.md). |
268
306
 
269
- **When the brief asks for a *range*, deliberately spread across the ladder.** A strong set: a couple of clean `rich-text` studio cuts, one or two `svg` colour-block promos, and several `html5` pieces carrying the real motion (kinetic headline, value reveal, shine-swept CTA, grain-graded teaser). Reserve the elaborate `html5` treatments for the hero / hype cuts where motion sells the product. If every clip in a "variety" brief is `rich-text`, you have not delivered variety.
307
+ **When the brief asks for a *range*, deliberately spread across the ladder.** A strong set: a couple of clean `rich-text` studio cuts, one or two `svg` colour-block promos, and several `html5` pieces carrying the real motion (kinetic headline, value reveal, shine-swept CTA, grain-graded teaser). If every clip in a "variety" brief is `rich-text`, you have not delivered variety.
270
308
 
271
309
  ## Fonts
272
310
 
@@ -293,7 +331,11 @@ Paste the **url** into `timeline.fonts[].src`, paste the **family** into `asset.
293
331
  | Anton (display) | `1Ptgg87LROyAm0K08i4gS7lu` | `https://fonts.gstatic.com/s/anton/v27/1Ptgg87LROyAm0K08i4gS7lu.ttf` |
294
332
  | Playfair Display (serif, variable) | `nuFiD-vYSZviVYUb_rj3ij__anPXPTvSgWE_-xU` | `https://fonts.gstatic.com/s/playfairdisplay/v40/nuFiD-vYSZviVYUb_rj3ij__anPXPTvSgWE_-xU.ttf` |
295
333
 
296
- Variable fonts cover the full weight range (100–900) from a single URL — set `font.weight` on the clip. For a font not listed: copy an entry **verbatim** from the Studio SDK catalogue (~400 fonts) at <https://github.com/shotstack/shotstack-studio-sdk/blob/main/src/core/fonts/google-fonts.ts>, or open the font on <https://fonts.google.com>, view the CSS, and copy the exact `.ttf` URL from the `@font-face` `src`. Never hand-edit the version or hash segments.
334
+ Variable fonts cover the full weight range (100–900) from a single URL — set `font.weight` on the clip. Poppins, Bebas Neue and Anton aren't variable: the listed URL is the 400 weight.
335
+
336
+ For a font not listed: copy an entry **verbatim** from the Studio SDK catalogue (~400 fonts) at <https://github.com/shotstack/shotstack-studio-sdk/blob/main/src/core/fonts/google-fonts.ts>, or open the font on <https://fonts.google.com>, view the CSS, and copy the exact `.ttf` URL from the `@font-face` `src`. Never hand-edit the version or hash segments.
337
+
338
+ Never invent or hand-edit a font URL. If a font isn't listed and you can't copy its exact entry, use the closest listed font.
297
339
 
298
340
  ### Usage example
299
341
 
@@ -319,7 +361,10 @@ Variable fonts cover the full weight range (100–900) from a single URL — set
319
361
  }
320
362
  ```
321
363
 
322
- Use the `family` column from the table above: the URL's filename without `.ttf`, matched ignoring case. The family name stored inside the font file also matches, but it isn't always the name on Google Fonts (Space Grotesk's file calls itself `Space Grotesk Light`), so the filename is the safe choice. A `font.family` that matches nothing renders in Roboto with no error. The exception is a lone entry in `timeline.fonts[]`, which is used whatever `font.family` says, so a wrong name only shows once a second font is added. `shotstack validate` warns on any `font.family` that isn't a built-in or a loaded filename.
364
+ Use the `family` column from the table above: the URL's filename without `.ttf`, matched ignoring case. The family name stored inside the font file also matches, but it isn't always the name on Google Fonts (Space Grotesk's file calls itself `Space Grotesk Light`), so the filename is the safe choice. A `font.family` that matches nothing renders in Roboto with no error. The exception is a lone entry in `timeline.fonts[]`, which is used whatever `font.family` says, so a wrong name only shows once a second font is added.
365
+ <!-- surface:cli -->
366
+ `shotstack validate` warns on any `font.family` that isn't a built-in or a loaded filename.
367
+ <!-- /surface -->
323
368
 
324
369
  ## Top 5 mistakes
325
370
 
@@ -331,4 +376,7 @@ Use the `family` column from the table above: the URL's filename without `.ttf`,
331
376
 
332
377
  ## Per-topic deep dives
333
378
 
334
- For details beyond this core guide (rich-caption presets, SVG constraints, full font URL list, troubleshooting), see the `references/` directory in the Shotstack CLI repo or fetch the topic-specific docs from `https://shotstack.io/docs/guide/llms-full.txt`.
379
+ For details beyond this core guide (rich-caption presets, SVG constraints, full font URL list, troubleshooting), fetch the topic-specific docs from `https://shotstack.io/docs/guide/llms-full.txt`.
380
+ <!-- surface:cli -->
381
+ The `references/` directory in the Shotstack CLI repo has the same detail.
382
+ <!-- /surface -->