@shotstack/cli 0.5.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shotstack/cli",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Command-line interface for the Shotstack video rendering API.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/shotstack/shotstack-cli",
@@ -17,8 +17,7 @@
17
17
  },
18
18
  "files": [
19
19
  "dist/shotstack.js",
20
- "SKILL.md",
21
- "references"
20
+ "skills"
22
21
  ],
23
22
  "scripts": {
24
23
  "start": "bun run src/index.ts",
@@ -126,6 +126,8 @@ The Shotstack schema does **not** match CSS or web conventions. Composing Edit J
126
126
 
127
127
  The full ruleset (asset types, fonts, smart-string clip values, top-5 mistakes) lives in `shared/agent-core.md`. The same file is also returned by the Shotstack MCP server's `get_shotstack_guide` tool, so the conventions are identical across surfaces.
128
128
 
129
+ **For anything that animates, compose motion from the house tokens** — one duration scale (`base` 0.6 s in / `fast` 0.33 s out), one house ease (`power3.out` / `cubic-bezier(0.16,1,0.3,1)`, no overshoot by default), one stagger (`0.13 s`). The *Motion language* section of `agent-core.md` is the summary; [`references/motion.md`](references/motion.md) has the full GSAP/CSS recipes and the brand kit. **Don't invent a new easing or duration per clip** — shared tokens are what make a multi-clip edit feel like one production.
130
+
129
131
  ## Exit codes
130
132
 
131
133
  | Code | Meaning |
@@ -154,8 +156,9 @@ This skill ships sub-references for the gnarly bits:
154
156
  - [`references/positioning.md`](references/positioning.md) — coordinate model, `position`/`offset` (fraction of frame, +y up), clip bounding box & `fit`, text sizing, transform order
155
157
  - [`references/caption.md`](references/caption.md) — sizing per resolution, default style, the 5 named presets, alias pattern (asset type: `rich-caption`)
156
158
  - [`references/svg.md`](references/svg.md) — required attrs, supported elements
159
+ - [`references/motion.md`](references/motion.md) — **the house motion language**: one duration scale, one ease, one stagger; choreography recipes (GSAP/CSS), the rich-text/transition mappings, and the brand kit. Read before composing any animation.
157
160
  - [`references/html5.md`](references/html5.md) — HTML5 asset: fields, preloaded libs (gsap/d3/anime/lottie), browser harness, sizing, worked examples
158
- - [`references/html5-snippets.md`](references/html5-snippets.md) — copy-paste motion-graphic clips: kinetic headline, count-up/price odometer, shine sweep, pulsing CTA, film grain
161
+ - [`references/html5-snippets.md`](references/html5-snippets.md) — copy-paste motion-graphic clips: kinetic headline, value reveal, shine sweep, pulsing CTA, film grain
159
162
  - [`references/fonts.md`](references/fonts.md) — built-in fonts, Google Fonts URL pattern, custom-font workflow
160
163
  - [`references/asset-library.md`](references/asset-library.md) — placeholder videos, images, music
161
164
  - [`references/troubleshooting.md`](references/troubleshooting.md) — common errors and fixes
@@ -0,0 +1,207 @@
1
+ # HTML5 snippet pack — drop-in motion graphics
2
+
3
+ Copy-paste `html5` clips that "pop" — kinetic type, value reveals, shine sweeps,
4
+ pulsing CTAs, grain. Each is a **single clip**: paste it into a track's
5
+ `clips[]`, set `start`/`length`, and position with `offset`.
6
+
7
+ Every snippet here is composed from the house **[`motion.md`](motion.md)** tokens —
8
+ one duration scale (`base` 0.6 s in, `fast` 0.33 s out), one house ease
9
+ (`power3.out` / `cubic-bezier(0.16,1,0.3,1)`), one stagger (`0.13 s`), and a single
10
+ shared palette (ink `#141414`, accent `#D96B82`). That shared vocabulary is what
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.
14
+
15
+ Read [`html5.md`](html5.md) for the rules these obey. The non-negotiables:
16
+
17
+ - **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
+ - **`onUpdate` does not fire under seek.** GSAP `onUpdate` callbacks are **not** invoked when the harness seeks to a frame, so any DOM mutations made inside them (`textContent`, `innerHTML`, class swaps) won't appear in the rendered video. Animate **CSS properties only** (opacity, transform, filter, scale). Bake final values into the HTML at generation time and reveal them with opacity/transform tweens.
19
+ - **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
+ - **The body is transparent by default** — the clip composites over the layers below (only set an opaque background if you want one).
21
+ - **No `<canvas>`** — Studio capture serialises the DOM; canvas bitmaps come through empty. Use SVG or positioned DOM.
22
+ - **No network.** gsap/anime/d3/lottie are preloaded; external `<script src>`, `fetch`, remote `<img>`, and remote fonts are all CSP-blocked — inline everything as `data:` URIs.
23
+ - **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
+
25
+ Coordinates below assume a **1080×1920 vertical** canvas; adjust `offset` for other sizes.
26
+
27
+ > **Continuous-motion exception.** Looping or drifting effects (the pulse, the sweep, the grain) are *ambient*, not entrances — they correctly use `ease-in-out` / `linear` and their own loop durations rather than the entrance tokens. Everything that *reveals* uses the house entrance tokens.
28
+
29
+ ---
30
+
31
+ ## 1. Blur reveal — calm text entrance
32
+
33
+ **Category** entrances · **Use when** the default text reveal; the calm house entrance for a title or line — reach for a punchier one (snippet 2) only with intent · **Canvas** 900×300 · **Tags** text, reveal, entrance, blur
34
+
35
+ The reference entrance: opacity, blur and a 16 px rise settle **together** off one tween (one progress, many channels) on the house ease — no overshoot. Quietly cinematic.
36
+
37
+ ```json
38
+ {
39
+ "asset": {
40
+ "type": "html5",
41
+ "html": "<div class=\"t\">{{title}}</div>",
42
+ "css": "html,body{margin:0;width:900px;height:300px;overflow:hidden;background:transparent;font-family:'Clash Display',system-ui,sans-serif}.t{display:flex;align-items:center;justify-content:center;width:900px;height:300px;font-size:150px;font-weight:600;letter-spacing:-3px;color:#141414;text-align:center}",
43
+ "js": "gsap.from('.t',{opacity:0,y:16,filter:'blur(10px)',duration:0.6,ease:'power3.out'});gsap.to({},{duration:1.5});"
44
+ },
45
+ "start": 0,
46
+ "length": 3,
47
+ "width": 900,
48
+ "height": 300,
49
+ "offset": { "x": 0, "y": 0 }
50
+ }
51
+ ```
52
+
53
+ `base` (0.6 s) entrance on `power3.out`, then a `hold` (1.5 s) settle. Swap `{{title}}` via top-level `merge[]`. `Clash Display` falls back to `system-ui` unless you inline it as a `data:` `@font-face` (see Fonts above).
54
+
55
+ ---
56
+
57
+ ## 2. Kinetic headline (word-by-word rise)
58
+
59
+ **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
60
+
61
+ Each word springs up in sequence. GSAP timeline, fully seekable, on the house stagger.
62
+
63
+ ```json
64
+ {
65
+ "asset": {
66
+ "type": "html5",
67
+ "html": "<div class=\"h\"><span>JUST</span> <span>DROPPED</span></div>",
68
+ "css": "html,body{margin:0;width:980px;height:420px;overflow:hidden;background:transparent;font-family:'Anton',system-ui,sans-serif}.h{display:flex;flex-wrap:wrap;gap:0 18px;align-items:center;justify-content:center;width:980px;height:420px;text-transform:uppercase;line-height:0.9}.h span{display:inline-block;font-size:150px;font-weight:800;color:#141414;transform:translateY(120%);opacity:0}",
69
+ "js": "const tl=gsap.timeline();gsap.utils.toArray('.h span').forEach((el,i)=>{tl.to(el,{y:0,opacity:1,duration:0.6,ease:'power3.out'},i*0.13)});tl.to({},{duration:1.5});"
70
+ },
71
+ "start": 0,
72
+ "length": 3,
73
+ "width": 980,
74
+ "height": 420,
75
+ "offset": { "x": 0, "y": 0.02 }
76
+ }
77
+ ```
78
+
79
+ Each word starts `translateY(120%)`, opacity 0; `0.6 s` rise on `power3.out`, words `0.13 s` apart (the house stagger); a trailing empty tween holds the title still. **Punchy variant:** this is the one "hero" spot where a pop is allowed — swap `ease:'power3.out'` for `ease:'back.out(1.4)'` for a gentle overshoot. Keep it to one headline per scene. `Anton` falls back to `system-ui` unless inlined.
80
+
81
+ ---
82
+
83
+ ## 3. Value reveal — bake and fade-in
84
+
85
+ **Category** data · **Use when** revealing a value, price, stat or metric · **Canvas** 620×220 · **Tags** number, price, stat, data · **Merge-friendly** target value
86
+
87
+ Bake the final value into the HTML and reveal it with opacity + blur + rise. The value is always present in the DOM — the animation controls only its visibility — so every captured frame shows the correct number. **Never use `onUpdate` to mutate `textContent`**: the seek harness doesn't fire `onUpdate` callbacks, so the value stays at its initial state (`$0`) in every frame.
88
+
89
+ ```json
90
+ {
91
+ "asset": {
92
+ "type": "html5",
93
+ "html": "<div class=\"wrap\"><span class=\"cur\">$</span><span class=\"n\">395</span></div>",
94
+ "css": "html,body{margin:0;width:620px;height:220px;overflow:hidden;background:transparent;font-family:system-ui,sans-serif}.wrap{display:flex;align-items:baseline;justify-content:center;width:620px;height:220px;color:#141414;font-weight:800;font-variant-numeric:tabular-nums;opacity:0;transform:translateY(16px);filter:blur(10px)}.cur{font-size:70px;margin-right:6px}.n{font-size:150px;letter-spacing:-2px}",
95
+ "js": "gsap.to('.wrap',{opacity:1,y:0,filter:'blur(0px)',duration:0.8,ease:'power3.out'});gsap.to({},{duration:1.5});"
96
+ },
97
+ "start": 0,
98
+ "length": 3,
99
+ "width": 620,
100
+ "height": 220,
101
+ "offset": { "x": 0, "y": -0.1 }
102
+ }
103
+ ```
104
+
105
+ `slow` (0.8 s) reveal on `power3.out` (a decelerating settle reads right for a value), then a `hold`. Swap `395` for any value at generation time; for thousands separators format the string before baking it (`$63,642` not `63642`). Pair with a static label on an adjacent `rich-text` track ("FROM", "AUD").
106
+
107
+ ---
108
+
109
+ ## 4. Shine / gloss sweep
110
+
111
+ **Category** graphics (emphasis) · **Use when** adding premium gloss to a product name, logo or card · **Canvas** 860×280 · **Tags** shine, gloss, emphasis, specular
112
+
113
+ A specular highlight slides across text — premium product gloss. *Continuous* motion (the sweep itself), so it uses `ease-in-out` over `slower` (1.0 s), not an entrance token. Pure CSS keyframe (WAAPI-seekable). Put this clip **above** the thing it shines on, or wrap the content in the same clip.
114
+
115
+ ```json
116
+ {
117
+ "asset": {
118
+ "type": "html5",
119
+ "html": "<div class=\"plate\"><div class=\"label\">UTOPIA</div><div class=\"shine\"></div></div>",
120
+ "css": "html,body{margin:0;width:860px;height:280px;overflow:hidden;background:transparent;font-family:'Anton',system-ui,sans-serif}.plate{position:relative;width:860px;height:280px;display:flex;align-items:center;justify-content:center;overflow:hidden}.label{font-size:200px;font-weight:800;letter-spacing:6px;color:#141414;text-transform:uppercase}.shine{position:absolute;top:0;left:0;width:240px;height:280px;background:linear-gradient(100deg,transparent,rgba(255,255,255,0.85),transparent);mix-blend-mode:overlay;transform:translateX(-300px) skewX(-12deg);animation:sweep 1s ease-in-out 0.4s 1 both}@keyframes sweep{to{transform:translateX(1100px) skewX(-12deg)}}",
121
+ "js": ""
122
+ },
123
+ "start": 0,
124
+ "length": 3,
125
+ "width": 860,
126
+ "height": 280,
127
+ "offset": { "x": 0, "y": 0 }
128
+ }
129
+ ```
130
+
131
+ `mix-blend-mode: overlay` makes the sweep read as a real highlight rather than a white bar. No JS needed. The `0.4 s` start delay lets the thing it shines on settle first.
132
+
133
+ ---
134
+
135
+ ## 5. Pulsing CTA button
136
+
137
+ **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`
138
+
139
+ A "SHOP NOW" pill with a soft breathing glow. *Ambient loop* — `ease-in-out`, its own 1.6 s cycle (exempt from the entrance tokens). CSS keyframes seek cleanly.
140
+
141
+ ```json
142
+ {
143
+ "asset": {
144
+ "type": "html5",
145
+ "html": "<div class=\"cta\">SHOP NOW</div>",
146
+ "css": "html,body{margin:0;width:620px;height:170px;overflow:hidden;background:transparent;font-family:system-ui,sans-serif}.cta{width:560px;height:128px;margin:21px auto;display:flex;align-items:center;justify-content:center;border-radius:16px;background:#D96B82;color:#fff;font-size:46px;font-weight:700;letter-spacing:4px;text-transform:uppercase;box-shadow:0 0 0 0 rgba(217,107,130,0.5);animation:pulse 1.6s ease-in-out 0s infinite both}@keyframes pulse{0%,100%{transform:scale(1);box-shadow:0 0 0 0 rgba(217,107,130,0.45)}50%{transform:scale(1.04);box-shadow:0 0 36px 8px rgba(217,107,130,0.35)}}",
147
+ "js": ""
148
+ },
149
+ "start": 0,
150
+ "length": 4,
151
+ "width": 620,
152
+ "height": 170,
153
+ "offset": { "x": 0, "y": -0.4 }
154
+ }
155
+ ```
156
+
157
+ The accent (`#D96B82`) is used here as the single earned colour. The clip is wider/taller than the pill so the glow has room. For a non-looping single pop, replace `infinite` with `1`.
158
+
159
+ ---
160
+
161
+ ## 6. Film-grain / texture overlay
162
+
163
+ **Category** atmosphere · **Use when** adding a moody analogue texture over a dark scene · **Canvas** 1080×1920 (full frame) · **Tags** grain, texture, overlay, atmosphere
164
+
165
+ A subtle moving grain over the whole frame. *Continuous drift* — `steps()`/`linear` is correct here (not an entrance). SVG `feTurbulence`, animated by shifting a slightly-oversized layer so it never reveals an edge. Size this one **to the canvas** and put it on a top track at low opacity.
166
+
167
+ ```json
168
+ {
169
+ "asset": {
170
+ "type": "html5",
171
+ "html": "<div class=\"grain\"></div>",
172
+ "css": "html,body{margin:0;width:1080px;height:1920px;overflow:hidden;background:transparent}.grain{position:absolute;top:-5%;left:-5%;width:110%;height:110%;opacity:0.12;background-image:url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='200' height='200' filter='url(%23n)'/></svg>\");background-size:240px 240px;animation:drift 0.6s steps(4) 0s infinite both}@keyframes drift{0%{transform:translate(0,0)}25%{transform:translate(-12px,8px)}50%{transform:translate(10px,-10px)}75%{transform:translate(-8px,-6px)}100%{transform:translate(0,0)}}",
173
+ "js": ""
174
+ },
175
+ "start": 0,
176
+ "length": 10,
177
+ "width": 1080,
178
+ "height": 1920,
179
+ "offset": { "x": 0, "y": 0 }
180
+ }
181
+ ```
182
+
183
+ A `data:` URI is fine **inside an html5 asset's CSS** (it's iframe content, not an `asset.src` the render workers fetch). Tune `opacity` (0.06–0.18) and `baseFrequency` (higher = finer grain). `steps(4)` gives a filmic stutter rather than smooth slide.
184
+
185
+ ---
186
+
187
+ ## Brand kit — re-skin every snippet at once
188
+
189
+ The snippets share one palette (ink `#141414`, accent `#D96B82`) so a set already looks coherent. To re-skin a whole edit to a brand in one place, lift the colours/font into top-level `merge[]` and reference the tokens in each clip's `html`, `css`, and `js` — `merge` find/replace runs over all three strings:
190
+
191
+ ```json
192
+ "merge": [
193
+ { "find": "ink", "replace": "#1A1A2E" },
194
+ { "find": "accent", "replace": "#E8552D" },
195
+ { "find": "font", "replace": "Anton, system-ui, sans-serif" }
196
+ ]
197
+ ```
198
+
199
+ 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. Keep the accent **earned** — a headline word, a number, a CTA, one glow; everything else neutral. (If you ship a snippet *without* a matching `merge[]` entry, leave the literal hex in — an undefined `{{token}}` renders as invalid CSS.)
200
+
201
+ ## Composing these
202
+
203
+ - 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.
204
+ - They don't overlap on a single track, so `shotstack validate <file>` stays clean. Run it before rendering.
205
+ - Reuse text via top-level `merge[]` (`{{title}}` in the HTML) — see the lower-third example in `html5.md`.
206
+ - Mix calm and punchy deliberately: a `blur-reveal` title, a `kinetic-headline` hero line, a `value-reveal` stat, a `shine` on the product, a pulsing CTA — all on the same tokens, so the set reads as one piece.
207
+ - Heavier motion = longer render. Preview in `shotstack studio <file>` before spending credits.
@@ -23,8 +23,8 @@ This is the **modern replacement for the deprecated `html` asset.** `html5` runs
23
23
  |---|---|---|---|
24
24
  | `type` | Yes | `"html5"` | Discriminator. |
25
25
  | `html` | Yes | string | Body markup. Supports merge fields (`{{title}}`). |
26
- | `css` | No | string | Stylesheet. Inlined into the iframe `<head>`. |
27
- | `js` | No | string | Script. Runs after libraries are preloaded. |
26
+ | `css` | No | string | Stylesheet. Inlined into the iframe `<head>`. Supports merge fields. |
27
+ | `js` | No | string | Script. Runs after libraries are preloaded. Supports merge fields. |
28
28
 
29
29
  Clip-level `width` and `height` set the iframe's pixel dimensions. They default to the edit's natural size.
30
30
 
@@ -56,7 +56,9 @@ These cover ~95% of motion-graphics use cases. **You can't load other libraries
56
56
 
57
57
  ## The browser harness (deterministic auto-seek)
58
58
 
59
- 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 an `onUpdate` tween (see the count-up snippet).
59
+ 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
+
61
+ **`onUpdate` callbacks do not fire under seek.** The harness seeks the GSAP timeline to each frame's timestamp without playing through, so `onUpdate` handlers are not invoked. Any DOM mutation made inside an `onUpdate` callback (`textContent`, `innerHTML`, class swaps, attribute changes) will not appear in the rendered video — the element stays at its initial state. **Animate CSS properties only** (opacity, transform, filter, scale) — those are applied directly by GSAP's seek. To display a value, bake it into the HTML at generation time and reveal it with an opacity/transform tween.
60
62
 
61
63
  **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`.
62
64
 
@@ -156,7 +158,7 @@ Slide-in name + role bar with subtle accent. **Clip sized to the bar (560×120),
156
158
  - **Clip is the size of the bar, not the canvas** — placement is one `offset` change.
157
159
  - **`html, body, .bar` all 560×120.** No absolute positioning inside the iframe — the bar IS the iframe content.
158
160
  - One GSAP timeline drives every animation.
159
- - Merge fields (`{{name}}`, `{{role}}`) in the HTML, populated by **top-level** `merge[]` (sibling of `timeline`/`output`, NOT a clip property). Keeps the asset reusable.
161
+ - Merge fields (`{{name}}`, `{{role}}`) in the HTML, populated by **top-level** `merge[]` (sibling of `timeline`/`output`, NOT a clip property). Keeps the asset reusable. Merge fields also resolve in `css` and `js` — use `{{accent}}` in CSS for brand colours, or `{{targetValue}}` inside a JS string literal for data-driven animation targets.
160
162
  - A trailing `.to({}, { duration: 3.5 })` holds the final state before the clip ends.
161
163
 
162
164
  ## Worked example: animated bar chart (D3 + GSAP)
@@ -236,6 +238,7 @@ The same pattern scales to scene transitions (each scene is a `<section>` with i
236
238
 
237
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.
238
240
  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. `shotstack validate` catches JS syntax errors offline; runtime errors (e.g. referencing a DOM element that doesn't exist) are silent. If a clip is blank, check the JS first: run `node --check` on the string, or wrap suspect code in `try/catch` to surface the error.
239
242
 
240
243
  ## When to use `html5` vs `rich-text`/`svg`
241
244
 
@@ -0,0 +1,151 @@
1
+ # Motion language — the house vocabulary
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.
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.
6
+
7
+ ## The tokens
8
+
9
+ ### Duration scale (seconds)
10
+
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.
21
+
22
+ ### Easing — one house curve, no overshoot by default
23
+
24
+ | Where | Entrance / settle | Exit |
25
+ |---|---|---|
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, …`).
36
+
37
+ ### Travel & restraint
38
+
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.
42
+
43
+ ## The three motion layers
44
+
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.
46
+
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 |
52
+
53
+ ### Layer 2 — rich-text animation, tokenised
54
+
55
+ `animation.preset` maps onto the choreography names; set `duration` from the scale:
56
+
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" }` |
64
+
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).
66
+
67
+ ## Choreography recipes (html5 / GSAP)
68
+
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.
70
+
71
+ ```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' });
74
+
75
+ // entryFade — presence only (overlays, avatars)
76
+ gsap.from('.el', { opacity: 0, duration: 0.6, ease: 'power3.out' });
77
+
78
+ // entryScale — calm scale-up
79
+ gsap.from('.el', { opacity: 0, scale: 0.92, duration: 0.6, ease: 'power3.out' });
80
+
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' });
83
+
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 });
86
+
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)' });
89
+
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
+ ```
93
+
94
+ **Entrance → hold → exit, on one timeline:**
95
+
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)
101
+ ```
102
+
103
+ ### CSS equivalents (no JS)
104
+
105
+ ```css
106
+ /* house ease entrance */
107
+ .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; } }
109
+
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}
112
+ ```
113
+
114
+ ## Brand kit — set the palette once, re-skin everything
115
+
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:
117
+
118
+ ```json
119
+ "merge": [
120
+ { "find": "ink", "replace": "#141414" },
121
+ { "find": "accent", "replace": "#D96B82" },
122
+ { "find": "bg", "replace": "#08080A" },
123
+ { "find": "font", "replace": "Clash Display, system-ui, sans-serif" }
124
+ ]
125
+ ```
126
+
127
+ ```css
128
+ .title { color: {{ink}}; font-family: {{font}}; }
129
+ .cta { background: {{accent}}; }
130
+ ```
131
+
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
+ ## See also
148
+
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)
@@ -6,12 +6,13 @@ Common errors and the fix for each. **Catch most of them before rendering:** `sh
6
6
 
7
7
  - "Unknown property: alignment" / wrong property names
8
8
  - "Invalid option: expected one of top|middle|bottom"
9
- - Video looks stretched / squished after rendering
9
+ - Video looks stretched / squishing after rendering
10
10
  - "Font not found"
11
11
  - Captions cover the whole frame
12
12
  - Clips flicker / "clips overlap" on one track
13
13
  - Timeline renders but layers are wrong
14
14
  - "Invalid asset URL"
15
+ - html5 clip is blank but render succeeded
15
16
  - "Render failed" with no other detail
16
17
  - Render takes much longer than expected
17
18
  - Credits exhausted on stage environment
@@ -82,6 +83,18 @@ The `src` is not a public HTTPS URL, or it's a `data:` URI, or it's a local path
82
83
 
83
84
  **Fix:** host the asset at a public HTTPS URL or use a presigned URL with credentials in the URL itself. For tests, pull from `references/asset-library.md`.
84
85
 
86
+ ## html5 clip is blank but render succeeded
87
+
88
+ An `html5` clip renders as a completely blank frame, but the render reports `status: "done"` with no error. This is a **silent failure** — there is no feedback loop.
89
+
90
+ Two causes:
91
+
92
+ 1. **JS syntax error in `asset.js`.** A missing semicolon, unbalanced brace, or any other parse error crashes the entire script before any animation runs. `shotstack validate <file>` catches this offline — it runs each `asset.js` string through a syntax check and reports the error with the clip path. **Always validate before rendering.**
93
+
94
+ 2. **JS runtime error.** Referencing a DOM element that doesn't exist (`document.getElementById("missing")`), calling a method on `null`, or any other uncaught exception — the script stops mid-execution and the clip stays in its initial state (typically invisible if elements start at `opacity:0`). These are not caught by `validate` (they require the runtime DOM). To debug, wrap suspect code in `try/catch` and log to a visible element, or simplify the JS to isolate the failing line.
95
+
96
+ A third variant: values stuck at `$0` or their initial state. This happens when `onUpdate` callbacks are used to mutate `textContent` — the seek harness doesn't fire `onUpdate`, so the DOM never updates. Bake final values into the HTML and animate opacity/transform instead. See `references/html5.md` → "The browser harness".
97
+
85
98
  ## "Render failed" with no other detail
86
99
 
87
100
  `shotstack status <id> --output json` returns the full response including the error message. The `--watch` text view truncates.