Skip to content

Commit 47ba935

Browse files
committed
feat(skill): add house motion language with shared tokens for coherent animation
1 parent b2505c4 commit 47ba935

4 files changed

Lines changed: 245 additions & 19 deletions

File tree

SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ The Shotstack schema does **not** match CSS or web conventions. Composing Edit J
126126

127127
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.
128128

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+
129131
## Exit codes
130132

131133
| Code | Meaning |
@@ -154,6 +156,7 @@ This skill ships sub-references for the gnarly bits:
154156
- [`references/positioning.md`](references/positioning.md) — coordinate model, `position`/`offset` (fraction of frame, +y up), clip bounding box & `fit`, text sizing, transform order
155157
- [`references/caption.md`](references/caption.md) — sizing per resolution, default style, the 5 named presets, alias pattern (asset type: `rich-caption`)
156158
- [`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.
157160
- [`references/html5.md`](references/html5.md) — HTML5 asset: fields, preloaded libs (gsap/d3/anime/lottie), browser harness, sizing, worked examples
158161
- [`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
159162
- [`references/fonts.md`](references/fonts.md) — built-in fonts, Google Fonts URL pattern, custom-font workflow

references/html5-snippets.md

Lines changed: 80 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,15 @@ Copy-paste `html5` clips that "pop" — kinetic type, count-ups, shine sweeps,
44
pulsing CTAs, grain. Each is a **single clip**: paste it into a track's
55
`clips[]`, set `start`/`length`, and position with `offset`.
66

7-
Read [`html5.md`](html5.md) first for the rules these obey. The non-negotiables:
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:
816

917
- **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.
1018
- **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`/`%`.
@@ -13,21 +21,51 @@ Read [`html5.md`](html5.md) first for the rules these obey. The non-negotiables:
1321
- **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.
1422
- **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`.
1523

16-
Coordinates below assume a **1080×1920 vertical** canvas; adjust `offset` for other sizes. Colours use a neutral ink/accent — swap for your palette.
24+
Coordinates below assume a **1080×1920 vertical** canvas; adjust `offset` for other sizes.
25+
26+
> **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.
27+
28+
---
29+
30+
## 1. Blur reveal — calm text entrance
31+
32+
**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
33+
34+
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.
35+
36+
```json
37+
{
38+
"asset": {
39+
"type": "html5",
40+
"html": "<div class=\"t\">{{title}}</div>",
41+
"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}",
42+
"js": "gsap.from('.t',{opacity:0,y:16,filter:'blur(10px)',duration:0.6,ease:'power3.out'});gsap.to({},{duration:1.5});"
43+
},
44+
"start": 0,
45+
"length": 3,
46+
"width": 900,
47+
"height": 300,
48+
"offset": { "x": 0, "y": 0 }
49+
}
50+
```
51+
52+
`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).
1753

1854
---
1955

20-
## 1. Kinetic headline (word-by-word rise)
56+
## 2. Kinetic headline (word-by-word rise)
2157

22-
Big title where each word springs up in sequence. GSAP timeline, fully seekable.
58+
**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
59+
60+
Each word springs up in sequence. GSAP timeline, fully seekable, on the house stagger.
2361

2462
```json
2563
{
2664
"asset": {
2765
"type": "html5",
2866
"html": "<div class=\"h\"><span>JUST</span> <span>DROPPED</span></div>",
2967
"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});"
68+
"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});"
3169
},
3270
"start": 0,
3371
"length": 3,
@@ -37,11 +75,13 @@ Big title where each word springs up in sequence. GSAP timeline, fully seekable.
3775
}
3876
```
3977

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')}`.
78+
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.
4179

4280
---
4381

44-
## 2. Count-up number / price odometer
82+
## 3. Count-up number / price odometer
83+
84+
**Category** data · **Use when** revealing a value, price, stat or metric · **Canvas** 620×220 · **Tags** number, count, price, data · **Merge-friendly** target value
4585

4686
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.
4787

@@ -51,7 +91,7 @@ Animate a value from 0 to its target. Seek-safe because the count lives in a **t
5191
"type": "html5",
5292
"html": "<div class=\"wrap\"><span class=\"cur\">$</span><span id=\"n\">0</span></div>",
5393
"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});"
94+
"js": "const o={v:0};const out=document.getElementById('n');const tl=gsap.timeline();tl.to(o,{v:395,duration:0.8,ease:'power2.out',onUpdate:()=>{out.textContent=Math.round(o.v)}});tl.to({},{duration:1.5});"
5595
},
5696
"start": 0,
5797
"length": 3,
@@ -61,20 +101,22 @@ Animate a value from 0 to its target. Seek-safe because the count lives in a **t
61101
}
62102
```
63103

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").
104+
`slow` (0.8 s) count on `power2.out` (a decelerating settle reads right for a value), then a `hold`. 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").
65105

66106
---
67107

68-
## 3. Shine / gloss sweep
108+
## 4. Shine / gloss sweep
69109

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.
110+
**Category** graphics (emphasis) · **Use when** adding premium gloss to a product name, logo or card · **Canvas** 860×280 · **Tags** shine, gloss, emphasis, specular
111+
112+
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.
71113

72114
```json
73115
{
74116
"asset": {
75117
"type": "html5",
76118
"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)}}",
119+
"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)}}",
78120
"js": ""
79121
},
80122
"start": 0,
@@ -85,20 +127,22 @@ A specular highlight that slides across text or a card — premium product gloss
85127
}
86128
```
87129

88-
`mix-blend-mode: overlay` makes the sweep read as a real highlight rather than a white bar. No JS needed.
130+
`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.
89131

90132
---
91133

92-
## 4. Pulsing CTA button
134+
## 5. Pulsing CTA button
135+
136+
**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`
93137

94-
A "SHOP NOW" pill with a soft breathing glow — draws the eye on an end card. CSS keyframes loop and seek cleanly.
138+
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.
95139

96140
```json
97141
{
98142
"asset": {
99143
"type": "html5",
100144
"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)}}",
145+
"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)}}",
102146
"js": ""
103147
},
104148
"start": 0,
@@ -109,13 +153,15 @@ A "SHOP NOW" pill with a soft breathing glow — draws the eye on an end card. C
109153
}
110154
```
111155

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`.
156+
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`.
113157

114158
---
115159

116-
## 5. Film-grain / texture overlay
160+
## 6. Film-grain / texture overlay
117161

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.
162+
**Category** atmosphere · **Use when** adding a moody analogue texture over a dark scene · **Canvas** 1080×1920 (full frame) · **Tags** grain, texture, overlay, atmosphere
163+
164+
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.
119165

120166
```json
121167
{
@@ -137,9 +183,24 @@ A `data:` URI is fine **inside an html5 asset's CSS** (it's iframe content, not
137183

138184
---
139185

186+
## Brand kit — re-skin every snippet at once
187+
188+
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 `css``merge` find/replace runs over the `html`/`css` strings too:
189+
190+
```json
191+
"merge": [
192+
{ "find": "ink", "replace": "#1A1A2E" },
193+
{ "find": "accent", "replace": "#E8552D" },
194+
{ "find": "font", "replace": "Anton, system-ui, sans-serif" }
195+
]
196+
```
197+
198+
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.)
199+
140200
## Composing these
141201

142202
- 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.
143203
- They don't overlap on a single track, so `shotstack validate <file>` stays clean. Run it before rendering.
144204
- Reuse text via top-level `merge[]` (`{{title}}` in the HTML) — see the lower-third example in `html5.md`.
205+
- Mix calm and punchy deliberately: a `blur-reveal` title, a `kinetic-headline` hero line, a `count-up` stat, a `shine` on the product, a pulsing CTA — all on the same tokens, so the set reads as one piece.
145206
- Heavier motion = longer render. Preview in `shotstack studio <file>` before spending credits.

0 commit comments

Comments
 (0)