Skip to content

Commit 5ce3489

Browse files
committed
docs: remove custom animation engine overrides and clarify seekable patterns
1 parent 8cf343f commit 5ce3489

2 files changed

Lines changed: 13 additions & 64 deletions

File tree

references/html5-snippets.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Big title where each word springs up in sequence. GSAP timeline, fully seekable.
3737
}
3838
```
3939

40-
Each word is a `<span>` starting `translateY(120%)`. The trailing empty tween is a hold so the title sits still before the clip ends. The CSS names `Anton`, but custom fonts must be inlined — `timeline.fonts[]` doesn't apply inside `html5` and remote `@font-face` is CSP-blocked — so without an inline `data:` `@font-face` it renders in the `system-ui` fallback. To get `Anton`: `@font-face{font-family:'Anton';src:url('data:font/woff2;base64,…') format('woff2')}`.
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')}`.
4141

4242
---
4343

references/html5.md

Lines changed: 12 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
The `html5` asset type renders a self-contained HTML/CSS/JS page inside a video clip. Use it for animated overlays, data visualisations, motion graphics, and anything you'd build as a tiny single-page web app.
44

5-
This is the **modern replacement for the deprecated `html` asset.** `html5` runs in a real browser iframe with a JS runtime, library preloads, and deterministic frame capture — the old `html` asset only rendered static markup.
5+
This is the **modern replacement for the deprecated `html` asset.** `html5` runs in a real browser iframe with a JS runtime, library preloads, and deterministic frame capture — the old `html` asset should never be used.
66

77
## Contents
88

@@ -13,7 +13,7 @@ This is the **modern replacement for the deprecated `html` asset.** `html5` runs
1313
- Sizing
1414
- Worked example: animated lower-third (GSAP)
1515
- Worked example: animated bar chart (D3 + GSAP)
16-
- Author overrides for custom animation engines
16+
- Worked example: 10-second countdown (pure CSS)
1717
- Common mistakes
1818
- When to use `html5` vs `rich-text`/`svg`
1919

@@ -48,39 +48,23 @@ Clip-level `width` and `height` set the iframe's pixel dimensions. They default
4848
Four libraries are always available — no `<script src=>` tags needed:
4949

5050
- **GSAP** (`window.gsap`) — primary animation library. Use timelines (`gsap.timeline()`) over loose tweens; the harness seeks timelines correctly.
51-
- **anime.js** (`window.anime`) — alternative animation library. Every instance is auto-tracked via `__shotstackAnimeInstances` (catches `autoplay: false` instances that `anime.running` misses).
51+
- **anime.js** (`window.anime`) — alternative animation library.
5252
- **D3** (`window.d3`) — for data binding, scales, and SVG/DOM construction. Pair with GSAP for the actual animation; D3's transitions work too but GSAP is more reliable under seek.
5353
- **Lottie** (`window.lottie`) — Bodymovin JSON player (SVG-renderer build; `renderer: "canvas"` is unavailable).
5454

55-
These cover ~95% of motion-graphics use cases. **You can't load other libraries via `<script src=>`** — the iframe's CSP blocks all external scripts and network access (see *Sandbox restrictions* below). GSAP, anime.js and Lottie are the animation engines the seek harness drives; D3 just builds the DOM/data (animate it with GSAP). Capture is a static seek at the edit's frame rate (`output.fps`, default 30).
55+
These cover ~95% of motion-graphics use cases. **You can't load other libraries via `<script src=>`** — the iframe's CSP blocks all external scripts and network access (see *Sandbox restrictions* below).
5656

5757
## The browser harness (deterministic auto-seek)
5858

59-
When a clip plays, the SDK captures frames by seeking the iframe's animation state to specific timestamps and rasterising each frame. This is **deterministic**: the same input always produces the same output, regardless of how long real-time playback would take.
60-
61-
The harness is injected automatically. It installs one `window` function you do **not** write yourself:
62-
63-
- `__shotstackSeek(timeMs)` — seeks every detected animation source to `timeMs`.
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).
6460

6561
**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`.
6662

67-
**What gets seeked automatically:**
68-
69-
| Source | How it's driven |
70-
|---|---|
71-
| GSAP timelines (`gsap.timeline()`) | `tl.pause()` + `tl.seek(seconds)` on every descendant timeline. |
72-
| GSAP loose tweens | `gsap.globalTimeline.seek(seconds)`. |
73-
| anime.js instances | `instance.pause()` + `instance.seek(timeMs)` on every tracked instance. |
74-
| Lottie animations | `anim.goToAndStop(timeMs, false)` on every registered animation. |
75-
| CSS `@keyframes`, transitions, `Element.animate(...)` | `document.getAnimations()` → set `currentTime` on each. |
76-
77-
**Practical consequence:** write your animation as you would for a normal web page. Use GSAP timelines, anime.js, Lottie, or CSS — the harness drives time. Don't rely on `setTimeout`, `requestAnimationFrame` loops, or `Date.now()` for animation timing — none of those are seekable.
78-
7963
## Sandbox restrictions: no network, inline everything
8064

8165
The iframe renders under a strict Content-Security-Policy (`default-src 'none'`), in both Studio preview and cloud render. Nothing is fetched from the network at render time:
8266

83-
- **No external `<script src=>`.** Only the bundled GSAP/anime.js/D3/Lottie run (they're inlined for you). Another library would have to be inlined into your `js` — and must be seekable (see *Author overrides*).
67+
- **No external `<script src=>`.** Only the bundled GSAP/anime.js/D3/Lottie run (they're inlined for you). Another library would have to be inlined into your `js` — and must be seekable.
8468
- **No `fetch` / `XMLHttpRequest`** (`connect-src 'none'`). Bundle any data inline — e.g. the JS array in the bar-chart example.
8569
- **No remote images** (`img-src 'self' data: blob:`). Embed images as `data:` URIs. A `data:image/svg+xml` background is fine.
8670
- **No remote fonts** (`font-src 'self' data:`) — see below.
@@ -102,9 +86,7 @@ An unresolved family silently falls back to the browser default — the render w
10286

10387
The single most important rule: **size the clip to the content, not to the canvas.** Then position with `offset`.
10488

105-
The clip's `width` / `height` becomes the iframe's natural pixel dimensions AND the editor's selection box. When the user drags the clip's corners to resize, the iframe gets CSS-scaled — the bar stays the same shape but every internal pixel scales proportionally. This is true regardless of CSS / GSAP / anime / Lottie.
106-
107-
If the clip is sized to the canvas (1920×1080) but the visual content is a small lower-third in the corner, the selection box appears huge over empty space — confusing for the user, and resizing scales the lower-third with the empty space.
89+
The clip's `width` / `height` are the iframe's natural pixel dimensions — match them to the content's real size; every coordinate inside the iframe is in that pixel space. Sizing the clip to the whole canvas when the content is a small corner overlay wastes space and makes it harder to place — size to the content and move it with `offset`.
10890

10991
**Right pattern:**
11092

@@ -171,12 +153,11 @@ Slide-in name + role bar with subtle accent. **Clip sized to the bar (560×120),
171153

172154
**Patterns to copy:**
173155

174-
- **Clip is the size of the bar, not the canvas.** Selection box wraps the bar; resizing changes the bar's size; placement is one `offset` change.
156+
- **Clip is the size of the bar, not the canvas** placement is one `offset` change.
175157
- **`html, body, .bar` all 560×120.** No absolute positioning inside the iframe — the bar IS the iframe content.
176-
- Single GSAP timeline driving every animation. The harness seeks the timeline; child tweens follow automatically.
158+
- One GSAP timeline drives every animation.
177159
- Merge fields (`{{name}}`, `{{role}}`) in the HTML, populated by **top-level** `merge[]` (sibling of `timeline`/`output`, NOT a clip property). Keeps the asset reusable.
178-
- Background `rgba(...,0.92)` so the bar sits over a video underneath without being fully opaque.
179-
- Final `.to({}, { duration: 3.5 })` is a hold — no animation, just consumes timeline time so the bar stays visible before fading out.
160+
- A trailing `.to({}, { duration: 3.5 })` holds the final state before the clip ends.
180161

181162
## Worked example: animated bar chart (D3 + GSAP)
182163

@@ -213,7 +194,6 @@ D3 builds the SVG; GSAP animates the bars growing in. 6 seconds, 1080p.
213194
- D3 builds DOM; **GSAP animates it**. D3's `.transition()` works but GSAP is the more reliable seek target.
214195
- Bars start at `y = ih, height = 0` (collapsed at the baseline); GSAP grows them up via `attr: { y, height }`.
215196
- Stagger via `1.0 + i * 0.08` per bar — each bar starts 80ms after the previous.
216-
- Single `<linearGradient>` in `<defs>`, all bars reference it via `fill="url(#g)"`. SVG-native, no per-bar styling.
217197

218198
## Worked example: 10-second countdown (pure CSS)
219199

@@ -233,46 +213,19 @@ A countdown is "show different content at different times" — the classic case
233213
}
234214
```
235215

236-
No JS at all. The harness's WAAPI driver pauses each animation, sets `currentTime` to the capture timestamp, and commits the seeked values — so each frame correctly shows whichever number is mid-pop.
216+
No JS at all — each number is a CSS animation in its own time slot, so every captured frame shows whichever number is mid-pop.
237217

238218
**Patterns to copy:**
239219

240220
- **One element per state.** `<div id="n10">10</div>`, `<div id="n9">9</div>`, ... — never mutate `textContent` from JS.
241221
- **Stagger via `animation-delay`.** `0s, 1s, 2s, ...` lines each animation up to a unique slot.
242222
- **`animation-fill-mode: both`.** During the delay phase the element shows the `0%` keyframe (`opacity:0`); after the animation it shows the `100%` keyframe (`opacity:0`). Together with `position:absolute;inset:0`, only the currently-animating element is visible.
243-
- **Linear easing.** The harness can seek at any timestamp; non-linear easings still work, but `linear` keeps the math obvious when iterating on timing.
244223

245224
The same pattern scales to scene transitions (each scene is a `<section>` with its own animation slot), tickers (each price is a `<div>` with a slot), animated lists, etc.
246225

247-
## What to avoid for time-driven content
248-
249-
- **`setTimeout` / `setInterval` / `requestAnimationFrame` loops** — none advance during capture. The harness drives time deterministically.
250-
- **`new Date()` / `Date.now()` / `performance.now()` reads** — return the host's wall-clock, not the capture's seeked time.
251-
- **`gsap.call(fn, args, time)`** — callbacks aren't seekable. Use `gsap.fromTo()` / `gsap.to()` tweens, or pure CSS animations.
252-
- **Any state mutation triggered by an event other than the harness seek.**
253-
254-
## Author overrides for exotic animation engines
255-
256-
You almost never need these. They exist for engines outside the supported libraries (Three.js, P5, custom WebGL loops) — and since external scripts are blocked (see *Sandbox restrictions*), such an engine has to be inlined in your `js`. If you're using GSAP, anime.js, Lottie, or CSS animations, **do not install these** — the auto-detected path is the only one tested for parity between Studio playback and cloud render.
257-
258-
```js
259-
// Custom timeline — the harness calls seek() alongside the auto-detected sources.
260-
// seek() receives SECONDS.
261-
window.__shotstackTimeline = {
262-
seek(seconds) { /* set every part of your scene to time `seconds` */ }
263-
};
264-
265-
// Or install a bare seek hook. NOTE: this one receives MILLISECONDS.
266-
window.__shotstackSeek = (timeMs) => { /* set state for time timeMs */ };
267-
```
268-
269-
Install before any animation starts — the harness loads at the end of the document and captures whatever hook you've defined. The clip's `length` sets the duration; there is no duration hook.
270-
271226
## Common mistakes
272227

273-
1. **Time-based logic in JS that isn't seekable.** `setTimeout`, `setInterval`, `requestAnimationFrame` loops, `new Date()` / `Date.now()` reads, and **`gsap.call()`** callbacks all fail because they don't run during the deterministic capture. Use GSAP timelines (`tl.to/fromTo`), anime.js instances, Lottie, or pure CSS keyframes — the four sources the harness auto-seeks.
274-
275-
2. **`<canvas>` elements.** The Studio frame capture serialises the iframe's DOM via `XMLSerializer`. A `<canvas>` element's bitmap lives in the 2D-context backing store, NOT in the DOM — so the cloned canvas comes through empty and **the captured frames show nothing where the canvas was**. The cloud render (puppeteer) handles canvas correctly, but Studio playback won't, so the parity is broken. Use SVG or positioned HTML elements instead:
228+
1. **`<canvas>` elements.** The Studio frame capture serialises the iframe's DOM via `XMLSerializer`. A `<canvas>` element's bitmap lives in the 2D-context backing store, NOT in the DOM — so the cloned canvas comes through empty and **the captured frames show nothing where the canvas was**. The cloud render (puppeteer) handles canvas correctly, but Studio playback won't, so the parity is broken. Use SVG or positioned HTML elements instead:
276229

277230
| Want | Replace `<canvas>` with |
278231
|---|---|
@@ -283,10 +236,6 @@ Install before any animation starts — the harness loads at the end of the docu
283236

284237
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.
285238
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.
286-
3. **Expecting network access.** External `<script src=>`, `fetch`/XHR, remote `<img src="https://…">`, and remote `@font-face` URLs are all blocked by the iframe's Content-Security-Policy. Only the bundled libraries run, and there is no network — inline all data, images, and fonts as `data:` URIs (see *Sandbox restrictions*).
287-
4. **Forgetting `overflow: hidden`.** Without it, animations can scroll the iframe and capture extra content beyond the intended viewport.
288-
5. **Using the deprecated `html` asset type.** It still parses but is static-only and ignores the harness. Always use `html5`.
289-
6. **Painting an opaque background by accident.** The body is **transparent by default**, so a clip above a video already composites over it. Set `body { background: … }` only when you genuinely want a solid backdrop — it will hide the layers below.
290239

291240
## When to use `html5` vs `rich-text`/`svg`
292241

0 commit comments

Comments
 (0)