@opendirectory.dev/skills 0.1.67 → 0.1.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/registry.json +8 -0
- package/skills/vid-product-launch/README.md +138 -0
- package/skills/vid-product-launch/SKILL.md +713 -0
- package/skills/vid-product-launch/references/scene-library.md +896 -0
- package/skills/vid-product-launch/references/style-presets.md +180 -0
- package/skills/vid-product-launch/scripts/capture-frames.mjs +187 -0
- package/skills/vid-product-launch/scripts/export-video.sh +495 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: vid-product-launch
|
|
3
|
+
description: Generates a cinematic product launch video (MP4) from a product description and launch context. 5-section narrative arc — Tease, Build, Reveal, Proof, CTA — rendered as HTML/CSS in headless Chromium via Playwright, assembled with FFmpeg. 4 tone presets. 30/60/90 second durations. Trigger when user says "product launch video", "launch video", "product reveal video", "cinematic product video", "product announcement video", or "launch day video".
|
|
4
|
+
compatibility: [claude-code, gemini-cli, github-copilot]
|
|
5
|
+
author: OpenDirectory
|
|
6
|
+
version: 1.2.0
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# vid-product-launch
|
|
10
|
+
|
|
11
|
+
Generates a narrative MP4 product launch video from a product description and launch context.
|
|
12
|
+
Pipeline: HTML/CSS animations → headless Chromium (Playwright, frame-by-frame) → FFmpeg (H.264 MP4).
|
|
13
|
+
No React. No AI video APIs. No Python. Zero runtime cost beyond Playwright + FFmpeg.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Critical Rules (read before every generation)
|
|
18
|
+
|
|
19
|
+
1. **The tagline is not optional. Do not skip it.** It is the product's entire promise in 4–6 words. If the user did not provide it, derive one from the description — a sharp, active-voice distillation. Never write "[tagline here]" or leave it blank.
|
|
20
|
+
|
|
21
|
+
2. **The reveal moment must feel earned.** The tease and build sections exist to create tension. If the product name appears in the first 20% of the video, the narrative collapses. Never reveal the product name before the Reveal section.
|
|
22
|
+
|
|
23
|
+
3. **product_name font size: minimum 120px for 16:9, minimum 80px for 9:16.** The name must be the largest element at the reveal moment. If it isn't dominant, the reveal fails.
|
|
24
|
+
|
|
25
|
+
4. **One proof stat. Not a list.** If the user provides multiple stats, pick the strongest one. A list of 5 numbers destroys the punch. One oversized number creates it.
|
|
26
|
+
|
|
27
|
+
5. **Use `window.renderFrame(t)` — no CSS `@keyframes` for scene transitions.** CSS `currentTime` seeking is silently ignored for backward seeks in Chromium. The renderFrame function computes element styles directly from milliseconds. Playwright calls it once per frame.
|
|
28
|
+
|
|
29
|
+
6. **No `animation-delay` on ANY element.** Not needed with renderFrame. If you write `animation-delay`, stop — you are using the wrong architecture.
|
|
30
|
+
|
|
31
|
+
7. **`window.__videoReady = true` only inside `document.fonts.ready.then(...)`.** Never set synchronously.
|
|
32
|
+
|
|
33
|
+
8. **Expose `window.__stopPreview()`.** The rAF preview loop races with Playwright's evaluate/screenshot calls. The capture script calls `__stopPreview()` before the frame loop.
|
|
34
|
+
|
|
35
|
+
9. **Use `t < startMs` (not `t <= startMs`) in scene boundary checks.** `t <= 0` at frame 0 makes scene 1 black.
|
|
36
|
+
|
|
37
|
+
10. **Body = exact pixel dimensions.** Width and height are integers. No `%`, `vw/vh`, `rem`.
|
|
38
|
+
|
|
39
|
+
11. **Read `references/scene-library.md` AND `references/style-presets.md` before generating ANY HTML.**
|
|
40
|
+
|
|
41
|
+
12. **Never dump HTML in chat.** Save to file. Show summary only.
|
|
42
|
+
|
|
43
|
+
13. **Film grain canvas MUST be 240×135, not W×H.** Set `width="240" height="135"` on the canvas element, then stretch with `style="width:[W]px;height:[H]px"`. Full-resolution grain at 1920×1080 is 64× slower — 8MB of ImageData per frame — and will make 1800-frame exports take hours.
|
|
44
|
+
|
|
45
|
+
14. **The tease section MUST be dark (#000 or near-black), regardless of tone.** White backgrounds in the tease section read as demo slides, not product launch videos. The dark-to-light narrative arc (dark tease → dark build → product reveal) is how launch videos create drama. Even the `minimal` and `energetic` presets should use `background: #000` for tease-problem and tension-build scenes.
|
|
46
|
+
|
|
47
|
+
15. **Each tease word must be its own beat at 200px+ font size.** Word-by-word on a single line is not punchy enough. Each problem word (e.g. "Research." / "Write." / "Outreach." / "Repeat.") gets 1500–1800ms of screen time at `font-size: 200px; font-weight: 900` centered, one at a time. See scene-library.md `tease-words` pattern.
|
|
48
|
+
|
|
49
|
+
16. **Build section must show content that narrates the problem — not just particles.** Use the `terminal-card` pattern: 3 cards appear sequentially with a typewriter animation showing the manual work being done. Cards have `border: 1px solid rgba(255,255,255,0.07)`, blue dot accent, monospace text. Particles alone for 20 seconds is empty screen time.
|
|
50
|
+
|
|
51
|
+
17. **Dot-grid CSS background on dark scenes.** Add `background-image: radial-gradient(circle, rgba(255,255,255,0.035) 1px, transparent 1px); background-size: 60px 60px;` to build, reveal, proof, and CTA scenes. It costs zero compute and adds depth.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Step 1: Intake
|
|
56
|
+
|
|
57
|
+
**Required:**
|
|
58
|
+
- `product_name` — the name of the product or feature being launched
|
|
59
|
+
- `product_description` — 2–3 sentences: what it does, who it's for, key benefit
|
|
60
|
+
|
|
61
|
+
**Optional parameters and defaults:**
|
|
62
|
+
|
|
63
|
+
| Parameter | Default | Description |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| tagline | auto | 4–6 words — the product's core promise |
|
|
66
|
+
| problem_statement | auto-inferred | 1 sentence for tease section (the pain the product solves) |
|
|
67
|
+
| proof_stat | auto-inferred | Single metric (e.g. "500+ teams", "10× faster", "$2M saved") |
|
|
68
|
+
| cta | auto | URL or action phrase (e.g. "gooseworks.ai", "Join the waitlist") |
|
|
69
|
+
| launch_date | — | ISO date string (enables countdown-card scene) |
|
|
70
|
+
| tone | cinematic | cinematic / energetic / minimal / emotional |
|
|
71
|
+
| duration | 60 | 30 / 60 / 90 (seconds) |
|
|
72
|
+
| aspect_ratio | 16:9 | 16:9 (1920×1080) / 9:16 (1080×1920) |
|
|
73
|
+
| letterbox | false | Cinematic 2.35:1 black bars — 16:9 only |
|
|
74
|
+
| music | — | Path to audio file (mp3/m4a/wav) |
|
|
75
|
+
| fps | 30 | Frames per second (24, 30, or 60) |
|
|
76
|
+
|
|
77
|
+
**If `product_name` or `product_description` is missing, ask exactly:**
|
|
78
|
+
|
|
79
|
+
> "To create the launch video, I need two things:
|
|
80
|
+
>
|
|
81
|
+
> 1. **Product name** — what is the product called?
|
|
82
|
+
> 2. **Product description** — 2–3 sentences: what does it do, who is it for, what is the key benefit?
|
|
83
|
+
>
|
|
84
|
+
> Optional: tagline (4–6 words), proof stat (one number), CTA (URL or action phrase), tone (cinematic / energetic / minimal / emotional), duration (30 / 60 / 90s)."
|
|
85
|
+
|
|
86
|
+
If both are present → proceed to Step 2 immediately.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Step 2: Internal Architecture (never shown to user)
|
|
91
|
+
|
|
92
|
+
**1. Derive missing params:**
|
|
93
|
+
- **tagline** (if not provided): Write one from product_description. 4–6 words. Active voice. No filler ("The future of…", "Introducing…"). Derive the sharpest possible promise.
|
|
94
|
+
- **problem_statement** (if not provided): Infer from product_description. 1 sentence stating the pain. Never mentions the product name. Written to make the audience nod in recognition.
|
|
95
|
+
- **proof_stat** (if not provided): Infer from product_description. If no stat is available, use a credible scale indicator ("Trusted by 500+ teams", "From days to minutes").
|
|
96
|
+
- **cta** (if not provided): Use product domain if inferable from product_name, or "Learn more" as action phrase.
|
|
97
|
+
|
|
98
|
+
**2. Calculate section timing from `duration`:**
|
|
99
|
+
|
|
100
|
+
| Section | 30s | 60s | 90s | Start formula |
|
|
101
|
+
|---|---|---|---|---|
|
|
102
|
+
| Tease | 0–5s | 0–10s | 0–15s | 0ms |
|
|
103
|
+
| Build | 5–12s | 10–30s | 15–40s | Tease end |
|
|
104
|
+
| Reveal | 12–20s | 30–45s | 40–60s | Build end |
|
|
105
|
+
| Proof | 20–25s | 45–55s | 60–75s | Reveal end |
|
|
106
|
+
| CTA | 25–30s | 55–60s | 75–90s | Proof end |
|
|
107
|
+
|
|
108
|
+
Convert every boundary to milliseconds. Assign to constants:
|
|
109
|
+
```
|
|
110
|
+
TEASE_START_MS, TEASE_END_MS
|
|
111
|
+
BUILD_START_MS, BUILD_END_MS
|
|
112
|
+
REVEAL_START_MS, REVEAL_END_MS
|
|
113
|
+
PROOF_START_MS, PROOF_END_MS
|
|
114
|
+
CTA_START_MS, CTA_END_MS
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**3. Select scenes per section:**
|
|
118
|
+
|
|
119
|
+
- **Tease:** Always starts with `blackout-opener` (1500ms). Then `tease-words` (each problem keyword as its own 1600–1800ms beat at 200px+). If `launch_date` provided, optionally add `countdown-card` at end of tease.
|
|
120
|
+
- **Build:** `terminal-card` sequence (3 cards showing manual tasks being typed in) + optional closing accent line. Duration ≥ 30s gets all 3 cards. Duration = 30s uses 2 cards. Never use bare particles-only for more than 5s — the screen must show content that narrates the problem.
|
|
121
|
+
- **Reveal:** `reveal-hero` — always first in this section. Optionally followed by `tagline-card` if duration ≥ 60s.
|
|
122
|
+
- **Proof:** `proof-stat` if a numeric stat is available. Otherwise `feature-bullet`.
|
|
123
|
+
- **CTA:** `cta-card` — always, always last.
|
|
124
|
+
|
|
125
|
+
Typical scene counts: 30s = 4 scenes, 60s = 6–7 scenes, 90s = 7–8 scenes.
|
|
126
|
+
|
|
127
|
+
**4. Calculate per-scene timing:**
|
|
128
|
+
Within each section, divide time equally across scenes in that section.
|
|
129
|
+
Exception: `blackout-opener` always gets exactly the first 1500ms of Tease.
|
|
130
|
+
|
|
131
|
+
**5. Determine pixel dimensions:**
|
|
132
|
+
- `16:9` → W=1920, H=1080
|
|
133
|
+
- `9:16` → W=1080, H=1920
|
|
134
|
+
|
|
135
|
+
**6. Letterbox calculation (if enabled, 16:9 only):**
|
|
136
|
+
- 2.35:1 content height = W / 2.35 = 1920 / 2.35 ≈ 817px
|
|
137
|
+
- Bar height = (H - 817) / 2 = (1080 - 817) / 2 ≈ 132px
|
|
138
|
+
- Top bar: `top: 0; height: 132px`
|
|
139
|
+
- Bottom bar: `bottom: 0; height: 132px`
|
|
140
|
+
|
|
141
|
+
**7. Cinematic effects flags (by preset):**
|
|
142
|
+
- `cinematic`: film-grain=ON (canvas 240×135, opacity 0.025), vignette=ON, light-leak=ON (warm gold), dot-grid=ON on dark scenes
|
|
143
|
+
- `energetic`: film-grain=OFF, vignette=OFF, dot-grid=ON, white flash at reveal
|
|
144
|
+
- `minimal`: film-grain=ON (canvas 240×135, opacity 0.022), vignette=ON (subtle, 0.5 radial), dot-grid=ON, no light-leak — accent color = #4B9FFF (electric blue)
|
|
145
|
+
- `emotional`: film-grain=ON (canvas 240×135, opacity 0.018, warm tint blend), vignette=ON (soft), light-leak=ON (copper warm), dot-grid=OFF
|
|
146
|
+
|
|
147
|
+
**All presets: tease and build scenes ALWAYS use dark background (#000 or near-black), regardless of preset.**
|
|
148
|
+
|
|
149
|
+
**8. Embed `window.__sfxTimeline` in the HTML (always — even when no music is provided):**
|
|
150
|
+
|
|
151
|
+
The export script reads this array, synthesizes each SFX type natively with FFmpeg `aevalsrc`/`anoisesrc`, and places events at exact millisecond offsets using `adelay`. No external audio files required.
|
|
152
|
+
|
|
153
|
+
SFX type reference:
|
|
154
|
+
|
|
155
|
+
| Type | Sound | Duration | Notes |
|
|
156
|
+
|---|---|---|---|
|
|
157
|
+
| `word-hit` | Sub punch (50Hz) + transient click (2.2kHz) + noise burst | 180ms | One per tease word — 3 layers via amix |
|
|
158
|
+
| `type-sequence` | Mechanical keyboard clicks at 15Hz (sin^30 pulse envelope) | 1.9s | One per card — runs for full TYPE_DUR |
|
|
159
|
+
| `whoosh` | Two-band noise sweep (1.1kHz body + 4–8kHz air) | 700ms | At `BUILD_CLOSE` transition |
|
|
160
|
+
| `tension-riser` | Low rumble (250Hz) + sub tone growing with t/2.8 ramp | 2.9s | Start at `BUILD_CLOSE + 700ms` — peaks at `REVEAL_START` |
|
|
161
|
+
| `reveal-boom` | Sub (45Hz) + body (90Hz) + shimmer (4.5–11kHz) + 85ms echo | 900ms | Exactly at `REVEAL_START` — THE hit |
|
|
162
|
+
| `counter-tick` | Harmonic click (880Hz + 440Hz + 1760Hz + 2640Hz) | 80ms | 3 beats — decrescendo 0.32 → 0.26 → 0.20 |
|
|
163
|
+
| `cta-chime` | A major chord (440 + 554 + 659 + 880Hz) + aecho bell shimmer | 1.2s | Exactly at `CTA_START` |
|
|
164
|
+
|
|
165
|
+
SFX timing map — compute `ms` values from section constants, never hardcode:
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
window.__sfxTimeline = [
|
|
169
|
+
// ── TEASE: one word-hit per word beat, last louder ───────────────────────
|
|
170
|
+
{ ms: WORD_BEATS[0].start, sfx: 'word-hit', vol: 0.55 },
|
|
171
|
+
{ ms: WORD_BEATS[1].start, sfx: 'word-hit', vol: 0.55 },
|
|
172
|
+
{ ms: WORD_BEATS[2].start, sfx: 'word-hit', vol: 0.55 },
|
|
173
|
+
{ ms: WORD_BEATS[N].start, sfx: 'word-hit', vol: 0.65 }, // last word louder
|
|
174
|
+
|
|
175
|
+
// ── BUILD: type-sequence per card (1.9s, matches TYPE_DUR 1800ms) ────────
|
|
176
|
+
{ ms: CARDS[0].start + 180, sfx: 'type-sequence', vol: 0.28 },
|
|
177
|
+
{ ms: CARDS[1].start + 180, sfx: 'type-sequence', vol: 0.28 },
|
|
178
|
+
{ ms: CARDS[2].start + 180, sfx: 'type-sequence', vol: 0.28 },
|
|
179
|
+
|
|
180
|
+
// ── TRANSITION: whoosh then 2.9s riser peaking at REVEAL_START ───────────
|
|
181
|
+
{ ms: BUILD_CLOSE, sfx: 'whoosh', vol: 0.50 },
|
|
182
|
+
{ ms: BUILD_CLOSE + 700, sfx: 'tension-riser', vol: 0.35 },
|
|
183
|
+
|
|
184
|
+
// ── REVEAL: the cinematic hit ─────────────────────────────────────────────
|
|
185
|
+
{ ms: REVEAL_START, sfx: 'reveal-boom', vol: 0.88 },
|
|
186
|
+
|
|
187
|
+
// ── PROOF: harmonic ticks, decrescendo ───────────────────────────────────
|
|
188
|
+
{ ms: PROOF_START, sfx: 'counter-tick', vol: 0.32 },
|
|
189
|
+
{ ms: PROOF_START + 2200, sfx: 'counter-tick', vol: 0.26 },
|
|
190
|
+
{ ms: PROOF_START + 4400, sfx: 'counter-tick', vol: 0.20 },
|
|
191
|
+
|
|
192
|
+
// ── CTA: A major chord landing ────────────────────────────────────────────
|
|
193
|
+
{ ms: CTA_START, sfx: 'cta-chime', vol: 0.62 },
|
|
194
|
+
];
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Place this block immediately before the preview loop, after `window.renderFrame`.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Step 3: HTML Generation
|
|
202
|
+
|
|
203
|
+
Read `references/scene-library.md` AND `references/style-presets.md` before writing any code.
|
|
204
|
+
Use the exact CSS class names, HTML structure, and renderFrame patterns from those files.
|
|
205
|
+
|
|
206
|
+
**Required HTML skeleton:**
|
|
207
|
+
|
|
208
|
+
```html
|
|
209
|
+
<!DOCTYPE html>
|
|
210
|
+
<html lang="en">
|
|
211
|
+
<head>
|
|
212
|
+
<meta charset="UTF-8">
|
|
213
|
+
[font CDN link from style preset]
|
|
214
|
+
<style>
|
|
215
|
+
:root {
|
|
216
|
+
[all CSS tokens from style preset]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
220
|
+
html, body {
|
|
221
|
+
width: [W]px; height: [H]px;
|
|
222
|
+
overflow: hidden;
|
|
223
|
+
background: var(--bg);
|
|
224
|
+
font-family: var(--font-body);
|
|
225
|
+
position: relative;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.scene {
|
|
229
|
+
position: absolute;
|
|
230
|
+
inset: 0;
|
|
231
|
+
display: flex;
|
|
232
|
+
flex-direction: column;
|
|
233
|
+
align-items: center;
|
|
234
|
+
justify-content: center;
|
|
235
|
+
padding: 80px;
|
|
236
|
+
opacity: 0;
|
|
237
|
+
will-change: opacity, transform;
|
|
238
|
+
}
|
|
239
|
+
.scene-inner {
|
|
240
|
+
width: 100%;
|
|
241
|
+
max-width: 1200px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
[paste CSS from each scene type in scene-library.md]
|
|
245
|
+
</style>
|
|
246
|
+
</head>
|
|
247
|
+
<body>
|
|
248
|
+
|
|
249
|
+
<!-- Cinematic overlay elements — rendered on top of all scenes -->
|
|
250
|
+
[if film-grain: ON]
|
|
251
|
+
<canvas id="grain-overlay"
|
|
252
|
+
width="[W]" height="[H]"
|
|
253
|
+
style="position:fixed;inset:0;pointer-events:none;opacity:0.025;mix-blend-mode:overlay;z-index:50"></canvas>
|
|
254
|
+
[end if]
|
|
255
|
+
|
|
256
|
+
[if vignette: ON]
|
|
257
|
+
<div id="vignette-overlay"
|
|
258
|
+
style="position:fixed;inset:0;background:radial-gradient(ellipse at center,transparent 35%,rgba(0,0,0,0.65) 100%);pointer-events:none;z-index:51"></div>
|
|
259
|
+
[end if]
|
|
260
|
+
|
|
261
|
+
[if light-leak: ON]
|
|
262
|
+
<div id="light-leak"
|
|
263
|
+
style="position:fixed;inset:0;background:linear-gradient(135deg,rgba(255,220,140,0.5) 0%,rgba(255,255,255,0.3) 45%,transparent 70%);opacity:0;pointer-events:none;z-index:52"></div>
|
|
264
|
+
[end if]
|
|
265
|
+
|
|
266
|
+
[if letterbox: ON]
|
|
267
|
+
<div id="lbox-top" style="position:fixed;top:0;left:0;width:[W]px;height:132px;background:#000;z-index:100"></div>
|
|
268
|
+
<div id="lbox-bot" style="position:fixed;bottom:0;left:0;width:[W]px;height:132px;background:#000;z-index:100"></div>
|
|
269
|
+
[end if]
|
|
270
|
+
|
|
271
|
+
<!-- Scenes -->
|
|
272
|
+
[scene HTML from scene-library.md templates, one per selected scene]
|
|
273
|
+
|
|
274
|
+
<script>
|
|
275
|
+
window.__videoReady = false;
|
|
276
|
+
window.TOTAL_DURATION_MS = [duration * 1000];
|
|
277
|
+
|
|
278
|
+
// ── Section timing constants ──────────────────────────────────────────────────
|
|
279
|
+
const TEASE_START_MS = [N];
|
|
280
|
+
const TEASE_END_MS = [N];
|
|
281
|
+
const BUILD_START_MS = [N];
|
|
282
|
+
const BUILD_END_MS = [N];
|
|
283
|
+
const REVEAL_START_MS = [N];
|
|
284
|
+
const REVEAL_END_MS = [N];
|
|
285
|
+
const PROOF_START_MS = [N];
|
|
286
|
+
const PROOF_END_MS = [N];
|
|
287
|
+
const CTA_START_MS = [N];
|
|
288
|
+
const CTA_END_MS = [N];
|
|
289
|
+
|
|
290
|
+
// ── Animation helpers ─────────────────────────────────────────────────────────
|
|
291
|
+
function lerp(a, b, p) { return a + (b - a) * p; }
|
|
292
|
+
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
|
|
293
|
+
function easeOutCubic(t) { return 1 - Math.pow(1 - clamp(t, 0, 1), 3); }
|
|
294
|
+
function easeInOutCubic(t) { return t < 0.5 ? 4*t*t*t : 1 - Math.pow(-2*t+2,3)/2; }
|
|
295
|
+
|
|
296
|
+
function sceneState(t, startMs, endMs) {
|
|
297
|
+
// Standard scene envelope: fade-in, hold, fade-out
|
|
298
|
+
if (t < startMs || t >= endMs) return { opacity: 0, ty: 0 };
|
|
299
|
+
const prog = (t - startMs) / (endMs - startMs);
|
|
300
|
+
if (prog < 0.10) {
|
|
301
|
+
const p = easeOutCubic(prog / 0.10);
|
|
302
|
+
return { opacity: p, ty: lerp(20, 0, p) };
|
|
303
|
+
}
|
|
304
|
+
if (prog < 0.88) return { opacity: 1, ty: 0 };
|
|
305
|
+
// CTA scene: no exit fade (holds until end)
|
|
306
|
+
if (startMs === CTA_START_MS) return { opacity: 1, ty: 0 };
|
|
307
|
+
const p = (prog - 0.88) / 0.12;
|
|
308
|
+
return { opacity: 1 - easeOutCubic(p), ty: lerp(0, -10, p) };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function applySceneState(el, state) {
|
|
312
|
+
el.style.opacity = state.opacity.toFixed(3);
|
|
313
|
+
el.style.transform = state.ty !== 0 ? `translateY(${state.ty.toFixed(2)}px)` : '';
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── Film grain (if enabled) ───────────────────────────────────────────────────
|
|
317
|
+
[if film-grain: ON]
|
|
318
|
+
const grainCanvas = document.getElementById('grain-overlay');
|
|
319
|
+
const grainCtx = grainCanvas ? grainCanvas.getContext('2d') : null;
|
|
320
|
+
const W_GRAIN = [W], H_GRAIN = [H];
|
|
321
|
+
function renderGrain(t) {
|
|
322
|
+
if (!grainCtx) return;
|
|
323
|
+
const d = grainCtx.createImageData(W_GRAIN, H_GRAIN);
|
|
324
|
+
const buf = d.data;
|
|
325
|
+
let seed = (Math.floor(t * 37) ^ 0x5E3779B9) >>> 0;
|
|
326
|
+
for (let i = 0; i < buf.length; i += 4) {
|
|
327
|
+
seed = (seed ^ (seed >>> 13)) >>> 0;
|
|
328
|
+
seed = (seed * 1664525 + 1013904223) >>> 0;
|
|
329
|
+
const n = seed >>> 24;
|
|
330
|
+
buf[i] = buf[i+1] = buf[i+2] = n;
|
|
331
|
+
buf[i+3] = 255;
|
|
332
|
+
}
|
|
333
|
+
grainCtx.putImageData(d, 0, 0);
|
|
334
|
+
}
|
|
335
|
+
[end if]
|
|
336
|
+
|
|
337
|
+
// ── Light leak (if enabled) ───────────────────────────────────────────────────
|
|
338
|
+
[if light-leak: ON]
|
|
339
|
+
const lightLeakEl = document.getElementById('light-leak');
|
|
340
|
+
function renderLightLeak(t) {
|
|
341
|
+
if (!lightLeakEl) return;
|
|
342
|
+
const leakStart = REVEAL_START_MS;
|
|
343
|
+
const leakDur = 500; // ms
|
|
344
|
+
const lt = t - leakStart;
|
|
345
|
+
if (lt < 0 || lt > leakDur) { lightLeakEl.style.opacity = 0; return; }
|
|
346
|
+
const p = lt / leakDur;
|
|
347
|
+
const v = p < 0.35 ? p / 0.35 : 1 - (p - 0.35) / 0.65;
|
|
348
|
+
lightLeakEl.style.opacity = (v * 0.55).toFixed(3);
|
|
349
|
+
}
|
|
350
|
+
[end if]
|
|
351
|
+
|
|
352
|
+
// ── Main render function ──────────────────────────────────────────────────────
|
|
353
|
+
window.renderFrame = function(t) {
|
|
354
|
+
|
|
355
|
+
// Cinematic overlays
|
|
356
|
+
[if film-grain: ON] renderGrain(t); [end if]
|
|
357
|
+
[if light-leak: ON] renderLightLeak(t); [end if]
|
|
358
|
+
|
|
359
|
+
// ── TEASE SECTION ──
|
|
360
|
+
// blackout-opener
|
|
361
|
+
applySceneState(document.querySelector('.blackout-opener'), sceneState(t, TEASE_START_MS, TEASE_START_MS + 1500));
|
|
362
|
+
// Opener line appears at 40% of its scene
|
|
363
|
+
const openerLine = document.querySelector('.opener-line');
|
|
364
|
+
if (openerLine) {
|
|
365
|
+
const opP = clamp((t - (TEASE_START_MS + 600)) / 500, 0, 1);
|
|
366
|
+
openerLine.style.opacity = easeOutCubic(opP).toFixed(3);
|
|
367
|
+
openerLine.style.transform = `translateY(${lerp(8, 0, easeOutCubic(opP)).toFixed(2)}px)`;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// tease-problem
|
|
371
|
+
const tpScene = document.querySelector('.tease-problem');
|
|
372
|
+
const tpStart = TEASE_START_MS + 1500;
|
|
373
|
+
const tpEnd = TEASE_END_MS;
|
|
374
|
+
applySceneState(tpScene, sceneState(t, tpStart, tpEnd));
|
|
375
|
+
const words = tpScene ? tpScene.querySelectorAll('.word') : [];
|
|
376
|
+
if (words.length) {
|
|
377
|
+
const stagger = 180, revealStart = tpStart + (tpEnd - tpStart) * 0.15;
|
|
378
|
+
words.forEach((w, i) => {
|
|
379
|
+
const ws = revealStart + i * stagger;
|
|
380
|
+
const wp = clamp((t - ws) / 350, 0, 1);
|
|
381
|
+
w.style.opacity = easeOutCubic(wp).toFixed(3);
|
|
382
|
+
w.style.transform = `translateY(${lerp(20, 0, easeOutCubic(wp)).toFixed(2)}px)`;
|
|
383
|
+
});
|
|
384
|
+
const subLine = tpScene.querySelector('.sub-line');
|
|
385
|
+
if (subLine) {
|
|
386
|
+
const slStart = revealStart + words.length * stagger + 200;
|
|
387
|
+
const slP = clamp((t - slStart) / 400, 0, 1);
|
|
388
|
+
subLine.style.opacity = easeOutCubic(slP).toFixed(3);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
[if countdown-card exists in this video]
|
|
393
|
+
// countdown-card (in tease)
|
|
394
|
+
const cdScene = document.querySelector('.countdown-card');
|
|
395
|
+
if (cdScene) {
|
|
396
|
+
applySceneState(cdScene, sceneState(t, [CD_START_MS], [CD_END_MS]));
|
|
397
|
+
const cdLabel = document.getElementById('cd-label');
|
|
398
|
+
const cdGrid = document.getElementById('cd-grid');
|
|
399
|
+
const cdDate = document.getElementById('cd-date');
|
|
400
|
+
if (cdLabel) cdLabel.style.opacity = easeOutCubic(clamp((t - [CD_START_MS]) / 300, 0, 1)).toFixed(3);
|
|
401
|
+
if (cdGrid) cdGrid.style.opacity = easeOutCubic(clamp((t - ([CD_START_MS] + 200)) / 400, 0, 1)).toFixed(3);
|
|
402
|
+
if (cdDate) cdDate.style.opacity = easeOutCubic(clamp((t - ([CD_START_MS] + 600)) / 400, 0, 1)).toFixed(3);
|
|
403
|
+
}
|
|
404
|
+
[end if]
|
|
405
|
+
|
|
406
|
+
// ── BUILD SECTION ──
|
|
407
|
+
// tension-build
|
|
408
|
+
const tbScene = document.querySelector('.tension-build');
|
|
409
|
+
applySceneState(tbScene, sceneState(t, BUILD_START_MS, BUILD_END_MS));
|
|
410
|
+
if (tbScene) {
|
|
411
|
+
// Particle canvas — agent replaces accent hex with literal preset color
|
|
412
|
+
const tbCanvas = document.getElementById('tension-canvas');
|
|
413
|
+
if (tbCanvas && t >= BUILD_START_MS && t < BUILD_END_MS) {
|
|
414
|
+
const tbCtx = tbCanvas.getContext('2d');
|
|
415
|
+
if (!window.__tensionParticles) {
|
|
416
|
+
window.__tensionParticles = Array.from({length: 60}, () => ({
|
|
417
|
+
x: Math.random() * [W], y: Math.random() * [H],
|
|
418
|
+
tx: [W]/2 + (Math.random()-0.5)*60, ty: [H]/2 + (Math.random()-0.5)*60,
|
|
419
|
+
r: Math.random() * 2.5 + 1,
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
const prog = clamp((t - BUILD_START_MS) / (BUILD_END_MS - BUILD_START_MS), 0, 1);
|
|
423
|
+
tbCtx.clearRect(0, 0, [W], [H]);
|
|
424
|
+
window.__tensionParticles.forEach(p => {
|
|
425
|
+
const px = lerp(p.x, p.tx, easeInOutCubic(prog));
|
|
426
|
+
const py = lerp(p.y, p.ty, easeInOutCubic(prog));
|
|
427
|
+
tbCtx.beginPath();
|
|
428
|
+
tbCtx.arc(px, py, p.r, 0, Math.PI * 2);
|
|
429
|
+
tbCtx.fillStyle = '[ACCENT_HEX_FROM_PRESET]'; // replace with literal hex
|
|
430
|
+
tbCtx.globalAlpha = easeOutCubic(prog) * 0.55;
|
|
431
|
+
tbCtx.fill();
|
|
432
|
+
tbCtx.globalAlpha = 1;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
// Build counter (optional label)
|
|
436
|
+
const buildCounter = document.getElementById('build-counter');
|
|
437
|
+
const buildLabel = tbScene.querySelector('.build-label');
|
|
438
|
+
if (buildCounter) {
|
|
439
|
+
const cp = clamp((t - BUILD_START_MS) / ((BUILD_END_MS - BUILD_START_MS) * 0.65), 0, 1);
|
|
440
|
+
buildCounter.style.opacity = easeOutCubic(clamp((t - BUILD_START_MS) / 300, 0, 1)).toFixed(3);
|
|
441
|
+
}
|
|
442
|
+
if (buildLabel) {
|
|
443
|
+
buildLabel.style.opacity = easeOutCubic(clamp((t - (BUILD_START_MS + 600)) / 400, 0, 1)).toFixed(3);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── REVEAL SECTION ──
|
|
448
|
+
// reveal-hero
|
|
449
|
+
const rhScene = document.querySelector('.reveal-hero');
|
|
450
|
+
const rhStart = REVEAL_START_MS;
|
|
451
|
+
const rhEnd = [REVEAL_END_MS — full section if no tagline-card, else midpoint];
|
|
452
|
+
applySceneState(rhScene, sceneState(t, rhStart, rhEnd));
|
|
453
|
+
if (rhScene) {
|
|
454
|
+
const flash = document.getElementById('reveal-flash');
|
|
455
|
+
const nameEl = document.getElementById('product-name-el');
|
|
456
|
+
const tagEl = document.getElementById('tagline-el');
|
|
457
|
+
|
|
458
|
+
// Flash: triangle wave over 350ms at reveal start
|
|
459
|
+
if (flash) {
|
|
460
|
+
const fp = clamp((t - rhStart) / 350, 0, 1);
|
|
461
|
+
const fv = fp < 0.4 ? fp / 0.4 : 1 - (fp - 0.4) / 0.6;
|
|
462
|
+
flash.style.opacity = (fv * 0.65).toFixed(3);
|
|
463
|
+
}
|
|
464
|
+
// Product name: materialise (cinematic/emotional) or slam (energetic) or fade (minimal)
|
|
465
|
+
if (nameEl) {
|
|
466
|
+
const nameStart = rhStart + 220;
|
|
467
|
+
const nameDur = [700 for cinematic/emotional | 120 for energetic | 450 for minimal];
|
|
468
|
+
const np = clamp((t - nameStart) / nameDur, 0, 1);
|
|
469
|
+
nameEl.style.opacity = easeOutCubic(np).toFixed(3);
|
|
470
|
+
// cinematic/emotional: blur
|
|
471
|
+
// [if cinematic or emotional]
|
|
472
|
+
nameEl.style.filter = `blur(${lerp(10, 0, easeOutCubic(np)).toFixed(2)}px)`;
|
|
473
|
+
// [if energetic]
|
|
474
|
+
// nameEl.style.transform = `scale(${lerp(1.12, 1, easeOutCubic(np)).toFixed(4)})`;
|
|
475
|
+
}
|
|
476
|
+
// Tagline: after name
|
|
477
|
+
if (tagEl) {
|
|
478
|
+
const tagStart = rhStart + 220 + [nameDur] + 150;
|
|
479
|
+
const tp = clamp((t - tagStart) / 500, 0, 1);
|
|
480
|
+
tagEl.style.opacity = easeOutCubic(tp).toFixed(3);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
[if tagline-card exists]
|
|
485
|
+
// tagline-card (second beat of Reveal)
|
|
486
|
+
const tcScene = document.querySelector('.tagline-card');
|
|
487
|
+
applySceneState(tcScene, sceneState(t, [TC_START_MS], REVEAL_END_MS));
|
|
488
|
+
if (tcScene) {
|
|
489
|
+
const tcMain = document.getElementById('tagline-main');
|
|
490
|
+
const tcBar = document.getElementById('tagline-bar');
|
|
491
|
+
if (tcMain) tcMain.style.opacity = easeOutCubic(clamp((t - [TC_START_MS]) / 500, 0, 1)).toFixed(3);
|
|
492
|
+
if (tcBar) tcBar.style.transform = `scaleX(${easeOutCubic(clamp((t - ([TC_START_MS]+600)) / 200, 0, 1)).toFixed(3)})`;
|
|
493
|
+
}
|
|
494
|
+
[end if]
|
|
495
|
+
|
|
496
|
+
// ── PROOF SECTION ──
|
|
497
|
+
[if proof-stat]
|
|
498
|
+
const psScene = document.querySelector('.proof-stat');
|
|
499
|
+
applySceneState(psScene, sceneState(t, PROOF_START_MS, PROOF_END_MS));
|
|
500
|
+
if (psScene) {
|
|
501
|
+
const statEl = psScene.querySelector('.stat-value');
|
|
502
|
+
const counterEl = document.getElementById('stat-counter');
|
|
503
|
+
const labelEl = document.getElementById('stat-label');
|
|
504
|
+
const TARGET_NUM = [numeric value — agent fills this in];
|
|
505
|
+
|
|
506
|
+
const ap = clamp((t - PROOF_START_MS) / 200, 0, 1);
|
|
507
|
+
if (statEl) statEl.style.opacity = easeOutCubic(ap).toFixed(3);
|
|
508
|
+
|
|
509
|
+
const countDur = (PROOF_END_MS - PROOF_START_MS) * 0.60;
|
|
510
|
+
const cp = clamp((t - PROOF_START_MS) / countDur, 0, 1);
|
|
511
|
+
if (counterEl) counterEl.textContent = Math.round(easeOutCubic(cp) * TARGET_NUM).toLocaleString();
|
|
512
|
+
|
|
513
|
+
const labelStart = PROOF_START_MS + countDur + 80;
|
|
514
|
+
if (labelEl) labelEl.style.opacity = easeOutCubic(clamp((t - labelStart) / 400, 0, 1)).toFixed(3);
|
|
515
|
+
}
|
|
516
|
+
[end if]
|
|
517
|
+
|
|
518
|
+
[if feature-bullet]
|
|
519
|
+
const fbScene = document.querySelector('.feature-bullet');
|
|
520
|
+
applySceneState(fbScene, sceneState(t, PROOF_START_MS, PROOF_END_MS));
|
|
521
|
+
if (fbScene) {
|
|
522
|
+
const iconEl = document.getElementById('bullet-icon');
|
|
523
|
+
const mainEl = document.getElementById('bullet-main');
|
|
524
|
+
const subEl = document.getElementById('bullet-sub');
|
|
525
|
+
if (iconEl) iconEl.style.transform = `scaleX(${easeOutCubic(clamp((t - PROOF_START_MS) / 200, 0, 1)).toFixed(3)})`;
|
|
526
|
+
if (mainEl) mainEl.style.opacity = easeOutCubic(clamp((t - (PROOF_START_MS + 200)) / 500, 0, 1)).toFixed(3);
|
|
527
|
+
if (subEl) subEl.style.opacity = easeOutCubic(clamp((t - (PROOF_START_MS + 700)) / 400, 0, 1)).toFixed(3);
|
|
528
|
+
}
|
|
529
|
+
[end if]
|
|
530
|
+
|
|
531
|
+
// ── CTA SECTION ──
|
|
532
|
+
const ctaScene = document.querySelector('.cta-card');
|
|
533
|
+
applySceneState(ctaScene, sceneState(t, CTA_START_MS, CTA_END_MS));
|
|
534
|
+
if (ctaScene) {
|
|
535
|
+
const actionEl = document.getElementById('cta-action');
|
|
536
|
+
const urlEl = document.getElementById('cta-url');
|
|
537
|
+
const accentEl = document.getElementById('cta-accent');
|
|
538
|
+
const subEl = document.getElementById('cta-sub');
|
|
539
|
+
if (actionEl) actionEl.style.opacity = easeOutCubic(clamp((t - CTA_START_MS) / 350, 0, 1)).toFixed(3);
|
|
540
|
+
if (urlEl) {
|
|
541
|
+
const up = clamp((t - (CTA_START_MS + 250)) / 500, 0, 1);
|
|
542
|
+
urlEl.style.opacity = easeOutCubic(up).toFixed(3);
|
|
543
|
+
// cinematic/emotional: blur reveal
|
|
544
|
+
urlEl.style.filter = `blur(${lerp(6, 0, easeOutCubic(up)).toFixed(2)}px)`;
|
|
545
|
+
}
|
|
546
|
+
if (accentEl) accentEl.style.transform = `scaleX(${easeOutCubic(clamp((t-(CTA_START_MS+800))/200,0,1)).toFixed(3)})`;
|
|
547
|
+
if (subEl) subEl.style.opacity = easeOutCubic(clamp((t-(CTA_START_MS+1050))/400,0,1)).toFixed(3);
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// ── Preview loop (stopped by Playwright before frame capture) ─────────────────
|
|
552
|
+
let __previewActive = false;
|
|
553
|
+
let __previewRafId = null;
|
|
554
|
+
window.__stopPreview = function() {
|
|
555
|
+
__previewActive = false;
|
|
556
|
+
if (__previewRafId !== null) { cancelAnimationFrame(__previewRafId); __previewRafId = null; }
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
document.fonts.ready.then(() => {
|
|
560
|
+
window.renderFrame(0);
|
|
561
|
+
window.__videoReady = true;
|
|
562
|
+
__previewActive = true;
|
|
563
|
+
const startTime = performance.now();
|
|
564
|
+
function tick() {
|
|
565
|
+
if (!__previewActive) return;
|
|
566
|
+
const elapsed = performance.now() - startTime;
|
|
567
|
+
if (elapsed < window.TOTAL_DURATION_MS) {
|
|
568
|
+
window.renderFrame(elapsed);
|
|
569
|
+
__previewRafId = requestAnimationFrame(tick);
|
|
570
|
+
} else {
|
|
571
|
+
window.renderFrame(window.TOTAL_DURATION_MS - 1);
|
|
572
|
+
__previewActive = false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
__previewRafId = requestAnimationFrame(tick);
|
|
576
|
+
});
|
|
577
|
+
</script>
|
|
578
|
+
</body>
|
|
579
|
+
</html>
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
**Design quality rules:**
|
|
583
|
+
- `product_name` font size: ≥120px for 16:9, ≥80px for 9:16. Non-negotiable.
|
|
584
|
+
- Headline letter-spacing: `var(--tracking-tight)` — always.
|
|
585
|
+
- One accent color highlight per scene.
|
|
586
|
+
- `transform-origin: center center` on every element that uses `transform`.
|
|
587
|
+
- Padding inside `.scene`: minimum 80px — text must never touch viewport edges.
|
|
588
|
+
- Cinematic preset: all colors from `--` token variables. No free hex except on canvas fillStyle (replace `[ACCENT_HEX_FROM_PRESET]` with literal hex from preset).
|
|
589
|
+
- Never write placeholder text ("Your tagline here", "TBD", "[INSERT STAT]").
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Step 4: Self-QA (fix every failure before Step 5)
|
|
594
|
+
|
|
595
|
+
**Narrative structure:**
|
|
596
|
+
- [ ] product_name does NOT appear before `REVEAL_START_MS`
|
|
597
|
+
- [ ] Tagline is a real 4–6 word phrase — not a placeholder or generic line
|
|
598
|
+
- [ ] `reveal-hero` scene falls within `REVEAL_START_MS` to `REVEAL_END_MS`
|
|
599
|
+
- [ ] `cta-card` is the final scene, covering `CTA_START_MS` to `CTA_END_MS`
|
|
600
|
+
- [ ] CTA contains actual URL or action phrase — not "[your URL here]"
|
|
601
|
+
|
|
602
|
+
**renderFrame correctness:**
|
|
603
|
+
- [ ] `window.renderFrame(t)` is a pure function — no side effects outside style writes
|
|
604
|
+
- [ ] Scene boundary uses `t < startMs` (not `t <= startMs`)
|
|
605
|
+
- [ ] Zero `animation-delay` or `@keyframes` for scene transitions
|
|
606
|
+
- [ ] `window.__stopPreview()` exposed and checks `__previewActive` flag
|
|
607
|
+
- [ ] Film grain canvas re-seeds from `t` each frame (different grain per frame)
|
|
608
|
+
|
|
609
|
+
**Readiness signal:**
|
|
610
|
+
- [ ] `window.__videoReady = false` declared before fonts.ready
|
|
611
|
+
- [ ] `window.__videoReady = true` set ONLY inside `document.fonts.ready.then(...)`
|
|
612
|
+
- [ ] `window.renderFrame(0)` called inside fonts.ready BEFORE setting `__videoReady = true`
|
|
613
|
+
|
|
614
|
+
**Layout:**
|
|
615
|
+
- [ ] `html, body` use exact pixel dimensions (`[W]px`, `[H]px`)
|
|
616
|
+
- [ ] No `%`, `vw`, `vh`, `rem` on body width/height
|
|
617
|
+
- [ ] `overflow: hidden` on `html, body`
|
|
618
|
+
- [ ] All scenes `position: absolute; inset: 0`
|
|
619
|
+
|
|
620
|
+
**Cinematic effects:**
|
|
621
|
+
- [ ] Film grain canvas: `id="grain-overlay"`, `position: fixed`, above all scenes
|
|
622
|
+
- [ ] Vignette div: `id="vignette-overlay"`, `position: fixed`, above all scenes
|
|
623
|
+
- [ ] Light leak div: `id="light-leak"`, `opacity: 0` initially, fires at `REVEAL_START_MS`
|
|
624
|
+
- [ ] Letterbox (if enabled): exact bar heights for 2.35:1 ratio, `z-index: 100`
|
|
625
|
+
|
|
626
|
+
**Design:**
|
|
627
|
+
- [ ] `product_name` font size ≥ 120px (16:9) or ≥ 80px (9:16)
|
|
628
|
+
- [ ] All colors from preset token variables
|
|
629
|
+
- [ ] All fonts from preset CDN link
|
|
630
|
+
- [ ] No placeholder text anywhere
|
|
631
|
+
|
|
632
|
+
**Sound effects:**
|
|
633
|
+
- [ ] `window.__sfxTimeline` present before the preview loop
|
|
634
|
+
- [ ] `ms` values derived from actual timing constants — never hardcoded guesses
|
|
635
|
+
- [ ] `reveal-boom` fires at exactly `REVEAL_START`
|
|
636
|
+
- [ ] `cta-chime` fires at exactly `CTA_START`
|
|
637
|
+
- [ ] No SFX event fires before 1000ms (blackout silence must be true silence)
|
|
638
|
+
- [ ] Last tease word has `vol: 0.65` (louder than others at `0.55`)
|
|
639
|
+
|
|
640
|
+
---
|
|
641
|
+
|
|
642
|
+
## Step 5: Export
|
|
643
|
+
|
|
644
|
+
Determine slug from `product_name` (kebab-case, ≤30 chars):
|
|
645
|
+
|
|
646
|
+
```bash
|
|
647
|
+
mkdir -p launch/[slug]
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Save HTML: `launch/[slug]/product-launch.html`
|
|
651
|
+
|
|
652
|
+
Open for browser preview:
|
|
653
|
+
```bash
|
|
654
|
+
open launch/[slug]/product-launch.html
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Run export (replace `[skill-root]` with path to this skill's directory):
|
|
658
|
+
```bash
|
|
659
|
+
bash [skill-root]/scripts/export-video.sh \
|
|
660
|
+
launch/[slug]/product-launch.html \
|
|
661
|
+
launch/[slug]/product-launch.mp4 \
|
|
662
|
+
--duration [total_duration] \
|
|
663
|
+
--fps [fps] \
|
|
664
|
+
--width [W] \
|
|
665
|
+
--height [H] \
|
|
666
|
+
[--music path/to/audio.mp3]
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
First run installs Playwright (~200MB Chromium, cached after first use) and verifies FFmpeg.
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## Step 6: Output Summary
|
|
674
|
+
|
|
675
|
+
```
|
|
676
|
+
## Launch Video: [product_name]
|
|
677
|
+
Date: [YYYY-MM-DD] | Tone: [tone] | Duration: [N]s | Aspect: [ratio]
|
|
678
|
+
Sections: Tease [0–Ns] → Build [N–Ns] → Reveal [N–Ns] → Proof [N–Ns] → CTA [N–Ns]
|
|
679
|
+
|
|
680
|
+
Narrative
|
|
681
|
+
Tease: [problem_statement — 1 sentence]
|
|
682
|
+
Reveal: [product_name] — "[tagline]"
|
|
683
|
+
Proof: [proof_stat]
|
|
684
|
+
CTA: [cta]
|
|
685
|
+
|
|
686
|
+
Files
|
|
687
|
+
Source: launch/[slug]/product-launch.html
|
|
688
|
+
Output: launch/[slug]/product-launch.mp4
|
|
689
|
+
|
|
690
|
+
Checklist
|
|
691
|
+
- [ ] Product name does not appear before reveal section
|
|
692
|
+
- [ ] Reveal moment feels distinct — flash/materialise visible
|
|
693
|
+
- [ ] Tagline is present and legible
|
|
694
|
+
- [ ] Proof stat counter animates
|
|
695
|
+
- [ ] CTA URL readable at final frame
|
|
696
|
+
- [ ] No blank frames
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## Prompt Tips (show when user asks for guidance)
|
|
702
|
+
|
|
703
|
+
> "Write the tagline yourself. It is the most important 4–6 words in the video — don't AI-generate it lazily."
|
|
704
|
+
>
|
|
705
|
+
> "The reveal moment is everything. Everything before it builds tension; the reveal must feel earned."
|
|
706
|
+
>
|
|
707
|
+
> "One benefit in the proof section. Trying to show 5 features kills launch video pacing."
|
|
708
|
+
>
|
|
709
|
+
> "Match tone to your market: cinematic for B2C premium / Series A+, energetic for dev tools and SaaS, minimal for design-forward products, emotional for consumer / mission-driven."
|
|
710
|
+
>
|
|
711
|
+
> ✅ Good: "Product launch video, 60 seconds. Product: Gooseworks. Description: AI workspace that automates research, content creation, and outreach for growth teams. Tagline: 'Work at AI speed.' Tone: minimal. Proof: '500+ growth teams, 10× output.' CTA: 'Join the waitlist at gooseworks.ai.' Aspect: 16:9."
|
|
712
|
+
>
|
|
713
|
+
> ❌ Bad: "launch video for our new product"
|