Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eec1c433b | ||
|
|
059e1f5852 | ||
|
|
5867633254 | ||
|
|
d4728061a9 |
No files matched your search
File renamed without changes.
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: after-effects
|
||||
description: This skill should be used when the user asks anything about Adobe After Effects — to "write an After Effects expression", "make a wiggle / loopOut / inertial bounce", "rig a control with a slider / null", "build a MOGRT / Essential Graphics template", "export AE to Lottie or Media Encoder"; OR to "export from After Effects", "export with transparency / an alpha channel", "fix a black background on export", "choose ProRes vs H.264", "which codec / what bitrate for 1080p / 4K", "stop H.264 disappearing from the Render Queue"; OR when "After Effects is slow / laggy / crashing / froze", "RAM preview won't play in real time", "AE is using all my memory / render takes forever", "how do I speed up After Effects"; OR to "organize my After Effects project", "relink missing footage", "fix broken links after moving files", "set a folder structure / naming conventions", "batch rename layers", or "write an ExtendScript / .jsx script". Covers expressions & rigging, export recipes, performance triage, and project hygiene.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# After Effects
|
||||
|
||||
After Effects is GUI software, so this skill delivers knowledge and ready-to-paste assets — expressions, rig setups, export checklists, diagnostic flows, naming schemes, and ExtendScript — rather than a rendered file. It is a hub covering four domains; route to the matching reference for depth and complete recipes.
|
||||
|
||||
## Routing
|
||||
|
||||
| If the user wants to… | Go to |
|
||||
|---|---|
|
||||
| Write/debug an expression, rig sliders & nulls, build a MOGRT, export to Lottie | `references/expressions.md` (deep cookbook in `references/expression-library.md`) |
|
||||
| Choose a codec/container, export with alpha, fix a black background, set bitrate, recover H.264 | `references/export-recipes.md` |
|
||||
| Fix slow/laggy previews, slow renders, crashes, freezes, out-of-memory | `references/performance-triage.md` |
|
||||
| Organize the project, relink missing footage, set naming/folders, batch-automate with .jsx | `references/project-hygiene.md` |
|
||||
|
||||
The `.jsx` automation scripts live in `scripts/`.
|
||||
|
||||
## Expressions & rigging
|
||||
|
||||
Expressions are JavaScript evaluated on the property they are pasted on; `value` is that property's value, `time` is comp time in seconds, and `index`, `thisLayer`, `thisComp` are in scope. An expression overrides keyframes unless it incorporates `value`.
|
||||
|
||||
```js
|
||||
wiggle(2, 30); // 2 wiggles/sec, amplitude 30 (property units)
|
||||
loopOut("cycle"); // repeat keyframed range forever; "pingpong" / "offset" / "continue"
|
||||
seedRandom(index, true); // stable-but-unique per layer (timeless) before random()/wiggle()
|
||||
thisComp.layer("Lead").transform.position.valueAtTime(time - 0.2 * index); // staggered echo
|
||||
```
|
||||
|
||||
Inertial bounce (overshoot/settle after the last keyframe) and the full loop/wiggle/rigging set are in `references/expressions.md`.
|
||||
|
||||
Rig controllable motion: add a null with **Slider / Checkbox / Color / Angle / Point** control effects, then reference them and parent transforms to drive many layers from one.
|
||||
|
||||
```js
|
||||
ctrl = thisComp.layer("Controls");
|
||||
amt = ctrl.effect("Intensity")("Slider"); // Checkbox → 1/0, Color → [r,g,b,a]
|
||||
value * amt;
|
||||
```
|
||||
|
||||
Expose those controls via **Window → Essential Graphics** and **Export Motion Graphics Template** (.mogrt) for Premiere/AE editors.
|
||||
|
||||
## Export recipes
|
||||
|
||||
Two engines, and choosing wrong is the root of most export confusion:
|
||||
- **Render Queue (RQ)** — masters/intermediates: ProRes, image sequences, lossless, alpha.
|
||||
- **Adobe Media Encoder (AME)** — delivery: H.264 MP4, H.265, social presets.
|
||||
|
||||
Since AE CC 2014/2015 the native **H.264 output module was removed from the RQ** — make MP4s in AME, not the RQ. **H.264/MP4 cannot store alpha**, so a transparent comp exported to H.264 always fills with black; that is the classic "black background" complaint and not a bug. For transparency use **ProRes 4444**, a **PNG/TIFF sequence**, or **HEVC-alpha (Safari) + VP9/WebM-alpha (Chrome)**, with Output Module **Channels: RGB + Alpha**.
|
||||
|
||||
Quick targets: editor handoff → **ProRes 422 HQ**; master/grade or alpha → **ProRes 4444**; web/social/client → **H.264 via AME**, VBR 2-pass (~10–16 Mbps 1080p, ~35–45 Mbps 4K). Full matrix, social specs, and step-by-step alpha exports in `references/export-recipes.md`.
|
||||
|
||||
## Performance triage
|
||||
|
||||
Work top-down by symptom, cheapest fix first, and stop when it clears. Most settings are in `Edit > Preferences` (Win) / `After Effects > Settings` (Mac).
|
||||
|
||||
- **Slow previews:** Half/Third resolution → enable **Multi-Frame Rendering** → raise RAM allocation (leave ~6 GB for OS) → Adaptive fast previews → purge cache → Disk Cache on SSD/NVMe → solo heavy layers/effects → HW-accelerated decode → proxy heavy comps.
|
||||
- **Slow renders:** confirm MFR on → RQ for masters / AME for delivery → HW decode H.264 → pre-render slow precomps → cheaper effects → transcode long-GOP to ProRes.
|
||||
- **Crashes/freezes:** purge + empty disk cache → update **NVIDIA Studio** (not Game-Ready) drivers → reset prefs (`Ctrl/Cmd+Alt+Shift` on launch) → pull third-party plugins → free disk space → disable MFR if MFR-specific → roll back AE after a bad update.
|
||||
|
||||
MFR needs several GB RAM per active core. Ordered flows, exact preference paths, and the proxy workflow are in `references/performance-triage.md`.
|
||||
|
||||
## Project hygiene
|
||||
|
||||
Keep projects organized, portable, and automatable.
|
||||
|
||||
- **Folders:** numeric-prefixed bins (`01_Comps`, `02_Precomps`, `03_Footage`, …, `99_Old`) for stable sort order.
|
||||
- **Naming:** `PROJECT_section_descriptor_v01`; precomps `PRE_`; rename solids/nulls to their role (`CTRL_master`); no spaces/special chars for farm/cross-OS safety.
|
||||
- **Broken links:** AE references footage by path, so moving files breaks links. Double-click a missing item → **Replace Footage > File** and AE relinks the whole folder at once; enumerate with `File > Dependencies > Find Missing Footage`. **Always hand off projects via `File > Dependencies > Collect Files`**, never a bare `.aep`.
|
||||
- **ExtendScript (.jsx):** wrap batches in `app.beginUndoGroup(...) / app.endUndoGroup()`; apply effects by **matchName** (e.g. `"ADBE Gaussian Blur 2"`), not the display name. Ready scripts: `scripts/batch-rename.jsx`, `scripts/sequential-offset.jsx`, `scripts/apply-effect-to-layers.jsx`.
|
||||
|
||||
Full naming scheme, label-color system, relink/Collect-Files/Reduce workflows, and scripting setup in `references/project-hygiene.md`.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/expressions.md` — expressions, rigging, MOGRT, Lottie/Media Encoder basics.
|
||||
- `references/expression-library.md` — deeper expression cookbook: tuned inertial bounce, all loop variants, advanced wiggle, valueAtTime echoes, seedRandom grids, full rigging + coordinate spaces, MOGRT and Lottie/Bodymovin steps, Media Encoder presets.
|
||||
- `references/export-recipes.md` — full codec/alpha/bitrate matrix, per-platform social specs, step-by-step transparency exports, AME preset cautions.
|
||||
- `references/performance-triage.md` — ordered preview/render/crash flows, preference paths and values, MFR/GPU/disk-cache tuning, pre-render & proxy workflow.
|
||||
- `references/project-hygiene.md` — folder template, naming and label systems, relink/Collect Files/Reduce details, scripting setup.
|
||||
- `scripts/batch-rename.jsx`, `scripts/sequential-offset.jsx`, `scripts/apply-effect-to-layers.jsx` — ExtendScript automations.
|
||||
</content>
|
||||
@@ -0,0 +1,168 @@
|
||||
# Export Recipes — Codec, Container, Alpha, Bitrate
|
||||
|
||||
Pick the correct codec, container, alpha handling, and bitrate for a given delivery target, and avoid the classic failures: black backgrounds where transparency was expected, H.264 that vanished from the Render Queue, and bloated or muddy files.
|
||||
|
||||
## The two export engines
|
||||
|
||||
After Effects has two output paths. Choosing the wrong one is the root of most export confusion.
|
||||
|
||||
- **Render Queue (RQ)** — `Composition > Add to Render Queue`. Best for mastering/intermediate formats: ProRes, image sequences, lossless, alpha. Renders inside AE.
|
||||
- **Adobe Media Encoder (AME)** — `Composition > Add to Adobe Media Encoder Queue` (or the RQ "Queue in AME" button). Best for delivery/compressed formats: H.264 MP4, H.265, social presets. Renders in a separate app so AE stays usable.
|
||||
|
||||
**Why H.264 disappeared from the Render Queue:** Since AE CC 2014/2015, Adobe removed the native H.264, H.264 Blu-ray, and MPEG-2 output modules from the RQ. To make an MP4, use AME — not the RQ. There is no setting to restore it in the RQ; that workflow is intentional.
|
||||
|
||||
## Decision tree
|
||||
|
||||
```
|
||||
Need transparency (alpha) in the final file?
|
||||
├─ YES, for an editor/compositor (NLE) → ProRes 4444 (.mov), RGB+Alpha, RQ
|
||||
│ └─ smaller, still alpha? → PNG sequence or TIFF sequence (RGB+Alpha)
|
||||
├─ YES, for the web/browser → dual export:
|
||||
│ • HEVC with alpha (.mov, Safari/iOS)
|
||||
│ • VP9/WebM with alpha (Chrome/Firefox/Edge)
|
||||
└─ NO alpha needed
|
||||
├─ Final delivery / upload (YouTube, social, client review) → H.264 MP4 via AME
|
||||
├─ Editorial / intermediate / grading → ProRes 422 HQ or 422 (.mov), RQ
|
||||
├─ Mastering / archival, highest fidelity → ProRes 4444 or lossless / image seq
|
||||
└─ Tiny looping clip, limited colors → GIF (via AME / Photoshop), or WebM
|
||||
```
|
||||
|
||||
## Transparency — the black background problem
|
||||
|
||||
H.264/MP4 **cannot store an alpha channel**. Exporting a comp with transparent areas to H.264 always fills them with black (or white). This is the single most common export complaint and it is not a bug.
|
||||
|
||||
To actually keep transparency:
|
||||
|
||||
1. Use a format that supports alpha: **ProRes 4444**, **PNG/TIFF sequence**, **HEVC-with-alpha**, or **VP9/WebM-with-alpha**.
|
||||
2. In the RQ Output Module, set **Channels: RGB + Alpha** and **Depth** to Millions of Colors+ (the "+" is the alpha).
|
||||
3. Choose a matte type: **Premultiplied (Matted with color)** for most cases; **Straight (Unmatted)** if the compositor explicitly wants straight alpha. Straight alpha avoids dark/light fringing on edges when re-composited, but only some apps import it cleanly.
|
||||
|
||||
For web transparent video, no single file plays everywhere. Ship two and use the HTML `<video>` element with multiple `<source>` tags: HEVC-alpha for Safari/iOS, VP9/WebM-alpha for Chromium/Firefox.
|
||||
|
||||
## Bitrate guidance (H.264 via AME)
|
||||
|
||||
Set in AME Export Settings → Video → Bitrate Settings.
|
||||
|
||||
- **Encoding:** VBR, 2 pass (better quality per byte than CBR or 1-pass).
|
||||
- **1080p30:** Target ~10–16 Mbps, Max ~20 Mbps.
|
||||
- **1080p60:** Target ~16–24 Mbps.
|
||||
- **4K30:** Target ~35–45 Mbps, Max ~55 Mbps.
|
||||
- **4K60:** Target ~50–68 Mbps.
|
||||
- Higher target/max for high-motion or grainy footage; lower for flat graphics/motion design.
|
||||
- For uploading to YouTube/Vimeo, lean to the high end — the platform re-encodes, so feed it a clean, high-bitrate master rather than over-compressing twice.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Target | Engine | Codec / Container | Channels | Bitrate / Notes |
|
||||
|---|---|---|---|---|
|
||||
| YouTube / Vimeo upload | AME | H.264 .mp4 | RGB | VBR 2-pass, 1080p ~16 Mbps / 4K ~45 Mbps |
|
||||
| Instagram / TikTok / Reels | AME | H.264 .mp4 | RGB | 1080×1920 or 1080×1080, ~10–14 Mbps |
|
||||
| Client review | AME | H.264 .mp4 | RGB | VBR 2-pass ~10–16 Mbps |
|
||||
| Editorial / hand to editor | RQ | ProRes 422 HQ .mov | RGB | Mezzanine; large but edit-friendly |
|
||||
| Mastering / grading | RQ | ProRes 4444 .mov | RGB(+A) | Highest fidelity, optional alpha |
|
||||
| Transparency → NLE | RQ | ProRes 4444 .mov | RGB+Alpha | Millions+; premultiplied usually |
|
||||
| Transparency → frames | RQ | PNG or TIFF sequence | RGB+Alpha | Lossless per-frame, robust |
|
||||
| Transparent web (Safari) | AME/RQ | HEVC w/ alpha .mov | RGB+Alpha | Pair with VP9 below |
|
||||
| Transparent web (Chrome) | AME/3rd-party | VP9 / WebM w/ alpha | RGB+Alpha | `yuva420p` alpha mode |
|
||||
| Looping clip, few colors | AME/PS | GIF or WebM | — | GIF = large/limited; prefer WebM |
|
||||
| Archival lossless | RQ | Lossless / PNG seq | RGB(+A) | Huge files |
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **H.264 in the RQ is gone** — use AME. Don't search for a missing module.
|
||||
- **Black background = wrong codec.** Switching matte/straight settings won't help if the codec has no alpha. Change the codec first.
|
||||
- **"Millions of Colors" has no alpha; "Millions of Colors+" does.** The `+` is the alpha channel in RQ Depth.
|
||||
- **ProRes on Windows** is fully supported for export in modern AE; no extra codec install needed.
|
||||
- **GIF is rarely the right answer** — large files, dithered color, no real alpha softness. Prefer WebM/VP9 or short H.264 loops.
|
||||
- **Don't double-compress.** Master to ProRes/lossless, then make one H.264 from that — not H.264 from H.264.
|
||||
- **AME "Match Source"** presets can silently downscale or drop the bitrate; verify resolution and bitrate before queuing.
|
||||
- **Render multiple outputs efficiently:** in the RQ, one render item can have several Output Modules (e.g. ProRes master + PNG seq) from a single render pass.
|
||||
|
||||
---
|
||||
|
||||
# Codec & Export Matrix — Detailed Reference
|
||||
|
||||
## ProRes family — when to use which
|
||||
|
||||
| Variant | Use for | Alpha | Relative size |
|
||||
|---|---|---|---|
|
||||
| ProRes 422 Proxy | Offline edit, rough cuts | No | Smallest |
|
||||
| ProRes 422 LT | Lightweight editorial | No | Small |
|
||||
| ProRes 422 | General editorial | No | Medium |
|
||||
| ProRes 422 HQ | High-quality editorial / delivery to editor | No | Large |
|
||||
| ProRes 4444 | Mastering, grading, **transparency** | Yes | Larger |
|
||||
| ProRes 4444 XQ | Highest fidelity, HDR, max-quality alpha | Yes | Largest |
|
||||
|
||||
Rule of thumb: hand an editor **422 HQ**; master/keep alpha with **4444**.
|
||||
|
||||
## Full target → settings matrix
|
||||
|
||||
| Delivery target | Engine | Container | Codec | Resolution | Channels / Depth | Bitrate / Quality | Audio |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| YouTube 1080p | AME | .mp4 | H.264 | 1920×1080 | RGB, 8-bit | VBR 2-pass, Tgt 16 / Max 20 Mbps | AAC 320 kbps 48 kHz |
|
||||
| YouTube 4K | AME | .mp4 | H.264 | 3840×2160 | RGB, 8-bit | VBR 2-pass, Tgt 45 / Max 55 Mbps | AAC 384 kbps |
|
||||
| Vimeo 1080p | AME | .mp4 | H.264 | 1920×1080 | RGB | VBR 2-pass, Tgt 20 Mbps | AAC 320 kbps |
|
||||
| Instagram Feed | AME | .mp4 | H.264 | 1080×1080 or 1080×1350 | RGB | ~10–12 Mbps | AAC 128–256 kbps |
|
||||
| Instagram Reels / TikTok | AME | .mp4 | H.264 | 1080×1920 | RGB | ~12–14 Mbps | AAC 256 kbps |
|
||||
| X / Twitter | AME | .mp4 | H.264 | 1280×720 or 1080p | RGB | ~6–10 Mbps | AAC |
|
||||
| Broadcast delivery | RQ | .mov | ProRes 422 HQ | as spec (often 1080i/p) | RGB, 10-bit | — | PCM 48 kHz |
|
||||
| Editorial handoff | RQ | .mov | ProRes 422 HQ | comp size | RGB | — | PCM |
|
||||
| Color grading master | RQ | .mov | ProRes 4444 | comp size | RGB, 12-bit | — | PCM |
|
||||
| VFX plate / alpha to NLE | RQ | .mov | ProRes 4444 | comp size | RGB+Alpha, Millions+ | premultiplied | — |
|
||||
| Compositing frames | RQ | seq | PNG or 16-bit TIFF | comp size | RGB+Alpha | lossless | — |
|
||||
| EXR for VFX pipeline | RQ | seq | OpenEXR | comp size | RGB+Alpha, 32-bit float | linear/ACES | — |
|
||||
| Transparent web (Apple) | AME or RQ | .mov | HEVC w/ alpha | comp size | RGB+Alpha | quality ~ 60–80% | optional |
|
||||
| Transparent web (Chrome) | 3rd-party/ffmpeg | .webm | VP9 (yuva420p) | comp size | RGB+Alpha | CRF ~30 | optional |
|
||||
| Animated icon / loop | AME or PS | .gif / .webm | GIF / VP9 | small | indexed / RGBA | minimize frames | — |
|
||||
|
||||
## Transparency — step by step
|
||||
|
||||
### ProRes 4444 with alpha (Render Queue)
|
||||
1. `Composition > Add to Render Queue`.
|
||||
2. Click the blue **Output Module** text.
|
||||
3. Format: **QuickTime**. Click **Format Options** → Codec: **Apple ProRes 4444**.
|
||||
4. **Channels: RGB + Alpha**. **Depth: Millions of Colors+**.
|
||||
5. **Color: Premultiplied (Matted)** for most uses; choose **Straight (Unmatted)** only if the downstream app asks for straight alpha.
|
||||
6. Set Output To path, then **Render**.
|
||||
|
||||
### PNG / TIFF sequence with alpha (Render Queue)
|
||||
1. Add to RQ → Output Module.
|
||||
2. Format: **PNG Sequence** (or **TIFF Sequence**).
|
||||
3. **Channels: RGB + Alpha**, Depth **Millions of Colors+** (PNG) or 16-bit (TIFF).
|
||||
4. Output To a **dedicated empty folder** — sequences write one file per frame.
|
||||
|
||||
### HEVC with alpha (Apple/Safari)
|
||||
- In AME, choose **H.265 (HEVC)** and enable an alpha/transparency option where available, or use the QuickTime HEVC export with RGB+Alpha. Verify playback in Safari; Chrome will not show its alpha.
|
||||
|
||||
### VP9 / WebM with alpha (Chrome)
|
||||
- AME does not reliably produce alpha WebM. Render a ProRes 4444 or PNG sequence from AE, then encode with ffmpeg:
|
||||
- `ffmpeg -i master.mov -c:v libvpx-vp9 -pix_fmt yuva420p -b:v 0 -crf 30 out.webm`
|
||||
- `yuva420p` is the alpha-carrying pixel format; without it WebM has no transparency.
|
||||
|
||||
### HTML for cross-browser transparent video
|
||||
```html
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="clip.mov" type="video/quicktime"> <!-- HEVC alpha, Safari/iOS -->
|
||||
<source src="clip.webm" type="video/webm"> <!-- VP9 alpha, Chrome/Firefox -->
|
||||
</video>
|
||||
```
|
||||
|
||||
## Alpha matte types explained
|
||||
- **Straight (Unmatted):** color stored independent of alpha; cleanest edges when re-composited, but fewer apps import correctly.
|
||||
- **Premultiplied (Matted with color):** color blended with a matte color (usually black). Most compatible; can show fringing if the wrong matte color is assumed downstream.
|
||||
- When in doubt for NLE handoff, **premultiplied with black** is the safe default.
|
||||
|
||||
## AME preset cautions
|
||||
- **Match Source – High bitrate** can downscale to the source clip rather than the comp; confirm output resolution.
|
||||
- Always check **VBR 2-pass** is selected for final masters; presets sometimes default to 1-pass.
|
||||
- Use **Render at Maximum Depth** and **Use Maximum Render Quality** for scaling-heavy comps (slower but cleaner).
|
||||
- For social verticals, set the resolution explicitly — don't trust a 16:9 preset.
|
||||
|
||||
## Bitrate cheat sheet (H.264 VBR 2-pass)
|
||||
| Resolution / fps | Target Mbps | Max Mbps |
|
||||
|---|---|---|
|
||||
| 720p30 | 5–8 | 10 |
|
||||
| 1080p30 | 10–16 | 20 |
|
||||
| 1080p60 | 16–24 | 30 |
|
||||
| 4K30 | 35–45 | 55 |
|
||||
| 4K60 | 50–68 | 80 |
|
||||
@@ -0,0 +1,184 @@
|
||||
# After Effects Expression Library, Rigging & Export
|
||||
|
||||
A deeper cookbook. All expressions are JavaScript (the modern AE expression engine). They evaluate on the property they are pasted on, where `value`, `time`, `index`, `thisLayer`, `thisComp` are in scope.
|
||||
|
||||
## Inertial bounce (parameterized)
|
||||
|
||||
```js
|
||||
// Overshoot/settle after the most recent keyframe. Tune amp/freq/decay.
|
||||
amp = 0.10; // 0.05 = subtle, 0.2 = bouncy
|
||||
freq = 3.0; // oscillations per second
|
||||
decay = 5.0; // higher = settles faster
|
||||
|
||||
n = 0;
|
||||
if (numKeys > 0) {
|
||||
n = nearestKey(time).index;
|
||||
if (key(n).time > time) n--;
|
||||
}
|
||||
if (n === 0) {
|
||||
value;
|
||||
} else {
|
||||
t = time - key(n).time;
|
||||
v = velocityAtTime(key(n).time - thisComp.frameDuration / 10);
|
||||
value + v * amp * Math.sin(freq * t * 2 * Math.PI) / Math.exp(decay * t);
|
||||
}
|
||||
```
|
||||
|
||||
Inertial bounce works on multi-dimensional properties because `velocityAtTime` returns a vector; the arithmetic is per-component.
|
||||
|
||||
## Loop variants
|
||||
|
||||
```js
|
||||
loopOut("cycle"); // A B C A B C ...
|
||||
loopOut("pingpong"); // A B C B A B C ...
|
||||
loopOut("offset"); // continues accumulating the delta each cycle
|
||||
loopOut("continue"); // linear extension of the final velocity (no loop)
|
||||
loopOut("cycle", 3); // loop only the last 3 keyframes
|
||||
loopIn("cycle", 2); // loop the first 2 keyframes, backwards in time
|
||||
loopOutDuration("cycle", 2); // loop the last 2 SECONDS of keyframes
|
||||
```
|
||||
|
||||
Use `offset` for endless conveyor/scroll motion; `continue` for inertia past the last keyframe without oscillation.
|
||||
|
||||
## Wiggle — advanced
|
||||
|
||||
```js
|
||||
// Full signature: wiggle(freq, amp, octaves, amp_mult, time)
|
||||
wiggle(2, 30, 3, 0.5); // 3 octaves adds finer detail
|
||||
|
||||
// Per-axis wiggle (different amplitude X vs Y)
|
||||
x = wiggle(2, 50)[0];
|
||||
y = wiggle(2, 10)[1];
|
||||
[x, y];
|
||||
|
||||
// Wiggle only rotation while keeping keyframed position
|
||||
wiggle(1, 5); // on Rotation
|
||||
|
||||
// Smooth, looping ambient motion without true wiggle (loopable):
|
||||
amp = 20; period = 4; // seconds
|
||||
[Math.sin(time / period * 2 * Math.PI) * amp,
|
||||
Math.cos(time / period * 2 * Math.PI) * amp] + value;
|
||||
```
|
||||
|
||||
`wiggle` is non-looping noise; for seamless loops use periodic sin/cos as above.
|
||||
|
||||
## Stable randomness across a grid
|
||||
|
||||
```js
|
||||
seedRandom(index, true); // timeless=true → constant over time, varies per layer
|
||||
startDelay = random(0, 0.5); // each layer gets a stable random delay
|
||||
opacity = ease(time, startDelay, startDelay + 0.4, 0, 100);
|
||||
```
|
||||
|
||||
`seedRandom(index, false)` (timeless=false) lets `random()` vary over time — use for flicker; `true` for stable offsets.
|
||||
|
||||
## Time remap & valueAtTime echoes
|
||||
|
||||
```js
|
||||
// Trailing/echo: sample the lead layer in the past, staggered by index
|
||||
delay = 0.08 * (index - 1);
|
||||
thisComp.layer("Lead").transform.position.valueAtTime(time - delay);
|
||||
|
||||
// Speed-ramp a time-remapped layer using easing on the source time
|
||||
src = thisComp.layer("Source");
|
||||
linear(time, 0, 2, 0, src.source.duration); // play a 2s window of the source
|
||||
|
||||
// Sample own value in the past for motion smear / trail
|
||||
valueAtTime(time - 0.1);
|
||||
```
|
||||
|
||||
## Rigging walkthrough
|
||||
|
||||
### Control types and how to read them
|
||||
|
||||
```js
|
||||
ctrl = thisComp.layer("Controls");
|
||||
ctrl.effect("Intensity")("Slider"); // number
|
||||
ctrl.effect("Visible")("Checkbox"); // 1 or 0
|
||||
ctrl.effect("Brand")("Color"); // [r,g,b,a] 0..1
|
||||
ctrl.effect("Spin")("Angle"); // degrees
|
||||
ctrl.effect("Anchor")("Point"); // [x,y]
|
||||
```
|
||||
|
||||
### One slider, staggered reveal across N layers
|
||||
|
||||
```js
|
||||
// On each layer's Opacity:
|
||||
master = thisComp.layer("Controls").effect("Reveal")("Slider"); // 0..100 driver
|
||||
per = 6; // frames of stagger per layer
|
||||
shift = framesToTime(per * (index - 1));
|
||||
ease(master, 0, 100, 0, 100) * (time > shift ? 1 : 0);
|
||||
```
|
||||
|
||||
### Coordinate-space conversions
|
||||
|
||||
```js
|
||||
// Place this layer where another parented layer appears in comp space
|
||||
target = thisComp.layer("Marker");
|
||||
fromComp(target.toComp(target.anchorPoint));
|
||||
|
||||
// World position of a point on this layer
|
||||
toComp([0, 0]);
|
||||
```
|
||||
|
||||
`toComp` converts layer space → composition space; `fromComp` does the inverse. Use them whenever layers are parented or live in different (nested) comps.
|
||||
|
||||
### Linking many layers to a master null
|
||||
|
||||
1. Select all children → set Parent to the null.
|
||||
2. Animate only the null; children inherit transform.
|
||||
3. For non-transform links (effects, opacity), pick-whip to the control layer's effect.
|
||||
|
||||
## Essential Graphics / MOGRT export steps
|
||||
|
||||
1. **Window → Essential Graphics**, choose the master comp.
|
||||
2. Drag exposed properties (Slider, Checkbox, Color, Text Source, Media replacement) into the panel.
|
||||
3. Add **Group** headers, rename controls, fill comment fields for editors.
|
||||
4. Use **Drop-Zone / Media Replacement** for swappable footage/logos.
|
||||
5. Click **Export Motion Graphics Template**, choose destination (Local Templates Folder / Library / file). Toggle "Make fonts available" and check "Raster/vector" compatibility for Premiere.
|
||||
6. Test in Premiere Pro's Essential Graphics → Browse panel.
|
||||
|
||||
Keep all motion driven by expressions referencing the exposed controls so editors only touch the exposed params; hide internal layers with shy.
|
||||
|
||||
## Lottie / Bodymovin export
|
||||
|
||||
Supported reliably:
|
||||
- Shape layers, solids, transforms (position/scale/rotation/opacity/anchor).
|
||||
- Masks and track mattes (alpha), parenting, basic gradients.
|
||||
- Trim Paths, simple repeaters.
|
||||
- Expressions: enable in Bodymovin settings, but verify on the target renderer — `lottie-web`, lottie-ios, and lottie-android differ in expression and effect support.
|
||||
|
||||
Not supported / risky (bake or avoid):
|
||||
- Blend modes, most native Effects, layer blurs, 3D layers/cameras, time remapping, adjustment-layer effects.
|
||||
|
||||
Workflow:
|
||||
1. Install Bodymovin extension; **Window → Extensions → Bodymovin**.
|
||||
2. Select comp(s), set destination, choose Standard/Demo.
|
||||
3. Settings: enable "Glyphs"/"Hidden", "Expressions" only if needed; "Assets" → export images or use base64.
|
||||
4. Render `data.json`. Preview in a Lottie player; check file size and that masks/mattes survived.
|
||||
5. For web, load with `lottie-web`:
|
||||
|
||||
```js
|
||||
import lottie from 'lottie-web';
|
||||
lottie.loadAnimation({
|
||||
container: document.getElementById('lottie'),
|
||||
renderer: 'svg', // 'canvas' for heavy fills, better perf
|
||||
loop: true, autoplay: true,
|
||||
path: '/data.json',
|
||||
});
|
||||
```
|
||||
|
||||
## Media Encoder presets
|
||||
|
||||
| Use | Codec / format | Settings |
|
||||
|---|---|---|
|
||||
| Web/social delivery | H.264 (MP4) | VBR 2-pass, 1080p ~12–16 Mbps; 4K ~35–50 Mbps |
|
||||
| Master/intermediate | ProRes 422 HQ (MOV) | full range, matched fps |
|
||||
| With alpha | ProRes 4444 (MOV) | enable alpha channel |
|
||||
| Lossless interchange | PNG sequence or ProRes 4444 | per-frame, alpha |
|
||||
| GIF | via Media Encoder/Photoshop | reduce palette, dither, cap dimensions |
|
||||
|
||||
Tips:
|
||||
- Match the comp frame rate; avoid frame-blending unless intentionally retiming.
|
||||
- H.264 cannot store alpha — composite onto a background or use ProRes 4444 / PNG.
|
||||
- For crisp text, render at the delivery resolution (don't upscale); set quality to maximum and use 2-pass VBR.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Expressions, Rigging & MOGRT
|
||||
|
||||
After Effects is GUI software, so this delivers knowledge: ready-to-paste JavaScript expressions, rigging setups that make motion controllable, and template/export pipelines. Expressions read on the property they are pasted on; `value` is that property's keyframed/static value, `time` is the composition time in seconds, and `index`, `thisLayer`, `thisComp` are in scope.
|
||||
|
||||
For a deeper cookbook (tuned inertial bounce, every loop variant, advanced per-axis wiggle, `valueAtTime` echoes, `seedRandom` grids, full rigging walkthrough, Essential Graphics → MOGRT steps, Lottie/Bodymovin + Media Encoder presets), see `expression-library.md`.
|
||||
|
||||
## Core expressions
|
||||
|
||||
### Inertial bounce (overshoot after the last keyframe)
|
||||
|
||||
Paste on a Position, Scale, or Rotation property. It continues the velocity at the final keyframe into a damped sine — the classic "settle" motion.
|
||||
|
||||
```js
|
||||
amp = 0.08; // overshoot amount
|
||||
freq = 2.5; // oscillations per second
|
||||
decay = 6.0; // how fast it settles
|
||||
n = 0;
|
||||
if (numKeys > 0) {
|
||||
n = nearestKey(time).index;
|
||||
if (key(n).time > time) n--;
|
||||
}
|
||||
if (n > 0) {
|
||||
t = time - key(n).time;
|
||||
v = velocityAtTime(key(n).time - 0.001);
|
||||
value + v * (amp * Math.sin(freq * t * 2 * Math.PI) / Math.exp(decay * t));
|
||||
} else {
|
||||
value;
|
||||
}
|
||||
```
|
||||
|
||||
### Looping
|
||||
|
||||
```js
|
||||
loopOut("cycle"); // repeat keyframed range forever
|
||||
loopOut("pingpong"); // bounce back and forth
|
||||
loopOut("offset"); // continue, accumulating (e.g. continuous travel)
|
||||
loopOut("continue"); // extend final velocity linearly
|
||||
loopOut("cycle", 2); // loop only the last 2 keyframes
|
||||
loopIn("cycle"); // mirror, before the first keyframe
|
||||
```
|
||||
|
||||
### Wiggle and stable randomness
|
||||
|
||||
```js
|
||||
wiggle(2, 30); // 2 wiggles/sec, amplitude 30 (units match the property)
|
||||
seedRandom(index, true); // unique-but-stable per layer; timeless=true
|
||||
random(0, 100); // stable after seedRandom
|
||||
wiggle(freq, amp, octaves=1, amp_mult=0.5, t=time); // full signature
|
||||
```
|
||||
|
||||
`seedRandom(index, true)` before `random()` or `wiggle()` makes each layer differ but stay constant across renders — essential for grids of layers that must not flicker.
|
||||
|
||||
### Time remap & value-at-time
|
||||
|
||||
```js
|
||||
// Sample another property/layer at a shifted time (echo/trailing motion)
|
||||
delay = 0.2 * index;
|
||||
thisComp.layer("Lead").transform.position.valueAtTime(time - delay);
|
||||
|
||||
// Speed up a precomp via time remap
|
||||
linear(time, 0, thisComp.duration, 0, 6); // 6s source mapped over comp duration
|
||||
```
|
||||
|
||||
## Rigging
|
||||
|
||||
Make motion controllable instead of hand-keyed:
|
||||
|
||||
1. Add a null/control layer (often a "Controls" layer). Add **Slider Control**, **Checkbox Control**, **Color Control**, **Angle Control**, or **Point Control** effects to it.
|
||||
2. Reference them with the pick-whip or by path:
|
||||
|
||||
```js
|
||||
ctrl = thisComp.layer("Controls");
|
||||
amt = ctrl.effect("Intensity")("Slider"); // Slider Control → ADBE Slider Control-0001
|
||||
on = ctrl.effect("Enable")("Checkbox"); // Checkbox: 1 or 0
|
||||
col = ctrl.effect("Tint")("Color");
|
||||
|
||||
if (on == 1) value * amt else value;
|
||||
```
|
||||
|
||||
3. Parent transforms to a null so one move drives many layers. Use `toComp`/`fromComp` to convert coordinate spaces when layers are parented or in different comps.
|
||||
|
||||
```js
|
||||
// Stagger N layers off one master slider
|
||||
master = thisComp.layer("Controls").effect("Reveal")("Slider");
|
||||
delay = 4 * index; // 4 frames per layer
|
||||
master.valueAtTime(time - framesToTime(delay));
|
||||
```
|
||||
|
||||
## Essential Graphics / MOGRT
|
||||
|
||||
1. Open **Window → Essential Graphics**, pick the comp as the Master.
|
||||
2. Drag the Slider/Checkbox/Color/Text source properties into the panel to expose them.
|
||||
3. Group, rename, and set comment fields for editors.
|
||||
4. **Export Motion Graphics Template** → a `.mogrt` usable in Premiere Pro / AE. Keep driving expressions referencing the exposed controls so editors change only the exposed params.
|
||||
|
||||
Set "Allow fonts to substitute" off and embed/package fonts where possible so the template renders consistently elsewhere.
|
||||
|
||||
## Export from expressions/template work
|
||||
|
||||
### To Lottie (Bodymovin / `@lottiefiles/bodymovin`)
|
||||
|
||||
- Stick to Lottie-supported features: shape layers, transforms, masks, track mattes, basic expressions converted to keyframes.
|
||||
- Avoid blur-heavy effects, blend modes, 3D layers, and most native effects — they don't translate.
|
||||
- Bake expressions to keyframes if the renderer is strict, or use the `expressions` flag in Bodymovin and test on the target player (web/iOS/Android renderers differ).
|
||||
- Run **Bodymovin → Render** to produce `data.json` (+ optional images). Verify in a Lottie preview before handoff.
|
||||
|
||||
### To video (Media Encoder)
|
||||
|
||||
- **File → Export → Add to Adobe Media Encoder Queue** (or render queue for AE-native).
|
||||
- Delivery: **H.264** (MP4), bitrate ~10–20 Mbps for 1080p, VBR 2-pass.
|
||||
- Master/intermediate: **ProRes 422 HQ** (or ProRes 4444 when alpha is needed).
|
||||
- Transparency: ProRes 4444 or PNG/QuickTime with an alpha channel; H.264 cannot carry alpha. (See `export-recipes.md` for the full codec/alpha/bitrate matrix.)
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Goal | Expression |
|
||||
|---|---|
|
||||
| Loop keyframes | `loopOut("cycle")` |
|
||||
| Continuous travel | `loopOut("offset")` |
|
||||
| Subtle life | `wiggle(2, 20)` |
|
||||
| Stable per-layer random | `seedRandom(index,true); random(...)` |
|
||||
| Settle/overshoot | inertial bounce (above) |
|
||||
| Echo another layer | `valueAtTime(time - delay)` |
|
||||
| Read a slider | `effect("Name")("Slider")` |
|
||||
| Frames ↔ seconds | `framesToTime(n)` / `timeToFrames(t)` |
|
||||
|
||||
## Gotchas
|
||||
|
||||
- An expression overrides keyframes on that property unless it incorporates `value`; start most expressions from `value`.
|
||||
- `index` is the layer's stacking number — it changes if layers are reordered; prefer named lookups for stable rigs.
|
||||
- Expression errors disable the expression and show a yellow banner; check the property's path names (effect names must match exactly, including duplicates' suffixes).
|
||||
- Pre-compose for reuse; name layers; use shy/lock and guide layers for safe areas before handoff.
|
||||
</content>
|
||||
</invoke>
|
||||
@@ -0,0 +1,138 @@
|
||||
# Performance Triage — Slow, Laggy, Crashing, Frozen
|
||||
|
||||
Diagnose slow previews, slow renders, freezes, and crashes in a fixed order, from cheapest fix to most invasive. Work top-down and stop when the symptom clears — most issues are solved in the first few steps.
|
||||
|
||||
Most fixes live in Preferences. Open `Edit > Preferences` (Windows) or `After Effects > Settings/Preferences` (macOS).
|
||||
|
||||
## First: identify the symptom
|
||||
|
||||
- **Slow / non-real-time previews (RAM preview)** → Flow A
|
||||
- **Slow renders (final output)** → Flow B
|
||||
- **Crashes / freezes / hangs** → Flow C
|
||||
|
||||
## Flow A — Slow previews (do in order)
|
||||
|
||||
1. **Set Resolution to Auto/Half/Third.** In the Composition viewer, change Resolution from Full to **Half** or **Third**. Adaptive/Draft resolution gives the biggest instant win.
|
||||
2. **Enable Multi-Frame Rendering (MFR).** `Preferences > Memory & Performance > Enable Multi-Frame Rendering`. Uses all CPU cores for preview and render (AE 2022+). Confirm "CPUs reserved for other applications" isn't set too high.
|
||||
3. **Check RAM allocation.** `Preferences > Memory & Performance`. Leave ~6 GB for the OS; give the rest to AE. Too little RAM = a tiny green preview bar.
|
||||
4. **Set Fast Previews to Adaptive Resolution.** Viewer's lightning-bolt icon → **Adaptive Resolution** (degrades resolution only while interacting).
|
||||
5. **Purge the cache.** `Edit > Purge > All Memory & Disk Cache`. Stale/full cache stalls previews.
|
||||
6. **Verify the Disk Cache is on a fast drive.** `Preferences > Media & Disk Cache` → Enable Disk Cache, point it at a fast **SSD/NVMe**, give it 50–100+ GB. Never on a slow external HDD.
|
||||
7. **Solo the offending layer/effect.** Toggle effects off (the fx switch) or solo layers to find the bottleneck. Heavy culprits: blurs, glows, Mocha/tracking, particles, expressions, large 3D, and many third-party plugins.
|
||||
8. **Enable GPU / hardware-accelerated decoding.** `Preferences > Video Preview` (Mercury GPU) and `Preferences > Import > Hardware Accelerate H.264/HEVC decoding`. Helps long-GOP source footage.
|
||||
9. **Pre-render or proxy heavy comps.** Pre-render a finished/slow precomp to ProRes and set it as a **proxy** (`File > Create Proxy`, or right-click footage → Set Proxy). Frees compute for the part being worked on.
|
||||
10. **Trim the work area** to just the section in progress so RAM preview fills faster.
|
||||
|
||||
## Flow B — Slow renders (do in order)
|
||||
|
||||
1. **Confirm Multi-Frame Rendering is on** (`Memory & Performance`) — the single largest render speedup on multi-core CPUs.
|
||||
2. **Render with the Render Queue for masters, AME for delivery** — and don't run heavy work in AE while AME renders.
|
||||
3. **Enable hardware-accelerated decoding** for H.264/HEVC sources (`Preferences > Import`).
|
||||
4. **Pre-render slow precomps** once and reuse them, instead of recomputing every frame. Heavy effect stacks, time-remapping, and 3rd-party renderers benefit most.
|
||||
5. **Replace effects with cheaper equivalents** where possible (e.g., Fast Box Blur over Gaussian; native over plugin).
|
||||
6. **Convert long-GOP source to an intermediate** (ProRes/DNxHR). Decoding H.264 every frame is slow; ProRes is cheap to read.
|
||||
7. **Check the renderer:** `Composition > Composition Settings > 3D Renderer`. Classic 3D is fastest; Cinema 4D/Ray-traced renderers are far slower.
|
||||
8. **Disk Cache on NVMe** so repeated frames aren't recomputed.
|
||||
9. **Close other apps**, especially other Adobe apps and browsers, to free RAM/CPU for MFR.
|
||||
|
||||
## Flow C — Crashes / freezes / hangs (do in order)
|
||||
|
||||
1. **Purge cache** (`Edit > Purge > All`). A full/corrupt cache causes hangs.
|
||||
2. **Clear the Disk Cache folder** in `Preferences > Media & Disk Cache > Empty Disk Cache`. Corrupt cache files cause crash-on-open.
|
||||
3. **Update GPU drivers** (NVIDIA Studio driver / AMD). Outdated/Game-Ready drivers cause Mercury GPU crashes. As a test, switch Project renderer GPU→CPU (`File > Project Settings > Video Rendering and Effects: Mercury Software Only`).
|
||||
4. **Reset Preferences:** hold `Ctrl+Alt+Shift` (Win) / `Cmd+Opt+Shift` (Mac) while launching AE. Corrupt prefs are a top crash cause.
|
||||
5. **Disable third-party plugins** by temporarily moving them out of the plug-ins folder, then reintroduce to find the offender. Old plugins are a leading crash/freeze source after AE updates.
|
||||
6. **Check Disk Cache and Output drives for free space.** A full scratch/cache disk causes freezes mid-render.
|
||||
7. **Lower or disable MFR** if crashes appear only during multi-frame render — some plugins are not MFR-safe; reserve more CPUs or disable MFR for those projects.
|
||||
8. **Roll back the AE version** if crashes started right after an update. Install a previous version via Creative Cloud (Other Versions) — many crash waves are version-specific regressions.
|
||||
9. **Look for a single corrupt asset** — import comps one at a time, or replace footage, to isolate a bad file.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Symptom | First three moves |
|
||||
|---|---|
|
||||
| Choppy RAM preview | Half resolution → enable MFR → purge cache |
|
||||
| Tiny green preview bar | Raise RAM allocation → SSD disk cache → trim work area |
|
||||
| Slow final render | Enable MFR → HW decode H.264 → pre-render precomps |
|
||||
| Crash on launch | Reset prefs (modifier keys) → empty disk cache → update GPU driver |
|
||||
| Crash mid-render | Free disk space → disable/reserve CPUs for MFR → disable 3rd-party plugins |
|
||||
| Started after update | Roll back AE version → update plugins → reset prefs |
|
||||
| Laggy with footage | HW-accelerated decode → transcode to ProRes → proxy heavy comps |
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **MFR needs RAM per core.** With many cores but little RAM, MFR can underperform or stall — aim for several GB per active core.
|
||||
- **Game-Ready drivers crash AE.** Prefer the **Studio** driver branch on NVIDIA.
|
||||
- **Disk Cache on an external HDD is worse than none** — it must be SSD/NVMe to help.
|
||||
- **Plugins lag behind AE updates.** After a major AE upgrade, a sudden crash wave usually means a plugin needs updating or is not MFR-safe.
|
||||
- **Purging clears the green bar.** Previews must rebuild afterward; that initial slowness is expected, not a new problem.
|
||||
- **CPU/GPU effects are not equal.** A few GPU-accelerated effects are fast; mixing many CPU-only effects forces round-trips that kill preview speed.
|
||||
- **Don't render in AE and AME simultaneously** on the same machine — they fight for cores and RAM.
|
||||
|
||||
---
|
||||
|
||||
# AE Performance — Detailed Diagnostics Reference
|
||||
|
||||
## Preference locations and recommended values
|
||||
|
||||
| Setting | Path | Recommended |
|
||||
|---|---|---|
|
||||
| RAM for AE | Preferences > Memory & Performance | Total RAM minus ~6 GB for OS |
|
||||
| Multi-Frame Rendering | Memory & Performance > Enable MFR | On; reserve 0–2 CPUs for other apps |
|
||||
| Disk Cache | Media & Disk Cache > Enable Disk Cache | On, SSD/NVMe, 50–150 GB |
|
||||
| Conformed media cache | Media & Disk Cache | On SSD, separate from project if possible |
|
||||
| HW decode (H.264/HEVC) | Import > Hardware Accelerate Decoding | On |
|
||||
| Mercury GPU | Project Settings > Video Rendering and Effects | GPU (CPU as crash test) |
|
||||
| Fast Previews | Viewer lightning icon | Adaptive Resolution |
|
||||
|
||||
## Multi-Frame Rendering (MFR) tuning
|
||||
- Introduced in AE 2022 (22.x); renders multiple frames in parallel across CPU cores for both preview and final render.
|
||||
- **RAM is the limiter.** Each rendering frame needs memory; with too little RAM, AE quietly reduces the cores it uses. Target several GB of free RAM per logical core you want active.
|
||||
- "CPUs reserved for other applications" trades render speed for system responsiveness. Set 0–2 on a dedicated machine.
|
||||
- Some legacy/3rd-party effects are **not MFR-safe** and can crash or render incorrectly under MFR. If a specific project crashes only with MFR on, disable MFR for that project or update the plugin.
|
||||
|
||||
## GPU and drivers
|
||||
- Use the **NVIDIA Studio Driver** (not Game Ready) for stability with Adobe apps; keep AMD drivers current too.
|
||||
- Mercury GPU acceleration speeds some effects and the renderer. If AE crashes on launch or while playing back, switch `Project Settings > Video Rendering and Effects` to **Mercury Software Only** to confirm GPU/driver fault, then update drivers.
|
||||
- Hardware-accelerated **decoding** (separate from rendering) speeds reading H.264/HEVC source footage; enable in `Preferences > Import`.
|
||||
|
||||
## Disk cache sizing and placement
|
||||
- Put the disk cache on the **fastest local drive** (internal NVMe ideal). Never on a slow USB HDD or a network drive.
|
||||
- 50–150 GB is typical; more helps long projects retain cached frames between sessions.
|
||||
- Symptoms of a bad cache: hangs on open, garbage frames, refusal to play. Fix via `Empty Disk Cache` and `Edit > Purge > All`.
|
||||
- Keep the cache drive from filling up — a full cache/scratch disk causes mid-render freezes.
|
||||
|
||||
## Pre-render and proxy workflow
|
||||
1. Identify a slow, finished precomp (heavy effects, particles, 3D, tracking).
|
||||
2. Add it to the Render Queue and render to **ProRes 422/422 HQ** (or PNG sequence).
|
||||
3. Right-click the footage/comp in the Project panel → **Set Proxy > File**, select the rendered file. A box icon shows the proxy is active.
|
||||
4. Toggle the proxy off for final render if you want to re-render at full quality, or keep it if the pre-render is final.
|
||||
5. Alternatively `File > Create Proxy > Movie/Still` to queue a proxy directly.
|
||||
- Proxies let AE substitute a cheap-to-read file for an expensive comp/footage during editing.
|
||||
|
||||
## Heavy effect culprits (preview/render cost)
|
||||
- Gaussian Blur, Camera Lens Blur, Glow → use Fast Box Blur where acceptable.
|
||||
- Particle systems (Particular, Form), Element 3D, Plexus.
|
||||
- Mocha tracking, Warp Stabilizer (analysis), motion tracking.
|
||||
- Expressions that read many layers/time, especially per-frame heavy ones.
|
||||
- Nested precomps with collapsed transformations and continuous rasterization.
|
||||
- Ray-traced / Cinema 4D 3D renderer vs Classic 3D.
|
||||
|
||||
## Version-specific culprits
|
||||
- After a **major AE update**, a sudden crash/slowdown wave usually traces to a third-party plugin not yet updated for that AE version — update plugins or remove them to test.
|
||||
- If problems began immediately after updating AE, install the **previous version** from Creative Cloud → app → Other Versions, and report the regression.
|
||||
- Reset corrupt preferences by holding `Ctrl+Alt+Shift` (Win) / `Cmd+Opt+Shift` (Mac) during launch; this resolves many post-update oddities.
|
||||
|
||||
## Isolation checklist for crashes
|
||||
1. Reset preferences (modifier-key launch).
|
||||
2. Empty disk cache + Purge All.
|
||||
3. Update GPU driver (Studio branch).
|
||||
4. Move 3rd-party plugins out, relaunch, reintroduce one at a time.
|
||||
5. Switch Mercury to Software Only to rule out GPU.
|
||||
6. Import comps/footage incrementally to find a corrupt asset.
|
||||
7. Disable MFR for the project if crashes are MFR-specific.
|
||||
8. Roll back AE version if the crash wave is version-correlated.
|
||||
|
||||
## When to pre-compose vs pre-render
|
||||
- **Pre-compose** to organize and to apply an effect to a group — does not by itself speed anything up (it can even add overhead).
|
||||
- **Pre-render** (render to a file + use as proxy/footage) when a precomp is finished and slow — this is the actual performance win.
|
||||
@@ -0,0 +1,188 @@
|
||||
# Project Hygiene — Organize, Relink, ExtendScript
|
||||
|
||||
Keep AE projects organized, portable, and automatable: consistent naming, a clear folder/bin structure, label discipline, pre-comp discipline, reliable relinking when files move, and ExtendScript (.jsx) automation for repetitive tasks.
|
||||
|
||||
## Folder (bin) structure
|
||||
|
||||
Create folders in the Project panel (the folder icon at the bottom). A reliable layout:
|
||||
|
||||
```
|
||||
01_Comps/ finished/active compositions
|
||||
02_Precomps/ nested precomps
|
||||
03_Footage/ imported video
|
||||
04_Images/ stills, logos, textures
|
||||
05_Audio/
|
||||
06_Solids/ (AE's auto "Solids" folder — keep it here)
|
||||
07_Assets_3D/
|
||||
99_Old/ deprecated comps/layers, not deleted yet
|
||||
```
|
||||
|
||||
Numeric prefixes force a stable sort order. After importing, immediately drag items into the right bin — `File > Reduce Project` and `File > Consolidate All Footage` help later, but discipline up front prevents chaos.
|
||||
|
||||
## Naming conventions
|
||||
|
||||
- **Comps:** `PROJECT_section_descriptor_v01` (e.g., `BRAND_intro_logoBuild_v03`). Version with zero-padded `v01`, `v02`.
|
||||
- **Precomps:** prefix `PRE_` or suffix `_pre` so they're obvious in lists.
|
||||
- **Layers:** rename solids/nulls to their role (`CTRL_master`, `BG_gradient`, `MATTE_text`). Never ship "Black Solid 12".
|
||||
- **No spaces / special chars** in filenames destined for render farms or cross-OS sharing — use `_` and `-`.
|
||||
- Keep names short but unique; long names truncate in the timeline.
|
||||
|
||||
## Label colors
|
||||
|
||||
Use the Label color (the swatch left of each Project/Timeline item) as a visual system. Pick a convention and apply it consistently, e.g.:
|
||||
- Red = control/null layers
|
||||
- Blue = footage
|
||||
- Green = precomps
|
||||
- Yellow = text
|
||||
- Purple = adjustment/effects layers
|
||||
|
||||
Right-click an item → **Label** to set. Select multiple, then `Edit > Label > Select Label Group` to act on a whole color at once. Customize label names/colors in `Preferences > Labels`.
|
||||
|
||||
## Pre-comp discipline
|
||||
|
||||
- Pre-compose (`Layer > Pre-compose`, `Ctrl/Cmd+Shift+C`) to **group** related layers and to apply one effect to a group — not reflexively for everything.
|
||||
- When pre-composing a single layer, choose **"Leave all attributes in"** vs **"Move all attributes into the new composition"** deliberately: "Move" carries transforms/effects inside; "Leave" keeps them on the outer layer.
|
||||
- Name the precomp immediately and file it in `02_Precomps`.
|
||||
- Avoid deep nesting beyond a few levels — it slows navigation and makes relinking/debugging painful.
|
||||
- Match precomp duration and frame rate to need; an over-long precomp wastes cache.
|
||||
|
||||
## The "moved one file and everything broke" rescue
|
||||
|
||||
AE references footage by file path. Moving, renaming, or sending a project without its assets breaks links (shown as **colored bars / "Missing Footage"** placeholders).
|
||||
|
||||
### Fix options, in order
|
||||
1. **Relink one, fix many:** double-click a missing item (or right-click → **Replace Footage > File**). Point it at the moved file; AE offers to relink *all* missing items from the same folder automatically. This single step usually fixes the whole project.
|
||||
2. **Find Missing Footage:** `File > Dependencies > Find Missing Footage` lists every broken link to resolve.
|
||||
3. **Replace Footage:** right-click footage → **Replace Footage > File** to swap an asset while keeping every use, effect, and keyframe intact.
|
||||
|
||||
### Prevent it: Collect Files
|
||||
Before moving or sharing a project, use `File > Dependencies > Collect Files`. This copies the project plus every used asset into one new folder with relative structure, so links survive on any machine. Always hand off projects as a Collect Files package, not a bare `.aep`.
|
||||
|
||||
Related: `File > Dependencies > Consolidate All Footage` (merge duplicate imports), `Remove Unused Footage`, and `Reduce Project` (strip everything not used by selected comps) to slim a project before collecting.
|
||||
|
||||
## Batch automation with ExtendScript (.jsx)
|
||||
|
||||
AE scripts (ExtendScript / JSX) automate repetitive work. Run via `File > Scripts > Run Script File...`, or place in the **ScriptUI Panels** folder to dock as a panel. Always wrap edits in `app.beginUndoGroup("name") ... app.endUndoGroup()` so one Undo reverts the whole batch.
|
||||
|
||||
Common automations (full code in `../scripts/`):
|
||||
- **Batch rename** selected layers with a base name + auto-increment (`../scripts/batch-rename.jsx`).
|
||||
- **Sequential offset** — stagger selected layers in time by N frames each, a staggered "domino" start (`../scripts/sequential-offset.jsx`).
|
||||
- **Apply an effect** (e.g., a Gaussian Blur, or any matchName) across all selected layers at once (`../scripts/apply-effect-to-layers.jsx`).
|
||||
|
||||
Minimal pattern:
|
||||
```javascript
|
||||
app.beginUndoGroup("My Batch Op");
|
||||
var comp = app.project.activeItem;
|
||||
if (comp && comp instanceof CompItem) {
|
||||
var sel = comp.selectedLayers;
|
||||
for (var i = 0; i < sel.length; i++) {
|
||||
// operate on sel[i]
|
||||
}
|
||||
}
|
||||
app.endUndoGroup();
|
||||
```
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Task | How |
|
||||
|---|---|
|
||||
| New bin | Folder icon at bottom of Project panel |
|
||||
| Set label color | Right-click item → Label |
|
||||
| Relink missing footage | Double-click item / Replace Footage > File |
|
||||
| List broken links | File > Dependencies > Find Missing Footage |
|
||||
| Package for handoff | File > Dependencies > Collect Files |
|
||||
| Swap an asset everywhere | Replace Footage > File |
|
||||
| Slim project | Reduce Project / Remove Unused Footage |
|
||||
| Run a script | File > Scripts > Run Script File... |
|
||||
| Pre-compose | Layer > Pre-compose (Ctrl/Cmd+Shift+C) |
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Collect Files before moving anything.** Moving the `.aep` alone always breaks links.
|
||||
- **Relinking one item often relinks the rest** from the same folder — try that before fixing each by hand.
|
||||
- **Replace Footage preserves effects/keyframes;** deleting + re-importing does not.
|
||||
- **AE auto-creates a "Solids" folder** — don't scatter solids elsewhere or it regenerates duplicates.
|
||||
- **Scripts need `app.beginUndoGroup`/`endUndoGroup`** or a batch becomes many separate undos.
|
||||
- **Applying effects by name needs the `matchName`,** not the display name (e.g., `"ADBE Gaussian Blur 2"`), to be reliable across AE language versions.
|
||||
- **Allow Scripts to Write Files** must be enabled in `Preferences > Scripting & Expressions` for scripts that save/render.
|
||||
- **Deep precomp nesting** makes both performance and relinking worse — keep it shallow.
|
||||
|
||||
---
|
||||
|
||||
# AE Project Organization — Detailed Reference
|
||||
|
||||
## Full folder (bin) template
|
||||
```
|
||||
PROJECT.aep
|
||||
├── 00_Output/ (final renders live next to project on disk)
|
||||
01_Comps/ master & active comps
|
||||
02_Precomps/ nested precomps (prefix PRE_)
|
||||
03_Footage/ video clips
|
||||
04_Images/ stills, logos, vector, textures
|
||||
05_Audio/ music, VO, SFX
|
||||
06_Solids/ AE auto-folder; leave it here
|
||||
07_Assets_3D/ OBJ/C4D/3D model assets
|
||||
08_Reference/ storyboards, briefs, temp ref
|
||||
99_Old/ deprecated, kept until final
|
||||
```
|
||||
Numeric prefixes lock sort order. Keep the on-disk asset folders mirroring these bins so Collect Files stays clean.
|
||||
|
||||
## Naming scheme
|
||||
| Item | Pattern | Example |
|
||||
|---|---|---|
|
||||
| Master comp | `PROJ_section_v##` | `BRAND_main_v04` |
|
||||
| Sub/shot comp | `PROJ_section_shot_v##` | `BRAND_intro_logo_v02` |
|
||||
| Precomp | `PRE_descriptor` | `PRE_textReveal` |
|
||||
| Null/control | `CTRL_role` | `CTRL_master` |
|
||||
| Solid/BG | `BG_role` | `BG_gradient` |
|
||||
| Matte | `MATTE_role` | `MATTE_logoTrack` |
|
||||
| Adjustment | `ADJ_role` | `ADJ_grade` |
|
||||
|
||||
Rules: zero-pad versions; no spaces/special characters for farm/cross-OS safety; keep unique and short.
|
||||
|
||||
## Label color system (example)
|
||||
| Color | Meaning |
|
||||
|---|---|
|
||||
| Red | Control / null layers |
|
||||
| Blue | Footage |
|
||||
| Green | Precomps |
|
||||
| Yellow | Text |
|
||||
| Purple | Adjustment / effects |
|
||||
| Orange | Audio |
|
||||
| Gray | Disabled / old |
|
||||
|
||||
Set via right-click → Label. Customize names/colors in `Preferences > Labels`. Use `Edit > Label > Select Label Group` to select everything of one color.
|
||||
|
||||
## Relink / missing footage — full workflow
|
||||
1. **Identify:** missing items show as colored bars with "Missing" placeholders; the Project panel marks them.
|
||||
2. **Relink one to fix many:** right-click a missing item → **Replace Footage > File** (or double-click it). Choose the relocated file. AE prompts to relink all other missing files found in the same folder — accept to fix the batch at once.
|
||||
3. **Enumerate:** `File > Dependencies > Find Missing Footage`, `Find Missing Fonts`, `Find Missing Effects` to locate every broken dependency.
|
||||
4. **Swap assets:** **Replace Footage > File** swaps the source while preserving all layer instances, effects, masks, and keyframes.
|
||||
|
||||
## Collect Files (portable handoff)
|
||||
- `File > Dependencies > Collect Files`.
|
||||
- Options: Collect Source Files = **All**; enable **Reduce Project** to drop unused; optionally **Generate Report**.
|
||||
- Produces a folder containing the `.aep` plus a `(Footage)` subfolder with every used asset, preserving relative links so the project opens anywhere.
|
||||
- Always deliver/move projects as a Collect Files package, never a lone `.aep`.
|
||||
|
||||
## Slimming a project
|
||||
- `File > Reduce Project` — removes all items not used by the currently selected comp(s). Destructive to unused items; save first.
|
||||
- `File > Dependencies > Consolidate All Footage` — merges duplicate imports of the same file.
|
||||
- `File > Dependencies > Remove Unused Footage` — drops footage no comp references.
|
||||
- Run these before Collect Files to keep the package small.
|
||||
|
||||
## Scripting setup
|
||||
- Enable `Preferences > Scripting & Expressions > Allow Scripts to Write Files and Access Network` for scripts that save or render.
|
||||
- Run a script: `File > Scripts > Run Script File...`.
|
||||
- Dock a panel script: place the `.jsx` in `(AE install)/Scripts/ScriptUI Panels/`, restart AE, open from the `Window` menu.
|
||||
- Wrap every batch in `app.beginUndoGroup("...")` / `app.endUndoGroup()` so it undoes in one step.
|
||||
- Apply effects by **matchName** (internal id like `"ADBE Gaussian Blur 2"`), not the localized display name, for cross-version reliability. Use `layer.property("ADBE Effect Parade").addProperty(matchName)`.
|
||||
|
||||
## Pre-comp discipline details
|
||||
- "Leave all attributes in the new composition" keeps the outer layer's transform; "Move all attributes" pushes them inside the precomp. Choosing wrong shifts/rescales the result.
|
||||
- Trim precomp duration to the content; over-long precomps waste RAM/disk cache.
|
||||
- Name and file precomps immediately; never leave "Pre-comp 1".
|
||||
- Keep nesting shallow (2–3 levels) for performance and easier relinking/debugging.
|
||||
|
||||
---
|
||||
Keep the project tidy and named well and footage relinks without a fight. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=motion-design-skills&utm_content=skill_footer&utm_term=after-effects)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* apply-effect-to-layers.jsx
|
||||
* Applies an effect (by internal matchName) to every selected layer.
|
||||
*
|
||||
* Why matchName: display names are localized and change between AE language
|
||||
* versions; matchName is stable. Examples:
|
||||
* "ADBE Gaussian Blur 2" -> Gaussian Blur
|
||||
* "ADBE Fast Box Blur" -> Fast Box Blur
|
||||
* "ADBE Tint" -> Tint
|
||||
* "ADBE Drop Shadow" -> Drop Shadow
|
||||
* "ADBE Glo2" -> Glow
|
||||
*
|
||||
* Usage: select layers in a comp, then File > Scripts > Run Script File...
|
||||
* Edit EFFECT_MATCHNAME (and the optional parameter block) below.
|
||||
*/
|
||||
|
||||
(function applyEffectToLayers() {
|
||||
var EFFECT_MATCHNAME = "ADBE Gaussian Blur 2";
|
||||
|
||||
var comp = app.project.activeItem;
|
||||
if (!(comp && comp instanceof CompItem)) {
|
||||
alert("Open a composition and select one or more layers first.");
|
||||
return;
|
||||
}
|
||||
|
||||
var sel = comp.selectedLayers;
|
||||
if (sel.length === 0) {
|
||||
alert("No layers selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
app.beginUndoGroup("Apply Effect To Layers");
|
||||
try {
|
||||
for (var i = 0; i < sel.length; i++) {
|
||||
var layer = sel[i];
|
||||
if (!(layer instanceof AVLayer)) continue; // effects need an AVLayer
|
||||
|
||||
var effects = layer.property("ADBE Effect Parade");
|
||||
if (!effects.canAddProperty(EFFECT_MATCHNAME)) continue;
|
||||
|
||||
var fx = effects.addProperty(EFFECT_MATCHNAME);
|
||||
|
||||
// --- Optional: set a parameter. Example for Gaussian Blur radius ---
|
||||
// try { fx.property("Blurriness").setValue(25); } catch (e) {}
|
||||
}
|
||||
} finally {
|
||||
app.endUndoGroup();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* batch-rename.jsx
|
||||
* Renames the selected layers in the active comp to: baseName_##
|
||||
* with a zero-padded, incrementing index.
|
||||
*
|
||||
* Usage: select layers in a comp, then File > Scripts > Run Script File...
|
||||
* Edit BASE_NAME, START_INDEX, and PAD below.
|
||||
*/
|
||||
|
||||
(function batchRename() {
|
||||
var BASE_NAME = "layer"; // base name applied to every selected layer
|
||||
var START_INDEX = 1; // first number
|
||||
var PAD = 2; // zero-padding width (2 -> 01, 02 ...)
|
||||
|
||||
var comp = app.project.activeItem;
|
||||
if (!(comp && comp instanceof CompItem)) {
|
||||
alert("Open a composition and select one or more layers first.");
|
||||
return;
|
||||
}
|
||||
|
||||
var sel = comp.selectedLayers;
|
||||
if (sel.length === 0) {
|
||||
alert("No layers selected.");
|
||||
return;
|
||||
}
|
||||
|
||||
function pad(n, width) {
|
||||
var s = String(n);
|
||||
while (s.length < width) s = "0" + s;
|
||||
return s;
|
||||
}
|
||||
|
||||
app.beginUndoGroup("Batch Rename Layers");
|
||||
try {
|
||||
for (var i = 0; i < sel.length; i++) {
|
||||
sel[i].name = BASE_NAME + "_" + pad(START_INDEX + i, PAD);
|
||||
}
|
||||
} finally {
|
||||
app.endUndoGroup();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* sequential-offset.jsx
|
||||
* Staggers the selected layers in time so each starts OFFSET_FRAMES after
|
||||
* the previous one (a "domino" / cascade start), based on the first
|
||||
* selected layer's start time.
|
||||
*
|
||||
* Usage: select layers in a comp, then File > Scripts > Run Script File...
|
||||
* Edit OFFSET_FRAMES and ORDER below.
|
||||
*/
|
||||
|
||||
(function sequentialOffset() {
|
||||
var OFFSET_FRAMES = 2; // frames between each layer's start
|
||||
var ORDER_BY_SELECTION = true; // true = order of selection; false = by current startTime
|
||||
|
||||
var comp = app.project.activeItem;
|
||||
if (!(comp && comp instanceof CompItem)) {
|
||||
alert("Open a composition and select two or more layers first.");
|
||||
return;
|
||||
}
|
||||
|
||||
var sel = comp.selectedLayers;
|
||||
if (sel.length < 2) {
|
||||
alert("Select at least two layers.");
|
||||
return;
|
||||
}
|
||||
|
||||
var frameDur = comp.frameDuration; // seconds per frame
|
||||
var offsetSec = OFFSET_FRAMES * frameDur;
|
||||
|
||||
// Build the working order.
|
||||
var layers = [];
|
||||
for (var i = 0; i < sel.length; i++) layers.push(sel[i]);
|
||||
if (!ORDER_BY_SELECTION) {
|
||||
layers.sort(function (a, b) { return a.startTime - b.startTime; });
|
||||
}
|
||||
|
||||
var base = layers[0].startTime;
|
||||
|
||||
app.beginUndoGroup("Sequential Offset");
|
||||
try {
|
||||
for (var j = 0; j < layers.length; j++) {
|
||||
layers[j].startTime = base + (j * offsetSec);
|
||||
}
|
||||
} finally {
|
||||
app.endUndoGroup();
|
||||
}
|
||||
})();
|
||||
File renamed without changes.
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: animation-principles
|
||||
description: This skill should be used when the user asks to "make this animation feel natural", "fix motion that feels stiff/floaty/cheap", "my animation looks robotic", "choose an easing curve", "how do I use the Graph Editor", "add overshoot or bounce", "Easy Ease isn't enough", "make snappy motion", "pick a duration for this transition", "stagger a list animation", "sync animation to a beat", or "review motion for good timing". It is the tech-agnostic foundation for deciding how something should move.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Motion Principles
|
||||
|
||||
Decide how something should move so it feels right — independent of the tool used to build it. This skill answers timing, easing, spacing, weight, and rhythm. It does not pick a tech stack; it produces concrete numbers (durations, cubic-bezier curves, stagger offsets, spring configs) ready to hand to any implementation.
|
||||
|
||||
## When to use
|
||||
|
||||
- Motion feels stiff, floaty, mechanical, robotic, or cheap and needs diagnosis.
|
||||
- Choosing a clip or transition's duration, easing curve, and stagger offset.
|
||||
- Reviewing whether motion respects physics intuition (weight, follow-through, arcs).
|
||||
- Synchronizing keyframes to a musical beat or edit accent.
|
||||
|
||||
## Three pillars — settle these before any number
|
||||
|
||||
Every piece of motion should answer three questions *before* you pick a duration or curve. Skip them and you get technically-correct motion that feels like nothing. These are the shared vocabulary the rest of the collection refers back to.
|
||||
|
||||
| Pillar | The question | What it drives |
|
||||
|---|---|---|
|
||||
| **Emotional intent** | What should the viewer *feel*? (joy, calm, urgency, trust, elegance) | Easing, duration, amplitude/overshoot |
|
||||
| **Visual narrative** | What's the micro-story? Setup → action → resolution | Sequencing, staging, what enters when |
|
||||
| **Motion craft** | How do we make it believable? | Physics, secondary motion, arcs, follow-through |
|
||||
|
||||
## Three motion layers — depth, not flatness
|
||||
|
||||
Flat motion = only the main thing moves. Believable motion runs three layers at once:
|
||||
|
||||
- **Primary** — the main action the eye follows (the hero move).
|
||||
- **Secondary** — supporting richness reacting to the primary (a shadow shifting, an icon nudging, a label settling late).
|
||||
- **Ambient** — background life that never demands attention (slow gradient drift, a low-contrast pulse).
|
||||
|
||||
> The judgment layer (`motion-art-direction`) calls these **Hero / Support / Texture** — same three layers, ranked for *what earns motion*. Primary≈Hero, Secondary≈Support, Ambient≈Texture.
|
||||
|
||||
## Core principles
|
||||
|
||||
### Easing is the single biggest lever
|
||||
|
||||
Linear motion reads as robotic. Reserve `linear` for continuous loops (spinners, marquees, infinite scroll) only. Everything an eye perceives as a discrete event needs acceleration.
|
||||
|
||||
Match the curve to the action:
|
||||
- **Enter / appear** -> ease-out (fast start, gentle settle). Object arrives with energy and decelerates into place. `cubic-bezier(0.16, 1, 0.3, 1)`.
|
||||
- **Exit / disappear** -> ease-in (gentle start, fast end). Object accelerates away. `cubic-bezier(0.7, 0, 0.84, 0)`.
|
||||
- **Move / reposition (stays on screen)** -> ease-in-out (symmetric). `cubic-bezier(0.65, 0, 0.35, 1)`.
|
||||
- **Playful / branded** -> slight overshoot. `cubic-bezier(0.34, 1.56, 0.64, 1)` (the `1.56` pushes past 1.0, creating an overshoot-and-settle that reads as "weight").
|
||||
|
||||
Rule of thumb: the eye forgives a slow start far less than a slow end. When in doubt, decelerate into rest.
|
||||
|
||||
**Hand-shaping easing (After Effects).** When keyframing by hand rather than supplying a curve, the same principles map to the Graph Editor: shape the Speed Graph so the ends flatten toward zero, and push deceleration influence to 80–90% for a snappy launch and smooth settle. `F9` (Easy Ease) is only a 33% symmetric starting point — make it asymmetric. See `references/easing-graph-editor.md` for Graph Editor reading, handle/influence techniques, and step-by-step overshoot, anticipation, and bounce recipes (including a decaying-bounce expression).
|
||||
|
||||
### Timing communicates weight and importance
|
||||
|
||||
| Element | Duration |
|
||||
|---|---|
|
||||
| Micro-interaction (hover, toggle, tap feedback) | 100-200ms |
|
||||
| UI transition (panel, modal, page region) | 200-400ms |
|
||||
| Hero / large element / full-screen | 400-800ms |
|
||||
| Cinematic camera move | 800-2000ms |
|
||||
|
||||
Distance and size scale duration. A small icon moving 20px and a full-bleed panel moving 800px should NOT share a duration — the panel needs more time or it looks weightless. A practical scaling: keep perceived velocity roughly constant by adding ~30-50% duration when distance or area roughly doubles. Heavy objects start slowly and overshoot less; light objects snap and may bounce.
|
||||
|
||||
### Spacing and stagger create rhythm
|
||||
|
||||
Never reveal a list, grid, or group all at once — it reads as a single flat event. Stagger each child's start.
|
||||
|
||||
- Lists / sequential items: **40-80ms** between items.
|
||||
- Dense grids: **20-40ms** (more items, less per-item delay, or the tail drags).
|
||||
- Cap total reveal at roughly **600-800ms** for a group; if `count * offset` exceeds that, shrink the offset or switch to a distance-based stagger (items further from an origin point start later).
|
||||
- Stagger direction should follow the eye: top-to-bottom, or radiating from a focal point.
|
||||
|
||||
**The 1/3 Rule (two forms, both universal):**
|
||||
- *Distance* — no element travels more than ~1/3 of the screen without an intermediate keyframe or a scale/opacity change. Long unbroken slides read as "the template moved," not "something happened."
|
||||
- *Simultaneity* — with 3+ elements, keep no more than ~1/3 in active motion at once. The rest hold or move as ambient. Everything moving together = noise with no focal point.
|
||||
|
||||
### Anticipation and follow-through sell physical motion
|
||||
|
||||
- **Anticipation**: a tiny counter-move before the main action (a button dips `scale 0.95` before popping, a character crouches before jumping). 60-120ms is enough.
|
||||
- **Follow-through / overlapping action**: trailing parts keep moving after the main body stops (a hero card lands, then its shadow and label settle 40-80ms later). Stagger the settle of attached elements rather than stopping everything on the same frame.
|
||||
- **Arcs**: natural movement curves; pure straight-line translation of an organic object looks mechanical. Add a slight arc to x/y or animate a path.
|
||||
|
||||
### Beat-sync and rhythm
|
||||
|
||||
When motion accompanies audio, land impact keyframes (a hit, a cut, a reveal) on the beat, not between beats. At 120 BPM a beat is 500ms; an eighth-note grid is 250ms. Quantize key moments to that grid. For edits without music, establish a visual pulse (e.g. one major event per ~500-800ms) so the piece breathes consistently.
|
||||
|
||||
### Spring vs. duration-based motion
|
||||
|
||||
Springs (mass / stiffness / damping) self-determine duration and feel more physical for interactive, interruptible motion (drag-release, gestures). Duration+easing is better for choreographed, timeline-locked sequences (video, scroll-tied reveals) where exact sync matters. A snappy default spring: `stiffness 300, damping 30, mass 1`. See `references/easing-curves.md` for a full spring table.
|
||||
|
||||
## Diagnosing common failures
|
||||
|
||||
- **Feels stiff/robotic** -> using `linear` or symmetric easing on an enter; switch to ease-out.
|
||||
- **Feels floaty/sluggish** -> duration too long or ease-out too gentle; cut duration 30%, sharpen the curve.
|
||||
- **Feels cheap/janky** -> everything appears at once (no stagger), or all elements share one duration regardless of size.
|
||||
- **Feels mechanical despite easing** -> no anticipation, no follow-through, straight-line paths; add arcs and overlap.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Need | Use |
|
||||
|---|---|
|
||||
| Enter | `cubic-bezier(0.16, 1, 0.3, 1)`, 300-500ms |
|
||||
| Exit | `cubic-bezier(0.7, 0, 0.84, 0)`, 200-300ms |
|
||||
| Move | `cubic-bezier(0.65, 0, 0.35, 1)`, 300-400ms |
|
||||
| Branded pop | `cubic-bezier(0.34, 1.56, 0.64, 1)`, 400-600ms |
|
||||
| List stagger | 40-80ms per item, cap ~700ms total |
|
||||
| Loop only | `linear` |
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/easing-curves.md` — named cubic-bezier library with exact values, spring configs (Framer/react-spring/iOS), and per-element-type duration guidance.
|
||||
- `references/easing-graph-editor.md` — After Effects Graph Editor craft: speed graph vs value graph, easing handles and influence/velocity numbers, and step-by-step snappy, overshoot, anticipation, and bounce recipes.
|
||||
- `references/twelve-principles.md` — all 12 Disney principles, each with a concrete, numeric motion-graphics example.
|
||||
@@ -0,0 +1,136 @@
|
||||
# Easing curves, springs, and durations
|
||||
|
||||
A reference library of exact values. Copy directly.
|
||||
|
||||
## Named cubic-bezier library
|
||||
|
||||
These are battle-tested curves. The four numbers are `(x1, y1, x2, y2)` control points.
|
||||
|
||||
### Ease-out family (use for ENTER / appear / settle)
|
||||
|
||||
| Name | cubic-bezier | Feel |
|
||||
|---|---|---|
|
||||
| easeOutQuad | `0.5, 1, 0.89, 1` | gentle, subtle |
|
||||
| easeOutCubic | `0.33, 1, 0.68, 1` | balanced, safe default |
|
||||
| easeOutQuart | `0.25, 1, 0.5, 1` | snappy |
|
||||
| easeOutQuint | `0.22, 1, 0.36, 1` | very snappy, decisive |
|
||||
| easeOutExpo | `0.16, 1, 0.3, 1` | dramatic, "premium" arrival |
|
||||
| easeOutCirc | `0, 0.55, 0.45, 1` | strong late deceleration |
|
||||
|
||||
### Ease-in family (use for EXIT / disappear / accelerate away)
|
||||
|
||||
| Name | cubic-bezier | Feel |
|
||||
|---|---|---|
|
||||
| easeInQuad | `0.11, 0, 0.5, 0` | gentle pull-away |
|
||||
| easeInCubic | `0.32, 0, 0.67, 0` | balanced exit |
|
||||
| easeInQuart | `0.5, 0, 0.75, 0` | sharp exit |
|
||||
| easeInExpo | `0.7, 0, 0.84, 0` | dramatic, fast vanish |
|
||||
|
||||
### Ease-in-out family (use for MOVE / reposition while on screen)
|
||||
|
||||
| Name | cubic-bezier | Feel |
|
||||
|---|---|---|
|
||||
| easeInOutQuad | `0.45, 0, 0.55, 1` | smooth, neutral |
|
||||
| easeInOutCubic | `0.65, 0, 0.35, 1` | balanced default |
|
||||
| easeInOutQuart | `0.76, 0, 0.24, 1` | punchy in and out |
|
||||
| easeInOutExpo | `0.87, 0, 0.13, 1` | very dramatic, long hold mid |
|
||||
|
||||
### Overshoot / back family (use for PLAYFUL / branded / "weighty")
|
||||
|
||||
| Name | cubic-bezier | Notes |
|
||||
|---|---|---|
|
||||
| easeOutBack | `0.34, 1.56, 0.64, 1` | overshoots past target then settles |
|
||||
| easeInBack | `0.36, 0, 0.66, -0.56` | dips below before launching (anticipation) |
|
||||
| easeInOutBack | `0.68, -0.6, 0.32, 1.6` | anticipate + overshoot |
|
||||
|
||||
The `y` value above 1 (e.g. `1.56`) or below 0 (e.g. `-0.56`) is what creates overshoot/anticipation. Bigger excursion = more bounce.
|
||||
|
||||
## Material Design standard curves (good for product UI)
|
||||
|
||||
| Token | cubic-bezier | Use |
|
||||
|---|---|---|
|
||||
| Standard | `0.2, 0, 0, 1` | most transitions |
|
||||
| Decelerate (enter) | `0, 0, 0, 1` | elements entering screen |
|
||||
| Accelerate (exit) | `0.3, 0, 1, 1` | elements leaving screen |
|
||||
| Emphasized | `0.2, 0, 0, 1` (300-500ms) | hero moments |
|
||||
|
||||
## iOS / Apple feel
|
||||
|
||||
Apple favors springs, but the closest bezier is `0.25, 0.1, 0.25, 1` (the legacy `ease`) sharpened toward `0.33, 1, 0.68, 1`. For modal presentation Apple uses a spring close to `stiffness 280, damping 28`.
|
||||
|
||||
## Spring configurations
|
||||
|
||||
Springs are defined by stiffness (tension), damping (friction), and mass. Higher stiffness = faster; higher damping = less bounce; higher mass = more sluggish/heavy.
|
||||
|
||||
### Framer Motion (`type: "spring"`)
|
||||
|
||||
```js
|
||||
// Snappy default — interactive UI
|
||||
{ type: "spring", stiffness: 300, damping: 30, mass: 1 }
|
||||
|
||||
// Bouncy — playful, branded
|
||||
{ type: "spring", stiffness: 400, damping: 17, mass: 1 }
|
||||
|
||||
// Gentle — large/heavy elements, modals
|
||||
{ type: "spring", stiffness: 120, damping: 20, mass: 1 }
|
||||
|
||||
// Stiff/no bounce — precise, critically damped
|
||||
{ type: "spring", stiffness: 500, damping: 50, mass: 1 }
|
||||
|
||||
// Alternative API: duration-based spring (Framer Motion v10+)
|
||||
{ type: "spring", duration: 0.5, bounce: 0.25 } // bounce 0 = no overshoot, 0.5 = lively
|
||||
```
|
||||
|
||||
### react-spring presets
|
||||
|
||||
```js
|
||||
import { config } from '@react-spring/web'
|
||||
config.default // { tension: 170, friction: 26 }
|
||||
config.gentle // { tension: 120, friction: 14 }
|
||||
config.wobbly // { tension: 180, friction: 12 } — visible bounce
|
||||
config.stiff // { tension: 210, friction: 20 }
|
||||
config.slow // { tension: 280, friction: 60 } — heavy, no bounce
|
||||
config.molasses // { tension: 280, friction: 120 }
|
||||
```
|
||||
|
||||
### SwiftUI / iOS
|
||||
|
||||
```swift
|
||||
.spring(response: 0.4, dampingFraction: 0.8) // response = approx duration, dampingFraction 1.0 = no bounce
|
||||
.interactiveSpring() // for gesture-driven
|
||||
```
|
||||
|
||||
## Duration guidance per element type
|
||||
|
||||
| Element / event | Duration | Easing |
|
||||
|---|---|---|
|
||||
| Hover state, tap highlight | 100-150ms | easeOutQuad |
|
||||
| Toggle, checkbox, switch | 150-200ms | easeOutCubic |
|
||||
| Tooltip, dropdown open | 150-250ms | easeOutCubic |
|
||||
| Button press feedback | 80-120ms down / 200ms up | easeOut + back on release |
|
||||
| Modal / dialog enter | 300-400ms | easeOutExpo or spring(gentle) |
|
||||
| Modal / dialog exit | 200-250ms | easeInQuart |
|
||||
| Page / route region transition | 300-500ms | easeInOutCubic |
|
||||
| Card / list item reveal | 400-500ms each | easeOutExpo |
|
||||
| Hero headline / full-screen | 500-800ms | easeOutExpo |
|
||||
| Cinematic camera push/pan | 800-2000ms | easeInOutQuad |
|
||||
| Loading spinner | continuous | linear |
|
||||
| Number count-up | 600-1200ms | easeOutExpo |
|
||||
|
||||
## Distance-to-duration scaling
|
||||
|
||||
To keep perceived velocity natural, scale duration with travel distance:
|
||||
|
||||
```
|
||||
duration_ms ≈ base_ms * (distance_px / base_distance_px) ^ 0.5
|
||||
```
|
||||
|
||||
The `^0.5` (square root) prevents long moves from feeling tediously slow while still granting them more time than short moves. Example: base 300ms at 200px -> a 800px move ≈ 300 * sqrt(4) = 600ms.
|
||||
|
||||
## Stagger math
|
||||
|
||||
```
|
||||
total = (count - 1) * offset + per_item_duration
|
||||
```
|
||||
|
||||
Keep `total` under ~800ms for group reveals. If it exceeds that, reduce `offset` or use distance-based staggering (each item's delay proportional to its distance from a focal origin), which keeps a constant feel regardless of count.
|
||||
@@ -0,0 +1,153 @@
|
||||
# Easing in the After Effects Graph Editor
|
||||
|
||||
Tool-specific craft for turning the abstract easing principles into hand-shaped After Effects keyframes. The concepts here (slow-in/slow-out, asymmetric easing, overshoot, bounce) apply everywhere; the controls are AE's Graph Editor.
|
||||
|
||||
## The principle: slow-in / slow-out
|
||||
|
||||
Natural motion accelerates from rest and decelerates to rest — "slow-in, slow-out". Constant-speed (linear) motion reads as robotic because nothing physical starts and stops instantly. Easing means shaping how fast a property changes over time, not just its start and end values.
|
||||
|
||||
## Graph Editor basics
|
||||
|
||||
Open with the **Graph Editor** button (the curve icon) in the Timeline (or Shift+F3). It replaces the keyframe strip with curves.
|
||||
|
||||
Two graph modes, switched via the **graph type menu** at the bottom of the Graph Editor:
|
||||
|
||||
- **Value Graph** — plots the property's value over time (e.g., X position in pixels). The line's height = the value; its slope = the speed.
|
||||
- **Speed Graph** — plots the rate of change (speed) over time. The line's height = speed; a hump = fast, a valley near zero = slow/stopped.
|
||||
|
||||
**How to tell which is shown:** Open the graph type menu → "Edit Value Graph" / "Edit Speed Graph" shows the current choice (the active one is checked). Quick tell: for a single moving property, the **Speed Graph** reads as a hill (accelerate up, decelerate down); the **Value Graph** reads as an S-curve climbing from start value to end value. Spatial properties (Position) usually default to the Speed Graph; single-dimension properties to the Value Graph.
|
||||
|
||||
For shaping ease feel, the **Speed Graph** is the most intuitive: flatten the ends toward zero (slow start/stop), let the middle rise (fast).
|
||||
|
||||
### Display tips
|
||||
- Enable "Show Reference Graph" to see the other graph faintly behind the editable one.
|
||||
- "Snap" and "Fit selection to view" make handle work easier.
|
||||
|
||||
## Why Easy Ease is rarely enough
|
||||
|
||||
`F9` (Easy Ease) applies a symmetric ease with ~33% influence on both sides. It's a starting point, not a finished move. Two reasons it falls flat:
|
||||
|
||||
1. **Symmetry.** Most compelling motion is asymmetric — e.g., snappy UI eases *out* hard (fast start, long glide to stop) far more than it eases in.
|
||||
2. **Low influence.** 33% is gentle. Strong, modern motion-design easing pushes influence to **80–90%** on the deceleration side, producing a sharp launch and a smooth settle.
|
||||
|
||||
So: start with `F9`, then open the Graph Editor and **drag the bezier handles** to make it asymmetric and increase influence.
|
||||
|
||||
## Handle techniques
|
||||
|
||||
In the Graph Editor each keyframe has bezier handles:
|
||||
|
||||
- **Pull a handle horizontally (longer)** → more influence → the ease extends further in time → smoother, slower approach.
|
||||
- **Pull a handle down toward zero speed** → the property comes more fully to rest at that keyframe.
|
||||
- **Ease out 80–90%:** select the final keyframe, drag its incoming handle nearly flat and long so speed glides to near-zero over most of the move. This is the core "snappy" feel.
|
||||
- **Overshoot:** let the value go past the target, then settle back. In the Value Graph, push the curve above the end value briefly, then return. Or add a small keyframe past the target and ease back.
|
||||
- **Bounce:** repeated decaying overshoots — the value hits, overshoots less each time, settling. Build with successive keyframes of decreasing amplitude, each eased.
|
||||
|
||||
## Influence and velocity numbers
|
||||
|
||||
Right-click a keyframe → **Keyframe Velocity** to set exact values:
|
||||
|
||||
- **Incoming/Outgoing Velocity** — the speed at the keyframe (units/sec). Zero = full stop.
|
||||
- **Influence %** — how far the ease extends toward the neighboring keyframe. Higher = smoother, longer ease.
|
||||
|
||||
Targets:
|
||||
- Snappy UI move: outgoing influence ~15–25% (quick launch), **incoming influence 80–90%** (long settle).
|
||||
- Smooth symmetric float: both sides ~50–65%.
|
||||
- Anticipation: a small reverse move before the main action (pull back, then go).
|
||||
|
||||
## Recipe table
|
||||
|
||||
| Feel | Speed Graph shape | Outgoing inf. | Incoming inf. | Notes |
|
||||
|---|---|---|---|---|
|
||||
| Snappy UI | sharp rise, long tail to 0 | 10–25% | 80–90% | The default "good" motion-design ease |
|
||||
| Smooth float | gentle symmetric hill | 50–65% | 50–65% | Calm, even, no snap |
|
||||
| Anticipation | dip below 0 then rise | add pre-keyframe | 70–85% | Tiny reverse move first |
|
||||
| Overshoot | rise, overshoot value, settle | 20–40% | 85–95% | 1 extra keyframe past target |
|
||||
| Bounce | repeated decaying humps | per-bounce | per-bounce | Decreasing amplitude keyframes |
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Action | How |
|
||||
|---|---|
|
||||
| Open Graph Editor | Curve icon in Timeline (or Shift+F3) |
|
||||
| Apply Easy Ease | `F9` (Ease In `Shift+F9`, Ease Out `Ctrl/Cmd+Shift+F9`) |
|
||||
| Switch Value/Speed graph | Graph type menu (bottom of Graph Editor) |
|
||||
| Set exact velocity | Right-click keyframe → Keyframe Velocity |
|
||||
| Convert to bezier handles | Easy Ease, then drag handles |
|
||||
| Strong settle | Incoming influence 80–90% on last keyframe |
|
||||
|
||||
## Step-by-step recipes
|
||||
|
||||
### Snappy UI ease (the workhorse)
|
||||
1. Set two Position (or Scale) keyframes for the move.
|
||||
2. Select both, press `F9` (Easy Ease).
|
||||
3. Open Graph Editor → Speed Graph.
|
||||
4. Select the **first** keyframe; shorten its outgoing handle (low influence, ~15–25%) so it launches fast.
|
||||
5. Select the **last** keyframe; pull its incoming handle long and nearly flat to the time axis — influence **80–90%** — so it glides to a stop.
|
||||
6. Result: fast start, long smooth settle. This is the standard "premium" motion-design feel.
|
||||
|
||||
Exact via Keyframe Velocity (right-click keyframe):
|
||||
- First keyframe: Outgoing influence 18%, velocity 0.
|
||||
- Last keyframe: Incoming influence 85%, velocity 0.
|
||||
|
||||
### Overshoot
|
||||
Goal: element flies in, slightly passes its target, settles back.
|
||||
1. Animate from start to target value (2 keyframes), ease.
|
||||
2. Add a **third keyframe ~6–10 frames before** the final, set its value **past** the target (e.g., target 100% → set 112%).
|
||||
3. Final keyframe returns to target (100%).
|
||||
4. Ease all keyframes; give the settle keyframes high incoming influence (85–95%).
|
||||
5. Tune overshoot amount (how far past) and settle time for snappiness.
|
||||
|
||||
Value Graph view: the curve rises above the target line, then dips back down to it.
|
||||
|
||||
### Anticipation
|
||||
Goal: a small reverse move before the main action (wind-up).
|
||||
1. Before the main motion's first keyframe, add a keyframe that moves **slightly opposite** the main direction (e.g., a jump-up preceded by a small crouch-down).
|
||||
2. Ease the wind-up gently, then the main move fast (low outgoing influence).
|
||||
3. Optionally combine with overshoot on the landing for a full squash-stretch feel.
|
||||
|
||||
### Bounce (hand-keyed)
|
||||
1. First keyframe: rest/start.
|
||||
2. Impact keyframe: the object hits the floor value.
|
||||
3. Bounce keyframes: each successive peak is **lower** than the last (decaying amplitude), spaced **closer** in time (decreasing period).
|
||||
4. At each floor contact, set incoming/outgoing velocity sharp (near-instant direction change); at each peak, ease to near-zero speed (apex).
|
||||
5. 3–4 decaying bounces usually read as natural.
|
||||
|
||||
Amplitude example for a drop settling to 0: 0 → -200 (rise) → 0 → -90 → 0 → -35 → 0 → -12 → 0, with shrinking time gaps.
|
||||
|
||||
### Bounce (expression)
|
||||
Apply to a property (e.g., Position) after a single ease-in keyframe to auto-generate decaying bounce:
|
||||
```javascript
|
||||
amp = 0.12; // bounce strength
|
||||
freq = 2.5; // bounces per second
|
||||
decay = 5.0; // how fast it settles
|
||||
n = 0;
|
||||
if (numKeys > 0) { n = nearestKey(time).index; if (key(n).time > time) n--; }
|
||||
if (n > 0) {
|
||||
t = time - key(n).time;
|
||||
v = velocityAtTime(key(n).time - thisComp.frameDuration/10);
|
||||
value + v*(amp*Math.sin(freq*t*2*Math.PI)/Math.exp(decay*t));
|
||||
} else { value; }
|
||||
```
|
||||
- `amp` raises the overshoot height; `freq` the number of wobbles; `decay` how quickly it stops.
|
||||
- Place a single eased keyframe (or two) where the motion arrives; the expression adds the bounce after.
|
||||
|
||||
## Velocity / influence cheat sheet
|
||||
|
||||
| Feel | Out influence | In influence | Velocity at stops |
|
||||
|---|---|---|---|
|
||||
| Snappy UI | 15–25% | 80–90% | 0 |
|
||||
| Smooth float | 50–65% | 50–65% | 0 |
|
||||
| Mechanical/linear | 0% | 0% | constant |
|
||||
| Overshoot settle | 20–40% | 85–95% | 0 at final |
|
||||
| Bounce apex | high | high | 0 at apex |
|
||||
| Bounce impact | low | low | sharp/non-zero |
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Easy Ease ≠ finished.** It's a 33% symmetric starting point; almost always refine in the Graph Editor.
|
||||
- **Know your graph.** Editing a Value Graph thinking it's a Speed Graph produces confusing results — confirm in the graph type menu.
|
||||
- **Influence is per-side.** Asymmetric influence (low out, high in) is what makes UI motion feel snappy.
|
||||
- **Spatial vs temporal.** Position has both a motion path (spatial bezier in the Composition viewer) and speed easing (Graph Editor). A floaty arc may be a spatial handle issue, not temporal. Right-click a Position keyframe for **Rove Across Time** and spatial interpolation (Linear/Auto/Continuous Bezier).
|
||||
- **Auto-Bezier roving keyframes** can smooth speed across multiple keyframes but remove precise per-keyframe timing — use deliberately.
|
||||
- **Overshoot needs room past the target** — clamp/limits or layout edges can hide it.
|
||||
- **Bounce by hand is tedious;** expression-driven bounce (decaying sine) is common, but hand-keyed gives the most control.
|
||||
@@ -0,0 +1,82 @@
|
||||
# The 12 principles of animation, applied to motion graphics
|
||||
|
||||
The Disney principles, translated to UI/motion-graphics with concrete numeric examples. Ordered by how often they matter in screen motion (most-used first).
|
||||
|
||||
## 1. Slow in and slow out (easing)
|
||||
|
||||
The most important. Objects accelerate and decelerate; they don't snap to constant velocity.
|
||||
|
||||
Example: a card sliding in from the right travels 600px over 450ms with `cubic-bezier(0.16, 1, 0.3, 1)` (easeOutExpo) — fast at first, settling gently into place.
|
||||
|
||||
## 2. Timing
|
||||
|
||||
Number of frames / duration defines weight and mood. Fast = light/urgent, slow = heavy/calm.
|
||||
|
||||
Example: a 24px notification badge pops in over 150ms; a full-screen onboarding panel slides over 500ms. Same easing, different timing communicates different mass.
|
||||
|
||||
## 3. Follow-through and overlapping action
|
||||
|
||||
Parts don't all stop at once; trailing elements continue and settle after the lead.
|
||||
|
||||
Example: a hero card lands at 400ms; its drop-shadow finishes settling at 450ms, its title text at 460ms, its subtitle at 500ms — a 40-60ms cascade rather than a synchronized stop.
|
||||
|
||||
## 4. Anticipation
|
||||
|
||||
A small opposite move telegraphs the main action.
|
||||
|
||||
Example: a "send" button scales to `0.95` over 80ms (the dip), then springs to `1.0` and the message launches. Or a modal dips `y: +8px` for 60ms before sliding up — the recoil makes the launch feel powered.
|
||||
|
||||
## 5. Staging (focal direction)
|
||||
|
||||
Direct attention to one thing at a time; the motion makes the important element unambiguous.
|
||||
|
||||
Example: dim background to 40% opacity over 200ms while the target element scales `0.9 -> 1.0` and brightens — the eye has exactly one place to land.
|
||||
|
||||
## 6. Arcs
|
||||
|
||||
Natural movement follows curved paths, not straight lines.
|
||||
|
||||
Example: a floating action button moving from bottom-right to center-screen animates along a quadratic bezier path instead of a diagonal line; the slight arc reads as organic rather than mechanical.
|
||||
|
||||
## 7. Squash and stretch
|
||||
|
||||
Volume-preserving deformation conveys impact and elasticity.
|
||||
|
||||
Example: a bouncing dot stretches `scaleY 1.2 / scaleX 0.85` at the top of its arc and squashes `scaleY 0.8 / scaleX 1.15` on landing impact. Keep the product of scales ≈ 1 to preserve perceived volume. Use sparingly in product UI; common in playful loaders and mascots.
|
||||
|
||||
## 8. Exaggeration
|
||||
|
||||
Push the key pose beyond literal realism for clarity and appeal.
|
||||
|
||||
Example: a success checkmark overshoots to `scale 1.15` before settling to `1.0` (using easeOutBack `0.34, 1.56, 0.64, 1`); the overshoot makes success feel emphatic.
|
||||
|
||||
## 9. Secondary action
|
||||
|
||||
A subordinate motion supporting the primary one.
|
||||
|
||||
Example: while a panel slides up (primary), its icon rotates 90deg and a subtle particle shimmer fades in (secondary) — supporting, never competing for attention.
|
||||
|
||||
## 10. Straight-ahead vs. pose-to-pose
|
||||
|
||||
Pose-to-pose (define keyframes, interpolate between) is the norm for choreographed motion graphics; straight-ahead (frame-by-frame, momentum-driven) suits chaotic/organic effects like particles, smoke, and procedural noise.
|
||||
|
||||
Example: a logo build is pose-to-pose (start pose, end pose, eased between); a confetti burst is straight-ahead/physics-driven.
|
||||
|
||||
## 11. Solid drawing (depth and weight)
|
||||
|
||||
Maintain consistent perspective, volume, and lighting so motion respects 3D space.
|
||||
|
||||
Example: when a 2.5D card tilts on hover (`rotateY 8deg`), its shadow shifts and lengthens consistently and a subtle specular highlight sweeps — selling that it's a solid object catching light, not a flat sticker.
|
||||
|
||||
## 12. Appeal
|
||||
|
||||
The composite charm — clean silhouettes, confident timing, no jank. Achieved by applying the other 11 with restraint: correct easing, deliberate timing, follow-through, a touch of overshoot, and removing anything that competes.
|
||||
|
||||
Example: a polished toggle = 180ms easeOutCubic slide, a 40ms follow-through on the label color, a hairline overshoot on the knob — small, coherent, satisfying.
|
||||
|
||||
## Practical priority for screen motion
|
||||
|
||||
For most product/web work, nailing **slow-in/slow-out, timing, follow-through, anticipation, staging, and arcs** delivers 90% of the quality. Squash/stretch, exaggeration, and secondary action add personality where the brand calls for it.
|
||||
|
||||
---
|
||||
Apply the twelve principles with restraint and UI motion reads as intentional. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=motion-design-skills&utm_content=skill_footer&utm_term=animation-principles)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
Whitespace-only changes.
@@ -0,0 +1,229 @@
|
||||
---
|
||||
name: beat-sync-editing
|
||||
description: This skill should be used when the user asks to "cut to the beat", "fix the pacing", "edit this sequence", "sync cuts to music/BPM", "add a match cut / J-cut / L-cut", "build a speed ramp", or "plan a shot sequence / edit timeline". Covers editing rhythm, transitions, retiming, and translating an edit plan to an NLE or a code timeline (Remotion/AE frames).
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Beat-Sync Editing (Cuts & Rhythm)
|
||||
|
||||
The timeline craft: how clips and beats assemble into a piece that *feels* right. Editing is temporal design — when a cut lands matters as much as what is on screen. This skill turns musical and narrative structure into concrete cut points, transitions, and retimes, expressible in an NLE or as frame numbers in code.
|
||||
|
||||
## When to use
|
||||
|
||||
- Assembling a sequence or cutting footage to a music track.
|
||||
- Pacing drags or feels choppy; transitions feel arbitrary.
|
||||
- Building speed ramps, match cuts, or tension/release structure.
|
||||
- Converting an edit plan into a frame-accurate timeline (Remotion, AE, NLE).
|
||||
|
||||
## Decision tree
|
||||
|
||||
```
|
||||
What's the editing problem?
|
||||
├─ Cuts feel off the music
|
||||
│ └─ get BPM + offset → frames_per_beat = (60/BPM)*fps
|
||||
│ → cut on PHRASES (2/4/8 beats), biggest visual on the drop
|
||||
├─ A cut feels jarring / visible
|
||||
│ └─ cut on action (mid-motion), match direction + velocity
|
||||
│ → still rough? motivate a transition (match / J / L), else hard cut
|
||||
├─ Pacing drags or feels flat
|
||||
│ └─ shape an arc: Establish → Develop → Climax → Resolve
|
||||
│ → shorten avg shot length toward climax, hold after it
|
||||
├─ A hit needs impact
|
||||
│ └─ speed ramp INTO it (slow-mo), snap back; land slowest frame on the beat
|
||||
└─ Need to hand it to code
|
||||
└─ express every cut as a frame number, clip as from/duration,
|
||||
transition as overlap, retime as eased time-remap (Remotion/AE)
|
||||
```
|
||||
|
||||
**Cross-reference — apply the shared frameworks.**
|
||||
|
||||
- **Motion Personality** (`motion-art-direction`) sets cut rhythm: the chosen energy maps straight to phrase length and transition family. Pick one and commit (see the table below).
|
||||
- **Three Pillars** + beat-sync (`animation-principles`): settle *emotional intent* first (hype vs. calm) — it decides whether you cut every bar or hold for four. `animation-principles` defines the BPM→beat relationship; this skill turns it into frame-accurate cut points and the transition/retime craft on top.
|
||||
|
||||
### Motion Personality → cut rhythm
|
||||
|
||||
| Personality | Phrase (cut every) | Transition family | Retime | Reads as |
|
||||
|---|---|---|---|---|
|
||||
| **Playful** | 4 beats, syncopated accents | match cuts, quick whips | light ramps, bouncy | fun, kinetic |
|
||||
| **Premium** | 8–16 beats, long holds | hard cut + occasional dissolve | slow, smooth ramps | elegant, unhurried |
|
||||
| **Corporate** | 8 beats, steady | hard cut, clean | minimal | clean, trustworthy |
|
||||
| **Energetic** | 1–2 beats at climax | hard cut, whips, hard snaps | aggressive ramps on beats | bold, hype |
|
||||
|
||||
## Core workflow
|
||||
|
||||
### 1. Find the grid: BPM → frames
|
||||
|
||||
Cut on the music's rhythmic grid, not on arbitrary timecodes. Convert BPM to frames so cut points are frame-accurate.
|
||||
|
||||
```
|
||||
frames_per_beat = (60 / BPM) * fps
|
||||
```
|
||||
|
||||
At 120 BPM, 30 fps: `(60/120)*30 = 15` frames per beat (500ms/beat) — a cut every 15 frames is on-beat. Cut on *phrases* (every 2/4/8 beats), not every beat, or the edit feels frantic. Land the single biggest visual moment on the musical drop.
|
||||
|
||||
The **eighth-grid**: subdivisions of the beat give finer accent points without going off-grid. At 120 BPM / 30 fps:
|
||||
|
||||
| Subdivision | ms | frames @30fps | Use |
|
||||
|---|---|---|---|
|
||||
| 1 beat (quarter) | 500 | 15 | main cut grid |
|
||||
| ½ beat (eighth) | 250 | 7.5 | accent / quick flash cut |
|
||||
| ¼ beat (sixteenth) | 125 | 3.75 | hits, stutter, transient FX |
|
||||
| 1 bar (4 beats) | 2000 | 60 | phrase / section boundary |
|
||||
|
||||
Snap accents to the eighth-grid; keep structural cuts on the beat or bar.
|
||||
|
||||
```js
|
||||
// Generate on-beat frame numbers (JS / Remotion / AE-friendly)
|
||||
const bpm = 120, fps = 30;
|
||||
const framesPerBeat = (60 / bpm) * fps; // 15
|
||||
const phrase = 4; // cut every 4 beats
|
||||
const cuts = Array.from({length: 16}, (_, i) =>
|
||||
Math.round(i * framesPerBeat * phrase)); // [0,60,120,180,...]
|
||||
```
|
||||
|
||||
### 2. Hide the seam: cut on action / motion
|
||||
|
||||
A cut is invisible when motion carries the eye across it. Cut *during* a movement (a turn, a hand reaching, a whip) so the brain tracks the action through the cut. Match the motion's direction and speed across the cut. For motion-graphics, cut on a transform peak — at the moment a shape is mid-travel, not at rest.
|
||||
|
||||
### 3. Choose transitions with intent
|
||||
|
||||
Default to the **hard cut**. Reach for anything else only when motivated:
|
||||
|
||||
- **Match cut** — outgoing and incoming frames share a shape, position, or motion vector, so the cut reads as a transformation (a circle becomes the moon; a wipe of an arm reveals the next scene). The strongest transition in motion design.
|
||||
- **J-cut** — audio of the *next* shot starts *before* its picture (sound leads). Pulls the viewer forward; great for dialogue and reveals.
|
||||
- **L-cut** — audio of the *current* shot continues *under* the next picture (sound trails). Smooths scene changes.
|
||||
- **Whip / motion blur transition** — fast pan blends two shots; needs matched blur direction.
|
||||
- **Luma/wipe/dissolve** — signals time passing or a softer mood; overuse reads as amateur.
|
||||
|
||||
Cut-type quick table:
|
||||
|
||||
| Cut type | What aligns | Signals | Typical offset | Reach for it when |
|
||||
|---|---|---|---|---|
|
||||
| Hard cut | nothing — instant | neutral, default | 0 | 90% of cuts |
|
||||
| Match cut | shared shape / position / motion vector | transformation | 0 (1–2f tolerance) | two shots rhyme visually |
|
||||
| J-cut | audio of B leads its picture | pulls viewer forward | audio 4–12f early | dialogue, reveals |
|
||||
| L-cut | audio of A trails under B | smooths the change | audio 12–24f late | scene changes, ambient |
|
||||
| Whip / motion blur | matched blur direction across pan | energy, "swoosh" | ~6–10f blend | hype, scene jumps |
|
||||
| Speed ramp | retime, not a cut | impact / emphasis | lands on beat | a hit needs to land |
|
||||
|
||||
### 4. Retime: speed ramps
|
||||
|
||||
Speed ramps (gradual time-warp) add impact: ramp *into* a hit with slow-motion, then snap back to speed. Keep motion blur consistent through the ramp or the slow section looks strobed.
|
||||
|
||||
```
|
||||
Speed ramp on an impact (frames, 30fps):
|
||||
0–20 100% speed (run-up)
|
||||
20–28 ease down to 25% (anticipation, slow-mo on the hit)
|
||||
28 IMPACT lands on the beat
|
||||
28–40 ease back to 100% (release)
|
||||
```
|
||||
|
||||
In an NLE use time-remap keyframes with smooth interpolation and enable frame-blending/optical-flow for clean slow-mo. In Remotion/AE, drive a `timeRemap`/`valueAtTime` curve with eased interpolation and keep a consistent shutter/motion-blur setting.
|
||||
|
||||
### 5. Shape the arc: sequence pacing
|
||||
|
||||
A piece needs a shape, not one constant intensity:
|
||||
|
||||
```
|
||||
Establish → Develop → Climax → Resolve
|
||||
longer holds shortening cuts fastest + the drop release/breathe
|
||||
```
|
||||
|
||||
Vary shot length deliberately: fast cuts build energy, longer holds let key moments land. Build toward the climax by shortening average shot length, then give a beat of rest after it. The classic mistake is uniform pacing — it reads as flat regardless of content.
|
||||
|
||||
Pacing arc — tension → release (energy is the *derivative* of cut frequency):
|
||||
|
||||
| Phase | Cut every | Avg shot length | Tension | Note |
|
||||
|---|---|---|---|---|
|
||||
| Establish | 8–16 beats | longest | low | set place/mood, let it breathe |
|
||||
| Develop | 8 → 4 beats | shortening | rising | accelerate cuts to build |
|
||||
| Climax | 1–2 beats | shortest | peak | the drop; biggest visual + speed ramp |
|
||||
| Resolve | one long hold | longest | release | logo/CTA breathes after the peak |
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Goal | Move |
|
||||
|---|---|
|
||||
| On-beat cut | `(60/BPM)*fps` frames per beat; cut on phrases |
|
||||
| Land the drop | biggest visual on the musical drop |
|
||||
| Invisible cut | cut on action / matched motion |
|
||||
| Transform feel | match cut (shared shape/motion) |
|
||||
| Pull forward | J-cut (audio leads) |
|
||||
| Smooth scene change | L-cut (audio trails) |
|
||||
| Impact | speed ramp into the hit, snap out |
|
||||
| Build energy | shorten average shot length |
|
||||
| Let it breathe | hold after the climax |
|
||||
|
||||
## Worked examples
|
||||
|
||||
### GOOD — 30s energetic product spot, 128 BPM, 30 fps
|
||||
|
||||
> **Brief**: hype sneaker drop, Energetic personality, drop at beat 32.
|
||||
>
|
||||
> - **Grid**: 14.06 f/beat. Drop = beat 32 ≈ frame 450 (rounded cumulatively to avoid drift).
|
||||
> - **Arc**: Establish 0–120 (two 2s holds). Develop 120–450 (cuts shortening 8→4→2 beats, rising energy — matches Energetic). Climax at 450 (the drop). Resolve 700–900 (one long CTA hold).
|
||||
> - **Cuts**: structural cuts on the bar; two accent flash-cuts snapped to the eighth-grid in Develop.
|
||||
> - **Craft**: f112 is a match cut (shared circular logo → wheel). The drop at 450 carries the biggest visual *and* a speed ramp 100%→25%→100% landing the slowest frame on the downbeat. CTA enters under an L-cut (audio trails ~18f).
|
||||
> - **To code**: each cut a frame number, each clip `<Sequence from/durationInFrames>`, ramp = eased time-remap, beats verified `frame/fps` against detected beat seconds.
|
||||
>
|
||||
> Why it works: phrase-based grid, an arc, one big moment on the drop, motivated transitions, frame-accurate and portable.
|
||||
|
||||
### ANTI-PATTERN — "cut on every beat with a dissolve each time"
|
||||
|
||||
> - A hard cut on all 32 beats → relentless, no focal moment, viewer fatigues by beat 8.
|
||||
> - A 12-frame crossfade on *every* cut to "smooth it" → mushy, amateur, and now nothing is on the beat because the dissolve midpoint drifts off-grid.
|
||||
> - Uniform 15-frame shots start to finish → flat; no establish, no climax, the drop lands on nothing.
|
||||
> - BPM math done from frame 0 ignoring the track's offset → every cut sits a few frames early, feels "almost right" but wrong.
|
||||
>
|
||||
> Fix: cut on 4/8-beat phrases, hard cut by default, build an arc, anchor the grid to beat-1 offset, and put the one biggest visual on the drop.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
| Symptom | Why | Fix |
|
||||
|---|---|---|
|
||||
| Edit feels frantic / busy | cutting on every beat | cut on 2/4/8-beat phrases; accent on the eighth-grid sparingly |
|
||||
| Cuts feel "almost on beat" | grid started at 0:00, ignored track offset | capture beat-1 offset; anchor cumulative frame math to it |
|
||||
| Drift over a long edit | rounding frames_per_beat per step | round each *cumulative* beat, not per-step |
|
||||
| Cut feels jarring | placed at rest, between motions | cut on action / mid-motion; match direction + velocity |
|
||||
| Slow-mo strobes | sub-100% speed without interpolated frames | enable optical-flow / frame-blending; consistent shutter |
|
||||
| Whole piece feels flat | uniform pacing | shape Establish→Develop→Climax→Resolve; shorten toward the drop |
|
||||
| Transitions look amateur | dissolves/wipes used as a crutch | default to hard cut; motivate match/J/L; fix timing first |
|
||||
| Nothing lands | no single climax | put the biggest visual on the drop; hold after it |
|
||||
|
||||
## Deliverable — an edit-timing plan
|
||||
|
||||
A good plan is renderer-agnostic and hands off to an NLE or a code timeline (`remotion-video`, `after-effects`):
|
||||
|
||||
- **Track spec**: BPM, fps, beat-1 offset, drop frame(s), frames_per_beat.
|
||||
- **Arc**: the four phases with frame ranges and target average shot length per phase.
|
||||
- **Cut list**: every cut as an absolute **frame number** (from the BPM math), the clip at that cut, and the cut type.
|
||||
- **Transitions**: type (hard/match/J/L/whip), and for J/L the audio offset in frames.
|
||||
- **Retimes**: each speed ramp as eased time-remap keyframes (from→to %, frames, optical-flow on).
|
||||
- **Beat markers**: the on-beat/eighth-grid frame array for verification.
|
||||
- **Cross-refs**: the Motion Personality that set the rhythm.
|
||||
|
||||
> For Remotion/AE this plan IS the frame timing: cuts → `from`/`durationInFrames` (or layer in/out), ramps → eased `interpolate`/time-remap, J/L → offset `<Audio from>` / audio layer. See `references/editing-techniques.md` for the NLE↔code mapping table.
|
||||
|
||||
### Before you finish — checklist
|
||||
|
||||
- [ ] BPM + fps + beat-1 offset captured; frames_per_beat computed and cumulatively rounded.
|
||||
- [ ] Cuts on phrases (2/4/8 beats), not every beat; accents on the eighth-grid only.
|
||||
- [ ] The single biggest visual lands on the drop.
|
||||
- [ ] An arc exists (Establish→Develop→Climax→Resolve); avg shot length shortens toward the climax, then a hold.
|
||||
- [ ] Hard cut is the default; every other transition is motivated; J/L audio offsets specified in frames.
|
||||
- [ ] Speed ramps land the slowest frame on the beat; optical-flow/frame-blending enabled.
|
||||
- [ ] Cut rhythm matches the chosen Motion Personality.
|
||||
- [ ] Every cut, transition, and retime expressed as a frame number / from-duration so it ports to Remotion/AE/NLE.
|
||||
- [ ] Audio sync verified: `frame/fps` against detected beat seconds.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Cutting on *every* beat is busy; cut on 2/4/8-beat phrases and accent sparingly.
|
||||
- Speed ramps without consistent motion blur look strobed; enable optical-flow/frame-blending for slow-mo.
|
||||
- Crossfades/dissolves are not a fix for a bad cut — fix the timing first.
|
||||
- Audio sync drift: in code timelines verify `frame/fps` against detected beat seconds; in NLEs lock audio to a frame-accurate guide track.
|
||||
- One pacing throughout = flat. Always shape an arc.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/editing-techniques.md` — BPM→frame math with phrase tables, cut-on-action breakdowns, building a match cut step by step, J-cut/L-cut audio-lead/trail diagrams with frame offsets, full speed-ramp recipe with motion-blur/optical-flow settings, sequence-arc templates with shot-length curves, and mapping an edit plan between an NLE timeline and a code timeline (Remotion `<Sequence>` / AE time-remap).
|
||||
@@ -0,0 +1,137 @@
|
||||
# Editing Techniques — Detailed Cookbook
|
||||
|
||||
Concrete recipes for rhythm, transitions, and retiming, with the math to make cuts frame-accurate.
|
||||
|
||||
## BPM → frame math
|
||||
|
||||
```
|
||||
frames_per_beat = (60 / BPM) * fps
|
||||
seconds_per_beat = 60 / BPM
|
||||
```
|
||||
|
||||
Reference (frames per beat):
|
||||
|
||||
| BPM | 24 fps | 25 fps | 30 fps | 60 fps |
|
||||
|----|----|----|----|----|
|
||||
| 90 | 16.0 | 16.7 | 20.0 | 40.0 |
|
||||
| 100 | 14.4 | 15.0 | 18.0 | 36.0 |
|
||||
| 120 | 12.0 | 12.5 | 15.0 | 30.0 |
|
||||
| 128 | 11.25| 11.7 | 14.06| 28.13|
|
||||
| 140 | 10.3 | 10.7 | 12.86| 25.7 |
|
||||
|
||||
When `frames_per_beat` is fractional (e.g. 128 BPM at 30 fps = 14.06), round each *cumulative* beat to avoid accumulating drift:
|
||||
|
||||
```js
|
||||
const cut = (i) => Math.round(i * (60 / bpm) * fps); // round at the end, not per-step
|
||||
```
|
||||
|
||||
Cut on phrases, not beats:
|
||||
|
||||
| Energy | Cut every |
|
||||
|---|---|
|
||||
| Frantic / climax | 1–2 beats |
|
||||
| Energetic | 4 beats (1 bar in 4/4) |
|
||||
| Moderate | 8 beats (2 bars) |
|
||||
| Calm / establishing | 16 beats (4 bars) |
|
||||
|
||||
Detect BPM offline (aubio, `web-audio-beat-detector`, or tap-tempo) and also capture the *offset* of beat 1 — the grid rarely starts at 0:00.
|
||||
|
||||
## Cut on action / cut on motion
|
||||
|
||||
The eye tracks moving subjects; a cut placed mid-motion is masked by that movement.
|
||||
|
||||
- Identify a continuous action spanning two shots (door opening, head turn, object thrown).
|
||||
- Place the cut roughly one-third into the action, and continue the *same* action and direction in the next shot.
|
||||
- Match screen direction (subject moving left exits left, enters from right) and velocity.
|
||||
- In motion graphics: cut while a shape is mid-travel (transform velocity high), not when it has settled — the motion blur and momentum hide the splice.
|
||||
|
||||
## Match cut, step by step
|
||||
|
||||
A match cut equates two shots via a shared visual so the cut reads as a transformation.
|
||||
|
||||
1. Find or design a shape/position/motion shared by the end of shot A and the start of shot B (a round clock → a round wheel; an arm wiping frame → next scene revealed).
|
||||
2. Align them in screen space: same size, same position, same orientation at the cut frame.
|
||||
3. Match motion vector and speed across the cut so momentum carries through.
|
||||
4. Cut on the exact frame where the two forms register; a 1–2 frame mismatch breaks the illusion.
|
||||
5. Optionally add a 1–3 frame motion-blur bridge to fuse them.
|
||||
|
||||
Common forms: graphic match (shape), motion match (direction/velocity), audio match (a sound bridges the cut), and the "invisible wipe" where a foreground object wipes the frame and the next shot is revealed behind it.
|
||||
|
||||
## J-cut and L-cut
|
||||
|
||||
Audio and picture do not have to cut at the same frame.
|
||||
|
||||
```
|
||||
J-CUT (audio of B leads its picture — pulls forward)
|
||||
Picture: AAAAAAAA | BBBBBBBB
|
||||
Audio: AAAAA BBBBBBBBBBBBB
|
||||
^ B's audio starts ~6–12 frames early
|
||||
|
||||
L-CUT (audio of A trails under B's picture — smooths)
|
||||
Picture: AAAAAAAA | BBBBBBBB
|
||||
Audio: AAAAAAAAAAAA BBBBBB
|
||||
^ A's audio continues ~6–18 frames past the picture cut
|
||||
```
|
||||
|
||||
Typical offsets: dialogue J-cuts 4–12 frames; ambient L-cuts 12–24 frames. In an NLE, unlink audio/video and trim only the audio edge. In a code timeline, place the `<Audio>` for shot B with a `from` earlier than B's picture sequence.
|
||||
|
||||
## Speed ramp recipe (with motion blur)
|
||||
|
||||
```
|
||||
Impact ramp, 30 fps, drop on frame 28:
|
||||
Frame Speed Note
|
||||
0–20 100% run-up, normal motion blur
|
||||
20–28 100%→25% ease-OUT deceleration into the hit
|
||||
28 25% IMPACT, lands on the downbeat
|
||||
28–34 25%→100% ease-IN re-acceleration
|
||||
34+ 100% release
|
||||
```
|
||||
|
||||
Settings:
|
||||
- Use smooth (Bezier) interpolation on time-remap keyframes; linear ramps look mechanical.
|
||||
- Enable **optical flow** (or frame-blending) for sub-100% speeds so slow-mo isn't strobed; optical flow generates intermediate frames.
|
||||
- Keep motion-blur shutter consistent across the ramp — if the source has motion blur, the slow section keeps it; if synthetic, hold a constant shutter angle (e.g. 180°).
|
||||
- Land the slowest frame *on the beat*; the ramp is felt, the impact is heard.
|
||||
|
||||
In AE: enable time remapping, keyframe the source time, ease the keyframes, set frame blending to Pixel Motion. In Remotion: drive a remapped source time via eased `interpolate` and keep effects deterministic.
|
||||
|
||||
## Sequence arc templates
|
||||
|
||||
```
|
||||
30s social spot (≈ 900 frames @30fps):
|
||||
Establish 0–120 2 shots, ~2s each (longer holds)
|
||||
Develop 120–540 shots shortening 1.5s → 0.8s, rising energy
|
||||
Climax 540–720 fastest cuts 0.3–0.5s, the drop at ~600
|
||||
Resolve 720–900 one long hold, logo/CTA breathes
|
||||
```
|
||||
|
||||
Plot average shot length as a curve: high at the start, descending to the climax, then a single long shot to release. Energy is the *derivative* of cut frequency — accelerate cuts to build, decelerate to land.
|
||||
|
||||
## NLE timeline vs code timeline
|
||||
|
||||
| Concept | NLE | Remotion | After Effects |
|
||||
|---|---|---|---|
|
||||
| Clip placement | drag to track at TC | `<Sequence from durationInFrames>` | layer in/out points |
|
||||
| Back-to-back shots | adjacent clips | `<Series>` | trimmed layers |
|
||||
| Crossfade | overlap + dissolve | overlap `Series` + opacity interp | opacity keyframes / overlap |
|
||||
| Speed ramp | time-remap keyframes | eased source-time `interpolate` | time remap + frame blending |
|
||||
| J/L cut | unlink + trim audio | `<Audio from>` ≠ picture seq | offset audio layer |
|
||||
| Beat marker | timeline markers | beat array `frame/fps` | comp markers / expression |
|
||||
|
||||
To port an edit plan: express each cut as a frame number (from the BPM math), each clip as `from`/`duration`, each transition as an overlap, and each retime as an eased time-remap. The plan is renderer-agnostic; only the placement syntax differs.
|
||||
|
||||
## Example edit plan (portable)
|
||||
|
||||
```
|
||||
Track: 128 BPM, 30fps → 14.06 f/beat, drop at beat 32 (frame 450)
|
||||
Cut on 4-beat phrases (≈56 frames):
|
||||
f0 ShotA establish, hold
|
||||
f56 ShotB cut on action (subject turns)
|
||||
f112 ShotC match cut from B (shared circle)
|
||||
f168 ... cuts shorten toward climax
|
||||
f450 DROP biggest visual; speed ramp 100→25→100 around f450
|
||||
f700 CTA long hold, L-cut audio trails in
|
||||
```
|
||||
|
||||
---
|
||||
Cut on the beat and ramp with purpose and the edit moves with the music. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=motion-design-skills&utm_content=skill_footer&utm_term=beat-sync-editing)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
Whitespace-only changes.
@@ -0,0 +1,175 @@
|
||||
---
|
||||
name: color-motion
|
||||
description: This skill should be used when the user asks to "build a color palette for a motion piece", "make a gradient background", "animate a color transition", "interpolate colors smoothly", "fix muddy/gray gradients", "convert colors to OKLCH", "color-grade a video for mood", "fix washed-out After Effects renders", "gamma shift", "Rec.709 vs sRGB", "colors shifted after rendering", or "match preview to final video". Covers palette construction, gradients, perceptual interpolation (OKLCH/Lab), grading order, and video/render color management.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Color in Motion
|
||||
|
||||
Choose, pair, and animate color for motion design: building restrained palettes, constructing gradients that read as premium, interpolating colors in perceptually-uniform spaces, and grading a piece for mood and consistency. Produces palettes with exact OKLCH/hex values, gradient CSS/code, and transition parameters.
|
||||
|
||||
## When to use
|
||||
|
||||
- Picking or pairing a palette for a motion or video piece.
|
||||
- Building gradient backgrounds (mesh, noise, animated drift).
|
||||
- Animating color transitions (button states, theme switch, loaders).
|
||||
- Grading a video for mood, or fixing dull/gray gradient interpolation.
|
||||
- Fixing washed-out, gamma-shifted, or platform-inconsistent After Effects renders, and choosing correct per-platform export/color tags.
|
||||
|
||||
## Core techniques
|
||||
|
||||
### 1. Build a restrained palette
|
||||
|
||||
Limit hues. A strong motion palette is **1 primary + 1 accent + 2-3 neutrals**. Derive contrast and depth from lightness and chroma, not from adding more hues. Extra hues fight for attention and read as amateur.
|
||||
|
||||
In dark scenes, make the accent pop with **low-to-moderate chroma and high lightness** rather than maximum saturation — a blown-out saturated accent vibrates and looks cheap on dark backgrounds.
|
||||
|
||||
Use OKLCH to reason about color: `oklch(L C H)` where L is perceptual lightness (0-1), C is chroma (0 ≈ 0.37 max), H is hue angle (0-360). Holding L and C constant while rotating H gives hues of genuinely equal visual weight — impossible to do reliably in HSL.
|
||||
|
||||
```
|
||||
Primary: oklch(0.62 0.19 264) /* confident blue */
|
||||
Accent: oklch(0.78 0.17 50) /* warm amber, lighter so it pops */
|
||||
Neutral-0: oklch(0.97 0.005 264) /* near-white, faint hue tint */
|
||||
Neutral-1: oklch(0.55 0.01 264)
|
||||
Neutral-2: oklch(0.18 0.015 264) /* near-black, same hue family */
|
||||
```
|
||||
|
||||
Tinting neutrals slightly toward the primary hue (tiny chroma) unifies the frame; pure gray neutrals next to colored elements look disconnected.
|
||||
|
||||
### 2. Interpolate in OKLCH or Lab — never raw sRGB
|
||||
|
||||
Linearly interpolating two colors in sRGB passes through a desaturated, often gray or muddy midpoint (blue->yellow goes through gray; blue->red through muddy purple). Perceptual spaces (OKLCH, OKLab, CIE Lab) keep midpoints vivid and lightness even.
|
||||
|
||||
CSS gradients support this natively:
|
||||
|
||||
```css
|
||||
/* Vivid, even midpoint */
|
||||
background: linear-gradient(in oklch, #2b6cff, #ffb02e);
|
||||
|
||||
/* Control hue path around the wheel for two-stop gradients */
|
||||
background: linear-gradient(in oklch longer hue, #2b6cff, #ffb02e);
|
||||
background: conic-gradient(in oklch, red, blue, red);
|
||||
```
|
||||
|
||||
For animated transitions interpolate with a library (see references) and emit per-frame colors, or for two-color CSS transitions register a typed custom property so the browser interpolates in the chosen space.
|
||||
|
||||
### 3. Gradients that read as premium
|
||||
|
||||
- **Mesh / multi-radial**: stack several `radial-gradient`s at different positions over a base color. Soft, organic, expensive-looking.
|
||||
- **Add grain**: a faint noise overlay (SVG `feTurbulence` or a tiled PNG at ~3-5% opacity) kills banding and adds texture.
|
||||
- **Animate slowly**: drift gradient positions over 8-20s with `ease-in-out alternate`. Slow is premium; fast gradient motion looks like a screensaver.
|
||||
|
||||
```css
|
||||
.mesh {
|
||||
background-color: #0b0f1a;
|
||||
background-image:
|
||||
radial-gradient(at 20% 25%, oklch(0.62 0.19 264 / 0.7), transparent 50%),
|
||||
radial-gradient(at 80% 30%, oklch(0.70 0.16 320 / 0.6), transparent 55%),
|
||||
radial-gradient(at 50% 85%, oklch(0.78 0.17 50 / 0.5), transparent 50%);
|
||||
animation: drift 16s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes drift {
|
||||
to { background-position: 8% -6%, -8% 6%, 4% 8%; }
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Color transition animation
|
||||
|
||||
Inline timing essentials so this skill stands alone: enter/highlight with ease-out (`cubic-bezier(0.16, 1, 0.3, 1)`), state changes 150-300ms, theme/full-screen washes 400-800ms. Use `linear` only for continuous loops.
|
||||
|
||||
```css
|
||||
@property --c { syntax: "<color>"; initial-value: #2b6cff; inherits: false; }
|
||||
.btn { background: var(--c); transition: --c 200ms cubic-bezier(0.16,1,0.3,1); }
|
||||
.btn:hover { --c: #ffb02e; }
|
||||
```
|
||||
|
||||
`@property` registration is what makes the color actually interpolate (unregistered custom properties jump instantly).
|
||||
|
||||
### 5. Grade for mood — correct order
|
||||
|
||||
Apply grading operations in this order; reordering changes the result:
|
||||
|
||||
1. **Exposure / white balance** — fix overall brightness and neutralize/intentionally set the cast first.
|
||||
2. **Contrast** — set black and white points; expand or compress the tonal range.
|
||||
3. **Midtones / saturation** — adjust overall saturation and midtone color.
|
||||
4. **Split-tone** — tint shadows and highlights separately. The classic cinematic look is **teal shadows + orange highlights** (complementary, flatters skin). Keep it subtle.
|
||||
5. **Unify** — a final overall tint/LUT and a subtle vignette to bind the frame.
|
||||
|
||||
In code, grade by chaining CSS filters or a fragment shader; for video, an actual LUT or grading panel. Always grade the whole piece consistently so shots cut together.
|
||||
|
||||
### 6. Video / render color management
|
||||
|
||||
When a render looks correct in the After Effects comp viewer but washed-out, too bright, milky, or gamma-shifted in QuickTime, a browser, or across macOS/Windows, the cause is a gamma/transfer mismatch across four stages — AE working space, the render's embedded tag, the player's interpretation, and the display/OS gamma — not the artwork.
|
||||
|
||||
Core rules:
|
||||
|
||||
- **Set an explicit working space.** File > Project Settings > Color tab: Working Space = **Rec.709 Gamma 2.4** for SDR video (or **sRGB** for UI/stills); "None" disables color management and the viewer lies about the final look. Set Depth to **16 bpc** for grades/gradients to avoid banding.
|
||||
- **Rec.709 vs sRGB.** They share primaries but differ in transfer (sRGB ~2.2 piecewise vs Rec.709 display 2.4 / BT.1886). Using the wrong one shifts midtone brightness — Rec.709 for video, sRGB for screenshots/PNG/UI.
|
||||
- **Embed a color tag on export.** The shift across macOS/Windows/players comes from untagged files; each player guesses its own default. Tag Rec.709 (or sRGB) explicitly via Render Queue > **Output Module > Color Management** (do not leave "Preserve RGB" when a shift occurs).
|
||||
- **The QuickTime gamma bug.** QuickTime/AVFoundation apply an extra ~1.96 gamma, so a file can look fine in QuickTime but dark/contrasty in Chrome. **Never grade or approve to QuickTime** — Chrome, Premiere, or Resolve is the correct reference for web delivery.
|
||||
- **Match preview to final.** Toggle "Use Display Color Management" in the comp viewer while reviewing, and verify the export numerically: place a 50% grey (RGB 128) patch, render, re-sample — a value that rises (128 → 150+) confirms a washed-out gamma shift. Confirm the embedded tag with `ffprobe` (`color_primaries`/`color_transfer`/`color_space` = bt709).
|
||||
|
||||
Per-platform export at a glance: **ProRes 422 HQ/4444** tagged Rec.709 for editing masters; **H.264 (MP4)** tagged Rec.709 for web/`<video>`/social (verify in Chrome); ProRes 4444 + alpha → PNG (sRGB) for transparency; ProRes 4444 / HEVC 10-bit Rec.2020 PQ/HLG for HDR (needs 32 bpc float).
|
||||
|
||||
See `references/color-management.md` for the full diagnosis decision tree, exact menu paths, per-platform export table, the QuickTime workarounds, per-OS/player gamma values, the numeric test-patch method, `ffprobe`/`ffmpeg` tag commands, limited-vs-full-range pitfalls, and the pre-export checklist.
|
||||
|
||||
## Deliver & verify (standalone HTML)
|
||||
|
||||
> **Packaged helper** (`scripts/`): `scripts/seek-shot.sh anim.html 0 1.5 3` freezes the `?t=N` harness and screenshots each moment; `scripts/contact-sheet.sh sheet.png frame-*.png` tiles them for one-glance review. See `scripts/README.md`.
|
||||
|
||||
For a web palette/gradient/color-transition deliverable, ship **one `.html` file that opens directly in a browser** — no build step. CSS handles the color work; the file is self-contained.
|
||||
|
||||
**Output contract:**
|
||||
- One `.html` file: your markup + the gradient/transition in inline `<style>` (CDN only if a JS interpolation lib is needed).
|
||||
- One animation driver — a CSS `@keyframes` drift or a single `@property` transition; nothing competing.
|
||||
- Include the freeze harness below so any moment can be screenshotted deterministically.
|
||||
|
||||
**Freeze harness — pin a CSS-animation frame for screenshots.** `?t=N` sets a negative `animation-delay` and pauses, so the gradient/transition lands on the exact frame at `N` seconds.
|
||||
|
||||
```html
|
||||
<script>
|
||||
const t = new URLSearchParams(location.search).get("t");
|
||||
if (t !== null) {
|
||||
document.querySelectorAll(".bg, .btn").forEach(el => { // animated elements
|
||||
el.style.animationDelay = (-parseFloat(t)) + "s";
|
||||
el.style.animationPlayState = "paused";
|
||||
});
|
||||
}
|
||||
window.__ready = true; // ready signal for headless wait
|
||||
</script>
|
||||
```
|
||||
|
||||
**Verify loop — render → freeze → screenshot → check:**
|
||||
1. Open the file frozen at start / mid / end: `…/color.html?t=0`, `?t=<period/2>`, `?t=<period>`.
|
||||
2. Screenshot each frozen frame.
|
||||
3. Check **fidelity** (matches the palette/brief) and **color artifacts** — at each frozen frame confirm the gradient/transition has **no banding and no muddy or gray midpoint** (the OKLCH/Lab path stays vivid). Add grain if banding appears.
|
||||
4. **Iterate:** if banding or a muddy/gray midpoint shows up, adjust the OKLCH/Lab chroma & lightness (or add grain), re-render the **same** `?t=N`, and re-screenshot to confirm the fix — loop until every frozen frame is clean.
|
||||
|
||||
```bash
|
||||
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/color.html?t=8" frame-mid.png
|
||||
```
|
||||
|
||||
**Before you finish:**
|
||||
1. Opens standalone in a browser — no console errors, no missing CDN.
|
||||
2. One color driver; `?t=N` freezes the exact frame correctly.
|
||||
3. Screenshotted at start / mid / end — no banding, no muddy/gray midpoints, lightness even.
|
||||
4. `prefers-reduced-motion` honored (drift paused on a static, readable frame).
|
||||
5. Interpolation is in OKLCH/Lab (gradients/`@property`), never raw sRGB.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Need | Do |
|
||||
|---|---|
|
||||
| Palette size | 1 primary + 1 accent + 2-3 neutrals |
|
||||
| Equal-weight hues | hold L,C in OKLCH, vary H |
|
||||
| Vivid gradient | `linear-gradient(in oklch, ...)` |
|
||||
| Smooth color anim | interpolate in OKLCH/Lab, register via `@property` |
|
||||
| Premium bg | stacked radials + grain + 8-20s slow drift |
|
||||
| Cinematic grade | teal shadows / orange highlights, subtle |
|
||||
| Fix washed-out AE render | tag Rec.709 in Output Module > Color Management; verify in Chrome |
|
||||
| Avoid | raw-sRGB interpolation (muddy midpoints); grading to QuickTime |
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/palettes-and-interpolation.md` — palette construction rules with OKLCH values and named recipes, gradient cookbook (mesh, conic, grain), culori and d3-interpolate code for perceptual interpolation and scales, and the full grading-order checklist with filter/shader examples.
|
||||
- `references/color-management.md` — full video/render color management: diagnosis decision tree, AE project/output color settings with exact menu paths, per-platform export table, QuickTime gamma-bug workarounds, per-OS/player gamma values, the numeric test-patch method, `ffprobe`/`ffmpeg` tag verification, limited-vs-full-range pitfalls, and the pre-export checklist.
|
||||
@@ -0,0 +1,165 @@
|
||||
# Video / render color management (After Effects)
|
||||
|
||||
How to diagnose and prevent the most common After Effects color complaint: a render that looks correct inside the AE composition viewer but appears washed-out, too bright, milky, or gamma-shifted when played in QuickTime Player, uploaded to a browser, or moved between macOS and Windows. The root cause is almost always a mismatch in how gamma / transfer functions are interpreted across AE, the codec metadata, the OS player, and the display pipeline — not a problem with the artwork.
|
||||
|
||||
## Mental model: where shifts come from
|
||||
|
||||
A pixel value travels through four stages, each of which can apply or assume a gamma. A shift is a mismatch between two of them:
|
||||
|
||||
1. **AE project working space** — the math AE uses internally (linear vs a tagged display space). Set in File > Project Settings > Color tab.
|
||||
2. **The render's embedded tag** — what transfer/primaries metadata the codec writes (or omits) into the file.
|
||||
3. **The player's interpretation** — QuickTime, Chrome, VLC, and Windows Media each guess differently when the tag is missing or when they apply ColorSync.
|
||||
4. **The display + OS gamma** — macOS historically assumed ~1.8, now ~2.2/2.4-ish via ColorSync; the long-standing "QuickTime gamma bug" comes from QuickTime/AVFoundation applying an extra gamma curve (~1.96) that Chrome and most editors do not.
|
||||
|
||||
Key fact: most web video is **Rec.709**, whose camera/encode side is roughly gamma 2.2-ish but whose *standard display* gamma is **2.4** (BT.1886). macOS QuickTime effectively renders nearer **1.96**, which is why a file can look fine in QuickTime but slightly dark/contrasty in a browser, or vice-versa. The fix is never "eyeball it in QuickTime" — QuickTime is the least trustworthy reference.
|
||||
|
||||
## Decision tree: which problem is this?
|
||||
|
||||
```
|
||||
Render looks DIFFERENT from comp preview?
|
||||
├─ Looks washed-out / milky / brighter, low contrast
|
||||
│ └─ Almost always: untagged or mistagged H.264/ProRes +
|
||||
│ player applying its own gamma (QuickTime gamma bug).
|
||||
│ → Use ProRes for masters; tag Rec.709; verify in
|
||||
│ Chrome, NOT QuickTime.
|
||||
├─ Looks fine in QuickTime, WRONG (dark/contrasty) in Chrome
|
||||
│ └─ QuickTime is applying ~1.96 gamma; Chrome honors the
|
||||
│ 2.4/sRGB pipeline. The browser is the correct reference
|
||||
│ for web delivery.
|
||||
├─ Looks fine on macOS, wrong on Windows (or reverse)
|
||||
│ └─ Missing color tag → each OS assumes a default. Embed an
|
||||
│ explicit Rec.709 (or sRGB) tag on export.
|
||||
└─ Banding / posterization in gradients
|
||||
└─ 8-bit project on heavy grades. Set Project > Depth to
|
||||
16 bpc and/or enable Linearize Working Space.
|
||||
```
|
||||
|
||||
## Project Color Settings (exact menu paths)
|
||||
|
||||
Open **File > Project Settings… > Color** tab.
|
||||
|
||||
- **Depth**: set to **16 bits per channel** for grades/gradients (8 bpc is fine for simple cuts; 32 bpc float only for HDR/heavy linear comps).
|
||||
- **Working Space**: For standard web/SDR delivery choose **Rec.709 Gamma 2.4** (or **sRGB IEC61966-2.1** if the deliverable is screenshots/UI). Setting any working space (not "None") turns AE color management ON, so the viewer now shows display-referred color consistently.
|
||||
- **Linearize Working Space** / **Blend Colors Using 1.0 Gamma**: enable when doing glows, blurs, and additive composites so blending math is physically correct. This changes the look of feathered edges and screen blends — enable it early, not at the end.
|
||||
- **Compensate for Scene-referred Profiles**: leave default unless mixing log/cineon footage.
|
||||
|
||||
To preview what a viewer/OS will actually see, use the composition viewer's **bottom toolbar > "Use Display Color Management"** toggle, and the eyedropper/Info panel to read numeric RGB values rather than trusting the on-screen brightness.
|
||||
|
||||
## Export settings per platform
|
||||
|
||||
Prefer the **Render Queue** (Composition > Add to Render Queue) for ProRes masters, or **File > Export > Add to Adobe Media Encoder Queue** for H.264 web files.
|
||||
|
||||
| Target | Codec / format | Color tag to embed | Notes |
|
||||
|---|---|---|---|
|
||||
| Editing master (FCP/Premiere/Resolve) | **ProRes 422 HQ / 4444** (QuickTime) | Rec.709 | Highest fidelity; avoids H.264 gamma guessing |
|
||||
| Web / `<video>` / social | **H.264** (MP4) via Media Encoder | **Rec.709**, tagged | Set Match Source then override profile; verify in Chrome |
|
||||
| YouTube / Vimeo upload | H.264 high bitrate or ProRes | Rec.709 | Platforms re-encode; correct tag prevents their guess |
|
||||
| Transparency for web | **PNG / ProRes 4444 + alpha** then convert | sRGB | Browsers treat PNG as sRGB |
|
||||
| HDR | ProRes 4444 / HEVC 10-bit | Rec.2020 PQ/HLG | Requires 32 bpc float project |
|
||||
|
||||
In **Adobe Media Encoder**, the QuickTime/ProRes color tag is governed by the AE **Output Module > Color Management** sub-tab. In the Render Queue, click the **Output Module** name > **Color Management** tab and set the output profile to **Rec.709 Gamma 2.4** (do not leave "Preserve RGB" if a shift is occurring).
|
||||
|
||||
## The QuickTime gamma bug — workarounds
|
||||
|
||||
QuickTime Player and any AVFoundation-based player can apply an extra ~1.96 gamma to ProRes/H.264, making files look darker/contrastier than they do in Chrome, Premiere, or Resolve. It is a *player* problem, not a render problem.
|
||||
|
||||
1. **Never grade or approve to QuickTime.** Use Chrome, Premiere, or Resolve as the reference, since those match how the web and other editors display the file.
|
||||
2. For ProRes masters that must look right in QuickTime too, **embed the Rec.709 (Gamma 2.4) tag** via Output Module > Color Management. Correctly tagged ProRes is interpreted consistently by modern macOS.
|
||||
3. If a legacy pipeline still shifts, render a **second 1:1 reference still** (Composition > Save Frame As > File) as PNG and compare numeric values — pixels, not perception.
|
||||
4. Avoid the old "nudge gamma by 1.08 to cancel QuickTime" hacks for new work; they bake a wrong curve that breaks on every correct player. Only use a compensating curve if delivering *exclusively* to a known-broken legacy QuickTime target.
|
||||
|
||||
## Per-player / per-OS gamma reference
|
||||
|
||||
| Environment | Effective transfer assumption | Notes |
|
||||
|---|---|---|
|
||||
| Rec.709 standard display (BT.1886) | **2.4** | The correct broadcast/web display gamma |
|
||||
| sRGB display | **~2.2** (piecewise) | UI, screenshots, PNG |
|
||||
| macOS QuickTime / AVFoundation | **~1.96** | Source of the "QuickTime gamma bug"; brighter/contrast-shifted vs browser |
|
||||
| Chrome / Firefox `<video>` | honors 2.4 / sRGB pipeline | Correct reference for web delivery |
|
||||
| macOS classic (pre-10.6) | 1.8 | Legacy only; ignore for new work |
|
||||
| Windows default | 2.2 | Assumed when file is untagged |
|
||||
|
||||
Takeaway: the *only* reliable cross-environment guarantee is an **explicit embedded tag**. Without it, every player above falls back to its own default and they disagree.
|
||||
|
||||
## Numeric test-patch method (perception-free)
|
||||
|
||||
Eyes adapt; numbers do not. To prove whether a shift exists:
|
||||
|
||||
1. In the comp, add a Solid (Layer > New > Solid) filled with **50% grey: RGB 128,128,128** (or 188,188,188 for a known mid-light patch).
|
||||
2. Note the value AE reports in the **Info panel** when hovering with managed display on.
|
||||
3. Render the file with the intended export settings.
|
||||
4. Re-import the rendered file into a fresh comp (or open in an app with a pixel readout) and sample the same patch.
|
||||
5. If the sampled value differs from 128 by more than ~2-3 levels, a gamma transform is being applied somewhere in the chain. A washed-out look corresponds to the value rising (e.g. 128 → 150+).
|
||||
|
||||
This isolates whether the shift is in the *render* (values changed in the file) vs the *playback* (file values correct, player re-curving).
|
||||
|
||||
## Verify the embedded tag with ffprobe
|
||||
|
||||
```bash
|
||||
ffprobe -v error -select_streams v:0 \
|
||||
-show_entries stream=color_transfer,color_primaries,color_space,color_range \
|
||||
-of default=nk=0 export.mp4
|
||||
```
|
||||
|
||||
Desired for SDR web (Rec.709):
|
||||
|
||||
```
|
||||
color_space=bt709
|
||||
color_primaries=bt709
|
||||
color_transfer=bt709
|
||||
color_range=tv # (limited) for video; pc/full for some web pipelines
|
||||
```
|
||||
|
||||
If `color_transfer=unknown` or fields are absent, the file is untagged → re-export with the tag, or stamp it without re-encoding:
|
||||
|
||||
```bash
|
||||
ffmpeg -i export.mp4 -c copy \
|
||||
-color_primaries bt709 -color_trc bt709 -colorspace bt709 \
|
||||
export_tagged.mp4
|
||||
```
|
||||
|
||||
Note: `-c copy` only writes container/stream metadata; it does not fix pixels that were already baked wrong — use it to *tag*, not to *correct*.
|
||||
|
||||
## Limited vs full range pitfall
|
||||
|
||||
H.264 for video is typically **limited/TV range** (luma 16-235). If AE/AME outputs full-range pixels but the file is tagged limited (or vice versa), blacks crush or wash. Symptoms: milky blacks = full data read as limited; crushed blacks = limited data read as full. Keep range consistent end-to-end and tag it (`color_range`).
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| Washed-out / milky blacks | Untagged H.264, player adds gamma | Tag Rec.709 in Output Module > Color Management |
|
||||
| Fine in QuickTime, dark in Chrome | QuickTime ~1.96 gamma | Trust Chrome; QuickTime is wrong for web |
|
||||
| Differs macOS vs Windows | No embedded color tag | Embed explicit Rec.709/sRGB tag |
|
||||
| Banding in gradients | 8 bpc project | Project Settings > Depth = 16 bpc |
|
||||
| Glows/blurs look wrong | Non-linear blending | Enable Linearize Working Space |
|
||||
| Screenshots/UI look off | Wrong working space | Use sRGB working space, not Rec.709 |
|
||||
|
||||
## Gotchas
|
||||
|
||||
- "Working Space = None" disables AE color management entirely — the viewer then lies about the final look. Always set an explicit space for managed delivery.
|
||||
- Toggling Linearize Working Space late changes the look of every feather, glow, and screen blend; decide at project start.
|
||||
- AME "Match Source" can silently drop the color tag; verify the exported file's tag (e.g. with `ffprobe -show_streams`, look for `color_transfer`/`color_primaries`).
|
||||
- ProRes is not magic — an untagged ProRes still shifts. The *tag* is what fixes consistency, not the codec.
|
||||
- sRGB and Rec.709 share primaries but differ in transfer (sRGB ~2.2 piecewise vs Rec.709 display 2.4); using the wrong one shifts midtone brightness.
|
||||
|
||||
## Pre-export checklist
|
||||
|
||||
- [ ] Project Settings > Color: Working Space set to the deliverable's space (Rec.709 Gamma 2.4 for SDR video; sRGB for UI/stills).
|
||||
- [ ] Project Settings > Color: Depth = 16 bpc (or 32 for HDR/heavy linear).
|
||||
- [ ] Linearize Working Space decision made at project start (on for glow/blur-heavy comps).
|
||||
- [ ] Display Color Management toggle ON while reviewing, so the viewer reflects final look.
|
||||
- [ ] 50% grey test patch placed and noted before render.
|
||||
- [ ] Output Module > Color Management: explicit output profile = Rec.709 Gamma 2.4 (not "Preserve RGB" when a shift exists).
|
||||
- [ ] Codec chosen per target (ProRes 422 HQ/4444 master, H.264 web).
|
||||
- [ ] Reviewed final in **Chrome / Premiere / Resolve**, not QuickTime.
|
||||
- [ ] `ffprobe` confirms color_primaries/transfer/space = bt709 (or sRGB target) and color_range consistent.
|
||||
- [ ] Re-imported render and re-sampled the grey patch; value matches within ~2-3 levels.
|
||||
|
||||
## When the client only has QuickTime
|
||||
|
||||
If the approval environment is genuinely locked to macOS QuickTime and the file must look right *there*:
|
||||
|
||||
1. Deliver correctly tagged ProRes (modern macOS QuickTime respects Rec.709 tags fairly well).
|
||||
2. If a residual shift remains and cannot be avoided, produce a *separate* QuickTime-targeted file with a measured compensating gamma adjustment — and clearly label it "QuickTime-only; do not use for web." Never let this compensated file become the web master.
|
||||
3. Educate: provide the same file opened in Chrome side-by-side so the difference is attributed to the player, not the work.
|
||||
@@ -0,0 +1,180 @@
|
||||
# Palettes, gradients, interpolation, and grading
|
||||
|
||||
Exact recipes and runnable code. OKLCH notation: `oklch(L C H)`, L in 0-1 perceptual lightness, C chroma (0 to ~0.37), H hue degrees.
|
||||
|
||||
## Palette construction rules
|
||||
|
||||
1. **Count**: 1 primary, 1 accent, 2-3 neutrals. Resist adding hues.
|
||||
2. **Contrast from lightness**: build a 5-9 step ramp by varying L while holding C and H roughly constant, nudging chroma down at the extremes (very light/dark colors can't hold high chroma).
|
||||
3. **Accent placement**: accent hue should sit 90-180deg from the primary on the OKLCH wheel for clean separation; complementary (~180deg) is boldest, analogous (~30deg) is calmest.
|
||||
4. **Neutral tinting**: give neutrals a tiny chroma (0.005-0.02) in the primary's hue family so the frame feels unified, not assembled from separate kits.
|
||||
5. **Dark-scene accents**: high L (0.7-0.85), moderate C (0.12-0.18). Avoid max chroma — it vibrates on dark backgrounds.
|
||||
|
||||
### Generating a tonal ramp (hold hue, vary lightness)
|
||||
|
||||
```
|
||||
oklch(0.97 0.03 264) // 50
|
||||
oklch(0.92 0.06 264) // 100
|
||||
oklch(0.84 0.10 264) // 200
|
||||
oklch(0.74 0.14 264) // 300
|
||||
oklch(0.66 0.18 264) // 400
|
||||
oklch(0.58 0.19 264) // 500 (base)
|
||||
oklch(0.50 0.17 264) // 600
|
||||
oklch(0.40 0.14 264) // 700
|
||||
oklch(0.28 0.10 264) // 800
|
||||
oklch(0.18 0.06 264) // 900
|
||||
```
|
||||
|
||||
Note chroma rises toward the mid-lightness band and falls at both ends — this matches what's physically representable and looks natural.
|
||||
|
||||
### Named starter palettes (OKLCH)
|
||||
|
||||
```
|
||||
// "Midnight product" — confident, techy
|
||||
primary oklch(0.62 0.19 264) accent oklch(0.78 0.17 50)
|
||||
bg oklch(0.16 0.02 264) fg oklch(0.97 0.005 264)
|
||||
|
||||
// "Warm editorial" — premium, soft
|
||||
primary oklch(0.55 0.12 25) accent oklch(0.80 0.10 90)
|
||||
bg oklch(0.96 0.01 60) fg oklch(0.25 0.03 25)
|
||||
|
||||
// "Neon dark" — energetic, bold
|
||||
primary oklch(0.72 0.20 150) accent oklch(0.68 0.24 330)
|
||||
bg oklch(0.14 0.03 280) fg oklch(0.95 0.02 150)
|
||||
```
|
||||
|
||||
## Gradient cookbook
|
||||
|
||||
### Two-stop, vivid (OKLCH)
|
||||
|
||||
```css
|
||||
background: linear-gradient(135deg in oklch, #2b6cff, #ffb02e);
|
||||
/* control the hue arc explicitly */
|
||||
background: linear-gradient(in oklch shorter hue, oklch(0.6 0.2 264), oklch(0.8 0.17 50));
|
||||
```
|
||||
|
||||
`shorter hue` / `longer hue` / `increasing hue` / `decreasing hue` pick the direction around the wheel — `longer hue` produces rainbow sweeps, `shorter` the direct path.
|
||||
|
||||
### Mesh background (stacked radials)
|
||||
|
||||
```css
|
||||
.mesh {
|
||||
background-color: #0b0f1a;
|
||||
background-image:
|
||||
radial-gradient(at 18% 22%, oklch(0.62 0.19 264 / 0.7), transparent 50%),
|
||||
radial-gradient(at 82% 28%, oklch(0.70 0.16 320 / 0.6), transparent 55%),
|
||||
radial-gradient(at 55% 88%, oklch(0.78 0.17 50 / 0.5), transparent 50%),
|
||||
radial-gradient(at 40% 50%, oklch(0.55 0.14 200 / 0.4), transparent 60%);
|
||||
}
|
||||
```
|
||||
|
||||
### Animated drift
|
||||
|
||||
```css
|
||||
.mesh { background-size: 140% 140%; animation: drift 16s ease-in-out infinite alternate; }
|
||||
@keyframes drift {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 12% -8%; }
|
||||
}
|
||||
```
|
||||
|
||||
### Conic
|
||||
|
||||
```css
|
||||
background: conic-gradient(in oklch from 90deg, #2b6cff, #b02eff, #ffb02e, #2b6cff);
|
||||
```
|
||||
|
||||
### Grain overlay (kills banding)
|
||||
|
||||
```html
|
||||
<svg width="0" height="0">
|
||||
<filter id="grain"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="2"/></filter>
|
||||
</svg>
|
||||
```
|
||||
```css
|
||||
.grain::after {
|
||||
content: ""; position: absolute; inset: 0; pointer-events: none;
|
||||
filter: url(#grain); opacity: 0.04; mix-blend-mode: overlay;
|
||||
}
|
||||
```
|
||||
|
||||
## Perceptual interpolation in code
|
||||
|
||||
### culori (JS) — interpolate and sample
|
||||
|
||||
```js
|
||||
import { interpolate, formatHex, formatCss } from "culori";
|
||||
|
||||
// Interpolate in OKLCH (vivid midpoints, even lightness)
|
||||
const mix = interpolate(["#2b6cff", "#ffb02e"], "oklch");
|
||||
formatHex(mix(0.5)); // vivid mid, not gray
|
||||
|
||||
// Build N stops for an animated transition
|
||||
const stops = Array.from({ length: 60 }, (_, i) => formatCss(mix(i / 59)));
|
||||
|
||||
// Control hue direction
|
||||
import { fixupHueLonger } from "culori";
|
||||
const rainbow = interpolate(["#2b6cff", "#ffb02e"], "oklch", {
|
||||
h: { use: fixupHueLonger },
|
||||
});
|
||||
```
|
||||
|
||||
### d3-interpolate / d3-scale-chromatic
|
||||
|
||||
```js
|
||||
import { interpolateLab, interpolateHcl, quantize } from "d3-interpolate";
|
||||
|
||||
const lab = interpolateLab("#2b6cff", "#ffb02e"); // perceptual, smooth lightness
|
||||
lab(0.5); // mid color
|
||||
const ramp = quantize(interpolateHcl("#2b6cff", "#ffb02e"), 8); // 8 even stops
|
||||
```
|
||||
|
||||
`interpolateLab` and `interpolateHcl` avoid the sRGB gray-midpoint problem; `interpolateRgb` does not — only use RGB interpolation for near-identical colors.
|
||||
|
||||
### Animating a color with @property (CSS, browser interpolates)
|
||||
|
||||
```css
|
||||
@property --from { syntax: "<color>"; initial-value: oklch(0.62 0.19 264); inherits: false; }
|
||||
.swatch { background: var(--from); transition: --from 400ms cubic-bezier(0.16,1,0.3,1); }
|
||||
.swatch.active { --from: oklch(0.78 0.17 50); }
|
||||
```
|
||||
|
||||
Without `@property` registration the color snaps instead of interpolating. The browser interpolates registered `<color>` properties in OKLab by default — perceptually clean for free.
|
||||
|
||||
## Grading order checklist
|
||||
|
||||
Apply in this exact order; reordering changes the result.
|
||||
|
||||
1. **Exposure / white balance** — set overall brightness, neutralize or intentionally tint the cast.
|
||||
2. **Contrast** — set black/white points, expand or compress tonal range.
|
||||
3. **Midtones / saturation** — overall saturation and midtone color shift.
|
||||
4. **Split-tone** — tint shadows and highlights separately. Cinematic default: teal shadows (`hue ~190`), orange highlights (`hue ~40`), kept subtle.
|
||||
5. **Unify** — final overall tint / LUT and a gentle vignette.
|
||||
|
||||
### CSS-filter grade (quick, for web video/canvas)
|
||||
|
||||
```css
|
||||
.graded {
|
||||
filter:
|
||||
brightness(1.02) /* 1: exposure */
|
||||
contrast(1.12) /* 2: contrast */
|
||||
saturate(1.08); /* 3: saturation */
|
||||
}
|
||||
/* 4 split-tone: overlay a teal->orange gradient at low opacity with mix-blend-mode */
|
||||
```
|
||||
|
||||
### Fragment-shader split-tone (GLSL sketch)
|
||||
|
||||
```glsl
|
||||
vec3 graded = color;
|
||||
float lum = dot(graded, vec3(0.2126, 0.7152, 0.0722));
|
||||
vec3 shadowTint = vec3(0.0, 0.45, 0.5); // teal
|
||||
vec3 highlightTint = vec3(1.0, 0.6, 0.2); // orange
|
||||
graded = mix(graded, graded * shadowTint, (1.0 - lum) * 0.12);
|
||||
graded = mix(graded, graded + highlightTint * 0.06, lum);
|
||||
```
|
||||
|
||||
Keep split-tone strength under ~0.15; over-tinting reads as a cheap Instagram filter. Grade the whole piece with the same chain so cuts match.
|
||||
|
||||
---
|
||||
Interpolate color in perceptual space and gradients shift without muddy mids. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=motion-design-skills&utm_content=skill_footer&utm_term=color-motion)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
Whitespace-only changes.
@@ -0,0 +1,148 @@
|
||||
---
|
||||
name: logo-animation
|
||||
description: This skill should be used when the user asks to "animate our logo", "make a logo intro/stinger", "build a brand reveal", "create an app splash animation", "loop a loader logo", "draw on our SVG mark", "morph an icon into the wordmark", or "sync the logo to a sound-logo". Covers web (SVG/Lottie), video, and app-splash delivery.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Logo Animation
|
||||
|
||||
Bring a brand mark to life with one clear idea, on-brand geometry, and a crisp settle. Animate for web (SVG/Lottie), video intros/outros, or app splash screens, and always ship a static end-frame fallback.
|
||||
|
||||
## When to use
|
||||
|
||||
- Build an intro stinger, sign-off, app splash, or loader idle loop.
|
||||
- Reveal a brand mark on a landing hero or sizzle reel.
|
||||
- Draw on an SVG mark, wipe/build it in piece by piece, or morph an icon into a wordmark.
|
||||
- Sync the final settle of the mark to a sound-logo beat.
|
||||
|
||||
## Core principle
|
||||
|
||||
Pick ONE technique per logo — draw OR build OR morph — not all at once. Reveals run 0.8–2.5s. Never distort brand geometry; scale and fade are safe, skew and squash usually are not. Respect clearspace (keep the mark's minimum margin clear of motion debris). Always provide a `prefers-reduced-motion` path that shows the final mark with no motion.
|
||||
|
||||
## Technique map
|
||||
|
||||
| Technique | Best for | Mechanism |
|
||||
|---|---|---|
|
||||
| Stroke draw-on | Line marks, monograms, signatures | `stroke-dashoffset` 1→0 |
|
||||
| Mask wipe / reveal | Filled shapes, gradients | animate a `<clipPath>` / `<mask>` rect |
|
||||
| Build-on (stagger) | Multi-piece marks, grids | per-group opacity+transform, staggered |
|
||||
| Morph | Icon ↔ wordmark, shape→logo | GSAP MorphSVG or matched path interpolation |
|
||||
| Idle loop | Loaders, ambient splash | seamless cyclic transform (rotate/pulse) |
|
||||
| Wordmark | Logotypes | per-letter mask/translate stagger |
|
||||
|
||||
## Quick start: SVG stroke draw-on
|
||||
|
||||
The fastest on-brand web reveal. Each path draws itself, then fills settle in.
|
||||
|
||||
```html
|
||||
<svg viewBox="0 0 200 80" id="logo">
|
||||
<path class="ink" d="M10 60 L40 20 L70 60" fill="none"
|
||||
stroke="#111" stroke-width="6" stroke-linecap="round"
|
||||
pathLength="1" stroke-dasharray="1" stroke-dashoffset="1"/>
|
||||
</svg>
|
||||
<style>
|
||||
@keyframes draw { to { stroke-dashoffset: 0; } }
|
||||
#logo .ink { animation: draw 1.1s cubic-bezier(.65,0,.35,1) forwards; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#logo .ink { animation: none; stroke-dashoffset: 0; }
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
`pathLength="1"` normalizes every path to length 1, so one `stroke-dasharray`/`offset` value works regardless of geometry. Stagger multiple strokes with `animation-delay`.
|
||||
|
||||
## Quick start: build-on with stagger (GSAP)
|
||||
|
||||
```js
|
||||
import gsap from "gsap";
|
||||
// each <g class="piece"> is one mark component
|
||||
gsap.from(".piece", {
|
||||
opacity: 0, scale: 0.85, transformOrigin: "50% 50%",
|
||||
duration: 0.5, ease: "back.out(1.6)", stagger: 0.08,
|
||||
onComplete: () => gsap.to("#mark", { scale: 1, duration: 0.2, ease: "power2.out" })
|
||||
});
|
||||
```
|
||||
|
||||
Group order matters: reveal the base/container first, then accents, then the wordmark last so the eye lands on the name.
|
||||
|
||||
## Sound-logo sync
|
||||
|
||||
When audio exists, the brand's final settle frame must land exactly on the sonic accent (the "ding"/whoosh peak). Decide the audio first, find the peak timecode, then time the reveal so the mark snaps to full opacity/scale on that frame. A 1.0s reveal landing a 1.2s mnemonic feels premium; a settle that floats free of the sound feels cheap. Add a tiny overshoot-and-settle (scale 1.04→1.00 over ~0.15s) on the beat for impact.
|
||||
|
||||
## Delivery targets
|
||||
|
||||
- **Web, interactive / hero**: inline SVG + CSS or GSAP. Smallest payload, crisp at any size, scriptable.
|
||||
- **Product / app, designer-made**: Lottie (`.json` exported from After Effects via Bodymovin). Use `lottie-web` or `@lottiefiles/dotlottie-web`; set `loop`, `autoplay`, and a poster fallback frame.
|
||||
- **Video intro/outro**: Remotion (React) or After Effects render to ProRes/H.264. Pre-render at 2x for crispness.
|
||||
|
||||
```js
|
||||
// Lottie delivery with reduced-motion guard
|
||||
import { DotLottie } from "@lottiefiles/dotlottie-web";
|
||||
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
const dl = new DotLottie({
|
||||
canvas: document.querySelector("#logo-canvas"),
|
||||
src: "/logo.lottie", autoplay: !reduce, loop: false,
|
||||
});
|
||||
if (reduce) dl.addEventListener("load", () => dl.setFrame(dl.totalFrames - 1));
|
||||
```
|
||||
|
||||
## Output checklist
|
||||
|
||||
- Animated mark in the target format (SVG/Lottie/video).
|
||||
- Static final-frame fallback (SVG or PNG) for reduced-motion and posters.
|
||||
- One clear technique, reveal ≤2.5s, brand geometry intact, clearspace respected.
|
||||
- Sound-logo settle aligned to the audio peak (if audio present).
|
||||
|
||||
## Deliver & verify (standalone HTML)
|
||||
|
||||
> **Packaged helper** (`scripts/`): `scripts/seek-shot.sh anim.html 0 1.5 3` freezes the `?t=N` harness and screenshots each moment; `scripts/contact-sheet.sh sheet.png frame-*.png` tiles them for one-glance review. See `scripts/README.md`.
|
||||
|
||||
Logo animation spans web/SVG/Lottie, video, and app splash. **For web/SVG/Lottie delivery use this standalone-HTML loop; for a video logo sting, render via the remotion-video verify loop instead.**
|
||||
|
||||
For web/SVG/Lottie, ship **one `.html` file that opens directly in a browser** — inline SVG markup plus the reveal (CSS or one CDN GSAP `<script>`), self-contained. One file is the right tier.
|
||||
|
||||
**Output contract:**
|
||||
- One `.html` file: the inline SVG (or Lottie canvas) + the reveal in inline `<style>`/`<script>`.
|
||||
- One animation driver — the CSS `@keyframes` draw/build, or a single GSAP timeline; not both.
|
||||
- Include the freeze harness below, matched to the technique, so any moment can be screenshotted deterministically.
|
||||
|
||||
**Freeze harness — pin a frame for screenshots.** Match the mechanism to the technique:
|
||||
|
||||
```html
|
||||
<script>
|
||||
const t = new URLSearchParams(location.search).get("t");
|
||||
if (t !== null) {
|
||||
const T = parseFloat(t);
|
||||
// CSS stroke-draw / build-on:
|
||||
document.querySelectorAll("#logo *").forEach(el => {
|
||||
el.style.animationDelay = (-T) + "s";
|
||||
el.style.animationPlayState = "paused";
|
||||
});
|
||||
// GSAP timeline instead? → tl.pause(); tl.seek(T);
|
||||
// SMIL <animate>? → const s = document.querySelector("svg"); s.pauseAnimations(); s.setCurrentTime(T);
|
||||
// Lottie? → dl.setFrame(Math.round(T * fps));
|
||||
}
|
||||
window.__ready = true; // ready signal for headless wait
|
||||
</script>
|
||||
```
|
||||
|
||||
**Verify loop — render → freeze → screenshot → check:**
|
||||
1. Open the file frozen at start / mid / end of the reveal (≤2.5s): `…/logo.html?t=0`, `?t=<dur/2>`, `?t=<dur>`.
|
||||
2. Screenshot each frozen frame.
|
||||
3. Check **fidelity** (matches the mark, one clear technique) and **artifacts** — brand geometry undistorted, clearspace clear of motion debris, no half-drawn strokes left at the end frame, the static final frame is the true logo.
|
||||
4. **Iterate:** if any check fails (e.g. a half-drawn stroke at `?t=<dur>` or distorted geometry mid-reveal), fix the stroke-dashoffset/duration/easing, re-render the **same** `?t=N`, and re-screenshot to confirm the fix — loop until every frame is clean.
|
||||
|
||||
```bash
|
||||
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/logo.html?t=1.1" frame-mid.png
|
||||
```
|
||||
|
||||
**Before you finish:**
|
||||
1. Opens standalone in a browser — no console errors, no missing CDN.
|
||||
2. One technique/driver; `?t=N` freezes the exact frame correctly.
|
||||
3. Screenshotted at start / mid / end — geometry intact, clearspace respected, final frame is the clean mark.
|
||||
4. `prefers-reduced-motion` honored (final static mark shown, no motion) + static end-frame fallback shipped.
|
||||
5. Reveal ≤2.5s; if audio present, the settle lands on the sound-logo peak.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/logo-patterns.md` — runnable code for every technique: stroke draw-on, mask wipe build-on, GSAP MorphSVG morph, seamless idle loop, Lottie delivery, plus timing tables (0.8–2.5s), brand clearspace rules, and reduced-motion fallbacks.
|
||||
@@ -0,0 +1,199 @@
|
||||
# Logo Animation Patterns — Cookbook
|
||||
|
||||
Self-contained, runnable recipes for every logo technique. Pick ONE per logo.
|
||||
|
||||
## Timing guidance
|
||||
|
||||
| Beat | Duration | Easing |
|
||||
|---|---|---|
|
||||
| Stroke draw-on (per path) | 0.6–1.2s | `cubic-bezier(.65,0,.35,1)` (easeInOutCubic) |
|
||||
| Mask wipe reveal | 0.5–0.9s | `cubic-bezier(.22,1,.36,1)` (easeOutQuint) |
|
||||
| Build-on per piece | 0.4–0.6s, stagger 0.06–0.10s | `back.out(1.5)` |
|
||||
| Morph | 0.6–1.0s | `power2.inOut` |
|
||||
| Final settle / overshoot | 0.12–0.20s | scale 1.04→1.00, `power2.out` |
|
||||
| Full reveal envelope | 0.8–2.5s total | — |
|
||||
| Idle loop cycle | 1.5–4s, infinite | `sine.inOut` / `linear` for spin |
|
||||
|
||||
Rule of thumb: longer marks (wordmarks, detailed monograms) sit near 2.0–2.5s; simple icons near 0.8–1.4s. Hold the settled mark at least 0.4s before any sign-off cut.
|
||||
|
||||
## Brand clearspace & geometry rules
|
||||
|
||||
- **Clearspace**: keep a margin equal to the mark's cap-height (or the brand's defined unit) clear of moving particles, wipes, or overshoot. Motion must not visually crop into the safe margin.
|
||||
- **No distortion**: scale uniformly, fade, translate, rotate-on-axis, and mask are safe. Avoid non-uniform `scaleX≠scaleY`, skew, and squash on the final mark — they read as off-brand. A squash-and-settle is acceptable ONLY as a sub-0.2s micro-overshoot that returns to 1.0.
|
||||
- **Color**: never animate brand color away from spec mid-reveal. Reveal in brand color, or grayscale→brand as an intentional, approved beat.
|
||||
- **Lockup integrity**: animate the symbol and wordmark with a clear hierarchy (symbol leads, wordmark follows) but land both in the exact locked relationship.
|
||||
|
||||
## 1. Stroke draw-on (SVG + CSS)
|
||||
|
||||
```html
|
||||
<svg viewBox="0 0 240 80" id="mark">
|
||||
<path class="ink" pathLength="1" fill="none" stroke="#0A84FF"
|
||||
stroke-width="7" stroke-linecap="round" stroke-linejoin="round"
|
||||
d="M12 64 L48 16 L84 64 M120 16 L120 64 M156 16 H204"/>
|
||||
</svg>
|
||||
<style>
|
||||
#mark .ink {
|
||||
stroke-dasharray: 1; stroke-dashoffset: 1;
|
||||
animation: draw 1.1s cubic-bezier(.65,0,.35,1) forwards;
|
||||
}
|
||||
@keyframes draw { to { stroke-dashoffset: 0; } }
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
#mark .ink { animation: none; stroke-dashoffset: 0; }
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
Draw-then-fill (line settles into a solid mark):
|
||||
|
||||
```css
|
||||
#mark .ink { fill: transparent; }
|
||||
@keyframes fill { to { fill: #0A84FF; stroke-width: 0; } }
|
||||
#mark .ink { animation: draw 1.1s cubic-bezier(.65,0,.35,1) forwards,
|
||||
fill .3s ease .9s forwards; }
|
||||
```
|
||||
|
||||
GSAP equivalent with per-path stagger via DrawSVGPlugin:
|
||||
|
||||
```js
|
||||
import gsap from "gsap";
|
||||
import { DrawSVGPlugin } from "gsap/DrawSVGPlugin";
|
||||
gsap.registerPlugin(DrawSVGPlugin);
|
||||
gsap.fromTo("#mark .ink",
|
||||
{ drawSVG: "0%" },
|
||||
{ drawSVG: "100%", duration: 1.0, ease: "power2.inOut", stagger: 0.15 });
|
||||
```
|
||||
|
||||
## 2. Mask wipe / build-on (clipPath)
|
||||
|
||||
Reveal a filled mark by sweeping a clip rect across it.
|
||||
|
||||
```html
|
||||
<svg viewBox="0 0 200 80">
|
||||
<defs>
|
||||
<clipPath id="wipe"><rect id="wiper" x="0" y="0" width="0" height="80"/></clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#wipe)">
|
||||
<!-- the full filled logo art here -->
|
||||
<path d="M10 10 H190 V70 H10 Z" fill="#111"/>
|
||||
</g>
|
||||
</svg>
|
||||
<style>
|
||||
#wiper { animation: wipe .8s cubic-bezier(.22,1,.36,1) forwards; }
|
||||
@keyframes wipe { to { width: 200px; } }
|
||||
@media (prefers-reduced-motion: reduce){ #wiper{ animation:none; width:200px; } }
|
||||
</style>
|
||||
```
|
||||
|
||||
Directional variants: animate `y`/`height` for a top-down build, or use a radial `<clipPath>` with a growing `<circle r>` for an iris reveal.
|
||||
|
||||
## 3. Build-on with stagger (GSAP timeline)
|
||||
|
||||
```js
|
||||
import gsap from "gsap";
|
||||
const tl = gsap.timeline({ defaults: { ease: "back.out(1.6)" } });
|
||||
tl.from("#base", { scale: .8, opacity: 0, transformOrigin: "50% 50%", duration: .5 })
|
||||
.from(".accent", { scale: .7, opacity: 0, transformOrigin: "50% 50%",
|
||||
duration: .45, stagger: 0.08 }, "-=0.2")
|
||||
.from(".word > *", { yPercent: 120, opacity: 0, duration: .4,
|
||||
stagger: 0.04, ease: "power3.out" }, "-=0.15")
|
||||
.to("#mark", { scale: 1.04, duration: .12, ease: "power2.out" })
|
||||
.to("#mark", { scale: 1.0, duration: .14, ease: "power2.inOut" });
|
||||
```
|
||||
|
||||
## 4. Morph (GSAP MorphSVG)
|
||||
|
||||
Morph requires two paths; MorphSVG handles differing point counts.
|
||||
|
||||
```js
|
||||
import gsap from "gsap";
|
||||
import { MorphSVGPlugin } from "gsap/MorphSVGPlugin";
|
||||
gsap.registerPlugin(MorphSVGPlugin);
|
||||
// #shape: a simple start shape (e.g. circle path) -> #logoPath: the brand mark
|
||||
gsap.to("#shape", {
|
||||
duration: .9, ease: "power2.inOut",
|
||||
morphSVG: { shape: "#logoPath", shapeIndex: "auto" }
|
||||
});
|
||||
```
|
||||
|
||||
No-plugin fallback (Flubber for interpolation + requestAnimationFrame):
|
||||
|
||||
```js
|
||||
import { interpolate } from "flubber";
|
||||
const from = "M10,10 ...", to = "M0,0 ...";
|
||||
const lerp = interpolate(from, to, { maxSegmentLength: 2 });
|
||||
const path = document.querySelector("#shape");
|
||||
const dur = 900, t0 = performance.now();
|
||||
(function frame(now){
|
||||
const k = Math.min(1, (now - t0) / dur);
|
||||
const e = k < .5 ? 2*k*k : 1 - Math.pow(-2*k+2, 2)/2; // easeInOutQuad
|
||||
path.setAttribute("d", lerp(e));
|
||||
if (k < 1) requestAnimationFrame(frame);
|
||||
})(t0);
|
||||
```
|
||||
|
||||
## 5. Seamless idle loop (loader)
|
||||
|
||||
Loop must have identical start and end frames — design the cycle so frame 0 == frame N.
|
||||
|
||||
```css
|
||||
#loader { animation: spin 1.4s linear infinite, pulse 2.8s ease-in-out infinite; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes pulse { 0%,100%{ opacity:.85 } 50%{ opacity:1 } }
|
||||
@media (prefers-reduced-motion: reduce){
|
||||
#loader { animation: pulse 2.8s ease-in-out infinite; } /* drop rotation */
|
||||
}
|
||||
```
|
||||
|
||||
GSAP `yoyo` loop for a breathing mark:
|
||||
|
||||
```js
|
||||
gsap.to("#mark", { scale: 1.03, duration: 1.4, ease: "sine.inOut",
|
||||
yoyo: true, repeat: -1 });
|
||||
```
|
||||
|
||||
## 6. Lottie delivery (designer-made)
|
||||
|
||||
Export from After Effects via Bodymovin to `.json`, or compressed `.lottie`.
|
||||
|
||||
```html
|
||||
<canvas id="logo-canvas" width="512" height="512"></canvas>
|
||||
<script type="module">
|
||||
import { DotLottie } from "https://cdn.jsdelivr.net/npm/@lottiefiles/dotlottie-web/+esm";
|
||||
const reduce = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
const dl = new DotLottie({
|
||||
canvas: document.querySelector("#logo-canvas"),
|
||||
src: "/logo.lottie", autoplay: !reduce, loop: false, speed: 1,
|
||||
});
|
||||
// Reduced motion: jump to the final settled frame, no playback.
|
||||
if (reduce) dl.addEventListener("load", () => dl.setFrame(dl.totalFrames - 1));
|
||||
// Sound-logo sync: fire audio when the settle frame is reached.
|
||||
dl.addEventListener("frame", ({ currentFrame }) => {
|
||||
if (currentFrame >= SETTLE_FRAME && !audioFired) { audio.play(); audioFired = true; }
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
Classic `lottie-web` (SVG renderer, sharpest for vector logos):
|
||||
|
||||
```js
|
||||
import lottie from "lottie-web";
|
||||
const anim = lottie.loadAnimation({
|
||||
container: document.getElementById("logo"),
|
||||
renderer: "svg", loop: false, autoplay: !reduce, path: "/logo.json",
|
||||
});
|
||||
if (reduce) anim.goToAndStop(anim.totalFrames - 1, true);
|
||||
```
|
||||
|
||||
Lottie tips: keep layers/effects Bodymovin-supported (no native blurs/expressions unless baked), name layers cleanly, and request a poster PNG of the final frame from the designer for the static fallback.
|
||||
|
||||
## 7. Reduced-motion fallback (always ship)
|
||||
|
||||
Every delivery must degrade to the static, fully-revealed mark:
|
||||
|
||||
- **CSS/SVG**: `@media (prefers-reduced-motion: reduce)` sets the final state (`stroke-dashoffset:0`, `width:full`, `opacity:1`) with `animation:none`.
|
||||
- **Lottie**: `goToAndStop(totalFrames-1)` / `setFrame(last)`.
|
||||
- **Video**: serve a poster image of the last frame; only autoplay motion when motion is allowed.
|
||||
- **JS gate**: branch on `matchMedia("(prefers-reduced-motion: reduce)").matches` before building any timeline.
|
||||
|
||||
---
|
||||
Match the reveal to the delivery target and a logo lands across web, video, and app. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=motion-design-skills&utm_content=skill_footer&utm_term=logo-animation)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
Whitespace-only changes.
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: motion-art-direction
|
||||
description: This skill should be used when the user asks to "direct a motion graphics video", "do creative/art direction for a video", "define a motion language", "set the tone and pacing", "make the animation feel consistent", "decide what to animate (and what not to)", "turn a brief/brand into motion choices", or needs the senior creative judgment that sits above specific techniques (gsap/lottie/shader). Covers motion language, tone & energy, pacing/rhythm, motion hierarchy, restraint, and cross-shot consistency.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Motion Direction
|
||||
|
||||
Make the creative decisions for a motion piece before anyone animates a frame. Direction is the judgment layer: it turns a brief and a brand into a small, repeatable set of motion rules — one easing vocabulary, one timing scale, one transition family — so every shot feels like it came from the same hand. Good direction is mostly subtraction: deciding what NOT to move.
|
||||
|
||||
## When to use
|
||||
|
||||
- Starting any piece — explainer, launch film, UI demo, data story, loop — and the look/feel isn't decided yet.
|
||||
- Animation feels "busy", "cheap", or "inconsistent" and the cause is decisions, not craft.
|
||||
- Orchestrating other skills (gsap/lottie/shader/three.js): direction sets the rules they execute.
|
||||
- Translating a brand or brief into directable, written choices a team (or an agent) can follow.
|
||||
|
||||
## The one rule
|
||||
|
||||
**Define the motion language first, then animate to it.** A motion language is a short spec — pick *one* of each: easing family, base timing unit, transition family, stagger rhythm, motion intensity. Everything in the piece obeys it. Consistency reads as confidence; variety reads as noise. When in doubt, reuse, don't invent.
|
||||
|
||||
## Direction workflow
|
||||
|
||||
| Step | Decision | Output |
|
||||
|---|---|---|
|
||||
| 1. Brief | Audience, one message, platform, duration, mood (3 adjectives) | One-line creative intent |
|
||||
| 2. Tone & energy | Where it sits on calm↔kinetic and soft↔sharp | A point on the matrix below |
|
||||
| 3. Motion language | Lock the spec table | The motion-language spec (filled) |
|
||||
| 4. Hierarchy | Rank what moves: hero / support / texture | Motion hierarchy list |
|
||||
| 5. Restraint pass | Cut motion that doesn't carry meaning | List of what stays static |
|
||||
| 6. Shot direction | Note intent per shot, not keyframes | Direction notes (see template) |
|
||||
| 7. Consistency check | Audit against the spec | Pass/fail per shot |
|
||||
|
||||
## Tone & energy matrix
|
||||
|
||||
Place the piece on two axes; the cell dictates the motion language defaults.
|
||||
|
||||
| | Soft (organic, eased) | Sharp (precise, snappy) |
|
||||
|---|---|---|
|
||||
| **Calm** | Luxury, wellness, editorial — slow, long eases, generous holds, minimal stagger | Premium tech, fintech — deliberate, clean linear-ish moves, tight but unhurried |
|
||||
| **Kinetic** | Playful, lifestyle, kids — bouncy/overshoot, springy, loose timing | Sports, hype, gaming — fast cuts, hard snaps, aggressive overshoot on beats |
|
||||
|
||||
Pick one cell and commit. Mixing cells within a piece is the most common cause of "inconsistent" motion.
|
||||
|
||||
## Motion personality (named presets)
|
||||
|
||||
The tone matrix tells you *where* the piece sits; a **motion personality** is the named, numeric preset you hand to whoever animates. Pick ONE per project and apply it everywhere. This is shared vocabulary — other skills (e.g. `gsap-web`, `micro-interaction`, the video packs) can just say "use the Premium personality."
|
||||
|
||||
| Personality | Duration | Signature easing | Overshoot | Reads as |
|
||||
|---|---|---|---|---|
|
||||
| **Playful** | 150–300ms | `ease-out-back` / `cubic-bezier(0.34,1.56,0.64,1)` | 10–20% | fun, bouncy, whimsical |
|
||||
| **Premium** | 350–600ms | `cubic-bezier(0.4,0,0.2,1)` | 0% | elegant, minimal, luxury |
|
||||
| **Corporate** | 200–400ms | `cubic-bezier(0.2,0,0,1)` | 0–3% | clean, professional, trustworthy |
|
||||
| **Energetic** | 100–250ms | `ease-out-expo` | 15–30% | bold, dynamic, hype |
|
||||
|
||||
Default: **Corporate** for UI/product, **Playful** for illustration/lifestyle. Maps onto the matrix — Premium≈calm-soft, Corporate≈calm-sharp, Playful≈kinetic-soft, Energetic≈kinetic-sharp. The personality fills the "easing family / base timing unit / motion intensity" rows of the spec below.
|
||||
|
||||
## The motion-language spec
|
||||
|
||||
The core artifact. Fill every row with ONE choice and reuse it everywhere.
|
||||
|
||||
| Property | Choose one | Example (calm-sharp / premium tech) |
|
||||
|---|---|---|
|
||||
| Easing family | The signature curve for ~90% of moves | `cubic-bezier(0.22, 1, 0.36, 1)` (decelerate, soft landing) |
|
||||
| Base timing unit | The atomic duration; build others as multiples | 0.4s (enters 0.4s, big moves 0.8s, micro 0.2s) |
|
||||
| Transition family | How shots connect | Match-cut + crossfade only; no wipes/spins |
|
||||
| Stagger rhythm | Delay between grouped elements | 60ms cascade, top-to-bottom |
|
||||
| Motion intensity | Distance/scale/overshoot budget | Travel ≤ 24px, scale 0.96→1.0, overshoot ≤ 2% |
|
||||
| Hold discipline | Minimum rest between moves | ≥ 0.3s of stillness after each beat |
|
||||
|
||||
Two eases maximum: one "in" (exits) and one "out" (entrances/landings). A third curve must justify its existence.
|
||||
|
||||
## Motion hierarchy
|
||||
|
||||
Not everything earns motion. Rank every element, then animate down the list — and stop early. These three tiers are the universal **three motion layers** (see `animation-principles`): Hero = **Primary**, Support = **Secondary**, Texture = **Ambient**.
|
||||
|
||||
| Tier | What it is | How it moves |
|
||||
|---|---|---|
|
||||
| Hero | The one thing the shot is about | Gets the boldest, slowest, most-eased move; lands on the beat |
|
||||
| Support | Context that helps the hero land | Smaller, faster, eased *out* of the way; never competes |
|
||||
| Texture | Background, grain, ambient drift | Subliminal only — slow loops, low contrast, no hard cuts |
|
||||
|
||||
If two elements compete for the eye in one frame, the direction has failed. Demote one to support.
|
||||
|
||||
## Restraint — what NOT to animate
|
||||
|
||||
The senior move. For each element ask: *does the motion carry meaning?* If not, hold it still.
|
||||
|
||||
- Don't animate the entire frame at once — stagger or hold a anchor so the eye has a home.
|
||||
- Don't transition the transition (no animated wipe *and* a spin *and* a fade).
|
||||
- Don't loop-animate text the viewer is still reading.
|
||||
- Don't add overshoot to serious/financial content — it reads as toy-like.
|
||||
- One "wow" moment per piece. A second one cancels the first.
|
||||
|
||||
## Pacing & rhythm
|
||||
|
||||
Pacing is the edit's heartbeat. Vary it on purpose: tension (faster, tighter) → release (a hold). Map energy across the timeline before timing individual moves — most pieces want a low-energy open, a build, a peak, and a settled end card. Cut and land key moments on the audio (see launch-video for beat-syncing); silence and stillness are pacing tools, not gaps to fill.
|
||||
|
||||
## Consistency check
|
||||
|
||||
Before sign-off, audit every shot against the spec table:
|
||||
|
||||
- Same easing family on comparable moves? (No stray `linear` or `ease`.)
|
||||
- All durations are multiples of the base unit?
|
||||
- Only the chosen transition family between shots?
|
||||
- Stagger rhythm identical across grouped reveals?
|
||||
- Hold discipline respected — no two moves stacked without rest?
|
||||
- Exactly one hero per frame?
|
||||
|
||||
Any "no" is a direction defect, not a craft preference — fix it before polishing.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/direction-playbook.md` — worked examples of the motion-language spec for five tone cells, a full brief template, a shot-by-shot direction-notes template with example entries, brand→motion translation tables, an easing-vocabulary cheat sheet, and a complete pre-render direction checklist.
|
||||
@@ -0,0 +1,184 @@
|
||||
# Motion Direction Playbook
|
||||
|
||||
Depth behind the SKILL.md frameworks: a brief template, filled motion-language specs for five tone cells, brand→motion translation, a direction-notes template with worked entries, an easing cheat sheet, and a pre-render checklist.
|
||||
|
||||
## Creative brief template
|
||||
|
||||
Fill before any other decision. One line per row — if a row can't be answered, the piece isn't ready to direct.
|
||||
|
||||
```
|
||||
PIECE: <name / working title>
|
||||
AUDIENCE: <who, and what they already know>
|
||||
ONE MESSAGE: <the single thing they remember after watching>
|
||||
PLATFORM: <YouTube / Reels / in-app / landing hero — sets aspect, sound, duration>
|
||||
DURATION: <seconds; shorter = fewer ideas, not faster ideas>
|
||||
MOOD: <exactly 3 adjectives, e.g. "confident, precise, warm">
|
||||
BRAND CUES: <colors, type, logo motion rules, any existing motion system>
|
||||
CONSTRAINTS: <must-include / must-avoid; legal supers; localization>
|
||||
REFERENCE: <1–3 links + one line on WHAT to borrow from each>
|
||||
```
|
||||
|
||||
The 3 adjectives are load-bearing: they resolve every later judgment call. "Confident, precise, warm" rules out bouncy overshoot (not precise) and cold linear snaps (not warm) on its own.
|
||||
|
||||
## Filled motion-language specs (five tone cells)
|
||||
|
||||
Each is internally consistent — copy the closest one and adjust. Easings are CSS `cubic-bezier`; the same curves map to GSAP eases and Lottie keyframe handles.
|
||||
|
||||
### Calm + Soft — luxury / wellness / editorial
|
||||
| Property | Choice |
|
||||
|---|---|
|
||||
| Easing | `cubic-bezier(0.4, 0, 0.2, 1)` in/out, long tails |
|
||||
| Base unit | 0.6s (micro 0.3s, hero 1.2s) |
|
||||
| Transition | Slow crossfade + gentle scale; no hard cuts |
|
||||
| Stagger | 120ms, generous |
|
||||
| Intensity | Travel ≤ 16px, scale 0.98→1.0, zero overshoot |
|
||||
| Hold | ≥ 0.6s stillness; let frames breathe |
|
||||
|
||||
### Calm + Sharp — premium tech / fintech
|
||||
| Property | Choice |
|
||||
|---|---|
|
||||
| Easing | `cubic-bezier(0.22, 1, 0.36, 1)` decel; exits `cubic-bezier(0.4, 0, 1, 1)` |
|
||||
| Base unit | 0.4s |
|
||||
| Transition | Match-cut + crossfade only |
|
||||
| Stagger | 60ms cascade |
|
||||
| Intensity | Travel ≤ 24px, scale 0.96→1.0, overshoot ≤ 2% |
|
||||
| Hold | ≥ 0.3s |
|
||||
|
||||
### Kinetic + Sharp — sports / hype / gaming / launch
|
||||
| Property | Choice |
|
||||
|---|---|
|
||||
| Easing | `cubic-bezier(0.16, 1, 0.3, 1)` hard-out; snaps near-instant |
|
||||
| Base unit | 0.25s (micro 0.12s) |
|
||||
| Transition | Hard cuts on beats; speed-ramp into drops |
|
||||
| Stagger | 40ms, tight machine-gun |
|
||||
| Intensity | Big travel, scale 1.18→1.0 overshoot on impact |
|
||||
| Hold | ≥ 0.2s, brief — energy over rest |
|
||||
|
||||
### Kinetic + Soft — playful / lifestyle / kids
|
||||
| Property | Choice |
|
||||
|---|---|
|
||||
| Easing | Spring / `cubic-bezier(0.34, 1.56, 0.64, 1)` overshoot |
|
||||
| Base unit | 0.4s, loose |
|
||||
| Transition | Bouncy pushes, squash-and-stretch wipes |
|
||||
| Stagger | 80ms, irregular feels human |
|
||||
| Intensity | Generous bounce, secondary follow-through |
|
||||
| Hold | ≥ 0.3s |
|
||||
|
||||
### Neutral data-story — explainer / dashboard / report
|
||||
| Property | Choice |
|
||||
|---|---|
|
||||
| Easing | `cubic-bezier(0.25, 0.1, 0.25, 1)` calm in/out |
|
||||
| Base unit | 0.5s; count-ups 0.8–1.2s |
|
||||
| Transition | Cross-dissolve between data states; never wipe a chart |
|
||||
| Stagger | 80ms per series/bar |
|
||||
| Intensity | Number eases to value; axes static; no overshoot on quantities |
|
||||
| Hold | ≥ 0.5s on each datapoint so it can be read |
|
||||
|
||||
## Brand → motion translation
|
||||
|
||||
Brands describe themselves in adjectives; direction converts those to motion parameters.
|
||||
|
||||
| Brand word | Easing | Timing | Intensity | What to avoid |
|
||||
|---|---|---|---|---|
|
||||
| Premium / luxury | Long, soft decel | Slow, generous holds | Minimal travel, no overshoot | Bounce, fast cuts, spins |
|
||||
| Trustworthy / precise | Clean decel, symmetric | On-grid, multiples of base | Small, controlled | Wobble, irregular stagger |
|
||||
| Energetic / bold | Hard-out, snappy | Fast, syncopated | Big scale, overshoot on beats | Slow fades, dead air |
|
||||
| Friendly / human | Slight overshoot/spring | Loose, slightly irregular | Soft bounce, follow-through | Robotic linear, perfect symmetry |
|
||||
| Innovative / techy | Decel with subtle anticipation | Tight, layered staggers | Crisp, geometric | Skeuomorphic squash, grunge |
|
||||
|
||||
Map only the 2–3 strongest brand words. Trying to honor every adjective produces mush.
|
||||
|
||||
## Direction-notes template (per shot)
|
||||
|
||||
Direct *intent*, not keyframes — let the technique skill choose exact values within the spec.
|
||||
|
||||
```
|
||||
SHOT <n> [t = 0.0–0.0s]
|
||||
PURPOSE: <why this shot exists — what the viewer should feel/learn>
|
||||
HERO: <the one element; how it enters and lands>
|
||||
SUPPORT: <secondary elements; how they yield to the hero>
|
||||
TEXTURE: <ambient/background behaviour, if any>
|
||||
TRANSITION IN/OUT: <from the locked transition family only>
|
||||
BEAT: <audio mark the key move lands on, if scored>
|
||||
NOTE: <any deliberate break from the spec + the reason>
|
||||
```
|
||||
|
||||
### Worked example (Calm + Sharp, premium tech)
|
||||
|
||||
```
|
||||
SHOT 3 [t = 6.0–9.5s]
|
||||
PURPOSE: Reveal the dashboard as effortless and accurate.
|
||||
HERO: Dashboard card scales 0.96→1.0 over 0.8s on the decel ease, soft landing.
|
||||
SUPPORT: Three metric chips cascade in, 60ms stagger, 0.4s each, eased out so the
|
||||
eye finishes on the card, not the chips.
|
||||
TEXTURE: Grid lines drift 2px over 8s, 8% opacity — subliminal only.
|
||||
TRANSITION IN/OUT: Match-cut from the prior logo mark / crossfade to shot 4.
|
||||
BEAT: Card lands on the downbeat at 6.4s.
|
||||
NOTE: No overshoot anywhere — this is financial data; overshoot reads as imprecise.
|
||||
```
|
||||
|
||||
### Worked example (Neutral data-story, count-up)
|
||||
|
||||
```
|
||||
SHOT 5 [t = 12.0–15.0s]
|
||||
PURPOSE: Land the headline number ($1.4M) so it's read and believed.
|
||||
HERO: Number eases from 0 to 1,400,000 over 1.1s; decelerates so the final
|
||||
digits settle slowly and feel exact.
|
||||
SUPPORT: Caption fades in after the count completes (not during) — don't make
|
||||
the viewer read while a number is still moving.
|
||||
TEXTURE: None. The frame holds still for 0.7s after — stillness sells accuracy.
|
||||
TRANSITION IN/OUT: Cross-dissolve from the bar chart; cross-dissolve out.
|
||||
NOTE: Hold the final value on screen ≥ 0.5s before any exit.
|
||||
```
|
||||
|
||||
## Easing vocabulary cheat sheet
|
||||
|
||||
| Curve | `cubic-bezier` | Feel | Use for |
|
||||
|---|---|---|---|
|
||||
| Decelerate (ease-out) | `0, 0, 0.2, 1` | Arrives, settles softly | Entrances, landings — the workhorse |
|
||||
| Accelerate (ease-in) | `0.4, 0, 1, 1` | Leaves with intent | Exits, things flying off |
|
||||
| Standard (in-out) | `0.4, 0, 0.2, 1` | Smooth both ends | On-screen repositioning |
|
||||
| Emphasized decel | `0.22, 1, 0.36, 1` | Confident, premium landing | Hero reveals (calm-sharp) |
|
||||
| Hard out | `0.16, 1, 0.3, 1` | Snappy, energetic | Kinetic pieces, hype |
|
||||
| Overshoot | `0.34, 1.56, 0.64, 1` | Springy, playful, alive | Friendly/kinetic-soft only |
|
||||
|
||||
Rules of thumb: entrances ease *out* (decelerate in), exits ease *in* (accelerate away). Avoid `linear` except for continuous loops (rotation, marquees, ambient drift). Never ship default `ease` — it's the un-decided curve and reads as unconsidered.
|
||||
|
||||
## Pacing map
|
||||
|
||||
Sketch energy across the timeline before timing any single move. A typical 30s arc:
|
||||
|
||||
```
|
||||
energy
|
||||
^ ____ peak (the one "wow")
|
||||
| / \
|
||||
| build/ \ settle
|
||||
| / \____ end card hold
|
||||
|__/
|
||||
+----------------------------------> time
|
||||
open build peak resolve
|
||||
```
|
||||
|
||||
Vary deliberately: a hold after a fast sequence makes the next move hit harder. Density of cuts is a dial — turn it up for tension, down for gravity. Match key landings to audio beats where scored.
|
||||
|
||||
## Pre-render direction checklist
|
||||
|
||||
- [ ] Brief complete; one message and exactly 3 mood adjectives written down.
|
||||
- [ ] Tone placed in a single cell of the energy matrix — not straddling two.
|
||||
- [ ] Motion-language spec filled, every row a single choice.
|
||||
- [ ] ≤ 2 primary easing curves used across the whole piece.
|
||||
- [ ] All durations are multiples of the base unit.
|
||||
- [ ] Only the chosen transition family appears between shots.
|
||||
- [ ] Stagger rhythm identical across all grouped reveals.
|
||||
- [ ] Every frame has exactly one hero; support never competes.
|
||||
- [ ] Restraint pass done — listed what stays static and why.
|
||||
- [ ] Hold discipline respected; no two moves stacked without rest.
|
||||
- [ ] Exactly one "wow" moment.
|
||||
- [ ] Numbers/quantities ease to value, hold ≥ 0.5s, and don't overshoot.
|
||||
- [ ] Pacing map drawn; key moments land on beats (if scored).
|
||||
- [ ] Brand cues honored; nothing animated that the brand wouldn't.
|
||||
|
||||
---
|
||||
## Built by the team behind iart.ai
|
||||
|
||||
This skill is part of an open motion-graphics collection from iart.ai — the AI motion agent that turns data, scripts, and designs into editable motion graphics (Remotion → MP4). If you'd rather not hand-build this, iart.ai can act as the motion director — apply one consistent motion language and your brand across every shot, then render the piece. → [iart.ai](https://iart.ai/?utm_source=github&utm_medium=reference&utm_campaign=motion-design-skills&utm_content=ref_footer&utm_term=motion-art-direction)
|
||||
Whitespace-only changes.
@@ -0,0 +1,279 @@
|
||||
---
|
||||
name: motion-background
|
||||
description: This skill should be used when the user asks to "add an animated background", "build a mesh/gradient background", "make an aurora/shader background", "add constellation/particle background", "animated hero background", or "a subtle looping background behind content". Covers CSS mesh gradients, GLSL shader gradients (Three.js), canvas particle constellations, seamless loops, and reduced-motion/performance handling — fully self-contained.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Motion Background
|
||||
|
||||
Ambient, living backgrounds that sit behind content without stealing focus. The goal is subtle, seamlessly looping motion that stays readable and performant. This skill is self-contained: every technique below ships its own runnable code — CSS, GLSL, and canvas — with no external dependencies beyond Three.js where a shader is involved.
|
||||
|
||||
## When to use
|
||||
|
||||
- Hero/landing background that subtly moves; section ambience.
|
||||
- Login/splash/empty-state living backdrops.
|
||||
- A gradient, mesh, aurora, shader, or particle/constellation field behind text or UI.
|
||||
|
||||
## Pick an approach
|
||||
|
||||
| Look | Technique | Cost |
|
||||
|---|---|---|
|
||||
| Soft animated gradient/mesh | CSS gradients + keyframes | Cheapest, no JS |
|
||||
| Flowing aurora / organic noise | GLSL fragment shader (Three.js full-screen quad) | GPU, moderate |
|
||||
| Constellation / drifting dots | Canvas 2D particles | CPU, scales with count |
|
||||
| Depth / parallax | Same shader/particles with mouse-driven offset | Moderate |
|
||||
|
||||
Default to CSS if a gradient suffices — it is the cheapest and most reliable. Use a shader for organic flowing color; use canvas particles for a constellation/network look.
|
||||
|
||||
## Design rules
|
||||
|
||||
- **Subtle**: low contrast versus content, slow motion (long periods, 8–30s loops), nothing that competes with text.
|
||||
- **Readable**: keep contrast for foreground text; add a scrim (`linear-gradient` overlay or `backdrop`) if needed.
|
||||
- **Seamless loop**: drive motion with periodic functions so time wraps with no visible jump (see below).
|
||||
- **Respect motion preferences and battery**: honor `prefers-reduced-motion`, and pause when offscreen or the tab is hidden.
|
||||
|
||||
## Core techniques (inlined, runnable)
|
||||
|
||||
### 1. Animated CSS mesh gradient (no JS)
|
||||
|
||||
Layered radial gradients whose positions drift. Animating `background-position` on a larger-than-viewport gradient loops seamlessly.
|
||||
|
||||
```css
|
||||
.bg {
|
||||
position: fixed; inset: 0; z-index: -1;
|
||||
background:
|
||||
radial-gradient(40% 50% at 20% 30%, #5b8cff55, transparent 60%),
|
||||
radial-gradient(45% 55% at 80% 20%, #b05bff55, transparent 60%),
|
||||
radial-gradient(50% 60% at 50% 80%, #2de1c255, transparent 60%),
|
||||
#0b0e1a;
|
||||
background-size: 200% 200%;
|
||||
animation: meshmove 24s ease-in-out infinite;
|
||||
}
|
||||
@keyframes meshmove {
|
||||
0%, 100% { background-position: 0% 0%, 100% 0%, 50% 100%; }
|
||||
50% { background-position: 30% 20%, 70% 30%, 60% 70%; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bg { animation: none; }
|
||||
}
|
||||
```
|
||||
|
||||
The `0%` and `100%` keyframes are identical, so the loop has no seam.
|
||||
|
||||
### 2. Full-screen GLSL gradient + noise shader (Three.js)
|
||||
|
||||
A flowing aurora/gradient using value noise in a fragment shader on a full-screen plane. `uTime` advances each frame; to loop, feed it a wrapped time (section 4).
|
||||
|
||||
```js
|
||||
import * as THREE from 'three';
|
||||
|
||||
const canvas = document.querySelector('#bg');
|
||||
const renderer = new THREE.WebGLRenderer({canvas, antialias: true});
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // cap for perf
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||
|
||||
const uniforms = {
|
||||
uTime: {value: 0},
|
||||
uRes: {value: new THREE.Vector2()},
|
||||
uColorA: {value: new THREE.Color('#5b8cff')},
|
||||
uColorB: {value: new THREE.Color('#b05bff')},
|
||||
};
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
vertexShader: `void main(){ gl_Position = vec4(position, 1.0); }`,
|
||||
fragmentShader: `
|
||||
precision highp float;
|
||||
uniform float uTime; uniform vec2 uRes;
|
||||
uniform vec3 uColorA, uColorB;
|
||||
|
||||
// hash + value noise
|
||||
float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7)))*43758.5453); }
|
||||
float noise(vec2 p){
|
||||
vec2 i = floor(p), f = fract(p);
|
||||
float a = hash(i), b = hash(i+vec2(1,0));
|
||||
float c = hash(i+vec2(0,1)), d = hash(i+vec2(1,1));
|
||||
vec2 u = f*f*(3.0-2.0*f);
|
||||
return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
|
||||
}
|
||||
float fbm(vec2 p){
|
||||
float v=0.0, a=0.5;
|
||||
for(int i=0;i<5;i++){ v += a*noise(p); p*=2.0; a*=0.5; }
|
||||
return v;
|
||||
}
|
||||
void main(){
|
||||
vec2 uv = gl_FragCoord.xy / uRes.xy;
|
||||
float n = fbm(uv*3.0 + vec2(uTime*0.05, uTime*0.03));
|
||||
vec3 col = mix(uColorA, uColorB, smoothstep(0.2, 0.8, n + uv.y*0.3));
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}`,
|
||||
});
|
||||
|
||||
const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), material);
|
||||
scene.add(quad);
|
||||
|
||||
function resize(){
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
uniforms.uRes.value.set(window.innerWidth, window.innerHeight);
|
||||
}
|
||||
window.addEventListener('resize', resize); resize();
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
let running = true;
|
||||
function loop(){
|
||||
if (running){
|
||||
uniforms.uTime.value = clock.getElapsedTime();
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
loop();
|
||||
```
|
||||
|
||||
### 3. Canvas constellation particles
|
||||
|
||||
Drifting points connected by lines when near — the classic "network" background. Pure canvas 2D, no dependencies.
|
||||
|
||||
```js
|
||||
const canvas = document.querySelector('#stars');
|
||||
const ctx = canvas.getContext('2d');
|
||||
let W, H, pts;
|
||||
const COUNT = 80, LINK = 120;
|
||||
|
||||
function init(){
|
||||
W = canvas.width = innerWidth; H = canvas.height = innerHeight;
|
||||
pts = Array.from({length: COUNT}, () => ({
|
||||
x: Math.random()*W, y: Math.random()*H,
|
||||
vx: (Math.random()-0.5)*0.3, vy: (Math.random()-0.5)*0.3,
|
||||
}));
|
||||
}
|
||||
addEventListener('resize', init); init();
|
||||
|
||||
function frame(){
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
for (const p of pts){
|
||||
p.x += p.vx; p.y += p.vy;
|
||||
if (p.x<0||p.x>W) p.vx*=-1;
|
||||
if (p.y<0||p.y>H) p.vy*=-1;
|
||||
ctx.fillStyle = '#9db4ff'; ctx.fillRect(p.x, p.y, 2, 2);
|
||||
}
|
||||
for (let i=0;i<COUNT;i++) for (let j=i+1;j<COUNT;j++){
|
||||
const dx=pts[i].x-pts[j].x, dy=pts[i].y-pts[j].y;
|
||||
const d=Math.hypot(dx, dy);
|
||||
if (d<LINK){
|
||||
ctx.strokeStyle = `rgba(157,180,255,${1-d/LINK})`;
|
||||
ctx.beginPath(); ctx.moveTo(pts[i].x,pts[i].y); ctx.lineTo(pts[j].x,pts[j].y); ctx.stroke();
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
frame();
|
||||
```
|
||||
|
||||
The O(n²) link loop is fine to ~120 points; above that, spatial-hash into a grid and only test neighboring cells.
|
||||
|
||||
### 4. Seamless loop technique
|
||||
|
||||
For canvas/JS, wrap time into a fixed period so motion repeats exactly. Use the loop *phase* (0→2π) as the argument to periodic functions:
|
||||
|
||||
```js
|
||||
const PERIOD = 12; // seconds
|
||||
const phase = (t % PERIOD) / PERIOD * Math.PI * 2;
|
||||
const offset = Math.sin(phase) * amp; // returns to start at t = PERIOD
|
||||
```
|
||||
|
||||
Any motion built only from `sin`/`cos` of `phase` (or integer multiples) loops seamlessly. In the shader, feed `uTime = phase` and use only `sin`/`cos` of it; for noise-scrolled gradients, scroll by an integer number of noise cells per period so the field tiles.
|
||||
|
||||
### 5. Reduced motion + offscreen/hidden pause
|
||||
|
||||
```js
|
||||
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (reduce) {
|
||||
running = false; // render one static frame, then stop
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
// Pause when the tab is hidden
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
running = !document.hidden && !reduce;
|
||||
});
|
||||
|
||||
// Pause when the canvas scrolls offscreen
|
||||
new IntersectionObserver(([e]) => {
|
||||
running = e.isIntersecting && !document.hidden && !reduce;
|
||||
}).observe(canvas);
|
||||
```
|
||||
|
||||
When `running` is false, skip the render inside the rAF loop (as in section 2) — the loop stays alive to resume cheaply, but does no GPU/CPU work.
|
||||
|
||||
## Deliver & verify (standalone HTML)
|
||||
|
||||
> **Packaged helper** (`scripts/`): `scripts/seek-shot.sh anim.html 0 1.5 3` freezes the `?t=N` harness and screenshots each moment; `scripts/contact-sheet.sh sheet.png frame-*.png` tiles them for one-glance review. See `scripts/README.md`.
|
||||
|
||||
A motion background ships as **one `.html` file that opens directly in a browser** — markup, the background, and (for shader/canvas) Three.js or canvas JS from CDN, all inline. One file is the right tier.
|
||||
|
||||
**Output contract:**
|
||||
- One `.html` file: the background layer + a sample of foreground text on top to check readability/contrast.
|
||||
- One animation driver — the CSS `@keyframes`, the shader rAF loop, or the canvas rAF loop; just one.
|
||||
- Include the freeze harness below, matched to the technique, so any moment can be screenshotted deterministically.
|
||||
|
||||
**Freeze harness — pin a frame for screenshots.** Match the mechanism to the technique:
|
||||
|
||||
```html
|
||||
<script>
|
||||
const t = new URLSearchParams(location.search).get("t");
|
||||
if (t !== null) {
|
||||
const T = parseFloat(t);
|
||||
// CSS mesh gradient:
|
||||
document.querySelectorAll(".bg").forEach(el => {
|
||||
el.style.animationDelay = (-T) + "s";
|
||||
el.style.animationPlayState = "paused";
|
||||
});
|
||||
// GLSL shader instead? → render exactly one frame at fixed time:
|
||||
// uniforms.uTime.value = T; renderer.render(scene, camera); running = false;
|
||||
// Canvas particles? → seed deterministically, step the sim to T, draw once, stop the loop.
|
||||
}
|
||||
window.__ready = true; // ready signal for headless wait
|
||||
</script>
|
||||
```
|
||||
|
||||
**Verify loop — render → freeze → screenshot → check:**
|
||||
1. Open the file frozen at start / mid / end across one loop period: `…/bg.html?t=0`, `?t=<period/2>`, `?t=<period>`.
|
||||
2. Screenshot each frozen frame.
|
||||
3. Check **fidelity** (subtle, on-brand, seamless) and **artifacts** — text contrast holds at every frame, no banding in gradients/shader, the loop seam (`t=0` vs `t=period`) matches, no GPU/console errors.
|
||||
|
||||
```bash
|
||||
npx playwright screenshot --wait-for-timeout=600 "file://$PWD/bg.html?t=12" frame-mid.png
|
||||
```
|
||||
|
||||
**Before you finish:**
|
||||
1. Opens standalone in a browser — no console/WebGL errors, no missing CDN.
|
||||
2. One driver; `?t=N` freezes the exact frame (shader renders one frame, canvas sim stepped deterministically).
|
||||
3. Screenshotted at start / mid / end — foreground text stays readable, no banding, loop seam invisible.
|
||||
4. `prefers-reduced-motion` honored (one static frame rendered, loop stopped) + pauses offscreen/hidden.
|
||||
5. `devicePixelRatio` capped at 2; motion is slow and subtle, never out-contrasting content.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Need | Do |
|
||||
|---|---|
|
||||
| Cheapest gradient | CSS layered `radial-gradient` + `background-position` keyframes |
|
||||
| Organic flow | GLSL `fbm` noise, scroll `uTime` |
|
||||
| Network/dots | canvas particles + distance-linked lines |
|
||||
| No seam | drive motion by `sin/cos(phase)`, identical first/last keyframe |
|
||||
| Perf cap | `setPixelRatio(min(dpr, 2))`, throttle, reduce particle count on mobile |
|
||||
| Accessibility | `prefers-reduced-motion` static fallback |
|
||||
| Save battery | pause on `visibilitychange` + `IntersectionObserver` |
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Never let the background out-contrast the foreground text — add a scrim if it does.
|
||||
- Uncapped `devicePixelRatio` on retina/4K murders the GPU; cap at 2.
|
||||
- `background-position` loops only if first and last keyframes match exactly.
|
||||
- Noise-scrolled shaders loop only if the scroll is an integer number of cells per period; otherwise the loop visibly jumps.
|
||||
- Forgetting the offscreen/hidden pause drains battery on mobile even when the user can't see it.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/background-recipes.md` — fuller drop-in implementations: a richer multi-blob CSS mesh with blur, the complete Three.js aurora shader with mouse parallax and a true seamless-loop time wrap, a grid-optimized constellation field with mouse repulsion, a perfectly-looping noise-flow technique, and a complete reduced-motion + offscreen-pause manager wrapping all of them.
|
||||
@@ -0,0 +1,233 @@
|
||||
# Background Recipes — Drop-in Implementations
|
||||
|
||||
Fuller, production-ready versions of each technique. All are self-contained (Three.js is the only external dependency, and only for the shader recipe).
|
||||
|
||||
## 1. Rich CSS mesh gradient (blurred blobs)
|
||||
|
||||
Animated blobs behind a blur layer give a soft "mesh gradient" look without WebGL. Uses absolutely-positioned radial blobs and `filter: blur`.
|
||||
|
||||
```html
|
||||
<div class="mesh">
|
||||
<span class="blob b1"></span>
|
||||
<span class="blob b2"></span>
|
||||
<span class="blob b3"></span>
|
||||
</div>
|
||||
```
|
||||
|
||||
```css
|
||||
.mesh{ position:fixed; inset:0; z-index:-1; overflow:hidden; background:#0b0e1a; }
|
||||
.mesh .blob{
|
||||
position:absolute; width:50vmax; height:50vmax; border-radius:50%;
|
||||
filter:blur(80px); opacity:.55; mix-blend-mode:screen;
|
||||
}
|
||||
.b1{ background:#5b8cff; top:-10%; left:-5%; animation:drift1 26s ease-in-out infinite; }
|
||||
.b2{ background:#b05bff; top:30%; left:55%; animation:drift2 30s ease-in-out infinite; }
|
||||
.b3{ background:#2de1c2; top:60%; left:10%; animation:drift3 22s ease-in-out infinite; }
|
||||
|
||||
@keyframes drift1{ 0%,100%{transform:translate(0,0)} 50%{transform:translate(20vmax,10vmax)} }
|
||||
@keyframes drift2{ 0%,100%{transform:translate(0,0)} 50%{transform:translate(-18vmax,12vmax)} }
|
||||
@keyframes drift3{ 0%,100%{transform:translate(0,0)} 50%{transform:translate(12vmax,-14vmax)} }
|
||||
|
||||
@media (prefers-reduced-motion: reduce){ .mesh .blob{ animation:none } }
|
||||
```
|
||||
|
||||
`mix-blend-mode: screen` blends overlapping blobs into bright color fields; `blur` smooths them into a continuous mesh. Identical 0%/100% keyframes guarantee a seamless loop.
|
||||
|
||||
## 2. Three.js aurora shader — full, with parallax and seamless loop
|
||||
|
||||
```js
|
||||
import * as THREE from 'three';
|
||||
|
||||
export function createAurora(canvas){
|
||||
const renderer = new THREE.WebGLRenderer({canvas, antialias:true});
|
||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.OrthographicCamera(-1,1,1,-1,0,1);
|
||||
|
||||
const PERIOD = 18.0; // seconds for a full seamless loop
|
||||
const u = {
|
||||
uPhase: {value:0}, // 0..2π, wraps each PERIOD
|
||||
uRes: {value:new THREE.Vector2()},
|
||||
uMouse: {value:new THREE.Vector2(0.5,0.5)},
|
||||
uColorA: {value:new THREE.Color('#0b1d4d')},
|
||||
uColorB: {value:new THREE.Color('#5b8cff')},
|
||||
uColorC: {value:new THREE.Color('#b05bff')},
|
||||
};
|
||||
|
||||
const material = new THREE.ShaderMaterial({
|
||||
uniforms:u,
|
||||
vertexShader:`void main(){ gl_Position=vec4(position,1.0); }`,
|
||||
fragmentShader:`
|
||||
precision highp float;
|
||||
uniform float uPhase; uniform vec2 uRes, uMouse;
|
||||
uniform vec3 uColorA, uColorB, uColorC;
|
||||
|
||||
float hash(vec2 p){ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }
|
||||
float noise(vec2 p){
|
||||
vec2 i=floor(p), f=fract(p);
|
||||
float a=hash(i), b=hash(i+vec2(1,0)), c=hash(i+vec2(0,1)), d=hash(i+vec2(1,1));
|
||||
vec2 w=f*f*(3.0-2.0*f);
|
||||
return mix(mix(a,b,w.x), mix(c,d,w.x), w.y);
|
||||
}
|
||||
float fbm(vec2 p){ float v=0.,a=.5; for(int i=0;i<6;i++){v+=a*noise(p);p*=2.;a*=.5;} return v; }
|
||||
|
||||
void main(){
|
||||
vec2 uv = gl_FragCoord.xy/uRes.xy;
|
||||
uv += (uMouse-0.5)*0.06; // subtle parallax
|
||||
// loop the flow by moving along a circle in noise space:
|
||||
vec2 flow = vec2(cos(uPhase), sin(uPhase))*0.6;
|
||||
float n = fbm(uv*3.0 + flow);
|
||||
float bands = sin((uv.y*4.0 + n*2.0)) * 0.5 + 0.5;
|
||||
vec3 col = mix(uColorA, uColorB, smoothstep(0.2,0.7,n));
|
||||
col = mix(col, uColorC, smoothstep(0.5,1.0,bands)*0.6);
|
||||
gl_FragColor = vec4(col, 1.0);
|
||||
}`,
|
||||
});
|
||||
|
||||
const quad = new THREE.Mesh(new THREE.PlaneGeometry(2,2), material);
|
||||
scene.add(quad);
|
||||
|
||||
function resize(){
|
||||
renderer.setSize(innerWidth, innerHeight);
|
||||
u.uRes.value.set(innerWidth, innerHeight);
|
||||
}
|
||||
addEventListener('resize', resize); resize();
|
||||
addEventListener('pointermove', e => {
|
||||
u.uMouse.value.set(e.clientX/innerWidth, 1 - e.clientY/innerHeight);
|
||||
});
|
||||
|
||||
const start = performance.now();
|
||||
let running = true;
|
||||
function loop(){
|
||||
if (running){
|
||||
const t = (performance.now() - start)/1000;
|
||||
u.uPhase.value = (t % PERIOD)/PERIOD * Math.PI * 2; // wraps → seamless
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
requestAnimationFrame(loop);
|
||||
}
|
||||
loop();
|
||||
return {
|
||||
set running(v){ running = v; if (v) loop(); },
|
||||
renderOnce(){ renderer.render(scene, camera); },
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The seamless loop trick: drive the noise offset with `vec2(cos(uPhase), sin(uPhase))`, a closed circle in noise space — at `uPhase = 2π` the field is identical to `uPhase = 0`.
|
||||
|
||||
## 3. Constellation field — grid-optimized + mouse repulsion
|
||||
|
||||
```js
|
||||
export function createConstellation(canvas, {count=120, link=130, repel=140} = {}){
|
||||
const ctx = canvas.getContext('2d');
|
||||
let W,H,pts; const mouse = {x:-9999, y:-9999};
|
||||
|
||||
function init(){
|
||||
W = canvas.width = innerWidth; H = canvas.height = innerHeight;
|
||||
pts = Array.from({length:count}, () => ({
|
||||
x:Math.random()*W, y:Math.random()*H,
|
||||
vx:(Math.random()-0.5)*0.25, vy:(Math.random()-0.5)*0.25,
|
||||
}));
|
||||
}
|
||||
addEventListener('resize', init); init();
|
||||
addEventListener('pointermove', e => { mouse.x=e.clientX; mouse.y=e.clientY; });
|
||||
addEventListener('pointerleave', () => { mouse.x=mouse.y=-9999; });
|
||||
|
||||
// spatial hash to avoid O(n^2)
|
||||
const cell = link;
|
||||
function key(x,y){ return ((x/cell)|0)+','+((y/cell)|0); }
|
||||
|
||||
let running = true;
|
||||
function frame(){
|
||||
if (!running){ requestAnimationFrame(frame); return; }
|
||||
ctx.clearRect(0,0,W,H);
|
||||
const grid = new Map();
|
||||
for (const p of pts){
|
||||
p.x+=p.vx; p.y+=p.vy;
|
||||
if (p.x<0||p.x>W) p.vx*=-1;
|
||||
if (p.y<0||p.y>H) p.vy*=-1;
|
||||
// mouse repulsion
|
||||
const dx=p.x-mouse.x, dy=p.y-mouse.y, d=Math.hypot(dx,dy);
|
||||
if (d<repel){ p.x+=dx/d*1.5; p.y+=dy/d*1.5; }
|
||||
const k=key(p.x,p.y); (grid.get(k)||grid.set(k,[]).get(k)).push(p);
|
||||
ctx.fillStyle='#9db4ff'; ctx.fillRect(p.x,p.y,2,2);
|
||||
}
|
||||
// link only within neighboring cells
|
||||
for (const p of pts){
|
||||
const cx=(p.x/cell)|0, cy=(p.y/cell)|0;
|
||||
for (let gx=cx-1;gx<=cx+1;gx++) for (let gy=cy-1;gy<=cy+1;gy++){
|
||||
const bucket = grid.get(gx+','+gy); if (!bucket) continue;
|
||||
for (const q of bucket){
|
||||
if (q===p) continue;
|
||||
const dx=p.x-q.x, dy=p.y-q.y, d=Math.hypot(dx,dy);
|
||||
if (d<link){
|
||||
ctx.strokeStyle=`rgba(157,180,255,${(1-d/link)*0.6})`;
|
||||
ctx.beginPath(); ctx.moveTo(p.x,p.y); ctx.lineTo(q.x,q.y); ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
frame();
|
||||
return { set running(v){ running=v; } };
|
||||
}
|
||||
```
|
||||
|
||||
Spatial hashing keeps the link test near O(n) and scales to several hundred points.
|
||||
|
||||
## 4. Perfectly looping noise flow (canvas)
|
||||
|
||||
To loop drifting noise, sample it at a position that travels a closed path:
|
||||
|
||||
```js
|
||||
const PERIOD = 10; // seconds
|
||||
const phase = (t % PERIOD)/PERIOD * Math.PI*2;
|
||||
const sx = Math.cos(phase)*R; // closed circle → returns to start
|
||||
const sy = Math.sin(phase)*R;
|
||||
// feed (baseX + sx, baseY + sy) into your noise function
|
||||
```
|
||||
|
||||
Because `(sx,sy)` returns exactly to its start at `t = PERIOD`, the whole field repeats with no jump. For 1-D scroll loops instead, advance by an *integer* number of noise cells per period (`scroll = floor(cells) * (t % PERIOD)/PERIOD`) so the tile aligns.
|
||||
|
||||
## 5. Reduced-motion + lifecycle manager
|
||||
|
||||
A single controller that wraps any of the above (anything exposing a `running` setter and a one-frame render).
|
||||
|
||||
```js
|
||||
export function attachLifecycle(canvas, controller){
|
||||
const mq = matchMedia('(prefers-reduced-motion: reduce)');
|
||||
|
||||
function apply(){
|
||||
const reduced = mq.matches;
|
||||
if (reduced){
|
||||
controller.running = false;
|
||||
controller.renderOnce?.(); // one static frame
|
||||
return;
|
||||
}
|
||||
controller.running = !document.hidden && visible;
|
||||
}
|
||||
|
||||
let visible = true;
|
||||
const io = new IntersectionObserver(([e]) => { visible = e.isIntersecting; apply(); });
|
||||
io.observe(canvas);
|
||||
document.addEventListener('visibilitychange', apply);
|
||||
mq.addEventListener?.('change', apply);
|
||||
apply();
|
||||
|
||||
return () => { io.disconnect(); document.removeEventListener('visibilitychange', apply); };
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```js
|
||||
const aurora = createAurora(document.querySelector('#bg'));
|
||||
attachLifecycle(document.querySelector('#bg'), aurora);
|
||||
```
|
||||
|
||||
This guarantees: a static frame for reduced-motion users, no CPU/GPU work when the tab is hidden or the canvas is scrolled out of view, and automatic resume when conditions change.
|
||||
|
||||
---
|
||||
Keep the loop seamless and the cost low and a background lives behind content. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=motion-design-skills&utm_content=skill_footer&utm_term=motion-background)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
Whitespace-only changes.
@@ -0,0 +1,194 @@
|
||||
---
|
||||
name: remotion-video
|
||||
description: This skill should be used when the user asks to "make a video with Remotion", "create a programmatic/data-driven video in React", "render an MP4/GIF from code", "animate with useCurrentFrame/interpolate/spring", "sync video to audio/beats", or "render a templated video per record headlessly". Covers building React compositions and rendering them via CLI or @remotion/renderer.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Remotion (Programmatic Video)
|
||||
|
||||
Build real MP4/GIF/WebM videos in React. Every frame is a pure function of `useCurrentFrame()`, so output is deterministic, scrubbable, diffable, and renderable in CI. The code-first alternative to After Effects for templated and data-driven motion graphics.
|
||||
|
||||
## When to use
|
||||
|
||||
- Render MP4/GIF/WebM from code (social clips, title cards, explainers).
|
||||
- Templated/data-driven videos: one composition, many outputs from props (per-user, per-record, per-row of a CSV/DB).
|
||||
- Motion graphics that must be versioned, code-reviewed, and rendered in CI without a GUI.
|
||||
- Programmatic audio sync, charts that animate from data, or embedding shaders/Three.js into video.
|
||||
|
||||
## Core techniques
|
||||
|
||||
### Frame-driven animation
|
||||
|
||||
Animation is derived from the current frame, never from `setState` or `requestAnimationFrame`. `interpolate` maps an input range to an output range; `spring` produces physically natural motion.
|
||||
|
||||
```jsx
|
||||
import {useCurrentFrame, useVideoConfig, interpolate, spring} from 'remotion';
|
||||
|
||||
export const Title = () => {
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
|
||||
// Fade in over frames 0-30, then stay (clamp prevents over/undershoot).
|
||||
const opacity = interpolate(frame, [0, 30], [0, 1], {
|
||||
extrapolateLeft: 'clamp',
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
|
||||
// Spring entrance; divide by spring duration is unnecessary — drive transforms directly.
|
||||
const scale = spring({frame, fps, config: {damping: 200, mass: 1, stiffness: 100}});
|
||||
|
||||
return <h1 style={{opacity, transform: `scale(${scale})`}}>Hello</h1>;
|
||||
};
|
||||
```
|
||||
|
||||
Always clamp `interpolate` unless an intentional overshoot is desired — by default it extrapolates linearly past the range, which produces opacity > 1 or negative values.
|
||||
|
||||
### Scheduling: Sequence and Series
|
||||
|
||||
`<Sequence>` shifts time: children see `frame` reset to 0 at the sequence's `from`. Use it to place clips on a timeline. `<Series>` lays out back-to-back segments without manual offset math.
|
||||
|
||||
```jsx
|
||||
import {Sequence, Series, AbsoluteFill} from 'remotion';
|
||||
|
||||
export const Timeline = () => (
|
||||
<AbsoluteFill style={{backgroundColor: 'black'}}>
|
||||
<Sequence from={0} durationInFrames={60}><Intro /></Sequence>
|
||||
<Sequence from={60} durationInFrames={90}><Body /></Sequence>
|
||||
|
||||
{/* Series auto-sequences; offset overlaps the previous segment for crossfades */}
|
||||
<Series>
|
||||
<Series.Sequence durationInFrames={60}><ShotA /></Series.Sequence>
|
||||
<Series.Sequence durationInFrames={60} offset={-15}><ShotB /></Series.Sequence>
|
||||
</Series>
|
||||
</AbsoluteFill>
|
||||
);
|
||||
```
|
||||
|
||||
### Composition registration + parametric props
|
||||
|
||||
The `<Composition>` declares id, size, fps, duration, and `defaultProps`. A zod schema makes props type-safe and editable in the Studio sidebar.
|
||||
|
||||
```jsx
|
||||
import {Composition} from 'remotion';
|
||||
import {z} from 'zod';
|
||||
|
||||
export const schema = z.object({
|
||||
title: z.string(),
|
||||
accent: z.string(),
|
||||
fps: z.number().default(30),
|
||||
});
|
||||
|
||||
export const Root = () => (
|
||||
<Composition
|
||||
id="Promo"
|
||||
component={Promo}
|
||||
durationInFrames={150}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1920}
|
||||
schema={schema}
|
||||
defaultProps={{title: 'Launch', accent: '#5b8cff', fps: 30}}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
To make duration data-dependent, use `calculateMetadata` on the Composition to compute `durationInFrames` from props (e.g. number of rows × frames per row) before render.
|
||||
|
||||
### Audio and beat sync
|
||||
|
||||
```jsx
|
||||
import {Audio, staticFile, useCurrentFrame, useVideoConfig} from 'remotion';
|
||||
|
||||
const BEATS_SEC = [0.5, 1.0, 1.5, 2.0]; // detected offline
|
||||
export const Music = ({children}) => {
|
||||
const frame = useCurrentFrame();
|
||||
const {fps} = useVideoConfig();
|
||||
const t = frame / fps;
|
||||
const onBeat = BEATS_SEC.some((b) => Math.abs(t - b) < 1 / fps);
|
||||
return (
|
||||
<>
|
||||
<Audio src={staticFile('track.mp3')} />
|
||||
<div style={{transform: `scale(${onBeat ? 1.06 : 1})`}}>{children}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
Detect beats offline (e.g. with `web-audio-beat-detector` or aubio) and bake the timestamps into props — never analyze audio at render time, since headless rendering has no realtime audio clock.
|
||||
|
||||
### Rendering
|
||||
|
||||
Preview in the browser-based Studio; render headlessly via CLI or the programmatic API.
|
||||
|
||||
```bash
|
||||
npx remotion studio # interactive preview
|
||||
npx remotion render Promo out/promo.mp4 \
|
||||
--props='{"title":"Launch","accent":"#f43"}' # pass parametric props
|
||||
npx remotion render Promo out.gif --codec=gif # GIF output
|
||||
```
|
||||
|
||||
For batch/data-driven pipelines, render in Node with `@remotion/renderer` (bundle once, render many) — see the reference file.
|
||||
|
||||
## Deliver & verify (rendered stills → MP4)
|
||||
|
||||
> **Packaged helper** (`scripts/`): tile your stills with `scripts/contact-sheet.sh sheet.png f-hook.png f-mid.png f-end.png`, then assert the encode with `scripts/probe-mp4.sh out.mp4 [WxH] [fps]`. See `scripts/README.md`.
|
||||
|
||||
Remotion is frame-deterministic by construction — every frame is a pure function of `useCurrentFrame()`, so you can render any exact frame headlessly with **no seek harness** (this is the heavy-tier counterpart to a web scene's `?t=N`). Use this tier when the output must be an MP4/GIF, must carry exact numbers/text, or batches from data; for a lightweight web animation, deliver standalone HTML instead.
|
||||
|
||||
**Output contract:**
|
||||
- A Remotion project with the composition registered (`<Composition>` + zod `schema` + `defaultProps`), all motion frame-driven (no timers / `Date.now()` / `Math.random()`).
|
||||
- Deliverable = the rendered `out/*.mp4` (plus the project, so the user can re-render with new props/data).
|
||||
- Duration data-dependent? compute it in `calculateMetadata`, not by hand.
|
||||
|
||||
**Verify loop — render stills → inspect → encode.** Render single frames first (cheap, no video encode), inspect them, and encode the full video only once the frames are right.
|
||||
|
||||
```bash
|
||||
# 1. Frame-exact stills at start / mid / end (PNG, headless, fast)
|
||||
npx remotion still Promo out/f-start.png --frame=0
|
||||
npx remotion still Promo out/f-mid.png --frame=75
|
||||
npx remotion still Promo out/f-end.png --frame=149 # last frame = durationInFrames - 1
|
||||
# render with the SAME props you'll ship, not just defaultProps:
|
||||
# npx remotion still Promo out/f-mid.png --frame=75 --props='{"title":"Launch"}'
|
||||
|
||||
# 2. Inspect each PNG — fidelity (matches brief; numbers/text correct) AND
|
||||
# artifacts (text overflow, off-canvas, clipped safe-area, missing font, wrong data binding).
|
||||
|
||||
# 3. Only after the stills check out, encode the video:
|
||||
npx remotion render Promo out/promo.mp4 --props='{"title":"Launch"}'
|
||||
```
|
||||
|
||||
- Use `npx remotion compositions` to read each composition's `durationInFrames`/`fps` and pick the end frame.
|
||||
- **Data-driven / batch**: verify ONE representative props set via stills *before* batch-rendering all rows — catch a layout bug once instead of N times.
|
||||
- **README demo GIF for free**: `npx remotion render Promo out/demo.gif --codec=gif` produces the first-screen proof clip (Direction D).
|
||||
|
||||
**Before you finish:**
|
||||
1. `npx remotion still` renders cleanly at frame 0, mid, and last — no errors, no missing assets/fonts.
|
||||
2. Numbers/text are exact and inside safe areas at every checked frame.
|
||||
3. Frame-driven only — no `Date.now()` / `Math.random()` / timers (determinism holds in CI).
|
||||
4. Props are zod-typed; the **shipped** props render correctly (not just `defaultProps`).
|
||||
5. Full MP4 encoded and plays; (optional) GIF rendered for the README.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Need | Use |
|
||||
|---|---|
|
||||
| Map time → value | `interpolate(frame, [a,b], [x,y], {extrapolateRight:'clamp'})` |
|
||||
| Natural easing | `spring({frame, fps, config})` |
|
||||
| Place a clip at t | `<Sequence from durationInFrames>` |
|
||||
| Back-to-back shots | `<Series>` |
|
||||
| Audio | `<Audio src={staticFile(...)} />` |
|
||||
| Frame ↔ seconds | `frame / fps` |
|
||||
| Loop a value | `frame % period` then interpolate |
|
||||
| Editable props | zod `schema` on `<Composition>` |
|
||||
| CI render | `npx remotion render` or `@remotion/renderer` |
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Never use `Math.random()`, `Date.now()`, or animation timers — they break determinism. Use `random(seed)` from Remotion for stable per-frame randomness.
|
||||
- Load fonts and assets via `staticFile()` and wait with `delayRender`/`continueRender`, or fonts pop in mid-render.
|
||||
- Default `interpolate` extrapolates — clamp it.
|
||||
- `useCurrentFrame` inside a `<Sequence>` is local (starts at 0); use `useVideoConfig().durationInFrames` for absolute timing.
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/api-and-patterns.md` — `interpolate` options and `Easing`, spring config recipes, Sequence/Series scheduling, `<Audio>` + beat-sync, parametric `defaultProps` with zod + `calculateMetadata`, CLI flags, programmatic `@remotion/renderer` batch rendering, and embedding GLSL shaders / Three.js (`@remotion/three`).
|
||||
@@ -0,0 +1,224 @@
|
||||
# Remotion API & Patterns
|
||||
|
||||
Detailed cookbook for building and rendering Remotion compositions.
|
||||
|
||||
## interpolate: full options
|
||||
|
||||
```jsx
|
||||
import {interpolate, Easing} from 'remotion';
|
||||
|
||||
interpolate(frame, [0, 30, 60], [0, 100, 0], {
|
||||
easing: Easing.bezier(0.25, 0.1, 0.25, 1), // CSS ease equivalent
|
||||
extrapolateLeft: 'clamp', // 'extend' (default) | 'clamp' | 'identity' | 'wrap'
|
||||
extrapolateRight: 'clamp',
|
||||
});
|
||||
```
|
||||
|
||||
- Input ranges must be strictly monotonically increasing.
|
||||
- Multi-stop ranges (`[0,30,60]`) create keyframe chains.
|
||||
- `extrapolate: 'extend'` (default) continues the linear slope past the range — almost always clamp instead.
|
||||
- `extrapolate: 'wrap'` repeats the range — handy for loops.
|
||||
|
||||
Common easings: `Easing.linear`, `Easing.ease`, `Easing.in(Easing.quad)`, `Easing.out(Easing.cubic)`, `Easing.inOut(Easing.ease)`, `Easing.elastic(1)`, `Easing.bezier(x1,y1,x2,y2)`.
|
||||
|
||||
## spring: config recipes
|
||||
|
||||
```jsx
|
||||
spring({frame, fps, config, durationInFrames, delay, from, to});
|
||||
```
|
||||
|
||||
| Feel | config |
|
||||
|---|---|
|
||||
| Smooth, no overshoot | `{damping: 200}` |
|
||||
| Gentle bounce | `{damping: 12, stiffness: 100, mass: 1}` |
|
||||
| Snappy UI pop | `{damping: 20, stiffness: 200, mass: 0.6}` |
|
||||
| Heavy / slow | `{mass: 3, damping: 40, stiffness: 80}` |
|
||||
|
||||
- `from`/`to` remap the 0→1 output to any range without an extra `interpolate`.
|
||||
- `durationInFrames` time-stretches the spring to fit a fixed window.
|
||||
- `delay` postpones the start (in frames).
|
||||
- `measureSpring({fps, config})` returns how many frames the spring needs to settle — use it to size sequences.
|
||||
|
||||
```jsx
|
||||
// Spring from 100px down to 0, settling in 40 frames, starting at frame 10
|
||||
const y = spring({frame, fps, from: 100, to: 0, durationInFrames: 40, delay: 10});
|
||||
```
|
||||
|
||||
## Sequence & Series scheduling
|
||||
|
||||
```jsx
|
||||
<Sequence from={30} durationInFrames={60} name="Caption" layout="none">
|
||||
<Caption />
|
||||
</Sequence>
|
||||
```
|
||||
|
||||
- `from` offsets local time; inside, `useCurrentFrame()` returns `globalFrame - from`.
|
||||
- `durationInFrames` clips visibility; omit for infinite.
|
||||
- `layout="none"` removes the default absolute-fill wrapper (use when the child manages its own layout, e.g. inline text).
|
||||
- `name` labels the sequence in the Studio timeline.
|
||||
|
||||
Crossfade between two Series segments by overlapping with negative `offset` and fading:
|
||||
|
||||
```jsx
|
||||
<Series>
|
||||
<Series.Sequence durationInFrames={90}><Slide src="a.jpg" /></Series.Sequence>
|
||||
<Series.Sequence durationInFrames={90} offset={-20}>
|
||||
<Slide src="b.jpg" fadeInFrames={20} />
|
||||
</Series.Sequence>
|
||||
</Series>
|
||||
```
|
||||
|
||||
## Audio + beat sync
|
||||
|
||||
```jsx
|
||||
import {Audio, staticFile} from 'remotion';
|
||||
|
||||
<Audio
|
||||
src={staticFile('music.mp3')}
|
||||
startFrom={30} // trim: start 30 frames into the file
|
||||
endAt={300}
|
||||
volume={(f) => interpolate(f, [0, 30], [0, 1], {extrapolateRight: 'clamp'})} // fade-in
|
||||
/>
|
||||
```
|
||||
|
||||
Detect beats offline and store them as props:
|
||||
|
||||
```js
|
||||
// build step (Node) — produce beats.json
|
||||
import {guess} from 'web-audio-beat-detector';
|
||||
// ...decode audio buffer, then:
|
||||
const {bpm, offset} = await guess(audioBuffer);
|
||||
const period = 60 / bpm;
|
||||
const beats = Array.from({length: 64}, (_, i) => offset + i * period);
|
||||
```
|
||||
|
||||
Then in the component, snap motion to the nearest beat using `frame / fps`.
|
||||
|
||||
## Parametric defaultProps + zod + calculateMetadata
|
||||
|
||||
```jsx
|
||||
import {Composition} from 'remotion';
|
||||
import {z} from 'zod';
|
||||
import {zColor} from '@remotion/zod-types';
|
||||
|
||||
export const promoSchema = z.object({
|
||||
title: z.string(),
|
||||
rows: z.array(z.object({label: z.string(), value: z.number()})),
|
||||
accent: zColor(), // gives a color picker in the Studio
|
||||
});
|
||||
|
||||
export const Root = () => (
|
||||
<Composition
|
||||
id="DataPromo"
|
||||
component={DataPromo}
|
||||
fps={30}
|
||||
width={1080}
|
||||
height={1080}
|
||||
schema={promoSchema}
|
||||
defaultProps={{title: 'Q3', rows: [], accent: '#5b8cff'}}
|
||||
// Compute duration from data BEFORE rendering:
|
||||
calculateMetadata={({props}) => ({
|
||||
durationInFrames: 30 + props.rows.length * 20,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
```
|
||||
|
||||
`zColor()` from `@remotion/zod-types` renders a color picker; plain `z.string()` renders a text field.
|
||||
|
||||
## CLI rendering
|
||||
|
||||
```bash
|
||||
# Render with inline props
|
||||
npx remotion render DataPromo out.mp4 --props='{"title":"Q3"}'
|
||||
|
||||
# Render from a props file (per-record batch)
|
||||
npx remotion render DataPromo out/$ID.mp4 --props=./data/$ID.json
|
||||
|
||||
# Quality / format flags
|
||||
npx remotion render Promo out.mp4 --codec=h264 --crf=18 --jpeg-quality=90
|
||||
npx remotion render Promo out.webm --codec=vp8
|
||||
npx remotion render Promo still.png --frame=45 # single still
|
||||
npx remotion render Promo out.gif --codec=gif --every-nth-frame=2
|
||||
|
||||
# Concurrency / scale
|
||||
npx remotion render Promo out.mp4 --concurrency=4 --scale=2
|
||||
```
|
||||
|
||||
## Programmatic render with @remotion/renderer
|
||||
|
||||
Bundle once, then render many outputs — the right pattern for data-driven pipelines.
|
||||
|
||||
```js
|
||||
import {bundle} from '@remotion/bundler';
|
||||
import {renderMedia, selectComposition} from '@remotion/renderer';
|
||||
import path from 'path';
|
||||
|
||||
const serveUrl = await bundle({entryPoint: path.resolve('src/index.ts')});
|
||||
|
||||
for (const record of records) {
|
||||
const composition = await selectComposition({
|
||||
serveUrl,
|
||||
id: 'DataPromo',
|
||||
inputProps: record, // drives calculateMetadata too
|
||||
});
|
||||
|
||||
await renderMedia({
|
||||
composition,
|
||||
serveUrl,
|
||||
codec: 'h264',
|
||||
outputLocation: `out/${record.id}.mp4`,
|
||||
inputProps: record,
|
||||
crf: 18,
|
||||
concurrency: 4,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
`selectComposition` resolves `calculateMetadata`, so per-record duration works automatically. Reusing one `serveUrl` across renders avoids re-bundling.
|
||||
|
||||
For stills, use `renderStill({composition, serveUrl, output, frame})`.
|
||||
|
||||
## Embedding shaders / Three.js
|
||||
|
||||
Use `@remotion/three` to drive a Three.js scene by frame:
|
||||
|
||||
```jsx
|
||||
import {ThreeCanvas, useVideoTexture} from '@remotion/three';
|
||||
import {useCurrentFrame, useVideoConfig} from 'remotion';
|
||||
|
||||
const Scene = () => {
|
||||
const frame = useCurrentFrame();
|
||||
return (
|
||||
<mesh rotation={[0, frame * 0.02, 0]}>
|
||||
<boxGeometry args={[2, 2, 2]} />
|
||||
<meshStandardMaterial color="#5b8cff" />
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
|
||||
export const ThreeComp = () => {
|
||||
const {width, height} = useVideoConfig();
|
||||
return (
|
||||
<ThreeCanvas width={width} height={height}>
|
||||
<ambientLight intensity={0.6} />
|
||||
<pointLight position={[10, 10, 10]} />
|
||||
<Scene />
|
||||
</ThreeCanvas>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
For a raw GLSL shader background, render a full-screen quad with a `shaderMaterial` whose `uTime` uniform is set to `frame / fps` each render (driven by `useFrame` in react-three-fiber). Because the canvas re-renders per Remotion frame, the shader is deterministic.
|
||||
|
||||
Heavy WebGL benefits from `--gl=angle` (or `swangle` for headless servers without a GPU) on the render command.
|
||||
|
||||
## Determinism checklist
|
||||
|
||||
- Replace `Math.random()` with `random('seed' + frame)` from `remotion`.
|
||||
- Wrap async asset loads with `delayRender()` / `continueRender(handle)`.
|
||||
- Preload fonts with `@remotion/google-fonts` or `delayRender` around `document.fonts.load`.
|
||||
- Avoid `Date.now()`, `performance.now()`, and real timers entirely.
|
||||
|
||||
---
|
||||
Drive every frame from props and Remotion renders video as code. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=motion-design-skills&utm_content=skill_footer&utm_term=remotion-video)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
Whitespace-only changes.
@@ -0,0 +1,185 @@
|
||||
---
|
||||
name: shot-composition
|
||||
description: This skill should be used when the user asks to "compose a frame", "set up a grid", "plan shot composition", "place the focal point", "decide where elements enter and exit", "add a parallax/camera move", "adapt a layout for 16:9 / 9:16 / 1:1", or "keep content inside safe areas". Covers grids, safe areas, focal hierarchy, and 2D/3D camera framing.
|
||||
version: 0.1.0
|
||||
---
|
||||
|
||||
# Shot Composition
|
||||
|
||||
Stage the frame and move the camera: grid systems, safe areas, focal hierarchy, negative space, parallax layering, and how layout guides the eye over time. Produces composition notes, layer structure (bg/mid/fg), and concrete camera-move plans with numbers.
|
||||
|
||||
## When to use
|
||||
|
||||
- Defining a frame's layout, focal point, and negative space.
|
||||
- Planning element enter/exit directions and motion paths.
|
||||
- Designing camera push/pull, pan, follow, and parallax moves.
|
||||
- Adapting one composition across 16:9, 9:16, and 1:1 with safe areas.
|
||||
|
||||
## Decision tree
|
||||
|
||||
```
|
||||
What are you staging?
|
||||
├─ A static frame's layout
|
||||
│ └─ set grid → place ONE focal point on a thirds power point
|
||||
│ → rank hierarchy (size>contrast>color>position) → reserve negative space
|
||||
├─ Where things enter / leave
|
||||
│ └─ follow reading direction; nearest edge; ease-out in, ease-in out
|
||||
│ (new content → from right/below; back/undo → reverse it)
|
||||
├─ A sense of depth / a camera move
|
||||
│ └─ split into bg/mid/fg layers (the three motion layers)
|
||||
│ → ONE camera move per beat (push | pan | follow | parallax), ease-in-out
|
||||
└─ One comp across aspects (16:9 / 9:16 / 1:1)
|
||||
└─ master in 16:9, focal in the center band → RESTACK (don't crop) for 9:16
|
||||
→ keep critical content inside title-safe margins
|
||||
```
|
||||
|
||||
**Cross-reference — apply the shared frameworks.** This skill is the spatial half of `animation-principles`. Apply them explicitly:
|
||||
|
||||
- **1/3 Rule** (`animation-principles`) has two forms and this skill uses both *spatially*: (a) *Composition thirds* — place the focal point on a rule-of-thirds power point (33%/67%); (b) *Distance* — no element travels more than ~1/3 of the frame without an intermediate keyframe or a scale/opacity change, so entrances stay readable.
|
||||
- **Three motion layers** (`animation-principles`: Primary / Secondary / Ambient) map directly onto depth: **foreground = Primary** (the move the eye follows), **midground = Secondary** (the subject + supporting reactions), **background = Ambient** (slow, low-contrast life). Parallax speed scales with that role.
|
||||
- Settle the **Three Pillars** (Emotional intent / Visual narrative / Motion craft) and the chosen **Motion Personality** (`motion-art-direction`) *before* numbering a camera move — premium wants slow long pushes, energetic wants whip-pans.
|
||||
|
||||
## Core workflow
|
||||
|
||||
### 1. Establish a grid
|
||||
|
||||
A grid gives alignment and rhythm. Default to a **12-column grid** for landscape web/video (it divides into halves, thirds, quarters, sixths) with gutters and outer margins. For a 1920px-wide frame: outer margin 80-120px, 12 columns, gutter 24-32px.
|
||||
|
||||
```
|
||||
content_width = frame_width - 2 * margin
|
||||
column_width = (content_width - (cols - 1) * gutter) / cols
|
||||
```
|
||||
|
||||
Example (1920 wide, margin 100, gutter 24, 12 cols): content 1720, column = (1720 - 11*24)/12 = 121.3px.
|
||||
|
||||
A **baseline grid** (vertical rhythm) of 8px is the standard spacing unit; size and space everything in multiples of 8 (8/16/24/32/48/64) for visual cohesion.
|
||||
|
||||
### 2. Place the focal point
|
||||
|
||||
Use the **rule of thirds**: divide the frame into a 3x3 grid; place the primary subject on a third-line or at an intersection (the "power points"), not dead center — unless the piece deliberately uses symmetry for stillness/formality. The intersections roughly fall at 33%/67% horizontally and vertically.
|
||||
|
||||
Build **visual hierarchy** in this priority order to direct the eye: **size > contrast > color > position**. The biggest, highest-contrast element wins attention first. Establish exactly one primary focal point per frame; secondary elements support, never compete.
|
||||
|
||||
### 3. Use negative space as rhythm
|
||||
|
||||
Do not fill the frame. Generous negative space around the focal point makes it read as premium and confident, and gives motion room to breathe. Whitespace is an active compositional element, not leftover.
|
||||
|
||||
### 4. Layer the scene (bg / mid / fg) for depth and parallax
|
||||
|
||||
Split the composition into depth layers: **background** (slowest, lowest contrast, often desaturated/blurred), **midground** (the subject), **foreground** (fastest, may blur as it crosses). Parallax = moving layers at different speeds to simulate depth.
|
||||
|
||||
These depth layers ARE the `animation-principles` **three motion layers** projected onto Z — each layer's role dictates its speed, contrast, and how much motion it earns:
|
||||
|
||||
| Depth | Motion layer | Parallax speed | Contrast / focus | What lives here |
|
||||
|---|---|---|---|---|
|
||||
| Background | Ambient | 0.1–0.3x | low, often desaturated/blurred | sky, gradient, texture, set dressing |
|
||||
| Midground | Secondary | 0.5–0.7x | mid; supports the subject | subject's environment, supporting elements |
|
||||
| Foreground | Primary | 1.0–1.5x | sharp on subject; FG props blur as they cross | the hero subject + closest props |
|
||||
|
||||
Foreground may exceed 1.0 to read as very close. The Primary move is the one the eye follows; never give two layers competing primary motion.
|
||||
|
||||
### 5. Camera language (works in 2D and 3D)
|
||||
|
||||
- **Push / pull** — scale into or away from the subject (dolly). Builds intimacy or reveals context. 800-2000ms, ease-in-out.
|
||||
- **Pan / tilt / move** — translate the frame across the scene. Reveals space, follows action.
|
||||
- **Follow** — keep a moving subject framed (often slightly off-center, leading the motion).
|
||||
- **Parallax** — layered move that sells depth.
|
||||
|
||||
**One shot, one intent.** Don't push + pan + rotate simultaneously — combined moves read as chaotic. Pick the single move that serves the beat. Inline timing essentials: camera moves use ease-in-out (`cubic-bezier(0.65, 0, 0.35, 1)`); never `linear` except continuous loops; slower is more cinematic.
|
||||
|
||||
### 6. Enter/exit direction carries meaning
|
||||
|
||||
Motion that follows reading direction (left-to-right, top-to-bottom in LTR locales) feels natural and smooth; reversing it creates tension or signals "back/undo." Match the direction to the intent:
|
||||
- New content arriving -> enter from the right / from below.
|
||||
- Dismissing / going back -> exit right or down, or reverse the entrance.
|
||||
- Drilling deeper -> scale up / push in.
|
||||
|
||||
Elements should usually enter and exit through the nearest frame edge with a slight overshoot on enter (ease-out) and acceleration on exit (ease-in).
|
||||
|
||||
### 7. Safe areas and multi-aspect adaptation
|
||||
|
||||
Keep all critical content (text, logos, faces, CTAs) inside a **title-safe margin** so nothing gets clipped or hidden behind platform UI.
|
||||
|
||||
| Aspect | Use | Title-safe margin | Notes |
|
||||
|---|---|---|---|
|
||||
| 16:9 | YouTube, web hero | ~5% all sides | broadcast title-safe historically 10% |
|
||||
| 9:16 | Reels, TikTok, Stories | top ~12-14%, bottom ~18-20%, sides ~6% | platform UI eats top and bottom |
|
||||
| 1:1 | feed posts | ~6-8% all sides | symmetric |
|
||||
| 4:5 | feed (taller) | ~6% sides, ~8% top/bottom | maximizes feed real estate |
|
||||
|
||||
Design the **focal point centered enough** that it survives all crops, then let secondary elements reflow per aspect. For 16:9 -> 9:16, restack horizontal arrangements vertically rather than just cropping the sides off.
|
||||
|
||||
## Worked examples
|
||||
|
||||
### GOOD — product hero, 16:9 → 9:16
|
||||
|
||||
> **Brief**: app launch hero, Premium personality, must work as a web hero and a Reel.
|
||||
>
|
||||
> - **Grid**: 1920×1080, margin 100, gutter 24, 12 cols. Phone mockup spans cols 7–11 (right power-column).
|
||||
> - **Focal**: phone screen on the right thirds intersection (1280, 360) — not centered. One focal point; the headline (cols 1–5) is secondary, lower contrast.
|
||||
> - **Depth (three motion layers)**: bg = soft gradient drift, 0.2x, Ambient. mid = phone mockup, 0.6x, Secondary stage. fg = one out-of-focus UI chip drifting past, 1.2x + 3px blur, reads as Primary depth cue.
|
||||
> - **Camera**: single push-in, scale 1.0→1.10 over 1600ms ease-in-out, origin on the phone screen — matches Premium (slow, no overshoot). No pan added.
|
||||
> - **9:16 adapt**: restack — headline moves *above* the phone, phone pulled up to the upper-middle to clear the bottom 19% CTA/nav zone, type +20%. Focal survives the crop because it started in the center band.
|
||||
>
|
||||
> Why it works: one focal point, depth roles assigned, one camera move, safe-area-aware reflow instead of a blind crop.
|
||||
|
||||
### ANTI-PATTERN — the "everything centered, everything moving" frame
|
||||
|
||||
> - Phone mockup dead-center, headline centered above, logo centered below → three elements fighting on the same axis, no hierarchy, no power point.
|
||||
> - All three layers slide left at the same 1.0x speed → no parallax, no depth, reads as "the whole template moved" (violates the **1/3 Rule / distance**: a full-width slide with no intermediate keyframe).
|
||||
> - Camera does push + pan + slight rotate at once → chaotic, no single intent.
|
||||
> - 9:16 made by cropping the 16:9 sides → headline and logo clipped, phone too low behind the nav bar.
|
||||
>
|
||||
> Fix: pick one focal power point, assign each layer a depth role + distinct parallax speed, one camera move, and restack for vertical instead of cropping.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
| Symptom | Why | Fix |
|
||||
|---|---|---|
|
||||
| Frame feels flat / "the template moved" | all layers move at one speed; no depth roles | assign bg/mid/fg the Ambient/Secondary/Primary roles + 0.2/0.6/1.2x parallax |
|
||||
| Eye doesn't know where to look | 2+ elements at equal size/contrast, all centered | one focal point on a thirds power point; rank size>contrast>color>position |
|
||||
| Camera move reads as chaotic | push + pan + rotate combined | one move per beat; pick the one that serves the intent |
|
||||
| Content clipped on phones | designed for 16:9, cropped to 9:16 | restack vertically; keep critical content in title-safe margins (top 13% / bottom 19% on 9:16) |
|
||||
| Entrance reads as "slide", not "event" | element travels >1/3 of frame in one unbroken move | break with an intermediate keyframe or pair travel with scale/opacity (1/3 Rule, distance) |
|
||||
| Looks cheap / cramped | frame filled edge to edge | reserve negative space around the focal point; it's an active element |
|
||||
| Motion fights the story | numbers chosen before intent | settle Three Pillars + Motion Personality first, then number the move |
|
||||
|
||||
## Deliverable — a composition plan
|
||||
|
||||
A good composition plan is handed to an implementation skill (`gsap-web`, `remotion-video`, `after-effects`) and contains:
|
||||
|
||||
- **Frame spec**: aspect(s), resolution, fps, grid (cols / margin / gutter), baseline unit.
|
||||
- **Focal point**: coordinates (px or %) on a thirds power point + the hierarchy ranking of every element.
|
||||
- **Layer map**: each element assigned bg/mid/fg → Ambient/Secondary/Primary, with parallax speed.
|
||||
- **Enter/exit**: per element — edge, direction, easing family, and whether travel exceeds 1/3 (and how it's broken).
|
||||
- **Camera move**: the single move, its property + from→to values, duration, easing, transform-origin.
|
||||
- **Safe areas**: title-safe margins per target aspect + the restack plan for vertical.
|
||||
- **Cross-refs**: which Motion Personality and which Pillars decisions drive the timing.
|
||||
|
||||
### Before you finish — checklist
|
||||
|
||||
- [ ] Exactly one primary focal point, on a thirds power point (or deliberate symmetry).
|
||||
- [ ] Hierarchy ranked (size > contrast > color > position); secondaries don't compete.
|
||||
- [ ] Negative space reserved around the focal point.
|
||||
- [ ] Every element assigned a depth layer (bg/mid/fg = Ambient/Secondary/Primary) with a parallax speed.
|
||||
- [ ] One camera move, not stacked; eased (not linear unless a loop); 800–2000ms for hero moves.
|
||||
- [ ] Entrances/exits use nearest edge, follow reading direction, ease-out in / ease-in out.
|
||||
- [ ] No element travels >1/3 of frame without an intermediate keyframe or scale/opacity change.
|
||||
- [ ] Critical content inside title-safe margins for every target aspect; vertical is restacked, not cropped.
|
||||
- [ ] Motion Personality + Three Pillars settled before any number was chosen.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Need | Default |
|
||||
|---|---|
|
||||
| Grid | 12 columns, 8px baseline, multiples of 8 spacing |
|
||||
| Focal placement | rule-of-thirds power point, not dead center |
|
||||
| Hierarchy order | size > contrast > color > position |
|
||||
| Depth layers | bg / mid / fg, parallax 0.2x / 0.6x / 1.2x |
|
||||
| Camera | one move at a time, ease-in-out, 800-2000ms |
|
||||
| Enter / exit | follow reading direction; ease-out in, ease-in out |
|
||||
| 9:16 safe | top 12%, bottom 18%, sides 6% |
|
||||
|
||||
## Reference files
|
||||
|
||||
- `references/camera-and-grids.md` — grid math with worked numbers, full safe-area percentage tables per aspect, parallax layering code (CSS scroll + JS pointer + Three.js), and named camera-move recipes (push-in, whip-pan, orbit, dolly-zoom) with keyframe values.
|
||||
@@ -0,0 +1,153 @@
|
||||
# Camera moves, grids, safe areas, and parallax
|
||||
|
||||
Worked math and runnable code. Camera timings use ease-in-out (`cubic-bezier(0.65, 0, 0.35, 1)`); never `linear` except continuous loops.
|
||||
|
||||
## Grid math
|
||||
|
||||
```
|
||||
content_width = frame_width - 2 * margin
|
||||
column_width = (content_width - (cols - 1) * gutter) / cols
|
||||
column_x(i) = margin + i * (column_width + gutter) // 0-indexed
|
||||
span_width(n) = n * column_width + (n - 1) * gutter // width of n columns
|
||||
```
|
||||
|
||||
Worked examples:
|
||||
|
||||
| Frame | margin | gutter | cols | content | column | 4-col span |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 1920x1080 | 100 | 24 | 12 | 1720 | 121.3 | 557.3 |
|
||||
| 1080x1080 | 64 | 16 | 12 | 952 | 64.7 | 306.6 |
|
||||
| 1080x1920 | 48 | 16 | 6 | 984 | 150.7 | 650.6 |
|
||||
|
||||
Baseline / spacing: use an 8px base unit. Allowed spacing values: 4, 8, 16, 24, 32, 48, 64, 96. Type sizes also on the scale (e.g. 16/20/24/32/48/64/96). Everything snapping to multiples of 8 produces automatic visual cohesion.
|
||||
|
||||
Rule-of-thirds lines for any frame: verticals at `W/3` and `2W/3`, horizontals at `H/3` and `2H/3`. Power points = the four intersections. For 1920x1080: verticals 640 / 1280, horizontals 360 / 720.
|
||||
|
||||
## Safe-area percentages
|
||||
|
||||
Margins are fractions of the frame dimension on that side. Keep text, logos, faces, and CTAs inside.
|
||||
|
||||
| Aspect | Top | Bottom | Left | Right | Reason |
|
||||
|---|---|---|---|---|---|
|
||||
| 16:9 (1920x1080) | 5% (54px) | 5% (54px) | 5% (96px) | 5% (96px) | general web/video; 10% for broadcast |
|
||||
| 9:16 (1080x1920) | 13% (250px) | 19% (365px) | 6% (65px) | 6% (65px) | top status/caption, bottom CTA bar + nav |
|
||||
| 1:1 (1080x1080) | 7% (76px) | 7% (76px) | 7% (76px) | 7% (76px) | feed crop symmetric |
|
||||
| 4:5 (1080x1350) | 8% (108px) | 8% (108px) | 6% (65px) | 6% (65px) | taller feed crop |
|
||||
|
||||
9:16 platform-specific danger zones (approximate, design around these): TikTok right-rail icons occupy the right ~12% mid-to-lower; caption/handle sit in the bottom ~15-20%; Instagram Reels similar. Center the focal subject so it survives all crops, then reflow secondary content per aspect.
|
||||
|
||||
Multi-aspect strategy: design the master in 16:9 with the focal point near the vertical center band; for 9:16, restack horizontally-arranged elements into a vertical stack (don't merely crop the sides), enlarge type ~15-25%, and pull the focal point up toward the upper-middle to clear the bottom UI.
|
||||
|
||||
## Parallax layering
|
||||
|
||||
### CSS scroll-driven (pure CSS, modern browsers)
|
||||
|
||||
```css
|
||||
@keyframes parallax { to { translate: 0 calc(var(--speed) * 200px); } }
|
||||
.layer { animation: parallax linear; animation-timeline: scroll(root block); }
|
||||
.bg { --speed: 0.2; }
|
||||
.mid { --speed: 0.6; }
|
||||
.fg { --speed: 1.2; }
|
||||
```
|
||||
|
||||
### Pointer / mouse parallax (JS)
|
||||
|
||||
```js
|
||||
const layers = [
|
||||
{ el: document.querySelector('.bg'), depth: 0.2 },
|
||||
{ el: document.querySelector('.mid'), depth: 0.6 },
|
||||
{ el: document.querySelector('.fg'), depth: 1.2 },
|
||||
];
|
||||
addEventListener('pointermove', (e) => {
|
||||
const x = (e.clientX / innerWidth - 0.5) * 2; // -1..1
|
||||
const y = (e.clientY / innerHeight - 0.5) * 2;
|
||||
for (const { el, depth } of layers) {
|
||||
el.style.transform = `translate3d(${-x * depth * 30}px, ${-y * depth * 30}px, 0)`;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Background depth 0.1-0.3, midground 0.5-0.7, foreground 1.0-1.5. Foreground may exceed 1.0 to read as very close; apply a slight blur to foreground layers as they cross the subject.
|
||||
|
||||
### Three.js depth parallax
|
||||
|
||||
```js
|
||||
// Place planes at different z; perspective camera makes nearer planes move more
|
||||
camera.position.z = 5;
|
||||
bg.position.z = -8; // far -> moves least
|
||||
mid.position.z = -2;
|
||||
fg.position.z = 1.5; // near -> moves most
|
||||
function onMove(nx, ny) { // nx, ny in -1..1
|
||||
camera.position.x = nx * 0.6;
|
||||
camera.position.y = ny * 0.4;
|
||||
camera.lookAt(0, 0, 0);
|
||||
}
|
||||
```
|
||||
|
||||
## Named camera-move recipes
|
||||
|
||||
### Push-in (dolly in) — build focus/intimacy
|
||||
|
||||
```
|
||||
scale 1.0 -> 1.12 over 1600ms, ease-in-out
|
||||
origin: the subject (transform-origin at focal point)
|
||||
optional: slight bg blur ramp 0 -> 3px to add depth cue
|
||||
```
|
||||
|
||||
### Pull-out (reveal context)
|
||||
|
||||
```
|
||||
scale 1.15 -> 1.0 over 1800ms, ease-in-out, origin centered
|
||||
```
|
||||
|
||||
### Whip-pan (energetic transition between scenes)
|
||||
|
||||
```
|
||||
translateX 0 -> -120% over 280ms, ease-in (accelerate out)
|
||||
add motion blur (CSS: filter blur on X, or directional blur in compositor)
|
||||
incoming scene: translateX 120% -> 0 over 280ms ease-out, overlapped
|
||||
```
|
||||
|
||||
### Pan / move (reveal space)
|
||||
|
||||
```
|
||||
translateX 0 -> -300px over 1500ms ease-in-out
|
||||
keep the subject leading: frame it ~1/3 from the trailing edge so it "leads" into open space
|
||||
```
|
||||
|
||||
### Orbit (3D)
|
||||
|
||||
```js
|
||||
// angle sweep around subject at fixed radius
|
||||
const r = 6, a0 = 0, a1 = Math.PI / 4, dur = 2000; // 45deg over 2s
|
||||
// ease t in 0..1, then:
|
||||
const a = a0 + (a1 - a0) * easeInOut(t);
|
||||
camera.position.set(Math.sin(a) * r, camera.position.y, Math.cos(a) * r);
|
||||
camera.lookAt(target);
|
||||
```
|
||||
|
||||
### Dolly-zoom (vertigo) — unsettling, dramatic
|
||||
|
||||
Move the camera toward the subject while zooming out (or vice versa) so the subject stays the same size but the background warps.
|
||||
|
||||
```js
|
||||
// keep subject screen-size constant: as camera moves closer (z down), widen FOV
|
||||
const baseDist = 8, baseFov = 35;
|
||||
function dollyZoom(dist) {
|
||||
camera.position.z = dist;
|
||||
// maintain framing: fov such that subjectHeight subtends same angle
|
||||
camera.fov = 2 * Math.atan((Math.tan((baseFov * Math.PI/180)/2) * baseDist) / dist) * 180/Math.PI;
|
||||
camera.updateProjectionMatrix();
|
||||
}
|
||||
// animate dist from 8 -> 4 over ~2000ms ease-in-out
|
||||
```
|
||||
|
||||
### General rules
|
||||
|
||||
- One move per beat. Combining push + pan + rotate reads as chaos; pick the single move that serves the moment.
|
||||
- Slower = more cinematic and expensive-looking; 800-2000ms for hero camera moves.
|
||||
- Lead moving subjects: frame them off-center toward the trailing edge so there's open space in the direction of travel.
|
||||
- Enter/exit through the nearest edge: ease-out (slight overshoot) on enter, ease-in (accelerate) on exit, following reading direction for natural feel.
|
||||
|
||||
---
|
||||
Respect the grid and safe areas and the camera move guides the eye. Built by **[iart.ai](https://iart.ai/?utm_source=github&utm_medium=readme&utm_campaign=motion-design-skills&utm_content=skill_footer&utm_term=shot-composition)** — the AI motion agent for editable, on-brand motion graphics.
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Only run in Claude Code cloud sessions.
|
||||
if [ "${CLAUDE_CODE_REMOTE:-}" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Install the rea CLI (used by the reverse-engineer-anything skill) if missing.
|
||||
if ! command -v rea >/dev/null 2>&1; then
|
||||
npm install --global rea-agents
|
||||
fi
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/after-effects
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/animation-principles
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/beat-sync-editing
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/color-motion
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
name: figma
|
||||
description: Bring Figma into a HyperFrames video - rendered assets, brand tokens, components, storyboard frames as reconstructed motion (frames read as states, not slides), shaders and connector-assisted motion. Use when the person pastes a figma.com link or asks to use a Figma design, frame, logo or brand.
|
||||
---
|
||||
|
||||
**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin.
|
||||
|
||||
# Figma → HyperFrames
|
||||
|
||||
Every import happens at build time and freezes a local file with its source recorded under `.media/`, so renders never call Figma and re-running a command re-imports only what changed. Assets, tokens and components go over REST with a token; motion and shader data need a Figma connector, or a native export from the person.
|
||||
|
||||
## Access
|
||||
|
||||
`FIGMA_TOKEN` in the shell or the project `.env`. Check it exists before the first command; if not, tell the person how to make one and stop: figma.com → Settings → Security → Personal access tokens, read-only **File content** and **File metadata**, plus **Library content** for `tokens` on non-Enterprise plans. They set it themselves; never ask them to paste it into the chat.
|
||||
|
||||
| Error | Meaning |
|
||||
| --- | --- |
|
||||
| `BAD_TOKEN` (401) | Expired or revoked: make a new one |
|
||||
| `FORBIDDEN` (403) | The message names the missing scope, or the file isn't visible to the account |
|
||||
| `REQUIRES_ENTERPRISE` (403, variables) | Not a failure: the published-styles fallback already ran |
|
||||
| `RATE_LIMITED` (429) | Already retried with backoff; wait a minute or import fewer nodes per call |
|
||||
|
||||
## Commands
|
||||
|
||||
- A layer, logo or image: `npx hyperframes figma asset '<url or fileKey:nodeId>' [more refs] --format svg|png --scale 2 --description "<what it is>" [--entity "<brand mark>"]`. SVG for vectors and logos, PNG at 2x for raster. Several nodes of one file in one call render in a single request; `--entity` lets `/media-use` find a brand mark later.
|
||||
- Brand colours and type: `npx hyperframes figma tokens <fileKey>`. Run it before `component` and paste the printed entries into `data-composition-variables`. Without Enterprise it falls back to published styles. A connector can supply variables there instead: cache its raw response in `.media/figma-cache/` and write the bindings to `.media/figma-bindings.jsonl` (`{kind:"binding", figmaId, sourceFileKey, compositionVariableId:"figma:<name>", version}`).
|
||||
- A frame as editable HTML at exact geometry: `npx hyperframes figma component '<url or fileKey:nodeId>'`, packaged under `compositions/components/<name>/`. A fill bound to an imported token comes out as `var(--slug, #literal)`, so one variable re-brands every import; an unknown token stays a literal marked `data-figma-unresolved`: import that library's tokens and re-import. Ask once which file a library is; never match by hex.
|
||||
- Check hero components against Figma's pixels (`figma asset <same node> --format png`). Text drift is the usual cause; report drift you can't fix instead of hand-tweaking the import.
|
||||
|
||||
## Motion (connector)
|
||||
|
||||
Usage beacon: these phases have no CLI touchpoint, so run `npx hyperframes events --skill=figma-motion` when you begin and `npx hyperframes events --skill=figma-motion --event=skill_completed --outcome=success|error` when done. Same for shaders (`figma-shaders`) and storyboards (`figma-storyboard`).
|
||||
|
||||
1. Fetch motion context for the parent frame in one recursive request; cache the raw JSON in `.media/figma-cache/`.
|
||||
2. Normalise with `motionContextToDocs(raw, { selectorFor, repeat })` from `@hyperframes/core/figma`, never by transcribing keyframes. It reads the motion.dev snippets (the CSS snippets stretch durations), drops loop-wrap tail keyframes and keeps bezier eases. `selectorFor` returns the ids from the component import.
|
||||
3. `motionToGsap(doc)` → `emitTimelineScript(spec)`, injected after the GSAP and CustomEase tags: paused, finite, a literal `window.__timelines` key. Named eases outside the mapped set fall back to linear; say so.
|
||||
4. Verify against Figma's own export: `node <SKILL_DIR>/scripts/verify-motion.mjs --reference <export.mp4> --render <render.mp4> --crop WxH+X+Y`. It compares motion between samples, so static drift cancels out, and fails under 15 dB (faithful ≈ 20+, diverging ≈ 5). Measure `--crop` from the render's card edges. A fail means fix the translation, not the threshold.
|
||||
5. A track that won't translate: export it, freeze the MP4 and place it as `<video class="clip">`.
|
||||
|
||||
## Shaders
|
||||
|
||||
Figma's export path flattens shaders to their base colour, and shader source is only reachable for library-published styles. Ask the person for a native export of that frame (PNG or MP4) and place it as an asset or clip.
|
||||
|
||||
## Storyboards: frames are KEYFRAMES, not slides
|
||||
|
||||
Two frames holding the same element describe that element's state through time: animate the element between the states. Playing the frames back to back as stills is the failure mode.
|
||||
|
||||
1. Scenes are every frame-sized node in the section, named frames and loose full-frame rectangles alike: filter by size, not type or name. Order by `absoluteBoundingBox.x`.
|
||||
2. Match children across neighbouring frames by name first, then by similar size and position. Tween geometry between states; crossfade two exports in place when the pixels changed. Export one asset per element, not one still per frame. Unmatched children enter or exit at their scene's beat.
|
||||
3. Text nodes below the strip are motion notes for the scene above them, not on-screen copy. A note about motion inside a scene ("lines reveal one after another") means import that frame as a component and animate it.
|
||||
4. Export in batches of about 4 scene frames per call: past about 12 ids `/v1/images` times out.
|
||||
5. Frames that are one product UI in successive states: rebuild the UI as live DOM: code what changes state, freeze what doesn't, and treat each frame change as an interaction to perform (the cursor moves, clicks, the screen responds), not a tween to apply.
|
||||
6. Stills are the fallback, only for frames with no shared elements.
|
||||
|
||||
Never leave a Figma URL in the composition.
|
||||
@@ -1,153 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Objective fidelity gate for figma-motion imports (skill step 2b).
|
||||
*
|
||||
* Compares the HyperFrames render against Figma's own `export_video` output
|
||||
* using MOTION-ENERGY deltas: for each sample window [t, t+interval], the
|
||||
* frame difference ref(t+i)-ref(t) is compared (PSNR) against
|
||||
* render(t+i)-render(t). Static import divergence (fonts, rasterized edges,
|
||||
* subpixel geometry — the hybrid-fidelity ceiling) cancels out of both
|
||||
* deltas, so the score isolates choreography: trajectories, timing, easing.
|
||||
*
|
||||
* Calibration (SDS "Unlocked" card, 2026-07): a faithful translation scored
|
||||
* min 20.3dB / mean 27.7dB; a diverging one (invented retract keyframes,
|
||||
* wrong durations) scored min 5.0dB / mean 23.1dB. Default threshold 15dB
|
||||
* sits between with margin on both sides.
|
||||
*
|
||||
* node verify-motion.mjs --reference figma-export.mp4 --render out.mp4 \
|
||||
* [--crop WxH+X+Y] [--interval 0.2] [--min-motion-psnr 15]
|
||||
*
|
||||
* --crop selects the card region inside the (usually larger) composition
|
||||
* frame. Measure it from the render (the card's left/top edge + scaled
|
||||
* size), don't guess: a wrong crop reads as motion divergence.
|
||||
*/
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
function arg(name, fallback) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
return i > -1 ? process.argv[i + 1] : fallback;
|
||||
}
|
||||
const reference = arg("reference");
|
||||
const render = arg("render");
|
||||
if (!reference || !render) {
|
||||
console.error(
|
||||
"usage: verify-motion.mjs --reference ref.mp4 --render out.mp4 [--crop WxH+X+Y] [--interval 0.2] [--min-motion-psnr 15]",
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
const crop = arg("crop", null);
|
||||
const interval = Number(arg("interval", "0.2"));
|
||||
const minMotion = Number(arg("min-motion-psnr", "15"));
|
||||
|
||||
const ffprobe = (file) =>
|
||||
Number(
|
||||
execFileSync("ffprobe", [
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
"--",
|
||||
file,
|
||||
])
|
||||
.toString()
|
||||
.trim(),
|
||||
);
|
||||
const refDur = ffprobe(reference);
|
||||
const renderDur = ffprobe(render);
|
||||
const end = Math.min(refDur, renderDur) - interval - 0.01;
|
||||
|
||||
const dims = execFileSync("ffprobe", [
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v",
|
||||
"-show_entries",
|
||||
"stream=width,height",
|
||||
"-of",
|
||||
"csv=p=0",
|
||||
"--",
|
||||
reference,
|
||||
])
|
||||
.toString()
|
||||
.trim()
|
||||
.split(",")
|
||||
.map(Number);
|
||||
const [rw, rh] = dims;
|
||||
|
||||
let cropFilter = "";
|
||||
if (crop) {
|
||||
const m = crop.match(/^(\d+)x(\d+)\+(\d+)\+(\d+)$/);
|
||||
if (!m) {
|
||||
console.error("bad --crop, expected WxH+X+Y");
|
||||
process.exit(2);
|
||||
}
|
||||
cropFilter = `crop=${m[1]}:${m[2]}:${m[3]}:${m[4]},`;
|
||||
}
|
||||
|
||||
const dir = mkdtempSync(join(tmpdir(), "verify-motion-"));
|
||||
const frame = (src, t, vf, dst) => {
|
||||
const args = ["-y", "-v", "error", "-ss", String(t), "-i", src, "-frames:v", "1"];
|
||||
if (vf) args.push("-vf", vf);
|
||||
execFileSync("ffmpeg", args.concat(dst));
|
||||
};
|
||||
const diff = (a, b, dst) =>
|
||||
execFileSync("ffmpeg", [
|
||||
"-y",
|
||||
"-v",
|
||||
"error",
|
||||
"-i",
|
||||
a,
|
||||
"-i",
|
||||
b,
|
||||
"-filter_complex",
|
||||
"blend=all_mode=difference",
|
||||
dst,
|
||||
]);
|
||||
const psnr = (a, b) => {
|
||||
// spawnSync with array args (no shell): psnr stats land on stderr
|
||||
const r = spawnSync("ffmpeg", ["-i", a, "-i", b, "-lavfi", "psnr", "-f", "null", "-"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
const m = (r.stderr || "").match(/average:([\d.]+|inf)/);
|
||||
return m ? (m[1] === "inf" ? 99 : Number(m[1])) : NaN;
|
||||
};
|
||||
|
||||
const renderVf = `${cropFilter}scale=${rw}:${rh}`;
|
||||
const results = [];
|
||||
for (let t = 0; t <= end; t = Math.round((t + interval) * 1000) / 1000) {
|
||||
const t1 = Math.round((t + interval) * 1000) / 1000;
|
||||
frame(reference, t, null, join(dir, "ra.png"));
|
||||
frame(reference, t1, null, join(dir, "rb.png"));
|
||||
frame(render, t, renderVf, join(dir, "oa.png"));
|
||||
frame(render, t1, renderVf, join(dir, "ob.png"));
|
||||
diff(join(dir, "ra.png"), join(dir, "rb.png"), join(dir, "rd.png"));
|
||||
diff(join(dir, "oa.png"), join(dir, "ob.png"), join(dir, "od.png"));
|
||||
results.push({
|
||||
t,
|
||||
motion: psnr(join(dir, "rd.png"), join(dir, "od.png")),
|
||||
abs: psnr(join(dir, "rb.png"), join(dir, "ob.png")),
|
||||
});
|
||||
}
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
|
||||
const min = Math.min(...results.map((r) => r.motion));
|
||||
const mean = results.reduce((s, r) => s + r.motion, 0) / results.length;
|
||||
for (const r of results)
|
||||
console.log(
|
||||
`window ${r.t.toFixed(2)}s→${(r.t + interval).toFixed(2)}s motion-psnr=${r.motion.toFixed(2)}dB (abs=${r.abs.toFixed(1)}dB)${r.motion < minMotion ? " <-- BELOW THRESHOLD" : ""}`,
|
||||
);
|
||||
console.log(
|
||||
`\nwindows=${results.length} min-motion=${min.toFixed(2)}dB mean-motion=${mean.toFixed(2)}dB threshold=${minMotion}dB`,
|
||||
);
|
||||
if (min < minMotion) {
|
||||
console.log(
|
||||
"VERDICT: FAIL — choreography diverges from the Figma export (check timings, invented keyframes, durations)",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("VERDICT: PASS — motion matches the Figma export within the static-fidelity ceiling");
|
||||
@@ -1,97 +0,0 @@
|
||||
---
|
||||
name: hyperframes-cli
|
||||
description: The HyperFrames CLI by job - scaffold, check, look at frames, edit the timeline, preview, render locally or in the cloud, publish, project history - plus reading its JSON and recovering from build or render failures.
|
||||
---
|
||||
|
||||
**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin.
|
||||
|
||||
# HyperFrames CLI
|
||||
|
||||
Run `npx hyperframes <command>` unless the project gives a wrapper; then use the wrapper. It needs Node.js 22+ and FFmpeg. Every command has `--help`; most take `--json`, and a failure prints `✗ …`, a `Fix:` line and exits non-zero.
|
||||
|
||||
## Commands by job
|
||||
|
||||
| Job | Commands |
|
||||
| --- | --- |
|
||||
| Start | `init <dir>` (blank; `--example <name>` only when asked), `capture <url>` |
|
||||
| Read a project | `info`, `compositions`, `timeline --json`, `docs <topic>` |
|
||||
| Check it | `lint` while writing; `check --json` as the gate (lint, runtime, layout, contrast) |
|
||||
| Look at it | `snapshot` (`--at <t,…>`, `--range START,END`, `--composition <id>`, `--zoom`, `--angle`, `--against <video>`), `keyframes`, `compare`, `audio` |
|
||||
| Edit the timeline | `timeline` with `move`, `trim`, `split`, `set`, `duplicate`, `delete`, `apply`, `undo`, `ids` |
|
||||
| Reuse | `catalog`, `add` (`/hyperframes-registry`) |
|
||||
| Media and sound | `media-treatment`, `grade-compare`, `remove-background`, `tts`, `transcribe`, `beats`, `normalize-audio` (`/media-use`) |
|
||||
| Show and ship | `preview`, `present` (`/slideshow`), `render`, `publish`, `cloud`, `lambda`, `cloudrun` |
|
||||
| Desktop app | `open`, `catch-up` |
|
||||
| Upkeep | `doctor`, `browser`, `upgrade`, `skills`, `usage`, `clean`, `feedback` |
|
||||
|
||||
`validate`, `inspect` and `layout` are deprecated aliases of `check`; `events` is telemetry for skills' own scripts. Don't run them.
|
||||
|
||||
## The loop
|
||||
|
||||
1. Write, then `npx hyperframes lint` after the first pass and after structural changes.
|
||||
2. `npx hyperframes check --json` and fix every error. While a lint error remains the layout and contrast audits don't run; "0 samples" means nothing was checked. Contrast failures carry a `suggestedColor`. A finding at a single sample (an entrance in flight) is info; a 3 s+ video with no visible change fails `sweep_static`.
|
||||
3. `npx hyperframes snapshot` and read the PNGs (under `.hyperframes/artifacts/snapshots/`): that is how you see the video. With sub-compositions, snapshot a midpoint of every scene; tiny unstyled content or a missing hero is a mount defect (`../hyperframes/references/format.md`).
|
||||
4. `npx hyperframes preview --background`, check the URL answers 200, give the person the project URL and wait, unless they asked for the file or for no stops. Passing checks alone is not a reason to render.
|
||||
5. When it is time to render (step 4): `npx hyperframes render --quality looks --output out.mp4` (`draft` while iterating, `delivery` for the final). Confirm the file is non-empty and its `ffprobe` duration matches the root `data-duration`, then look at decoded frames.
|
||||
|
||||
"This element" or the current selection: `npx hyperframes preview --context --json --context-fields selection`; `no-selection` means ask them to click it.
|
||||
|
||||
## Edit the timeline
|
||||
|
||||
These are the edits Studio itself makes, so the result stays in Studio's shape.
|
||||
|
||||
- `timeline --json` lists clips with absolute `absStart`/`absEnd`, owning `file` and the `#id` ref the edit commands take (`timeline ids` stamps missing ids).
|
||||
- `timeline move|trim|split|set|duplicate|delete <ref> … --plan --json` previews one edit; run it again without `--plan`. `--start`, `--end`, `--duration` set one edge or the length.
|
||||
- An edit is refused when the clip would overlap a neighbour (`--overwrite`, or move the neighbour first) or end past its composition: raise the root's `data-duration` first. No command changes `data-duration`.
|
||||
- Several edits at once: a JSON file such as `[{"verb": "move", "ref": "#outro", "time": "9"}]`, then `timeline apply plan.json --json`. Each run prints a `receipt`; save it and `timeline undo receipt.json` reverts it.
|
||||
|
||||
<!-- history (trial): remove this block together with the command -->
|
||||
|
||||
## Project history
|
||||
|
||||
Every write is kept and can be undone. At the start of a turn, `npx hyperframes history begin --who <you> --label "<plan>"`, then `history --since mine --who <you>` to see what the person changed; build on their edits. A check failed or they say it got worse: `history undo --who <you>`, never hand-edit back. End the turn with `history end`.
|
||||
|
||||
<!-- /history (trial) -->
|
||||
|
||||
## Render where
|
||||
|
||||
| Need | Command |
|
||||
| --- | --- |
|
||||
| Local | `render --quality looks --output out.mp4` (`draft`, `delivery`) |
|
||||
| Reproducible container | `render --docker --strict --output out.mp4` |
|
||||
| Batch from variables | `render --batch rows.json --output "renders/{name}.mp4"` |
|
||||
| Hosted, no local Chrome or FFmpeg | `cloud render` (`references/cloud.md`) |
|
||||
| Your own AWS or GCP | `lambda render`, `cloudrun render` (`references/lambda.md`, `references/cloudrun.md`) |
|
||||
|
||||
A 30 s film renders in a minute or two: run it in the foreground. Detach only a render longer than your tool's timeout (`HYPERFRAMES_RENDER_DETACHED=1`, its own group, output to a log) and wait with `until grep -qE 'Render (complete|failed)' render.log; do sleep 5; done`, never `pgrep -f`, which matches its own loop and waits until the tool times out.
|
||||
|
||||
After a verified render, send one `feedback --rating`, plus a report for anything the person asked for that HyperFrames can't do, unless telemetry is off: `references/feedback.md`.
|
||||
|
||||
## When a command fails
|
||||
|
||||
| Symptom | Fix |
|
||||
| --- | --- |
|
||||
| Chrome missing or won't start | `npx hyperframes browser ensure`; `doctor --json` always exits 0, gate on `.ok` |
|
||||
| A heavy page isn't ready in time | Raise `--timeout` on `check` or `snapshot` |
|
||||
| Unusual codec looks blank | Keep `--proxy` on (the default) |
|
||||
| Render slow on Linux | The summary's second line says `screenshot` + `software gpu`: the slow path |
|
||||
| `capture` fails or writes `BLOCKED.md` | Take nothing from its partial files; retry into a new `-o` folder |
|
||||
| Disk tight | `clean --dry-run`, then `clean`; never delete `.hyperframes` (it holds history) |
|
||||
|
||||
## References
|
||||
|
||||
Read the row before running its command.
|
||||
|
||||
| Commands | Read |
|
||||
| --- | --- |
|
||||
| `init`, `capture`, `skills` | `references/init-and-scaffold.md` |
|
||||
| `lint`, `check`, motion sidecars, `snapshot` | `references/lint-validate-inspect.md` |
|
||||
| `compare`, `grade-compare`, `render --batch` | `references/compare-and-batch.md` |
|
||||
| `beats` | `references/beats.md` |
|
||||
| `preview`, `play`, `render`, `publish`, Studio context | `references/preview-render.md` |
|
||||
| `doctor`, `browser` | `references/doctor-browser.md` |
|
||||
| `auth`, `cloud`, template variables | `references/cloud.md` |
|
||||
| `lambda`, `cloudrun` | `references/lambda.md`, `references/cloudrun.md` |
|
||||
| `info`, `upgrade`, `compositions`, `timeline`, `docs`, `usage`, telemetry | `references/upgrade-info-misc.md` |
|
||||
| `--strict` flags, run ids, `clean`, shader lint, JSON paths | `references/conventions.md` |
|
||||
| `keyframes` | `../hyperframes/references/motion.md` (Seeing motion) |
|
||||
@@ -1,15 +0,0 @@
|
||||
# Generate a project beat grid
|
||||
|
||||
Use `hyperframes beats` when an existing HyperFrames project needs the Studio-compatible beat file for its music track. This is a CLI utility, not a complete video workflow.
|
||||
|
||||
```bash
|
||||
npx hyperframes beats
|
||||
npx hyperframes beats ./my-video
|
||||
npx hyperframes beats ./my-video --json
|
||||
```
|
||||
|
||||
The project must contain a local music `<audio>` source. Mark it with `data-timeline-role="music"`; an id containing `music`, `bgm`, or `soundtrack` is also recognized. The command decodes that file with ffmpeg, tracks its beats, and writes `beats/<audio-relative-path>.json`.
|
||||
|
||||
If the track has no steady beat, the command fails and writes nothing. If ffmpeg is missing, run `npx hyperframes doctor`.
|
||||
|
||||
For a whole video cut to a music track, see `/hyperframes` → `references/film.md`.
|
||||
@@ -1,163 +0,0 @@
|
||||
# cloud — HeyGen-hosted rendering (zero-infra)
|
||||
|
||||
`hyperframes cloud render` renders a composition on HeyGen's managed cloud. The CLI zips the project, uploads it, runs the render on HeyGen's infrastructure (Chromium + FFmpeg), and downloads the finished video. Nothing to deploy, and no Chrome/FFmpeg/AWS to manage; you pay per credit.
|
||||
|
||||
```bash
|
||||
npx hyperframes auth login # one-time sign-in
|
||||
npx hyperframes cloud render # zip, upload, render, download
|
||||
```
|
||||
|
||||
## When to use managed cloud, Lambda, Cloud Run, or local
|
||||
|
||||
- **`hyperframes render`** (local): fastest iteration loop, use while authoring.
|
||||
- **`hyperframes cloud render`**: zero-infra. HeyGen runs the render and you pay per credit. This is the default answer to "render in the cloud" when you don't want to manage Chrome/FFmpeg/AWS.
|
||||
- **`hyperframes lambda render`**: bring-your-own-AWS distributed rendering with chunked parallelism. Only worth it when you've already invested in AWS (see `lambda.md`).
|
||||
- **`hyperframes cloudrun render`**: bring-your-own-GCP distributed rendering through Cloud Run and Workflows. Use only when GCP ownership is explicit (see `cloudrun.md`).
|
||||
|
||||
## Authentication
|
||||
|
||||
Cloud rendering needs a HeyGen credential, stored at `~/.heygen/credentials` (`0600`) and shared with the [`heygen` CLI](https://github.com/heygen-com/heygen-cli): sign in with one and the other picks up the session.
|
||||
|
||||
```bash
|
||||
npx hyperframes auth login # OAuth 2.0 + PKCE, opens the browser
|
||||
npx hyperframes auth login --api-key # CI/headless: hidden prompt, or pipe: echo "$HEYGEN_API_KEY" | ... --api-key
|
||||
npx hyperframes auth status # active credential source, identity, billing snapshot
|
||||
# exit 0 = signed in and verified; exit 1 = not signed in,
|
||||
# or the credential was rejected — signed-out exit 1 is the
|
||||
# normal offline state (scripts: `auth status || echo offline`),
|
||||
# not a command failure
|
||||
npx hyperframes auth refresh # force-refresh an OAuth token before a long job
|
||||
npx hyperframes auth logout # clear the stored credential
|
||||
```
|
||||
|
||||
Credential resolution order (first match wins): `HEYGEN_API_KEY`, then `HYPERFRAMES_API_KEY`, then `HEYGEN_ACCESS_TOKEN` (an OAuth token a host app injects; never refreshed or saved), then `~/.heygen/credentials`. Point at a different backend with `HEYGEN_API_URL` (default `https://api.heygen.com`).
|
||||
|
||||
## The render pipeline
|
||||
|
||||
`cloud render` runs end-to-end:
|
||||
|
||||
1. **Resolve the project**: a local directory (default `.`), or skip the upload with `--asset-id` / `--url`.
|
||||
2. **Auto-detect aspect ratio** from the entry HTML's `data-width`/`data-height`.
|
||||
3. **Zip** the project (same ignore set as `hyperframes publish`, including `.hyperframesignore`).
|
||||
4. **Upload** the zip through the direct-to-S3 asset flow, yielding an `asset_id`.
|
||||
5. **Submit** the render to `POST /v3/hyperframes/renders`, yielding a `render_id`.
|
||||
6. **Poll** `GET /v3/hyperframes/renders/{id}` until it completes or fails (skip with `--no-wait`).
|
||||
7. **Download** the signed video URL to disk.
|
||||
|
||||
## Archive size and `.hyperframesignore`
|
||||
|
||||
The direct-upload limit is 200 MB. HyperFrames automatically excludes root-level `renders/` and `snapshots/`, along with its existing development exclusions such as `.git`, `node_modules`, `dist`, `.next`, `coverage`, and dotfiles. Add project-specific gitignore-style rules to `<project>/.hyperframesignore` when other generated or intermediate assets are not required at render time. The same rules affect `hyperframes publish`.
|
||||
|
||||
Inspect the exact archive without authenticating, uploading, spending credits, or starting a render:
|
||||
|
||||
```bash
|
||||
npx hyperframes cloud render <project> --dry-run --json
|
||||
```
|
||||
|
||||
The result reports compressed `size_bytes`, `file_count`, the 200 MB limit, and the ten largest included files.
|
||||
|
||||
When a cloud upload reports a size-limit error, agents must use this workflow:
|
||||
|
||||
1. Run the dry-run command and inspect the largest included files and directories.
|
||||
2. Classify obvious generated outputs first: old renders, extra snapshot/contact-sheet directories, caches, exported previews, and source media used only to produce final assets.
|
||||
3. Before excluding anything else, search `src`, `href`, `url()`, `data-composition-src`, JavaScript strings, manifests, and variable-driven paths across every HTML, CSS, and JavaScript entry.
|
||||
4. Preserve existing `.hyperframesignore` comments and rules. Add the narrowest verified-unneeded root-relative paths; prefer an exact directory or file over a broad wildcard.
|
||||
5. Never ignore `index.html`, the selected composition, mounted sub-compositions, fonts, images, audio, video, scripts, or manifests merely because they are large. Never ignore all of `assets/`.
|
||||
6. Rerun dry-run until the archive is below the limit, then run `npx hyperframes check`. Remember that `check` sees the source directory, so it cannot prove a dynamically computed asset path remains in the filtered archive; the reference audit is still required.
|
||||
|
||||
Example:
|
||||
|
||||
```gitignore
|
||||
# Additional generated verification passes
|
||||
/snapshots2/
|
||||
/snapshots3/
|
||||
|
||||
# Master used only to produce the final background clips
|
||||
/assets/bg-pattern.mp4
|
||||
```
|
||||
|
||||
Rules support comments, globs, and negation. A later rule can override a default, for example `!/snapshots/` when that directory intentionally contains render inputs.
|
||||
|
||||
## Render options
|
||||
|
||||
| Flag | Default | Meaning |
|
||||
| ---------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `--fps` | `30` | Frames per second, 1–240. |
|
||||
| `--quality` | `standard` | `draft`, `standard`, or `high`. |
|
||||
| `--format` | `mp4` | `mp4`, `webm`, or `mov` (webm/mov carry alpha). |
|
||||
| `--resolution` | `1080p` | `1080p` or `4k` (4k billed at 1.5×). |
|
||||
| `--aspect-ratio` | auto | `16:9`, `9:16`, or `1:1`. Auto from a local project's `data-width`/`data-height`; defaults to `16:9` for `--asset-id`/`--url`. |
|
||||
| `--composition` / `-c` | `index.html` | Entry HTML file inside the zip. |
|
||||
| `--output` / `-o` | `renders/<render_id>.<ext>` | Local download destination. |
|
||||
| `--dry-run` | off | Build and inspect a local project zip without authenticating, uploading, or rendering. |
|
||||
|
||||
```bash
|
||||
npx hyperframes cloud render . \
|
||||
--composition compositions/intro.html \
|
||||
--output ./renders/intro.mp4
|
||||
|
||||
npx hyperframes cloud render --quality high --fps 60
|
||||
```
|
||||
|
||||
`--resolution 4k` cannot combine with `--format webm`/`mov`: the 4k supersampling path has no alpha channel. Render 4k as mp4, or render alpha at native resolution.
|
||||
|
||||
## Templates and variables
|
||||
|
||||
Cloud rendering supports [composition variables](../../hyperframes/references/format.md#variables): declare `data-composition-variables` on the composition, then fill them at render time.
|
||||
|
||||
```bash
|
||||
npx hyperframes cloud render --variables '{"title":"Q4 Recap","theme":"dark"}'
|
||||
npx hyperframes cloud render --variables-file ./vars.json
|
||||
npx hyperframes cloud render --variables '{"title":"Q4 Recap"}' --strict-variables
|
||||
```
|
||||
|
||||
For a **local project** the CLI validates `--variables` against the declared schema _before_ uploading. For `--asset-id`/`--url` the schema lives server-side, so mismatches surface as a `hyperframes_project_invalid` API error.
|
||||
|
||||
**Upload once, re-render many** is the idiomatic template loop: render a local project to get its `asset_id`, then re-submit against that asset with new values (no re-zip, no re-upload).
|
||||
|
||||
```bash
|
||||
npx hyperframes cloud render ./card-template # note the asset_id printed on upload
|
||||
npx hyperframes cloud render --asset-id asst_abc123 --variables '{"name":"Ada"}'
|
||||
npx hyperframes cloud render --asset-id asst_abc123 --variables '{"name":"Linus"}'
|
||||
```
|
||||
|
||||
For high-volume personalized batches, both self-managed paths provide JSONL fan-out: AWS Lambda (`lambda.md`) and Google Cloud Run (`cloudrun.md`). The full variables schema (types, declarative bindings, sub-composition overrides, precedence) lives in [`/hyperframes` format.md](../../hyperframes/references/format.md#variables).
|
||||
|
||||
## Fire-and-forget and webhooks
|
||||
|
||||
By default the CLI blocks, polls, and downloads. Combine `--no-wait` (submit and exit with just the `render_id`) with `--callback-url` (HTTPS webhook on terminal status) for true fire-and-forget:
|
||||
|
||||
```bash
|
||||
npx hyperframes cloud render --callback-url https://example.com/hf-hook --no-wait
|
||||
# Poll later with: hyperframes cloud get hfr_def456
|
||||
```
|
||||
|
||||
| Flag | Meaning |
|
||||
| ----------------- | --------------------------------------------------- |
|
||||
| `--no-wait` | Submit and exit immediately; print the `render_id`. |
|
||||
| `--callback-url` | HTTPS webhook fired when the render terminates. |
|
||||
| `--callback-id` | Opaque tracking ID echoed in webhook payloads. |
|
||||
| `--poll-interval` | Poll cadence in seconds (default `2`). |
|
||||
| `--max-wait` | Max poll duration in minutes (default `60`). |
|
||||
|
||||
## Managing renders
|
||||
|
||||
```bash
|
||||
npx hyperframes cloud list # recent renders (--limit, --token, --all)
|
||||
npx hyperframes cloud get hfr_def456 # full detail + short-lived signed video_url
|
||||
npx hyperframes cloud delete hfr_def456 # soft-delete (--no-confirm to skip the prompt)
|
||||
```
|
||||
|
||||
`video_url` and `thumbnail_url` are short-lived presigned URLs, so re-fetch with `cloud get` rather than caching them.
|
||||
|
||||
## Safe retries
|
||||
|
||||
The CLI transparently retries a `401` by force-refreshing the OAuth token and replaying. That's harmless for reads, but the zip upload (`POST /v3/assets`) is **not** idempotent: a blind retry creates a duplicate asset and bills twice. Pass `--idempotency-key` so retries are safe:
|
||||
|
||||
```bash
|
||||
npx hyperframes cloud render . --idempotency-key "$(uuidgen)"
|
||||
```
|
||||
|
||||
The key is forwarded to both upload and submit (the server scopes idempotency per-endpoint, so reusing one value is safe). Use any opaque string in `[A-Za-z0-9_:.-]`, 1–255 chars.
|
||||
|
||||
Full flag reference: docs `/deploy/cloud` and `/packages/cli#hyperframes-cloud`.
|
||||
@@ -1,62 +0,0 @@
|
||||
# Cloud Run rendering on Google Cloud
|
||||
|
||||
Use `hyperframes cloudrun` only when the user explicitly wants self-managed Google Cloud infrastructure. It deploys Cloud Run, Workflows, and Cloud Storage. For a managed default use `hyperframes cloud`; for AWS use `hyperframes lambda`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- `gcloud` is authenticated and the target project has billing enabled.
|
||||
- Terraform 1.5 or newer is on `PATH`.
|
||||
- Docker or permission to use Cloud Build is available.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```bash
|
||||
npx hyperframes cloudrun deploy --project <gcp-project> --region us-central1
|
||||
npx hyperframes cloudrun sites create ./project
|
||||
npx hyperframes cloudrun render ./project --width 1920 --height 1080 --wait
|
||||
npx hyperframes cloudrun progress <execution-name>
|
||||
npx hyperframes cloudrun destroy --project <gcp-project>
|
||||
```
|
||||
|
||||
`deploy` enables the required Google APIs, builds or accepts a container image, applies the bundled Terraform module, and stores the resulting coordinates in `~/.hyperframes/cloudrun-state.json`. Use deploy flags such as `--image`, `--repo`, `--cpu`, `--memory`, `--max-instances`, and `--timeout` only when the infrastructure needs those overrides.
|
||||
|
||||
`sites create` uploads a content-addressed project archive for reuse by `render-batch`; pass its `--site-id` to that command to skip the batch upload. Single `cloudrun render` currently resolves the project from its directory and does not consume `--site-id`. Both render commands require `--width` and `--height`; supported output formats are `mp4`, `mov`, `png-sequence`, and `webm`. Use `--output-resolution 4k` to supersample an authored composition without changing its layout dimensions.
|
||||
|
||||
Common render flags are `--fps 24|30|60`, `--quality draft|standard|high`, `--codec h264|h265` for MP4, `--chunk-size`, `--max-parallel-chunks`, `--target-chunk-frames`, `--render-id`, `--output-key`, `--wait`, and `--wait-interval-ms`. Use `--json` for machine-readable output.
|
||||
|
||||
For a variable-driven single render:
|
||||
|
||||
```bash
|
||||
npx hyperframes cloudrun render ./template \
|
||||
--width 1920 --height 1080 \
|
||||
--variables-file ./alice.json \
|
||||
--strict-variables \
|
||||
--wait
|
||||
```
|
||||
|
||||
Use exactly one of `--variables` and `--variables-file`. Read [Variables](../../hyperframes/references/format.md#variables) for the composition-side contract.
|
||||
|
||||
## JSONL batches
|
||||
|
||||
```bash
|
||||
npx hyperframes cloudrun render-batch ./template \
|
||||
--batch ./users.jsonl \
|
||||
--width 1920 --height 1080 \
|
||||
--max-concurrent 10 \
|
||||
--site-id <site-id> \
|
||||
--json
|
||||
```
|
||||
|
||||
Each nonblank line must contain an `outputKey`; `variables` is optional:
|
||||
|
||||
```json
|
||||
{ "outputKey": "renders/alice.mp4", "variables": { "name": "Alice" } }
|
||||
```
|
||||
|
||||
- `--max-concurrent` defaults to `50` and limits in-flight executions. `--max-parallel-chunks` separately limits chunks inside one render.
|
||||
- `--dry-run` parses the file and prints `would-start` rows without starting executions.
|
||||
- The template uploads once unless `--site-id` is supplied.
|
||||
- Per-entry start errors remain visible and make the command exit nonzero.
|
||||
- Do not rely on `--strict-variables` for `cloudrun render-batch`: the current command accepts the flag but does not validate batch rows. Validate the JSONL variable objects against the composition schema before dispatch. The strict gate does work for single `cloudrun render`.
|
||||
|
||||
`render` without `--wait` returns an execution name. Use `cloudrun progress <execution-name>` until it succeeds, then verify the reported GCS output. `destroy` removes the Terraform-managed stack and its scratch bucket; preserve any deliverables that must outlive the stack.
|
||||
@@ -1,105 +0,0 @@
|
||||
# Compare and batch rendering
|
||||
|
||||
Use these commands for deliberate visual comparison or variable-driven template output. They do not replace `lint`, `check`, final preview approval, or output verification.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Compare projects or variants](#compare-projects-or-variants)
|
||||
- [Compare color grades](#compare-color-grades)
|
||||
- [Batch template renders](#batch-template-renders)
|
||||
|
||||
## Compare projects or variants
|
||||
|
||||
Render the same timestamp from two or more project directories or HTML files into one labeled contact sheet:
|
||||
|
||||
```bash
|
||||
npx hyperframes compare <path-a> <path-b> [<path-c> ...] \
|
||||
--at <seconds> \
|
||||
--labels baseline,candidate \
|
||||
--out compare.png \
|
||||
--cols 2
|
||||
```
|
||||
|
||||
Useful options:
|
||||
|
||||
- `--at <seconds>` selects the shared comparison time.
|
||||
- `--labels <a,b,...>` labels cells in input order.
|
||||
- `--out <file>` chooses the sheet path.
|
||||
- `--cols <n>` controls its grid.
|
||||
- `--json` returns machine-readable results.
|
||||
- `--timeout <ms>` changes the per-variant render-ready timeout.
|
||||
|
||||
One sheet accepts at most 16 variants. Extra inputs are truncated with a warning; split larger comparisons into several runs.
|
||||
|
||||
`compare` is a visual review surface, not a quality gate. Run it when checking a baseline against a candidate, comparing implementation variants, or verifying that a repair preserves the intended look. Inspect the generated image; do not treat command success as visual approval.
|
||||
|
||||
## Compare color grades
|
||||
|
||||
Create grade candidates from a source frame:
|
||||
|
||||
```bash
|
||||
npx hyperframes grade-compare \
|
||||
--for frame.png \
|
||||
--grades grades.json \
|
||||
--project . \
|
||||
--out grade-compare.png
|
||||
```
|
||||
|
||||
`grades.json` is an array of labeled HyperFrames grading blocks:
|
||||
|
||||
```json
|
||||
[{ "label": "warm", "grading": { "adjust": { "temperature": 0.2, "contrast": 0.1 } } }]
|
||||
```
|
||||
|
||||
Or compare explicit LUT files:
|
||||
|
||||
```bash
|
||||
npx hyperframes grade-compare \
|
||||
--for source.mp4 \
|
||||
--luts warm.cube,cool.cube \
|
||||
--out grade-compare.png
|
||||
```
|
||||
|
||||
- `--for` accepts an image or a video. For video input, the command extracts the first frame.
|
||||
- Supply exactly one candidate source: `--grades <json>` or `--luts <a.cube,b.cube>`.
|
||||
- A neutral baseline is included by default; pass `--no-baseline` only when the baseline is not a useful reference.
|
||||
- The command accepts at most 16 candidate grades. With the default neutral baseline, the sheet may contain 17 cells. Extra candidates are truncated with a warning; split larger sets into several runs.
|
||||
- `--timeout <ms>` changes the render-ready timeout for the generated comparison composition.
|
||||
- Use `--json` for machine-readable output.
|
||||
|
||||
This command helps select a grade. It does not apply the selected grade to the composition or replace `/media-use` provenance and LUT validation.
|
||||
|
||||
## Batch template renders
|
||||
|
||||
`render --batch` accepts either a JSON array of variable objects or an object with a `rows` array:
|
||||
|
||||
```json
|
||||
{
|
||||
"rows": [
|
||||
{ "name": "alpha", "headline": "Hello" },
|
||||
{ "name": "beta", "headline": "Welcome" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Declare the variables in the composition, then run:
|
||||
|
||||
```bash
|
||||
npx hyperframes render \
|
||||
--batch rows.json \
|
||||
--output "renders/{name}.mp4" \
|
||||
--batch-concurrency 1 \
|
||||
--strict-variables
|
||||
```
|
||||
|
||||
Batch rules:
|
||||
|
||||
- Do not combine `--batch` with `--variables` or `--variables-file`; each row is the variable set for one render.
|
||||
- If `--output` is omitted, the generated filename includes `{index}` so rows remain unique.
|
||||
- Output templates support `{index}` and row keys containing letters, numbers, `_`, `.`, or `-`. A placeholder value must be a string, number, or boolean; `null`, objects, and arrays are invalid. Missing placeholders and output collisions are errors.
|
||||
- `--batch-concurrency` defaults to `1`. Raise it conservatively because each render already uses workers.
|
||||
- `--batch-fail-fast` stops scheduling after the first failure. Without it, independent rows keep running and failures remain visible in the manifest.
|
||||
- `--strict-variables` validates every row before rendering and aborts before output when the declared variable contract is violated.
|
||||
- `--json` emits progress events suitable for agents and CI.
|
||||
|
||||
The command writes `manifest.json` in the common output directory and updates it throughout the run. It records each row's variables, status, output, error, and timing. Completion means the manifest has no failed rows and every completed output exists, is non-empty, and has a plausible duration.
|
||||
@@ -1,17 +0,0 @@
|
||||
# CLI conventions for agents and CI
|
||||
|
||||
- Prefer `--json` for agent and CI calls. Server-mode `render`, `preview`, and `play` do not provide ordinary JSON output; `preview --selection --json` and `preview --context --json` are query-mode exceptions.
|
||||
- `doctor --json` always exits zero. Gate on its payload:
|
||||
|
||||
```bash
|
||||
npx hyperframes doctor --json | jq -e '.ok' >/dev/null
|
||||
```
|
||||
|
||||
- Non-TTY mode is automatic and scaffolds the centered blank. Pass `--example` only to start from a named example. Use `--non-interactive` to force flag-only mode on a TTY.
|
||||
- Use one `HYPERFRAMES_RUN_ID` for all commands in the same verification loop.
|
||||
- When disk is tight, run `npx hyperframes clean` (`--dry-run` to list first); it removes what dead renders left and idle caches that rebuild themselves, never outputs, sources or anything a running render uses. Write disposable QC frames to a temp dir, then remove only that owned directory. Managed snapshot runs retain the newest 10 plus current/latest/rendered evidence; active captures and runs younger than 24 hours are protected. Copy intentional before/after evidence outside managed storage before it ages out. Never delete `.hyperframes` wholesale: it also holds project history.
|
||||
- Use `--strict`, `--strict-all`, and `--strict-variables` when the corresponding warnings, variables, or CI conditions must gate the render.
|
||||
- For `shader_unbounded_cosine_pow`, clamp the cosine to `[0, 1]` before `pow(1 - cosine, exponent)`; a lower-only `max` is insufficient. Then preview and export on the affected GPU to confirm. Lint only recognizes static shader expressions, and software rendering can miss hardware-specific invalid pixels.
|
||||
- JSON paths redact the home directory as `$HOME`; do not try to reverse the redaction.
|
||||
- When a hosted cloud project approaches or exceeds the 200 MB upload limit, use `cloud render --dry-run --json` and follow the `.hyperframesignore` investigation in `references/cloud.md`. Never ignore an asset merely because it is large.
|
||||
- Never render merely because checks pass. Pause at the final preview and wait for approval.
|
||||
@@ -1,80 +0,0 @@
|
||||
# doctor, browser
|
||||
|
||||
Environment diagnosis and bundled-Chrome management. Run these first when a render or preview fails.
|
||||
|
||||
## doctor
|
||||
|
||||
```bash
|
||||
npx hyperframes doctor
|
||||
npx hyperframes doctor --json # CI / agent output (always exit 0; gate on payload `ok`)
|
||||
```
|
||||
|
||||
Runs independent checks and reports each as ok/warn/fail:
|
||||
|
||||
- **Version** — installed CLI vs latest on npm (hints upgrade when stale)
|
||||
- **Node.js** — ≥ 22 required
|
||||
- **CPU**, **Memory**, **Disk** — host resources
|
||||
- **Environment** — env vars that affect the renderer
|
||||
- **FFmpeg** / **FFprobe** — found, version, codecs
|
||||
- **Chrome** — bundled or system, version, path
|
||||
- **Docker** / **Docker running** — required only for `render --docker`
|
||||
- **/dev/shm** — inside containers only
|
||||
|
||||
Run `doctor` first when:
|
||||
|
||||
- `render` fails with a Chrome or FFmpeg error.
|
||||
- `preview` opens but the composition fails to load.
|
||||
- A fresh machine has never run HyperFrames.
|
||||
|
||||
Common issues:
|
||||
|
||||
- **Missing FFmpeg** — install via `brew install ffmpeg` (macOS) or your package manager, or [configure existing binaries](#configure-ffmpeg-without-a-package-manager).
|
||||
- **Missing bundled Chrome** — run `npx hyperframes browser ensure`.
|
||||
- **Low memory** — close other Chromes, reduce `--workers`, or use `--quality draft`.
|
||||
- **Chrome exits instantly inside an agent sandbox (macOS)** — seatbelt-style sandboxes
|
||||
(e.g. codex `workspace-write`) block Chromium's Mach port bootstrap
|
||||
(`MachPortRendezvous`; openai/codex#21292), so every Chrome — bundled, system, or
|
||||
headless shell — dies at startup. This is a host-level block, not a HyperFrames or
|
||||
Chrome install problem: compile checks and audio still pass, only rendering is
|
||||
unavailable. State the blocker and deliver the checked composition; render outside the
|
||||
sandbox or via `render --docker` / cloud rendering where available. **Do not build a
|
||||
substitute rasterizer** (magick/PIL/SVG frame pipelines) — on a blocked-browser host
|
||||
the deliverable IS the checked composition plus this blocker note, and rendering is
|
||||
handed to `--docker`, cloud, or the user. Write your final summary the moment the
|
||||
blocker is identified, BEFORE any optional fallback work: a later session failure must
|
||||
not erase the report of work already done.
|
||||
|
||||
### Configure FFmpeg without a package manager
|
||||
|
||||
Get FFmpeg and FFprobe binaries for your operating system and architecture from the [FFmpeg download page](https://ffmpeg.org/download.html). Both must be executable. You can use them without Homebrew or a system-wide installation:
|
||||
|
||||
```bash
|
||||
export HYPERFRAMES_FFMPEG_PATH="/absolute/path/to/ffmpeg"
|
||||
export HYPERFRAMES_FFPROBE_PATH="/absolute/path/to/ffprobe"
|
||||
npx hyperframes doctor --json
|
||||
```
|
||||
|
||||
Set both variables in the environment that launches HyperFrames. On Windows, point them at `ffmpeg.exe` and `ffprobe.exe` using your shell's environment-variable syntax. A configured path takes precedence over automatic discovery; a missing configured file is reported as missing.
|
||||
|
||||
Alternatively, put the binaries in `.hyperframes/bin/ffmpeg` and `.hyperframes/bin/ffprobe` (`.exe` on Windows), then run HyperFrames from that project's directory. Discovery checks `PATH` before this project-local directory, so use the environment variables when you need a particular build.
|
||||
|
||||
Verify the **FFmpeg** and **FFprobe** entries in `doctor --json` report `ok: true` and the expected paths. To gate only these encoding dependencies with `jq`:
|
||||
|
||||
```bash
|
||||
npx hyperframes doctor --json | jq -e \
|
||||
'[.checks[] | select(.name == "FFmpeg" or .name == "FFprobe")] | length == 2 and all(.ok)'
|
||||
```
|
||||
|
||||
The report's top-level `ok` also includes optional tools such as Docker and local voice providers; those failures do not require installing them for a local video render. Check Chrome separately with the browser commands below.
|
||||
|
||||
## browser
|
||||
|
||||
```bash
|
||||
npx hyperframes browser ensure # find or download the pinned Chrome
|
||||
npx hyperframes browser path # print the browser executable path (for scripting)
|
||||
npx hyperframes browser clear # remove the cached Chrome download
|
||||
```
|
||||
|
||||
Manage the Chrome build HyperFrames uses for rendering. The pinned version exists because pixel output drifts across Chrome versions — using the bundled build keeps rendered output reproducible across machines.
|
||||
|
||||
Use `path` to embed the binary in scripts: `$(npx hyperframes browser path)`.
|
||||
@@ -1,14 +0,0 @@
|
||||
# Feedback reports
|
||||
|
||||
After verifying a successful render, send one feedback report unless telemetry is disabled or the user opted out:
|
||||
|
||||
```bash
|
||||
npx hyperframes feedback --rating <0-10> --comment "<specific result or friction>"
|
||||
```
|
||||
|
||||
Keep clean-run feedback concise. For any bug or friction, capture a **reproduction packet** before submitting; do not send only a symptom summary. Include the rerunnable command (relative to the project directory — feedback is submitted to a public channel, so do **not** paste absolute paths, home-directory prefixes, or user/machine identifiers), expected versus actual behavior, exact error (also strip absolute paths from stack traces — keep basename + line, drop the leading directory), whether output completed/fell back/failed, workaround, and repro-project status. For a rating ≤ 7 that describes a visual defect (black frame, flicker, corrupt output, wrong frame, blank output, other visual anomaly), also include a `COMPOSITION_STRUCTURE:` block — a privacy-preserving structural anatomy (element census + attribute presence + timeline shape) so maintainers can pattern-match against known bug families without the composition ZIP. Agents auto-fill this via the composition-census helper; the human user does not fill it by hand. If the issue did not reproduce again, say so and still include the last failing command and logs. Use `--file-issue` only with consent: it publishes a minimal reproduction to a public URL. The required packet format and privacy warning live in `references/preview-render.md`.
|
||||
|
||||
Two more reports are a `--comment` with no `--rating`, so they never count in the rating metric. Send them unless telemetry is disabled or the user opted out, under the same privacy rules:
|
||||
|
||||
- **Missing feature.** Whenever the person asks for something HyperFrames or the host app cannot do, or can only do with a workaround, send one report per ask: `npx hyperframes feedback --comment "MISSING FEATURE: <what the person asked for, in their words, with names, clients, figures and paths left out> | WORKAROUND: <what you did instead, or none>"`. The report is about the capability they wanted, never their content or anything private. A catalog search that found nothing goes to `--search-miss` instead.
|
||||
- **Host app.** When `HYPERFRAMES_CLIENT` is set, the CLI is running inside a host app. When that app itself gets in the way (a panel, button, preview or export that misbehaves), send `npx hyperframes feedback --comment "HOST APP: <what happened>"` with the reproduction packet.
|
||||
@@ -1,78 +0,0 @@
|
||||
# init, capture, skills
|
||||
|
||||
<!-- registry-items: allow=blank,landscape-4k,portrait-4k,square-4k,media-use -->
|
||||
|
||||
Scaffolding commands. Use these instead of creating files by hand — they set up the right file structure, copy media, run transcription, and install AI coding skills.
|
||||
|
||||
## init
|
||||
|
||||
```bash
|
||||
npx hyperframes init my-video # centered blank (TTY: wizard)
|
||||
npx hyperframes init my-video --example warm-grain # pick an example
|
||||
npx hyperframes init my-video --resolution portrait
|
||||
npx hyperframes init my-video --video clip.mp4 # with video file
|
||||
npx hyperframes init my-video --audio track.mp3 # with audio file
|
||||
npx hyperframes init my-video --tailwind # Tailwind v4 browser runtime
|
||||
npx hyperframes init my-video --non-interactive # CI — flag-only, same blank
|
||||
```
|
||||
|
||||
**Default depends on TTY**: in a terminal, the CLI prompts for example/options (default: centered blank). Outside a TTY (CI, agents, piped output) it auto-switches to non-interactive and scaffolds that blank. Pass `--example` only to start from a named example. Pass `--non-interactive` to force flag-only mode on a TTY.
|
||||
|
||||
Templates: `blank`, `warm-grain`, `play-mode`, `swiss-grid`, `vignelli`, `decision-tree`, `kinetic-type`, `product-promo`, `nyt-graph`. (The closed set of `hyperframes:example` items in `registry/registry.json` plus the bundled `blank` template. `hyperframes catalog` does not list examples — its `--type` takes only `block` or `component` — so this list has no live equivalent and is checked by `bun run lint:skills`.)
|
||||
|
||||
Other useful flags:
|
||||
|
||||
- `--resolution` — preset: `landscape` (1920×1080), `portrait` (1080×1920), `landscape-4k`, `portrait-4k`, `square` (1080×1080), `square-4k`. Aliases: `1080p`, `4k`, `uhd`, `1080p-square`, `4k-square`.
|
||||
- `--skill=<slug>` — record the owning skill (e.g. `hyperframes`) in `hyperframes.json`, so every later render of this project — re-renders, `npm run render`, `--batch` — is attributed to it on anonymous telemetry without re-passing the flag. You rarely pass it by hand.
|
||||
- `--skip-skills` — **temporarily ignored**: `init` always checks AI coding skills against GitHub while the skills.sh registry catches up. To opt out (CI/tests), set the `HYPERFRAMES_SKIP_SKILLS=1` env var instead.
|
||||
- `--skip-transcribe` — don't auto-transcribe `--audio` / `--video` with Whisper.
|
||||
- `--model`, `--language` — Whisper model / language for the auto-transcription.
|
||||
|
||||
**Tailwind** (`--tailwind`): the scaffold pins the Tailwind v4 browser runtime, not Studio's Tailwind v3 setup.
|
||||
|
||||
- Keep the pinned runtime; never swap in `cdn.tailwindcss.com`. Renders wait for `window.__tailwindReady` before frame 0. For offline or locked-down renders, compile to CSS and ship the stylesheet.
|
||||
- Theme and custom utilities go in `<style type="text/tailwindcss">` with `@theme` and `@utility`. No v3 `@tailwind base/components/utilities`, no `tailwind.config.js`; `@config` and `@plugin` abort the browser compile.
|
||||
- Write complete class tokens in the HTML. The runtime only sees classes it can scan, so never build a render-critical class name at seek time (`bg-${color}-500`); vary items through CSS variables (`style="--i: 2"`) or data variants.
|
||||
- Tailwind is for static layout and style; timing stays in GSAP or another seekable runtime.
|
||||
|
||||
When `--audio` or `--video` is supplied, `init` transcribes the file with Whisper. For voice/model selection see the `media-use` skill.
|
||||
|
||||
## capture
|
||||
|
||||
```bash
|
||||
npx hyperframes capture https://stripe.com # scaffold from a website
|
||||
npx hyperframes capture https://linear.app -o linear-video # custom output directory
|
||||
npx hyperframes capture https://example.com --json # JSON output for agents
|
||||
npx hyperframes capture https://example.com --skip-assets # skip image/SVG download
|
||||
npx hyperframes capture https://example.com --skip-vision # skip optional AI captions
|
||||
npx hyperframes capture https://example.com --max-screenshots 12
|
||||
npx hyperframes capture https://example.com --timeout 60000 # page-load timeout in ms
|
||||
npx hyperframes capture https://example.com --capture-budget 90000 # post-navigation budget
|
||||
```
|
||||
|
||||
Captures a live URL as an editable HyperFrames project: screenshots become layered scenes, assets are downloaded locally, and the result is a normal project you can `lint` / `preview` / `render`. Use this when the user supplies a URL as the starting point for a video.
|
||||
|
||||
`--timeout` bounds page navigation; `--capture-budget` is the separate cooperative budget for work
|
||||
after navigation (fonts, assets, vision, and contact sheets). The latter is not a hard wall-clock
|
||||
watchdog and cannot interrupt native work already in flight. An outer caller deadline is therefore a
|
||||
third, distinct timeout. An outer caller timeout leaves the capture result unknown; it does not prove
|
||||
HyperFrames hung or that the navigation timeout should be increased. Preserve the last phase and
|
||||
classify the boundary that fired. `--skip-vision` disables only optional AI image captioning.
|
||||
|
||||
For agents, use `--json`. The result includes `ok`, warnings, and `lastPhase`. The command also emits
|
||||
stable `HYPERFRAMES_CAPTURE_PHASE` records so a watchdog can report the last started, completed, or
|
||||
degraded phase without retaining sensitive payloads.
|
||||
|
||||
Treat a non-zero exit, JSON `ok: false`, or an output `BLOCKED.md` as a **hard stop**. Do not render,
|
||||
build, or infer brand/design data from partial files in a blocked capture. A successful capture may
|
||||
degrade an optional phase within budget, but its structural output still has to be usable. Exit zero and file existence alone are not semantic success: require the current
|
||||
invocation's JSON `ok: true`, no `BLOCKED.md`, and artifacts usable for the video. Run each retry
|
||||
into a fresh output directory; never merge or reuse a blocked attempt's partial output.
|
||||
|
||||
## skills
|
||||
|
||||
```bash
|
||||
npx hyperframes skills # install HyperFrames skills for AI coding tools
|
||||
```
|
||||
|
||||
One-time setup that adds the HyperFrames skill pack (/hyperframes with its references, /hyperframes-cli, /hyperframes-registry, /media-use, /figma, /slideshow, /remotion-to-hyperframes) to the local AI coding environment so agents follow the framework conventions. Re-run after major HyperFrames upgrades.
|
||||
@@ -1,185 +0,0 @@
|
||||
# Lambda rendering on AWS
|
||||
|
||||
Use `hyperframes lambda` when the user explicitly wants self-managed AWS infrastructure or needs distributed rendering. It wraps `@hyperframes/aws-lambda` and AWS SAM.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Choose Lambda or local rendering](#choose-lambda-or-local-rendering)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Deploy](#deploy)
|
||||
- [Upload a reusable site](#upload-a-reusable-site)
|
||||
- [Render one composition](#render-one-composition)
|
||||
- [Render a JSONL batch](#render-a-jsonl-batch)
|
||||
- [Inspect progress](#inspect-progress)
|
||||
- [Destroy the stack](#destroy-the-stack)
|
||||
- [IAM policies](#iam-policies)
|
||||
- [State, cost, and cleanup](#state-cost-and-cleanup)
|
||||
|
||||
The basic lifecycle is:
|
||||
|
||||
```bash
|
||||
npx hyperframes lambda deploy
|
||||
npx hyperframes lambda render ./my-project --width 1920 --height 1080 --wait
|
||||
npx hyperframes lambda destroy
|
||||
```
|
||||
|
||||
## Choose Lambda or local rendering
|
||||
|
||||
- **Local `render`** — dev-loop iteration, single host, anything under a few minutes at 1080p.
|
||||
- **`lambda render`** — long videos, 4K, large parallel batches, or anything where local Chrome would time out / exhaust RAM. Pay-per-invocation, no idle cost.
|
||||
|
||||
For one-off short renders Lambda is not worth the deploy overhead.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- AWS credentials configured (env vars, `~/.aws/credentials`, SSO, or IMDS).
|
||||
- AWS SAM CLI on `PATH`.
|
||||
- `bun` on `PATH` (builds the Lambda handler ZIP).
|
||||
|
||||
## Deploy
|
||||
|
||||
```bash
|
||||
npx hyperframes lambda deploy \
|
||||
--stack-name=hyperframes-prod \
|
||||
--region=us-east-1 \
|
||||
--concurrency=8 \
|
||||
--memory=10240
|
||||
```
|
||||
|
||||
Builds `packages/aws-lambda/dist/handler.zip` and SAM-deploys the stack (Lambda + Step Functions + S3 + IAM). Idempotent — re-running on the same `--stack-name` is a no-op when nothing changed. Writes `<cwd>/.hyperframes/lambda-stack-<name>.json` so later subcommands don't need to call `describe-stacks`.
|
||||
|
||||
| Flag | Default | Description |
|
||||
| ----------------- | ------------------------------- | -------------------------------------- |
|
||||
| `--stack-name` | `hyperframes-default` | CloudFormation stack name |
|
||||
| `--region` | `AWS_REGION` env or `us-east-1` | AWS region |
|
||||
| `--profile` | `AWS_PROFILE` env | Named AWS credentials profile |
|
||||
| `--concurrency` | `8` | Lambda reserved concurrency |
|
||||
| `--chrome-source` | `sparticuz` | `sparticuz` or `chrome-headless-shell` |
|
||||
| `--memory` | `10240` | Lambda memory in MB |
|
||||
| `--skip-build` | off | Reuse existing `handler.zip` |
|
||||
|
||||
## Upload a reusable site
|
||||
|
||||
```bash
|
||||
npx hyperframes lambda sites create ./my-project
|
||||
# → siteId: abc1234deadbeef0 (stable across re-runs of the same tree)
|
||||
|
||||
npx hyperframes lambda render ./my-project --site-id=abc1234deadbeef0 ...
|
||||
```
|
||||
|
||||
Tars + uploads `<projectDir>` to S3 with a content-addressed key. Returns a stable `siteId` you can reuse — re-renders of the same tree skip the upload.
|
||||
|
||||
## Render one composition
|
||||
|
||||
```bash
|
||||
npx hyperframes lambda render ./my-project \
|
||||
--width 1920 --height 1080 --fps 30 --format mp4 \
|
||||
--chunk-size 240 --max-parallel-chunks 16 \
|
||||
--wait
|
||||
```
|
||||
|
||||
Starts a Step Functions execution. Returns immediately with a `renderId` unless `--wait` is set, in which case the CLI blocks until completion and streams per-chunk progress lines. Add `--json` for machine-parseable output.
|
||||
|
||||
| Flag | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--width` / `--height` | Output dimensions in pixels |
|
||||
| `--output-resolution` | Supersampling preset (engages Chrome `deviceScaleFactor`) — `landscape` / `landscape-4k` / `portrait` / `portrait-4k` / `square` / `square-4k`, plus aliases (`1080p`, `4k`, `uhd`, `hd`, `1080p-portrait`, `4k-portrait`, `1080p-square`, `4k-square`). Use this to render an authored-at-1080p composition at 4K without re-laying-out — see footgun below. |
|
||||
| `--fps` | `24` / `30` / `60` |
|
||||
| `--format` | `mp4` / `mov` / `png-sequence` / `webm` (default `mp4`) |
|
||||
| `--codec` | `h264` / `h265` (mp4 only) |
|
||||
| `--quality` | `draft` / `standard` / `high` |
|
||||
| `--chunk-size` | Frames per chunk (default `240`) |
|
||||
| `--max-parallel-chunks` | Max concurrent chunks (default `16`) |
|
||||
| `--target-chunk-frames` | Cap frames per chunk and let the planner add chunks up to the parallel limit |
|
||||
| `--site-id` | Reuse an existing site (skip upload) |
|
||||
| `--execution-name` | Explicit Step Functions execution name |
|
||||
| `--output-key` | Explicit final S3 object key |
|
||||
| `--variables` | Inline JSON object with composition variable values |
|
||||
| `--variables-file` | JSON file containing one composition variable object |
|
||||
| `--strict-variables` | Fail when supplied variables are undeclared or have the wrong type |
|
||||
| `--wait` | Block until completion, stream progress |
|
||||
| `--wait-interval-ms` | Poll cadence while waiting (default `5000`) |
|
||||
| `--json` | Machine-parseable progress snapshot |
|
||||
|
||||
**`--width` / `--height` footgun.** Setting `--width 3840 --height 2160` against a composition whose `data-width="1920"` silently produces 1080p — the runtime lays out the page at the composition's authored dimensions and the CLI flags are ignored for layout. To actually output at 4K, use `--output-resolution 4k` (supersamples via `deviceScaleFactor`). The CLI now prints a warning when CLI dimensions disagree with the composition's `data-width` / `data-height` and `--output-resolution` is not set; the warning is suppressed when `--json` is on or `index.html` isn't on disk (`--site-id` flows).
|
||||
|
||||
For variable-driven templates, declare the schema in the composition and pass either `--variables` or `--variables-file`, never both. `--strict-variables` checks local project input before any render starts. Also read [Variables](../../hyperframes/references/format.md#variables).
|
||||
|
||||
## Render a JSONL batch
|
||||
|
||||
Use `render-batch` to upload one template once and start one Step Functions execution per nonblank JSONL line:
|
||||
|
||||
```bash
|
||||
npx hyperframes lambda render-batch ./template \
|
||||
--batch ./users.jsonl \
|
||||
--width 1920 --height 1080 \
|
||||
--max-concurrent 10 \
|
||||
--strict-variables \
|
||||
--json
|
||||
```
|
||||
|
||||
Each line must be an object with a non-empty `outputKey`. Choose unique keys to prevent outputs from overwriting one another. `variables` and `executionName` are optional:
|
||||
|
||||
```json
|
||||
{
|
||||
"outputKey": "renders/alice.mp4",
|
||||
"variables": { "name": "Alice" },
|
||||
"executionName": "alice-video"
|
||||
}
|
||||
```
|
||||
|
||||
Batch rules:
|
||||
|
||||
- The project is uploaded once unless `--site-id` reuses an earlier upload.
|
||||
- `--max-concurrent` defaults to `50` and limits in-flight render executions. `--max-parallel-chunks` separately limits chunks inside each render.
|
||||
- `--strict-variables` checks every entry, reports all variable issues, and aborts before AWS calls.
|
||||
- `--dry-run` performs no upload or AWS render call. Every manifest row becomes `would-invoke`.
|
||||
- The emitted manifest preserves input order and records `inputLine`, `outputKey`, `executionArn`, and `status` (`started`, `would-invoke`, or `failed-to-start`), plus an error when applicable.
|
||||
- A per-entry start failure does not hide other rows. Human-output mode exits nonzero when a row fails to start. In `--json` mode the current CLI prints the manifest and exits zero, so gate on every row's `status`, not the process code alone. Dispatch success is not render completion; inspect each execution with `progress`.
|
||||
|
||||
## Inspect progress
|
||||
|
||||
```bash
|
||||
npx hyperframes lambda progress hf-render-abcd1234
|
||||
npx hyperframes lambda progress arn:aws:states:us-east-1:...:execution:...
|
||||
```
|
||||
|
||||
Prints one snapshot — overall percent, frames rendered, Lambda invocations, accrued cost, and any errors. Accepts a bare `renderId` (resolved against the stack's state-machine ARN) or a full SFN execution ARN.
|
||||
|
||||
## Destroy the stack
|
||||
|
||||
```bash
|
||||
npx hyperframes lambda destroy
|
||||
```
|
||||
|
||||
Calls `sam delete --no-prompts` and drops the local state file. **The render S3 bucket is configured `Retain`** so it survives stack destruction — empty + delete it via the AWS console / CLI if you want the storage back.
|
||||
|
||||
### Non-retryable errors
|
||||
|
||||
A subset of failures the Step Functions state machine short-circuits instead of running through its 4× 15-min retry budget. `progress` surfaces these immediately with the error class name; do not re-issue `lambda render` blindly when you see one.
|
||||
|
||||
- **`ChromeBinaryUnavailableError`** — `@sparticuz/chromium` returned an empty/missing executable path. A prior chunk hit `Sandbox.Timedout` mid-extraction and the warm instance is wedged until the execution environment recycles. Remedy: bump a Lambda env var (forces a new exec env) or `lambda deploy` again. Not a transient render failure; retries will burn budget on the same wedged instance.
|
||||
- **`FFMPEG_VERSION_MISMATCH`** / **`PLAN_HASH_MISMATCH`** — planner / executor version drift. Re-deploy.
|
||||
|
||||
## IAM policies
|
||||
|
||||
Print or validate the minimum IAM permissions the CLI needs.
|
||||
|
||||
```bash
|
||||
npx hyperframes lambda policies user # inline policy for an IAM user
|
||||
npx hyperframes lambda policies role # { TrustRelationship, InlinePolicy }
|
||||
npx hyperframes lambda policies validate ./infra/iam/hf-deploy.json # CI gate
|
||||
```
|
||||
|
||||
`validate` reads a JSON policy doc and checks the union of its `Effect: Allow` actions (expanding `s3:*` / `s3:Get*` / `*` wildcards) against the CLI's required action set. Missing actions print to stderr; the command exits non-zero. Wire it into CI to catch policy drift before the next deploy fails.
|
||||
|
||||
The default action set is deliberately broad (`Resource: "*"`) because CloudFormation creates new ARNs on every adopter's first deploy. Tighten `Resource` after that first run if security posture requires it.
|
||||
|
||||
## State, cost, and cleanup
|
||||
|
||||
`hyperframes lambda` stores per-stack metadata under `<cwd>/.hyperframes/lambda-stack-<name>.json` (bucket name, state-machine ARN, region). Not secret, but AWS-account-identifying. Commit it to a repo or `.gitignore` it per your workflow.
|
||||
|
||||
- `lambda destroy` removes the SAM stack but **leaves the S3 bucket** (`Retain`). Delete it manually if you want the storage back.
|
||||
- Lambda billing is per-invocation + duration. `progress` reports the accrued cost.
|
||||
- `--concurrency` caps parallel Lambda invocations — keep it aligned with your account quota.
|
||||
- `--chunk-size` and `--max-parallel-chunks` trade off per-chunk overhead against parallelism; larger chunks reduce coordinator overhead, smaller chunks parallelize more aggressively.
|
||||
@@ -1,155 +0,0 @@
|
||||
# lint, check, snapshot
|
||||
|
||||
Use `lint` for fast static feedback while iterating. Use `check` as the required final gate: it reruns the same linter, then audits runtime, layout, motion, and contrast in one browser session. Do not chain a redundant standalone `lint` immediately before `check`. `snapshot` is the standalone utility for capturing still frames and zoomed crops. `validate`, `inspect`, and `layout` still run but are deprecated: `check` covers all of them in one invocation.
|
||||
|
||||
## Discipline (motion-heavy work)
|
||||
|
||||
When the composition is animation-driven, run the checks before you reach for `preview` or `render`:
|
||||
|
||||
- Run `lint` after the first HTML pass for early feedback. It is an iteration aid, not a separate final gate.
|
||||
- Run `check --snapshots` at the first full pass: the overview frames and per-finding crops show you what the auditor saw.
|
||||
- Look at the PNGs before tuning automated warnings: your eye catches what the auditor misses, and the auditor catches what your eye misses.
|
||||
- Treat layout errors as defects unless a snapshot proves the layering is intentional, in which case mark it with `data-layout-allow-overflow` / `data-layout-allow-overlap` / `data-layout-allow-occlusion` / `data-layout-allow-caption-zone` (caption band only).
|
||||
- State motion intent in a `*.motion.json` sidecar so `check` verifies it automatically (entrances firing under seek, stagger order, in-frame, liveness). This is the closest automated proxy for "watch the MP4" and catches render-vs-preview bugs the eye misses (see **Motion verification** below).
|
||||
|
||||
## lint
|
||||
|
||||
```bash
|
||||
npx hyperframes lint # current directory
|
||||
npx hyperframes lint ./my-project # specific project
|
||||
npx hyperframes lint --verbose # info-level findings
|
||||
npx hyperframes lint --json # machine-readable
|
||||
```
|
||||
|
||||
Lints `index.html` and all files in `compositions/`. Reports errors (must fix), warnings (should fix), and info (with `--verbose`). Catches missing `data-composition-id`, overlapping tracks on the same `data-track-index`, unregistered timelines, and GSAP/CSS transform conflicts.
|
||||
|
||||
`<video>`/`<audio>` work at any nesting depth, including inside a `compositions/*.html` sub-composition or a wrapper `<div>`: the runtime discovers media with a flat DOM query and seeks/decodes it wherever it lives (`packages/core/src/runtime/{media,startResolver}.ts`). After a render, `snapshot` each scene that has a video and confirm the panel actually shows footage (a blank/black panel where a clip should play is a real bug, not a placeholder).
|
||||
|
||||
## check
|
||||
|
||||
```bash
|
||||
npx hyperframes check # current directory: the full browser gate
|
||||
npx hyperframes check ./my-project # specific project
|
||||
npx hyperframes check --json # agent-readable envelope {ok, browserSkipped, lint, runtime, layout, motion, contrast, craft, hdr, snapshots}
|
||||
npx hyperframes check --snapshots # also write overview frames (annotated) + per-finding crops
|
||||
npx hyperframes check --samples 15 # denser timeline sweep (default 9)
|
||||
npx hyperframes check --at 1.5,4,7.25 # explicit hero-frame timestamps
|
||||
npx hyperframes check --at-transitions # also sample every tween start/end boundary
|
||||
npx hyperframes check --tolerance 4 # allowed overflow px before reporting (default 2)
|
||||
npx hyperframes check --timeout 30000 # initial render-ready + navigation minimum in ms (defaults: 3000 / 10000)
|
||||
npx hyperframes check --no-contrast # skip the WCAG audit while iterating
|
||||
npx hyperframes check --strict # exit non-zero on warnings too (default: only errors)
|
||||
```
|
||||
|
||||
One command, one Chrome boot. `check` runs the linter first and skips the browser entirely when lint reports errors. When the browser never ran (lint errors, a linter crash, or a browser launch failure), `browserSkipped` is `true` and the `layout`, `motion` and `contrast` sections are empty, not clean. Otherwise it loads the bundled composition once, wires runtime listeners before navigation, and sweeps one seek grid running every audit per sample:
|
||||
|
||||
- **Runtime**: JavaScript console errors, unhandled exceptions, failed network requests (media-file `ERR_ABORTED` filtered out), HTTP 4xx/5xx.
|
||||
- **Layout**: text extending outside its container or the canvas, text clipped by its own box, held text overlaps and occlusion (with an approximate covered fraction), children escaping clipping containers. Text under a transparent `<video>` (a `remove-background` cutout) is measured per word against the frame's alpha: partial cover is info with the share hidden, a word 80% hidden or two neighbouring words each half hidden is an error, and a frame that cannot be read falls back to opaque with a `video_alpha_unread` note.
|
||||
- **Motion**: `*.motion.json` sidecar assertions against the same seeked timeline (see below).
|
||||
- **Contrast**: WCAG AA on visible text, sampled at 5 grid points. Failures are **errors** and each finding carries the sampled fg/bg colors, measured vs required ratio, and a suggested compliant color in the same palette direction, so most contrast fixes need no screenshot at all.
|
||||
- **Craft**: warnings only, even under `--strict` (a film may break one on purpose), measured on an 8 fps sweep of the whole film and skipped under `--range`. Each message names the fix and the reference to read.
|
||||
- `line_unread`: a reading-size line (≥ 30 px tall at 1080) on screen under its floor: 0.4 s for 1–2 words, otherwise ¾ of 0.5 s + 0.3 s a word (3 words 1.05 s, 6 words 1.73 s); the film's last line needs 2 s. Edges are re-measured to ~0.01 s. Captions-track text and `data-layout-ignore` are exempt.
|
||||
- `dead_stretch`: more than 3 s where the `keepsMoving` classifier sees nothing change. The final 3 s and any time a `data-hold` element is on screen are exempt; it stays quiet when `sweep_static` fires or a whole-composition `keepsMoving` assertion sets the limit.
|
||||
- `text_below_floor`: text under 2.6% of the frame's short side (28 px on 1080) held 0.5 s or more, one finding per file listing every element.
|
||||
- `first_frame_empty`: frame 0 paints nothing but the background (no text, image, video, canvas or svg); it is the thumbnail on social players.
|
||||
- `film_silent`: a film over 15 s with no unmuted `<audio>` or audible `<video>`.
|
||||
|
||||
Every finding carries a selector, the element's `data-*` identity, the composition source file, a bbox, and the sample time: jump straight from the JSON to the HTML you must edit and re-run.
|
||||
|
||||
**Severity is persistence-aware.** A dynamic issue observed at a single grid sample (an entrance/exit transient) demotes to info and never gates. Issues held across samples gate the exit code, a held `content_overlap` is an error, and a held, partially-visible `canvas_overflow` breaching ≥5% of the canvas promotes to warning. Coordinate-frame findings (`escaped_container`, `panel_out_of_canvas`, `connector_detached`) flag geometry computed in one frame but rendered in another — an element far outside its offset parent, a painted panel stuck across the canvas edge, a connector line detached from every node. Text drawn into a `<canvas>` has no DOM box, so `canvas_overflow` cannot see it; `canvas_content_at_edge` warns when a canvas's pixels show sharp content (drawn text, hard shapes) along the frame edge — mark intentional full-bleed art (particles, photos) with `data-layout-allow-overflow`. If a 3s+ composition shows no visible change across every sample, `check` fails with `sweep_static`: a frame that never changes while a GSAP timeline or tween, or a CSS or Web animation, holds the same time without ever finishing means the seek is not driving the page, so it refuses to pass. Any other frame that never changes (a still title card, an audio-only advance) is a warning that `--strict` fails; a composition meant to be still can take `data-no-timeline` on its root to skip it. The fingerprint includes per-element opacity, so opacity-only reveals (code typing, staggered fades) count as motion — but only while they're still in flight at the sampled times. The classic trap is a reveal that completes early and then holds a static frame for the rest of the duration: every sample lands on the settled state and check warns that nothing on screen moved (`--strict` fails it). Spread the reveal across the timeline or give the hold a real next action (the cursor heading to the next control, the camera's next move); don't bolt on a slow drift, a blinking caret or a loop just to appease the check.
|
||||
|
||||
**Escape hatches** (mark intent in the HTML, then re-run):
|
||||
|
||||
- `data-layout-allow-overflow` — overflow is intentional (entrance/exit travel).
|
||||
- `data-layout-allow-overlap` — deliberate text layering (e.g. a demo cursor label over a heading). Applies only to the marked text block; it is not inherited. Mark the specific layering participant, never a scene/root wrapper, so unrelated descendant collisions remain auditable.
|
||||
- `data-layout-allow-occlusion` — an element is meant to cover text.
|
||||
- `data-layout-allow-caption-zone` — intentional lower-third / caption-band copy under `--caption-zone`. Applies to the marked element and every descendant (`closest`); silences only `caption_zone_collision` (not overflow/overlap/occlusion). Prefer the narrowest wrapper that owns the intentional band copy.
|
||||
- `data-layout-ignore` — decorative element that should never be audited.
|
||||
- `data-hold` — deliberate stillness: while this element is on screen, `dead_stretch` does not count the time.
|
||||
|
||||
**Opt-in pipeline gates** (used by orchestrators; off by default):
|
||||
|
||||
```bash
|
||||
npx hyperframes check --caption-zone "x0=0;y0=.82;x1=1;y1=1;severity=error;seek=.25,1"
|
||||
npx hyperframes check --frame-check # media (img/svg/video/canvas) out-of-frame detection
|
||||
```
|
||||
|
||||
`--caption-zone` takes fractional band geometry (`x0/y0/x1/y1` required, 0-1 fractions of the composition's own canvas, portrait included) with optional `severity` and comma-separated `seek` fractions; it flags a text element's DOM box (`getBoundingClientRect`) that overlaps the band. Waive intentional lower-third copy with `data-layout-allow-caption-zone` on the element or its nearest wrapper (see Escape hatches). `--frame-check` reports media elements breaching the canvas beyond `max(120px, 6% of the min canvas dimension)`.
|
||||
|
||||
**Fixing contrast errors** — thresholds are 4.5:1 for normal text, 3:1 for large text (24px+, or 19px+ bold). The finding's `suggestedColor` already picks the nearest compliant color in the right direction (brighten on dark backgrounds, darken on light); apply it or adjust within the palette family, then re-run `check`.
|
||||
|
||||
## Motion verification (`*.motion.json` sidecar)
|
||||
|
||||
`check` verifies **motion intent** against the same seeked timeline the renderer uses — the closest automated proxy for "render the MP4 and watch it". It catches render-vs-preview bugs layout sampling can't: an entrance reveal the seek lands past, a broken stagger order, an element drifting off-frame mid-tween, a frozen shot.
|
||||
|
||||
Drop a `*.motion.json` sidecar next to the composition (matching the html basename when several compositions share a dir). `check` discovers it automatically — no flag, no authoring-framework changes. With no sidecar, `check` behaves exactly as before.
|
||||
|
||||
```json
|
||||
{
|
||||
"duration": 6,
|
||||
"assertions": [
|
||||
{ "kind": "appearsBy", "selector": "#headline", "bySec": 0.5 },
|
||||
{ "kind": "before", "a": "#headline", "b": "#cta" },
|
||||
{ "kind": "staysInFrame", "selector": ".card" },
|
||||
{ "kind": "keepsMoving", "withinSelector": ".scene" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Assertion | Fails (code) when |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `appearsBy(selector, bySec)` | not visible (opacity ≥ 0.5) by `bySec` — `motion_appears_late` |
|
||||
| `before(a, b)` | `a` does not first appear strictly before `b` — `motion_out_of_order` |
|
||||
| `staysInFrame(selector)` | once visible, its box leaves the canvas — `motion_off_frame` |
|
||||
| `keepsMoving(withinSelector?)` | a fully-static window exceeds `maxStaticSec` (default 2s); the film's final 3 s (an end card) never count — `motion_frozen` |
|
||||
|
||||
`duration`, `withinSelector`, and `maxStaticSec` are optional. Findings are **errors by default** and appear in the same human and `--json` output as layout findings. A selector that matches nothing is reported as `motion_selector_missing` rather than silently passing — a typo'd selector fails loudly. Use this in the feedback loop instead of eyeballing the render: assert what the motion is supposed to do, and let `check` tell you when the seek diverges from intent.
|
||||
|
||||
## snapshot
|
||||
|
||||
```bash
|
||||
npx hyperframes snapshot # 5 key frames as PNG
|
||||
npx hyperframes snapshot ./my-project # specific project
|
||||
npx hyperframes snapshot --frames 10 # evenly-spaced N frames
|
||||
npx hyperframes snapshot --range 3:5 --frames 8 # 8 frames within root-timeline seconds 3–5
|
||||
npx hyperframes snapshot --composition intro --frames 8 # 8 frames within this data-composition-id
|
||||
npx hyperframes snapshot --review # beat sheet, seam strips, phone sheet
|
||||
npx hyperframes snapshot --review --json # same, with paths and times as JSON
|
||||
```
|
||||
|
||||
Still PNGs. Each run writes `.hyperframes/artifacts/snapshots/<id>/` unless `--output` is set. `snapshot` is the standalone capture; `check --snapshots` is the gate.
|
||||
|
||||
One of `--at`, `--range START:END` (seconds), or `--composition <data-composition-id>` (not a CSS selector). `--frames` defaults to 5. Range and composition sample just before the end and add no extra end frame; `--at` adds one unless `--no-end`.
|
||||
|
||||
`--review` picks the frames that show a film over time. Scenes are the composition's top-level timed clips and sub-composition hosts, as `timeline` lists them (captions and audio excluded); `--composition` scopes to one. It writes `review-beats.jpg` (each scene at its start + 0.3 s and at its middle, and each timed `data-beat="name"` element at its start + 0.3 s, labelled with time and id), `review-seam-NN.jpg` (frames at −0.2, −0.1, 0, +0.1, +0.2 s around every cut) and `review-phone.jpg` (the beat frames 360 px wide, to judge legibility at phone size), beside the `beat-*.png` and `seam-*.png` stills. With fewer than two scenes the beats spread evenly. `--json` prints `{ok, outputDir, frames[{path,time,role,label}], contactSheets, review{window, scenes, beats, seams[{time,from,to,times,paths,sheet}], sheets, notes}}`.
|
||||
|
||||
### Keep verification clean and inspect the actual pixels
|
||||
|
||||
Inspect full-resolution frames in the opening 30 seconds, each mounted scene, and every reported problem interval. Contact sheets locate moments; use close-ups to judge thin edges, glass, particles, and WebGL geometry. Check the live preview and decoded render at matching times. Canvas shader defects can survive a successful DOM/layout audit. Record the browser/GPU and capture path when results differ across machines; isolate the failing material or pass before changing rendering defaults.
|
||||
|
||||
Managed snapshot storage keeps the newest 10 run folders, the newest evidence for the current revision and retained rendered revisions, and the latest snapshot. Live captures and folders younger than 24 hours are also preserved. Automatic cleanup removes whole eligible old UUID folders, including contact sheets and descriptions; arbitrary `--output` folders are untouched. Save intentional before/after evidence outside this managed storage.
|
||||
|
||||
For disposable checks, allocate a temporary output folder and remove that folder after inspecting it. Use `hyperframes clean --dry-run` to inspect broader cleanup. `clean --snapshots` explicitly discards snapshot evidence; use it only when that evidence is no longer needed. Never recommend removing all of `.hyperframes`, which also contains project history and other state.
|
||||
|
||||
### Zooming into a reported finding
|
||||
|
||||
`hyperframes check --snapshots` already writes a `finding-NN-<code>.png` crop for every error finding that carries a bbox, but the same zoom is available standalone once you know what to look at:
|
||||
|
||||
```bash
|
||||
npx hyperframes check --snapshots # reports a finding, e.g. content_overlap on "#cta"
|
||||
npx hyperframes snapshot --zoom "#cta" # crop the element to verify the defect, at 3x density
|
||||
npx hyperframes snapshot --zoom "100,50,400,300" --zoom-scale 2 # or an exact pixel region
|
||||
# fix the composition HTML, then re-check:
|
||||
npx hyperframes check
|
||||
```
|
||||
|
||||
`--zoom` takes a CSS selector or an exact `x,y,w,h` pixel region and always produces a real high-density crop (a raised `deviceScaleFactor`, never CSS zoom or a viewport resize), so the composition's layout — and its render determinism — is untouched. A selector matching nothing is a loud error, not a silent full-frame fallback, and a frame where the target has no visible box (collapsed or animated off-canvas) is skipped with a note instead of written as a sliver.
|
||||
|
||||
## Deprecated: validate, inspect, layout
|
||||
|
||||
All three keep working, print a deprecation notice on stderr, and mark `_meta.deprecated: true` in `--json`. Their functionality lives in `check`:
|
||||
|
||||
- `validate` (runtime errors + contrast) → `check` (contrast failures are now gating errors with fix payloads, not warnings).
|
||||
- `inspect` / `layout` (layout sweep + motion sidecar) → `check` (same flags: `--samples`, `--at`, `--at-transitions`, `--tolerance`, `--strict`).
|
||||
|
||||
Migrate scripts by replacing the sequence with the single `check` invocation; scaffolded projects' `npm run check` already points there.
|
||||
@@ -1,199 +0,0 @@
|
||||
# preview, play, render, publish
|
||||
|
||||
Serve, render, and share commands.
|
||||
|
||||
## preview
|
||||
|
||||
```bash
|
||||
npx hyperframes preview # foreground on a TTY; persistent in agent shells
|
||||
npx hyperframes preview --background # explicit persistent session
|
||||
npx hyperframes preview --foreground --json # ready JSON, then remain attached
|
||||
npx hyperframes preview --background --port 4567 # agent-safe custom port (default 3002)
|
||||
npx hyperframes preview --selection --json # print the current Studio selection and exit
|
||||
npx hyperframes preview --context --json # print compact agent context from Studio
|
||||
```
|
||||
|
||||
Hot-reloads on file changes. Opens Studio in the browser automatically — the full timeline editor, where the user can play the video and edit anything by hand before rendering. This is the review surface, not just a viewer.
|
||||
|
||||
When handing a project back to the user, use the Studio project URL, not the source `index.html` path:
|
||||
|
||||
```text
|
||||
http://localhost:<port>/#project/<project-name>
|
||||
```
|
||||
|
||||
Use the actual port and project directory name; treat `index.html` as source-code context, not the preview surface. For example, after `npx hyperframes preview --background --port 3017` in `codex-openai-video`, report `http://localhost:3017/#project/codex-openai-video`.
|
||||
|
||||
Two ways a handed URL turns out dead — check both before handing it back: the URL is missing its `#project/<project-name>` hash (Studio loads but has no project to open), or the server is not actually running. Bare `preview` automatically creates a managed persistent session in a non-TTY agent shell; `--background` remains the clearest explicit form. Verify the printed URL returns HTTP 200, keep it alive for the whole review, and stop it explicitly with `npx hyperframes preview --stop` afterward. Use the printed URL as-is: HyperFrames URL-encodes project names that contain route metacharacters.
|
||||
|
||||
### Agent context from Studio selection
|
||||
|
||||
`preview --context` and `preview --selection` are the agent bridge into a running Studio session. They do **not** start a new server; they find the active preview server for the current project, read agent-useful state from Studio, print it, and exit.
|
||||
|
||||
Use it when the user gives deictic edit instructions like "change this", "move the selected element", "make the card I clicked bigger", or "fix the current selection":
|
||||
|
||||
```bash
|
||||
npx hyperframes preview --context --json --context-fields selection
|
||||
```
|
||||
|
||||
The compact context payload includes the selected element's source file, composition path, current timeline time, `data-hf-id` / selector target, bounding box, text content, and a thumbnail URL for the selected element. Prefer `selection.target.hfId` when present; fall back to `selection.target.selector` only when no stable `data-hf-id` exists. If `selection` is `null`, inspect `errors.selection.code` (for example, `no-selection`).
|
||||
|
||||
Keep agent context small by asking only for the slices you need:
|
||||
|
||||
```bash
|
||||
npx hyperframes preview --context --json --context-fields selection
|
||||
npx hyperframes preview --context --json --context-fields lint
|
||||
npx hyperframes preview --context --json --context-fields selection,lint
|
||||
```
|
||||
|
||||
Use `--context-detail full` only when the edit genuinely needs heavy selection fields such as `computedStyles`, `inlineStyles`, `dataAttributes`, or editable text-field metadata:
|
||||
|
||||
```bash
|
||||
npx hyperframes preview --context --json --context-fields selection --context-detail full
|
||||
```
|
||||
|
||||
`preview --selection --json` remains available when you explicitly want the full selected-element payload and do not need lint/server context.
|
||||
|
||||
Failure modes:
|
||||
|
||||
| Code | Meaning |
|
||||
| -------------------------- | -------------------------------------------------------------------------- |
|
||||
| `preview-not-running` | Start Studio first with `npx hyperframes preview --background`. |
|
||||
| `ambiguous-preview-server` | Multiple matching Studio servers are open; rerun with one listed `--port`. |
|
||||
| `preview-port-mismatch` | The requested `--port` is not one of the matching Studio servers. |
|
||||
| `no-selection` | Studio is open, but the user has not selected an element yet. |
|
||||
| `selection-unavailable` | The running preview server does not expose selection context cleanly. |
|
||||
|
||||
If there is no selection, ask the user to click the target element in Studio and rerun the command. If the server error lists candidate ports, rerun the same command with `--port <candidate>`. Do not infer the target from a screenshot when the CLI can give a stable element target.
|
||||
|
||||
## play (lightweight player)
|
||||
|
||||
```bash
|
||||
npx hyperframes play # current project, port 3003
|
||||
npx hyperframes play ./my-video # specific project
|
||||
npx hyperframes play --port 8080 # custom port
|
||||
```
|
||||
|
||||
`play` serves the composition through the embeddable `<hyperframes-player>` web component instead of the full Studio UI. Use it when sharing a preview link or when Studio is heavier than needed (no editor, no panels). `play` reports the plain `http://localhost:<port>` URL — no `#project/<name>` fragment (that's a Studio routing convention only `preview` uses).
|
||||
|
||||
The player's `playback-rate` attribute (preview speed control, drives the timeline's `timeScale`) is clamped to `[0.1, 5]`; values `≤ 0` or non-finite fall back to `1`. This is a preview/playback knob, not a composition `data-*` attribute — authored motion still renders at `1×`.
|
||||
|
||||
### Launching with an external browser (preview + play)
|
||||
|
||||
Both `preview` and `play` can open inside an explicit Chromium-compatible browser instead of the OS default. Two use cases: isolated Chromium profile, or external CDP attach (DevTools / Playwright / Puppeteer / browser-MCP). **HyperFrames itself does not own CDP automation** — this only exposes the endpoint; whatever connects to it is your problem. Not to be confused with `--browser-gpu` (a `render` flag controlling Chrome GPU access during capture).
|
||||
|
||||
| Flag | Type | Notes |
|
||||
| ------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--browser-path` | path | Absolute path to a Chromium-compatible executable (`/usr/bin/chromium`, `/Applications/Brave Browser.app/...`). |
|
||||
| `--user-data-dir` | path | Chromium-compatible profile directory. Requires `--browser-path`. Use a throwaway directory to keep state out of your main profile. |
|
||||
| `--remote-debugging-port` | integer 1-65535 | Open a Chromium CDP endpoint on the given port. **Requires both** `--browser-path` and `--user-data-dir` — refused otherwise, so a CDP endpoint cannot leak into your main profile by accident. |
|
||||
|
||||
```bash
|
||||
# Open preview in an isolated Chromium profile
|
||||
npx hyperframes preview --background --browser-path /usr/bin/chromium --user-data-dir /tmp/hf-profile
|
||||
|
||||
# Same plus a CDP endpoint on :9222 (attach DevTools / Playwright / etc.)
|
||||
npx hyperframes play --browser-path /usr/bin/chromium --user-data-dir /tmp/hf-profile --remote-debugging-port 9222
|
||||
```
|
||||
|
||||
Validation runs before any server boots, so an invalid value exits cleanly without leaving a listening socket behind.
|
||||
|
||||
## render
|
||||
|
||||
> Render after the user has reviewed in `preview` and approved, or when they asked for the file or to work without stopping. Passing checks alone is not a reason to render.
|
||||
|
||||
```bash
|
||||
npx hyperframes render # standard MP4 from cwd
|
||||
npx hyperframes render ./my-video --output ./out.mp4 # render from outside the project dir
|
||||
npx hyperframes render --output final.mp4 # named output (no timestamp)
|
||||
npx hyperframes render -c compositions/intro.html -o intro.mp4 # render a specific sub-composition file
|
||||
npx hyperframes render --quality draft # fast iteration
|
||||
npx hyperframes render --quality looks # first real encode (default)
|
||||
npx hyperframes render --fps 60 --quality delivery # final delivery
|
||||
npx hyperframes render --format webm # transparent WebM
|
||||
npx hyperframes render --docker # byte-identical
|
||||
```
|
||||
|
||||
> Default `--output` is `renders/<project-name>_<YYYY-MM-DD>_<HH-MM-SS>.<ext>` — timestamped per render so successive runs don't clobber each other. Pass `--output` to get a stable name.
|
||||
|
||||
| Flag | Options | Default | Notes |
|
||||
| ------------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `dir` (positional) | path | cwd | Project directory. Omit to use current working directory. |
|
||||
| `--composition`, `-c` | path to composition file | `index.html` | Render a specific composition file (e.g. `compositions/intro.html`) instead of the project's `index.html`. |
|
||||
| `--output`, `-o` | path | `renders/<project>_<ts>.<ext>` | Output path. Default is timestamped (`<project-name>_YYYY-MM-DD_HH-MM-SS.<ext>`). |
|
||||
| `--fps` | 24, 30, 60 | 30 | 60fps doubles render time |
|
||||
| `--quality` | draft, looks, delivery, standard, high | looks | `looks` is CRF 16 on the standard preset. `delivery` is `high`. draft for iterating |
|
||||
| `--format` | mp4, webm, mov, gif, png-sequence, hls | mp4 | WebM/MOV render with transparency; gif for inline autoplay in GitHub PRs/READMEs/docs (two-pass palette encode, fps capped at 30 — prefer `--fps 15` — no audio, 1-bit transparency only, HDR falls back to SDR); png-sequence writes RGBA frames to a directory (AE/Nuke/Fusion ingest); hls writes an HLS VOD directory (master.m3u8 + video/audio playlists + MPEG-TS segments), SDR only, rejects `--gpu`, unavailable on lambda/cloudrun |
|
||||
| `--gif-loop` | 0-65535 | 0 | GIF loop count; `0` loops forever. Only with `--format gif`. |
|
||||
| `--hls-segment-seconds` | 1-60 | 4 | HLS target segment length in whole seconds; also locks the encoder GOP so every segment starts on a keyframe. Only with `--format hls`. |
|
||||
| `--resolution` | landscape, portrait, landscape-4k, portrait-4k, square, square-4k (+ aliases `1080p`, `4k`, `uhd`) | — | Supersample via Chrome `deviceScaleFactor`. Aspect ratio must match composition; scale must be an integer. Not with `--hdr`. |
|
||||
| `--crf` | 0-51 | — | Encoder CRF (lower = higher quality). Mutually exclusive with `--video-bitrate`. |
|
||||
| `--video-bitrate` | e.g. `10M`, `5000k` | — | Target bitrate. Mutually exclusive with `--crf`. |
|
||||
| `--hdr` | flag | off | Force HDR output even with SDR sources. MP4 only. |
|
||||
| `--sdr` | flag | off | Force SDR even with HDR sources. |
|
||||
| `--workers` | number or `auto` | auto | Each worker spawns Chrome (~256 MB) |
|
||||
| `--docker` | flag | off | Reproducible output across hosts |
|
||||
| `--gpu` | flag | off | GPU-accelerated FFmpeg encoding (NVENC / VideoToolbox / VAAPI / QSV) |
|
||||
| `--browser-gpu` / `--no-browser-gpu` | flag | auto (local), off (docker) | Host GPU for Chrome/WebGL capture |
|
||||
| `--browser-timeout` | seconds (0.001–86400) | 60 | Puppeteer page-navigation timeout for the entry HTML. Raise when heavy compositions (many videos / fonts / remote assets) can't reach `domcontentloaded` within the 60s default. |
|
||||
| `--quiet` | flag | off | Suppress verbose output |
|
||||
| `--strict` | flag | off | Fail on lint errors |
|
||||
| `--strict-all` | flag | off | Fail on lint errors AND warnings |
|
||||
| `--variables` | JSON object | — | Override values declared in `data-composition-variables` |
|
||||
| `--variables-file` | path | — | JSON file with variable values (alternative to `--variables`) |
|
||||
| `--strict-variables` | flag | off | Fail render on undeclared keys or type mismatches in `--variables` |
|
||||
|
||||
**Quality guidance:** `draft` while iterating, `looks` (the default) for the first real encode, `delivery` for final delivery. `standard` and `high` still work.
|
||||
|
||||
**Read the summary's second line.** After the output path and size, `render` prints the capture path, GPU mode, and per-stage timings, e.g. `beginframe capture · software gpu · compile 0.7s · setup 1.1s · capture 8.1s · encode 5.8s`. `capture` is the frame loop and `setup` is calibration plus session init; the capture path reads `a|b` when parallel workers diverged, and `encode (during capture)` means the streaming encoder overlapped the loop, so do not add it to the wall time. Local GPU `auto` (the default) requests BeginFrame. If the line still says `screenshot capture` with `software gpu` on Linux, a hint under it says BeginFrame did not run (needs chrome-headless-shell, no `--resolution` upscale). It only appears when the GPU was auto-probed, never under `--docker` or `--no-browser-gpu`, where software GL is the point. Heavy compositions can stall on software GL. Do not reach for `--debug` just to learn which path ran.
|
||||
|
||||
**Parametrized renders:** the composition declares its variables on the `<html>` root with **`data-composition-variables`** — a JSON **array of declarations** (`{id, type, label, default}` per entry) that defines the schema. Scripts inside read the resolved values via `window.__hyperframes.getVariables()`. The CLI `--variables '{"title":"Q4 Report"}'` is a JSON **object keyed by id** that overrides those declared defaults for one render; missing keys fall through, so the same composition runs unchanged in dev preview and in production. Sub-comp hosts can also override per-instance with `data-variable-values`. See `../../hyperframes/references/format.md` for the full pattern.
|
||||
|
||||
### feedback (report after rendering)
|
||||
|
||||
After a render is verified, send one feedback line per task. This is the maintainers' primary signal — a render that finishes silently tells them nothing.
|
||||
|
||||
```bash
|
||||
npx hyperframes feedback --rating 10 # clean run, no notes
|
||||
npx hyperframes feedback --rating 6 --comment "bg <video> renders grey in multi-scene; worked around with --format png-sequence"
|
||||
```
|
||||
|
||||
`--rating` is an integer from 0-10; `--comment` is free text. A `--comment` with no `--rating` is a ratingless report (`MISSING FEATURE:` and `HOST APP:` in `SKILL.md`) that never counts in the rating metric. Feedback is anonymous and attaches a `doctorSummary` (OS/Node/CPU/mem/ffmpeg) automatically, so don't repeat those fields. A clean run needs only a short result. Before sending any bug, workaround, or confusing behavior, collect this compact reproduction packet:
|
||||
|
||||
```text
|
||||
REPRO COMMAND: <HF_*/PRODUCER_* env> npx hyperframes <exact command> # run from the project directory; do NOT paste absolute paths
|
||||
EXPECTED / ACTUAL: <expected behavior> / <observed behavior and isolated trigger>
|
||||
EXACT ERROR: <verbatim error or warning; include frame/timestamp for visual defects>
|
||||
OUTCOME: <output correct | output corrupt | fallback succeeded | hard exit | command hung>
|
||||
WORKAROUND: <exact workaround, or none>
|
||||
COMPOSITION_STRUCTURE:
|
||||
elements: video=<n> audio=<n> img=<n> svg=<n> canvas=<n> subComps=<n>
|
||||
attributes: <comma-joined subset of clip-path, filter, mix-blend-mode, transform, mask, position:fixed, overflow:hidden, z-index, data-has-audio, data-duration, data-start, data-composition-src, background-image:url, mask-image:url — or "(none present)">
|
||||
timeline: <flat | nested (<n> sub-comps)>; driver=<gsap | data-timeline | gsap+data-timeline | none>
|
||||
delta: <what differs between the working workaround-render and the broken default render>
|
||||
defect: <spatial location + frame index range, e.g. top-left / frames 0-30 — omit for non-visual defects>
|
||||
```
|
||||
|
||||
`COMPOSITION_STRUCTURE:` is a privacy-preserving structural anatomy: counts + presence flags only, no file paths, no src URLs, no user text. It lets maintainers pattern-match the report against known bug families (e.g. "sub-comp mount + clip-path", "GSAP timeline + z-index") without receiving the composition ZIP. Required for any rating ≤ 7 that describes a visual defect (black frame, flicker, corrupt output, wrong frame, blank output, other visual anomaly); optional but appreciated on higher ratings. Agents on this skill can auto-fill the block by calling `buildCompositionCensus(html)` and `renderCompositionCensusBlock(census)` from `packages/cli/src/utils/compositionCensus.ts` against the composition HTML they already have access to — the human user does not fill this out by hand.
|
||||
|
||||
**Feedback is submitted to a public channel — anonymize before sending.** Redact absolute paths (which leak user home directory + machine identity), any user or project names embedded in paths, secrets, and credentials. Path arguments in the command should stay relative to the project directory (`./renders/out.mp4`, not `/Users/<user>/Documents/…/out.mp4`; `.hf-tmp/`, not `/home/<user>/projects/<real-name>/.hf-tmp/`). Similarly strip absolute paths from `EXACT ERROR:` stack traces and log excerpts — keep the file basename and line number, drop the leading directory. Preserve flags and relevant `HF_*` / `PRODUCER_*` variables verbatim. If the failure no longer reproduces, include the last failing command and log excerpt (redacted the same way). Share a project link only when one is already available and safe to share.
|
||||
|
||||
The `hyperframes feedback` command soft-warns when a non-10 `--comment` is missing `REPRO COMMAND:`, and when a rating-≤-7 visual-defect comment is missing `COMPOSITION_STRUCTURE:`. The warnings print above the submission ack and do not block — some legitimate reports (a one-line "cloudrun quota bumped yesterday, fine now") won't fit the mold. Fix the packet and rerun to silence them.
|
||||
|
||||
Hit a reproducible bug? Add `--file-issue` (optionally `--dir <project>` and `--yes` for non-interactive shells) to also publish a minimal repro to a public URL and open a pre-filled GitHub `bug` issue draft for a maintainer to file. This publishes the project publicly, so it is opt-in and consent-gated; the issue is never auto-submitted.
|
||||
|
||||
## publish
|
||||
|
||||
```bash
|
||||
npx hyperframes publish # upload current project privately, return stable URL
|
||||
npx hyperframes publish ./my-video # specific project
|
||||
npx hyperframes publish --public # allow anyone with the URL to view the claimed project
|
||||
npx hyperframes publish --yes # skip the confirmation prompt (scripts/CI)
|
||||
```
|
||||
|
||||
Uploads the project's source (HTML + assets) and returns a stable hosted URL that renders in the browser. A fresh publish is private by default and requires authentication plus access to view. Use `--public` to allow anyone with the URL to view the claimed project. Updating a project in place keeps its existing visibility: re-publishing without `--public` never turns a public project private. `--yes` only skips the confirmation prompt; it does not change visibility. A signed-out publish returns an authentication-required claim URL rather than a public playback URL. Lint findings are surfaced before upload but do not block.
|
||||
|
||||
## Where to render, and skill attribution
|
||||
|
||||
Use cloud rendering when the user wants hosted rendering without local Chrome, FFmpeg, or AWS. Use Lambda only when AWS ownership is a requirement. Use Cloud Run only when GCP ownership is a requirement. Read the matching reference before running any cloud path.
|
||||
|
||||
Skill attribution is automatic — the examples above need no `--skill`. A project scaffolded with `hyperframes init --skill=<slug>` records its owning skill in `hyperframes.json`, and every later render inherits it on anonymous telemetry: re-renders, `npm run render`, and `--batch` alike. Pass `--skill=<slug>` explicitly only to stamp a project that was not scaffolded with one (its first render then persists it).
|
||||
@@ -1,142 +0,0 @@
|
||||
# info, upgrade, compositions, timeline, docs, benchmark, telemetry, asset preprocessing
|
||||
|
||||
Catch-all reference for commands that don't fit the main dev loop.
|
||||
|
||||
## info
|
||||
|
||||
```bash
|
||||
npx hyperframes info # project metadata
|
||||
npx hyperframes info ./my-video # specific project
|
||||
npx hyperframes info --json
|
||||
```
|
||||
|
||||
Prints **project** metadata: name, resolution, duration, element counts by type, track count, and total project size. Project-level — not environment. For environment health use `doctor`.
|
||||
|
||||
## upgrade
|
||||
|
||||
```bash
|
||||
npx hyperframes upgrade # check + interactive prompt
|
||||
npx hyperframes upgrade --check # check and exit, no prompt (agent-friendly)
|
||||
npx hyperframes upgrade --check --json # machine-readable: current / latest / updateAvailable
|
||||
npx hyperframes upgrade --yes # print upgrade commands without prompting
|
||||
```
|
||||
|
||||
Compares the installed CLI version against npm latest.
|
||||
|
||||
`--project [dir]` bumps a **project's** pinned scripts instead of the global install: it rewrites every `npx …hyperframes@<version>…` in `<dir>/package.json` (default cwd) to npm-latest. Always invoke it unpinned (`npx hyperframes@latest upgrade --project`) — a project scaffolded on an old CLI stays frozen otherwise. `--project . --check` reports the delta without writing; add `--json` for `{ changed, from, to, path }`. Pass the dir explicitly whenever another flag follows `--project` — on older releases a bare `--project` consumes the next flag as its directory value.
|
||||
|
||||
## timeline
|
||||
|
||||
```bash
|
||||
npx hyperframes timeline [project-dir] # tracks and clips as a table with bars
|
||||
npx hyperframes timeline [project-dir] --json
|
||||
```
|
||||
|
||||
Reach for `timeline` instead of opening `index.html` and each `data-composition-src` file when you need to know what is on the timeline: which clips exist, when they start and end, what they play, and how loud. It reads the project's files statically (no browser).
|
||||
|
||||
Text output is `timeline <N>s`, then one block per track kind (`video`, `graphics`, `captions`, `audio`) with one row per clip of that kind, ordered by absolute start:
|
||||
|
||||
```
|
||||
graphics (2: 1 top-level, 1 nested)
|
||||
|██████ | sec-connector 0-6.7s src=compositions/connector-morph.html
|
||||
|██████████████ | box 15.67-17.99s (local 0-2.32s) nested in sec-connector compositions/connector-morph.html
|
||||
audio (1)
|
||||
| █ | vo 1.6-3.6s src=vo.mp3 vol=0.5 group=vo volume[0:0.2 2:1]
|
||||
```
|
||||
|
||||
- The bar is 40 columns over the whole timeline. Times are seconds.
|
||||
- `src=`, `vol=`, `rate=` (playback rate, only when not 1), `group=` (audio group), and `<target>[t:v ...]` (automation lane points, `t` in seconds from the clip start) appear only when the clip has them.
|
||||
- The header counts every clip of that kind, nested ones included: `video (6: 2 top-level, 4 nested)`. The list is in absolute order, so the Nth row is the Nth clip of that kind on the main timeline. A clip inside a sub-composition prints its absolute start-end, then `(local <start>-<end>s)` (time inside that sub-composition), then `nested in <host row id> <file that declares it>`. Only one level of nesting is read: a sub-composition inside a sub-composition is listed as a row saying `children=unread`, and the clips inside it are not counted.
|
||||
- With no `data-duration`/`data-end`, a media row still gets a resolved length and says where it came from: `duration=media` means ffprobe measured the source (with playback start and rate applied); `duration=default` means an `img` got the 3s default; `duration=inferred` means a composition host summed its children; `pending: <reason>` (dotted bar, `duration` 0) means the source could not be probed (missing file, remote `src`, ffprobe error). A non-media leaf with nothing to resolve prints no source.
|
||||
- `lanes unreadable: ...` means the clip's `data-automation` or `data-fx-chain` did not parse; fix the attribute.
|
||||
|
||||
`--json` prints `{ timeline: { duration, tracks: [{ kind, rows: [...] }] } }`. A track's `rows` are every clip of that kind, nested ones included, by `absStart`; read them, never only the top level. Each row has `id`, `kind` (tag), `trackKind`, `start`, `duration`, `end` (local to the row's own file), **`absStart`, `absEnd`, `file`** (main-timeline time and the project-relative file that declares the clip — use these, not `start`/`end`, to compare clips across nesting), `trackIndex`, `src`, `sourceFile`, `volume`, `lanes`, `playbackRate`, `audioGroup`, `durationAuthored`, **`durationSource`** (`"authored" | "media" | "default" | "inner" | "pending"`, or `null` for a non-media leaf with nothing to resolve), **`pendingReason`** (why nothing resolved; `null` unless `durationSource` is `"pending"`), `laneError`, **`index`** (the row's position in its kind's `rows`; a position, not an id), **`nested`** (declared inside a sub-composition), **`host`** (plain id of the row that hosts a nested clip, else `null`), **`hostRow`** (`{kind, index}` of that host row), and `children` (`{kind, index}` pointers to the sub-composition's clips, one level; every clip is already a full row in its kind's `rows`, so nothing needs following).
|
||||
|
||||
### Query one-liners (jq, node fallback if jq is absent)
|
||||
|
||||
```bash
|
||||
TL=$(npx hyperframes timeline --json)
|
||||
# 1. what plays at absolute time T=12.5
|
||||
jq --argjson t 12.5 '[.timeline.tracks[].rows[] | select(.absStart<=$t and .absEnd>$t)]' <<<"$TL"
|
||||
node -e 'const t=12.5,j=JSON.parse(require("fs").readFileSync(0,"utf8"));j.timeline.tracks.forEach(tr=>tr.rows.forEach(x=>{if(x.absStart<=t&&x.absEnd>t)console.log(x.id,x.file)}))' <<<"$TL"
|
||||
# 2. find a clip by id or src -> file, track, absStart, absEnd
|
||||
jq --arg q tsfx-pet2 '[.timeline.tracks[].rows[] | select(.id==$q or .src==$q)] | .[] | {file,trackKind,absStart,absEnd}' <<<"$TL"
|
||||
node -e 'const q="tsfx-pet2",j=JSON.parse(require("fs").readFileSync(0,"utf8"));j.timeline.tracks.forEach(tr=>tr.rows.forEach(x=>{if(x.id===q||x.src===q)console.log(x.file,x.trackKind,x.absStart,x.absEnd)}))' <<<"$TL"
|
||||
# 3. every clip of one kind, nested included, in absolute order
|
||||
jq --arg k video '.timeline.tracks[] | select(.kind==$k).rows[] | {id,absStart,absEnd}' <<<"$TL"
|
||||
node -e 'const k="video",j=JSON.parse(require("fs").readFileSync(0,"utf8"));j.timeline.tracks.find(t=>t.kind===k).rows.forEach(r=>console.log(r.id,r.absStart,r.absEnd))' <<<"$TL"
|
||||
# 4. gaps and overlaps within a kind (positive = gap, negative = overlap)
|
||||
jq --arg k video '(.timeline.tracks[] | select(.kind==$k).rows) as $r|[range(0;($r|length)-1)|{a:$r[.].id,b:$r[.+1].id,delta:($r[.+1].absStart-$r[.].absEnd)}]' <<<"$TL"
|
||||
node -e 'const k="video",j=JSON.parse(require("fs").readFileSync(0,"utf8"));const r=j.timeline.tracks.find(t=>t.kind===k).rows;for(let i=0;i<r.length-1;i++)console.log(r[i].id,r[i+1].id,r[i+1].absStart-r[i].absEnd)' <<<"$TL"
|
||||
# 5. the Nth clip of a kind by absolute start (N=2)
|
||||
jq --arg k video --argjson n 2 '.timeline.tracks[] | select(.kind==$k).rows[$n-1] | {index,id,absStart,file,nested}' <<<"$TL"
|
||||
node -e 'const k="video",n=2,j=JSON.parse(require("fs").readFileSync(0,"utf8"));const r=j.timeline.tracks.find(t=>t.kind===k).rows[n-1];console.log(r.index,r.id,r.absStart,r.file)' <<<"$TL"
|
||||
```
|
||||
|
||||
## compositions, docs
|
||||
|
||||
```bash
|
||||
npx hyperframes compositions # list compositions in project
|
||||
npx hyperframes compositions --json
|
||||
npx hyperframes docs # list available topics
|
||||
npx hyperframes docs rendering # print one topic inline in the terminal
|
||||
```
|
||||
|
||||
`compositions` lists every `data-composition-id` in the project (including sub-comps) with duration, resolution, and element count.
|
||||
|
||||
`docs` prints inline documentation **in the terminal** — it does not open a browser. Topics: `data-attributes`, `examples`, `rendering`, `gsap`, `troubleshooting`, `compositions`. Run without a topic to see the list.
|
||||
|
||||
## benchmark
|
||||
|
||||
```bash
|
||||
npx hyperframes benchmark # run the preset matrix in current project
|
||||
npx hyperframes benchmark ./my-video # specific project
|
||||
npx hyperframes benchmark --runs 5 # repeat each config N times (default 3)
|
||||
npx hyperframes benchmark --json
|
||||
```
|
||||
|
||||
Renders the project with 5 preset configurations — `30fps draft 2w`, `30fps standard 2w`, `30fps high 2w`, `30fps standard 4w`, `60fps standard 4w` — and prints a comparison of render speed and output file size. Use it to find the fastest acceptable preset for your machine. Not a single-render-with-stage-breakdown.
|
||||
|
||||
## telemetry
|
||||
|
||||
```bash
|
||||
npx hyperframes telemetry status # show telemetry state
|
||||
npx hyperframes telemetry disable # disable anonymous usage telemetry
|
||||
npx hyperframes telemetry enable # re-enable telemetry
|
||||
```
|
||||
|
||||
Telemetry is anonymous usage counters only. Disable globally with `HYPERFRAMES_NO_TELEMETRY=1` if env-var control is preferred over the subcommand.
|
||||
|
||||
Events include two fingerprint properties used to distinguish managed-sandbox runs from real laptops — no PII, no env-var **values**, only existence checks:
|
||||
|
||||
- **`sandbox_runtime`**: `gvisor` / `firecracker` / `docker` / `kvm` / `wsl` / `null`. gVisor via kernel string + `/proc/version`. Firecracker via `/dev/vsock` + DMI sys_vendor. Docker via `/.dockerenv` + cgroup.
|
||||
- **`agent_runtime`**: `claude_code` / `codex` / `cursor` / `copilot_agent` / `jules` / `replit` / `devin` / `aider` / `gemini_cli` / `hermes` / `openclaw` / `null`. Detected by the existence of well-known vendor env vars; the values themselves are never read.
|
||||
- **`client`**: set only when an app launched the CLI with a `HYPERFRAMES_CLIENT` tag (`<app>/<version>/<channel>`), sent as-is when it is a short slug.
|
||||
|
||||
## Asset Preprocessing
|
||||
|
||||
```bash
|
||||
npx hyperframes tts
|
||||
npx hyperframes transcribe
|
||||
npx hyperframes remove-background
|
||||
```
|
||||
|
||||
These produce assets (narration audio, word-level transcripts, transparent video) that get dropped into a composition. Each may download its own model on first run.
|
||||
|
||||
For voice selection, Whisper model rules, output format choice, and the TTS → transcript → captions chain, invoke the `media-use` skill. This skill stays focused on the dev loop.
|
||||
|
||||
## Remaining harness usage
|
||||
|
||||
Run `npx hyperframes usage --json` at the start of a video workflow and again at milestones such as after drafting and before rendering. A fresh read at handoff can serve as the start read. Use `--harness claude-code`, `--harness codex`, or `--harness grok` to select explicitly.
|
||||
|
||||
Known results contain `status: "known"`, `harness`, `planTier`, `session`, `weekly`, and `spend`. Each available window contains `usedPercent`, `remainingPercent`, and `resetsAt`; an unavailable window is `null`. `planTier` is the readable subscription tier when available, otherwise `null`. Claude Code reports its shared five-hour and weekly windows; a spend-metered plan (such as Enterprise) has neither and reports `spend`, the share of its billing-period spend limit already used, with `resetsAt: null`. Codex reports its shared session and weekly windows when available. Grok reports its included weekly allowance with `session: null`. Only Claude Code reports `spend`; it is `null` elsewhere. When no window is available the result is `status: "unknown"` with `reason: "no_usage_windows"`.
|
||||
|
||||
Unknown results contain `status: "unknown"` and a token-free `reason`. Missing, expired, unsupported, ambiguous, or unreadable logins and unavailable provider responses return unknown. Report the unknown state without guessing allowance. The command reads existing credentials without refreshing or rewriting them and emits no tokens or telemetry. Usage is a snapshot; it does not reserve allowance or estimate the next video's cost. Keep scope and workflow choices with the user.
|
||||
|
||||
## Remaining harness usage
|
||||
|
||||
Run `npx hyperframes usage --json` at the start of a video workflow and again at milestones such as after drafting and before rendering. A fresh read at handoff can serve as the start read. Use `--harness claude-code`, `--harness codex`, or `--harness grok` to select explicitly.
|
||||
|
||||
Known results contain `status: "known"`, `harness`, `planTier`, `session`, `weekly`, and `spend`. Each available window contains `usedPercent`, `remainingPercent`, and `resetsAt`; an unavailable window is `null`. `planTier` is the readable subscription tier when available, otherwise `null`. Claude Code reports its shared five-hour and weekly windows; a spend-metered plan (such as Enterprise) has neither and reports `spend`, the share of its billing-period spend limit already used, with `resetsAt: null`. Codex reports its shared session and weekly windows when available. Grok reports its included weekly allowance with `session: null`. Only Claude Code reports `spend`; it is `null` elsewhere. When no window is available the result is `status: "unknown"` with `reason: "no_usage_windows"`.
|
||||
|
||||
Unknown results contain `status: "unknown"` and a token-free `reason`. Missing, expired, unsupported, ambiguous, or unreadable logins and unavailable provider responses return unknown. Report the unknown state without guessing allowance. The command reads existing credentials without refreshing or rewriting them and emits no tokens or telemetry. Usage is a snapshot; it does not reserve allowance or estimate the next video's cost. Keep scope and workflow choices with the user.
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
name: hyperframes-registry
|
||||
description: About 450 ready-made HyperFrames blocks and components installed as editable source - transitions, overlays, titles, kinetic type, captions, charts, map overlays, 3D, camera moves. Use it before hand-building any named visual (CRT scanlines, glitch, film grain, a chart); also for hyperframes add and catalog.
|
||||
---
|
||||
|
||||
**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin.
|
||||
|
||||
# HyperFrames registry
|
||||
|
||||
Starting from an item is faster and usually better than writing the effect yourself, and an installed item is ordinary source in your project: change it, cut it down, restyle it.
|
||||
|
||||
## Find and install
|
||||
|
||||
```bash
|
||||
npx hyperframes catalog --query "<what the beat should do, in English>" --json
|
||||
npx hyperframes add <name> --json # prints the written files and the mount snippet
|
||||
```
|
||||
|
||||
- Query in English even when the video is not: the catalog is indexed in English, and another script returns `No searchable words in query`, which is not a gap.
|
||||
- Search is local and sends nothing. The default `words` tier only finds items that reuse your words; `--on-device` ranks by meaning after a one-time ~33 MB download. Offer that download and let the person decide; never enable it silently. The `--json` envelope names the tier that answered.
|
||||
- Narrow with `--type block|component` and `--tag <tag>`. `add <tag>` installs every block with that tag; dependencies install first. `add` needs the network even for an item installed before; search works offline from the cached manifest.
|
||||
- Nothing fits: report it before you hand-build, on either tier. `catalog --query` prints the line pre-filled (`report_gap` in `--json`): `npx hyperframes feedback --search-miss "<query>" --wanted "<the move you needed>" --tier <tier>`. Describe the effect, not an item name you imagined.
|
||||
|
||||
Tiers, `dropped` and `unindexed`, stale indexes, offline behaviour: `references/search.md`.
|
||||
|
||||
## What's in it
|
||||
|
||||
| Blocks: a scene with its own timeline | Examples |
|
||||
| --- | --- |
|
||||
| Transitions | `beat-freeze-cut`, `chromatic-radial-split`, `cinematic-zoom` |
|
||||
| Overlays and lower thirds | `camcorder-hud`, `editorial-flash-overlay`, `lt-clean-bar` |
|
||||
| Title cards and kinetic type | `canopy-part-title`, `code-slice-hero` |
|
||||
| Social, chat and UI cards | `ai-chat-reveal`, `chatgpt-exchange` |
|
||||
| Code, charts, data and maps | `code-diff`, `bar-chart-race`, `data-chart` |
|
||||
| 3D, shader and backgrounds | `app-showcase`, `aurora`, `blob` |
|
||||
|
||||
| Components: a snippet merged into your scene | Examples |
|
||||
| --- | --- |
|
||||
| Motion primitives and text effects | `agent-cursor`, `blur-in`, `bottom-up-letters` |
|
||||
| Caption styles | `caption-clip-wipe`, `caption-editorial-emphasis` |
|
||||
| Camera moves | `camera-shake`, `camera-scan-gate` |
|
||||
| Product demo and UI props | `before-after-wipe`, `browser-device-stage` |
|
||||
| Textures, particles and effects | `ascii-render-pass`, `grain-overlay` |
|
||||
|
||||
## Wiring
|
||||
|
||||
- **Block**: installs to `compositions/<name>.html`. Mount it with `data-composition-src`, `data-composition-id` matching the block's own id, `data-start` and the `data-duration` you need; the snippet `add` prints is the starting point. `references/wiring-blocks.md`.
|
||||
- **Component**: installs to `compositions/components/<name>.html`. Copy its markup, `<style>` and script into your scene, and follow its comment block if it hooks into your timeline. `references/wiring-components.md`.
|
||||
- Take the largest unit that fits, then edit it. Keep an installed item's ids prefixed as they are, so two items can't collide.
|
||||
- An item keeps its own variables; set them for one mount with `data-variable-values` on its host (`add --vars '{"title":"…"}'` writes that), and point its colour and font variables at your brand's.
|
||||
- Beat-synced blocks take hit times from `npx hyperframes beats --json`: the beat times minus the mount's `data-start`.
|
||||
- Install paths come from `hyperframes.json` (`references/install-locations.md`).
|
||||
|
||||
## Contributing
|
||||
|
||||
Authoring a new block or component for an upstream PR: `references/contributing.md`, starter templates in `references/templates.md`, quality bar in `references/component-quality-bar.md`.
|
||||
@@ -1,51 +0,0 @@
|
||||
# Worked Example: Adding a Block
|
||||
|
||||
## Scenario
|
||||
|
||||
User has an existing HyperFrames project and wants to add an animated chart alongside their video content.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Install the block
|
||||
|
||||
```bash
|
||||
hyperframes add data-chart
|
||||
```
|
||||
|
||||
### 2. Wire into index.html
|
||||
|
||||
```html
|
||||
<div id="stage" data-composition-id="main" data-width="1920" data-height="1080" data-duration="30">
|
||||
<video
|
||||
id="speaker"
|
||||
src="speaker.mp4"
|
||||
data-start="0"
|
||||
data-duration="30"
|
||||
data-track-index="0"
|
||||
style="position: absolute; width: 60%; height: 100%; left: 0; top: 0; object-fit: cover;"
|
||||
></video>
|
||||
|
||||
<!-- Data chart appears at 5s in the right 40% of the screen -->
|
||||
<div
|
||||
data-composition-id="data-chart"
|
||||
data-composition-src="compositions/data-chart.html"
|
||||
data-start="5"
|
||||
data-duration="15"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
style="position: absolute; right: 0; top: 0; width: 40%; height: 100%;"
|
||||
></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 3. Lint and preview
|
||||
|
||||
```bash
|
||||
hyperframes lint
|
||||
hyperframes preview
|
||||
```
|
||||
|
||||
### 4. Customize (optional)
|
||||
|
||||
Edit `compositions/data-chart.html` — data arrays are at the top of the script, colors are in the CSS rules scoped under `[data-composition-id="data-chart"]`.
|
||||
@@ -1,73 +0,0 @@
|
||||
# Worked Example: Adding a Component
|
||||
|
||||
## Scenario
|
||||
|
||||
User wants to add a shimmer light sweep effect to their title text.
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Install the component
|
||||
|
||||
```bash
|
||||
hyperframes add shimmer-sweep
|
||||
```
|
||||
|
||||
### 2. Read the snippet
|
||||
|
||||
Open `compositions/components/shimmer-sweep.html` and read the comment header.
|
||||
|
||||
### 3. Wire into your composition
|
||||
|
||||
**HTML** — wrap target elements:
|
||||
|
||||
```html
|
||||
<div class="shimmer-sweep-target" style="--shimmer-color: rgba(255, 255, 255, 0.5)">
|
||||
<h1 class="title">AI-Powered Video</h1>
|
||||
</div>
|
||||
```
|
||||
|
||||
**CSS** — paste the `.shimmer-sweep-target` and `.shimmer-mask` rules from the snippet.
|
||||
|
||||
**JS** — paste the auto-injection script (before timeline code):
|
||||
|
||||
```js
|
||||
document.querySelectorAll(".shimmer-sweep-target").forEach((el) => {
|
||||
if (!el.querySelector(".shimmer-mask")) {
|
||||
const mask = document.createElement("div");
|
||||
mask.className = "shimmer-mask";
|
||||
el.appendChild(mask);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Timeline** — add the sweep:
|
||||
|
||||
```js
|
||||
tl.fromTo(
|
||||
".shimmer-sweep-target",
|
||||
{
|
||||
"--shimmer-pos": "-20%",
|
||||
},
|
||||
{
|
||||
"--shimmer-pos": "120%",
|
||||
duration: 1.2,
|
||||
ease: "power2.inOut",
|
||||
stagger: 0.15,
|
||||
},
|
||||
1.5,
|
||||
);
|
||||
```
|
||||
|
||||
### 4. Lint and preview
|
||||
|
||||
```bash
|
||||
hyperframes lint
|
||||
hyperframes preview
|
||||
```
|
||||
|
||||
### 5. Customize
|
||||
|
||||
- `--shimmer-color`: highlight color per element
|
||||
- `--shimmer-width`: light band width (default 20%)
|
||||
- `--shimmer-angle`: sweep direction (default 120deg)
|
||||
- Timeline `duration`, `ease`, `stagger`: control speed and feel
|
||||
@@ -1,250 +0,0 @@
|
||||
# Component quality bar
|
||||
|
||||
What a catalog component has to be for us to keep shipping it. Every criterion here comes from a
|
||||
defect found and verified on this branch, not from taste.
|
||||
|
||||
A registry component is a motion primitive an author installs into their own composition and
|
||||
ships. The catalog page is marketing; the installed file is the product. Every criterion below is
|
||||
therefore evaluated against **the item's own `<name>.html`, mounted alone**, never against
|
||||
`demo.html` and never against the catalog page, because both of those carry scaffolding the author
|
||||
does not receive.
|
||||
|
||||
Read this before auditing, scoring or cutting anything. It exists so several people auditing in
|
||||
parallel reach the same verdict on the same item.
|
||||
|
||||
## The one rule
|
||||
|
||||
> An item earns its place when the file the author installs, mounted by itself on the ground it
|
||||
> was designed for, renders the subject its name promises and moves the way its description says.
|
||||
> Anything that fails that and cannot be fixed into something no other item already does is cut.
|
||||
|
||||
## How an audit runs
|
||||
|
||||
Two passes, in this order. The mechanical pass is free and runs across every item in seconds; the
|
||||
visual pass costs a browser and eyes, so it is spent only on what the mechanical pass could not
|
||||
decide. A mechanical signal is a **candidate**, never a verdict.
|
||||
|
||||
| Pass | Cost | Decides |
|
||||
| -------------- | -------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **Mechanical** | grep and hash | duplicates, missing timeline, banned hexes, empty markup, name gaps, unbounded variables |
|
||||
| **Visual** | render and eye | renders at all, implements its description, legible, deterministic |
|
||||
|
||||
`hyperframes check` is not a visual gate. It passes compositions that render nothing: a blank plot
|
||||
produces no error, no warning and no layout finding, because an empty render is a valid render. No
|
||||
criterion below may rest on `check` alone.
|
||||
|
||||
### The mount harness
|
||||
|
||||
Three ways to get a false verdict from a working item, all of them the harness's fault. Build the
|
||||
shell like this or the audit invents defects.
|
||||
|
||||
1. **Two shapes of item, two ways to mount.** If the file, with HTML comments stripped, contains a
|
||||
`data-composition-id`, it is a sub-composition: mount it with
|
||||
`data-composition-src="./<name>.html"` on a clip. If it does not, it is a snippet: paste it
|
||||
inline inside a `class="clip"` div. Inlining a sub-composition nests a document in a document
|
||||
and renders black, which reads exactly like a dead item.
|
||||
2. **Use the item's own ground.** Take the background off its `demo.html` body rule. A snippet
|
||||
whose ink defaults to `#18181b` is a 16:1 headline on its own `#f7f7f8` and an invisible 1.5:1
|
||||
smudge on a dark stage. The stage is not evidence.
|
||||
3. **Load GSAP and register a paused root timeline**, then snapshot with
|
||||
`hyperframes snapshot . --at 0.05,1.2,2.5,4.0 --no-end` and read the contact sheet.
|
||||
|
||||
An item whose own `data-duration` is shorter than the shell's will be blank in the last frames.
|
||||
That is arithmetic, not a defect.
|
||||
|
||||
## Fatal, cut the item
|
||||
|
||||
Fatal means there is nothing worth keeping underneath the defect: no edit short of writing a
|
||||
different item fixes it, or the fix produces something the catalog already ships. Cite the named
|
||||
evidence; a fatal verdict without it does not count.
|
||||
|
||||
**F1. Does not implement its own description.** The markup contains no trace of the subject the
|
||||
item is named and described for. Not "renders badly", but "the thing is absent from the file".
|
||||
`ecosystem-constellation`, `hero-device-assemble` and `terminal-to-browser-deploy` are the same
|
||||
file holding empty card divs with different headings.
|
||||
_Check:_ read the markup, then swap the name for any other item's name. If nothing in the file
|
||||
would have to change, the name is a label on a generic shell.
|
||||
_Evidence:_ the named subject has no element (no nodes in a constellation, no terminal in a
|
||||
terminal deploy).
|
||||
|
||||
**F2. Redundant duplicate.** Same **motion fingerprint** and same **markup skeleton** as another
|
||||
item that survives. Fingerprint is the gsap call list with selectors neutralised, keeping props,
|
||||
durations and eases; skeleton is the tag sequence with classes and text stripped. One wipe
|
||||
currently ships eight times with the same properties, durations and easings; one word-stagger
|
||||
ships seven times.
|
||||
_Evidence:_ both hashes match a sibling, and the sibling wins the tie-break below.
|
||||
|
||||
**F3. Renders nothing.** Frames are blank, or the named subject never appears, with the item
|
||||
mounted correctly on its own ground and its recipe applied.
|
||||
_Evidence:_ four blank frames plus the cause, in the item rather than the harness: a missing
|
||||
sibling asset, a `ReferenceError` in the console, a subject that never enters the viewport. A
|
||||
frame-capture artifact that renders correctly live is a false alarm, so confirm on a real page
|
||||
before recording it.
|
||||
|
||||
**F4. The description is a different item.** The frames show the promised event never happening: a
|
||||
wipe that never reveals its second panel, a chart that draws no series. Not a wording gap.
|
||||
|
||||
**F5. Cannot be made seekable.** Frame N genuinely depends on frame N-1 with no closed form and no
|
||||
bounded replay, and making it seekable would make it a different effect. Rare. Most accumulators
|
||||
have a trivial rewrite, so reach for this only after establishing there is none; a seeded,
|
||||
index-derived replacement for `Math.random()` is X7, not F5.
|
||||
_Evidence:_ two snapshots of the same timestamp reached by different seek paths differ.
|
||||
|
||||
## Fixable, keep and repair
|
||||
|
||||
Real defects, but the item has a reason to exist that nothing else covers and the repair is
|
||||
bounded. Log the specific fix, never "needs polish".
|
||||
|
||||
**X1. No timeline of its own.** No `__timelines` registration, so the installed artifact renders a
|
||||
still frame while the catalog page looks fine, because the generator transplants the demo's
|
||||
timeline into the preview. 97 of the 213 new components are in this state.
|
||||
_Repair:_ fold the trailing `Timeline integration:` recipe into a real `<script>` that builds a
|
||||
paused timeline and registers it. Roughly 10 to 15 minutes for a single-element item.
|
||||
_Escalates to fatal_ only when there is no motion anywhere to fold in, which usually means F1 too.
|
||||
|
||||
**X2. Name claims a technique the code lacks.** Grep the code region, never the doc header: the
|
||||
header's prose is full of the exact words you are looking for, and will report a match on an item
|
||||
that has none.
|
||||
| Name pattern | Must contain |
|
||||
| -------------------------------- | -------------------------------------------------- |
|
||||
| `spring-*` | `elastic`, `back.`, `bounce` or a custom spring ease |
|
||||
| `mask-*`, `*-mask*` | `mask` or `clip-path` |
|
||||
| `frosted*`, `*glass*` | `backdrop-filter` |
|
||||
| `*3d*`, `*depth*`, `*orbit*`, `*camera*` | `perspective`, `rotateX`, `rotateY`, `translateZ` |
|
||||
| `*-draw`, `*-trace`, `*stroke*` | `stroke-dash` or `pathLength` |
|
||||
_Repair:_ add the technique, or rename the item. Renaming is often the honest fix.
|
||||
|
||||
**X3. Illegible.** At 1920x1080 on its own ground: text under 4.5:1, or a subject whose smallest
|
||||
meaningful feature is under about 24px.
|
||||
_Repair:_ one value step, per `placeholder-material.md`. Text never sits below L1.
|
||||
|
||||
**X4. Placeholder gradients.** The purple and blue palette standing in for content.
|
||||
_Repair:_ the monochrome ramp. Already done across the catalog, so a new instance is a regression,
|
||||
not a legacy defect.
|
||||
|
||||
**X5. Hardcoded ink, no theme token.** A literal colour on the item's own text or subject with no
|
||||
`var(--...)` fallback chain, so it disappears when an author drops it on the opposite theme.
|
||||
_Repair:_ route it through the theme token with the literal as fallback.
|
||||
|
||||
**X6. No markup of its own.** The file is a `<style>` and a `<script>` and nothing else, so
|
||||
mounting it renders an empty box.
|
||||
_Repair:_ ship sample markup, or declare it an attachment snippet in `registry-item.json` and give
|
||||
the demo a host element.
|
||||
|
||||
**X7. Unseeded randomness.** Scatter derived from `Math.random()` rather than the element index.
|
||||
_Repair:_ derive from the index.
|
||||
|
||||
**X8. Declared bounds it cannot honour.** A number variable with no `min`/`max`, so the control
|
||||
offers values the item cannot express, or a default it can never return to.
|
||||
_Repair:_ declare real bounds, or use a numeric field instead of a slider.
|
||||
|
||||
## Duplicates, which one survives
|
||||
|
||||
A group is the set of items sharing both hashes from F2. Exactly one survives, chosen in order:
|
||||
|
||||
1. **The one whose name describes what the shared motion actually does.** A group where one member
|
||||
is a directional wipe and the rest borrowed it keeps the directional wipe.
|
||||
2. **Then the one with subject-specific markup.** More elements that only make sense for that
|
||||
name, not more elements.
|
||||
3. **Then the one already on `origin/main`.** Removing a shipped item breaks installs.
|
||||
4. **Never a member whose name claims something the shared implementation does not do.**
|
||||
`frosted-glass-wipe` has no `backdrop-filter`, `spring-scale-in` has no spring, `mask-reveal-up`
|
||||
has no mask, so none of those three is the survivor. Such a member is F2 and X2 at once, and X2
|
||||
cannot be fixed without breaking the group. If no member is honest, keep the plainest name.
|
||||
|
||||
**If every member of a group fails F1, the group is cut entire.** Do not preserve a survivor to
|
||||
soften the count. Twelve names on one empty card shell is one bad item, not twelve, and keeping one
|
||||
of them keeps the bad item.
|
||||
|
||||
Same motion with genuinely different subjects is not a duplicate. A bar chart, a line chart and a
|
||||
dashboard populate can share a stagger; the subject is the item.
|
||||
|
||||
## Never cut
|
||||
|
||||
Protection is per criterion, not blanket. A protected item still answers every other row.
|
||||
|
||||
**N1. Load-bearing colour** is exempt from X4 only. `chromatic-aberration-wipe` (the RGB split is
|
||||
the effect), `confetti` (multi-hue is the celebration), `matrix-decode` (green is its identity),
|
||||
`mesh-gradient-bg` (the gradient is the subject), `multi-device-splay`. `us-map`'s gradient is a
|
||||
sequential choropleth scale, which is colour carrying data.
|
||||
|
||||
**N2. Real product depiction** is exempt from X4 and F1. A Figma logo inside a Figma mock is not
|
||||
slop; the HyperFrames wordmark in `logo-brand-close` is the subject. Judge the placeholder content,
|
||||
not the depicted product.
|
||||
|
||||
**N3. Deliberate static** is exempt from X1. An item whose description promises no motion is not
|
||||
failing X1. A style snippet or a passive overlay is allowed to sit still.
|
||||
|
||||
**N4. Environment sets** are exempt from F3. An item that is a backdrop rather than a shot is not
|
||||
failing F3 for being calm. Measured PSNR between frames separates the two: sets score 45 or higher,
|
||||
things that genuinely run score 17 to 24. Judge against the description.
|
||||
|
||||
**N5. A rest state that is the recipe's `from` state** is exempt from F3. `confetti` ships
|
||||
`.particle` spans sitting at opacity 0 until the timeline fans them out. Still is not dead.
|
||||
|
||||
**N6. Attachment snippets** are exempt from F3. A text splitter has no markup by design. Grade X6.
|
||||
|
||||
**N7. The 36 items already on `origin/main`** are out of audit scope.
|
||||
|
||||
## Mechanical first pass
|
||||
|
||||
| Signal | How | Maps to |
|
||||
| ------------------- | ------------------------------------------------------------------------------------- | ------- |
|
||||
| No timeline | `grep -L __timelines` over each composition | X1 |
|
||||
| Duplicate | motion fingerprint AND markup skeleton hashes, matched pairwise | F2 |
|
||||
| Empty shell | markup skeleton matches an unrelated item, or is `<h3>` + `<p>` + generic panels only | F1 |
|
||||
| No markup at all | element count of the comment-, style- and script-stripped file is 0 | X6 |
|
||||
| Placeholder palette | grep the six banned hexes | X4 |
|
||||
| Name gap | the X2 table, grepped over the code region only | X2 |
|
||||
| Unbounded number | read `min`/`max` in `registry-item.json` | X8 |
|
||||
| Non-determinism | grep `Math.random`, `Date.now`, `performance.now`, `requestAnimationFrame` | X7, F5 |
|
||||
|
||||
## Visual pass, required for a verdict
|
||||
|
||||
Render at least four frames across the duration from the **composition**, not the demo, mounted per
|
||||
the harness rules above, and look at them. Then, for anything not scoring clean, confirm on the
|
||||
real catalog page before recording it.
|
||||
|
||||
Record per item: the criteria it fails, the evidence you saw, and fatal or fixable. An unviewed
|
||||
item is not a pass.
|
||||
|
||||
## Calibration
|
||||
|
||||
Ten items scored with this rubric, frames rendered and looked at. Three of them corrected the
|
||||
rubric rather than the other way round.
|
||||
|
||||
| Item | Expected | Frames actually showed | Verdict |
|
||||
| ---------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------ |
|
||||
| `ecosystem-constellation` | fatal | Sidebar with three pill buttons and three empty white panels. No nodes, no edges. Identical at all four timestamps. | **Cut** F1, F2, F4 |
|
||||
| `terminal-to-browser-deploy` | fatal | Pixel-identical to the above, only the `<h3>` and one subtitle differ. No terminal, no browser. | **Cut** F1, F2, F4 |
|
||||
| `frosted-glass-wipe` | fatal | One card reading "Before", static, forever; the "After" panel stays clipped. Recipe byte-identical to `directional-wipe`. | **Cut** F2, F4, X2 |
|
||||
| `char-slam-explode` | pass | Letters scattered at 0.05s, assembled into "Impact" by 1.2s, held. Real per-character motion. | **Keep** |
|
||||
| `echo-trail` | pass | Card travels left to right with a decaying blurred echo trail behind it. Legible on its own light ground. | **Keep** |
|
||||
| `logo-brand-close` | pass | "H" resolves into the full wordmark, tagline and URL land after it. Staged, legible. | **Keep** |
|
||||
| `blur-in` | borderline | Still, but the still is the correct rest state: legible headline, unique implementation, theme-token ink. | **Keep**, X1 |
|
||||
| `spring-scale-in` | borderline | Legible on its own `#f7f7f8`. Ease is `power3.out`, no spring anywhere. Shares its recipe with six others. | **Cut** F2, X2 |
|
||||
| `confetti` | borderline | Card and mesh, no particles visible. Source ships `.particle` spans the recipe fans out from opacity 0. | **Keep**, X1 |
|
||||
| `bottom-up-letters` | borderline | Four blank frames. The file is a splitter with no markup at all. | **Keep**, X6 |
|
||||
|
||||
Three corrections the calibration forced, all of them false cuts:
|
||||
|
||||
- `spring-scale-in` first scored X3 at roughly 1.5:1. That was the harness's dark stage, not the
|
||||
item. Hence "use the item's own ground".
|
||||
- `confetti` first scored F3. Its rest state is its recipe's `from` state. Hence N5.
|
||||
- `char-slam-explode` first scored F3 with four black frames. It is a sub-composition and was being
|
||||
inlined. Hence the two-shapes rule.
|
||||
|
||||
A rubric that cuts a working item is wrong even when the verdict is convenient.
|
||||
|
||||
## Checklist
|
||||
|
||||
Per item, in order. Stop at the first fatal.
|
||||
|
||||
- [ ] Mounted the right way for its shape, on its own ground, with GSAP and a paused root.
|
||||
- [ ] F1: the markup contains the named subject.
|
||||
- [ ] F2: motion fingerprint and markup skeleton are not both shared with a survivor.
|
||||
- [ ] F3: it renders something with the recipe applied, or it is protected by N3 to N6.
|
||||
- [ ] F4: the frames show the event the description promises.
|
||||
- [ ] F5: frame N is computed from N.
|
||||
- [ ] X1 through X8 logged with the specific fix.
|
||||
- [ ] Verdict cites the evidence the criterion names, not an impression.
|
||||
@@ -1,180 +0,0 @@
|
||||
# Contributing a Block or Component to the Registry
|
||||
|
||||
Guide the user from idea to merged PR for a new registry block or component.
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
1. Clarify → 2. Scaffold → 3. Build → 4. Validate → 5. Preview → 6. Ship
|
||||
```
|
||||
|
||||
### Step 1: Clarify
|
||||
|
||||
Ask what they're building. The registry has two item types:
|
||||
|
||||
- **Block** (`registry/blocks/`, type `hyperframes:block`) — a full standalone composition with fixed dimensions and duration. Caption styles, VFX effects, title cards, lower thirds.
|
||||
- **Component** (`registry/components/`, type `hyperframes:component`) — a reusable snippet with no fixed dimensions or duration. CSS effects, text treatments, overlays that adapt to any composition size.
|
||||
|
||||
Then ask:
|
||||
|
||||
- One-sentence description of the effect
|
||||
- Visual reference (URL, screenshot, or description)
|
||||
- Who uses this and when?
|
||||
|
||||
### Step 2: Scaffold
|
||||
|
||||
Create the registry structure:
|
||||
|
||||
**For blocks:**
|
||||
|
||||
```
|
||||
registry/blocks/{block-name}/
|
||||
{block-name}.html
|
||||
registry-item.json
|
||||
```
|
||||
|
||||
**For components:**
|
||||
|
||||
```
|
||||
registry/components/{component-name}/
|
||||
{component-name}.html
|
||||
registry-item.json
|
||||
```
|
||||
|
||||
**Naming convention:**
|
||||
|
||||
| Item name | ID prefix | Example IDs |
|
||||
| ---------------- | --------- | ---------------------- |
|
||||
| `cap-hormozi` | `hz` | `hz-cg-0`, `hz-cw-3` |
|
||||
| `cap-typewriter` | `tw` | `tw-cg-0`, `tw-ch-0-5` |
|
||||
| `vfx-chrome` | `vc` | `vc-canvas` |
|
||||
|
||||
Use a 2-3 letter prefix. ALL element IDs must use this prefix to avoid collisions in sub-compositions.
|
||||
|
||||
**registry-item.json** — use the canonical templates in [templates.md](templates.md) (block and component variants, both with all required fields).
|
||||
|
||||
### Step 3: Build
|
||||
|
||||
Apply the correct template based on type. See [templates.md](templates.md) for copy-paste starters.
|
||||
|
||||
#### Caption blocks
|
||||
|
||||
**Non-negotiable caption rules:**
|
||||
|
||||
- Font: **96px minimum** for proportional fonts. **64-72px acceptable for monospace** (wider characters need less size).
|
||||
- Readability: `-webkit-text-stroke: 2-3px` OR multi-layer `text-shadow`
|
||||
- Overflow: call `window.__hyperframes.fitTextFontSize()` on every group
|
||||
- Karaoke: highlight active word via `tl.to(wordEl, { color/scale }, WORDS[wi].start)`
|
||||
- Hard kill: `tl.set(groupEl, { opacity: 0, visibility: "hidden" }, g.end)` on EVERY group
|
||||
- **Never use `tl.from(el, { opacity: 0 })` at the same position as `tl.set(el, { opacity: 1 })`** — the from clobbers the set. Use `tl.to` instead.
|
||||
|
||||
**Per-character animation** (typewriter, scramble):
|
||||
|
||||
- Wrap each character in `<span>` with ID `{prefix}-ch-{group}-{char}`
|
||||
- Stagger via `tl.set` at computed intervals from word timestamps
|
||||
- Cursors/decorative elements: use `tl.set` at intervals — NOT CSS animation (not seekable)
|
||||
|
||||
**Positioning variants:**
|
||||
|
||||
- Centered: `display: flex; align-items: center; justify-content: center;`
|
||||
- Lower-third: `position: absolute; bottom: 100px; left: 0; width: 100%; text-align: center;`
|
||||
- Left-aligned: `position: absolute; bottom: 100px; left: 120px; text-align: left;`
|
||||
|
||||
#### VFX blocks (Three.js)
|
||||
|
||||
- Use `three@0.147.0` from CDN (global script)
|
||||
- `tl.eventCallback("onUpdate", renderScene); renderScene();` — NO requestAnimationFrame
|
||||
- State proxy pattern: GSAP animates plain JS object, render function reads it
|
||||
- Seeded PRNG (`mulberry32`) for randomness
|
||||
|
||||
#### All types
|
||||
|
||||
- **Load files the block ships through `__hyperframes.assetUrl("assets/...")`** whenever a script
|
||||
builds the path (textures, fonts, JSON, models). Attributes like `src` and `href` are rebased
|
||||
when the block is mounted; strings in a script are not, so a bare `"assets/..."` 404s once the
|
||||
block lives under `compositions/<name>/`. A separate `.js` file gets the URLs from the block's
|
||||
inline script. A block whose assets install to the project's own `assets/` (target
|
||||
`compositions/<name>.html`) keeps plain project-root paths; `assetUrl` would look beside the file.
|
||||
- **Placeholder content is monochrome** — read
|
||||
[placeholder-material.md](placeholder-material.md) before choosing a single colour. Stand-in
|
||||
screens, images, cards, avatars, logos and chart series use four alpha steps of the
|
||||
composition's ink; accent marks one element and never a placeholder.
|
||||
- `data-composition-id` MUST match `window.__timelines["id"]`
|
||||
- All element IDs prefixed with block abbreviation
|
||||
- `gsap.timeline({ paused: true })` — always paused
|
||||
- No `Math.random()`, no `Date.now()`
|
||||
|
||||
### Step 4: Validate
|
||||
|
||||
```bash
|
||||
hyperframes lint # 0 errors required
|
||||
hyperframes check --no-contrast # 0 console errors required
|
||||
```
|
||||
|
||||
### Step 5: Preview
|
||||
|
||||
```bash
|
||||
# Render preview video
|
||||
hyperframes render -o preview.mp4
|
||||
|
||||
# Snapshot for visual QA
|
||||
hyperframes snapshot --at "1.0,3.0,5.0,7.0"
|
||||
|
||||
# Publish to hyperframes.dev for review
|
||||
npx hyperframes publish
|
||||
```
|
||||
|
||||
**Catalog preview image** — For the default PNG preview, save your snapshot at `docs/images/catalog/{kind}/{name}.png` in the repository checkout (`{kind}` is `blocks` or `components`). After upload, the catalog serves it from `https://static.heygen.ai/hyperframes-oss/docs/images/catalog/{kind}/{name}.png`. If `registry-item.json` declares `preview`, the card uses its `poster` URL; a `preview` without `poster` has no image fallback.
|
||||
|
||||
- **HeyGen internal contributors:** run `scripts/upload-docs-images.sh` from the repository root (requires AWS profile `engineering-767398024897`)
|
||||
- **External contributors:** attach the preview MP4 to your PR description. A maintainer will generate and upload the catalog image before merging.
|
||||
|
||||
### Step 6: Ship
|
||||
|
||||
**All steps are required. Missing any one produces a broken catalog entry.**
|
||||
|
||||
`{kind}` is `blocks` or `components` depending on what you built in Step 1.
|
||||
|
||||
```bash
|
||||
# 1. Create branch
|
||||
git checkout -b feat/registry-{name}
|
||||
|
||||
# 2. Format HTML
|
||||
npx oxfmt registry/{kind}/{name}/*.html
|
||||
|
||||
# 3. Regenerate registry/registry.json from the item directories.
|
||||
# Do not hand-edit it: an entry added by hand survives until the next
|
||||
# regeneration and then vanishes, and one left behind for a directory that
|
||||
# no longer exists is worse, because `hyperframes add <name>` resolves the
|
||||
# name and then fails on missing files.
|
||||
npx tsx scripts/generate-registry-items.ts
|
||||
|
||||
# 4. Generate catalog docs page
|
||||
npx tsx scripts/generate-catalog-pages.ts
|
||||
|
||||
# 5. Publish to hyperframes.dev so reviewers can preview
|
||||
npx hyperframes publish
|
||||
|
||||
# 6. Stage everything
|
||||
git add registry/{kind}/{name}/ registry/registry.json docs/catalog/
|
||||
|
||||
# 7. Commit
|
||||
git commit -m "feat(registry): add {name} — {one sentence}"
|
||||
|
||||
# 8. Push and open PR with hyperframes.dev link
|
||||
git push origin feat/registry-{name}
|
||||
gh pr create --title "feat(registry): {name}" --body "preview: {hyperframes.dev-url}"
|
||||
```
|
||||
|
||||
**If you don't have a GitHub account:** you need one to open a PR. Sign up at https://github.com/signup, then run `gh auth login`.
|
||||
|
||||
## Quality Gate
|
||||
|
||||
- [ ] `hyperframes lint` → 0 errors
|
||||
- [ ] `hyperframes check` → 0 console errors
|
||||
- [ ] `npx oxfmt --check` passes
|
||||
- [ ] `registry/registry.json` updated with new entry
|
||||
- [ ] `scripts/generate-catalog-pages.ts` run (docs page generated)
|
||||
- [ ] `npx hyperframes publish` run (claim your project URL)
|
||||
- [ ] Preview MP4 attached to PR (external) or catalog PNG uploaded (internal)
|
||||
- [ ] All IDs unique and prefixed
|
||||
@@ -1,54 +0,0 @@
|
||||
# The demo.html Convention
|
||||
|
||||
## Why components ship demo.html
|
||||
|
||||
Every component in the registry ships a companion `demo.html` file alongside its snippet. The demo serves two purposes:
|
||||
|
||||
1. **Preview fixture** — the CI preview pipeline renders the demo to generate thumbnail images and preview videos for the catalog docs page.
|
||||
|
||||
2. **Usage example** — the demo shows the component effect applied to representative content, serving as a working reference.
|
||||
|
||||
## Demo structure
|
||||
|
||||
A demo is a complete, standalone HTML composition:
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Component Name — Demo</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
/* reset + canvas size */
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div data-composition-id="<name>-demo" data-width="1920" data-height="1080" data-duration="N">
|
||||
<!-- Demo content showing the effect -->
|
||||
<!-- Component snippet inlined here -->
|
||||
</div>
|
||||
<script>
|
||||
// GSAP timeline demonstrating the effect
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines["<name>-demo"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Key conventions:
|
||||
|
||||
- `data-composition-id` is `<component-name>-demo` to avoid collisions
|
||||
- The demo is self-contained — all CSS and JS from the snippet is inlined
|
||||
- The GSAP timeline is registered on `window.__timelines`
|
||||
- Duration should be long enough to showcase the effect (typically 5-8 seconds)
|
||||
|
||||
## Blocks don't need demo.html
|
||||
|
||||
Blocks are already standalone compositions that can be rendered directly. Only components need the demo wrapper.
|
||||
|
||||
## Demos are not installed
|
||||
|
||||
The `demo.html` is NOT installed by `hyperframes add` — it exists only in the registry for preview generation and as a reference.
|
||||
@@ -1,217 +0,0 @@
|
||||
# Registry discovery
|
||||
|
||||
<!-- registry-items: allow=data-composition-id,data-start,data-track-index -->
|
||||
|
||||
## Use the catalog command first
|
||||
|
||||
```bash
|
||||
npx hyperframes catalog
|
||||
npx hyperframes catalog --type block
|
||||
npx hyperframes catalog --type component
|
||||
npx hyperframes catalog --type block --tag social
|
||||
npx hyperframes catalog --json
|
||||
npx hyperframes catalog --human-friendly
|
||||
```
|
||||
|
||||
- Default output is a readable table. It does not install anything.
|
||||
- `--type` accepts `block` or `component`; `--tag` may narrow either result.
|
||||
- `--json` is the deterministic agent and CI surface. Select a name, then run `npx hyperframes add <name>`.
|
||||
- `--human-friendly` opens a picker and installs the selected item immediately.
|
||||
|
||||
## Read the registry manifest as a fallback
|
||||
|
||||
When the CLI is unavailable, the top-level `registry.json` lists all available items:
|
||||
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry/registry.json
|
||||
```
|
||||
|
||||
Each entry has `name` and `type` (`hyperframes:example`, `hyperframes:block`, or `hyperframes:component`).
|
||||
|
||||
## Reading an item's manifest
|
||||
|
||||
Each item has a `registry-item.json` with full metadata:
|
||||
|
||||
```
|
||||
<base>/<type-dir>/<name>/registry-item.json
|
||||
```
|
||||
|
||||
Where `<type-dir>` is `examples`, `blocks`, or `components`.
|
||||
|
||||
## Item manifest fields
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ---------------------- | -------- | -------- | ---------------------------------------------- |
|
||||
| `name` | string | yes | Kebab-case identifier |
|
||||
| `type` | string | yes | `hyperframes:block` or `hyperframes:component` |
|
||||
| `title` | string | yes | Human-readable title |
|
||||
| `description` | string | yes | One-line description |
|
||||
| `tags` | string[] | no | Filter tags (e.g., `["data", "chart"]`) |
|
||||
| `dimensions` | object | blocks | `{ width, height }` — blocks only |
|
||||
| `duration` | number | blocks | Duration in seconds — blocks only |
|
||||
| `files` | array | yes | Files to install (`path`, `target`, `type`) |
|
||||
| `registryDependencies` | string[] | no | Other registry items this depends on |
|
||||
|
||||
## Available items
|
||||
|
||||
### Blocks
|
||||
|
||||
For an always-current list run `npx hyperframes catalog --type block`. **The search is the source of truth; the tables below are a hand-maintained sample and under-cover by design** — they group 97 of the registry's 180 blocks by category, so a block missing from them is not evidence the registry lacks it. Only an empty `npx hyperframes catalog --query "<what you want>" --json` is that evidence. Do not grow these tables toward the full set; search instead. **Block name ≠ shader name**: shader-transition blocks (e.g. `domain-warp-dissolve`) wrap a HyperShader runtime whose internal name omits the `-dissolve`/`-warp` suffix — see the showcase HTML installed alongside the block for the canonical name.
|
||||
|
||||
#### Shader transitions (14)
|
||||
|
||||
Single-shader blocks; each installs one HyperShader runtime + a showcase composition. Use ≤2 per video.
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| `chromatic-radial-split` | Chromatic aberration radial split |
|
||||
| `cinematic-zoom` | Dramatic zoom blur |
|
||||
| `cross-warp-morph` | Cross-warped morphing |
|
||||
| `domain-warp-dissolve` | Fractal noise domain warping |
|
||||
| `flash-through-white` | White flash crossfade (rarely a neutral default — see SKILL.md guidance) |
|
||||
| `glitch` | Digital glitch artifacts |
|
||||
| `gravitational-lens` | Gravitational lensing distortion |
|
||||
| `light-leak` | Cinematic light leak overlay |
|
||||
| `ridged-burn` | Ridged turbulence burn |
|
||||
| `ripple-waves` | Concentric ripple wave distortion |
|
||||
| `sdf-iris` | Signed-distance-field iris reveal |
|
||||
| `swirl-vortex` | Swirling vortex distortion |
|
||||
| `thermal-distortion` | Heat-haze thermal distortion |
|
||||
| `whip-pan` | Fast camera whip-pan |
|
||||
|
||||
#### Transition galleries (13)
|
||||
|
||||
Showcase compositions grouping multiple CSS / GSAP transition styles by family. Use as reference for picking a CSS scene transition; not meant to embed as-is.
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------- | ----------------------------------- |
|
||||
| `transitions-3d` | 3D perspective flip and rotate |
|
||||
| `transitions-blur` | Blur-based scene transitions |
|
||||
| `transitions-cover` | Cover / uncover slide |
|
||||
| `transitions-destruction` | Destructive break-apart |
|
||||
| `transitions-dissolve` | Dissolve and fade |
|
||||
| `transitions-distortion` | Warp and distortion |
|
||||
| `transitions-grid` | Grid-based tile |
|
||||
| `transitions-light` | Light-based glow and flash |
|
||||
| `transitions-mechanical` | Mechanical shutter and iris |
|
||||
| `transitions-other` | Misc creative (VHS, gravity, morph) |
|
||||
| `transitions-push` | Push and slide |
|
||||
| `transitions-radial` | Radial wipe and reveal |
|
||||
| `transitions-scale` | Scale and zoom |
|
||||
|
||||
#### Liquid Glass (7)
|
||||
|
||||
WebGPU + html-in-canvas frosted-glass surfaces. **Require Brave / Chrome canary** with WebGPU enabled — set `PRODUCER_HEADLESS_SHELL_PATH` to point at the browser; engine auto-passes `--enable-unsafe-webgpu`. See `/hyperframes` → `references/runtimes.md`.
|
||||
|
||||
| Name | Description |
|
||||
| --------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `ios26-liquid-glass` | 3D iPhone (GLTF) + iOS 26 home screen, glass app icons, shader wallpaper, notifications |
|
||||
| `macos-tahoe-liquid-glass` | 3D MacBook (GLTF) + macOS Tahoe-style desktop, glass menu bar, Finder, dock |
|
||||
| `liquid-glass-widgets` | Frosted stat cards, showcase panel, pill chips over aurora shader |
|
||||
| `liquid-glass-notification` | Frosted notification cards floating over aurora shader |
|
||||
| `vfx-liquid-glass` | Bare VFX composition shell for liquid-glass effects |
|
||||
|
||||
#### VFX (6)
|
||||
|
||||
HTML-in-canvas + WebGL composition blocks. See `/hyperframes` → `references/runtimes.md` for the underlying runtimes.
|
||||
|
||||
| Name | Description |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `vfx-iphone-device` | GLTF iPhone 15 Pro Max + MacBook Pro with live HTML-in-canvas screens, glass-lens morph, 360° turntable |
|
||||
| `vfx-liquid-background` | Organic liquid sim — vertex displacement on subdivided plane, HTML floats above |
|
||||
| `vfx-magnetic` | VFX shell (magnetic field-line treatment) |
|
||||
| `vfx-portal` | VFX shell (portal reveal) |
|
||||
| `vfx-shatter` | VFX shell (shatter into fragments) |
|
||||
| `vfx-text-cursor` | Cursor glow + chromatic shadow rays + spectral edges on a black stage |
|
||||
|
||||
#### Showcases (6)
|
||||
|
||||
Story-driven showcase compositions — narrated YouTube-style inserts. Most include bundled SFX.
|
||||
|
||||
| Name | Description |
|
||||
| -------------------------- | ----------------------------------------------------------------- |
|
||||
| `app-showcase` | Three floating smartphone screens, fitness app product showcase |
|
||||
| `apple-money-count` | Counter $0 → $10,000, green flash, money-icon burst, SFX |
|
||||
| `blue-sweater-intro-video` | Warm AI-creator intro resolving into an X follow card |
|
||||
| `north-korea-locked-down` | Map zoom with red scribble circle, locked-down pop-up label |
|
||||
| `nyc-paris-flight` | Map animation, plane NYC → Paris, marker circle, landing pop, SFX |
|
||||
| `vpn-youtube-spot` | App-store scroll, VPN install flow, SFX |
|
||||
|
||||
#### Maps + data viz (8)
|
||||
|
||||
D3 + GSAP animated geographies and charts.
|
||||
|
||||
| Name | Description |
|
||||
| ---------------------------------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `us-map` | US choropleth, staggered state reveals, value labels, gradient legend |
|
||||
| `us-map-bubble` | US bubble map — proportional city markers, callouts, connection lines |
|
||||
| `us-map-flow` | US flow map — animated origin-destination arcs |
|
||||
| `us-map-hex` | US hex-grid map — each state as equal-weight hex with data fill |
|
||||
| `spain-map` | Spain choropleth by autonomous community — D3 conic conformal |
|
||||
| `world-map` | World choropleth + rotating globe inset, D3 Natural Earth |
|
||||
| `data-chart` | Animated bar + line chart, staggered reveal, NYT-style typography |
|
||||
| `flowchart` / `flowchart-vertical` | Decision tree, SVG connectors, sticky-note nodes, cursor + typing correction (vertical = portrait) |
|
||||
|
||||
#### Social overlays (7)
|
||||
|
||||
Platform-recognizable UI overlays. Stamp on top of a beat or use as a beat closer.
|
||||
|
||||
| Name | Description |
|
||||
| -------------------- | ------------------------------------------------ |
|
||||
| `instagram-follow` | Profile card + follow button |
|
||||
| `tiktok-follow` | Profile card + follow button |
|
||||
| `yt-lower-third` | YouTube subscribe lower third with avatar |
|
||||
| `x-post` | X/Twitter post card with engagement metrics |
|
||||
| `reddit-post` | Post card with upvotes and comments |
|
||||
| `spotify-card` | Now-playing card with album art and progress bar |
|
||||
| `macos-notification` | macOS-style banner with app icon and message |
|
||||
|
||||
#### Branding + 3D UI (2)
|
||||
|
||||
| Name | Description |
|
||||
| -------------- | ------------------------------------------------------------------- |
|
||||
| `logo-outro` | Piece-by-piece logo assembly, glow bloom, tagline fade-in, URL pill |
|
||||
| `ui-3d-reveal` | Perspective 3D reveal for UI elements |
|
||||
|
||||
#### Code snippets (24)
|
||||
|
||||
A code/terminal window that **types a code or shell session per-character**. Theme = visual chrome only; structure, wiring, and install are identical across all 24 — pick one by name, wire it like any block (`data-composition-id` + `data-start` + `data-track-index`, see `wiring-blocks.md`). Two chrome families:
|
||||
|
||||
**Apple Terminal (12)** — macOS Terminal.app window typing a shell session. Profile = window colors:
|
||||
|
||||
| Name | Look |
|
||||
| --------------------------------- | -------------------------------- |
|
||||
| `code-snippet-apple-terminal-pro` | Black bg, grey text, lime cursor |
|
||||
|
||||
#### Code Animations (9)
|
||||
|
||||
The richer, motion-first counterpart to the static `code-snippet-*` window themes above: each is a self-contained 1920×1080 block (~5–8s) with a paused, deterministic GSAP timeline that _animates_ code — typing, diffing, morphing, spotlighting, or GPU hero reveals — rather than typing a fixed snippet inside editor/terminal chrome. **Reuse-first**: `npx hyperframes add <name>`, then customize the baked code/diff content in place; hand-author only when no block covers the motion you need.
|
||||
|
||||
**DOM / text reveal (6):**
|
||||
|
||||
| Name | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `code-typing` | Token-streamed typing reveal, caret tracks the frontier (no CSS animation) — live-coding on screen |
|
||||
| `code-diff` | An edit shown as a colored diff: removed lines collapse red, added expand green — before/after at line level |
|
||||
| `code-morph` | One snippet transforms into another, tokens glide between positions (Shiki Magic Move) — a refactor / one state to another |
|
||||
| `code-highlight` | A highlight band sweeps a target line while surrounding context dims — spotlight one line |
|
||||
| `code-scroll` | Camera scrolls a long file to center + spotlight a target line — walk through a real module |
|
||||
|
||||
**GPU / WebGL hero reveals (3):** heavier, for a title-card / hero code moment.
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------ | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `code-3d-extrude` | Syntax-highlighted code on a lit beveled 3D slab that rotates through real space and settles (true WebGL depth) |
|
||||
| `code-shader-dissolve` | Code resolves out of seeded noise with a chromatic dissolve front + edge glow, then holds crisp |
|
||||
| `code-particle-assemble` | Thousands of GPU points fly to the exact glyph pixels and resolve into readable syntax-highlighted code |
|
||||
|
||||
### Components
|
||||
|
||||
| Name | Description | Tags |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| `grain-overlay` | Animated film grain texture overlay | texture, grain, overlay, film |
|
||||
| `shimmer-sweep` | CSS gradient light sweep for AI accents | text, shimmer, highlight, effect |
|
||||
| `morph-text` | Gooey text morph cycling an editable word list (SVG threshold + GSAP blur) | text, text-effect, typography, morph, gooey |
|
||||
| `grid-pixelate-wipe` | Grid dissolve transition between scenes | transition, wipe, grid, pixelate |
|
||||
| `parallax-zoom` | Center card scales up to fill the frame while siblings parallax outward (single `--pz-progress` 0→1) | transition, zoom, parallax, grid, hero |
|
||||
| `parallax-unzoom` | Reverse of `parallax-zoom` — focus card shrinks from full frame as siblings parallax in (`--pu-progress`) | transition, reveal, unzoom, parallax, grid, hero |
|
||||
@@ -1,45 +0,0 @@
|
||||
# Install Locations
|
||||
|
||||
## Default paths
|
||||
|
||||
| Item type | Default install path | Configured by |
|
||||
| --------- | ------------------------------------- | ----------------------------------- |
|
||||
| Block | `compositions/<name>.html` | `hyperframes.json#paths.blocks` |
|
||||
| Component | `compositions/components/<name>.html` | `hyperframes.json#paths.components` |
|
||||
|
||||
## How path remapping works
|
||||
|
||||
The `target` field in each item's `registry-item.json` specifies a default install path. The `add` command remaps the prefix based on `hyperframes.json#paths`:
|
||||
|
||||
- Block targets starting with `compositions/` get remapped to `<paths.blocks>/`
|
||||
- Component targets starting with `compositions/components/` get remapped to `<paths.components>/`
|
||||
|
||||
## hyperframes.json
|
||||
|
||||
Created automatically by `hyperframes init`. If it doesn't exist when you run `add`, the CLI creates it with defaults:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
|
||||
"registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
|
||||
"paths": {
|
||||
"blocks": "compositions",
|
||||
"components": "compositions/components",
|
||||
"assets": "assets"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom layouts
|
||||
|
||||
To install blocks into a `scenes/` directory instead of `compositions/`:
|
||||
|
||||
```json
|
||||
{
|
||||
"paths": {
|
||||
"blocks": "scenes"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then `hyperframes add data-chart` writes to `scenes/data-chart.html` instead of `compositions/data-chart.html`. The snippet output reflects the remapped path.
|
||||
@@ -1,149 +0,0 @@
|
||||
# Placeholder material
|
||||
|
||||
Registry items ship with stand-in content: a rectangle where the user's screenshot goes, a
|
||||
bar where their number goes, a chip where their teammate's face goes. That stand-in is
|
||||
**placeholder material**, and it is monochrome. No hue, no colour ramps, no purple-to-blue
|
||||
diagonal fill.
|
||||
|
||||
The rule exists because placeholder colour is always arbitrary. A violet card, a blue logo
|
||||
tile and a rainbow avatar row assert a brand the composition does not have; across 250 items
|
||||
the same three arbitrary hues become the catalog's identity instead of the author's. Value
|
||||
says everything colour was saying here, and says it truthfully.
|
||||
|
||||
## The one rule
|
||||
|
||||
> Placeholder content carries no hue. It is built from four alpha steps of the composition's
|
||||
> own ink over the composition's own ground, plus a hairline. Accent marks one element per
|
||||
> composition, and never a placeholder.
|
||||
|
||||
## The ramp
|
||||
|
||||
Four fill steps and a hairline. All of them are the ink at an alpha, so the ramp inverts for
|
||||
free on dark themes and needs no second table.
|
||||
|
||||
| Step | Ink alpha | What it is for |
|
||||
| ------ | --------- | ---------------------------------------------------------------------------- |
|
||||
| `ink` | 100% | Real content: headline, number, label. Not placeholder. |
|
||||
| `L1` | 72% | The subject. Chart bars, a filled logo mark, a play glyph, a front avatar. |
|
||||
| `L2` | 45% | Support. A comparison series, avatars behind the front one, secondary icons. |
|
||||
| `L3` | 18% | Media fill and skeleton text lines. "Content lives here." |
|
||||
| `L4` | 8% | Recessed plate interiors. The tray a card sits in. |
|
||||
| `hair` | 14% | Every 1px boundary. Replaces the separation a gradient edge was doing. |
|
||||
|
||||
Write them the way the file already writes colour:
|
||||
|
||||
```css
|
||||
/* theme-token primitives (--fg / --surface available) */
|
||||
background: color-mix(in srgb, var(--fg, #f8fafc) 18%, transparent);
|
||||
|
||||
/* fixed-palette ports (local --hf-ink, no theme tokens) */
|
||||
background: rgba(17, 24, 39, 0.18);
|
||||
```
|
||||
|
||||
**Four steps, not eight.** Grays go muddy when adjacent steps sit close together, so every
|
||||
step is at least 1.6x the alpha of the one below it. Reaching for a fifth value is the signal
|
||||
that the layout, not the palette, needs the work. Snap to the nearest step instead.
|
||||
|
||||
Contrast, measured against both shipped grounds (`neutral` light `#fcfcfd`, `bold` dark
|
||||
`#1b1230`): L1 is 7.7:1 / 8.9:1, L2 is 3.1:1 / 4.4:1, L3 and L4 are below 3:1 by design.
|
||||
Therefore **text never goes below L1**, and L2 is the floor for any graphic a viewer has to
|
||||
compare (a chart series, a state indicator). L3 and L4 are for surfaces only.
|
||||
|
||||
## Depth without colour
|
||||
|
||||
In priority order. Reach for the first one that works.
|
||||
|
||||
1. **Value step.** Nothing sits on the same step as its ground. One full step of separation
|
||||
minimum. This alone resolves most stacking.
|
||||
2. **Hairline.** `1px` of `hair` on every plate boundary. A border separates two surfaces more
|
||||
cleanly than a gradient ever did, and survives video compression that eats a soft edge.
|
||||
3. **Negative space.** Padding is elevation. A plate with `--space-2` inside it reads as raised
|
||||
without any fill difference at all.
|
||||
4. **Texture, at the finest grain only.** A `repeating-linear-gradient` at `L3`-and-below,
|
||||
period 2-4px, achromatic. This is the one thing value cannot say: a flat gray rectangle
|
||||
reads as an empty box, the same rectangle finely hatched reads as a surface with content on
|
||||
it. Use it to mark media, nothing else.
|
||||
|
||||
Shadows are allowed and must be achromatic and diffuse: `0 Npx 3Npx rgba(0,0,0,0.18)`. Never a
|
||||
coloured glow; a glow is an accent wearing a shadow's clothes.
|
||||
|
||||
## Reading each kind in black and white
|
||||
|
||||
A gray rectangle has to still say _screen_ and not _image_. Shape, aspect, glyph and texture
|
||||
carry the meaning that hue was carrying badly.
|
||||
|
||||
| Kind | Signature |
|
||||
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Screen / device | Device aspect + `--radius` + `hair` + scanline texture at L3 + inset vignette. The chrome is the signal; the fill is L3 and stays quiet. |
|
||||
| Video footage | Screen signature plus a centred play triangle in a ring at L1. The glyph is what makes it video rather than a static screen. |
|
||||
| Image | 4:3 or 3:2 plate at L3 with a horizon-and-disc glyph at L2, offset toward a corner, never centred. Offset is what reads as "photo". |
|
||||
| Card | Plate at L4, `hair` border, two skeleton rules at L3 (100% and 62% width). Two rules of unequal length is the whole idiom. |
|
||||
| Avatar / person | **Circle** at L2 with initials knocked out in the ground colour. Stacks differ by step (L1 front, L2, L3), never by hue. |
|
||||
| Logo / brand | **Rounded square** at L1 with the mark knocked out in the ground colour. Every mark on the same step: a logo wall means "many, equal". |
|
||||
| Chart series | Flat fills. Primary series L1, comparison series L2, gridlines `hair`. Never a gradient inside a bar. |
|
||||
| Text block | Rounded rules at L3, widths 100 / 86 / 62%. The ragged right edge is what reads as prose. |
|
||||
|
||||
Circle means person, rounded square means app or brand. Keeping those two shapes distinct is
|
||||
what lets both live at the same value step without ambiguity.
|
||||
|
||||
Why no gradient inside a chart bar: a vertical light-to-dark fill makes every bar lighter at
|
||||
its top, so the tallest bar reads _palest_ exactly where the eye lands to compare heights. The
|
||||
decoration contradicts the data. Flat fills are both plainer and more honest.
|
||||
|
||||
## Accent
|
||||
|
||||
The accent enum stays. `green` rides `--brand`, `blue` rides `--accent`, `violet` rides
|
||||
`--accent-2`, exactly as before, and every declared `accent` or `tone` variable keeps working
|
||||
unchanged. What changes is where accent is allowed to land:
|
||||
|
||||
> Accent marks at most **one** element per composition, and only where the accent _is_ the
|
||||
> meaning: the selected tier, the current step, the figure a count-up lands on, the ink of a
|
||||
> stroke being drawn. Placeholder content is never accent-coloured.
|
||||
|
||||
Fixed-palette ports have no theme tokens, so they get the accent through their own
|
||||
variable with a neutral fallback:
|
||||
|
||||
```css
|
||||
/* was: --hf-accent: #2563eb; */
|
||||
--hf-accent: var(--accent, #18181b);
|
||||
```
|
||||
|
||||
Unthemed, the item renders in pure black and white. Drop it into a themed composition and
|
||||
the author's accent lands on that one element and nowhere else. Where the accent is chosen by
|
||||
an enum, the lookup table maps to tokens, never to hex: `green` to `var(--brand, <neutral>)`,
|
||||
`blue` to `var(--accent, <neutral>)`, `violet` to `var(--accent-2, <neutral>)`. The enum's
|
||||
declared options and default are unchanged, so no mount breaks.
|
||||
|
||||
"One element" means one role, not one node: an app mark repeated on three devices of the same
|
||||
mock is still one element.
|
||||
|
||||
Killing accent outright was the alternative and it is worse. Accent is already load-bearing in
|
||||
exactly the cases where it is correct, and it is the only hook an author has for their own
|
||||
brand; removing it would push those cases into value tricks for a job one hue does better with
|
||||
one element. The failure was never that accent existed, it was that accent had become the
|
||||
default fill for every placeholder. Capping it at one element fixes the failure with no
|
||||
variable migration.
|
||||
|
||||
## Gradients that stay
|
||||
|
||||
The ban is on **hue-carrying fills**, not on the CSS function. These are material, not
|
||||
placeholder, and flattening them breaks the thing the item exists to do:
|
||||
|
||||
- **Physical surfaces** — device bezels, brushed metal, glass, a screen's inner vignette.
|
||||
- **Sheens and sweeps** — `linear-gradient(90deg, transparent, rgba(255,255,255,0.75), transparent)`
|
||||
driven across an element. That is motion, not decoration.
|
||||
- **Scrims and vignettes** — `rgba(0,0,0,α)` ramps that buy text contrast over media.
|
||||
- **Effects whose subject is the gradient** — aurora, liquid glass, chromatic aberration,
|
||||
grain fields, shader transitions. The gradient is the product.
|
||||
|
||||
All four are achromatic or physically motivated. If a gradient is neither, it is placeholder
|
||||
slop and it goes.
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] No `#7c3aed`, `#2563eb`, `#6366f1`, `#8b5cf6`, `#a855f7`, `#4f46e5` anywhere in the item.
|
||||
- [ ] Every placeholder fill is `ink` at 72 / 45 / 18 / 8 / 14 percent, and nothing else.
|
||||
- [ ] At most one accent-coloured element, and it means something.
|
||||
- [ ] Text sits at L1 or above; L3 and L4 carry no text.
|
||||
- [ ] Every remaining gradient is a surface, a sweep, a scrim, or the effect itself.
|
||||
- [ ] `hyperframes check` passes, and a rendered frame was looked at.
|
||||
@@ -1,67 +0,0 @@
|
||||
# Searching the catalog
|
||||
|
||||
How `hyperframes catalog` ranks, what its `--json` envelope means, offline behaviour, and reporting a miss.
|
||||
|
||||
Use the CLI as the primary discovery surface. **Search by intent before browsing:** the registry holds more items than you can scan by eye, so listing them and matching on names or tags is the slow path, and it fails whenever the author's wording differs from yours.
|
||||
|
||||
```bash
|
||||
# Rank the whole catalog against what the beat should do
|
||||
npx hyperframes catalog --query "reveal a headline one line at a time"
|
||||
npx hyperframes add caption-clip-wipe
|
||||
```
|
||||
|
||||
Search is local and sends nothing. By default it ranks on vocabulary shared with the item's name, title and description, so it only finds items that reuse your words; `--on-device` ranks by meaning instead, after a one-time model download. With `--json` the envelope names which tier answered, so check that rather than assuming a ranking happened.
|
||||
|
||||
**Always query in English, whatever language the video is in.** The catalog is written in English and both tiers index it that way (the on-device model is English-only too). A query in another script produces no searchable terms and returns nothing at all. This is easy to get wrong on a Japanese or Chinese project, where the brief, the captions and the narration are all in that language and the query naturally follows: describe the _move_ in English, then write the on-screen copy in whatever language the video needs. If a query does come back with `No searchable words in query`, that is this rule, not a missing component, and it is not worth a gap report.
|
||||
|
||||
Installability is applied after ranking, not before it: a name the vectors carry but this registry cannot serve is dropped from the results and counted in `dropped`, so a non-zero `dropped` means the two are different generations. The offline tier, the consent gate and refreshing a stale index are under "Search tiers" below.
|
||||
|
||||
To browse or filter instead of search:
|
||||
|
||||
```bash
|
||||
npx hyperframes catalog
|
||||
npx hyperframes catalog --type block
|
||||
npx hyperframes catalog --type component
|
||||
npx hyperframes catalog --type block --tag social
|
||||
npx hyperframes catalog --json
|
||||
npx hyperframes catalog --human-friendly
|
||||
```
|
||||
|
||||
The normal table and `--json` modes only list matches; install a selected name with `hyperframes add <name>`. `--human-friendly` opens an interactive picker and installs the selected item immediately. In CI or agent workflows, prefer `--json` followed by an explicit `add`.
|
||||
|
||||
### Report what the catalog does not have
|
||||
|
||||
When the search comes back and nothing in it does the job, say so before you hand-author the move:
|
||||
|
||||
```bash
|
||||
npx hyperframes feedback --search-miss "<the query you ran>" --wanted "<the move you needed>" --tier <the tier that answered>
|
||||
```
|
||||
|
||||
`catalog --query` prints this line for you, pre-filled, and `--json` carries it as `report_gap` — so it is already in hand at the moment you decide nothing fits.
|
||||
|
||||
**Report whenever nothing in the results does the job, on either tier.** Do not wait for the on-device tier to have answered: it needs a consented 33 MB download, so an agent run is on `words` unless it explicitly opted in, and gating on `on-device` would silence almost every report. The `--tier` value rides along so a vocabulary miss stays distinguishable from a meaning miss when these are read. Describe the effect you wanted, not the item name you imagined: what comes back is a list of moves worth building, and a report naming a non-existent item teaches nothing. This is the only path that sends a query anywhere, which is exactly why it is a separate deliberate command rather than something the search does on its own. It carries no rating and never lands in the rating metric.
|
||||
|
||||
This is the whole demand signal for the catalog. Skipping it means the gap you hit gets guessed at from install counts instead, which cannot see a move nobody could install.
|
||||
|
||||
If the CLI cannot reach the configured registry, inspect the raw manifest as a fallback:
|
||||
|
||||
```bash
|
||||
curl -s https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry/registry.json
|
||||
```
|
||||
|
||||
A registry the CLI cannot reach does **not** empty the catalog for **discovery**: a previously fetched manifest keeps serving past its 24h refresh window whenever revalidation fails, so `catalog` and `catalog --query` still list and rank against the last copy on disk.
|
||||
|
||||
**`add` still needs the network, even for an item you installed yesterday.** Only manifests are cached; the item's actual files are fetched on every install. So offline you can search, and you can see what an item is, but installing it fails at the file fetch. Do not promise a user an offline install.
|
||||
|
||||
Each item's `registry-item.json` contains: name, type, title, description, tags, dimensions (blocks only), duration (blocks only), and file list.
|
||||
|
||||
See [discovery.md](./discovery.md) for details on filtering by type and tags.
|
||||
|
||||
## Search tiers
|
||||
|
||||
- **Search the catalog before writing motion by hand.** `npx hyperframes catalog --query "<the beat, in plain English>"`. Search is entirely local: there is no hosted tier, no account, and the query text is never sent anywhere. By default it ranks on vocabulary shared with the item's name, title, description and tags, which misses any phrasing that does not reuse the catalog's own wording. Add `--on-device` to rank by meaning instead (see the offline tier below).
|
||||
- **Query in English even when the video is not.** Both tiers index an English catalog, so a query in another script produces no searchable terms and returns nothing. Describe the move in English; the on-screen copy stays in whatever language the video needs. `No searchable words in query` means exactly this and is not a missing component, so do not report it as a catalog gap.
|
||||
- **Read which tier answered; never infer it from results appearing.** With `--json` the envelope carries `query`, `tier` (`on-device` or `words`), `tier_detail`, `dropped`, `unindexed`, `shown`, `total` and `results`, plus `top_score` when the answering tier produces one and `warnings` when a tier was asked for and could not run, or when a search returned nothing and a better tier is still waiting on someone's consent. A weak result on `words` is expected; the same result on `on-device` is a bug. `top_score` is on-device only and has no threshold behind it: the ranker returns the whole catalog in some order for every query, so read it as evidence rather than as a pass or fail.
|
||||
- **`dropped` and `unindexed` are opposite skews between the registry and the on-device index, and rewording the query fixes neither.** `dropped` counts ranked names this registry cannot install, so the strongest matches are the ones being lost. `unindexed` counts registry moves the index cannot see at all, which no query can ever return. Refreshing the registry is not the answer to either: its manifest carries a 24h TTL and heals itself, while the vectors are a separately published artifact fetched into `~/.hyperframes/catalog/`. Re-running with `--on-device` refetches that index when `unindexed` is above zero, so that is the remedy to hand the user. A pure over-coverage skew (`dropped` above zero while `unindexed` is zero) does not trigger the refetch; clearing `~/.hyperframes/catalog/` is the only way out of that one. Both counts are of names rather than of results, so either can exceed `total`.
|
||||
- **When a search comes back with nothing worth installing, say so.** `npx hyperframes feedback --search-miss "<the query you ran>" --wanted "<the move you needed>" --tier <the tier that answered>`. You do not have to assemble that line: `catalog --query` prints it pre-filled, and every `--json` search envelope carries it as `report_gap` with the query and tier already correct — fill in `--wanted` and send. This is the only path that sends a query anywhere, and it is a separate deliberate command precisely so plain `catalog --query` keeps its promise of sending nothing. **Report on either tier**, whenever the results do not do the thing; do not hold out for the on-device tier, which needs a consented 33 MB download and is therefore off in most agent runs — waiting for it means never reporting at all. The tier rides along in the report, so a vocabulary miss stays distinguishable from a meaning miss without you having to judge which one you hit. What comes back is a list of moves the catalog does not have yet, read directly rather than guessed from install counts, so the phrasing that matters is the effect you wanted, not the item name you imagined. It carries no rating and never lands in the rating metric.
|
||||
- **Offer the offline tier; never enable it silently.** A one-time ~33 MB download (a quantized ONNX build of `bge-small-en-v1.5` plus its tokenizer, pinned to a fixed revision) and the catalog vectors from the registry, both cached under `~/.hyperframes/`, neither added to the project or any package. Once cached it ranks by meaning with nothing sent. Say the size out loud and let the person decide, then pass `--on-device` (with `-y` to skip the prompt) once they agree. The interactive offer only fires on a TTY. Under `--json` there is no prompt, but a search that found nothing puts the same ask in `warnings`, so read that array and put the decision to the user yourself. When the person asks what the download is, why this model, or what leaves the machine, point them to https://hyperframes.heygen.com/developers/catalog-search.
|
||||
@@ -1,417 +0,0 @@
|
||||
# Contribute Templates
|
||||
|
||||
Copy-paste starter templates for each component type. These embed the proven patterns that pass `lint` and `check`.
|
||||
|
||||
## Caption Template
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Montserrat:wght@800;900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #111;
|
||||
overflow: hidden;
|
||||
}
|
||||
#root-BLOCKNAME {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #111;
|
||||
}
|
||||
.cap-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.cg {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
max-width: 1700px;
|
||||
overflow: visible;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
.cw {
|
||||
font-family: "Montserrat", sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: 128px;
|
||||
color: #ffffff;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
-webkit-text-stroke: 3px rgba(0, 0, 0, 0.8);
|
||||
paint-order: stroke fill;
|
||||
text-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root-BLOCKNAME"
|
||||
data-composition-id="BLOCKNAME"
|
||||
data-start="0"
|
||||
data-duration="9"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div class="cap-container" id="cc-BLOCKNAME"></div>
|
||||
<div
|
||||
id="drv-BLOCKNAME"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="9"
|
||||
data-track-index="0"
|
||||
style="position:absolute;width:1px;height:1px;opacity:0;pointer-events:none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
// REPLACE with actual transcript data
|
||||
var WORDS = [
|
||||
{ text: "Welcome", start: 0.3, end: 0.65 },
|
||||
{ text: "to", start: 0.65, end: 0.8 },
|
||||
{ text: "the", start: 0.8, end: 0.95 },
|
||||
{ text: "future", start: 0.95, end: 1.4 },
|
||||
// ... add all words
|
||||
];
|
||||
|
||||
var GROUPS = [
|
||||
{ start: 0.3, end: 1.3, wordStart: 0, wordEnd: 3, text: "Welcome to the future" },
|
||||
// ... add all groups
|
||||
];
|
||||
|
||||
var container = document.getElementById("cc-BLOCKNAME");
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.id = "PREFIX-cg-" + gi;
|
||||
groupEl.className = "cg";
|
||||
|
||||
for (var wi = g.wordStart; wi <= g.wordEnd; wi++) {
|
||||
var wordEl = document.createElement("span");
|
||||
wordEl.id = "PREFIX-cw-" + wi;
|
||||
wordEl.className = "cw";
|
||||
wordEl.textContent = WORDS[wi].text;
|
||||
groupEl.appendChild(wordEl);
|
||||
}
|
||||
|
||||
// Pretext overflow prevention
|
||||
if (window.__hyperframes && window.__hyperframes.fitTextFontSize) {
|
||||
var _fit = window.__hyperframes.fitTextFontSize(g.text.toUpperCase(), {
|
||||
fontFamily: "Montserrat",
|
||||
fontWeight: 900,
|
||||
maxWidth: 1550,
|
||||
baseFontSize: 128,
|
||||
minFontSize: 48,
|
||||
});
|
||||
if (_fit.fontSize < 128) {
|
||||
for (var _fi = 0; _fi < groupEl.children.length; _fi++) {
|
||||
groupEl.children[_fi].style.fontSize = _fit.fontSize + "px";
|
||||
}
|
||||
}
|
||||
}
|
||||
container.appendChild(groupEl);
|
||||
});
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (g, gi) {
|
||||
var groupEl = document.getElementById("PREFIX-cg-" + gi);
|
||||
|
||||
// SHOW — set opacity to 1 (never use tl.from with opacity:0 here)
|
||||
tl.set(groupEl, { opacity: 1, visibility: "visible" }, g.start);
|
||||
|
||||
// ENTRANCE — customize this per style
|
||||
tl.from(groupEl, { scale: 1.3, duration: 0.15, ease: "back.out(2)" }, g.start);
|
||||
|
||||
// KARAOKE — highlight each word
|
||||
for (var wi = g.wordStart; wi <= g.wordEnd; wi++) {
|
||||
var wordEl = document.getElementById("PREFIX-cw-" + wi);
|
||||
tl.to(wordEl, { color: "#FFD700", scale: 1.1, duration: 0.06 }, WORDS[wi].start);
|
||||
tl.to(wordEl, { color: "#FFFFFF", scale: 1, duration: 0.08 }, WORDS[wi].end);
|
||||
}
|
||||
|
||||
// EXIT
|
||||
tl.to(groupEl, { opacity: 0, scale: 0.9, duration: 0.1 }, g.end - 0.1);
|
||||
|
||||
// HARD KILL (mandatory)
|
||||
tl.set(groupEl, { opacity: 0, visibility: "hidden" }, g.end);
|
||||
});
|
||||
|
||||
window.__timelines["BLOCKNAME"] = tl;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Replace checklist:**
|
||||
|
||||
- `BLOCKNAME` → your block name (e.g., `cap-swoosh`)
|
||||
- `PREFIX` → short unique prefix for IDs (e.g., `sw`)
|
||||
- Font family, weight, size → your style's typography
|
||||
- Entrance animation → your style's entrance
|
||||
- Karaoke highlight → your style's active word treatment
|
||||
- Colors → your style's palette
|
||||
|
||||
---
|
||||
|
||||
## VFX Template
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.147.0/build/three.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: #030308;
|
||||
overflow: hidden;
|
||||
}
|
||||
#root-BLOCKNAME {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #030308;
|
||||
}
|
||||
#canvas-BLOCKNAME {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root-BLOCKNAME"
|
||||
data-composition-id="BLOCKNAME"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<canvas id="canvas-BLOCKNAME" width="1920" height="1080"></canvas>
|
||||
<div
|
||||
id="drv-BLOCKNAME"
|
||||
class="clip"
|
||||
data-start="0"
|
||||
data-duration="10"
|
||||
data-track-index="0"
|
||||
style="position:absolute;width:1px;height:1px;opacity:0;pointer-events:none"
|
||||
></div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
window.__timelines = window.__timelines || {};
|
||||
|
||||
// Seeded PRNG — NEVER use Math.random()
|
||||
function mulberry32(a) {
|
||||
return function () {
|
||||
a |= 0;
|
||||
a = (a + 0x6d2b79f5) | 0;
|
||||
var t = Math.imul(a ^ (a >>> 15), 1 | a);
|
||||
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
var rng = mulberry32(42);
|
||||
|
||||
var W = 1920,
|
||||
H = 1080;
|
||||
var canvas = document.getElementById("canvas-BLOCKNAME");
|
||||
var renderer = new THREE.WebGLRenderer({ canvas: canvas, antialias: true });
|
||||
renderer.setSize(W, H);
|
||||
renderer.setPixelRatio(1);
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.1;
|
||||
|
||||
var scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color(0x030308);
|
||||
var camera = new THREE.PerspectiveCamera(50, W / H, 0.1, 100);
|
||||
camera.position.set(0, 0, 8);
|
||||
|
||||
// YOUR SCENE SETUP HERE
|
||||
// - lights
|
||||
// - geometry
|
||||
// - materials
|
||||
|
||||
// State proxy — GSAP animates this, render reads it
|
||||
var st = {
|
||||
rotY: 0,
|
||||
camZ: 8,
|
||||
// add your animated properties
|
||||
};
|
||||
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
// YOUR TWEENS HERE
|
||||
tl.to(st, { rotY: Math.PI * 2, duration: 10, ease: "none" }, 0);
|
||||
|
||||
window.__timelines["BLOCKNAME"] = tl;
|
||||
|
||||
function renderScene() {
|
||||
// Apply state to Three.js objects
|
||||
camera.position.z = st.camZ;
|
||||
// mesh.rotation.y = st.rotY;
|
||||
|
||||
renderer.render(scene, camera);
|
||||
}
|
||||
|
||||
// Render via onUpdate — NO requestAnimationFrame
|
||||
tl.eventCallback("onUpdate", renderScene);
|
||||
renderScene();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Replace checklist:**
|
||||
|
||||
- `BLOCKNAME` → your block name (e.g., `vfx-chrome-blob`)
|
||||
- Scene setup → your geometry, lights, materials
|
||||
- State proxy → your animated properties
|
||||
- Tweens → your animation timeline
|
||||
- renderScene → apply state to your Three.js objects
|
||||
|
||||
---
|
||||
|
||||
## registry-item.json Templates
|
||||
|
||||
**For blocks:**
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "BLOCKNAME",
|
||||
"type": "hyperframes:block",
|
||||
"title": "Human-Readable Title",
|
||||
"description": "One sentence: what it does and who uses it",
|
||||
"dimensions": { "width": 1920, "height": 1080 },
|
||||
"duration": 10,
|
||||
"tags": ["category", "subcategory"],
|
||||
"files": [
|
||||
{
|
||||
"path": "BLOCKNAME.html",
|
||||
"target": "compositions/BLOCKNAME.html",
|
||||
"type": "hyperframes:composition"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**For components** (no `dimensions` or `duration`):
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "COMPONENTNAME",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Human-Readable Title",
|
||||
"description": "One sentence: what it does",
|
||||
"tags": ["category"],
|
||||
"files": [
|
||||
{
|
||||
"path": "COMPONENTNAME.html",
|
||||
"target": "compositions/components/COMPONENTNAME.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Tags by category:
|
||||
|
||||
- Captions: `captions`, `viral`, `professional`, `karaoke`, `minimal`
|
||||
- VFX: `three-js`, `particles`, `shader`, `gpu`
|
||||
- Transitions: `transition`, `shader`, `wipe`, `dissolve`
|
||||
- Blocks: `lower-third`, `social`, `title-card`, `data-viz`
|
||||
- Components: `effect`, `overlay`, `text-treatment`
|
||||
|
||||
---
|
||||
|
||||
## Component Template
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
.COMPNAME-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="COMPNAME-wrap">
|
||||
<!-- Your reusable effect/overlay here -->
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
// Component snippet — no data-composition-id, no __timelines.
|
||||
// The parent composition controls timing.
|
||||
// Keep all class names and IDs prefixed with COMPNAME.
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
**Replace checklist:**
|
||||
|
||||
- `COMPNAME` → your component name (e.g., `shimmer-sweep`)
|
||||
- Background should be `transparent` so it overlays cleanly
|
||||
- No `data-composition-id` or `window.__timelines` — the parent owns timing
|
||||
@@ -1,61 +0,0 @@
|
||||
# Wiring Blocks
|
||||
|
||||
Blocks are standalone compositions with their own `data-composition-id`, dimensions, duration, and GSAP timeline. Include them in a host composition using `data-composition-src` on a `<div>`.
|
||||
|
||||
## Basic wiring
|
||||
|
||||
After `hyperframes add data-chart`, wire it into your `index.html`:
|
||||
|
||||
```html
|
||||
<div id="stage" data-composition-id="main" data-width="1920" data-height="1080" data-duration="20">
|
||||
<video id="a-roll" src="video.mp4" data-start="0" data-duration="20" data-track-index="0"></video>
|
||||
|
||||
<!-- Block: appears at 2s, plays for 15s, on layer 1 -->
|
||||
<div
|
||||
data-composition-id="data-chart"
|
||||
data-composition-src="compositions/data-chart.html"
|
||||
data-start="2"
|
||||
data-duration="15"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Required attributes
|
||||
|
||||
| Attribute | Description |
|
||||
| ---------------------- | -------------------------------------------------------------------- |
|
||||
| `data-composition-src` | Path to the block HTML file (relative to index.html) |
|
||||
| `data-composition-id` | Unique ID matching the block's internal composition ID |
|
||||
| `data-start` | When the block appears in the host timeline (seconds) |
|
||||
| `data-duration` | How long the block plays (seconds, at most the block's own duration) |
|
||||
| `data-track-index` | Layer ordering — higher numbers render in front |
|
||||
| `data-width` | Block canvas width (match the block's dimensions) |
|
||||
| `data-height` | Block canvas height (match the block's dimensions) |
|
||||
|
||||
## Timeline coordination
|
||||
|
||||
The block's internal GSAP timeline runs independently from the host timeline. The HyperFrames runtime loads the sub-composition, finds its `window.__timelines` registration, and seeks the block in sync with the host, offset by `data-start`. You do NOT need to reference the block's timeline in your host's GSAP code.
|
||||
|
||||
## Positioning blocks
|
||||
|
||||
To position a block in a specific area of the screen, add CSS:
|
||||
|
||||
```html
|
||||
<div
|
||||
data-composition-id="data-chart"
|
||||
data-composition-src="compositions/data-chart.html"
|
||||
data-start="2"
|
||||
data-duration="15"
|
||||
data-track-index="1"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
style="position: absolute; right: 0; top: 0; width: 40%; height: 100%;"
|
||||
></div>
|
||||
```
|
||||
|
||||
## Multiple blocks
|
||||
|
||||
Add additional `<div data-composition-src="...">` siblings with non-overlapping or overlapping `data-start` values — each block's timeline is independent and seeked in sync by the runtime.
|
||||
@@ -1,39 +0,0 @@
|
||||
# Wiring Components
|
||||
|
||||
Components are effect snippets — HTML, CSS, and optionally JS that you merge directly into an existing composition. Unlike blocks, components have no standalone timeline; they participate in the host composition's timeline.
|
||||
|
||||
## General process
|
||||
|
||||
1. Run `hyperframes add <component-name>`
|
||||
2. Open the installed file (e.g., `compositions/components/grain-overlay.html`)
|
||||
3. Read the comment header for usage instructions
|
||||
4. Copy the parts into your host composition:
|
||||
- **HTML elements** — inside your `<div data-composition-id="...">`
|
||||
- **CSS styles** — into your composition's `<style>` block
|
||||
- **JS setup** — into your composition's `<script>`, before your timeline code
|
||||
- **Timeline calls** — into your GSAP timeline (if the component exposes them)
|
||||
|
||||
## Example: grain-overlay (CSS-only, no timeline integration)
|
||||
|
||||
```html
|
||||
<!-- Paste the overlay div into your composition -->
|
||||
<div
|
||||
id="grain-overlay"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 100;"
|
||||
>
|
||||
<div class="grain-texture"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
Then paste the CSS keyframes and `.grain-texture` rule into your styles. No GSAP timeline calls needed — the grain animates via CSS `@keyframes`.
|
||||
|
||||
## Example: shimmer-sweep (needs timeline integration)
|
||||
|
||||
See `examples/add-component.md` for the full shimmer-sweep walkthrough (HTML wrapping, CSS, JS setup, and timeline call).
|
||||
|
||||
## Key principles
|
||||
|
||||
- Components inherit the host composition's dimensions and duration
|
||||
- Place component HTML at the appropriate z-index relative to your content
|
||||
- Read the comment header in each snippet for customizable values
|
||||
- Run `hyperframes lint` after wiring to catch structural issues
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
name: hyperframes
|
||||
description: Read first to make, edit, animate or render any video with HyperFrames (HTML rendered to video), including launch films, explainers, music videos, charts, maps, vertical clips, title cards, captions and overlays. Holds the render contract, the work loop and what each kind of video needs.
|
||||
---
|
||||
|
||||
**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](references/plugin-installation.md) when this skill is inside a HyperFrames plugin.
|
||||
|
||||
# HyperFrames
|
||||
|
||||
HyperFrames renders HTML to video by seeking a paused timeline frame by frame, so every render is identical; the person edits the same files in the Studio preview. Use it unless another framework or a plain browser recording is asked for. Run the CLI as `npx hyperframes <command>` (`/hyperframes-cli`); at the start of a new video, `npx hyperframes usage --json` reports the person's remaining plan usage (say "unknown" when it is).
|
||||
|
||||
`index.html` is the root composition: it declares the video's variables and mounts one scene per sub-composition from `compositions/`. Media and fonts live in `assets/`. A new project: `npx hyperframes init <dir>`. A website as the subject: `npx hyperframes capture <url> --json`.
|
||||
|
||||
Build what was asked: no voiceover or captions and no new aspect ratio unless the request or the source calls for them; a film over ~15 s that tells something gets music and sound on its visible actions unless the person asked for silence (`references/sound.md`). A new video is 1920×1080 at 30 fps. A question, a hold or a loose idea gets an answer or a proposal, not an edit. Before hand-writing a named effect, transition, caption style, chart or card, search the registry (`/hyperframes-registry`). Any media (a presenter or avatar, footage, voice, music): `/media-use`, which says when HeyGen is needed and what is free. Show real things from their source (the product's screens, the brand's logo, a real place's imagery); draw only what has none.
|
||||
|
||||
## Contract
|
||||
|
||||
`npx hyperframes check --json` reports most violations with a `Fix:` line.
|
||||
|
||||
1. Every timed element has `class="clip"`, `data-start` and `data-duration` in seconds, and shows over `[start, start + duration)`: land an end state just before its end. `data-track-index` is only a Studio lane; paint order is `z-index`.
|
||||
2. The video's length is the root's `data-duration`: overrun is cut, an early end holds the last frame.
|
||||
3. Each composition builds one paused timeline and registers it after its tweens: `window.__timelines["<data-composition-id>"] = tl`.
|
||||
4. No `Date.now()`, unseeded `Math.random()`, network fetches, timers or `repeat: -1`. Everything visible is a function of timeline time.
|
||||
5. Never tween `display`, `visibility` or `autoAlpha` on a `.clip`; animate a child.
|
||||
6. No CSS `transform` on an element GSAP transforms; animate transforms, not `left`/`top`/`width`/`height`.
|
||||
7. Never `play()`, `pause()` or seek media. A `<video>` keeps its sound with `data-has-audio="true"`, else it is `muted`; music, voice, replacement sound and J/L cuts are a separate `<audio>`.
|
||||
8. Every `<audio>` has an `id` or it is left out of the mix. No `crossorigin` on media. Time a `<video>` or its wrapper, never both.
|
||||
9. Only the bundled fonts (`references/format.md`) resolve by name; any other family needs `@font-face` to a file in `assets/fonts/`.
|
||||
10. Asset paths are root-relative (`assets/...`), in sub-compositions too.
|
||||
11. Grades, grain, blur and vignettes on footage go through `npx hyperframes media-treatment` (`/media-use`), never CSS filters.
|
||||
12. A scene file puts its styles, markup and scripts inside `<template>` and styles its root as `#root`. Intended overflow or overlap: `data-layout-allow-overflow`, `data-layout-allow-overlap`.
|
||||
|
||||
## Keep it editable
|
||||
|
||||
Studio edits the tags you write.
|
||||
|
||||
- One scene per sub-composition, short readable ids; the root holds only scene hosts, media and audio.
|
||||
- Timed elements are written in the HTML, not made by script, each with a stable `id`.
|
||||
- Copy, brand colours and swappable media are variables (`data-var-text`, `data-var-src`, `var(--id)`).
|
||||
- One kind per track: `data-track-kind="graphics"` on scene and overlay hosts, `captions` on the one host that holds every caption group.
|
||||
- Content stays inside the 90% action-safe box, captions and key text inside 80% title-safe; Studio draws the guides, never add them.
|
||||
|
||||
## Work loop
|
||||
|
||||
- An existing project: `npx hyperframes timeline --json` shows what plays when; before any change, `npx hyperframes history --since mine --who <you>` shows what the person changed since your last turn: build on it (`/hyperframes-cli`).
|
||||
- A new film over ~15 s that tells something: its type's file (below), then `references/story.md`: three storylines that are three different films, in words only; the person picks unless they asked for the file or no stops. Then `SPEC.md` (`references/spec.md`): every adjective as a number, the defaults banned by name, the shots by frame.
|
||||
- After your last edit: `npx hyperframes check --json`; fix every error, weigh every warning. A film: `npx hyperframes snapshot --review` and `references/review.md`, ending in `REVIEW.md`. A small edit: snapshot what you changed (`--at`, `--range`, `--composition`) and look before you reply.
|
||||
- Render when the person approves the Studio preview, asks for the file or asks for no stops, after your check and review pass; otherwise stop at the preview and wait.
|
||||
|
||||
## The HyperFrames desktop app
|
||||
|
||||
Call it "the HyperFrames desktop app", never "Studio"; offer it as the better way to keep editing, not the only one.
|
||||
|
||||
- Before the delivery render of a new video, open the Studio preview, even in no-questions mode, and say they can edit there. If `preview` printed "Keep editing by chatting with Framey", add that the **Edit with Framey** button opens the app.
|
||||
- After the delivery render, pass on its Framey line in your own words: offer to run the `hyperframes open` it names or give its download link; else say nothing (autonomous: the delivery note).
|
||||
- When any command says to run `catch-up`, run `npx hyperframes catch-up [dir]` before your next change; what it lists is their work, not a new request.
|
||||
|
||||
## What to read
|
||||
|
||||
| The request | Read |
|
||||
| --- | --- |
|
||||
| Launch, promo, feature | `references/types/launch.md` |
|
||||
| Explainer, tutorial | `references/types/explainer.md` |
|
||||
| Music video | `references/types/music.md` |
|
||||
| Chart, numbers | `references/types/data.md` |
|
||||
| Vertical or social clip | `references/types/vertical.md` |
|
||||
| 3D, simulation, generative | `references/types/3d.md` |
|
||||
| Character, mascot, story short | `references/types/character.md` |
|
||||
| A real place or map; a website, PR or script; kinetic type | `references/film.md` |
|
||||
| Captions; cards over a talking head | `references/captions.md`; `references/overlays.md` |
|
||||
| Motion: eases, titles, cuts, punch-ins, speed, holds | `references/motion.md` |
|
||||
| Scenes, attributes, media, lint; GSAP, Lottie, 3D, canvas | `references/format.md`; `references/runtimes.md` |
|
||||
|
||||
Decks: `/slideshow`. Remotion source: `/remotion-to-hyperframes`. Figma: `/figma`. Installing, refreshing or upgrading skills and the pinned CLI: `references/skill-lifecycle.md`.
|
||||
@@ -1,289 +0,0 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Biennale Yellow — Frame (video / frame layer)
|
||||
description: >
|
||||
Video-first companion to Biennale Yellow's design.md. The unit is the frame (1920×1080). Atoms
|
||||
are identical and sacred — warm parchment grounds, a single deep indigo ink, solar yellow deployed
|
||||
as bloom / panel / tile underprint, Instrument Serif display + Archivo sans + JetBrains Mono data,
|
||||
1px hairline rules as the only border, atmospheric depth (no shadows), and the bottom-right
|
||||
pagenum. Composition + frame scale rewritten. Restraint is the rule; motion out of scope.
|
||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
||||
principle: atoms are sacred · composition is free · numbers come from the script
|
||||
|
||||
colors:
|
||||
paper: "#E9E5DB"
|
||||
paper-deep: "#DCD6C4"
|
||||
sun: "#F1EE2E"
|
||||
sun-soft: "#F8F39B"
|
||||
haze: "#F0DA7C"
|
||||
ink: "#1B2566"
|
||||
ember: "#E26B4A"
|
||||
|
||||
typography:
|
||||
# — reading + data ramp —
|
||||
body: { fontFamily: "Archivo", cqw: 0.85, weight: 400, lineHeight: 1.5, color: "ink" }
|
||||
body-lede:{ fontFamily: "Archivo", cqw: 0.95, weight: 400, lineHeight: 1.55 }
|
||||
micro-label:{ fontFamily: "Archivo", px: 13, weight: 600, tracking: "0.18em", upper: true }
|
||||
rail-label:{ fontFamily: "Archivo", px: 13, weight: 600, tracking: "0.32em", upper: true }
|
||||
mono-data:{ fontFamily: "JetBrains Mono", cqw: 0.73, weight: 400, tracking: "0.04em" }
|
||||
pagenum: { fontFamily: "JetBrains Mono", px: 13, weight: 400, tracking: "0.08em", opacity: 0.75 }
|
||||
# — display ramp (Instrument Serif 400, tight, negative tracking) —
|
||||
ledger-title:{ fontFamily: "Instrument Serif", cqw: 1.55, weight: 400, lineHeight: 1.15 }
|
||||
strand-title:{ fontFamily: "Instrument Serif", cqw: 1.7, weight: 400, lineHeight: 1.1 }
|
||||
headline-sm:{ fontFamily: "Instrument Serif", cqw: 2.9, weight: 400, lineHeight: 1.0 }
|
||||
headline:{ fontFamily: "Instrument Serif", cqw: 4.6, weight: 400, lineHeight: 1.06, tracking: "-0.005em" }
|
||||
date-rail:{ fontFamily: "Instrument Serif", cqw: 5.0, weight: 400, lineHeight: 0.96, tracking: "-0.005em" }
|
||||
display-it:{ fontFamily: "Instrument Serif", cqw: 7.0, weight: 400, italic: true, lineHeight: 1.04, tracking: "-0.005em" }
|
||||
numeral-md:{ fontFamily: "Instrument Serif", cqw: 7.5, weight: 400, lineHeight: 0.92, tracking: "-0.01em" }
|
||||
display:{ fontFamily: "Instrument Serif", cqw: 14.6, weight: 400, lineHeight: 0.86, tracking: "-0.018em" }
|
||||
numeral-jumbo:{ fontFamily: "Instrument Serif", cqw: 28.0, weight: 400, lineHeight: 0.84, tracking: "-0.04em" }
|
||||
|
||||
spacing:
|
||||
pad-edge: "4cqw"
|
||||
pad-region: "4.2cqw"
|
||||
gap-region: "2.5cqw"
|
||||
|
||||
components:
|
||||
sun-bloom:
|
||||
background: "radial gradient {colors.sun} core → {colors.sun-soft} → {colors.haze} → transparent on {colors.paper}"
|
||||
size: "42–70% of the frame, off-center or behind the focal element"
|
||||
description: "The primary depth layer. One per frame; a flat parchment frame reads as broken."
|
||||
ember-bloom:
|
||||
background: "radial {colors.ember} at 15–22% opacity"
|
||||
placement: "corner opposite the sun-bloom"
|
||||
description: "Subordinate counter-temperature balance; never dominant."
|
||||
block-tile:
|
||||
backgroundColor: "{colors.sun} at 40–70% opacity"
|
||||
layout: "rectangles on an 8×4 grid behind cover/colophon"
|
||||
description: "A layered poster underprint."
|
||||
yellow-panel:
|
||||
backgroundColor: "{colors.sun}"
|
||||
textColor: "{colors.ink}"
|
||||
rounded: "0"
|
||||
border: "none (meets paper directly)"
|
||||
description: "Full-bleed column/third — the strongest color statement, ink on top."
|
||||
hairline-rule:
|
||||
rule: "1px solid {colors.ink} (soft variant: {colors.ink} at 18–20%)"
|
||||
description: "The ONLY border — header underlines, footer tops, ledger separators. No thicker rule exists."
|
||||
strand-row:
|
||||
borderBottom: "1px {colors.ink} 18–20%"
|
||||
typography: "{typography.strand-title} + {typography.body}"
|
||||
description: "Serif numeral + serif title + sans body. Numbered editorial lists."
|
||||
ledger-row:
|
||||
borderBottom: "1px {colors.ink} 18–20%"
|
||||
typography: "{typography.mono-date} · {typography.ledger-title} · {typography.body} · {typography.mono-data}"
|
||||
description: "4-col tabular — mono date · serif title · sans venue · mono duration (right)."
|
||||
footer-band:
|
||||
borderTop: "1px solid {colors.ink} per cell"
|
||||
typography: "{typography.micro-label} + {typography.body-sm}"
|
||||
description: "4-column metadata strip at the foot of cover/colophon."
|
||||
vertical-rail:
|
||||
typography: "{typography.rail-label}"
|
||||
transform: "rotated up the left edge"
|
||||
description: "Section marker on chapter/divider frames."
|
||||
pagenum:
|
||||
typography: "{typography.pagenum}"
|
||||
color: "{colors.ink} at 75%"
|
||||
placement: "bottom-right"
|
||||
description: "The only persistent chrome."
|
||||
---
|
||||
|
||||
# Biennale Yellow — Frame (video / frame layer)
|
||||
|
||||
## Overview
|
||||
|
||||
Biennale Yellow at frame scale is a **literary-editorial system** in the register of an art
|
||||
biennale catalogue: warm parchment, a single deep indigo ink, and solar yellow as atmosphere. No
|
||||
cards, no buttons, no shadows, no rounded corners — the structural vocabulary is just **paper, ink,
|
||||
and yellow.** Depth is delivered by soft radial **sun blooms**, not elevation.
|
||||
|
||||
The voice is three faces in rigid roles: **Instrument Serif** (weight 400, tight line-height,
|
||||
negative tracking) carries every display, numeral, and quote from 40px to 720px+; **Archivo** carries
|
||||
body and the wide-tracked uppercase micro-label; **JetBrains Mono** carries every date, figure, and
|
||||
the pagenum. Text is **always ink** — contrast comes from size and weight, never color. The mood
|
||||
sits between a folded museum brochure and a slow-reading literary quarterly: confident, atmospheric,
|
||||
deeply restrained.
|
||||
|
||||
**Key characteristics at frame scale:**
|
||||
|
||||
- **Warm parchment ground** on every frame; never white, never gray.
|
||||
- **Single ink** (`{colors.ink}`) for all type and all rules; **solar yellow** as bloom / panel / tile.
|
||||
- **Instrument Serif 400** display (tight, negative-tracked); **Archivo** body + micro-labels; **JetBrains Mono** data.
|
||||
- **1px hairline rules** are the only border — no thicker weight exists; **no shadows, no rounded corners**.
|
||||
- **Sun bloom** is the primary depth layer on every frame; an ember counter-bloom adds warm-cool tension.
|
||||
- **Editorial-restrained** — sparse reads as elegant; crowding breaks the catalogue feel.
|
||||
|
||||
## The Frame
|
||||
|
||||
### Frame Craft Bar
|
||||
|
||||
Three eyeball tests gate every frame before any structural check:
|
||||
|
||||
- **Squint** — one Instrument Serif moment dominates at 3–6× its neighbor; the sun bloom centers the eye.
|
||||
- **Silence** — frames read **55–60% empty**; the **ledger is the one dense exception** (density via quiet hairline repetition, not richness).
|
||||
- **Restraint** — **one ink color** for all type and rules; **one sun bloom** per frame (+ optional subordinate ember); never invert (no yellow text on ink).
|
||||
- **Reference** — aim at an **art-biennale catalogue / slow exhibition poster / literary quarterly**; failure looks like a **flat CMS template** (no bloom) or a **bordered-card deck**.
|
||||
|
||||
- **Primary:** 1920×1080 (16:9). Display authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`).
|
||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
||||
- **Safe area:** `pad-edge` 4cqw (40–76px equivalent) — the elegance depends on edge negative space; only blooms, tiles, and panels bleed.
|
||||
|
||||
**The container law (load-bearing).** Every frame ground sets `container-type: size`; ALL
|
||||
frame-relative units are `cqw`/`cqh` against it — never `vw`. Hairlines stay 1px; bloom sizes scale
|
||||
as `%` of the frame.
|
||||
|
||||
## Colors
|
||||
|
||||
Tokens identical to the source. `{colors.paper}` is the universal ground; `{colors.ink}` (deep
|
||||
indigo navy) is **every line of type and every rule** — there is no secondary text color.
|
||||
`{colors.sun}` deploys three ways: the **bloom** (radial atmosphere), the **yellow-panel**
|
||||
(full-bleed poster fill, ink on top), and the **block-tile** underprint. `{colors.sun-soft}`/`haze`
|
||||
are bloom mid/outer stops. `{colors.ember}` appears **only** as a 15–22% counter-bloom — never a
|
||||
fill, never text. **The system never inverts** — ink on sun is correct; yellow text on ink does not exist.
|
||||
|
||||
## Typography
|
||||
|
||||
Two ramps. The **reading/data ramp** (Archivo body 0.85cqw ink, micro-labels in px, JetBrains Mono
|
||||
data) carries copy + chrome; the **display ramp** (Instrument Serif `ledger-title` 1.55cqw →
|
||||
`numeral-jumbo` 28cqw) carries every headline, numeral, and quote.
|
||||
|
||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; mono/labels in px are chrome only.
|
||||
- **Fit-to-measure:** size the headline to its length. Cap the block at **≤ 78cqw**; ≤3 words → `display`; 4–6 → `headline`; 7+ → `headline-sm`. The jumbo numeral is a divider's whole point — don't shrink it.
|
||||
- **Instrument Serif is weight 400 only** (its contrast is the weight signal), tight line-height (0.84–1.06), negative tracking; **micro-labels uppercase Archivo 600, ≥0.16em**; **mono for all numerals/dates**. No bold serif, no second text color, no mono in body/display.
|
||||
|
||||
## Depth & Surface
|
||||
|
||||
Atmospheric, not structural. Depth from:
|
||||
|
||||
- **Sun bloom** — the primary layer: a layered radial (sun 70–95% core → sun-soft → haze 18–22% → paper 0%), 42–70% of the frame. One per frame.
|
||||
- **Ember bloom** — a 15–22% peach counter-bloom in the opposite corner; always subordinate.
|
||||
- **Block-tile underprint** — translucent yellow rectangles on an 8×4 grid (cover/colophon).
|
||||
- **Yellow panel** — the one "hard" color statement: a flooded column/third, ink on top.
|
||||
- **Hairline rules** — 1px ink for structural separation (soft 18–20% variant for dense rows).
|
||||
|
||||
**Ceiling:** zero box-shadow, zero text-shadow, zero rounded corner, no border thicker than 1px.
|
||||
|
||||
## Shapes
|
||||
|
||||
- **0 radius on everything** — strict rectangles. Blooms are edgeless (they fade into paper).
|
||||
|
||||
## Components
|
||||
|
||||
- **sun-bloom / ember-bloom / block-tile** — the atmospheric depth set. **yellow-panel** — the poster-fill statement.
|
||||
- **hairline-rule** — the only border (1px ink; soft variant for dense rows).
|
||||
- **strand-row / ledger-row / footer-band** — the editorial list, tabular calendar, and metadata strip.
|
||||
- **vertical-rail** — rotated chapter marker. **pagenum** — the bottom-right mono chrome.
|
||||
|
||||
## Frame Treatments
|
||||
|
||||
> Recipe: ground · container · composes · focal · chrome · accent · silence · Fixed/Free · density.
|
||||
> A sun bloom + bottom-right pagenum on every frame; sparse is the default.
|
||||
|
||||
### 1 · Cover (identity · move: display + sun bloom · left)
|
||||
|
||||
**Ground** paper + a large sun-bloom (left-of-center) + an ember counter-bloom (opposite corner).
|
||||
**Composes** micro-label, display, date-rail, footer-band, pagenum. **Focal** a 2-line Instrument
|
||||
Serif `display` (italic key word) in ink, left, under a micro-label. **Chrome** a serif date-rail
|
||||
top-right; a 4-column footer-band at the foot. **Accent** the sun bloom + yellow. **Silence** the
|
||||
bloom holds the open space. **Fixed** ink type, one bloom, 1px footer rules, no shadow. **Free**
|
||||
title, date, footer cells. **Density** low.
|
||||
|
||||
### 2 · Chapter Divider (section · move: jumbo numeral · vertical rail)
|
||||
|
||||
**Ground** paper + a corner-anchored sun-bloom. **Composes** vertical-rail, numeral-jumbo,
|
||||
headline-sm. **Focal** a single huge Instrument Serif `numeral-jumbo` (≈28cqw) dominating, with a
|
||||
serif title beneath. **Chrome** a rotated vertical-rail label up the left edge; pagenum. **Accent**
|
||||
the bloom behind the numeral. **Silence** ~60%. **Fixed** serif 400, jumbo numeral, rail label.
|
||||
**Free** the ordinal, title, rail text. **Density** low.
|
||||
|
||||
### 3 · Ledger (catalog · move: hairline tabular rows · the dense frame)
|
||||
|
||||
**Ground** paper (bloom optional, subtle). **Composes** headline-sm + micro-label topbar, ledger-rows.
|
||||
**Focal** a 4-column tabular calendar — mono date · serif title · sans venue · mono duration —
|
||||
separated by hairline-soft rules under a 1px ink header rule. **Chrome** pagenum. **Accent** none —
|
||||
density through quiet repetition, not color. **Silence** tight — the density exception (achieved by
|
||||
repetition, not richness). **Fixed** hairline rules, mono dates, serif titles. **Free** rows, venues.
|
||||
**Density** dense-exception.
|
||||
|
||||
### 4 · Manifesto / Quote (quote · move: italic serif · centered bloom)
|
||||
|
||||
**Ground** paper + a centered sun-bloom. **Composes** numeral-lg quote mark, display-it, attribution.
|
||||
**Focal** a 2-line Instrument Serif **italic** `display-it` quote in ink, centered, under an
|
||||
oversized serif quote mark. **Chrome** a micro-label attribution. **Accent** the centered bloom.
|
||||
**Silence** ~60% — deliberately open. **Fixed** italic serif, one bloom, ink. **Free** quote,
|
||||
attribution. **Density** low.
|
||||
|
||||
### 5 · Poster Panel (statement · move: yellow panel · split)
|
||||
|
||||
**Ground** paper with a full-bleed `{colors.sun}` `yellow-panel` (column or third). **Composes**
|
||||
yellow-panel, micro-label, headline. **Focal** an Instrument Serif `headline` in ink sitting **on
|
||||
the sun panel** (ink-on-yellow — a signature). **Chrome** micro-label; pagenum. **Accent** the panel
|
||||
itself. **Silence** the paper side stays open. **Fixed** ink-on-sun, panel meets paper directly (no
|
||||
border), no shadow. **Free** headline, panel side/width. **Density** low.
|
||||
|
||||
### 6 · Strand List (programme · move: numbered editorial rows · left)
|
||||
|
||||
**Ground** paper + a subtle sun-bloom. **Composes** micro-label, headline-sm, strand-rows. **Focal**
|
||||
a numbered list — serif numeral + serif title + sans body, hairline-soft separators. **Chrome**
|
||||
micro-label; pagenum. **Accent** the bloom. **Silence** moderate; rows breathe. **Fixed** serif
|
||||
numerals, hairline-soft rules. **Free** items, copy. **Density** standard.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Start on **warm parchment**; add **one sun bloom** (optionally an ember counter-bloom) — atmosphere is the depth.
|
||||
- Set every line in **ink**; use **Instrument Serif 400** (tight, negative-tracked) for display, **Archivo** body, **JetBrains Mono** for all numerals/dates.
|
||||
- Make every separator a **1px ink hairline** (soft variant for dense rows); flood a **yellow panel** for poster moments (ink on top).
|
||||
- Keep micro-labels **uppercase Archivo 600, 0.16–0.32em**; pin the **pagenum** bottom-right.
|
||||
- Lean sparse; left/asymmetric on cover/chapter/list, centered on manifesto.
|
||||
|
||||
### Don't
|
||||
|
||||
- No drop shadows, no rounded corners, no bordered cards, no border thicker than 1px.
|
||||
- No second text color; no bold Instrument Serif; no inverted ink grounds.
|
||||
- No mono for body/display; no font substitutes.
|
||||
- Don't crowd the canvas — sparse reads as elegant; don't omit the bloom (flat parchment reads as a CMS template).
|
||||
- Don't blow a headline edge-to-edge — step the ramp down.
|
||||
|
||||
## Aspect-Ratio Behavior
|
||||
|
||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
||||
| --------------- | ---------------------------------------------- | -------------------------- | -------------------------- |
|
||||
| Cover | display left, date-rail top-right, footer foot | display top, footer stacks | display upper, footer foot |
|
||||
| Chapter Divider | jumbo numeral, rail left | numeral centered, rail top | numeral centered |
|
||||
| Ledger | 4-col rows | drop venue col → 3-col | 3-col |
|
||||
| Manifesto | centered italic | centered, taller | centered |
|
||||
| Poster Panel | side panel + paper | top/bottom panel band | panel third |
|
||||
| Strand List | numbered rows | rows (tighter) | rows |
|
||||
|
||||
`pad-edge` holds on the short edge; display clamps use the shorter axis so portrait doesn't blow out
|
||||
headlines. Keep load-bearing lines ≥ 1.4cqw. Ledger/strand fixed first columns may tighten on 9:16.
|
||||
|
||||
## Approved Entities
|
||||
|
||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
||||
placeholder. Programme titles, venues, and dates are content; the system supplies paper, ink, yellow.
|
||||
|
||||
## Numerals & Claims (hard rule)
|
||||
|
||||
Never invent figures, dates, durations, or counts at frame scale. Render slots as `— figure —`,
|
||||
`{metric}`, `NN JUN`, `N m`. Ledger dates/durations and chart values especially carry placeholders
|
||||
until the script supplies them. Chapter ordinals (01, 02…) are decorative.
|
||||
|
||||
## Pre-Render Self-Audit
|
||||
|
||||
- **Squint** — one serif display moment dominates; the bloom centers the eye.
|
||||
- **Silence** — sparse frames 55–60% open; only the ledger runs dense (via repetition, not richness).
|
||||
- **One color** — ink for all text + rules; sun for bloom/panel/tile; ember counter-bloom only; no inversion.
|
||||
- **Type** — Instrument Serif 400 tight negative-tracked, fit-to-measure; micro-labels uppercase 0.16em+; mono numerals; ≥1.4cqw floor.
|
||||
- **Depth** — 0 shadow, 0 rounded corner, 1px hairlines only; one sun bloom present.
|
||||
- **Anchor** — left on cover/chapter/list, centered on manifesto; pagenum bottom-right.
|
||||
- **Fabrication** — every numeral/date traces to the script, else placeholder.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Motion intentionally out of scope.** frame.md specifies composition only; the source's 280ms crossfade is a deck mechanic.
|
||||
- **Instrument Serif + Archivo + JetBrains Mono via Google Fonts.** CJK: Smiley Sans (display) / Noto Serif SC (body) / Noto Sans SC (labels); italic serif and mono tabular figures have no exact Hanzi equal — keep ledger dates Latin.
|
||||
- **9:16 / 1:1 are guidance**; the `min(vw,vh)` clamp pattern keeps display from blowing out — verify per ratio.
|
||||
- Sun/ember blooms, block tiles, yellow panels, and hairline rules are CSS-only; no external imagery is required.
|
||||
@@ -1,220 +0,0 @@
|
||||
<!--
|
||||
Biennale Yellow — caption skin (preset-local source).
|
||||
|
||||
This is the preset's own lower-third karaoke caption look: a caption skin to read or
|
||||
copy, not to mount as is. A copy used as compositions/captions.html fills its three
|
||||
reserved holes and wraps the fragment in a <template>:
|
||||
· the empty GROUPS array → the engine's scene-aware word groups
|
||||
· the 0 duration + 0 width/height → real total + canvas size
|
||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
||||
|
||||
Token contract — reference ONLY this fixed vocab (set it from the
|
||||
project's frame.md; each var has a biennale literal
|
||||
fallback for standalone preview):
|
||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
||||
--font-body · --cap-band-top · --cap-band-height
|
||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
||||
|
||||
Visual: a warm-parchment card on a single 1px hairline indigo-ink border — biennale's
|
||||
ONLY border — square corners, NO shadow (biennale's depth is atmospheric, never cast).
|
||||
Instrument Serif, sentence case, ink glyphs. Upcoming words sit in faint ink; the
|
||||
current word gets a solar-yellow underprint (the brand's signature "sun as panel /
|
||||
underprint", a hard square block, no blur) with ink reading on top; spoken words
|
||||
settle to solid ink, underprint cleared.
|
||||
-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<!-- the project's :root tokens go here -->
|
||||
<style data-brand-tokens></style>
|
||||
|
||||
<style>
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE parchment card — single 1px hairline ink border (biennale's only border),
|
||||
square corners, NO shadow */
|
||||
.caption-pill {
|
||||
max-width: 78%;
|
||||
padding: 20px 44px 24px;
|
||||
background: var(--cap-canvas, #e9e5db);
|
||||
border: 1px solid var(--cap-ink, #1b2566);
|
||||
border-radius: 0;
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.1em 0.34em;
|
||||
font-family: var(--font-display, "Instrument Serif"), Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(40px, 4.4vw, 58px);
|
||||
line-height: 1.16;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* upcoming — faint indigo ink, no underprint */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0 0.06em;
|
||||
color: color-mix(in srgb, var(--cap-ink, #1b2566) 42%, var(--cap-canvas, #e9e5db));
|
||||
}
|
||||
/* current word — THE solar-yellow underprint (box-shadow spread = a hard square
|
||||
block w/o layout shift; no blur), indigo ink reading on top */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-ink, #1b2566);
|
||||
background: var(--cap-accent, #f1ee2e);
|
||||
box-shadow: 0 0 0 0.06em var(--cap-accent, #f1ee2e);
|
||||
}
|
||||
/* already spoken — solid indigo ink, underprint cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-ink, #1b2566);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — Instrument Serif italic, biennale's only display inflection */
|
||||
.caption-word.cap-brand {
|
||||
font-style: italic;
|
||||
}
|
||||
/* numerals / data — the mono voice, slightly recessed */
|
||||
.caption-word.cap-num {
|
||||
font-family: var(--font-body, "JetBrains Mono"), ui-monospace, monospace;
|
||||
color: color-mix(in srgb, var(--cap-ink, #1b2566) 72%, var(--cap-canvas, #e9e5db));
|
||||
}
|
||||
/* emphasis — an ember-red hairline underline (the counter-temperature accent) */
|
||||
.caption-word.cap-cta {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-color: var(--cap-accent-2, #e26b4a);
|
||||
text-underline-offset: 0.1em;
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(34px, 5.4vw, 48px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="0"
|
||||
data-fps="30"
|
||||
data-width="0"
|
||||
data-height="0"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// the scene-aware word groups + real duration go here.
|
||||
var GROUPS = [];
|
||||
var DURATION = 0;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.97 }, { scale: 1, duration: 0.14, ease: "power2.out" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,275 +0,0 @@
|
||||
---
|
||||
version: alpha
|
||||
name: BlockFrame — Frame (video / frame layer)
|
||||
description: >
|
||||
Video-first companion to BlockFrame's design.md. The unit is the frame (1920×1080). Atoms are
|
||||
identical and sacred — 4px black borders + 8px hard offset shadows, the five-pastel candy palette
|
||||
(pink/blue/green/yellow/cream) plus black/white/off-white, Inter 800–900 uppercase display +
|
||||
Space Grotesk label chrome, square corners, label-pills, tilted decorations, star bursts, stripe
|
||||
blocks, dot grids. Composition + frame scale rewritten. Motion out of scope.
|
||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
||||
principle: atoms are sacred · composition is free · numbers come from the script
|
||||
|
||||
colors:
|
||||
black: "#000000"
|
||||
white: "#FFFFFF"
|
||||
offwhite: "#FFFDF5"
|
||||
pink: "#FE90E8"
|
||||
blue: "#C0F7FE"
|
||||
green: "#99E885"
|
||||
yellow: "#F7CB46"
|
||||
cream: "#FFDC8B"
|
||||
|
||||
borders: { primary: "4px solid black", thin: "3px solid black" }
|
||||
shadows: { default: "8px 8px 0 black", small: "4px 4px 0 black", hover: "6px 6px 0 black", close-yellow: "12px 12px 0 yellow", close-white: "6px 6px 0 white" }
|
||||
|
||||
typography:
|
||||
# — reading + chrome ramp —
|
||||
body: { fontFamily: "Inter", cqw: 0.95, weight: 500, lineHeight: 1.6 }
|
||||
card-title:{ fontFamily: "Inter", cqw: 1.15, weight: 700, upper: true, lineHeight: 1.2 }
|
||||
label: { fontFamily: "Space Grotesk", px: 13, weight: 600, tracking: "0.08em", upper: true }
|
||||
counter: { fontFamily: "Space Grotesk", px: 14, weight: 700, tracking: "0.1em", upper: true }
|
||||
# — display ramp (Inter 800–900, uppercase, negative tracking) —
|
||||
heading-md:{ fontFamily: "Inter", cqw: 2.1, weight: 700, lineHeight: 1.1, tracking: "-0.01em", upper: true }
|
||||
quote-text:{ fontFamily: "Inter", cqw: 2.7, weight: 900, lineHeight: 1.15, tracking: "-0.02em", upper: true }
|
||||
stat-number:{ fontFamily: "Inter", cqw: 3.3, weight: 900, lineHeight: 1.0 }
|
||||
heading-lg:{ fontFamily: "Inter", cqw: 3.3, weight: 800, lineHeight: 1.0, tracking: "-0.02em", upper: true }
|
||||
close-title:{ fontFamily: "Inter", cqw: 4.2, weight: 900, lineHeight: 0.95, tracking: "-0.03em", upper: true }
|
||||
heading-xl:{ fontFamily: "Inter", cqw: 5.0, weight: 900, lineHeight: 0.95, tracking: "-0.03em", upper: true }
|
||||
|
||||
spacing:
|
||||
slide-pad: "3.1cqw" # 60px @1920
|
||||
gap-md: "1.7cqw"
|
||||
|
||||
components:
|
||||
card-elevated:
|
||||
backgroundColor: "{colors.white}"
|
||||
border: "0.4cqw solid {colors.black}"
|
||||
rounded: "0"
|
||||
shadow: "0.8cqw 0.8cqw 0 {colors.black}"
|
||||
description: "Primary card. Border/shadow coupled: 4px↔8px, 3px↔4px."
|
||||
card-small:
|
||||
backgroundColor: "{colors.white}"
|
||||
border: "0.3cqw solid {colors.black}"
|
||||
rounded: "0"
|
||||
shadow: "0.4cqw 0.4cqw 0 {colors.black}"
|
||||
description: "Stat cards, team cards, timeline steps."
|
||||
label-pill:
|
||||
border: "0.3cqw solid {colors.black}"
|
||||
backgroundColor: "any pastel / {colors.white}"
|
||||
rounded: "9999px"
|
||||
shadow: "0.4cqw 0.4cqw 0 {colors.black}"
|
||||
typography: "{typography.label}"
|
||||
description: "The universal eyebrow — never plain text."
|
||||
button-primary:
|
||||
backgroundColor: "{colors.yellow}"
|
||||
textColor: "{colors.black}"
|
||||
border: "0.3cqw solid {colors.black}"
|
||||
rounded: "0"
|
||||
shadow: "0.4cqw 0.4cqw 0 {colors.black}"
|
||||
typography: "Inter 700"
|
||||
description: "The CTA."
|
||||
star-burst:
|
||||
backgroundColor: "any pastel"
|
||||
border: "0.3cqw solid {colors.black}"
|
||||
clip: "10-point clip-path star"
|
||||
description: "Corner attention-grabber."
|
||||
stripe-block:
|
||||
backgroundImage: "45° {colors.black} + pastel diagonal stripes"
|
||||
border: "0.3cqw solid {colors.black}"
|
||||
description: "Poster decoration."
|
||||
bg-dot-grid:
|
||||
backgroundImage: "radial-dot ~1.2px dots, 24px grid, {colors.black}"
|
||||
description: "Faint corner/ground overlay."
|
||||
tilt-deco:
|
||||
transform: "rotate(±2°–12°)"
|
||||
description: "Rotated rectangle/badge/star. Stat cards alternate −2/+2°. The grid-puncture signature."
|
||||
stat-deco-dot:
|
||||
backgroundColor: "any pastel"
|
||||
border: "2px solid {colors.black}"
|
||||
rounded: "50%"
|
||||
size: "12px"
|
||||
description: "The ONLY round shape, pinned to stat cards."
|
||||
close-frame:
|
||||
backgroundColor: "{colors.black}"
|
||||
textColor: "{colors.white}"
|
||||
border: "0.4cqw solid {colors.white}"
|
||||
rounded: "0"
|
||||
shadow: "1.2cqw 1.2cqw 0 {colors.yellow}"
|
||||
description: "Inverted closer — the only colored shadow."
|
||||
---
|
||||
|
||||
# BlockFrame — Frame (video / frame layer)
|
||||
|
||||
## Overview
|
||||
|
||||
BlockFrame at frame scale is a **maximalist neobrutalist** system on five laws: every region has a
|
||||
4px black border, every elevated element an 8px hard offset shadow, every corner square, every
|
||||
accent a saturated pastel, and every layout allowed to be a little crooked. The joy is the
|
||||
deliberate collision — bordered cards meeting bordered cards, shadows stacking, tilted decorations
|
||||
puncturing the grid.
|
||||
|
||||
The voice is **Inter** at weight 800–900 in tight uppercase with negative tracking (display) +
|
||||
weight 500 sentence body, and **Space Grotesk** weight 600 uppercase 0.08em as the label/chrome
|
||||
voice. Five candy pastels (pink/blue/green/yellow/cream) cycle as full-bleed grounds across frames
|
||||
— the color cycling is the primary rhythm. Depth is **hard offset shadow** (8px/4px, solid black,
|
||||
zero blur, bottom-right); the close-frame's 12px yellow shadow is the one colored exception.
|
||||
|
||||
**Key characteristics at frame scale:**
|
||||
|
||||
- **4px black borders + 8px hard shadows** on primary cards; 3px + 4px on chrome (weights coupled).
|
||||
- **Five-pastel palette** cycled as full-bleed grounds; black/white/off-white structural.
|
||||
- **Inter 800–900 uppercase** negative-tracked display; **Space Grotesk** label chrome.
|
||||
- **Square corners** everywhere (only the stat-deco dot is round); **tilted decorations** puncture the grid.
|
||||
- **Label-pills** open every region; star bursts, stripe blocks, dot grids are reusable attention units.
|
||||
- **Comfortably dense** — packed reads as authoritative; empty corners read as broken.
|
||||
|
||||
## The Frame
|
||||
|
||||
### Frame Craft Bar
|
||||
|
||||
Three eyeball tests gate every frame before any structural check:
|
||||
|
||||
- **Squint** — one Inter display moment dominates at 3–6× its neighbor; cards read as a system, not rivals.
|
||||
- **Silence** — cover/quote/close keep air (decorations, not content); the **feature-card and stat grids are the dense exception**.
|
||||
- **Restraint** — the 4px→08px / 3px→4px coupling holds; black borders only (white on close); pastel ground **cycles** one per frame; no sixth pastel.
|
||||
- **Reference** — aim at a **zine / 1990s sticker-book / toy-packaging spread** (bordered blocks, tilted decorations); failure looks like a **flat, borderless, blurred-shadow web card grid**.
|
||||
|
||||
- **Primary:** 1920×1080 (16:9). Display authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`).
|
||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
||||
- **Safe area:** `slide-pad` ~3.1cqw; decorations may bleed off edges.
|
||||
|
||||
**The container law (load-bearing).** Every frame ground sets `container-type: size`; ALL
|
||||
frame-relative units are `cqw`/`cqh` against it — never `vw`. Borders/shadows scale in `cqw` so the
|
||||
4px↔8px coupling holds proportionally; corners stay square.
|
||||
|
||||
## Colors
|
||||
|
||||
Tokens identical to the source. `{colors.offwhite}` is the default ground, but frames **cycle**
|
||||
through `{colors.cream}`/`blue`/`pink`/`green`/`yellow` grounds — the cycle is the rhythm.
|
||||
`{colors.black}` is every border + structural text; `{colors.white}` is card fills. The five pastels
|
||||
are interchangeable with **no semantic meaning** — pair by juxtaposition (pink+blue+green trio,
|
||||
cream+yellow warm pair). `{colors.yellow}` is the CTA + the one colored (close) shadow;
|
||||
`{colors.black}` ground is the close surface. **No sixth pastel.**
|
||||
|
||||
## Typography
|
||||
|
||||
Two ramps. The **reading/chrome ramp** (Inter body 0.95cqw weight 500, Space Grotesk labels in px)
|
||||
carries copy + chrome; the **display ramp** (Inter `heading-md` 2.1cqw → `heading-xl` 5cqw, weight
|
||||
800–900 uppercase) carries every headline + stat.
|
||||
|
||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; px labels are chrome only.
|
||||
- **Fit-to-measure:** size the headline to its length. Cap the block at **≤ 78cqw**; ≤3 words → `heading-xl`; 4–6 → `heading-lg`; 7+ → `heading-md`.
|
||||
- **Inter display is uppercase, weight 800–900, negative-tracked** (−0.01..−0.03em); body weight 500 sentence case; **Space Grotesk labels uppercase 0.08em**. No sentence-case display, no untracked display.
|
||||
|
||||
## Depth & Surface
|
||||
|
||||
Hard offset shadow, solid black, zero blur, bottom-right:
|
||||
|
||||
- **0.8cqw (8px)** primary cards; **0.4cqw (4px)** chrome; **0.6cqw (6px)** hover.
|
||||
- **Border-based depth** — the 4px/3px borders do much of the lift; shadow makes it "elevated."
|
||||
- **Tilt** — ±2°–12° rotation breaks the grid for perceived dimension.
|
||||
- **Inverted close** — 12px YELLOW shadow (and 6px white on close-btn) — the only colored shadows.
|
||||
|
||||
**Ceiling:** no blurred shadow, no rounded corner (save the stat-deco dot), no gradient depth.
|
||||
|
||||
## Shapes
|
||||
|
||||
- **0 radius everywhere** except the 12px stat-deco dot (50%). Square corners are the structural identity.
|
||||
|
||||
## Components
|
||||
|
||||
- **card-elevated / card-small** — the bordered+shadowed content cards (weight-coupled).
|
||||
- **label-pill** — the universal eyebrow (border+shadow+pastel). **button-primary** — yellow CTA.
|
||||
- **star-burst / stripe-block / bg-dot-grid / tilt-deco** — the reusable decoration units (one per frame min).
|
||||
- **stat-deco-dot** — the lone round shape. **close-frame** — the inverted black+white+yellow-shadow closer.
|
||||
|
||||
## Frame Treatments
|
||||
|
||||
> Recipe: ground · container · composes · focal · chrome · accent · silence · Fixed/Free · density.
|
||||
> Cycle the ground color; add ≥1 decoration per frame; open with a label-pill.
|
||||
|
||||
### 1 · Cover (identity · move: decorations puncture · left)
|
||||
|
||||
**Ground** `{colors.cream}` (or offwhite) + faint dot-grid. **Composes** label-pill, heading-xl, tilt-deco rect, star-burst, counter. **Focal** a 2–3 line Inter `heading-xl` uppercase, left, under a label-pill; tilted pastel rects + a star burst puncture the right. **Chrome** counter pill. **Accent** the decorations' pastels. **Silence** right third holds decorations. **Fixed** 4px/8px coupling, uppercase display, square. **Free** title, decoration placement/colors. **Density** comfortable.
|
||||
|
||||
### 2 · Feature Cards (catalog · move: 3-up bordered grid · blue ground)
|
||||
|
||||
**Ground** `{colors.blue}`, `slide-pad`. **Composes** label-pill, heading-lg, 3× card-elevated (icon-square + card-title + body). **Focal** three white bordered+shadowed cards. **Chrome** label-pill eyebrow. **Accent** the pastel icon-squares (pink/green/yellow). **Silence** tight — dense by design. **Fixed** 4px border + 8px shadow, square, uppercase card-titles. **Free** card content, icon hues. **Density** dense-exception.
|
||||
|
||||
### 3 · Stat Grid (data · move: tilted stat cards · green ground)
|
||||
|
||||
**Ground** `{colors.green}`, `slide-pad`. **Composes** label-pill, heading-lg, 3× card-small (tilted −2/+2°, stat-deco dot, stat-number + label). **Focal** three tilted bordered stat cards. **Accent** the deco-dots' pastels + black stat numerals. **Silence** moderate. **Fixed** alternating tilt, 3px+4px, round deco-dot only. **Free** figures (from script), dot hues. **Density** dense-exception.
|
||||
|
||||
### 4 · Closing Plate (closer · move: inverted black · centered)
|
||||
|
||||
**Ground** `{colors.black}`. **Composes** close-frame (4px white border, 12px yellow shadow), label-pill (inverted), close-title, star-burst. **Focal** a white `close-title` inside the white-bordered frame with the yellow offset shadow; a pink star punctures a corner. **Accent** the yellow shadow + pink star. **Silence** ~50%. **Fixed** white-on-black, 12px yellow shadow (only here), square. **Free** sign-off, star placement. **Density** low.
|
||||
|
||||
### 5 · Quote (quote · move: bordered quote frame · pink ground)
|
||||
|
||||
**Ground** `{colors.pink}` (or offwhite). **Composes** label-pill, quote-text in a card-elevated, attribution. **Focal** an Inter `quote-text` (900 uppercase) inside a white bordered+shadowed frame. **Accent** the ground + one decoration. **Silence** moderate. **Fixed** uppercase quote, 4px/8px. **Free** quote, ground. **Density** comfortable.
|
||||
|
||||
### 6 · Timeline (process · move: stepped bordered cards · offwhite)
|
||||
|
||||
**Ground** `{colors.offwhite}`. **Composes** label-pill, heading-lg, 3–4 card-small steps + step-connectors. **Focal** a row of bordered step cards linked by black connector bars, each a pastel + step-num. **Accent** the step pastels. **Silence** moderate. **Fixed** 3px+4px steps, square, connectors. **Free** steps, hues. **Density** standard.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Pair **4px borders with 8px shadows**, **3px with 4px** — the coupling is non-negotiable.
|
||||
- **Cycle pastel grounds** across frames; keep the deck visually rhythmic.
|
||||
- Set Inter display **uppercase, 800–900, negative-tracked**; open every region with a **label-pill**.
|
||||
- Render shadows **solid black, zero blur, bottom-right**; add **≥1 decoration** (tilt/star/stripe/dots) per frame.
|
||||
- Use **yellow** for CTAs; use the **inverted black + 12px yellow shadow** close-frame for the closer.
|
||||
- Tilt decorations ±2°–12°; lean left on most frames.
|
||||
|
||||
### Don't
|
||||
|
||||
- No rounded corners (save the stat-deco dot); no blurred shadows.
|
||||
- No colored borders (black only, save the close-frame white); no sixth pastel.
|
||||
- No sentence-case Inter display; no untracked display; no label as plain text.
|
||||
- Don't keep everything perfectly aligned (tilt is the signature); don't leave corners empty.
|
||||
- Don't blow a headline edge-to-edge — fit to measure.
|
||||
|
||||
## Aspect-Ratio Behavior
|
||||
|
||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
||||
| ------------- | ----------------------------- | ---------------------------- | ------------------------ |
|
||||
| Cover | title left, decorations right | title top, decorations below | title, fewer decorations |
|
||||
| Feature Cards | 3 across | 3 stacked | 2+1 |
|
||||
| Stat Grid | 3 tilted across | 3 tilted stacked | 2×2 |
|
||||
| Closing | centered close-frame | centered, taller | centered |
|
||||
| Quote | quote frame | quote stacked | centered |
|
||||
| Timeline | horizontal steps | vertical (connectors hidden) | 2×2 |
|
||||
|
||||
`slide-pad` holds on the short edge; re-step display above the 1.4cqw floor. Decoration count drops
|
||||
on tighter ratios so the frame stays bordered, not cluttered.
|
||||
|
||||
## Approved Entities
|
||||
|
||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
||||
placeholder. Pastel fills, decorations, and tilts are content-agnostic.
|
||||
|
||||
## Numerals & Claims (hard rule)
|
||||
|
||||
Never invent figures, stats, or counts at frame scale. Render slots as `— figure —`, `{metric}`,
|
||||
`N×`. Stat cards and charts carry placeholders until the script supplies values. Slide counters and
|
||||
list numbers are decorative.
|
||||
|
||||
## Pre-Render Self-Audit
|
||||
|
||||
- **Squint** — one Inter display moment dominates; cards read as a system.
|
||||
- **Silence** — only feature/stat grids run dense; cover/quote/close keep air with decorations.
|
||||
- **Borders/shadows** — 4px↔8px / 3px↔4px coupling holds; solid black, zero blur.
|
||||
- **Color** — pastel ground cycles; black borders only (white on close); no sixth pastel.
|
||||
- **Type** — Inter uppercase 800–900 negative-tracked, fit-to-measure; Space Grotesk labels 0.08em; ≥1.4cqw floor.
|
||||
- **Shape** — square corners (only stat-deco dot round); ≥1 tilt/decoration per frame.
|
||||
- **Fabrication** — every numeral traces to the script, else placeholder.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Motion intentionally out of scope.** frame.md specifies composition only; the source toggles slides via display, no transition.
|
||||
- **Inter + Space Grotesk via Google Fonts.** CJK: Noto Sans SC 900 (sentence case — the uppercase signal drops); lean harder on borders/shadows/decoration to carry the brutalist identity.
|
||||
- **9:16 / 1:1 are guidance**; verify the floor and that decoration count scales down.
|
||||
- Star bursts (clip-path), stripe blocks, dot grids, and tilts are CSS-only; no external imagery is required.
|
||||
@@ -1,223 +0,0 @@
|
||||
<!--
|
||||
BlockFrame — caption skin (preset-local source).
|
||||
|
||||
This is the preset's own lower-third karaoke caption look: a caption skin to read or
|
||||
copy, not to mount as is. A copy used as compositions/captions.html fills its three
|
||||
reserved holes and wraps the fragment in a <template>:
|
||||
· the empty GROUPS array → the engine's scene-aware word groups
|
||||
· the 0 duration + 0 width/height → real total + canvas size
|
||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
||||
|
||||
Token contract — reference ONLY this fixed vocab (set it from the
|
||||
project's frame.md; each var has a blockframe literal
|
||||
fallback for standalone preview):
|
||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
||||
--font-body · --cap-band-top · --cap-band-height
|
||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
||||
|
||||
Visual: a white card carved by a 4px black border with an 8px hard offset shadow
|
||||
(solid black, zero blur, bottom-right — blockframe's sacred depth), square corners.
|
||||
Inter 900, UPPERCASE, negative-tracked. Upcoming words sit in faded ink; the current
|
||||
word gets a saturated-yellow stamp-block — a hard square highlight (box-shadow spread,
|
||||
no blur) with black ink reading on top, blockframe's signature stamp gesture; spoken
|
||||
words settle to solid black ink, stamp cleared. The word pops in with a snappy
|
||||
back.out overshoot — loud, a little crooked.
|
||||
-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<!-- the project's :root tokens go here -->
|
||||
<style data-brand-tokens></style>
|
||||
|
||||
<style>
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE bordered card — 4px black border + 8px hard offset shadow (solid black,
|
||||
zero blur, bottom-right — blockframe's sacred depth), square corners */
|
||||
.caption-pill {
|
||||
max-width: 80%;
|
||||
padding: 18px 40px 22px;
|
||||
background: var(--cap-canvas, #ffffff);
|
||||
border: 4px solid var(--cap-ink, #000000);
|
||||
border-radius: 0;
|
||||
box-shadow: 8px 8px 0 var(--cap-ink, #000000);
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.12em 0.32em;
|
||||
font-family: var(--font-display, "Inter"), system-ui, sans-serif;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
font-size: clamp(38px, 4.2vw, 56px);
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* upcoming — faded black ink, no stamp */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0 0.1em;
|
||||
color: color-mix(in srgb, var(--cap-ink, #000000) 40%, var(--cap-canvas, #ffffff));
|
||||
}
|
||||
/* current word — THE yellow stamp-block (box-shadow spread = a hard square block
|
||||
w/o layout shift; no blur), black ink reading on top — blockframe's signature stamp */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-ink, #000000);
|
||||
background: var(--cap-accent, #f7cb46);
|
||||
box-shadow: 0 0 0 0.08em var(--cap-accent, #f7cb46);
|
||||
}
|
||||
/* already spoken — solid black ink, stamp cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-ink, #000000);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — heavier ink weight, blockframe's display is already 900 */
|
||||
.caption-word.cap-brand {
|
||||
color: var(--cap-ink, #000000);
|
||||
}
|
||||
/* numerals / data — the body voice, slightly recessed */
|
||||
.caption-word.cap-num {
|
||||
font-family: var(--font-body, "Inter"), ui-monospace, monospace;
|
||||
color: color-mix(in srgb, var(--cap-ink, #000000) 74%, var(--cap-canvas, #ffffff));
|
||||
}
|
||||
/* emphasis — a cream hard underline bar (the second-chroma accent) */
|
||||
.caption-word.cap-cta {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 4px;
|
||||
text-decoration-color: var(--cap-accent-2, #ffdc8b);
|
||||
text-underline-offset: 0.12em;
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(34px, 5.4vw, 48px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="0"
|
||||
data-fps="30"
|
||||
data-width="0"
|
||||
data-height="0"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// the scene-aware word groups + real duration go here.
|
||||
var GROUPS = [];
|
||||
var DURATION = 0;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.86 }, { scale: 1, duration: 0.2, ease: "back.out(2.4)" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,307 +0,0 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Blue Professional — Frame (video / frame layer)
|
||||
description: >
|
||||
Video-first companion to Blue Professional's design.md. The unit is the frame (1920×1080). Atoms
|
||||
are identical and sacred — the warm cream canvas, a single saturated cobalt (#1e2bfa) as the only
|
||||
accent, the three-step gray text ladder, Space Grotesk (display/numerals/chrome) + Inter (body),
|
||||
soft cobalt-tinted cards (4% fill / 20% border / 10–14px radius) with NO shadows, pill chrome, and
|
||||
the cobalt progress bar. Composition + frame scale rewritten. Motion out of scope.
|
||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
||||
principle: atoms are sacred · composition is free · numbers come from the script
|
||||
|
||||
colors:
|
||||
bg: "#fdfae7"
|
||||
primary: "#1e2bfa"
|
||||
text: "#111111"
|
||||
text-muted: "#6b6b6b"
|
||||
text-light: "#9a9a9a"
|
||||
accent-light: "rgba(30,43,250,0.08)"
|
||||
accent-medium: "rgba(30,43,250,0.15)"
|
||||
border: "rgba(30,43,250,0.2)"
|
||||
card-bg: "rgba(30,43,250,0.04)"
|
||||
positive: "#059669"
|
||||
negative: "#dc2626"
|
||||
|
||||
radii:
|
||||
pill: "100px"
|
||||
card-lg: "14px"
|
||||
card-md: "12px"
|
||||
card-sm: "10px"
|
||||
bar: "6px"
|
||||
circle: "50%"
|
||||
|
||||
typography:
|
||||
# — reading ramp (Inter body + Space Grotesk chrome) —
|
||||
body: { fontFamily: "Inter", cqw: 0.85, weight: 400, lineHeight: 1.6, color: "text-muted" }
|
||||
h4-eyebrow:{ fontFamily: "Space Grotesk", cqw: 0.8, weight: 600, tracking: "0.08em", upper: true, color: "primary" }
|
||||
tag: { fontFamily: "Space Grotesk", px: 12, weight: 500, color: "primary" }
|
||||
counter: { fontFamily: "Space Grotesk", px: 13, weight: 500, tracking: "0.05em", color: "text-muted" }
|
||||
# — display / numerical ramp (Space Grotesk, near-black headings / cobalt numerals) —
|
||||
h3: { fontFamily: "Space Grotesk", cqw: 1.25, weight: 500, lineHeight: 1.3, tracking: "-0.02em", color: "text" }
|
||||
stat-num:{ fontFamily: "Space Grotesk", cqw: 1.9, weight: 700, lineHeight: 1.0, color: "primary" }
|
||||
blockquote:{ fontFamily: "Space Grotesk", cqw: 2.4, weight: 500, lineHeight: 1.35, color: "text" }
|
||||
h2: { fontFamily: "Space Grotesk", cqw: 2.6, weight: 600, lineHeight: 1.1, tracking: "-0.02em", color: "text" }
|
||||
metric-value:{ fontFamily: "Space Grotesk", cqw: 3.0, weight: 700, lineHeight: 1.0, color: "primary" }
|
||||
h1: { fontFamily: "Space Grotesk", cqw: 4.2, weight: 700, lineHeight: 1.08, tracking: "-0.02em", color: "text" }
|
||||
quote-mark:{ fontFamily: "Space Grotesk", cqw: 8.0, weight: 700, lineHeight: 0.5, color: "primary", opacity: 0.15 }
|
||||
|
||||
spacing:
|
||||
pad-x: "5cqw"
|
||||
pad-y-top: "5cqw"
|
||||
gap-cards: "1.4cqw"
|
||||
accent-line: "60px × 4px"
|
||||
|
||||
components:
|
||||
card-tinted:
|
||||
backgroundColor: "{colors.card-bg}"
|
||||
border: "1.5px solid {colors.border}"
|
||||
rounded: "{radii.card-lg}"
|
||||
shadow: "none"
|
||||
description: "Universal content card. Never solid-colored, never opaque-bordered, NO shadow."
|
||||
metric-card:
|
||||
backgroundColor: "{colors.card-bg}"
|
||||
border: "1.5px solid {colors.border}"
|
||||
rounded: "{radii.card-lg}"
|
||||
typography: "{typography.metric-value} ({colors.primary}) + {typography.metric-label} + {typography.metric-desc}"
|
||||
description: "+ optional inline ↑/↓ change chip ({colors.positive}/{colors.negative} text, no fill)."
|
||||
tag-pill:
|
||||
backgroundColor: "{colors.accent-light}"
|
||||
textColor: "{colors.primary}"
|
||||
rounded: "{radii.pill}"
|
||||
typography: "{typography.tag}"
|
||||
description: "Top-right of the slide-header."
|
||||
cta-button:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.bg}"
|
||||
rounded: "{radii.pill}"
|
||||
typography: "Space Grotesk 600"
|
||||
shadow: "soft cobalt on hover only — the system's only shadow"
|
||||
description: "The one solid element."
|
||||
accent-line:
|
||||
backgroundColor: "{colors.primary}"
|
||||
size: "60×4, 2px radius"
|
||||
description: "Above cover titles / eyebrow separators."
|
||||
bar-track:
|
||||
backgroundColor: "{colors.accent-light}"
|
||||
fill: "{colors.primary} (display:block so width resolves)"
|
||||
rounded: "{radii.bar}"
|
||||
description: "28px track; fill carries the value."
|
||||
step-circle:
|
||||
backgroundColor: "{colors.primary}"
|
||||
textColor: "{colors.bg}"
|
||||
rounded: "50%"
|
||||
size: "56px"
|
||||
description: "Sequential steps fade opacity 1.0→0.85→0.7→0.55."
|
||||
split-highlight:
|
||||
backgroundColor: "{colors.accent-light}"
|
||||
borderLeft: "4px solid {colors.primary}"
|
||||
rounded: "{radii.card-md}"
|
||||
description: "Inline pull-quote callout."
|
||||
slide-header:
|
||||
typography: "{typography.h4-eyebrow} (cobalt) left, tag-pill right; {typography.h2} below"
|
||||
description: "Top band of every content frame."
|
||||
atmosphere:
|
||||
elements: "clipped diagonal cobalt-tint panel, 3×3 cobalt dot grid, concentric closing rings"
|
||||
description: "Cover/closing only. Never on content frames."
|
||||
progress-bar:
|
||||
backgroundColor: "{colors.primary}"
|
||||
size: "3px tall, bottom edge, width grows with index"
|
||||
description: "Persistent progress strip."
|
||||
---
|
||||
|
||||
# Blue Professional — Frame (video / frame layer)
|
||||
|
||||
## Overview
|
||||
|
||||
Blue Professional at frame scale is a **consulting-grade system: restraint with one strong
|
||||
commitment.** A warm cream canvas and a single saturated cobalt that carries every accent —
|
||||
eyebrow, metric, CTA, chart fill, progress bar. No secondary brand color, no pastels, just cream,
|
||||
cobalt, and a tight ladder of grays. The register is investment-research / McKinsey briefing:
|
||||
measured, data-dense without crowding, executive-readable at distance.
|
||||
|
||||
The voice is two faces in fixed roles: **Space Grotesk** (display, every numeral, all chrome —
|
||||
eyebrows uppercase 0.08em) and **Inter** (body, muted gray, line 1.6). Headlines are near-black;
|
||||
cobalt is reserved for accent moments. Depth is **soft and tinted** — 4% cobalt card fills with 20%
|
||||
cobalt borders and 10–14px radii — never shadowed. The lack of harsh shadows is the premium signal.
|
||||
|
||||
**Key characteristics at frame scale:**
|
||||
|
||||
- **Warm cream ground** on every frame; **single cobalt** as the only accent.
|
||||
- **Space Grotesk** (display/numerals/chrome) + **Inter** (body) — near-black headlines, cobalt numerals.
|
||||
- **Tinted cards** — cobalt 4% fill, cobalt 20% 1.5px border, 10–14px radius, **no shadow**.
|
||||
- **Pill chrome** (100px) — tag pills + the one solid cobalt CTA; cobalt **progress bar**.
|
||||
- **Soft rounded corners everywhere** (no square corners save the progress bar).
|
||||
- **Atmosphere** (diagonal panel, dot grid, concentric rings) on cover/closing only.
|
||||
|
||||
## The Frame
|
||||
|
||||
### Frame Craft Bar
|
||||
|
||||
Three eyeball tests gate every frame before any structural check:
|
||||
|
||||
- **Squint** — one **near-black headline or cobalt numeral** dominates at 3–6× its neighbor.
|
||||
- **Silence** — content frames read **balanced, not crowded**; the **dashboard is the one dense exception**.
|
||||
- **Restraint** — a **single cobalt accent** carries everything; headlines stay near-black (never cobalt); no shadows (tinted cards do the lift); positive/negative inline-text only.
|
||||
- **Reference** — aim at an **investment-research / McKinsey quarterly briefing**; failure looks like a **heavy-outlined, multi-color dashboard**.
|
||||
|
||||
- **Primary:** 1920×1080 (16:9). Display authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`).
|
||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
||||
- **Safe area:** `pad-x` 5cqw; bottom reserves room for the counter + progress bar.
|
||||
|
||||
**The container law (load-bearing).** Every frame ground sets `container-type: size`; ALL
|
||||
frame-relative units are `cqw`/`cqh` against it — never `vw`. Card radii stay px (10–14px); the
|
||||
pill radius stays 100px; borders stay 1–1.5px.
|
||||
|
||||
## Colors
|
||||
|
||||
Tokens identical to the source. `{colors.bg}` cream is the universal ground; `{colors.primary}`
|
||||
cobalt is the **only** accent — every eyebrow, numeral, CTA, chart fill, progress bar, and the 4px
|
||||
highlight left-rule. Headlines are `{colors.text}` near-black (never cobalt); body is
|
||||
`{colors.text-muted}`; tertiary is `{colors.text-light}`. Cards fill `{colors.card-bg}` (4%) with
|
||||
`{colors.border}` (20%) borders. `{colors.positive}`/`{colors.negative}` appear **only inline** on
|
||||
directional change chips — never as fills. **No second accent color.**
|
||||
|
||||
## Typography
|
||||
|
||||
Two ramps. The **reading ramp** (Inter body 0.85cqw muted; Space Grotesk eyebrow uppercase 0.08em
|
||||
cobalt) carries copy + chrome; the **display/numerical ramp** (Space Grotesk `h3` 1.25cqw → `h1`
|
||||
4.2cqw near-black; numerals `stat-num`/`metric-value` in cobalt) carries headings and figures.
|
||||
|
||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; px chrome (tag/counter) is colophon only.
|
||||
- **Fit-to-measure:** size the headline to its length. Cap the block at **≤ 78cqw**; ≤3 words → `h1`; 4–6 → `h2`; 7+ → `h3`. Cobalt numerals scale `metric-value`→`stat-num` by card size.
|
||||
- **Headlines near-black, −0.02em**; **eyebrows cobalt, uppercase, 0.08em**; **numerals cobalt 600–700**; **body Inter 400 muted, line 1.6**. No italic, no uppercase body, no cobalt headline.
|
||||
|
||||
## Depth & Surface
|
||||
|
||||
Soft and tinted — never offset. Depth from:
|
||||
|
||||
- **Tinted cards** — 4% cobalt fill + 20% cobalt 1.5px border + 10–14px radius reads as lifted without offset.
|
||||
- **Border-left accent** — the 4px cobalt rule on split-highlight blocks pulls a callout forward.
|
||||
- **Rounded corners** — the 10–14px radius is part of the softness; square corners break it.
|
||||
|
||||
**Ceiling:** zero box-shadow on content (the only shadow is a soft cobalt CTA _hover_); no opaque cobalt borders; no harsh outlines.
|
||||
|
||||
## Shapes
|
||||
|
||||
- **100px** — tag pills, CTA, nav buttons (pill chrome).
|
||||
- **14/12/10px** — cards by size (large metric / standard stat / detail + mini).
|
||||
- **6px** — bar tracks + fills. **50%** — step circles, nav circles, dots, closing rings.
|
||||
- **0** — only the progress bar. No square-cornered content.
|
||||
|
||||
## Components
|
||||
|
||||
- **card-tinted / metric-card** — the universal soft-tint content cards (no shadow).
|
||||
- **tag-pill / cta-button / accent-line** — the cobalt pill chrome + the one solid CTA + the 60×4 rule.
|
||||
- **bar-track / step-circle / split-highlight** — cobalt data + sequence + callout patterns.
|
||||
- **slide-header** (eyebrow + tag pill) — the structural rhythm; **atmosphere** (diagonal/dots/rings) on cover/closing only; **progress-bar** on every frame.
|
||||
|
||||
## Frame Treatments
|
||||
|
||||
> Recipe: ground · container · composes · focal · chrome · accent · silence · Fixed/Free · density.
|
||||
> Atmosphere only on cover/closing; content frames carry the slide-header rhythm.
|
||||
|
||||
### 1 · Cover (identity · move: diagonal accent · left)
|
||||
|
||||
**Ground** cream + the clipped diagonal cobalt-tint panel (right ~36%) + a 3×3 cobalt dot grid.
|
||||
**Composes** accent-line, meta, h1, body sub. **Focal** a 2-line Space Grotesk `h1` near-black, left,
|
||||
under a cobalt accent-line + meta. **Chrome** counter + progress bar. **Accent** the cobalt line +
|
||||
diagonal panel. **Silence** the diagonal panel holds the right third. **Fixed** near-black h1, cobalt
|
||||
accents, atmosphere here only. **Free** title, meta. **Density** low.
|
||||
|
||||
### 2 · Dashboard (data · move: 3-up metric grid · the dense frame)
|
||||
|
||||
**Ground** cream, `pad-x`. **Composes** slide-header (eyebrow + tag-pill), h2, 3× metric/tinted card.
|
||||
**Focal** a row of tinted cards — cobalt `metric-value` + Inter label + muted desc + optional
|
||||
green/red change chip. **Chrome** eyebrow left, tag-pill right; progress bar. **Accent** the cobalt
|
||||
numerals. **Silence** tight — the density exception. **Fixed** 4% tint cards, 20% borders, no shadow,
|
||||
cobalt numerals. **Free** figures (from script), labels. **Density** dense-exception.
|
||||
|
||||
### 3 · Bar Ranking (data · move: cobalt bars · left)
|
||||
|
||||
**Ground** cream, `pad-x`. **Composes** eyebrow, h2, bar-track rows. **Focal** 3–5 labeled
|
||||
cobalt-fill bars on cobalt-8% tracks with cobalt percentages. **Chrome** eyebrow; progress bar.
|
||||
**Accent** the cobalt fills + figures. **Silence** moderate. **Fixed** 6px tracks, cobalt fills.
|
||||
**Free** rows, values (from script). **Density** standard.
|
||||
|
||||
### 4 · Pull Quote (quote · move: concentric rings · centered)
|
||||
|
||||
**Ground** cream, centered, with faint concentric closing-rings behind. **Composes** quote-mark,
|
||||
blockquote, cite. **Focal** a Space Grotesk `blockquote` near-black under a 15%-opacity cobalt
|
||||
quote-mark; an uppercase cobalt-muted cite beneath. **Accent** the faint rings + quote-mark. **Silence**
|
||||
~55%. **Fixed** near-black quote, soft rings. **Free** quote, cite. **Density** low.
|
||||
|
||||
### 5 · Split + Highlight (content · move: asymmetric split · left)
|
||||
|
||||
**Ground** cream, two columns. **Composes** eyebrow, h2, body, split-highlight block. **Focal** an
|
||||
Inter body column beside a cobalt-8% highlight block (4px cobalt left rule) carrying an inline pull
|
||||
quote. **Accent** the highlight's left rule. **Silence** generous gutter. **Fixed** tinted highlight,
|
||||
4px cobalt rule. **Free** body, callout. **Density** standard.
|
||||
|
||||
### 6 · Closing / CTA (closer · move: centered rings + CTA)
|
||||
|
||||
**Ground** cream + concentric closing-rings. **Composes** accent-line, h1, body, cta-button. **Focal**
|
||||
a Space Grotesk `h1` near-black, centered, with the one solid cobalt `cta-button` pill below. **Accent**
|
||||
the CTA + rings. **Silence** ~60%. **Fixed** one CTA, near-black h1, soft rings. **Free** sign-off, CTA
|
||||
label. **Density** low.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Start every frame on **warm cream**; let **cobalt carry every accent** (eyebrow, numeral, CTA, bar, progress).
|
||||
- Set headlines **near-black, −0.02em**; eyebrows **cobalt uppercase 0.08em**; numerals **cobalt 600–700**.
|
||||
- Use **tinted cards** (4% fill, 20% border, 10–14px radius, no shadow); body Inter 400 muted, line 1.6.
|
||||
- Keep all chrome **pill-shaped (100px)**; one solid cobalt CTA per closing frame.
|
||||
- Reserve **atmosphere** (diagonal panel, dots, rings) for cover/closing; content frames keep the slide-header rhythm.
|
||||
- Lean left on cover/dashboard/split, centered on quote/closer.
|
||||
|
||||
### Don't
|
||||
|
||||
- No second accent color; no cobalt headlines.
|
||||
- No drop shadows on content (only the soft cobalt CTA hover); no opaque cobalt borders.
|
||||
- No square corners (save the progress bar); no font substitutes; no uppercase body.
|
||||
- Don't use the green/red change chips as general accents — directional comparisons only.
|
||||
- Don't fill space with heavier borders — add substance; don't blow a headline edge-to-edge.
|
||||
|
||||
## Aspect-Ratio Behavior
|
||||
|
||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
||||
| ----------------- | -------------------------- | ------------------------------ | ------------------------ |
|
||||
| Cover | title left, diagonal right | title top, diagonal band below | title upper, dots corner |
|
||||
| Dashboard | 3 cards across | 3 stacked | 2×2 |
|
||||
| Bar Ranking | 3–5 bars | 3–5 bars (tighter) | 3 bars |
|
||||
| Pull Quote | centered, rings behind | centered, taller | centered |
|
||||
| Split + Highlight | side-by-side | stacked | stacked |
|
||||
| Closing / CTA | centered + CTA | centered + CTA | centered + CTA |
|
||||
|
||||
`pad-x` holds on the short edge; re-step display above the 1.4cqw floor. The diagonal cover panel
|
||||
becomes a top/bottom band on 9:16. Numerals stay Latin Arabic digits in CJK builds.
|
||||
|
||||
## Approved Entities
|
||||
|
||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
||||
placeholder. Figures, metrics, and quotes are content; the system supplies cream + cobalt + grays.
|
||||
|
||||
## Numerals & Claims (hard rule)
|
||||
|
||||
Never invent figures, financials, percentages, or dates at frame scale. Render slots as `— figure —`,
|
||||
`{metric}`, `+NN%`, `↑ —`. Metric cards, bar values, and stat cells especially carry placeholders
|
||||
until the script supplies them. Directional chips require a real comparison from the script.
|
||||
|
||||
## Pre-Render Self-Audit
|
||||
|
||||
- **Squint** — one near-black headline or cobalt numeral dominates per frame.
|
||||
- **Silence** — content frames balanced, not crowded; only the dashboard runs dense.
|
||||
- **Single accent** — cobalt only; headlines near-black; positive/negative inline only.
|
||||
- **Type** — Space Grotesk headings −0.02em near-black, cobalt eyebrows 0.08em + numerals; Inter body muted line 1.6; ≥1.4cqw floor.
|
||||
- **Depth** — tinted cards (no shadow), soft rounded corners, 20% cobalt borders; no square content corners.
|
||||
- **Anchor** — left on cover/dashboard/split, centered on quote/closer; atmosphere on cover/closing only.
|
||||
- **Fabrication** — every numeral traces to the script, else placeholder.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Motion intentionally out of scope.** frame.md specifies composition only; the source's 500ms translateX transitions + bar-fill animations are deck mechanics.
|
||||
- **Space Grotesk + Inter via Google Fonts.** CJK pairing (Noto Sans SC 700 display / Noto Serif SC 400 body) carries over; the eyebrow's uppercase+tracking signal weakens in CJK — pair it with the accent-line.
|
||||
- **9:16 / 1:1 are guidance**; verify the floor and that the diagonal panel reflows to a band.
|
||||
- Diagonal panel (clip-path), dot grid, concentric rings, and bars are CSS-only; no external imagery is required.
|
||||
@@ -1,222 +0,0 @@
|
||||
<!--
|
||||
Blue Professional — caption skin (preset-local source).
|
||||
|
||||
This is the preset's own lower-third karaoke caption look: a caption skin to read or
|
||||
copy, not to mount as is. A copy used as compositions/captions.html fills its three
|
||||
reserved holes and wraps the fragment in a <template>:
|
||||
· the empty GROUPS array → the engine's scene-aware word groups
|
||||
· the 0 duration + 0 width/height → real total + canvas size
|
||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
||||
|
||||
Token contract — reference ONLY this fixed vocab (set it from the
|
||||
project's frame.md; each var has a blue-professional
|
||||
literal fallback for standalone preview):
|
||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
||||
--font-body · --cap-band-top · --cap-band-height
|
||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
||||
|
||||
Visual: a soft cobalt-tinted card on warm cream — a 4% cobalt fill inside a 1.5px
|
||||
20%-cobalt border, soft 14px rounded corners, NO shadow (the system's premium signal
|
||||
is the absence of cast depth; the tinted card does the lift). Space Grotesk, sentence
|
||||
case, near-black glyphs. Upcoming words sit in muted gray; the current word becomes a
|
||||
solid cobalt chip — the brand's signature single-accent gesture, a pill-rounded fill
|
||||
with cream reading on top; spoken words settle to near-black ink, chip cleared.
|
||||
-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<!-- the project's :root tokens go here -->
|
||||
<style data-brand-tokens></style>
|
||||
|
||||
<style>
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE tinted card — 4% cobalt fill, 1.5px 20%-cobalt border, soft 14px corners,
|
||||
NO shadow (the tinted card does the lift; cast depth is forbidden) */
|
||||
.caption-pill {
|
||||
max-width: 78%;
|
||||
padding: 20px 44px 24px;
|
||||
background: color-mix(in srgb, var(--cap-accent, #1e2bfa) 4%, var(--cap-canvas, #fdfae7));
|
||||
border: 1.5px solid color-mix(in srgb, var(--cap-accent, #1e2bfa) 20%, transparent);
|
||||
border-radius: 14px;
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.1em 0.34em;
|
||||
font-family: var(--font-display, "Space Grotesk"), system-ui, sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: clamp(40px, 4.4vw, 58px);
|
||||
line-height: 1.16;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* upcoming — muted gray, no chip */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0 0.18em;
|
||||
border-radius: 0.32em;
|
||||
color: color-mix(in srgb, var(--cap-ink, #111111) 52%, var(--cap-canvas, #fdfae7));
|
||||
}
|
||||
/* current word — THE solid cobalt chip (the single-accent gesture; pill-rounded
|
||||
fill, NO shadow), cream reading on top */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-canvas, #fdfae7);
|
||||
background: var(--cap-accent, #1e2bfa);
|
||||
box-shadow: none;
|
||||
}
|
||||
/* already spoken — solid near-black ink, chip cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-ink, #111111);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — cobalt, the system's one accent on a key word */
|
||||
.caption-word.cap-brand {
|
||||
color: var(--cap-accent, #1e2bfa);
|
||||
}
|
||||
/* numerals / data — cobalt 700, the figure voice */
|
||||
.caption-word.cap-num {
|
||||
font-family: var(--font-display, "Space Grotesk"), system-ui, sans-serif;
|
||||
font-weight: 700;
|
||||
color: var(--cap-accent, #1e2bfa);
|
||||
}
|
||||
/* emphasis — a directional-red hairline underline (inline only, never a fill) */
|
||||
.caption-word.cap-cta {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-color: var(--cap-accent-2, #dc2626);
|
||||
text-underline-offset: 0.1em;
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(34px, 5.4vw, 48px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="0"
|
||||
data-fps="30"
|
||||
data-width="0"
|
||||
data-height="0"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// the scene-aware word groups + real duration go here.
|
||||
var GROUPS = [];
|
||||
var DURATION = 0;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.985 }, { scale: 1, duration: 0.18, ease: "power1.out" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
-1372
File diff suppressed because it is too large.
Load diff
@@ -1,275 +0,0 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Bold Poster — Frame (video / frame layer)
|
||||
description: >
|
||||
Video-first companion to Bold Poster's design.md. The unit is the frame (1920×1080). Atoms are
|
||||
identical and sacred — the four-color palette (white / brown-black ink / tomato red / off-white),
|
||||
the three-face stack (Shrikhand display tilted at poster scale, Libre Baskerville serif body,
|
||||
Space Grotesk mono chrome), the stacked text-shadow on red display, the 3px+1.5px double-border
|
||||
grid, red leftbar cards, red em-dash bullets, and the red progress bar. Composition + frame scale
|
||||
rewritten. Motion out of scope.
|
||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
||||
principle: atoms are sacred · composition is free · numbers come from the script
|
||||
|
||||
colors:
|
||||
bg: "#FFFFFF"
|
||||
dark: "#1C1410"
|
||||
red: "#D8000F"
|
||||
light: "#F5F2EF"
|
||||
|
||||
typography:
|
||||
# — reading + chrome ramp —
|
||||
body: { fontFamily: "Libre Baskerville", cqw: 0.85, weight: 400, lineHeight: 1.75, color: "dark" }
|
||||
body-cell:{ fontFamily: "Libre Baskerville", cqw: 0.7, weight: 400, lineHeight: 1.55 }
|
||||
label: { fontFamily: "Space Grotesk", px: 10, weight: 600, tracking: "2px", upper: true, color: "red" }
|
||||
bullet-body:{ fontFamily: "Space Grotesk", cqw: 0.62, weight: 400, lineHeight: 1.45 }
|
||||
# — display / hero ramp (Shrikhand 400, tilted) —
|
||||
card-title:{ fontFamily: "Shrikhand", cqw: 1.9, weight: 400, lineHeight: 1.1, color: "dark" }
|
||||
cell-number:{ fontFamily: "Shrikhand", cqw: 2.7, weight: 400, lineHeight: 1.0, color: "red" }
|
||||
section-header:{ fontFamily: "Shrikhand", cqw: 3.3, weight: 400, lineHeight: 1.0, color: "dark" }
|
||||
red-quote:{ fontFamily: "Shrikhand", cqw: 4.7, weight: 400, lineHeight: 1.15, color: "bg", shadow: "stacked" }
|
||||
hero-title-bottom:{ fontFamily: "Shrikhand", cqw: 10.4, weight: 400, lineHeight: 0.9, color: "dark", rotate: "2deg" }
|
||||
hero-title:{ fontFamily: "Shrikhand", cqw: 11.5, weight: 400, lineHeight: 0.88, color: "dark" }
|
||||
hero-title-red:{ fontFamily: "Shrikhand", cqw: 13.5, weight: 400, lineHeight: 0.85, color: "red", rotate: "-4deg" }
|
||||
close-big:{ fontFamily: "Shrikhand", cqw: 13.5, weight: 400, lineHeight: 0.88, color: "red", rotate: "-5deg" }
|
||||
stat-big:{ fontFamily: "Shrikhand", cqw: 22.0, weight: 400, lineHeight: 0.82, color: "red", rotate: "-6deg" }
|
||||
|
||||
spacing:
|
||||
pad-slide: "3cqw 3.6cqw"
|
||||
gap-grid: "1.5cqw 2cqw"
|
||||
|
||||
components:
|
||||
progress-bar:
|
||||
backgroundColor: "{colors.red}"
|
||||
size: "0.5cqw tall, bottom edge, width grows with index"
|
||||
description: "The most prominent chrome."
|
||||
hero-title-stack:
|
||||
typography: "{typography.hero-title} + {typography.hero-title-red} + {typography.hero-title-bottom}"
|
||||
transform: "≥4 tilt (−4°/+2°), ≥1 line in {colors.red}"
|
||||
description: "A 3-line Shrikhand composition — the signature opener."
|
||||
stat-big:
|
||||
typography: "{typography.stat-big}"
|
||||
color: "{colors.red} (or {colors.bg} on red with stacked shadow)"
|
||||
transform: "rotate(-6deg)"
|
||||
description: "Hero numeral at poster scale."
|
||||
fin-grid:
|
||||
border: "0.3cqw solid {colors.dark} outer"
|
||||
rule: "0.15cqw solid {colors.dark} inner (touching at intersections)"
|
||||
rounded: "0"
|
||||
typography: "{typography.cell-number} ({colors.red}) + {typography.label} + {typography.body-cell}"
|
||||
description: "The double-border tabular signature."
|
||||
red-leftbar-card:
|
||||
borderLeft: "4px solid {colors.red}"
|
||||
padding: "0 0 0 ~1cqw"
|
||||
rounded: "0"
|
||||
shadow: "none"
|
||||
typography: "{typography.card-title} + {typography.body} + red em-dash bullets"
|
||||
description: "Editorial card cantilevered off a red left rule — no outline."
|
||||
red-panel:
|
||||
backgroundColor: "{colors.red}"
|
||||
textColor: "{colors.bg}"
|
||||
description: "Full-bleed statement surface; display carries the stacked text-shadow."
|
||||
dark-panel:
|
||||
backgroundColor: "{colors.dark}"
|
||||
textColor: "{colors.bg}"
|
||||
description: "Full-bleed dark statement surface; red accents."
|
||||
stacked-text-shadow:
|
||||
shadow: "2px 2px 0 rgba(28,20,16,.25), 4px 4px 0 rgba(28,20,16,.2), 6px 6px 0 rgba(28,20,16,.15)"
|
||||
appliesTo: "red display text on red panels only"
|
||||
description: "The ONLY shadow in the system — text-shadow, not box-shadow."
|
||||
bullet:
|
||||
marker: "red em-dash (—) or round (•) glyph at absolute-left"
|
||||
color: "{colors.red}"
|
||||
description: "No default disc bullets; capped at three."
|
||||
---
|
||||
|
||||
# Bold Poster — Frame (video / frame layer)
|
||||
|
||||
## Overview
|
||||
|
||||
Bold Poster at frame scale is a **populist editorial poster** — vintage Italian sports-magazine
|
||||
display, classical serif body, one saturated tomato red, grids ruled in ink. Every frame should
|
||||
feel _printed_: heavy display type at poster scale, locked to one red accent, on a white/off-white
|
||||
sheet (or a full red/dark statement panel), with decoration kept to a strict minimum.
|
||||
|
||||
The voice is a three-face stack: **Shrikhand** (heavy slab-script, weight 400 only, routinely
|
||||
tilted −6°..+2°) carries every hero title, section header, stat, and card title; **Libre
|
||||
Baskerville** (literary serif) carries every body paragraph — it's what makes the system feel
|
||||
printed; **Space Grotesk** (uppercase, 2–3px tracked) is chrome only — labels, eyebrows, counters,
|
||||
bullet bodies. The plane is flat; the _only_ shadow is the stacked text-shadow on red display.
|
||||
|
||||
**Key characteristics at frame scale:**
|
||||
|
||||
- **Four colors only** — white / brown-black ink / tomato red / off-white. Red is the lone accent.
|
||||
- **Shrikhand display, tilted** (−6° stat, −5° close, −4° hero-red, +2° hero-bottom) — the signature movement.
|
||||
- **Libre Baskerville serif body** (line 1.75); **Space Grotesk** chrome (uppercase tracked).
|
||||
- **Double-border grids** (3px outer + 1.5px inner ink); **red leftbar cards**; **red em-dash bullets** (max 3).
|
||||
- **Stacked text-shadow** on red display — the only shadow; flat plane otherwise; square corners.
|
||||
- **Red progress bar** at the bottom edge of every frame.
|
||||
|
||||
## The Frame
|
||||
|
||||
### Frame Craft Bar
|
||||
|
||||
Three eyeball tests gate every frame before any structural check:
|
||||
|
||||
- **Squint** — **one display moment dominates** at 3–6× everything else (the hero stack, `stat-big`, or `close-big`); nothing competes.
|
||||
- **Silence** — statement frames reserve **~50–60% negative space**; the **financial grid is the one dense exception**.
|
||||
- **Restraint** — **four colors only**, red the lone accent; one display moment per frame; the stacked text-shadow on red is the only shadow; bullets capped at three.
|
||||
- **Reference** — aim at a **vintage Italian sports-magazine cover / mid-century European annual report**; failure looks like a **rounded, soft-shadowed multi-accent slide**.
|
||||
|
||||
- **Primary:** 1920×1080 (16:9). Display authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`).
|
||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
||||
- **Safe area:** `pad-slide` ~3cqw — deliberately tight so the poster type crowds the frame.
|
||||
|
||||
**The container law (load-bearing).** Every frame ground sets `container-type: size`; ALL
|
||||
frame-relative units are `cqw`/`cqh` against it — never `vw`. Borders stay px; rotation transforms hold.
|
||||
|
||||
## Colors
|
||||
|
||||
Tokens identical to the source. `{colors.bg}` white is the default ground; `{colors.dark}` is body,
|
||||
borders, headers; `{colors.red}` is the only accent — every numeral, section rule, eyebrow,
|
||||
leftbar, bullet, progress bar, and the full statement-panel ground. `{colors.light}` off-white
|
||||
stripes alternating panels. **No fifth color** (no green/blue/yellow); categorical difference comes
|
||||
from position, label, and tilt. Red is never body text, never a tint, never an untexted fill.
|
||||
|
||||
## Typography
|
||||
|
||||
Two ramps. The **reading/chrome ramp** (Baskerville body 0.85cqw, Space Grotesk labels in px)
|
||||
carries copy + chrome; the **display ramp** (Shrikhand `section-header` 3.3cqw → `stat-big` 22cqw)
|
||||
carries every statement and numeral.
|
||||
|
||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; mono labels are chrome only.
|
||||
- **Fit-to-measure:** size the headline to its length. Cap the block at **≤ 78cqw**; ≤2 words → `stat-big`/`hero-title-red`; 3–4 → `hero-title`; 5+ → `section-header`. The hero is a stacked 3-line composition.
|
||||
- **Shrikhand is weight 400, tilted on statement/hero elements, red on numerals**; **Baskerville body at line ≥1.5**; **Space Grotesk chrome uppercase, 2–3px**. Inline `<strong>` switches face to Space Grotesk 600. No italic display, no untilted red hero.
|
||||
|
||||
## Depth & Surface
|
||||
|
||||
Flat plane. Depth from:
|
||||
|
||||
- **Heavy borders** — 3px+1.5px double-border grids, 2px global-card outline, 4px red leftbar rules.
|
||||
- **Surface inversion** — full-bleed red or dark statement panels.
|
||||
- **Tilt** — rotated Shrikhand breaks the baseline for perceived dimension.
|
||||
- **The single shadow** — stacked text-shadow on red display only (text-shadow, three steps).
|
||||
|
||||
**Ceiling:** no box-shadow, no rounded surface (square corners; only the hint pill is 4px), no gradient.
|
||||
|
||||
## Shapes
|
||||
|
||||
- **0 radius everywhere** except the hint pill (4px). Cards, cells, panels, callouts — sharp rectangles.
|
||||
|
||||
## Components
|
||||
|
||||
- **hero-title-stack** — the 3-line tilted opener. **stat-big** — the poster numeral.
|
||||
- **fin-grid** — the double-border tabular signature. **red-leftbar-card** — the cantilevered editorial card.
|
||||
- **red-panel / dark-panel** — statement surfaces. **stacked-text-shadow** — the one shadow.
|
||||
- **bullet** — red em-dash (max 3). **progress-bar** — the red bottom strip.
|
||||
|
||||
## Frame Treatments
|
||||
|
||||
> Recipe: ground · register · composes · focal · chrome · accent · silence · Fixed/Free · density.
|
||||
> One display moment per frame; statement frames reserve massive negative space.
|
||||
|
||||
### 1 · Hero Stack (identity · move: 3-line tilted stack · left)
|
||||
|
||||
**Ground** white. **Composes** hero-title-stack, label, body tagline, progress-bar. **Focal** a
|
||||
3-line Shrikhand stack (e.g. ink / red-tilted / paper) — one red line, one+ tilted. **Chrome** mono
|
||||
eyebrow + Baskerville tagline; bottom progress bar + counter. **Accent** the red line. **Silence**
|
||||
right half open. **Fixed** Shrikhand 400, ≥1 tilt + ≥1 red, square. **Free** the words, line sizes.
|
||||
**Density** low.
|
||||
|
||||
### 2 · Hero Stat (statement · move: poster numeral · red panel · centered)
|
||||
|
||||
**Ground** full `{colors.red}`. **Composes** stat-big, label, body sub. **Focal** a `stat-big`
|
||||
numeral rotated −6° in white with the stacked shadow, centered, with a mono label above + Baskerville
|
||||
sub below. **Accent** the ink stacked-shadow on white. **Silence** ~60%. **Fixed** white-on-red +
|
||||
stacked shadow, −6° tilt. **Free** the figure (from script), label. **Density** low.
|
||||
|
||||
### 3 · Financial Grid (data · move: double-border matrix · the dense frame)
|
||||
|
||||
**Ground** white, `pad-slide`. **Composes** label, section-header, fin-grid. **Focal** a 3px-outer/
|
||||
1.5px-inner ink grid of cells (red Shrikhand numeral + mono label + Baskerville body). **Chrome** mono
|
||||
eyebrow; progress bar. **Accent** the red numerals. **Silence** tight — the density exception.
|
||||
**Fixed** double-border, red numerals, serif body. **Free** figures (from script), labels. **Density** dense-exception.
|
||||
|
||||
### 4 · Pull Quote (quote · move: tilted/stacked display · red panel · left)
|
||||
|
||||
**Ground** full `{colors.red}`. **Composes** red-quote, Baskerville cite. **Focal** a 2-line Shrikhand
|
||||
quote in white with the stacked shadow; a Baskerville cite beneath. **Accent** the stacked shadow.
|
||||
**Silence** ~50%. **Fixed** white-on-red + stacked shadow. **Free** quote, cite. **Density** low.
|
||||
|
||||
### 5 · Editorial Cards (content · move: red leftbar cards · left)
|
||||
|
||||
**Ground** white (or alternating off-white stripes). **Composes** label, section-header, 2–3×
|
||||
red-leftbar-card. **Focal** cards cantilevered off 4px red rules — Shrikhand title + Baskerville body
|
||||
|
||||
- red em-dash bullets (max 3). **Accent** the red left rules + bullets. **Silence** moderate. **Fixed**
|
||||
4px red leftbar, em-dash bullets, no outline. **Free** card content. **Density** standard.
|
||||
|
||||
### 6 · Closing Statement (closer · move: tilted close-big · centered/left)
|
||||
|
||||
**Ground** white or `{colors.dark}`. **Composes** close-big, label, body sub, progress-bar. **Focal**
|
||||
a Shrikhand `close-big` (rotated −5°, red) sign-off; mono eyebrow + Baskerville contact line. **Accent**
|
||||
the red tilted title. **Silence** ~60%. **Fixed** −5° tilt, red close-big. **Free** sign-off, contact.
|
||||
**Density** low.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Stack hero titles in 3 Shrikhand lines — at least one tilted, at least one red.
|
||||
- Make every numeral **red Shrikhand**; tilt statement display **−5° to −6°**.
|
||||
- Set eyebrows in **Space Grotesk 600 uppercase, 2–3px**, red; body in **Baskerville, line 1.75**.
|
||||
- Build data grids with the **3px outer + 1.5px inner double border**; use red leftbar cards elsewhere.
|
||||
- Use **red em-dash bullets, capped at three**; apply the stacked text-shadow on red display.
|
||||
- One display moment per frame; reserve negative space on statement frames.
|
||||
|
||||
### Don't
|
||||
|
||||
- No second accent color; no rounded corners (square only, save the hint pill).
|
||||
- No drop shadow — the stacked text-shadow on red is the only one.
|
||||
- No font substitutes; no Shrikhand body, no Baskerville labels, no Space Grotesk headlines.
|
||||
- No default disc bullets; no untilted red statement display.
|
||||
- Don't crowd a statement frame; don't blow a long line edge-to-edge (step down).
|
||||
|
||||
## Aspect-Ratio Behavior
|
||||
|
||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
||||
| --------------- | ------------------------- | ------------------------ | -------------------- |
|
||||
| Hero Stack | stack left, tagline below | stack centered, taller | stack, tagline below |
|
||||
| Hero Stat | numeral centered | numeral centered, taller | centered |
|
||||
| Financial Grid | 3 cells across | 2 across / stacked | 2×2 |
|
||||
| Pull Quote | quote left | quote stacked | centered |
|
||||
| Editorial Cards | 2–3 across | stacked | stacked |
|
||||
| Closing | close-big centered | close-big stacked | centered |
|
||||
|
||||
`pad-slide` stays tight on the short edge; re-step display so the line stays ≤78cqw above the floor.
|
||||
Tilts hold; the progress bar spans the bottom on every ratio.
|
||||
|
||||
## Approved Entities
|
||||
|
||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
||||
placeholder. The system supplies type, one red, and ink rules, not brands.
|
||||
|
||||
## Numerals & Claims (hard rule)
|
||||
|
||||
Never invent figures, financials, percentages, or dates at frame scale. Render slots as `— figure —`,
|
||||
`{metric}`, `+NN%`. Fin-grid cells and stat-big especially carry placeholders until the script
|
||||
supplies them. Catalogue numbers / progress are decorative.
|
||||
|
||||
## Pre-Render Self-Audit
|
||||
|
||||
- **Squint** — one display moment dominates; nothing competes.
|
||||
- **Silence** — statement frames reserve ~50–60% negative space; only the fin-grid runs dense.
|
||||
- **Color** — four colors only; red is the lone accent; no red body text.
|
||||
- **Type** — Shrikhand 400 tilted on statements, red numerals, fit-to-measure; Baskerville body line 1.75; mono chrome uppercase 2–3px; ≥1.4cqw floor.
|
||||
- **Depth** — flat; the stacked text-shadow on red display is the only shadow; square corners.
|
||||
- **Bullets** — red em-dash, capped at three.
|
||||
- **Fabrication** — every numeral traces to the script, else placeholder.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Motion intentionally out of scope.** frame.md specifies composition only; the source's 550ms slide transitions are deck mechanics.
|
||||
- **Shrikhand + Libre Baskerville + Space Grotesk via Google Fonts.** CJK: Noto Serif SC 900/400 + Noto Sans SC; Shrikhand's slab-script has no Hanzi equal — keep tilts + red + double-borders to carry the identity.
|
||||
- **9:16 / 1:1 are guidance**; verify the one big line ≤78cqw per ratio and that tilts don't clip.
|
||||
- Grids, leftbar cards, and the stacked shadow are CSS-only; no external imagery is required.
|
||||
@@ -1,229 +0,0 @@
|
||||
<!--
|
||||
Bold Poster — caption skin (preset-local source).
|
||||
|
||||
This is the preset's own lower-third karaoke caption look: a caption skin to read or
|
||||
copy, not to mount as is. A copy used as compositions/captions.html fills its three
|
||||
reserved holes and wraps the fragment in a <template>:
|
||||
· the empty GROUPS array → the engine's scene-aware word groups
|
||||
· the 0 duration + 0 width/height → real total + canvas size
|
||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
||||
|
||||
Token contract — reference ONLY this fixed vocab (set it from the
|
||||
project's frame.md; each var has a bold-poster
|
||||
literal fallback for standalone preview):
|
||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
||||
--font-body · --cap-band-top · --cap-band-height
|
||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
||||
|
||||
Visual: a fresh-paper card with bold poster framing — a heavy 1.5px brown-black ink
|
||||
rule plus a 4px tomato-red leftbar (the brand's cantilevered-card signature), SQUARE
|
||||
corners, NO drop shadow (the system's only shadow is the stacked text-shadow on red
|
||||
display). Serif body face, ink glyphs. Upcoming words sit in faint ink; the current
|
||||
word slams into a tomato-red panel (the brand's signature surface inversion, a hard
|
||||
square block on paper-white reading, no blur) with the stacked text-shadow on it;
|
||||
spoken words settle to solid ink, panel cleared.
|
||||
-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<!-- the project's :root tokens go here -->
|
||||
<style data-brand-tokens></style>
|
||||
|
||||
<style>
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE paper card — heavy 1.5px brown-black ink rule + a 4px tomato-red leftbar
|
||||
(bold poster's cantilevered red-leftbar-card signature), square corners, NO
|
||||
drop shadow (the only shadow in the system is the stacked text-shadow on red) */
|
||||
.caption-pill {
|
||||
max-width: 78%;
|
||||
padding: 20px 44px 24px;
|
||||
background: var(--cap-canvas, #ffffff);
|
||||
border: 1.5px solid var(--cap-ink, #1c1410);
|
||||
border-left: 4px solid var(--cap-accent, #d8000f);
|
||||
border-radius: 0;
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.1em 0.34em;
|
||||
font-family: var(--font-display, "Shrikhand"), Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(40px, 4.4vw, 58px);
|
||||
line-height: 1.16;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* upcoming — faint brown-black ink, no panel */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0 0.06em;
|
||||
color: color-mix(in srgb, var(--cap-ink, #1c1410) 42%, var(--cap-canvas, #ffffff));
|
||||
}
|
||||
/* current word — THE tomato-red panel (box-shadow spread = a hard square block
|
||||
w/o layout shift; no blur), paper-white reading on top carrying the stacked
|
||||
text-shadow — bold poster's surface-inversion + only-shadow signatures */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-canvas, #ffffff);
|
||||
background: var(--cap-accent, #d8000f);
|
||||
box-shadow: 0 0 0 0.06em var(--cap-accent, #d8000f);
|
||||
text-shadow:
|
||||
0.03em 0.03em 0 rgba(28, 20, 16, 0.25),
|
||||
0.06em 0.06em 0 rgba(28, 20, 16, 0.2),
|
||||
0.09em 0.09em 0 rgba(28, 20, 16, 0.15);
|
||||
}
|
||||
/* already spoken — solid brown-black ink, panel cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-ink, #1c1410);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — Shrikhand display, bold poster's display face inflection */
|
||||
.caption-word.cap-brand {
|
||||
font-family: var(--font-display, "Shrikhand"), Georgia, serif;
|
||||
}
|
||||
/* numerals / data — the mono chrome voice, slightly recessed */
|
||||
.caption-word.cap-num {
|
||||
font-family: var(--font-body, "Libre Baskerville"), ui-monospace, monospace;
|
||||
color: color-mix(in srgb, var(--cap-ink, #1c1410) 72%, var(--cap-canvas, #ffffff));
|
||||
}
|
||||
/* emphasis — a tomato-red em-dash-weight underline (the lone accent) */
|
||||
.caption-word.cap-cta {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-color: var(--cap-accent-2, #f5f2ef);
|
||||
text-underline-offset: 0.1em;
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(34px, 5.4vw, 48px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="0"
|
||||
data-fps="30"
|
||||
data-width="0"
|
||||
data-height="0"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// the scene-aware word groups + real duration go here.
|
||||
var GROUPS = [];
|
||||
var DURATION = 0;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.9 }, { scale: 1, duration: 0.16, ease: "back.out(2.2)" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,284 +0,0 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Broadside — Frame (video / frame layer)
|
||||
description: >
|
||||
Video-first companion to Broadside's design.md. The unit is the frame (1920×1080). Atoms are
|
||||
identical and sacred — the two-register surface system (dark ink-black / fire-orange), massive
|
||||
Barlow in lowercase weight 900 treated as graphic primitive, IBM Plex Mono chrome (uppercase,
|
||||
0.14em), the single fire-orange accent, the flat plane, and 1px hairline dividers. Composition +
|
||||
frame scale rewritten for the frame. Motion out of scope.
|
||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
||||
principle: atoms are sacred · composition is free · numbers come from the script
|
||||
|
||||
colors:
|
||||
ink-black: "#111111"
|
||||
ink-black-alt: "#1A1A18"
|
||||
fire-orange: "#E85D26"
|
||||
cream: "#F0ECE5"
|
||||
cream-muted: "#888880"
|
||||
cream-hint: "#505048"
|
||||
border-dark: "#282826"
|
||||
ink-on-orange-muted: "rgba(17,17,17,0.75)"
|
||||
ink-on-orange-hint: "rgba(17,17,17,0.55)"
|
||||
ink-on-orange-faint: "rgba(17,17,17,0.40)"
|
||||
ink-on-orange-border: "rgba(17,17,17,0.20)"
|
||||
|
||||
typography:
|
||||
# — reading ramp —
|
||||
body: { fontFamily: "Barlow", cqw: 1.2, weight: 400, lineHeight: 1.6 }
|
||||
lead: { fontFamily: "Barlow", cqw: 1.6, weight: 400, lineHeight: 1.5 }
|
||||
caption: { fontFamily: "Barlow", cqw: 0.9, weight: 400, lineHeight: 1.5 }
|
||||
label: { fontFamily: "IBM Plex Mono", cqw: 0.72, weight: 500, tracking: "0.14em", upper: true }
|
||||
# — display / hero ramp (Barlow, lowercase, negative tracking) —
|
||||
h3: { fontFamily: "Barlow", cqw: 2.8, weight: 600, lineHeight: 1.2, lower: true }
|
||||
quote-text:{ fontFamily: "Barlow", cqw: 3.8, weight: 700, lineHeight: 1.15, tracking: "-0.02em", lower: true }
|
||||
h2: { fontFamily: "Barlow", cqw: 4.5, weight: 700, lineHeight: 1.1, tracking: "-0.02em", lower: true }
|
||||
stat-value:{ fontFamily: "Barlow", cqw: 5.5, weight: 900, lineHeight: 1.0, tracking: "-0.04em" }
|
||||
h1: { fontFamily: "Barlow", cqw: 7.5, weight: 800, lineHeight: 0.9, tracking: "-0.03em", lower: true }
|
||||
fadelist-item:{ fontFamily: "Barlow", cqw: 7.5, weight: 900, lineHeight: 1.0, tracking: "-0.03em", lower: true }
|
||||
quote-mark:{ fontFamily: "Barlow", cqw: 10.0, weight: 900, lineHeight: 0.6 }
|
||||
fadelist-title:{ fontFamily: "Barlow", cqw: 10.5, weight: 900, lineHeight: 0.9, tracking: "-0.04em", lower: true }
|
||||
display: { fontFamily: "Barlow", cqw: 13.0, weight: 900, lineHeight: 0.88, tracking: "-0.04em", lower: true }
|
||||
|
||||
spacing:
|
||||
pad-x: "5.5cqw"
|
||||
pad-y: "5.5cqw"
|
||||
gap-lg: "3.5cqw"
|
||||
gap-md: "2cqw"
|
||||
gap-sm: "1cqw"
|
||||
|
||||
components:
|
||||
registers:
|
||||
dark: "ground {colors.ink-black}, text {colors.cream}, accent {colors.fire-orange}"
|
||||
orange: "ground {colors.fire-orange}, text {colors.ink-black}"
|
||||
description: "Two surfaces only — no cream/paper register. One register per frame."
|
||||
slide-chrome:
|
||||
rule: "1px solid {colors.border-dark} (dark) / 20% ink (orange)"
|
||||
placement: "top + bottom bars (label left, number right)"
|
||||
description: "SUPPRESSED on cover/chapter/statement/quote/end — declarative frames let type fill the field."
|
||||
kicker:
|
||||
typography: "{typography.label}"
|
||||
color: "{colors.fire-orange} (dark) / 55% ink (orange)"
|
||||
description: "Uppercase mono eyebrow."
|
||||
rule:
|
||||
backgroundColor: "{colors.fire-orange} (dark) / {colors.ink-black} (orange)"
|
||||
size: "36×2px"
|
||||
description: "Stub accent bar — the system's only ornament."
|
||||
broadside-num:
|
||||
typography: "{typography.label}"
|
||||
placement: "top-left of orange cover/chapter, low opacity"
|
||||
description: "Mono catalogue numeral."
|
||||
stat-card:
|
||||
borderTop: "1px solid {colors.border-dark}"
|
||||
typography: "{typography.stat-value} (orange on dark / ink on orange) + {typography.body} + {typography.label}"
|
||||
description: "Top-border-only block, no other borders."
|
||||
bullet:
|
||||
marker: "orange `/` mono via ::before"
|
||||
typography: "{typography.lead}"
|
||||
description: "Capped at THREE items."
|
||||
bar-track:
|
||||
borderLeft: "1px solid {colors.border-dark}"
|
||||
bars: "{colors.cream-hint}, one .accent {colors.fire-orange}"
|
||||
typography: "{typography.label} axis"
|
||||
description: "Vertical bar chart, left axis only."
|
||||
compare-panel:
|
||||
layout: "two equal panels split by a 1px vertical rule"
|
||||
payoff: "right panel may fill {colors.fire-orange}"
|
||||
description: "Before/after."
|
||||
fadelist:
|
||||
typography: "{typography.fadelist-item} ×3 at opacity 1.0/0.5/0.22 + {typography.fadelist-title}"
|
||||
description: "Three stacked words + one oversized title opposite."
|
||||
---
|
||||
|
||||
# Broadside — Frame (video / frame layer)
|
||||
|
||||
## Overview
|
||||
|
||||
Broadside at frame scale is a **protest-poster system where type is so large it stops reading as
|
||||
text and becomes graphic primitive.** Barlow `display` at 13cqw puts a single lowercase word
|
||||
nearly across the frame. The system runs in **two registers**: a dark ink-black ground with cream
|
||||
text for documentation, and a fire-orange ground with dark ink for declaration. Fire-orange is the
|
||||
_only_ color — accent on dark, environment on orange. The plane is flat; hierarchy is weight, size,
|
||||
and 1px hairlines.
|
||||
|
||||
**Barlow** carries every text role from display to body — expressive range from weight (400–900)
|
||||
and size, not face contrast. **IBM Plex Mono** is chrome only (numbers, kickers, tags, axis labels,
|
||||
the `/` bullet marker), always uppercase and tracked. Display is **lowercase** — the system's most
|
||||
distinctive single decision, a deliberate inversion of the brutalist norm.
|
||||
|
||||
**Key characteristics at frame scale:**
|
||||
|
||||
- **Two registers** — dark (cream text) / orange (ink text). No cream/paper register.
|
||||
- **Massive lowercase Barlow 900**, negative-tracked, as graphic primitive (display 13cqw).
|
||||
- **Fire-orange is the only color** — accent on dark, full environment on orange.
|
||||
- **IBM Plex Mono chrome** — uppercase, 0.14em; the `/` bullet marker; mono catalogue numbers.
|
||||
- **Flat plane** — no shadow, no radius (save nav dots), no gradient; 1px hairlines carry structure.
|
||||
- **Low density** — one statement per frame, bullets capped at three, chrome suppressed on declarative frames.
|
||||
|
||||
## The Frame
|
||||
|
||||
### Frame Craft Bar
|
||||
|
||||
Three eyeball tests gate every frame before any structural check:
|
||||
|
||||
- **Squint** — exactly **one display moment dominates** at 3–6× everything else; nothing competes.
|
||||
- **Silence** — declarative frames read **45–55% empty**; the **stat grid is the one dense exception**.
|
||||
- **Restraint** — **one register per frame**; **fire-orange is the only color** (accent on dark, environment on orange); one display moment; bullets capped at three.
|
||||
- **Reference** — aim at **broadside printing / a SPACE10 report / a Wim Crouwel grid with one loud color**; failure looks like a **multi-accent corporate slide deck**.
|
||||
|
||||
- **Primary:** 1920×1080 (16:9). Type authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`; or carry the source's `vw` 1:1).
|
||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
||||
- **Safe area:** `pad-x`/`pad-y` 5.5cqw — deliberately tight so the massive type crowds the frame edge.
|
||||
|
||||
**The container law (load-bearing).** Every frame ground sets `container-type: size`; ALL
|
||||
frame-relative units are `cqw`/`cqh` against it — never `vw` (a `vw`-sized frame inflates when not
|
||||
full-screen). 1px hairlines stay 1px.
|
||||
|
||||
## Colors
|
||||
|
||||
Tokens identical to the source, in two registers. **Dark:** `{colors.ink-black}` ground,
|
||||
`{colors.cream}` text, `{colors.fire-orange}` accent (kickers, accent stat, bullet `/`, lead bar,
|
||||
quote mark, rule stub). **Orange:** `{colors.fire-orange}` ground, `{colors.ink-black}` headlines +
|
||||
body, with the dark-ink overlays (75/55/40/20%) as the muted tones. Choose one register per frame
|
||||
and commit. **No second accent color** — on orange, emphasis is weight/opacity on the ink, never a
|
||||
new hue. Cream text on orange does not exist (ink-on-fire is absolute).
|
||||
|
||||
## Typography
|
||||
|
||||
Two ramps. The **reading ramp** (Barlow body 1.2cqw, mono label 0.72cqw) carries copy + chrome; the
|
||||
**display ramp** (Barlow `h2` 4.5cqw → `display` 13cqw, weight 700–900) carries every statement.
|
||||
|
||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; mono labels are chrome only.
|
||||
- **Fit-to-measure:** size the headline to its length. Cap the block at **≤ 78cqw**; ≤2 words → `display`; 3–4 → `h1`; 5+ → `h2`. Broadside packs only ONE display moment per frame.
|
||||
- **Barlow display is lowercase, weight 700–900, negative-tracked** (−0.04em largest, −0.02em h2). **Mono chrome is uppercase, 0.1em+.** No italic, no underline, no uppercase display.
|
||||
|
||||
## Depth & Surface
|
||||
|
||||
Flat plane, the only technique. Hierarchy from:
|
||||
|
||||
- **Weight + size contrast** — the dominant signal (900 lowercase display).
|
||||
- **1px hairlines** — chrome bars, stat-card top, compare divider, bar-track left, chart baseline.
|
||||
- **Color shift** — orange on ink, ink on cream, cream-muted on cream.
|
||||
- **Negative space** — generous, intentional empty regions.
|
||||
|
||||
**Ceiling:** no box-shadow, no elevation, no rounded surface (save nav dots), no gradient ground.
|
||||
|
||||
## Shapes
|
||||
|
||||
- **0 radius everywhere** except nav dots (50%). Cards, panels, tags, stat blocks, bars — sharp rectangles.
|
||||
|
||||
## Components
|
||||
|
||||
- **registers** — the two-surface system. **slide-chrome** — optional hairline bars, suppressed on declarative frames.
|
||||
- **kicker** (mono eyebrow) / **rule** (36×2 stub) / **broadside-num** (catalogue mark) — the chrome ornament set.
|
||||
- **stat-card** (top-border only) / **bullet** (orange `/`, max 3) / **bar-track** (one accent bar) / **compare-panel** (orange payoff) / **fadelist** (1.0/0.5/0.22 stack).
|
||||
|
||||
## Frame Treatments
|
||||
|
||||
> Recipe: ground · register · composes · focal · chrome · accent · silence · Fixed/Free · density.
|
||||
> One statement per frame; chrome suppressed on declarative frames.
|
||||
|
||||
### 1 · Cover (identity · move: massive type · ORANGE register · left)
|
||||
|
||||
**Ground** fire-orange. **Composes** broadside-num, rule, kicker, display, lead. **Focal** a 1–2 word
|
||||
Barlow `display` (13cqw) lowercase in ink, left-anchored, over a small ink rule stub + mono kicker; a
|
||||
Barlow lead line beneath in 75% ink. **Chrome** mono catalogue number top-left, mono meta top-right
|
||||
(no chrome bars). **Accent** the ink itself is the pop on orange. **Silence** ~45%. **Fixed** ink-on-fire,
|
||||
lowercase 900, flat. **Free** the word, kicker, lead. **Density** low.
|
||||
|
||||
### 2 · Statement (declarative · move: type IS composition · DARK register · left)
|
||||
|
||||
**Ground** ink-black. **Composes** kicker, display. **Focal** a 2–4 word Barlow `display`/`h1`
|
||||
lowercase in cream, with ONE clause inked `{colors.fire-orange}`. **Chrome** mono kicker; no bars.
|
||||
**Accent** the orange clause. **Silence** ~55%. **Fixed** lowercase 900, one orange clause, flat.
|
||||
**Free** the statement, which clause is orange. **Density** low.
|
||||
|
||||
### 3 · Stat Grid (data · move: top-border cards · DARK · the dense frame)
|
||||
|
||||
**Ground** ink-black, chrome bars present. **Composes** slide-chrome, kicker, 3× stat-card. **Focal** a
|
||||
row of three top-border-only stat-cards — big Barlow-900 numeral in `{colors.fire-orange}`, Barlow
|
||||
label, mono note. **Chrome** top + bottom hairline bars (label + number). **Accent** the orange
|
||||
numerals. **Silence** moderate — the density exception. **Fixed** top-border-only cards, orange
|
||||
numerals, 1px hairlines. **Free** figures (from script), labels. **Density** dense-exception.
|
||||
|
||||
### 4 · Fadelist (narrative · move: opacity stack · DARK)
|
||||
|
||||
**Ground** ink-black. **Composes** fadelist (3 stacked Barlow-900 words at 1.0/0.5/0.22), fadelist-title.
|
||||
**Focal** the three-stage word stack opposite an oversized display title in `{colors.fire-orange}`
|
||||
(before/during/after). **Accent** the orange title. **Silence** moderate. **Fixed** the opacity
|
||||
ladder, lowercase 900. **Free** the three words, the title. **Density** low-moderate.
|
||||
|
||||
### 5 · Pull Quote (quote · move: oversized mark · DARK · left)
|
||||
|
||||
**Ground** ink-black, chrome suppressed. **Composes** quote-mark, quote-text, attribution. **Focal** a
|
||||
Barlow `quote-text` (700, lowercase) at ≤78cqw under an oversized fire-orange `quote-mark` (10cqw,
|
||||
line-height 0.6). **Chrome** mono attribution (name + role). **Accent** the orange quote mark. **Silence**
|
||||
~50%. **Fixed** orange mark, lowercase quote. **Free** quote, attribution. **Density** low.
|
||||
|
||||
### 6 · Compare (argument · move: split + orange payoff · DARK→ORANGE)
|
||||
|
||||
**Ground** ink-black left panel + fire-orange right (payoff) panel, 1px divider. **Composes**
|
||||
compare-panel pair, kicker, h3. **Focal** two panels — left documents (cream on dark), right declares
|
||||
(ink on orange). **Chrome** mono panel labels. **Accent** the orange payoff panel. **Silence** moderate.
|
||||
**Fixed** ink-on-fire right panel, 1px divider, flat. **Free** the before/after content. **Density** standard.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Set every Barlow display in **lowercase weight 900**, negative-tracked — the system's signature.
|
||||
- Use **fire-orange as full environment** on declarative frames, the **lone accent** on dark.
|
||||
- Keep chrome in **IBM Plex Mono uppercase, 0.14em**; use the `/` mono bullet marker.
|
||||
- **Cap bullets at three; one statement per frame**; build hierarchy from weight, size, 1px hairlines.
|
||||
- Suppress chrome bars on cover/chapter/statement/quote/end; let type fill the field.
|
||||
- Lean left on most frames; the type IS the composition.
|
||||
|
||||
### Don't
|
||||
|
||||
- Never uppercase Barlow display; never add a second accent color.
|
||||
- Never put cream text on orange (ink-on-fire is absolute); never a cream/paper register.
|
||||
- No drop shadow, no rounded surface (save nav dots), no gradient ground.
|
||||
- No serif companion; chrome is never Barlow.
|
||||
- Don't pack two display moments into one frame; don't blow a long line edge-to-edge — step down.
|
||||
|
||||
## Aspect-Ratio Behavior
|
||||
|
||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
||||
| ---------- | -------------------------- | -------------------------- | ---------------- |
|
||||
| Cover | word left, lead below | word top, lead below | centered word |
|
||||
| Statement | display left | display stacked taller | display centered |
|
||||
| Stat Grid | 3 across | 3 stacked | 2+1 |
|
||||
| Fadelist | stack + title side-by-side | stack over title | stack over title |
|
||||
| Pull Quote | mark + quote left | mark top, quote below | centered |
|
||||
| Compare | side-by-side panels | stacked (dark over orange) | stacked |
|
||||
|
||||
`pad-x` holds tight on the short edge; re-step display so the one big line stays ≤78cqw and above the
|
||||
1.4cqw floor. Mono chrome stays Latin/digit-only.
|
||||
|
||||
## Approved Entities
|
||||
|
||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
||||
placeholder (the dashed `img-placeholder` at 55cqh). The system supplies type and one color, not brands.
|
||||
|
||||
## Numerals & Claims (hard rule)
|
||||
|
||||
Never invent figures, percentages, dates, or counts at frame scale. Render slots as `— figure —`,
|
||||
`{metric}`, `NN%`. Stat-card numerals and bar heights carry placeholders until the script supplies
|
||||
them. Catalogue numbers (No. 01) are decorative chrome and may be sequential.
|
||||
|
||||
## Pre-Render Self-Audit
|
||||
|
||||
- **Squint** — exactly one display moment dominates; nothing competes.
|
||||
- **Silence** — declarative frames ~45–55% empty; only the stat grid runs dense.
|
||||
- **Register** — one register per frame; ink-on-fire on orange, cream on dark; no second hue.
|
||||
- **Type** — Barlow lowercase 900 negative-tracked, fit-to-measure; mono chrome uppercase 0.14em; ≥1.4cqw floor.
|
||||
- **Depth** — 0 shadow, 0 radius (save nav dots); 1px hairlines only.
|
||||
- **Bullets** — capped at three, orange `/` marker.
|
||||
- **Fabrication** — every numeral traces to the script, else placeholder.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Motion intentionally out of scope.** frame.md specifies composition only; the source's 0.8s deck slide + per-element entry animations are deck mechanics.
|
||||
- **Barlow + IBM Plex Mono via Google Fonts**; Noto Sans SC is the CJK fallback (the lowercase-display signal has no CJK equivalent — the two-register color system carries the identity, per the source).
|
||||
- **9:16 / 1:1 are guidance**; verify the one big line stays ≤78cqw and above the floor per ratio.
|
||||
- Bars, compare panels, and the dashed image placeholder are CSS-only; no external imagery is required.
|
||||
@@ -1,224 +0,0 @@
|
||||
<!--
|
||||
Broadside — caption skin (preset-local source).
|
||||
|
||||
This is the preset's own lower-third karaoke caption look: a caption skin to read or
|
||||
copy, not to mount as is. A copy used as compositions/captions.html fills its three
|
||||
reserved holes and wraps the fragment in a <template>:
|
||||
· the empty GROUPS array → the engine's scene-aware word groups
|
||||
· the 0 duration + 0 width/height → real total + canvas size
|
||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
||||
|
||||
Token contract — reference ONLY this fixed vocab (set it from the
|
||||
project's frame.md; each var has a broadside literal
|
||||
fallback for standalone preview):
|
||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
||||
--font-body · --cap-band-top · --cap-band-height
|
||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
||||
|
||||
Visual: a protest-poster lower third — an ink-black plate on a single 1px fire-orange
|
||||
hairline, SHARP corners, NO shadow (broadside's plane is flat, never cast). Massive
|
||||
lowercase Barlow 900 as graphic primitive, cream glyphs on ink. Upcoming words sit in
|
||||
faint cream; the current word flips register — a hard flat fire-orange block with ink
|
||||
reading on top (broadside's signature two-register flip, no blur), the brand's lone
|
||||
accent; spoken words settle to solid cream, the orange block cleared.
|
||||
-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<!-- the project's :root tokens go here -->
|
||||
<style data-brand-tokens></style>
|
||||
|
||||
<style>
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE ink-black plate — single 1px fire-orange hairline (broadside's lone accent),
|
||||
SHARP corners (0 radius), NO shadow (the plane is flat) */
|
||||
.caption-pill {
|
||||
max-width: 80%;
|
||||
padding: 18px 44px 22px;
|
||||
background: var(--cap-ink, #111111);
|
||||
border: 1px solid var(--cap-accent, #e85d26);
|
||||
border-radius: 0;
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.04em 0.3em;
|
||||
font-family: var(--font-display, "Barlow"), system-ui, sans-serif;
|
||||
font-weight: 900;
|
||||
font-size: clamp(38px, 4.2vw, 56px);
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.03em;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
|
||||
/* upcoming — faint cream, no block (massive lowercase Barlow 900 as graphic primitive) */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0 0.08em;
|
||||
color: color-mix(in srgb, var(--cap-canvas, #f0ece5) 42%, var(--cap-ink, #111111));
|
||||
}
|
||||
/* current word — THE register flip: a hard flat fire-orange block (box-shadow spread =
|
||||
a sharp rectangle w/o layout shift; no blur), ink reading on top — the lone accent */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-ink, #111111);
|
||||
background: var(--cap-accent, #e85d26);
|
||||
box-shadow: 0 0 0 0.06em var(--cap-accent, #e85d26);
|
||||
}
|
||||
/* already spoken — solid cream, the orange block cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-canvas, #f0ece5);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — fire-orange ink, the cream-on-ink register's only inflection */
|
||||
.caption-word.cap-brand {
|
||||
color: var(--cap-accent, #e85d26);
|
||||
}
|
||||
/* numerals / data — the IBM Plex Mono chrome voice, uppercase + tracked, recessed */
|
||||
.caption-word.cap-num {
|
||||
font-family: var(--font-body, "IBM Plex Mono"), ui-monospace, monospace;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: color-mix(in srgb, var(--cap-canvas, #f0ece5) 72%, var(--cap-ink, #111111));
|
||||
}
|
||||
/* emphasis — a fire-orange hairline underline (no second hue; the lone accent again) */
|
||||
.caption-word.cap-cta {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-color: var(--cap-accent-2, #e85d26);
|
||||
text-underline-offset: 0.1em;
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(32px, 5.2vw, 46px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="0"
|
||||
data-fps="30"
|
||||
data-width="0"
|
||||
data-height="0"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// the scene-aware word groups + real duration go here.
|
||||
var GROUPS = [];
|
||||
var DURATION = 0;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.92 }, { scale: 1, duration: 0.1, ease: "power3.out" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,284 +0,0 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Capsule — Frame (video / frame layer)
|
||||
description: >
|
||||
Video-first companion to Capsule's design.md. The unit is the frame (1920×1080). Atoms are
|
||||
identical and sacred — the pill geometry (9999px small / 2rem cards) with a 2px ink outline on
|
||||
everything, the sun-bleached cream canvas, the nine-color candy palette, Bodoni Moda + Space
|
||||
Grotesk, soft hard-offset shadows (4/6/8/12px in 8% ink), floating decorative-pill wallpaper,
|
||||
radial accent glows, and the grain overlay. Composition + frame scale rewritten. Motion out of scope.
|
||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
||||
principle: atoms are sacred · composition is free · numbers come from the script
|
||||
|
||||
colors:
|
||||
cream: "#F5F5F0"
|
||||
ink: "#1A1A1A"
|
||||
outline: "#1E1E1E"
|
||||
white: "#FFFFFF"
|
||||
coral: "#E85D4E"
|
||||
lime: "#C4D94E"
|
||||
lavender: "#C5B5E0"
|
||||
sky: "#8BB4F7"
|
||||
violet: "#A06CE8"
|
||||
yellow: "#F2D160"
|
||||
peach: "#F5B895"
|
||||
mint: "#A8E6CF"
|
||||
shadow: "rgba(26,26,26,0.08)"
|
||||
|
||||
typography:
|
||||
# — reading ramp (Space Grotesk) —
|
||||
body: { fontFamily: "Space Grotesk", cqw: 0.85, weight: 400, lineHeight: 1.6 }
|
||||
subtitle: { fontFamily: "Space Grotesk", cqw: 0.95, weight: 400, tracking: "0.18em", upper: true }
|
||||
pill-text: { fontFamily: "Space Grotesk", cqw: 0.75, weight: 600, tracking: "0.12em", upper: true }
|
||||
label: { fontFamily: "Space Grotesk", px: 14, weight: 500, tracking: "0.1em", upper: true }
|
||||
# — display / hero ramp (Bodoni Moda, ink, sentence case) —
|
||||
card-headline:{ fontFamily: "Bodoni Moda", cqw: 1.9, weight: 700, lineHeight: 1.1 }
|
||||
orbit-numeral:{ fontFamily: "Bodoni Moda", cqw: 2.6, weight: 700, lineHeight: 1.0 }
|
||||
quote-display:{ fontFamily: "Bodoni Moda", cqw: 4.2, weight: 600, lineHeight: 1.3, tracking: "-0.01em" }
|
||||
stat-number: { fontFamily: "Bodoni Moda", cqw: 3.6, weight: 800, lineHeight: 1.0, tracking: "-0.03em" }
|
||||
section-headline:{ fontFamily: "Bodoni Moda", cqw: 4.0, weight: 700, lineHeight: 1.05, tracking: "-0.01em" }
|
||||
headline: { fontFamily: "Bodoni Moda", cqw: 5.0, weight: 700, lineHeight: 1.05, tracking: "-0.02em" }
|
||||
closing-display:{ fontFamily: "Bodoni Moda", cqw: 8.5, weight: 800, lineHeight: 0.95, tracking: "-0.03em" }
|
||||
display: { fontFamily: "Bodoni Moda", cqw: 12.0, weight: 800, lineHeight: 0.88, tracking: "-0.03em" }
|
||||
|
||||
spacing:
|
||||
pad: "5cqw"
|
||||
gap-md: "2cqw"
|
||||
card-pad: "2cqw 1.6cqw"
|
||||
|
||||
components:
|
||||
pill:
|
||||
rounded: "9999px (small) / 2rem (cards)"
|
||||
border: "0.2cqw solid {colors.outline}"
|
||||
backgroundColor: "any candy or {colors.white}"
|
||||
description: "The universal container — every chip/button/label/stat tile/node/bar. No unstroked pill exists."
|
||||
pill-card:
|
||||
backgroundColor: "{colors.white}"
|
||||
border: "0.2cqw solid {colors.outline}"
|
||||
rounded: "2rem"
|
||||
shadow: "0.4cqw 0.4cqw 0 {colors.shadow}"
|
||||
typography: "{typography.card-headline} + {typography.body}"
|
||||
description: "Holds a circular card-icon, Bodoni headline, Space Grotesk body."
|
||||
stat-pill:
|
||||
backgroundColor: "{colors.white}"
|
||||
border: "0.2cqw solid {colors.outline}"
|
||||
rounded: "2rem"
|
||||
shadow: "0.3cqw 0.3cqw 0 {colors.shadow}"
|
||||
typography: "{typography.stat-number} (COLORED) + {typography.pill-text}"
|
||||
description: "The one place color touches a numeral; + a 40px accent bar."
|
||||
title-pill:
|
||||
backgroundColor: "{colors.yellow}"
|
||||
border: "0.2cqw solid {colors.outline}"
|
||||
rounded: "9999px"
|
||||
shadow: "0.3cqw 0.3cqw 0 {colors.shadow}"
|
||||
typography: "{typography.pill-text}"
|
||||
description: "Sits above the display headline on cover/closing."
|
||||
card-icon:
|
||||
backgroundColor: "any candy"
|
||||
border: "0.2cqw solid {colors.outline}"
|
||||
rounded: "50%"
|
||||
size: "60px"
|
||||
typography: "Bodoni numeral/letter in {colors.ink}"
|
||||
description: "Card mark."
|
||||
floating-pill:
|
||||
backgroundColor: "any candy"
|
||||
border: "0.2cqw solid {colors.outline}"
|
||||
rounded: "9999px / 50%"
|
||||
transform: "rotate(−20°..+25°)"
|
||||
shadow: "none"
|
||||
typography: "{typography.pill-text}"
|
||||
description: "Decorative wallpaper confetti, 5–8 per declarative frame. Flat — no shadow."
|
||||
quote-highlight:
|
||||
backgroundColor: "{colors.lime} / {colors.sky}"
|
||||
border: "0.2cqw solid {colors.outline}"
|
||||
rounded: "9999px"
|
||||
description: "Inline candy pill wrapping a phrase inside a Bodoni quote — the emphasis mechanism, replacing bold/italic."
|
||||
bar-track:
|
||||
backgroundColor: "{colors.cream}"
|
||||
border: "0.2cqw solid {colors.outline}"
|
||||
rounded: "9999px"
|
||||
fill: "child candy pill, value at right edge"
|
||||
description: "36px pill chart track."
|
||||
accent-line:
|
||||
backgroundColor: "{colors.coral}"
|
||||
rounded: "9999px"
|
||||
size: "60×4 (80×4 closing)"
|
||||
description: "Coral pill rule."
|
||||
atmosphere:
|
||||
background: "1–3 radial candy glows (6–15% opacity) over {colors.cream} + 4% grain overlay"
|
||||
description: "Baseline canvas layers on every frame. Never absent."
|
||||
---
|
||||
|
||||
# Capsule — Frame (video / frame layer)
|
||||
|
||||
## Overview
|
||||
|
||||
Capsule at frame scale is a **playful editorial system where every container is a pill.** The
|
||||
`border-radius: 9999px` (small) / `2rem` (cards) rule plus a 2px ink outline wraps every chip,
|
||||
card, icon, bar, and node — inflated, friendly, graphically distinct. The canvas is sun-bleached
|
||||
cream warmed by soft radial candy glows and a permanent 4% grain overlay.
|
||||
|
||||
The voice is a two-face conversation: **Bodoni Moda** (didone serif, weight 700–800, always ink,
|
||||
always sentence case) carries every headline, stat, and quote; **Space Grotesk** carries body and
|
||||
all uppercase tracked pill/label text. Nine candy accents fill pills interchangeably with no
|
||||
semantic meaning. Depth is a **soft hard-offset shadow** (4/6/8/12px in 8% ink) — lifted, not
|
||||
stamped — reserved for content-bearing containers; decorative floating pills are flat.
|
||||
|
||||
**Key characteristics at frame scale:**
|
||||
|
||||
- **Pill geometry everywhere** — 9999px small, 2rem cards — each wrapped in a 2px `{colors.outline}` stroke.
|
||||
- **Bodoni Moda** display (ink, sentence case) + **Space Grotesk** body/pills (uppercase, tracked).
|
||||
- **Nine candy accents**, interchangeable; color touches stat numerals + pill fills, never headlines.
|
||||
- **Soft offset shadows** (4/6/8/12px, 8% ink) on content containers only; floating pills are flat.
|
||||
- **Floating decorative-pill wallpaper** (5–8, tilted) + radial glows + 4% grain on declarative frames.
|
||||
- **Cream canvas** warmed by atmosphere — bare corners read as broken.
|
||||
|
||||
## The Frame
|
||||
|
||||
### Frame Craft Bar
|
||||
|
||||
Three eyeball tests gate every frame before any structural check:
|
||||
|
||||
- **Squint** — one Bodoni headline or stat dominates at **3–6× its nearest neighbor**.
|
||||
- **Silence** — declarative frames carry **atmosphere, not clutter** (pills as wallpaper, not content); the **pillar-cards and stat grid are the dense exception**.
|
||||
- **Restraint** — every pill carries the 2px outline; color touches **fills + stat numerals only** (never a headline); soft shadow on content-bearing pills only (floating pills are flat); no tenth accent.
|
||||
- **Reference** — aim at a **Memphis / ice-cream-parlor editorial spread**; failure looks like a **flat, sticker-less SaaS card grid**.
|
||||
|
||||
- **Primary:** 1920×1080 (16:9). Display authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`).
|
||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
||||
- **Safe area:** `pad` (5cqw); floating pills may bleed off edges as wallpaper.
|
||||
|
||||
**The container law (load-bearing).** Every frame ground sets `container-type: size` AND carries
|
||||
the radial-glow + grain atmosphere; ALL frame-relative units are `cqw`/`cqh` against it — never
|
||||
`vw`. Pill radii stay `9999px`/`2rem` (shape, not scale); shadow offsets scale in `cqw`.
|
||||
|
||||
## Colors
|
||||
|
||||
Tokens identical to the source. `{colors.cream}` is the ground; `{colors.ink}`/`{colors.outline}`
|
||||
is type + the universal 2px stroke; the nine candy accents fill pills with **no semantic mapping**
|
||||
— pair a warm (coral/yellow/peach) with a cool (sky/lavender/violet/mint) with a neutral-bright
|
||||
(lime); never two same-family adjacent. Color appears on **stat numerals and pill fills only** —
|
||||
never on a Bodoni headline. Shadow is always `{colors.shadow}` (8% ink). No tenth color.
|
||||
|
||||
## Typography
|
||||
|
||||
Two ramps. The **reading ramp** (Space Grotesk body 0.85cqw, uppercase tracked pill/label text)
|
||||
carries copy + chips; the **display ramp** (Bodoni `card-headline` 1.9cqw → `display` 12cqw, all
|
||||
700–800) carries every headline, stat, and quote.
|
||||
|
||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; px labels are chrome only.
|
||||
- **Fit-to-measure:** size the headline to its length. Cap the block at **≤ 78cqw**; ≤3 words → `display`/`closing-display`; 4–6 → `headline`; 7+ → `section-headline`.
|
||||
- **Bodoni 700+ in ink, sentence case** (italic via `<em>` only, color allowed on the italic key word as a candy hue); **Space Grotesk pills/labels uppercase, 0.08em+ tracked**; negative-track Bodoni display.
|
||||
|
||||
## Depth & Surface
|
||||
|
||||
Soft hard-offset shadow is the only depth, in `{colors.shadow}` (8% ink), bottom-right:
|
||||
|
||||
- **0.2cqw (4px)** small nodes; **0.3cqw (6px)** stat-pills, orbit pills, diagram nodes; **0.4cqw (8px)** pillar-cards; **0.6cqw (12px)** the visual frame.
|
||||
- The **2px outline** does most of the lift against cream; the shadow adds the float.
|
||||
- **Decorative floating pills cast no shadow** — this separates content from atmosphere at a glance.
|
||||
|
||||
**Ceiling:** no blurred shadow, no re-colored shadow, no gradient depth.
|
||||
|
||||
## Shapes
|
||||
|
||||
- **9999px** — all small pills (chips, buttons, bars, nodes, floating pills, quote highlights, accent lines).
|
||||
- **2rem** — larger cards (pillar-card, stat-pill, chart container, visual frame).
|
||||
- **50%** — circular pills (card-icon 60px, step-node 56px, orbit-center 160px, nav dots).
|
||||
- **0** — only the grain overlay and the gradient region inside a visual frame. No sharp-cornered text container exists.
|
||||
|
||||
## Components
|
||||
|
||||
- **pill** — the universal 2px-outlined container. **pill-card / stat-pill / title-pill** — the white/yellow content pills with soft shadows.
|
||||
- **card-icon** — circular candy mark; **floating-pill** — flat tilted wallpaper confetti; **quote-highlight** — inline candy emphasis pill.
|
||||
- **bar-track** — pill-shaped chart bar; **accent-line** — coral pill rule; **atmosphere** — the baseline glow + grain on every frame.
|
||||
|
||||
## Frame Treatments
|
||||
|
||||
> Recipe: ground · container · composes · focal · chrome · accent · silence · Fixed/Free · density.
|
||||
> Atmosphere (glow + grain) on every frame; declarative frames carry floating-pill wallpaper.
|
||||
|
||||
### 1 · Cover (identity · move: title pill + display · centered)
|
||||
|
||||
**Ground** cream + atmosphere, floating-pill wallpaper. **Composes** title-pill, display, accent-line, floating-pills (5–8). **Focal** a 1–2 line Bodoni `display` headline in ink (italic key word in a candy hue), centered, under a yellow title-pill; a coral accent-line below. **Chrome** uppercase Space Grotesk sub. **Accent** the italic word + candy floating pills. **Silence** content centered; pills fill the edges. **Fixed** pill outlines, Bodoni ink, flat floating pills. **Free** title, which candy hues, pill words/positions. **Density** medium-atmospheric.
|
||||
|
||||
### 2 · Pillar Cards (catalog · move: 3-up grid · left — the dense frame)
|
||||
|
||||
**Ground** cream + atmosphere. **Composes** title-pill (lavender), section-headline, 3× pill-card. **Focal** three white 2rem pill-cards (circular candy card-icon, Bodoni card-headline, Space Grotesk body) with 0.4cqw shadows, under a Bodoni section-headline. **Chrome** a lavender header tag-pill. **Accent** the three card-icon fills (coral/sky/lime sequence). **Silence** tight — the density exception (no floating pills here). **Fixed** 2px outlines, soft shadows, ink headlines. **Free** card content, icon hues. **Density** dense-exception.
|
||||
|
||||
### 3 · Stat Grid (data · move: stat pills · centered head)
|
||||
|
||||
**Ground** cream + atmosphere. **Composes** section-headline, 3–4× stat-pill. **Focal** a row of white stat-pills, each a COLORED Bodoni stat-number + uppercase label + accent bar. **Accent** the colored numerals + bars (the one place color meets type). **Silence** moderate. **Fixed** 2rem pills, colored numerals only, soft shadow. **Free** figures (from script), hues. **Density** standard.
|
||||
|
||||
### 4 · Pull Quote (quote · move: highlight pill · left)
|
||||
|
||||
**Ground** cream + atmosphere, a few floating pills. **Composes** quote-display, quote-highlight, accent-line. **Focal** a Bodoni quote in ink with one phrase wrapped in a lime/sky `quote-highlight` pill (the emphasis mechanism — never bold). **Chrome** uppercase attribution. **Accent** the highlight pill. **Silence** ~50%. **Fixed** Bodoni 600, highlight-pill emphasis. **Free** quote, highlight color/phrase. **Density** sparse.
|
||||
|
||||
### 5 · Orbit (concept · move: gravitational pills · centered)
|
||||
|
||||
**Ground** cream + atmosphere. **Composes** orbit-center (160px lime circle, Bodoni ordinal), 4–6 orbit-pill satellites (candy, tilted, soft shadow). **Focal** the lime center anchored by orbiting candy pills. **Accent** the satellite hues. **Silence** moderate. **Fixed** circular center, outlined satellites with shadow. **Free** ordinal, satellite words/positions. **Density** medium.
|
||||
|
||||
### 6 · Closing Plate (closer · move: title pill + display · centered)
|
||||
|
||||
**Ground** cream + atmosphere, floating-pill wallpaper. **Composes** title-pill (yellow), closing-display, accent-line, floating-pills. **Focal** a Bodoni `closing-display` sign-off in ink (italic word in violet/candy), centered. **Accent** the italic word + pills. **Silence** content centered. **Fixed** Bodoni ink, flat pills. **Free** sign-off, pill words. **Density** medium-atmospheric.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Make every text container a **pill** (9999px / 2rem) with the **2px ink outline**.
|
||||
- Set **Bodoni headlines in ink, sentence case**; color lives on stat numerals + pill fills.
|
||||
- Use **soft offset shadows** (4/6/8/12px, 8% ink) on content containers; keep floating pills flat.
|
||||
- Float **5–8 candy pills** + radial glows + grain on declarative frames; bare corners read as broken.
|
||||
- Wrap inline emphasis in a **candy quote-highlight pill**, never bold/italic-alone.
|
||||
- Pair warm+cool+neutral-bright accents; lean centered on cover/closer, left on cards/quote.
|
||||
|
||||
### Don't
|
||||
|
||||
- No sharp-cornered text container; no unstroked pill.
|
||||
- No colored Bodoni headline; no uppercase Bodoni.
|
||||
- No blurred or re-colored shadow; no tenth accent color.
|
||||
- No shadow on decorative floating pills.
|
||||
- Don't blow a headline edge-to-edge — fit to measure.
|
||||
|
||||
## Aspect-Ratio Behavior
|
||||
|
||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
||||
| ------------ | ------------------------------ | --------------------------- | --------------------- |
|
||||
| Cover | display centered, pills around | display top, pills band | centered, fewer pills |
|
||||
| Pillar Cards | head + 3-up | head + 3 stacked | head + 2+1 |
|
||||
| Stat Grid | 3–4 across | 2×2 | 2×2 |
|
||||
| Pull Quote | quote left | quote top, highlight inline | centered |
|
||||
| Orbit | center + 4–6 satellites | center + 3 satellites | center + 4 |
|
||||
| Closing | centered + pills | centered, pills band | centered |
|
||||
|
||||
`pad` holds on the short edge; re-step display above the 1.4cqw floor. Floating-pill count drops
|
||||
on tighter ratios so they stay wallpaper, not clutter.
|
||||
|
||||
## Approved Entities
|
||||
|
||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
||||
placeholder. Floating-pill words are neutral atmospheres ("VISION", "FUTURE", "NEXT"), never content-specific.
|
||||
|
||||
## Numerals & Claims (hard rule)
|
||||
|
||||
Never invent figures, stats, or counts at frame scale. Render slots as `— figure —`, `{metric}`,
|
||||
`N%`. Stat-pill numbers and bar-track widths carry placeholders until the script supplies values.
|
||||
|
||||
## Pre-Render Self-Audit
|
||||
|
||||
- **Squint** — one Bodoni element dominates at 3–5× its neighbor.
|
||||
- **Silence** — declarative frames carry atmosphere not clutter; only pillar/stat grids run dense.
|
||||
- **Pills** — every container is a pill with a 2px outline; no sharp text container.
|
||||
- **Color** — candy on fills + stat numerals only; headlines ink; no tenth hue.
|
||||
- **Depth** — soft offset shadow on content only; floating pills flat; no blur.
|
||||
- **Type** — Bodoni ink sentence-case, fit-to-measure; pills uppercase tracked; ≥1.4cqw floor.
|
||||
- **Anchor** — centered on cover/closer/orbit, left on cards/quote; no 3 consecutive alike.
|
||||
- **Fabrication** — every numeral traces to the script, else placeholder.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Motion intentionally out of scope.** frame.md specifies composition only; the 0.6s fade in the source is a deck mechanic.
|
||||
- **Bodoni Moda + Space Grotesk via Google Fonts.** CJK pairing (ZCOOL XiaoWei / Yozai) carries over from the source.
|
||||
- **9:16 / 1:1 are guidance**; verify the floor and that floating-pill count scales down.
|
||||
- The grain overlay, radial glows, floating pills, and bar fills are CSS-only; no external imagery is required.
|
||||
@@ -1,223 +0,0 @@
|
||||
<!--
|
||||
Capsule — caption skin (preset-local source).
|
||||
|
||||
This is the preset's own lower-third karaoke caption look: a caption skin to read or
|
||||
copy, not to mount as is. A copy used as compositions/captions.html fills its three
|
||||
reserved holes and wraps the fragment in a <template>:
|
||||
· the empty GROUPS array → the engine's scene-aware word groups
|
||||
· the 0 duration + 0 width/height → real total + canvas size
|
||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
||||
|
||||
Token contract — reference ONLY this fixed vocab (set it from the
|
||||
project's frame.md; each var has a capsule literal
|
||||
fallback for standalone preview):
|
||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
||||
--font-body · --cap-band-top · --cap-band-height
|
||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
||||
|
||||
Visual: a cream caption pill — fully-rounded (9999px), wrapped in capsule's universal
|
||||
2px ink outline, lifted on a soft hard-offset shadow (no blur), Bodoni Moda glyphs in
|
||||
ink, sentence case. Upcoming words sit in faint ink; the current word lights up as a
|
||||
rounded candy highlight — a candy fill plus a box-shadow spread halo that follows the
|
||||
pill radius (a soft candy capsule around the word, no blur, NO layout shift) with ink
|
||||
reading on top; spoken words settle to solid ink, the candy highlight cleared.
|
||||
-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<!-- the project's :root tokens go here -->
|
||||
<style data-brand-tokens></style>
|
||||
|
||||
<style>
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE caption capsule — cream pill, fully-rounded (9999px), capsule's universal 2px
|
||||
ink outline, lifted on a soft hard-offset shadow (no blur, 8% ink) */
|
||||
.caption-pill {
|
||||
max-width: 80%;
|
||||
padding: 18px 46px 20px;
|
||||
background: var(--cap-canvas, #f5f5f0);
|
||||
border: 2px solid var(--cap-ink, #1a1a1a);
|
||||
border-radius: 9999px;
|
||||
box-shadow: 8px 8px 0 color-mix(in srgb, var(--cap-ink, #1a1a1a) 8%, transparent);
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.12em 0.36em;
|
||||
font-family: var(--font-display, "Bodoni Moda"), Georgia, serif;
|
||||
font-weight: 700;
|
||||
font-size: clamp(38px, 4.2vw, 56px);
|
||||
line-height: 1.16;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* upcoming — faint ink, no highlight */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0 0.1em;
|
||||
border-radius: 9999px;
|
||||
color: color-mix(in srgb, var(--cap-ink, #1a1a1a) 42%, var(--cap-canvas, #f5f5f0));
|
||||
}
|
||||
/* current word — THE rounded candy highlight: a candy fill + a box-shadow spread
|
||||
halo that follows the pill radius (a soft candy capsule around the word, no blur,
|
||||
no layout shift), ink reading on top */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-ink, #1a1a1a);
|
||||
background: var(--cap-accent, #e85d4e);
|
||||
box-shadow: 0 0 0 0.14em var(--cap-accent, #e85d4e);
|
||||
}
|
||||
/* already spoken — solid ink, candy highlight cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-ink, #1a1a1a);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — Bodoni Moda italic, capsule's display inflection */
|
||||
.caption-word.cap-brand {
|
||||
font-style: italic;
|
||||
}
|
||||
/* numerals / data — the Space Grotesk voice, slightly recessed */
|
||||
.caption-word.cap-num {
|
||||
font-family: var(--font-body, "Space Grotesk"), ui-sans-serif, sans-serif;
|
||||
color: color-mix(in srgb, var(--cap-ink, #1a1a1a) 72%, var(--cap-canvas, #f5f5f0));
|
||||
}
|
||||
/* emphasis — a second-candy rounded underline (the counter-accent) */
|
||||
.caption-word.cap-cta {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 3px;
|
||||
text-decoration-color: var(--cap-accent-2, #f2d160);
|
||||
text-underline-offset: 0.12em;
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(34px, 5.4vw, 48px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="0"
|
||||
data-fps="30"
|
||||
data-width="0"
|
||||
data-height="0"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// the scene-aware word groups + real duration go here.
|
||||
var GROUPS = [];
|
||||
var DURATION = 0;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.92 }, { scale: 1, duration: 0.18, ease: "back.out(2)" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,278 +0,0 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Cartesian — Frame (video / frame layer)
|
||||
description: >
|
||||
Video-first companion to Cartesian's design.md. The unit is the frame (1920×1080). Atoms are
|
||||
identical and sacred — the five-tone warm-stone palette, Playfair Display 400 + Inter, the
|
||||
universal 1px taupe hairline as the only structural device, compass-drafted geometric rings,
|
||||
and zero shadow / zero fill. Composition, frame scale, and aspect-ratio behavior are rewritten
|
||||
for the frame. Restraint is the rule; motion is out of scope.
|
||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
||||
principle: atoms are sacred · composition is free · numbers come from the script
|
||||
|
||||
colors:
|
||||
bg-primary: "#EDE8E0"
|
||||
bg-secondary: "#E2DBD1"
|
||||
text-primary: "#1A1A1A"
|
||||
text-secondary: "#5A5A5A"
|
||||
accent: "#8A8178"
|
||||
line: "#B8B0A4"
|
||||
white-overlay: "rgba(255,255,255,0.3)"
|
||||
|
||||
typography:
|
||||
# — reading ramp (Inter) —
|
||||
body: { fontFamily: "Inter", cqw: 1.0, weight: 400, lineHeight: 1.6, color: "text-secondary" }
|
||||
body-sm: { fontFamily: "Inter", cqw: 0.85, weight: 400, lineHeight: 1.6 }
|
||||
subtitle: { fontFamily: "Inter", cqw: 1.3, weight: 400, lineHeight: 1.5 }
|
||||
label: { fontFamily: "Inter", px: 14, weight: 500, tracking: "3px", upper: true, color: "accent" }
|
||||
attribution: { fontFamily: "Inter", px: 15, weight: 400, tracking: "2px", upper: true, color: "accent" }
|
||||
micro: { fontFamily: "Inter", px: 12, weight: 400, tracking: "2px", upper: true, color: "accent" }
|
||||
# — display / hero ramp (Playfair Display 400, sentence case) —
|
||||
h3: { fontFamily: "Playfair Display", cqw: 1.8, weight: 400, lineHeight: 1.1 }
|
||||
timeline-headline:{ fontFamily: "Playfair Display", cqw: 1.9, weight: 400, lineHeight: 1.1 }
|
||||
card-headline:{ fontFamily: "Playfair Display", cqw: 2.0, weight: 400, lineHeight: 1.15 }
|
||||
stat-figure: { fontFamily: "Playfair Display", cqw: 3.0, weight: 400, lineHeight: 1.0 }
|
||||
quote-mark: { fontFamily: "Playfair Display", cqw: 9.0, weight: 400, lineHeight: 0.5, color: "line" }
|
||||
h2: { fontFamily: "Playfair Display", cqw: 4.0, weight: 400, lineHeight: 1.1 }
|
||||
h1: { fontFamily: "Playfair Display", cqw: 6.2, weight: 400, lineHeight: 1.06 }
|
||||
display: { fontFamily: "Playfair Display", cqw: 8.0, weight: 400, lineHeight: 1.04 }
|
||||
|
||||
spacing:
|
||||
pad-x: "7cqw"
|
||||
pad-y: "5cqw"
|
||||
gap-xl: "6cqw"
|
||||
gap-lg: "5cqw"
|
||||
|
||||
components:
|
||||
hairline:
|
||||
rule: "0.07cqw solid {colors.line}"
|
||||
description: "The universal structural device — every separator (agenda rule, timeline connector, card border, stats top) is this 1px taupe line. No thick borders exist."
|
||||
card:
|
||||
backgroundColor: "{colors.white-overlay}"
|
||||
border: "0.07cqw solid {colors.line}"
|
||||
rounded: "0"
|
||||
shadow: "none"
|
||||
description: "Faint white-overlay fill (canvas bleeds through) is what makes a card distinct from a bare region."
|
||||
card-icon:
|
||||
border: "0.07cqw solid {colors.line}"
|
||||
rounded: "50%"
|
||||
size: "40px circle"
|
||||
textColor: "{colors.accent}"
|
||||
typography: "Playfair numeral/letter"
|
||||
description: "Ringed circle mark."
|
||||
agenda-row:
|
||||
borderBottom: "0.07cqw solid {colors.line}"
|
||||
typography: "{typography.h3} (numeral {colors.accent}, label {colors.text-primary})"
|
||||
description: "Numeral left, label right over a taupe rule."
|
||||
timeline:
|
||||
borderTop: "0.07cqw solid {colors.line}"
|
||||
typography: "{typography.timeline-headline} + {typography.body-sm}"
|
||||
description: "A single taupe top rule across items — no nodes, no dots."
|
||||
stats-cluster:
|
||||
borderTop: "0.07cqw solid {colors.line}"
|
||||
typography: "{typography.stat-figure} + uppercase {colors.accent} labels"
|
||||
description: "Inline modest stat figures (no hero numeral)."
|
||||
geo-ring:
|
||||
border: "1px solid {colors.line} (inner dashed ::before ~70–80%)"
|
||||
rounded: "50%"
|
||||
size: "10–50cqw"
|
||||
opacity: "0.2–0.5"
|
||||
description: "Compass-construction rings behind content. 1–2 per frame, never more."
|
||||
horizontal-accent:
|
||||
backgroundColor: "{colors.text-primary}"
|
||||
size: "~18cqw × 1px"
|
||||
description: "The system's only INK-BLACK rule — a strong terminal accent on cover/closing, sparingly."
|
||||
vertical-line:
|
||||
backgroundColor: "{colors.line}"
|
||||
size: "1px × full height, ~5cqw from edge"
|
||||
opacity: "0.3–0.4"
|
||||
description: "Drafting-paper alignment guide. Optional."
|
||||
image-placeholder:
|
||||
backgroundColor: "{colors.bg-secondary}"
|
||||
mark: "crossed +30°/−30° 1px taupe diagonals (an X)"
|
||||
typography: "small uppercase {typography.micro}"
|
||||
description: "The signature image-not-wired mark."
|
||||
team-photo:
|
||||
backgroundColor: "{colors.bg-secondary}"
|
||||
border: "0.07cqw solid {colors.line}"
|
||||
rounded: "50%"
|
||||
typography: "Playfair initial in {colors.accent}"
|
||||
description: "Circular portrait frame."
|
||||
---
|
||||
|
||||
# Cartesian — Frame (video / frame layer)
|
||||
|
||||
## Overview
|
||||
|
||||
Cartesian at frame scale is a **quiet museum-catalog editorial system** — restraint through 1px
|
||||
lines. Every structural separator is a single 1px taupe hairline; there are no thick borders, no
|
||||
fills (save the faint white-overlay card), no shadows, no rounded rectangles. Hierarchy comes from
|
||||
**type contrast and negative space**, and atmosphere from **compass-drafted geometric rings**
|
||||
drifting behind content.
|
||||
|
||||
The voice is a literary pairing: **Playfair Display** at weight 400 (the thin-stroke didone, never
|
||||
bold, always sentence case) carries every headline, numeral, and quote mark in ink; **Inter**
|
||||
carries body in warm gray and labels in uppercase taupe with 2–3px tracking. The palette is five
|
||||
warm stones plus ink — no populist accent color exists. The correct density is **sparse and
|
||||
breathing**: one clear idea, well-framed, on stone paper.
|
||||
|
||||
**Key characteristics at frame scale:**
|
||||
|
||||
- **1px taupe hairline** as the universal structural device — every separator is this one line.
|
||||
- **Playfair Display 400** (ink, sentence case) for display; **Inter** body (gray) + labels (taupe, tracked).
|
||||
- **Five warm stones + ink** — no red/blue/green; the only "color" is type contrast.
|
||||
- **Compass-drafted geometric rings** (solid + dashed, 20–50% opacity) behind content for mood.
|
||||
- **Flat** — zero shadow, zero rounded rectangle (circles only); the lone ink line is the `horizontal-accent`.
|
||||
- **Sparse and breathing** — generous negative space; crowding reads as broken.
|
||||
|
||||
## The Frame
|
||||
|
||||
### Frame Craft Bar
|
||||
|
||||
Three eyeball tests gate every frame before any structural check:
|
||||
|
||||
- **Squint** — one Playfair element dominates at **3–6× its nearest neighbor**; the serif/sans + size contrast carries hierarchy, not weight.
|
||||
- **Silence** — declarative frames read **55–60% empty**; Cartesian has **no dense frame** — even the agenda/index breathes (the system breaks when crowded).
|
||||
- **Restraint** — **at most two geo rings** per frame; the single INK-BLACK `horizontal-accent` rule used sparingly; no populist accent color ever.
|
||||
- **Reference** — aim at a **Vignelli editorial / Cooper Hewitt catalogue / pencil-and-tracing-paper plan**; failure looks like a **shadowed, rounded-card SaaS deck**.
|
||||
|
||||
- **Primary:** 1920×1080 (16:9). Display authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`).
|
||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
||||
- **Safe area:** `pad-x` (7cqw) generous gutters; geometry may bleed off an edge.
|
||||
|
||||
**The container law (load-bearing).** Every frame ground sets `container-type: size`; ALL
|
||||
frame-relative units are `cqw`/`cqh` against it — never `vw`. The 1px hairlines and geo rings hold
|
||||
their proportion against the frame at any render size.
|
||||
|
||||
## Colors
|
||||
|
||||
Tokens identical to the source. `{colors.bg-primary}` is the ground; `{colors.text-primary}` ink is
|
||||
headlines and the one black accent rule; `{colors.text-secondary}` gray is body; `{colors.accent}`
|
||||
taupe is labels, numerals, small text; `{colors.line}` taupe is every 1px structural border.
|
||||
`{colors.bg-secondary}` is the only secondary fill (placeholders, photo frames). **No populist
|
||||
accent** — when emphasis is needed, grow the type, switch sans→serif, or add a single
|
||||
`horizontal-accent` ink line. Headlines are never taupe; small text is never ink.
|
||||
|
||||
## Typography
|
||||
|
||||
Two ramps. The **reading ramp** (Inter body 1.0cqw gray, labels in px taupe) carries copy + chrome;
|
||||
the **display ramp** (Playfair `h3` 1.8cqw → `display` 8.0cqw, all weight 400) carries every headline.
|
||||
|
||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; px labels are chrome only.
|
||||
- **Fit-to-measure:** size the headline to its length. Cap the block at **≤ 78cqw**; ≤3 words → `display`/`h1`; 4–6 → `h2`; 7+ → `h3`. Cartesian has no hero-stat numeral — stats stay modest (`stat-figure` 3cqw).
|
||||
- **Playfair at 400, ink, sentence case** — never bold, never uppercase, never taupe. **Inter labels uppercase, 2–3px tracked, taupe.** Italic via Playfair italic for emphasis only.
|
||||
|
||||
## Depth & Surface
|
||||
|
||||
The flat plane is the only technique. Hierarchy from:
|
||||
|
||||
- **Type contrast** — Playfair serif vs Inter sans; the 8cqw→0.7cqw scale.
|
||||
- **1px taupe hairlines** — every divider, card outline, timeline rule, photo ring.
|
||||
- **Tone** — ink vs gray vs taupe.
|
||||
- **Negative space** — generous padding.
|
||||
- **Geometric atmosphere** — compass rings that suggest depth without creating it.
|
||||
|
||||
**Ceiling:** no box-shadow, no elevated card, no gradient, no rounded rectangle. The single ink line
|
||||
(`horizontal-accent`) is the only non-taupe rule.
|
||||
|
||||
## Shapes
|
||||
|
||||
- **50% (circle)** — card-icon, team-photo, nav-dot, every geo ring.
|
||||
- **0** — everything else; soft-rounded corners do not exist.
|
||||
|
||||
## Components
|
||||
|
||||
- **hairline** — the universal 1px taupe separator (the identity).
|
||||
- **card** (1px taupe + white-overlay) / **card-icon** (ringed circle) / **agenda-row** / **timeline** (line, no nodes) / **stats-cluster** — all built on the hairline.
|
||||
- **geo-ring** — compass decoration (solid + dashed), 1–2 per frame. **horizontal-accent** — the one ink line, sparingly. **vertical-line** — drafting guide.
|
||||
- **image-placeholder** (crossed-X) / **team-photo** (ringed initial) — the stone-fill placeholders.
|
||||
|
||||
## Frame Treatments
|
||||
|
||||
> Recipe: ground · container · composes · focal · chrome · accent · silence · Fixed/Free · density.
|
||||
> Every frame is sparse and breathing; 1–2 geo decorations max.
|
||||
|
||||
### 1 · Cover (identity · move: serif + compass ring · left)
|
||||
|
||||
**Ground** `{colors.bg-primary}`, `pad-x`. **Composes** geo-ring (right, ~34cqw, solid+dashed), vertical-line (left), label, display/h1. **Focal** a 2–3 line Playfair `display`/`h1` headline in ink (italic on the key word), left-anchored, with a taupe `label` above and an Inter subtitle below. **Chrome** optional bottom meta row (Playfair value + taupe label). **Accent** the geo ring; optionally one `horizontal-accent` ink line (if it clears the meta). **Silence** ~55% empty. **Fixed** Playfair 400 ink sentence-case, ≤2 geo elements, 1px lines. **Free** title, ring placement, meta. **Density** sparse.
|
||||
|
||||
### 2 · Agenda / Index (index · move: hairline list · left)
|
||||
|
||||
**Ground** `{colors.bg-primary}`, `pad-x`. **Composes** label, h2, agenda-rows. **Focal** a Playfair `h2` over 4–6 agenda rows (Playfair numeral in taupe + Playfair label in ink), each on a 1px taupe rule. **Chrome** taupe `label` eyebrow. **Accent** none — taupe numerals carry it. **Silence** moderate; rows generously spaced. **Fixed** 1px taupe rules, Playfair 400. **Free** items, count. **Density** standard (sparse rows).
|
||||
|
||||
### 3 · Pull Quote (quote · move: centered statement · compass ring)
|
||||
|
||||
**Ground** `{colors.bg-primary}`, centered. **Composes** geo-ring (centered dashed, ~26cqw), quote-mark, h2/display-quote, attribution. **Focal** a 2-line Playfair quote in ink, centered, under a 50%-taupe Playfair quote-mark; a taupe uppercase attribution beneath. **Accent** the faint centered ring. **Silence** ~60% — deliberately open. **Fixed** Playfair 400, one ring, centered. **Free** quote, attribution. **Density** sparse.
|
||||
|
||||
### 4 · Closing Plate (closer · move: centered ring · centered)
|
||||
|
||||
**Ground** `{colors.bg-primary}`, centered. **Composes** geo-ring (centered, ~40cqw, solid+dashed), label, h1/display, horizontal-accent. **Focal** a 1–2 line Playfair sign-off in ink (italic key word), centered inside the largest compass ring; a short ink `horizontal-accent` beneath. **Accent** the ring + the one ink line. **Silence** ~60%. **Fixed** Playfair 400, centered, ≤2 geo. **Free** sign-off, ring scale. **Density** sparse.
|
||||
|
||||
### 5 · Two-Column Editorial (content · move: asymmetric split · left)
|
||||
|
||||
**Ground** `{colors.bg-primary}`, `pad-x`, two columns with `gap-xl`. **Composes** label, h2, body, image-placeholder (crossed-X) or card. **Focal** a Playfair `h2` + Inter body in the text column; an image-placeholder or card in the other. **Accent** none. **Silence** generous gutter. **Fixed** 1px taupe card/placeholder borders, white-overlay fill. **Free** which side is text, body copy. **Density** standard.
|
||||
|
||||
### 6 · Stats / Timeline (data · move: hairline rail · left)
|
||||
|
||||
**Ground** `{colors.bg-primary}`, `pad-x`. **Composes** label, h3, stats-cluster or timeline. **Focal** a modest Playfair stat row (or a 1px-rule timeline with year + headline + body, no nodes), framed by a 1px taupe top rule. **Accent** none. **Silence** moderate. **Fixed** modest stat scale, hairline rule, no nodes. **Free** figures, phases. **Density** standard.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Use a **single 1px taupe line** for every separator — the hairline is the identity.
|
||||
- Set every **Playfair headline at 400, ink, sentence case**; render labels taupe, uppercase, 2–3px tracked.
|
||||
- Layer **one or two compass rings** (solid + dashed, 20–50% opacity) behind content for atmosphere.
|
||||
- Let frames **breathe** — sparse, generous negative space; 55–60% empty on declarative frames.
|
||||
- Lean centered on quote/closer; asymmetric/left on cover/agenda/editorial. Use `bg-secondary` for placeholder fills.
|
||||
|
||||
### Don't
|
||||
|
||||
- Don't introduce a populist accent color — stone and ink only.
|
||||
- Don't bold Playfair, render headlines in taupe, or use thick (2px+) borders.
|
||||
- Don't add shadows, elevated cards, or rounded rectangles (circles only).
|
||||
- Don't crowd the frame — packed layouts read as broken.
|
||||
- Never more than two geo decorations per frame; don't blow a headline edge-to-edge.
|
||||
|
||||
## Aspect-Ratio Behavior
|
||||
|
||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
||||
| -------------------- | -------------------------- | ----------------------------------- | --------------------------- |
|
||||
| Cover | headline left, ring right | headline top, ring below | headline upper, ring behind |
|
||||
| Agenda / Index | h2 + rows | h2 + rows (tighter) | h2 + rows |
|
||||
| Pull Quote | centered, ring behind | centered, taller | centered |
|
||||
| Closing Plate | centered in ring | centered, ring scaled | centered |
|
||||
| Two-Column Editorial | text + visual side-by-side | stacked (collapse) | stacked |
|
||||
| Stats / Timeline | horizontal rail | vertical stack (drop timeline rule) | compact |
|
||||
|
||||
Generous `pad-x` holds on the short edge; re-step display per ratio above the 1.4cqw floor. On
|
||||
9:16, the timeline rule loses meaning when stacked — switch to a vertical list (per source).
|
||||
|
||||
## Approved Entities
|
||||
|
||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
||||
placeholder (the crossed-X image-placeholder, or a ringed initial for portraits).
|
||||
|
||||
## Numerals & Claims (hard rule)
|
||||
|
||||
Never invent figures, dates, or counts at frame scale. Render slots as `— figure —`, `{metric}`.
|
||||
Stats and timeline years carry placeholders until the script supplies them. Agenda ordinals
|
||||
(01, 02…) are decorative and may be sequential.
|
||||
|
||||
## Pre-Render Self-Audit
|
||||
|
||||
- **Squint** — one Playfair element dominates; the serif/sans contrast carries hierarchy.
|
||||
- **Silence** — declarative frames 55–60% empty; nothing is crowded.
|
||||
- **Palette** — five stones + ink only; no populist accent; headlines ink, labels taupe.
|
||||
- **Lines** — every separator is a 1px taupe hairline; the only ink line is `horizontal-accent`.
|
||||
- **Type** — Playfair 400 sentence-case, fit-to-measure; labels uppercase 2–3px; ≥1.4cqw floor.
|
||||
- **Depth** — 0 shadow, 0 rounded rectangle; ≤2 geo rings per frame.
|
||||
- **Anchor** — centered on quote/closer, left/asymmetric on cover/agenda/editorial; no 3 in a row alike.
|
||||
- **Fabrication** — every numeral traces to the script, else placeholder.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Motion intentionally out of scope.** frame.md specifies composition only; the 0.6s fade in the source is a deck mechanic.
|
||||
- **Playfair Display + Inter via Google Fonts.** CJK pairing (Noto Serif SC 700/400) carries over; Playfair has no Hanzi italic — substitute weight/taupe for emphasis.
|
||||
- **9:16 / 1:1 are guidance**; verify the legibility floor and that the timeline collapses to a vertical list.
|
||||
- Geo rings, the crossed-X placeholder, and the dashed inner ring are CSS-only; no external imagery is required.
|
||||
@@ -1,221 +0,0 @@
|
||||
<!--
|
||||
Cartesian — caption skin (preset-local source).
|
||||
|
||||
This is the preset's own lower-third karaoke caption look: a caption skin to read or
|
||||
copy, not to mount as is. A copy used as compositions/captions.html fills its three
|
||||
reserved holes and wraps the fragment in a <template>:
|
||||
· the empty GROUPS array → the engine's scene-aware word groups
|
||||
· the 0 duration + 0 width/height → real total + canvas size
|
||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
||||
|
||||
Token contract — reference ONLY this fixed vocab (set it from the
|
||||
project's frame.md; each var has a cartesian literal
|
||||
fallback for standalone preview):
|
||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
||||
--font-body · --cap-band-top · --cap-band-height
|
||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
||||
|
||||
Visual: a museum-catalog lower-third — a warm-stone card on a single 1px taupe
|
||||
hairline border (cartesian's universal structural device), SHARP corners, ZERO
|
||||
shadow, ZERO fill beyond the stone canvas. Playfair Display 400, sentence case, ink
|
||||
glyphs. Upcoming words sit in faint ink; the current word is marked QUIETLY — a faint
|
||||
stone wash with a 1px taupe hairline underline (restraint, not a loud block); spoken
|
||||
words settle to solid ink, the wash and underline cleared.
|
||||
-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<!-- the project's :root tokens go here -->
|
||||
<style data-brand-tokens></style>
|
||||
|
||||
<style>
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE stone card — single 1px taupe hairline border (cartesian's universal
|
||||
structural device), SHARP corners, ZERO shadow, ZERO fill beyond the stone canvas */
|
||||
.caption-pill {
|
||||
max-width: 78%;
|
||||
padding: 20px 44px 24px;
|
||||
background: var(--cap-canvas, #ede8e0);
|
||||
border: 1px solid var(--cap-accent-2, #b8b0a4);
|
||||
border-radius: 0;
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.1em 0.34em;
|
||||
font-family: var(--font-display, "Playfair Display"), Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(40px, 4.4vw, 58px);
|
||||
line-height: 1.16;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* upcoming — faint ink, no mark */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0 0.06em;
|
||||
border-bottom: 1px solid transparent;
|
||||
color: color-mix(in srgb, var(--cap-ink, #1a1a1a) 42%, var(--cap-canvas, #ede8e0));
|
||||
}
|
||||
/* current word — a QUIET marker: a faint warm-stone wash behind solid ink, plus a
|
||||
1px taupe hairline underline (restraint, not a loud block; SHARP, NO shadow) */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-ink, #1a1a1a);
|
||||
background: color-mix(in srgb, var(--cap-accent, #8a8178) 18%, var(--cap-canvas, #ede8e0));
|
||||
border-bottom: 1px solid var(--cap-accent, #8a8178);
|
||||
}
|
||||
/* already spoken — solid ink, wash + underline cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-ink, #1a1a1a);
|
||||
background: transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — Playfair Display italic, cartesian's only display inflection */
|
||||
.caption-word.cap-brand {
|
||||
font-style: italic;
|
||||
}
|
||||
/* numerals / data — the body sans voice, slightly recessed */
|
||||
.caption-word.cap-num {
|
||||
font-family: var(--font-body, "Inter"), ui-sans-serif, sans-serif;
|
||||
color: color-mix(in srgb, var(--cap-ink, #1a1a1a) 72%, var(--cap-canvas, #ede8e0));
|
||||
}
|
||||
/* emphasis — a 1px taupe hairline underline (no second color; stone-and-ink only) */
|
||||
.caption-word.cap-cta {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-decoration-color: var(--cap-accent, #8a8178);
|
||||
text-underline-offset: 0.12em;
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(34px, 5.4vw, 48px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="0"
|
||||
data-fps="30"
|
||||
data-width="0"
|
||||
data-height="0"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// the scene-aware word groups + real duration go here.
|
||||
var GROUPS = [];
|
||||
var DURATION = 0;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.985 }, { scale: 1, duration: 0.2, ease: "power1.out" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,265 +0,0 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Cobalt Grid — Frame (video / frame layer)
|
||||
description: >
|
||||
Video-first companion to Cobalt Grid's design.md. The unit is the frame (1920×1080). Atoms
|
||||
are identical and sacred — warm cream paper, electric cobalt ink (the only ink), the permanent
|
||||
graph-paper grid, Newsreader serif 400 + Hanken Grotesk + DM Mono, the top/bottom cobalt
|
||||
hairlines, and the pixel-glitch + QR-block signatures. Composition, frame scale, and
|
||||
aspect-ratio behavior are rewritten for the frame. Motion is out of scope.
|
||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
||||
principle: atoms are sacred · composition is free · numbers come from the script
|
||||
|
||||
colors:
|
||||
paper: "#F0EBDE"
|
||||
paper-2: "#E6E0CE"
|
||||
ink: "#1F2BE0"
|
||||
ink-soft: "#5560E5"
|
||||
grid: "rgba(31,43,224,0.10)"
|
||||
ink-faint: "rgba(31,43,224,0.18)"
|
||||
|
||||
typography:
|
||||
# — reading ramp —
|
||||
body: { fontFamily: "Hanken Grotesk", cqw: 0.83, weight: 400, lineHeight: 1.5 }
|
||||
body-lede: { fontFamily: "Hanken Grotesk", cqw: 0.95, weight: 400, lineHeight: 1.5 }
|
||||
micro: { fontFamily: "Hanken Grotesk", px: 13, weight: 600, tracking: "0.16em", upper: true }
|
||||
micro-strong:{ fontFamily: "Hanken Grotesk", px: 16, weight: 600, tracking: "0.18em", upper: true }
|
||||
mono-tag: { fontFamily: "DM Mono", cqw: 0.78, weight: 400, tracking: "0.05em" }
|
||||
mono-chrome: { fontFamily: "DM Mono", px: 13, weight: 400, tracking: "0.06em" }
|
||||
# — display / hero ramp (Newsreader 400, negative tracking) —
|
||||
table-name: { fontFamily: "Newsreader", cqw: 1.5, weight: 400, lineHeight: 1.15 }
|
||||
row-headline:{ fontFamily: "Newsreader", cqw: 2.1, weight: 400, lineHeight: 1.05 }
|
||||
ed-callout: { fontFamily: "Newsreader", cqw: 2.6, weight: 400, lineHeight: 1.1, italic: true }
|
||||
headline: { fontFamily: "Newsreader", cqw: 4.6, weight: 400, lineHeight: 0.95 }
|
||||
headline-index:{ fontFamily: "Newsreader", cqw: 5.0, weight: 400, lineHeight: 0.95 }
|
||||
display-quote:{ fontFamily: "Newsreader", cqw: 5.7, weight: 400, lineHeight: 1.05, tracking: "-0.005em" }
|
||||
display-chapter:{ fontFamily: "Newsreader", cqw: 6.8, weight: 400, lineHeight: 1.0, tracking: "-0.005em" }
|
||||
display-closing:{ fontFamily: "Newsreader", cqw: 9.4, weight: 400, lineHeight: 0.96, tracking: "-0.005em" }
|
||||
display-hero:{ fontFamily: "Newsreader", cqw: 10.4, weight: 400, lineHeight: 0.9, tracking: "-0.008em" }
|
||||
vbig-numeral:{ fontFamily: "Newsreader", cqw: 12.5, weight: 400, lineHeight: 0.9, tracking: "-0.015em" }
|
||||
|
||||
spacing:
|
||||
edge: "4cqw" # standard frame edge inset (~80px@1920)
|
||||
pad-top: "7cqw"
|
||||
pad-bottom: "6cqw"
|
||||
gap-md: "2cqw"
|
||||
|
||||
components:
|
||||
graph-grid:
|
||||
backgroundImage: "linear-gradient grid, ~2cqw cells, 10% {colors.ink} ({colors.grid})"
|
||||
placement: "behind EVERY frame on the ground"
|
||||
description: "Permanent graph-paper grid — never disabled; the canvas tone."
|
||||
hairlines:
|
||||
rule: "0.12cqw solid {colors.ink}"
|
||||
placement: "≈3cqw from top + bottom, inset {spacing.edge}"
|
||||
description: "Two persistent cobalt rules framing every composition."
|
||||
page-chrome:
|
||||
typography: "{typography.pagenum}"
|
||||
color: "{colors.ink}"
|
||||
placement: "page number bottom-right, nav/meta hint bottom-left, above the bottom hairline"
|
||||
description: "The only persistent chrome."
|
||||
pixel-glitch:
|
||||
backgroundColor: "{colors.paper}"
|
||||
fill: "repeating-linear-gradient(90deg, {colors.ink} 0 2px, transparent 2px 8px)"
|
||||
size: "14–30cqw wide, full height, stair-stepped"
|
||||
placement: "right on cover/data, left on chapter/colophon; z above grid, below headline"
|
||||
description: "Stair-stepped scanline column. Decorative."
|
||||
qr-block:
|
||||
backgroundColor: "{colors.paper}"
|
||||
cells: "8×8, {colors.ink} on / {colors.grid} off"
|
||||
size: "3–9cqw square"
|
||||
shadow: "0 0 0 0.12cqw {colors.paper} (anti-shadow for readability, not elevation)"
|
||||
description: "QR mosaic patch — corner punctuation."
|
||||
topbar-rule:
|
||||
typography: "{typography.headline-index} + {typography.mono-tag}"
|
||||
borderBottom: "0.12cqw solid {colors.ink}"
|
||||
description: "Section header on index/data/table frames."
|
||||
ledger-row:
|
||||
layout: "grid: mono num · Newsreader name · Hanken desc · mono delta"
|
||||
borderBottom: "0.06cqw solid {colors.ink-faint} (header 0.12cqw solid {colors.ink})"
|
||||
typography: "{typography.mono-tag} + {typography.table-name} + {typography.body}"
|
||||
description: "Dense matrix row with ↑/↓/— delta."
|
||||
pixel-stack-bar:
|
||||
cells: "column-reverse, {colors.grid} off / {colors.ink} on"
|
||||
baseline: "0.12cqw solid {colors.ink} + {typography.mono-tick} ticks"
|
||||
description: "Data as grid-unit cells; echoes the glitch."
|
||||
vstack-label:
|
||||
typography: "{typography.mono-tick}"
|
||||
transform: "writing-mode: vertical-rl"
|
||||
description: "Catalogue chrome along a frame edge."
|
||||
---
|
||||
|
||||
# Cobalt Grid — Frame (video / frame layer)
|
||||
|
||||
## Overview
|
||||
|
||||
Cobalt Grid at frame scale is a **two-color risograph trend-report** — warm cream paper, electric
|
||||
cobalt ink, and a **permanent graph-paper grid** behind every frame. Cobalt is the _only_ ink:
|
||||
headlines, body, rules, the grid, the pixel-glitch decoration, the QR patches. There is no accent
|
||||
color and no second surface.
|
||||
|
||||
The voice is a three-face conversation: **Newsreader** serif at weight 400 (size, not weight,
|
||||
makes hierarchy) carries every display and headline; **Hanken Grotesk** carries body and
|
||||
uppercase tracked labels; **DM Mono** carries all chrome — page numbers, tags, ticks, vertical
|
||||
stacks. Every frame is framed by top + bottom cobalt hairlines; declarative frames carry the
|
||||
**pixel-glitch column** and a **QR-block** patch.
|
||||
|
||||
**Key characteristics at frame scale:**
|
||||
|
||||
- **Strictly two-color** — cream paper + cobalt ink; no accent, no second surface.
|
||||
- **Permanent ~2cqw graph-paper grid** (10% cobalt) behind every frame; cannot be disabled.
|
||||
- **Top + bottom 1.5px cobalt hairlines** frame every composition, inset by `edge`.
|
||||
- **Newsreader 400** for all display (negative tracking); **Hanken 600** uppercase 0.16em labels; **DM Mono** chrome.
|
||||
- **Pixel-glitch column + QR-block** as the signature decorative patches on declarative frames.
|
||||
- **Pixel-stack bars** render data as grid-unit cells (cobalt on / 10% off).
|
||||
|
||||
## The Frame
|
||||
|
||||
### Frame Craft Bar
|
||||
|
||||
Three eyeball tests gate every frame before any structural check:
|
||||
|
||||
- **Squint** — one Newsreader element dominates at **3–6× its nearest neighbor** (`display-hero`/`vbig-numeral`); hierarchy is size, never weight.
|
||||
- **Silence** — declarative frames (cover, chapter, quote, colophon) read **45–60% empty** with the grid showing; the **index ledger and data frame are the one dense exception** (density via quiet repetition, not richness).
|
||||
- **Restraint** — **two-color only** (cream + cobalt, never a second hue); one pixel-glitch column and at most one QR-block per declarative frame.
|
||||
- **Reference** — aim at a **WIRED Japan / Shift two-color risograph monograph**; failure looks like a **colorful dashboard with a second accent hue**.
|
||||
|
||||
- **Primary:** 1920×1080 (16:9). Display authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`).
|
||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
||||
- **Safe area:** `edge` (4cqw) inset; hairlines + page chrome live on the safe line.
|
||||
|
||||
**The container law (load-bearing).** Every frame ground sets `container-type: size` AND carries
|
||||
the graph-paper `background-image`; ALL frame-relative units are `cqw`/`cqh` against it — never
|
||||
`vw`. The grid `background-size` is `~2cqw 2cqw` so cell density holds at any render size.
|
||||
|
||||
## Colors
|
||||
|
||||
Tokens identical to the source. `{colors.paper}` is the ground; `{colors.ink}` cobalt is the only
|
||||
ink (type, rules, grid lines, glitch, QR). `{colors.ink-soft}` is a secondary cobalt for editorial
|
||||
subtitles only; `{colors.grid}` (10%) is the permanent grid + chart "off" cells; `{colors.ink-faint}`
|
||||
(18%) is faint row dividers. **Never a second hue** — emphasis comes from size, from switching
|
||||
Hanken→Newsreader, from a mono delta arrow, or from opacity, never from color.
|
||||
|
||||
## Typography
|
||||
|
||||
Two ramps. The **reading ramp** (Hanken body 0.83cqw, mono chrome in px) carries copy + labels;
|
||||
the **display/hero ramp** (Newsreader `headline` 4.6cqw → `vbig-numeral` 12.5cqw) carries every
|
||||
statement and figure.
|
||||
|
||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; mono chrome (px) is colophon only.
|
||||
- **Fit-to-measure:** size the headline to its line length. Cap the block at **≤ 78cqw**; ≤3 words → `display-hero`/`vbig-numeral`; 4–6 → `display-chapter`; 7+ → `headline`.
|
||||
- **Newsreader at 400 only**, in cobalt, with negative tracking (hero −0.008em, numeral −0.015em). **Hanken labels uppercase, 0.16–0.18em.** **DM Mono 0.04–0.08em.** No bold serif.
|
||||
|
||||
## Depth & Surface
|
||||
|
||||
Flat. Depth is structural only:
|
||||
|
||||
- **1.5px cobalt rules** — slide hairlines, topbar rule, chart baseline.
|
||||
- **1px ink-faint dividers** — dense list / ledger rows.
|
||||
- **The graph-paper grid** — measured-plane tone behind everything.
|
||||
- **Pixel-glitch + QR** — texture and graphic punctuation, no z-axis.
|
||||
|
||||
**Ceiling:** no drop shadow (the QR's 1.5px paper outset is an anti-shadow for readability, not elevation), no gradient, no rounded corner, no second color.
|
||||
|
||||
## Shapes
|
||||
|
||||
- **0 radius everywhere** — frames, ledger rows, QR cells, glitch blocks, charts. Zero circular elements; this squareness is part of the identity.
|
||||
|
||||
## Components
|
||||
|
||||
- **graph-grid / hairlines / page-chrome** — the permanent frame furniture, inherited by every composition.
|
||||
- **pixel-glitch** (edge column) + **qr-block** (corner patch) — the signature decoration on declarative frames.
|
||||
- **topbar-rule** — the index/data/table section header. **ledger-row** — dense matrix row with delta arrows.
|
||||
- **pixel-stack-bar** — data as grid-unit cells. **vstack-label** — vertical mono catalogue chrome.
|
||||
|
||||
## Frame Treatments
|
||||
|
||||
> Recipe: ground · container · composes · focal · chrome · accent · silence · Fixed/Free · density.
|
||||
> The grid + hairlines are present on every frame. Choose density by type: declarative = sparse, index/data = dense.
|
||||
|
||||
### 1 · Hero Cover (identity · move: serif + glitch · left)
|
||||
|
||||
**Ground** paper + grid, hairlines. **Composes** pixel-glitch (right, ~26cqw), qr-block (top-right), display-hero, ed-callout. **Focal** a 1–2 line `display-hero` Newsreader headline in cobalt, left-anchored, with an italic `ed-callout` subtitle in `{colors.ink-soft}`. **Chrome** Hanken kicker; mono meta top + page number. **Accent** none (cobalt is the only ink). **Silence** ~45% paper. **Fixed** Newsreader 400, glitch + QR present, grid + hairlines. **Free** title, glitch step pattern, QR placement. **Density** sparse.
|
||||
|
||||
### 2 · Index Ledger (catalog · move: dense matrix · left — the dense frame)
|
||||
|
||||
**Ground** paper + grid, hairlines, `pad-top`/`pad-bottom`. **Composes** topbar-rule, ledger-rows. **Focal** a topbar (`headline-index` + mono lab-tag, 1.5px rule) over 4–6 ledger rows (mono num · Newsreader name · Hanken desc), ink-faint dividers. **Chrome** page number. **Accent** mono delta arrows. **Silence** tight — the density exception (the grid wants filling). **Fixed** 1.5px topbar rule, 1px ink-faint dividers, Newsreader names. **Free** rows, lab-tag, deltas. **Density** dense-exception.
|
||||
|
||||
### 3 · Chapter Opener (section · move: scale · sparse · left)
|
||||
|
||||
**Ground** paper + grid, hairlines. **Composes** pixel-glitch (left, ~14cqw, low opacity), mono index, display-chapter, body-lede. **Focal** a `display-chapter` Newsreader title, with a small mono `CHAPTER NN` index above and an optional Hanken lede ≤42cqw. **Accent** none. **Silence** ~60% — let the grid breathe. **Fixed** Newsreader 400, glitch low-opacity, grid showing. **Free** title, lede, glitch side. **Density** sparse.
|
||||
|
||||
### 4 · Data Frame (chart · move: pixel-stack · left)
|
||||
|
||||
**Ground** paper + grid, hairlines, `pad-top`. **Composes** topbar-rule, pixel-stack-bar row. **Focal** a row of 6–8 pixel-stack bars (cobalt on / 10% off) over a 1.5px cobalt baseline with mono ticks. **Chrome** topbar headline + mono fig-tag; page number. **Accent** none — data is cobalt cells. **Silence** moderate. **Fixed** grid-unit cells, cobalt baseline, mono ticks. **Free** bar values (from script), tick labels. **Density** standard/dense.
|
||||
|
||||
### 5 · Manifesto / Quote (quote · move: centered statement · sparse)
|
||||
|
||||
**Ground** paper + grid, hairlines. **Composes** display-quote (or display-manifesto), attribution rule, optional compact glitch. **Focal** a 2–3 line Newsreader pull in cobalt, with a Hanken kicker above and a 1px cobalt attribution rule + mono byline beneath. **Accent** none. **Silence** ~55% — deliberately open. **Fixed** Newsreader 400, attribution rule, grid showing. **Free** quote, byline. **Density** sparse.
|
||||
|
||||
### 6 · Colophon (closer · move: right-aligned close · sparse)
|
||||
|
||||
**Ground** paper + grid, hairlines. **Composes** pixel-glitch (left edge, mirroring the cover), display-closing, mono credit columns. **Focal** a right-aligned `display-closing` Newsreader title with a Hanken kicker above. **Chrome** 3–4 column mono credit grid at the foot; page number. **Accent** none. **Silence** ~50%. **Fixed** glitch on left, Newsreader 400. **Free** closing line, credits. **Density** sparse.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Keep the **graph-paper grid + top/bottom hairlines** on every frame — they are the system.
|
||||
- Set **Newsreader at 400 in cobalt**; make hierarchy with size, not weight; negative-track display.
|
||||
- Track **Hanken labels uppercase 0.16em**; **DM Mono chrome 0.04–0.08em**.
|
||||
- Render data as **pixel-stack cells** (cobalt on / 10% off); echo the glitch language.
|
||||
- Use the **pixel-glitch column + QR patch** on declarative frames (cover, chapter, quote, colophon).
|
||||
- Lean: declarative frames sparse and breathing; index/data dense. Vary anchor (left for index/chapter, centered for quote/closer).
|
||||
|
||||
### Don't
|
||||
|
||||
- Don't introduce a second ink color — cream + cobalt only.
|
||||
- Don't bold Newsreader, round any corner, or add a drop shadow (QR outset is an anti-shadow only).
|
||||
- Don't disable the grid or suppress the hairlines.
|
||||
- Don't crowd chapter / quote / colophon — those let the grid show.
|
||||
- Don't blow a serif headline edge-to-edge — fit to measure.
|
||||
|
||||
## Aspect-Ratio Behavior
|
||||
|
||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
||||
| ----------------- | --------------------------- | ------------------------------------ | ---------------------------- |
|
||||
| Hero Cover | headline left, glitch right | headline top, glitch full-width band | headline upper, glitch lower |
|
||||
| Index Ledger | topbar + rows | topbar + fewer rows | 2-col compresses to 1 |
|
||||
| Chapter Opener | title left, glitch left | title top, glitch side | centered title |
|
||||
| Data Frame | 6–8 bars | 4–5 bars taller | square chart |
|
||||
| Manifesto / Quote | centered pull | centered, taller | centered |
|
||||
| Colophon | right-aligned, glitch left | stacked, glitch top | centered close |
|
||||
|
||||
Grid + hairlines hold on the short edge for every ratio; re-step display so no load-bearing line
|
||||
drops below 1.4cqw. Mono chrome stays Latin/digit-only.
|
||||
|
||||
## Approved Entities
|
||||
|
||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
||||
placeholder. Trend names, signals, and figures are content; the system supplies the grid and chrome.
|
||||
|
||||
## Numerals & Claims (hard rule)
|
||||
|
||||
Never invent figures, deltas, dates, or signal counts at frame scale. Render slots as `— figure —`,
|
||||
`{metric}`, `↑ —`. Pixel-stack bar heights and ledger deltas especially carry placeholders until the
|
||||
script supplies values. Mono catalogue ordinals (001, 002…) are decorative and may be sequential.
|
||||
|
||||
## Pre-Render Self-Audit
|
||||
|
||||
- **Squint** — one Newsreader element dominates at 3–5× its neighbor.
|
||||
- **Silence** — declarative frames 45–60% empty; only index/data run dense.
|
||||
- **Two-color** — cream + cobalt only; no second hue anywhere.
|
||||
- **Furniture** — grid present, top/bottom hairlines present, page chrome above the hairline.
|
||||
- **Type** — Newsreader 400 negative-tracked, fit-to-measure; Hanken labels 0.16em; ≥1.4cqw floor.
|
||||
- **Depth** — 0 shadow (QR outset excepted), 0 rounded corner.
|
||||
- **Anchor** — left on index/chapter/cover, centered on quote/closer; no 3 consecutive frames share an anchor.
|
||||
- **Fabrication** — every numeral/delta traces to the script, else placeholder.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Motion intentionally out of scope.** frame.md specifies composition only; the 280ms cross-fade in the source is a deck mechanic.
|
||||
- **Pixel-glitch is rendered in CSS here** (stacked scanline blocks) rather than the source's inline SVG, to keep the showcase SVG-free; fidelity is preserved.
|
||||
- **Three Google Fonts** (Newsreader, Hanken Grotesk, DM Mono); CJK pairing (Noto Serif SC 700/400) carries over from the source.
|
||||
- **9:16 / 1:1 are guidance**; verify the legibility floor and grid density per ratio.
|
||||
- The QR mosaic and glitch step patterns are hand-authored; there is no generative layer.
|
||||
@@ -1,237 +0,0 @@
|
||||
<!--
|
||||
Cobalt Grid — caption skin (preset-local source).
|
||||
|
||||
This is the preset's own lower-third karaoke caption look: a caption skin to read or
|
||||
copy, not to mount as is. A copy used as compositions/captions.html fills its three
|
||||
reserved holes and wraps the fragment in a <template>:
|
||||
· the empty GROUPS array → the engine's scene-aware word groups
|
||||
· the 0 duration + 0 width/height → real total + canvas size
|
||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
||||
|
||||
Token contract — reference ONLY this fixed vocab (set it from the
|
||||
project's frame.md; each var has a cobalt literal
|
||||
fallback for standalone preview):
|
||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
||||
--font-body · --cap-band-top · --cap-band-height
|
||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
||||
|
||||
Visual: a cream-paper card carrying the permanent graph-paper grid behind the line,
|
||||
framed by top + bottom 1.5px cobalt hairlines (cobalt's signature frame furniture) —
|
||||
square corners, NO drop shadow (cobalt's depth is structural and flat, never cast).
|
||||
Newsreader serif 400, sentence case, cobalt glyphs. Upcoming words sit in faint
|
||||
cobalt; the current word is a hard COBALT FLAT BLOCK — a risograph knockout: a solid
|
||||
cobalt fill (box-shadow spread = a square block, no blur, no layout shift) with the
|
||||
cream paper reading on top. Spoken words settle to solid cobalt ink, the block cleared.
|
||||
Strictly two-color throughout — cream + cobalt, never a second hue.
|
||||
-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<!-- the project's :root tokens go here -->
|
||||
<style data-brand-tokens></style>
|
||||
|
||||
<style>
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE cream card — graph-paper grid behind the line + top/bottom 1.5px cobalt
|
||||
hairlines (cobalt's frame furniture), square corners, NO drop shadow */
|
||||
.caption-pill {
|
||||
position: relative;
|
||||
max-width: 78%;
|
||||
padding: 20px 44px 24px;
|
||||
background: var(--cap-canvas, #f0ebde);
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to right,
|
||||
color-mix(in srgb, var(--cap-ink, #1f2be0) 10%, transparent) 1px,
|
||||
transparent 1px
|
||||
),
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
color-mix(in srgb, var(--cap-ink, #1f2be0) 10%, transparent) 1px,
|
||||
transparent 1px
|
||||
);
|
||||
background-size: 16px 16px;
|
||||
border-top: 1.5px solid var(--cap-ink, #1f2be0);
|
||||
border-bottom: 1.5px solid var(--cap-ink, #1f2be0);
|
||||
border-radius: 0;
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.1em 0.34em;
|
||||
font-family: var(--font-display, "Newsreader"), Georgia, serif;
|
||||
font-weight: 400;
|
||||
font-size: clamp(40px, 4.4vw, 58px);
|
||||
line-height: 1.16;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* upcoming — faint cobalt, no block */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0 0.06em;
|
||||
color: color-mix(in srgb, var(--cap-ink, #1f2be0) 42%, var(--cap-canvas, #f0ebde));
|
||||
}
|
||||
/* current word — THE cobalt flat block (risograph knockout): a solid cobalt fill
|
||||
via box-shadow spread = a hard square block w/o layout shift, no blur; the cream
|
||||
paper reads on top */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-canvas, #f0ebde);
|
||||
background: var(--cap-ink, #1f2be0);
|
||||
box-shadow: 0 0 0 0.06em var(--cap-ink, #1f2be0);
|
||||
}
|
||||
/* already spoken — solid cobalt ink, block cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-ink, #1f2be0);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — Newsreader italic, cobalt's only display inflection */
|
||||
.caption-word.cap-brand {
|
||||
font-style: italic;
|
||||
}
|
||||
/* numerals / data — the body sans voice, slightly recessed */
|
||||
.caption-word.cap-num {
|
||||
font-family: var(--font-body, "Hanken Grotesk"), ui-monospace, monospace;
|
||||
color: color-mix(in srgb, var(--cap-ink, #1f2be0) 72%, var(--cap-canvas, #f0ebde));
|
||||
}
|
||||
/* emphasis — a cobalt hairline underline (no second hue; emphasis is the ink) */
|
||||
.caption-word.cap-cta {
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 2px;
|
||||
text-decoration-color: var(--cap-accent, #5560e5);
|
||||
text-underline-offset: 0.1em;
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(34px, 5.4vw, 48px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="0"
|
||||
data-fps="30"
|
||||
data-width="0"
|
||||
data-height="0"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// the scene-aware word groups + real duration go here.
|
||||
var GROUPS = [];
|
||||
var DURATION = 0;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.985 }, { scale: 1, duration: 0.1, ease: "power3.out" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,277 +0,0 @@
|
||||
---
|
||||
version: alpha
|
||||
name: Code editorial — Frame (video / frame layer)
|
||||
description: >
|
||||
Video-first companion to Code editorial's design.md. The unit is the frame (1920×1080). Atoms are
|
||||
identical and sacred — warm cream paper (never pure white, never cool), a single terracotta coral
|
||||
as scarce "voltage", hairline ink elevation (no heavy shadow), EB Garamond for all
|
||||
display + Inter body + JetBrains Mono for the index/code voice on a warm-navy code surface,
|
||||
sentence-case display, and the ✱ coral spike mark. Composition + frame scale rewritten. Motion out
|
||||
of scope.
|
||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
||||
principle: atoms are sacred · composition is free · numbers come from the script
|
||||
|
||||
colors:
|
||||
ink: "#141413"
|
||||
cream: "#FAF9F5"
|
||||
tile: "#EFE9DE"
|
||||
tile-strong: "#ECE3D4"
|
||||
coral: "#CC785C"
|
||||
navy: "#181715"
|
||||
navy-soft: "#1F1E1B"
|
||||
navy-elev: "#252320"
|
||||
|
||||
borders: { hairline: "1px solid ink@12%", hairline-strong: "1px solid ink@20%", dark: "1px solid cream@14% (on navy)" }
|
||||
shadows: { card: "0 1px 3px ink@8%, 0 4px 16px ink@4%", none: "none" }
|
||||
|
||||
typography:
|
||||
# — reading + chrome ramp —
|
||||
body: { fontFamily: "Inter", cqw: 1.5, weight: 400, lineHeight: 1.5 }
|
||||
lead: { fontFamily: "Inter", cqw: 2.08, weight: 400, lineHeight: 1.5 }
|
||||
card-title:{ fontFamily: "Inter", cqw: 2.3, weight: 500, lineHeight: 1.25, tracking: "-0.005em" }
|
||||
button: { fontFamily: "Inter", cqw: 1.46, weight: 500, lineHeight: 1.0 }
|
||||
tag-upper:{ fontFamily: "Inter", cqw: 1.35, weight: 500, tracking: "0.18em", upper: true }
|
||||
kicker: { fontFamily: "JetBrains Mono", px: 28, cqw: 1.46, weight: 500, tracking: "0.16em", upper: true }
|
||||
mono-label:{ fontFamily: "JetBrains Mono", px: 26, cqw: 1.35, weight: 500, tracking: "0.02em" }
|
||||
code: { fontFamily: "JetBrains Mono", cqw: 1.67, weight: 400, lineHeight: 1.6 }
|
||||
# — display ramp (EB Garamond 400, sentence case, negative tracking. Renderer embeds only 400/700 — author at 400; italic is the synthesized slant) —
|
||||
headline:{ fontFamily: "EB Garamond", cqw: 4.6, weight: 400, lineHeight: 1.06, tracking: "-0.018em" }
|
||||
quote-pull:{ fontFamily: "EB Garamond", cqw: 5.0, weight: 400, lineHeight: 1.12, tracking: "-0.012em", italic: true }
|
||||
display-italic:{ fontFamily: "EB Garamond", cqw: 6.7, weight: 400, lineHeight: 1.05, tracking: "-0.012em", italic: true }
|
||||
display:{ fontFamily: "EB Garamond", cqw: 7.3, weight: 400, lineHeight: 1.02, tracking: "-0.022em" }
|
||||
number-hero:{ fontFamily: "EB Garamond", cqw: 9.4, weight: 400, lineHeight: 0.95, tracking: "-0.025em" }
|
||||
display-cover:{ fontFamily: "EB Garamond", cqw: 9.9, weight: 400, lineHeight: 0.98, tracking: "-0.028em" }
|
||||
number-unit:{ fontFamily: "JetBrains Mono", cqw: 2.08, weight: 500, lineHeight: 1.0 }
|
||||
|
||||
spacing:
|
||||
slide-pad: "4.2cqw" # ~80px @1920
|
||||
gap-md: "1.7cqw"
|
||||
hairline: "1px"
|
||||
radius-sm: "6px"
|
||||
radius-md: "8px"
|
||||
radius-lg: "12px"
|
||||
radius-pill: "9999px"
|
||||
|
||||
components:
|
||||
card-hairline:
|
||||
backgroundColor: "{colors.cream} or {colors.tile}"
|
||||
border: "1px solid {colors.ink}@12%"
|
||||
rounded: "{spacing.radius-lg}"
|
||||
shadow: "{shadows.card}"
|
||||
typography: "{typography.card-title} + {typography.body}"
|
||||
description: "The editorial content card. Elevation is the hairline + ONE soft warm shadow — never a heavy drop, glow, or gradient."
|
||||
kicker-spike:
|
||||
typography: "{typography.kicker}"
|
||||
mark: "✱ coral spike prefix"
|
||||
description: "The eyebrow — JetBrains Mono uppercase, indexical (2–5 words), prefixed with the coral ✱. Never plain text, never a sentence."
|
||||
coral-callout:
|
||||
backgroundColor: "{colors.coral} (full-bleed) or {colors.cream} with a coral edge"
|
||||
textColor: "{colors.cream} on coral"
|
||||
rounded: "{spacing.radius-md}"
|
||||
typography: "{typography.button} / {typography.h2}"
|
||||
description: "The ONE voltage moment per frame — the CTA, the single inline link, OR the full-bleed band. Never two corals in one frame."
|
||||
number-lockup:
|
||||
typography: "{typography.number-hero} figure + {typography.number-unit} unit"
|
||||
description: "Hero stat — a EB Garamond figure paired with a JetBrains Mono unit (200K, +1,204, −318, 17 files). Figure is serif; the unit is ALWAYS mono, never the serif."
|
||||
pull-quote:
|
||||
typography: "{typography.quote-pull} (EB Garamond italic) + {typography.tag-upper} cite"
|
||||
description: "A commit message, a reviewer line, or the thesis. EB Garamond italic, small Inter uppercase cite beneath."
|
||||
section-rule:
|
||||
rule: "1px solid {colors.ink}@12% (or {colors.cream}@14% on navy)"
|
||||
description: "The only separator. A coral 1px rule may draw on to introduce a section. Never 2px+, never a heavy divider."
|
||||
code-surface:
|
||||
backgroundColor: "{colors.navy} body / {colors.navy-elev} title bar + status strip"
|
||||
textColor: "{colors.cream} (JetBrains Mono); syntax in coral (keywords) / teal #5DB8A6 (strings) / amber #E8A55A (numbers)"
|
||||
border: "1px solid {colors.cream}@14%"
|
||||
rounded: "{spacing.radius-md}"
|
||||
description: "The warm-navy code / terminal surface. The CODE ITSELF is rendered by the code-* registry blocks (code-diff / code-typing / code-snippet-*); this preset owns the surrounding surface, title bar, status strip, and mono chrome — not the code rendering."
|
||||
spike-mark:
|
||||
glyph: "✱ (U+2731), always {colors.coral}"
|
||||
description: "The brand mark. Fades + scales 0.92→1 on a single emphasis beat; never spins."
|
||||
---
|
||||
|
||||
# Code editorial — Frame (video / frame layer)
|
||||
|
||||
## Overview
|
||||
|
||||
Code editorial at frame scale is a **warm-editorial brand book come to life** — the register of a literary
|
||||
imprint or a research note. The thesis is three colors: **cream is the ground, ink is the voice,
|
||||
coral is the voltage** — and a fourth (warm navy) only where code shows itself. Every surface is
|
||||
**warm cream** (never pure white, never cool gray); content gathers on a **tile** surface half a
|
||||
step darker — the demarcation is a half-step, never a hard contrast. Elevation is a **1px hairline**
|
||||
ink border at low alpha plus, rarely, one soft warm shadow. There are no heavy drops, no glows, no
|
||||
gradients on content.
|
||||
|
||||
Three editorial voices, each in its own face: **EB Garamond** carries every display moment — covers,
|
||||
headlines, pull-quotes, big stat numerals — at large display sizes with gentle negative tracking;
|
||||
its **italic** is the expressive register. **Inter** carries body, leads, card titles, buttons, and
|
||||
UI chrome. **JetBrains Mono** carries the indexical layer — kickers, technical labels, the code
|
||||
window, status strips. Switching a voice's face collapses the register: a sans headline or a serif
|
||||
label reads as a different brand.
|
||||
|
||||
**Key characteristics at frame scale:**
|
||||
|
||||
- **Cream / ink / coral trinity** + a warm-navy code surface; cream is the default ground, ink the voice, coral the scarce voltage.
|
||||
- **EB Garamond** (sentence case, negative-tracked) for all display; **Inter** body/chrome; **JetBrains Mono** index + code.
|
||||
- **Hairline elevation** — a 1px low-alpha ink border + at most one soft warm shadow. No heavy drop, glow, or gradient.
|
||||
- **Coral is rationed** — at most ONE coral moment per frame (CTA, inline link, OR full-bleed band); coral never sets a headline or a body run.
|
||||
- **Density is free** — fill the frame as the content wants; a frame may stand on a single focal or carry a dense, layered composition.
|
||||
- **The ✱ coral spike** opens kickers; warm navy is reserved for the code/terminal surface.
|
||||
|
||||
## The Frame
|
||||
|
||||
### Frame Craft Bar
|
||||
|
||||
Eyeball tests gate every frame before any structural check:
|
||||
|
||||
- **Squint** — one EB Garamond display moment dominates at 3–6× its neighbor.
|
||||
- **Trinity** — cream/tile ground, ink text, coral exactly **once**; warm navy only on the code surface; no cool gray / pure white / fourth hue.
|
||||
- **Type** — EB Garamond sentence-case display (negative-tracked); Inter 400 body; JetBrains Mono kickers (uppercase 0.16em, coral ✱) + code.
|
||||
|
||||
- **Primary:** 1920×1080 (16:9). Display authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`).
|
||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
||||
- **Safe area:** `slide-pad` ~4.2cqw; the kicker/mono chrome sit inside it.
|
||||
|
||||
**The container law (load-bearing).** Every frame ground sets `container-type: size`; ALL
|
||||
frame-relative units are `cqw`/`cqh` against it — never `vw`. Hairlines stay 1px; card radii stay
|
||||
6/8/12px; the warm-paper reading must survive every ratio.
|
||||
|
||||
## Colors
|
||||
|
||||
Tokens identical to the source. Default ground `{colors.cream}`; content gathers on
|
||||
`{colors.tile}` / `{colors.tile-strong}` (half-step warm steps, never a hard contrast).
|
||||
**Headlines & body:** `{colors.ink}` on cream/tile; `{colors.cream}` on navy. **Coral**
|
||||
(`{colors.coral}`) is the scarce voltage — one moment per frame (CTA, inline link, or full-bleed
|
||||
band), never body text, never a card fill. **Warm navy** (`{colors.navy}` / `navy-soft` /
|
||||
`navy-elev`) is the code / terminal / dark-card surface — a structural anchor, not a fourth brand
|
||||
hue. **No cool grays, no pure white, no pure black.**
|
||||
|
||||
**Fixed syntax colors (decoration, NOT remixable brand hues).** When a code line is hand-set rather
|
||||
than rendered by a `code-*` block, keywords are coral, strings are **teal `#5DB8A6`**, numbers are
|
||||
**amber `#E8A55A`**; status reads success `#5DB872` / warn `#C64545`. These track the code surface,
|
||||
not the brand trinity — keep them out of the brand palette.
|
||||
|
||||
## Typography
|
||||
|
||||
Two ramps. The **reading/chrome ramp** (Inter `body` 1.5cqw / `lead` 2.08cqw weight 400; JetBrains
|
||||
Mono `kicker`/`mono-label` in px) carries copy + chrome; the **display ramp** (EB Garamond `headline` 4.6cqw
|
||||
→ `display-cover` 9.9cqw, weight 400, negative-tracked) carries every headline + stat.
|
||||
|
||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; mono px labels are chrome only.
|
||||
- **Fit-to-measure:** size the headline to its length. Cap the block at **≤ 78cqw**; ≤3 words → `display-cover`; 4–6 → `display`; 7+ → `headline`. Reserve the 7.3–9.9cqw tier for cover / statement / stat.
|
||||
- **EB Garamond display is sentence case** (NOT title case, NOT uppercase), weight 400, negative-tracked (−0.012..−0.028em); reach for **italic** when the line is a stance or a definition. **Inter body** sentence case weight 400. **JetBrains Mono** kickers UPPERCASE 0.16em with the coral ✱. No uppercase serif, no sans headline, no serif label.
|
||||
|
||||
## Depth & Surface
|
||||
|
||||
Hairline elevation:
|
||||
|
||||
- **1px hairline** ink border at ~12% alpha is the primary lift (cream@14% on navy).
|
||||
- **One soft warm shadow** (`0 1px 3px ink@8%, 0 4px 16px ink@4%`) — used rarely, never heavy.
|
||||
- **Half-step surface** — a `{colors.tile}` block on cream reads elevated by the warm step, not by a cast shadow.
|
||||
|
||||
**Ceiling:** no heavy drop shadow, no glow, no gradient on content, no tilt. The system has no light
|
||||
to emit; it reads by warmth and hairline.
|
||||
|
||||
## Shapes
|
||||
|
||||
- **6px** small chrome, **8px** cards / code surface, **12px** large cards / quote frames, **9999px** true pills only. No square corners, no heavy rounding; the editorial register is gently rounded, never hard.
|
||||
|
||||
## Components
|
||||
|
||||
- **card-hairline** — the editorial content card (hairline + one soft shadow). **section-rule** — the only separator (1px, coral may draw on).
|
||||
- **kicker-spike** — the ✱ coral eyebrow. **coral-callout** — the one voltage moment (CTA / inline link / full-bleed band).
|
||||
- **number-lockup** — EB Garamond figure + mono unit (the PR `+N / −M`, `200K`, file counts). **pull-quote** — EB Garamond italic + cite (a commit message / reviewer line).
|
||||
- **code-surface** — the warm-navy code / terminal surface; the code itself comes from the **`code-*` registry blocks**, this owns the surface + mono chrome.
|
||||
- **spike-mark** — the ✱ brand glyph, always coral.
|
||||
|
||||
## Frame Treatments
|
||||
|
||||
> Recipe: ground · container · composes · focal · chrome · accent · Fixed/Free · density.
|
||||
> One coral moment per frame; open with a kicker-spike.
|
||||
|
||||
### 1 · Cover (identity · move: oversized EB Garamond · cream)
|
||||
|
||||
**Ground** `{colors.cream}`, `slide-pad`. **Composes** kicker-spike, display-cover, lead, mono-label index. **Focal** a 2–3 line EB Garamond `display-cover` (sentence case, ink) under a coral ✱ kicker. **Chrome** mono index strip (repo · branch). **Accent** the single coral ✱. **Fixed** EB Garamond 400 sentence case, hairline, cream ground. **Free** title, the mono index, layout + how full the frame runs. **Density** free.
|
||||
|
||||
### 2 · Statement (statement · move: single EB Garamond line · cream or navy)
|
||||
|
||||
**Ground** `{colors.cream}` (or `{colors.navy}` for gravity). **Composes** kicker-spike, display, optional lead. **Focal** one 2-line EB Garamond `display` carrying the change in a sentence — reach for **italic** if it's a stance. **Chrome** mono kicker. **Accent** none — the serif carries it (or one coral word). **Fixed** sentence-case serif. **Free** the line, ground, layout + density. **Density** free.
|
||||
|
||||
### 3 · Code Surface (code · move: warm-navy code window · the PR-critical frame)
|
||||
|
||||
**Ground** `{colors.cream}` framing a `{colors.navy}` **code-surface** (8px, cream@14% hairline, `navy-elev` title bar + filename in mono). **Composes** mono-label filename, the **`code-*` block** (code-diff / code-typing / code-snippet-_), optional `section-rule`. **Focal** the code panel — the diff / before→after / typed-on snippet. **Chrome** mono filename + status strip. **Accent** syntax coral/teal/amber inside the panel; one coral marker outside (e.g. a `+`/`−` gutter cue). **Fixed** warm-navy surface, mono code, hairline. **Free** which code-_ block, the code (from the diff), how large the panel runs. **Density** dense.
|
||||
|
||||
### 4 · Number / Impact (data · move: oversized figure · cream)
|
||||
|
||||
**Ground** `{colors.cream}`, `slide-pad`. **Composes** kicker-spike, number-lockup, lead/caption, optional `section-rule`. **Focal** a EB Garamond `number-hero` figure with a mono unit over a 1px rule — the PR impact (`+1,204 / −318`, `17 files`, `2.1× faster`). **Chrome** mono tag. **Accent** the figure in ink; at most one coral unit. **Fixed** serif figure + mono unit, hairline rule. **Free** the figures (from the script), tag, layout + density. **Density** free.
|
||||
|
||||
### 5 · Pull-quote (quote · move: EB Garamond italic · cream)
|
||||
|
||||
**Ground** `{colors.cream}`. **Composes** kicker-spike, pull-quote, tag-upper cite. **Focal** a EB Garamond **italic** `quote-pull` — a commit message, a reviewer line, or the thesis — with a small Inter uppercase cite (author · role) beneath. **Chrome** mono kicker. **Accent** none, or one coral mark. **Fixed** EB Garamond italic quote + uppercase cite. **Free** quote, attribution, layout + density. **Density** free.
|
||||
|
||||
### 6 · Closing / CTA (closer · move: coral voltage · cream or navy)
|
||||
|
||||
**Ground** `{colors.cream}` (or `{colors.navy}`). **Composes** display sign-off, coral-callout, optional contributor row (`assets/<login>.png` avatars + mono names). **Focal** a short EB Garamond sign-off with the one **coral-callout** (the CTA or full-bleed band) and, for a "shipped-by" close, a row of hairline-ringed avatar chips. **Chrome** mono index. **Accent** the single coral voltage. **Fixed** one coral moment, hairline avatar rings, sentence-case serif. **Free** sign-off, who ships, layout + density. **Density** free.
|
||||
|
||||
## Composition Rules
|
||||
|
||||
### Do
|
||||
|
||||
- Stand every frame on the **warm cream floor**; gather content on a **half-step tile** surface.
|
||||
- Set all display in **EB Garamond, sentence case**, negative-tracked; **Inter 400** body; **JetBrains Mono** kickers (uppercase, 0.16em, coral ✱) + code.
|
||||
- Ration **coral to one moment per frame** — CTA, inline link, OR full-bleed band.
|
||||
- Elevate with a **1px hairline** + at most one soft warm shadow; reserve **warm navy** for the code/terminal surface.
|
||||
- Lead with **one clear focal**; open regions with a **kicker-spike**. Fill the frame as the content wants.
|
||||
- Pair a EB Garamond figure with a **mono unit** for every stat; render code via the **`code-*` blocks** on the navy surface.
|
||||
|
||||
### Don't
|
||||
|
||||
- No pure white, no cool gray, no pure black; no fourth brand hue (navy is structural, syntax colors are decoration).
|
||||
- No heavy drop shadow, glow, gradient on content, or tilt — hairline elevation only.
|
||||
- No uppercase or title-case EB Garamond display; no sans headline; no serif label; no serif-set numeric unit.
|
||||
- No two coral moments in one frame; coral never sets a headline or body run.
|
||||
- Don't blow a headline past the measure — step the ramp down.
|
||||
|
||||
## Aspect-Ratio Behavior
|
||||
|
||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
||||
| ------------- | ----------------------- | --------------------------- | ---------------------- |
|
||||
| Cover | display left, index top | display top, index below | display, index corner |
|
||||
| Statement | line left/centered | line stacked | centered |
|
||||
| Code Surface | panel framed in cream | panel taller, fewer lines | panel centered, square |
|
||||
| Number/Impact | figure left | figure centered, taller | centered |
|
||||
| Pull-quote | quote left | quote stacked | centered |
|
||||
| Closing/CTA | sign-off + avatar row | sign-off top, avatars below | centered, avatars wrap |
|
||||
|
||||
`slide-pad` holds on the short edge; re-step display above the 1.4cqw floor. The code surface keeps
|
||||
its hairline + mono chrome on every ratio; the avatar row wraps rather than shrinks below legibility.
|
||||
|
||||
## Approved Real Entities
|
||||
|
||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
||||
placeholder. Contributor avatars come from the project's `assets/<login>.png` (staged from the PR's
|
||||
people graph); the ✱ spike, hairlines, and the code surface are CSS-only and need no external imagery.
|
||||
|
||||
## Numerals & Claims (hard rule)
|
||||
|
||||
Never invent figures, stats, diffs, or counts at frame scale. Render slots as `— figure —`,
|
||||
`{metric}`, `+N / −M`. Number-lockups, code panels, and impact stats carry placeholders until the
|
||||
script (from the PR ingest) supplies real values. Branch names, file counts, and `+/−` totals trace
|
||||
to the diff; commit/issue numbers are chrome.
|
||||
|
||||
## Pre-Render Self-Audit
|
||||
|
||||
- **Squint** — one EB Garamond display moment dominates.
|
||||
- **Trinity** — cream floor + tile step + ink voice; coral appears exactly once; warm navy only on the code surface; no cool gray / pure white / fourth hue.
|
||||
- **Type** — EB Garamond sentence-case display (negative-tracked); Inter 400 body; JetBrains Mono kickers (uppercase 0.16em, coral ✱) + code; ≥1.4cqw floor.
|
||||
- **Depth** — 1px hairline + at most one soft warm shadow; no heavy drop / glow / gradient / tilt; 6/8/12px radii.
|
||||
- **Code** — code rendered by a `code-*` block on the warm-navy surface; syntax coral/teal/amber; figures paired with a mono unit.
|
||||
- **Fabrication** — every numeral / diff traces to the PR, else placeholder.
|
||||
|
||||
## Known Gaps
|
||||
|
||||
- **Motion intentionally out of scope.** frame.md specifies composition only. Code editorial's motion register — short cross-dissolves, no overshoot/bounce/elastic, coral the only "draw-on", numbers count up, code types on line by line — lives in the skill's `references/motion.md`, not here.
|
||||
- **EB Garamond + Inter + JetBrains Mono ship as licensed local WOFF2 assets with this preset.** Copy the six files in this preset's `fonts/` folder (weights 400 + 700) into the project's `assets/fonts/` and add one `@font-face` per file, so Studio, snapshots, and renders resolve them offline without a first-run Google Fonts fetch. Nothing stages them for you. Author display at **weight 400** (700 reads as a heavy bold, off-register), and treat italic as the browser-synthesized slant (acceptable for the pull-quote register; add a real italic face only if a project leans hard on it). EB Garamond is a warm old-style serif (low contrast, humanist); if it ever fails, fall to Georgia or another old-style serif — never to a sans. CJK: Noto Serif SC (display) / Noto Sans SC (body) / Noto Sans Mono CJK (code); the sentence-case warmth carries when the serif drops.
|
||||
- **Syntax colors (teal `#5DB8A6` / amber `#E8A55A` / status) are fixed decoration**, declared in §Colors — they are NOT in the remixable `colors:` block, so a brand remix never repaints them.
|
||||
- **The code itself is the `code-*` registry blocks**, not this preset — this preset owns only the surrounding warm-navy surface + mono chrome.
|
||||
- **9:16 / 1:1 are guidance**; verify the legibility floor and that the cream/tile warmth + one-coral discipline hold per ratio.
|
||||
@@ -1,223 +0,0 @@
|
||||
<!--
|
||||
Code editorial — caption skin (preset-local source).
|
||||
|
||||
This is the preset's own lower-third karaoke caption look: a caption skin to read or
|
||||
copy, not to mount as is. A copy used as compositions/captions.html fills its three
|
||||
reserved holes and wraps the fragment in a <template>:
|
||||
· the empty GROUPS array → the engine's scene-aware word groups
|
||||
· the 0 duration + 0 width/height → real total + canvas size
|
||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
||||
|
||||
Token contract — reference ONLY this fixed vocab (set it from the
|
||||
project's frame.md; each var has a code-editorial literal
|
||||
fallback for standalone preview):
|
||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
||||
--font-body · --cap-band-top · --cap-band-height
|
||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
||||
|
||||
Visual: a warm cream card lifted off the page on a 1px hairline ink border + one soft
|
||||
warm shadow — code-editorial's only elevation language, no tilt, editorial and precise.
|
||||
EB Garamond sentence case, warm-ink glyphs. Upcoming words sit in faint ink; the current
|
||||
word reads in full ink under a 3px coral "voltage" underline (the brand's inline-link
|
||||
gesture, border-bottom = underline without layout shift); spoken words settle to solid
|
||||
ink, underline cleared.
|
||||
-->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
|
||||
<!-- the project's :root tokens go here -->
|
||||
<style data-brand-tokens></style>
|
||||
|
||||
<style>
|
||||
#captions-root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.caption-layer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 20;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
||||
.caption-stage {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: var(--cap-band-top, 900px);
|
||||
height: var(--cap-band-height, 180px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.caption-group {
|
||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* THE lifted cream card — warm cream fill, 1px hairline ink border, one soft warm
|
||||
shadow (code-editorial's only elevation language), 12px radius, no tilt */
|
||||
.caption-pill {
|
||||
max-width: 78%;
|
||||
padding: 22px 44px 26px;
|
||||
background: var(--cap-canvas, #faf9f5);
|
||||
border: 1px solid color-mix(in srgb, var(--cap-ink, #141413) 12%, transparent);
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
0 1px 3px color-mix(in srgb, var(--cap-ink, #141413) 8%, transparent),
|
||||
0 4px 16px color-mix(in srgb, var(--cap-ink, #141413) 4%, transparent);
|
||||
}
|
||||
.caption-line {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 0.12em 0.3em;
|
||||
font-family: var(--font-display, "EB Garamond"), Georgia, serif;
|
||||
font-weight: 500;
|
||||
font-size: clamp(40px, 4.4vw, 56px);
|
||||
line-height: 1.24;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* upcoming — faint warm ink, no underline */
|
||||
.caption-word {
|
||||
display: inline-block;
|
||||
padding: 0 0.02em 0.06em;
|
||||
color: color-mix(in srgb, var(--cap-ink, #141413) 40%, var(--cap-canvas, #faf9f5));
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
/* current word — full ink under the coral voltage underline (the brand's signature
|
||||
inline-link gesture; border-bottom = underline without layout shift) */
|
||||
.caption-word.is-active {
|
||||
color: var(--cap-ink, #141413);
|
||||
border-bottom: 3px solid var(--cap-accent, #cc785c);
|
||||
}
|
||||
/* already spoken — solid warm ink, underline cleared */
|
||||
.caption-word.is-spoken {
|
||||
color: var(--cap-ink, #141413);
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
|
||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
||||
/* brand / key term — the expressive EB Garamond italic */
|
||||
.caption-word.cap-brand {
|
||||
font-style: italic;
|
||||
color: var(--cap-ink, #141413);
|
||||
}
|
||||
.caption-word.cap-brand.is-active {
|
||||
border-bottom: 3px solid var(--cap-accent, #cc785c);
|
||||
}
|
||||
/* numerals / data — slightly recessed warm ink */
|
||||
.caption-word.cap-num {
|
||||
color: color-mix(in srgb, var(--cap-ink, #141413) 72%, var(--cap-canvas, #faf9f5));
|
||||
}
|
||||
/* emphasis — a persistent coral voltage underline */
|
||||
.caption-word.cap-cta {
|
||||
border-bottom: 3px solid var(--cap-accent-2, #b5654a);
|
||||
color: var(--cap-ink, #141413);
|
||||
}
|
||||
|
||||
@media (max-aspect-ratio: 9/16) {
|
||||
.caption-pill {
|
||||
max-width: 92%;
|
||||
}
|
||||
.caption-line {
|
||||
font-size: clamp(34px, 5.4vw, 46px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
id="captions-root"
|
||||
data-composition-id="captions"
|
||||
data-timeline-locked
|
||||
data-start="0"
|
||||
data-duration="0"
|
||||
data-fps="30"
|
||||
data-width="0"
|
||||
data-height="0"
|
||||
>
|
||||
<div class="caption-layer" aria-hidden="true">
|
||||
<div id="caption-stage" class="caption-stage"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// the scene-aware word groups + real duration go here.
|
||||
var GROUPS = [];
|
||||
var DURATION = 0;
|
||||
|
||||
(function () {
|
||||
var stage = document.getElementById("caption-stage");
|
||||
|
||||
// build DOM: one .caption-group per group, .caption-word per word
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.createElement("div");
|
||||
groupEl.className = "caption-group";
|
||||
groupEl.id = "caption-group-" + g;
|
||||
var pill = document.createElement("div");
|
||||
pill.className = "caption-pill";
|
||||
var line = document.createElement("div");
|
||||
line.className = "caption-line";
|
||||
(group.words || []).forEach(function (w, i) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "caption-word";
|
||||
span.id = "caption-word-" + g + "-" + i;
|
||||
span.textContent = String(w.text);
|
||||
line.appendChild(span);
|
||||
});
|
||||
pill.appendChild(line);
|
||||
groupEl.appendChild(pill);
|
||||
stage.appendChild(groupEl);
|
||||
});
|
||||
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
GROUPS.forEach(function (group, g) {
|
||||
var groupEl = document.getElementById("caption-group-" + g);
|
||||
var words = group.words || [];
|
||||
var next = GROUPS[g + 1];
|
||||
var isLast = g === GROUPS.length - 1;
|
||||
var start = Math.max(0, Number(group.start));
|
||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
||||
if (end <= start) end = start + 0.01;
|
||||
|
||||
// group on/off — exactly one group visible at a time
|
||||
tl.set(groupEl, { opacity: 1 }, start);
|
||||
tl.set(groupEl, { opacity: 0 }, end);
|
||||
|
||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
||||
words.forEach(function (w, i) {
|
||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
||||
var at = Math.max(start, Number(w.start));
|
||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
||||
tl.fromTo(el, { scale: 0.98 }, { scale: 1, duration: 0.16, ease: "power1.out" }, at);
|
||||
if (i + 1 < words.length) {
|
||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
||||
}
|
||||
});
|
||||
if (words.length) {
|
||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
||||
}
|
||||
});
|
||||
|
||||
// full-span anchor so the sub-comp timeline spans the whole video
|
||||
tl.to({}, { duration: DURATION }, 0);
|
||||
window.__timelines["captions"] = tl;
|
||||
})();
|
||||
</script>
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Loaded 100 of 417 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user