@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.
@@ -0,0 +1,307 @@
1
+ # Shotstack Agent Core
2
+
3
+ Universal Edit JSON authoring conventions. Read this **before composing any Edit JSON**. The conventions agents most often get wrong are listed here once. The same file ships with both the Shotstack CLI skill and the Shotstack MCP server.
4
+
5
+ ## Before composing JSON: check the schema
6
+
7
+ Don't invent property names or enum values. The Shotstack schema is published — fetch one of these before composing JSON from scratch:
8
+
9
+ - <https://shotstack.io/docs/api/api.edit.json> — single-file OpenAPI Schema. Machine-validatable; load it once and validate locally instead of round-tripping the API.
10
+ - <https://shotstack.io/docs/api/> — interactive HTML reference. Fastest for human scanning.
11
+ - <https://shotstack.io/docs/guide/llms-full.txt> — single-file LLM-friendly version of the full guide + reference.
12
+ - <https://github.com/shotstack/oas-api-definition/tree/main/schemas> — raw OpenAPI YAML, source of truth.
13
+
14
+ CSS naming conventions (`alignment`, `vertical: "center"`) **do not** apply. The spec uses precise names that often differ from web/CSS instincts:
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 |
25
+
26
+ When the API rejects a property, the error message names the field — fix and retry. Don't guess twice.
27
+
28
+ ## Track ordering is REVERSED
29
+
30
+ `timeline.tracks` is an array. **The first element (`tracks[0]`) is the TOP layer; the last element is the BOTTOM layer.** This is opposite to most z-index conventions.
31
+
32
+ > "Tracks are layered on top of each other in the same order they are added to the array with the top most track layered over the top of those below it." — [Shotstack docs](https://shotstack.io/docs/guide/architecting-an-application/guidelines/)
33
+
34
+ Practical rule: **put captions, overlays, and titles in early tracks; put video/image backgrounds in later tracks.**
35
+
36
+ ```json
37
+ {
38
+ "timeline": {
39
+ "tracks": [
40
+ { "clips": [/* TOP — captions */] },
41
+ { "clips": [/* MIDDLE — title overlay */] },
42
+ { "clips": [/* BOTTOM — background video */] }
43
+ ]
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Smart clip strings
49
+
50
+ These string values are accepted in addition to numbers:
51
+
52
+ | Where | Value | Meaning |
53
+ |---|---|---|
54
+ | `clip.start` | `"auto"` | Start when the previous clip on the same track finishes |
55
+ | `clip.start` | `"alias://<name>"` | Inherit start from another clip with `alias: "<name>"` |
56
+ | `clip.length` | `"auto"` | Asset's natural duration. Use for foreground (video, voiceover, scene). |
57
+ | `clip.length` | `"end"` | Until timeline ends, capped at asset duration. Use for background (music, captions, watermark). |
58
+ | `clip.length` | `"alias://<name>"` | Inherit length from another clip |
59
+
60
+ `"end"` does NOT loop short audio — use a numeric `length` if you need precise control.
61
+
62
+ The `alias://` protocol is also used in `rich-caption` `src` to auto-transcribe a referenced audio/video clip — see `references/caption.md`.
63
+
64
+ ## Public HTTPS URLs only
65
+
66
+ All asset `src` URLs must be publicly accessible HTTPS. **No local file paths, no `data:` URIs, no signed URLs that expire mid-render.** The render workers fetch assets from the public internet.
67
+
68
+ For test renders without your own assets, use the placeholder library at <https://shotstack-assets.s3.amazonaws.com/> — see `references/asset-library.md`.
69
+
70
+ ## Don't overlap clips on the same track
71
+
72
+ Clips on the same track must not have overlapping `start`/`length` ranges — overlapping clips flicker because the engine can't decide which to display. **Anything visible at the same time goes on a separate track.** This is the most common structural mistake.
73
+
74
+ ```jsonc
75
+ // WRONG — three simultaneous end-card lines on ONE track (all overlap 8.0–11.0)
76
+ { "clips": [
77
+ { "asset": { "type": "rich-text", "text": "TITLE" }, "start": 8.0, "length": 3 },
78
+ { "asset": { "type": "rich-text", "text": "$395" }, "start": 8.0, "length": 3 },
79
+ { "asset": { "type": "rich-text", "text": "SHOP NOW" }, "start": 8.0, "length": 3 }
80
+ ] }
81
+
82
+ // RIGHT — one track each
83
+ "tracks": [
84
+ { "clips": [ { "asset": { "type": "rich-text", "text": "TITLE" }, "start": 8.0, "length": 3 } ] },
85
+ { "clips": [ { "asset": { "type": "rich-text", "text": "$395" }, "start": 8.0, "length": 3 } ] },
86
+ { "clips": [ { "asset": { "type": "rich-text", "text": "SHOP NOW" }, "start": 8.0, "length": 3 } ] }
87
+ ]
88
+ ```
89
+
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
+
92
+ **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.
93
+
94
+ ## Clip motion & rich-text fields (cheatsheet)
95
+
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.)
97
+
98
+ **Clip-level (wraps any asset):**
99
+
100
+ | Field | Values |
101
+ |---|---|
102
+ | `fit` | `crop` (fill + crop, default — CSS `object-fit: cover`) · `contain` (letterbox) · `cover` (stretch, ignores aspect) · `none` |
103
+ | `position` | `center` `top` `bottom` `left` `right` `topLeft` `topRight` `bottomLeft` `bottomRight` |
104
+ | `offset` | `{ "x": <−1..1>, "y": <−1..1> }` — fraction of the canvas; **`y` positive = up** |
105
+ | `scale` / `opacity` | number (`1` = the fit result) / `0..1` |
106
+ | `effect` | Ken-Burns drift: `zoomIn` `zoomOut` `slideLeft` `slideRight` `slideUp` `slideDown` — each also `…Slow` / `…Fast` |
107
+ | `filter` | `blur` `boost` `contrast` `darken` `greyscale` `lighten` `muted` `negative` |
108
+ | `transition` | `{ "in": …, "out": … }` ↓ |
109
+
110
+ **`transition.in` / `.out`** — each also takes a `Slow`/`Fast` suffix (e.g. `fadeSlow`, `slideUpFast`):
111
+ `none` `fade` `reveal` `wipeLeft` `wipeRight` `slideLeft` `slideRight` `slideUp` `slideDown` `carouselLeft` `carouselRight` `carouselUp` `carouselDown` `shuffle*` (eight corners, e.g. `shuffleTopRight`) `zoom`.
112
+
113
+ **`rich-text` asset** — the styled-text workhorse (use instead of `text`/`title`):
114
+
115
+ ```json
116
+ {
117
+ "type": "rich-text",
118
+ "text": "UTOPIA",
119
+ "font": { "family": "<loaded-family>", "size": 160, "weight": "700", "color": "#141414", "opacity": 1 },
120
+ "style": { "letterSpacing": 6, "lineHeight": 0.95, "textTransform": "uppercase", "textDecoration": "none" },
121
+ "stroke": { "width": 3, "color": "#000000" },
122
+ "shadow": { "offsetX": 0, "offsetY": 6, "blur": 18, "color": "#000000", "opacity": 0.4 },
123
+ "background": { "color": "#ffffff", "opacity": 1, "borderRadius": 16 },
124
+ "align": { "horizontal": "center", "vertical": "middle" },
125
+ "animation": { "preset": "fadeIn", "duration": 0.6, "style": "word", "direction": "up" }
126
+ }
127
+ ```
128
+
129
+ `align.horizontal` = `left|center|right`; `align.vertical` = `top|middle|bottom` (**not** `center`). `animation.preset` = `fadeIn` `slideIn` `typewriter` `ascend` `shift` `movingLetters`; `animation.style` = `character|word`; `animation.direction` = `left|right|up|down`. Give the clip a `width`/`height` box so text wraps and aligns where you expect.
130
+
131
+ 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).
132
+
133
+ ## Motion language (house tokens)
134
+
135
+ 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).
136
+
137
+ - **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`).
138
+ - **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.
139
+ - **One stagger:** `0.13s` between siblings (GSAP `stagger: 0.13`; CSS `animation-delay` steps).
140
+ - **Restraint:** translate 12–24 px (not 80), scale ≥ 0.92, one focal element per moment, let a reveal settle (`hold`) before it cuts.
141
+ - **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`).
142
+ - **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.
143
+
144
+ ## Positioning & coordinates
145
+
146
+ `position` picks one of nine anchor points (`center` default; `top` `bottom` `left` `right` `topLeft` `topRight` `bottomLeft` `bottomRight`); `offset` nudges from there. **`offset` is a fraction of the output frame, not a centred −1..+1 grid:** `offset.x` positive → right (× frame width), `offset.y` positive → **up** (× frame height). Range is ±10; anything past ±1 pushes the clip off-frame.
147
+
148
+ Clip-level `width`/`height` (pixels) define a bounding box. For `image`/`video`, `fit` fills it — `crop` (keep aspect, crop overflow) · `contain` (letterbox) · `cover` (**stretch, distorts**) · `none`. For `rich-text`, that same clip box sets the text-wrap width and the area `align` positions within — **size text on the clip, not the asset.** Without `width`/`height` a clip fills the frame, so unsized text centres across the whole output. `scale` then multiplies the result (uniform on both axes).
149
+
150
+ Order of operations: fit → position → offset → rotate → scale. Full reference: [`references/positioning.md`](../references/positioning.md).
151
+
152
+ ## Output resolution
153
+
154
+ Pick `output.resolution` (preset) OR `output.size.width`+`output.size.height` (custom):
155
+
156
+ | Preset | Pixels @ fps |
157
+ |---|---|
158
+ | `preview` | 512×288 @ 15 |
159
+ | `mobile` | 640×360 @ 25 |
160
+ | `sd` | 1024×576 @ 25 |
161
+ | `hd` | 1280×720 @ 25 (default) |
162
+ | `1080` | 1920×1080 @ 25 |
163
+
164
+ Custom sizes must be divisible by 2.
165
+
166
+ ## Merge fields
167
+
168
+ Top-level `merge[]` (sibling of `timeline`/`output`, NOT a clip property) is a simple find-and-replace that runs over the entire Edit JSON **before** any asset is processed. Each entry is `{ "find": "<TOKEN>", "replace": "<value>" }` — the `find` value is the token name **without** braces; every `{{TOKEN}}` occurrence in any string throughout the Edit is swapped for the value.
169
+
170
+ ```json
171
+ {
172
+ "merge": [
173
+ { "find": "TITLE", "replace": "Q4 Results" },
174
+ { "find": "REVENUE", "replace": "$1,247,890" },
175
+ { "find": "ACCENT", "replace": "#22D3EE" }
176
+ ],
177
+ "timeline": {
178
+ "tracks": [{
179
+ "clips": [{
180
+ "asset": { "type": "rich-text", "text": "{{TITLE}}", "font": { "family": "Roboto", "size": 60, "color": "{{ACCENT}}" } },
181
+ "start": 0, "length": 3
182
+ }]
183
+ }]
184
+ },
185
+ "output": { "format": "mp4", "resolution": "1080" }
186
+ }
187
+ ```
188
+
189
+ **Rules:**
190
+ - Whitespace inside braces is ignored: `{{ TITLE }}` and `{{TITLE}}` are the same token.
191
+ - Token names are **case sensitive**. Use `UPPER_SNAKE_CASE` (the Shotstack convention).
192
+ - The `{{` delimiter cannot be changed.
193
+ - One `merge[]` entry can replace multiple `{{TOKEN}}` occurrences across the entire Edit.
194
+ - The replacement is a dumb string swap — no conditionals, no loops, no escaping.
195
+
196
+ **Where merge fields resolve:** every string value in the Edit JSON — `rich-text` `text` and `font.color`; `html5` `html`, `css`, and `js`; `svg` markup; `asset.src` URLs; `clip.transition` names; any string anywhere. This is an Edit-wide capability, not an html5 feature.
197
+
198
+ **When to use merge vs baking at generation time:**
199
+
200
+ | Scenario | Use |
201
+ |---|---|
202
+ | Reusable template rendered many times with different data (reports, personalised videos, per-user wraps) | `merge[]` — keep one Edit JSON, swap the `merge[]` per render |
203
+ | Brand re-skinning (palette, font, accent across every clip) | `merge[]` — change one `merge[]` entry to re-skin the whole video |
204
+ | One-off render or data unique to a single video | Bake values directly into the asset at generation time — simpler, no merge indirection |
205
+ | Generating multiple edits programmatically (e.g. one per user from a CSV) | Either — merge keeps the template reusable, baking is more direct. If the template won't be reused, bake. |
206
+
207
+ **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.
208
+
209
+ **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.
210
+
211
+ ## Asset types
212
+
213
+ Use only the **current** asset types; the deprecated ones still parse but should not be used in new templates.
214
+
215
+ ### Current
216
+
217
+ | Type | Purpose |
218
+ |---|---|
219
+ | `video` | Video file (mp4, mov, webm). |
220
+ | `image` | Static image — `jpg`, `png`, `webp`, `gif`, `bmp`, `tiff`. |
221
+ | `audio` | Audio clip placed at a specific time on the timeline. |
222
+ | `rich-text` | Styled text overlay with full typography control. **Use this instead of `text`/`html`/`title`.** |
223
+ | `svg` | Vector graphics from raw SVG markup. See `references/svg.md`. |
224
+ | `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.** |
225
+ | `rich-caption` | Word-level animated captions sourced from audio, video, or subtitle files. See `references/caption.md`. |
226
+ | `luma` | Luma matte for masking effects. |
227
+ | `image-to-video` | **AI**: animate a still image into a short video clip. Billed per generation. |
228
+ | `text-to-image` | **AI**: generate an image from a text prompt. Billed per generation. |
229
+ | `text-to-speech` | **AI**: generate speech from a text prompt. Billed per generation. |
230
+
231
+ `timeline.fonts[]` is a separate field for custom font URLs (not an asset type).
232
+
233
+ For background music, **use an `audio` asset on its own track** with `length: "end"`. Do NOT use `timeline.soundtrack` — it is deprecated. The audio asset path supports keyframes, custom timing, fades, and effects; soundtrack does not.
234
+
235
+ ### Deprecated — do not use
236
+
237
+ `text`, `title`, `caption`, `html`, `shape`. They still parse but produce inferior output. Replace with:
238
+
239
+ | If you'd use… | Use instead |
240
+ |---|---|
241
+ | `text` or `title` | `rich-text` |
242
+ | `caption` | `rich-caption` |
243
+ | `html` | `html5` (for motion graphics or animated overlays) or `rich-text` (for static styled text) |
244
+ | `shape` | `svg` with `<rect>`, `<circle>`, `<polygon>` etc. |
245
+ | `timeline.soundtrack` | `audio` asset on its own track with `length: "end"` |
246
+
247
+ ### AI-generated assets
248
+
249
+ `image-to-video`, `text-to-speech`, and `text-to-image` are billed per generation **even when invoked through the sandbox stage endpoint** (which is otherwise free). They are async — the render submits the AI job and waits. Renders containing AI assets take longer.
250
+
251
+ ## The design ladder — escalate from rich-text to html5
252
+
253
+ 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:
254
+
255
+ | Level | Asset | Use for |
256
+ |---|---|---|
257
+ | 1 — type & layout | `rich-text` | Titles, lower-thirds, kickers, captions, price/CTA pills. Fast and reliable; the right default for static styled text. |
258
+ | 2 — shapes | `svg` | Colour panels, rules, badges, frames, geometric accents behind or around type. |
259
+ | 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). |
260
+
261
+ **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.
262
+
263
+ ## Fonts
264
+
265
+ Use **custom Google Fonts via `timeline.fonts[]`**. System fonts (`Arial`, `Helvetica`, `Times New Roman`) are NOT installed and will fail with "Font not found".
266
+
267
+ **CRITICAL: Do NOT construct or fabricate Google Fonts URLs from memory.** Google rotates them (`v26 → v31 → ...`) and the hash filenames change with each version. Any URL you reconstruct from training data is almost certainly a 404. **Use ONLY the verified entries below, copied verbatim.**
268
+
269
+ See `references/fonts.md` for how to source fonts from the Studio SDK catalogue and the built-in fallback fonts.
270
+
271
+ ### Usage example
272
+
273
+ ```json
274
+ {
275
+ "timeline": {
276
+ "fonts": [
277
+ { "src": "https://fonts.gstatic.com/s/montserrat/v31/JTUSjIg1_i6t8kCHKm45xW5rygbi49c.ttf" }
278
+ ],
279
+ "tracks": [
280
+ {
281
+ "clips": [{
282
+ "asset": {
283
+ "type": "rich-text",
284
+ "text": "Hello",
285
+ "font": { "family": "JTUSjIg1_i6t8kCHKm45xW5rygbi49c", "size": 60, "weight": "700", "color": "#ffffff" }
286
+ },
287
+ "start": 0, "length": 3
288
+ }]
289
+ }
290
+ ]
291
+ }
292
+ }
293
+ ```
294
+
295
+ The `font.family` value MUST match the `family` column in the table above (it's the filename basename without `.ttf`). If `family` and the URL's basename don't match, the font will not load.
296
+
297
+ ## Top 5 mistakes
298
+
299
+ 1. **Reverse track order.** `tracks[0]` is the TOP layer, not the bottom. Captions go in early tracks; backgrounds go in late tracks.
300
+ 2. **System fonts.** `Arial`, `Helvetica`, `Times New Roman`, etc. are not installed. Use Google Fonts via `timeline.fonts[]` (preferred) or one of the built-in fonts in `references/fonts.md`.
301
+ 3. **Captions fill the whole frame.** A `rich-caption` clip without `width`, `height`, and `fit: "none"` covers the entire output. Use a named preset from `references/caption.md`.
302
+ 4. **`<text>` inside an SVG asset.** Raw `<text>` is unsupported. Use a `rich-text` asset for any text content; reserve SVG for shapes only.
303
+ 5. **Composing custom caption styles when presets exist.** The five named presets (Nico, Kai, Kapow, Lovely Little Lychee, Rizz) cover the common styles. Use one verbatim from `references/caption.md` unless the user asks for something specific.
304
+
305
+ ## Per-topic deep dives
306
+
307
+ 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`.
@@ -1,145 +0,0 @@
1
- # HTML5 snippet pack — drop-in motion graphics
2
-
3
- Copy-paste `html5` clips that "pop" — kinetic type, count-ups, 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
- Read [`html5.md`](html5.md) first for the rules these obey. The non-negotiables:
8
-
9
- - **Seekable animation only.** GSAP timelines, GSAP tweens (incl. `onUpdate`), anime.js, Lottie, or CSS `@keyframes`. **Never** `setTimeout`/`setInterval`/`requestAnimationFrame`/`Date.now()`/`gsap.call()` — the renderer seeks by absolute time, it doesn't play.
10
- - **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`/`%`.
11
- - **The body is transparent by default** — the clip composites over the layers below (only set an opaque background if you want one).
12
- - **No `<canvas>`** — Studio capture serialises the DOM; canvas bitmaps come through empty. Use SVG or positioned DOM.
13
- - **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.
14
- - **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`.
15
-
16
- Coordinates below assume a **1080×1920 vertical** canvas; adjust `offset` for other sizes. Colours use a neutral ink/accent — swap for your palette.
17
-
18
- ---
19
-
20
- ## 1. Kinetic headline (word-by-word rise)
21
-
22
- Big title where each word springs up in sequence. GSAP timeline, fully seekable.
23
-
24
- ```json
25
- {
26
- "asset": {
27
- "type": "html5",
28
- "html": "<div class=\"h\"><span>JUST</span> <span>DROPPED</span></div>",
29
- "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}",
30
- "js": "const tl=gsap.timeline();gsap.utils.toArray('.h span').forEach((el,i)=>{tl.to(el,{y:0,opacity:1,duration:0.6,ease:'back.out(1.7)'},i*0.18)});tl.to({},{duration:1.2});"
31
- },
32
- "start": 0,
33
- "length": 3,
34
- "width": 980,
35
- "height": 420,
36
- "offset": { "x": 0, "y": 0.02 }
37
- }
38
- ```
39
-
40
- Each word is a `<span>` starting `translateY(120%)`; the trailing empty tween holds the title still before the clip ends. `Anton` falls back to `system-ui` unless you inline it (see Fonts above): `@font-face{font-family:'Anton';src:url('data:font/woff2;base64,…') format('woff2')}`.
41
-
42
- ---
43
-
44
- ## 2. Count-up number / price odometer
45
-
46
- Animate a value from 0 to its target. Seek-safe because the count lives in a **tweened object with `onUpdate`** (fires on seek), never a timer.
47
-
48
- ```json
49
- {
50
- "asset": {
51
- "type": "html5",
52
- "html": "<div class=\"wrap\"><span class=\"cur\">$</span><span id=\"n\">0</span></div>",
53
- "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}.cur{font-size:70px;margin-right:6px}#n{font-size:150px;letter-spacing:-2px}",
54
- "js": "const o={v:0};const out=document.getElementById('n');const tl=gsap.timeline();tl.to(o,{v:395,duration:1.4,ease:'power2.out',onUpdate:()=>{out.textContent=Math.round(o.v)}});tl.to({},{duration:1.0});"
55
- },
56
- "start": 0,
57
- "length": 3,
58
- "width": 620,
59
- "height": 220,
60
- "offset": { "x": 0, "y": -0.1 }
61
- }
62
- ```
63
-
64
- Swap `v:395` for any target. For thousands separators use `Math.round(o.v).toLocaleString()`. Pair with a static label on an adjacent `rich-text` track ("FROM", "AUD").
65
-
66
- ---
67
-
68
- ## 3. Shine / gloss sweep
69
-
70
- A specular highlight that slides across text or a card — premium product gloss. Pure CSS keyframe (WAAPI-seekable). Put this clip **above** the thing it shines on, or wrap the content in the same clip.
71
-
72
- ```json
73
- {
74
- "asset": {
75
- "type": "html5",
76
- "html": "<div class=\"plate\"><div class=\"label\">UTOPIA</div><div class=\"shine\"></div></div>",
77
- "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 2.4s ease-in-out 0.4s 1 both}@keyframes sweep{to{transform:translateX(1100px) skewX(-12deg)}}",
78
- "js": ""
79
- },
80
- "start": 0,
81
- "length": 3,
82
- "width": 860,
83
- "height": 280,
84
- "offset": { "x": 0, "y": 0 }
85
- }
86
- ```
87
-
88
- `mix-blend-mode: overlay` makes the sweep read as a real highlight rather than a white bar. No JS needed.
89
-
90
- ---
91
-
92
- ## 4. Pulsing CTA button
93
-
94
- A "SHOP NOW" pill with a soft breathing glow — draws the eye on an end card. CSS keyframes loop and seek cleanly.
95
-
96
- ```json
97
- {
98
- "asset": {
99
- "type": "html5",
100
- "html": "<div class=\"cta\">SHOP NOW</div>",
101
- "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:#5c3a21;color:#fff;font-size:46px;font-weight:700;letter-spacing:4px;text-transform:uppercase;box-shadow:0 0 0 0 rgba(92,58,33,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(92,58,33,0.45)}50%{transform:scale(1.04);box-shadow:0 0 36px 8px rgba(92,58,33,0.35)}}",
102
- "js": ""
103
- },
104
- "start": 0,
105
- "length": 4,
106
- "width": 620,
107
- "height": 170,
108
- "offset": { "x": 0, "y": -0.4 }
109
- }
110
- ```
111
-
112
- The clip is wider/taller than the pill so the glow has room (`overflow:hidden` would otherwise clip it). For a non-looping single pop, replace `infinite` with `1`.
113
-
114
- ---
115
-
116
- ## 5. Film-grain / texture overlay
117
-
118
- A subtle moving grain over the whole frame — adds a moody, analogue feel (great over dark scenes). 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.
119
-
120
- ```json
121
- {
122
- "asset": {
123
- "type": "html5",
124
- "html": "<div class=\"grain\"></div>",
125
- "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)}}",
126
- "js": ""
127
- },
128
- "start": 0,
129
- "length": 10,
130
- "width": 1080,
131
- "height": 1920,
132
- "offset": { "x": 0, "y": 0 }
133
- }
134
- ```
135
-
136
- 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.
137
-
138
- ---
139
-
140
- ## Composing these
141
-
142
- - 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.
143
- - They don't overlap on a single track, so `shotstack validate <file>` stays clean. Run it before rendering.
144
- - Reuse text via top-level `merge[]` (`{{title}}` in the HTML) — see the lower-third example in `html5.md`.
145
- - Heavier motion = longer render. Preview in `shotstack studio <file>` before spending credits.