@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,896 @@
|
|
|
1
|
+
# Scene Library — vid-product-launch
|
|
2
|
+
|
|
3
|
+
11 scene types for the 5-section narrative arc. Read this file before generating HTML.
|
|
4
|
+
Each entry includes: purpose, narrative placement, CSS, HTML template, and renderFrame logic.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Scene Placement by Narrative Section
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
TEASE (builds problem / curiosity):
|
|
12
|
+
blackout-opener — always first (0–1500ms of tease)
|
|
13
|
+
tease-words ★ PRIMARY — each problem keyword as its own 200px+ beat
|
|
14
|
+
tease-problem — fallback: all words together (shorter durations only)
|
|
15
|
+
countdown-card — if launch_date provided (can sit in tease OR cta)
|
|
16
|
+
|
|
17
|
+
BUILD (tension rising, solution approaching):
|
|
18
|
+
terminal-card ★ PRIMARY — 3 typed cards showing manual work being done
|
|
19
|
+
tension-build — supplemental: particles/canvas (max 5s alone, never 20s)
|
|
20
|
+
|
|
21
|
+
REVEAL (the hero moment):
|
|
22
|
+
reveal-hero — product name + tagline. The slam / materialise.
|
|
23
|
+
tagline-card — optional second reveal beat for standalone tagline
|
|
24
|
+
|
|
25
|
+
PROOF (one result, one truth):
|
|
26
|
+
proof-stat — oversized single metric with counter animation
|
|
27
|
+
feature-bullet — single capability with supporting line
|
|
28
|
+
|
|
29
|
+
CTA (close):
|
|
30
|
+
cta-card — URL large, action text. Final frame. Always last.
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
★ = preferred. Use these first. Old alternatives still valid for 30s or special cases.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Visual Quality Rules (apply to every scene)
|
|
38
|
+
|
|
39
|
+
- **Dark tease/build**: `background: #000` or `#080808` always — even in minimal/energetic presets.
|
|
40
|
+
- **Dot-grid on dark scenes**: `background-image: radial-gradient(circle, rgba(255,255,255,0.035) 1px, transparent 1px); background-size: 60px 60px;` on build, reveal, proof, cta.
|
|
41
|
+
- **Film grain canvas**: `width="240" height="135"` (stretched via CSS to W×H). Never full resolution.
|
|
42
|
+
- **Vignette**: `radial-gradient(ellipse at center, transparent 28%, rgba(0,0,0,0.65) 100%)` fixed overlay.
|
|
43
|
+
- **Accent glow**: blue/gold lines get `box-shadow: 0 0 18px rgba([accent-rgb],0.45)`.
|
|
44
|
+
- **Typography scale**: tease words ≥ 200px. Product name ≥ 220px. Proof stat ≥ 240px. CTA URL ≥ 88px.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## SFX Cue Reference
|
|
49
|
+
|
|
50
|
+
Six synthesized SFX types. The export script generates all from FFmpeg `aevalsrc`/`anoisesrc` — no audio files required. Embed `window.__sfxTimeline` in the HTML before the preview loop.
|
|
51
|
+
|
|
52
|
+
| Scene | SFX Type | Trigger Moment | Vol | Character |
|
|
53
|
+
|---|---|---|---|---|
|
|
54
|
+
| `blackout-opener` | _(silence)_ | — | — | True silence — no SFX before 1000ms |
|
|
55
|
+
| `tease-words` | `word-hit` | Each `WORD_BEATS[i].start` | 0.55 (last: 0.65) | Sub punch (50Hz) + transient click (2.2kHz) + noise burst — 180ms, 3 layers |
|
|
56
|
+
| `terminal-card` | `type-sequence` | `CARDS[i].start + 180` | 0.28 | 15Hz mechanical keyboard clicks for 1.9s — sin^30 pulse × 1200Hz click + 200Hz thud |
|
|
57
|
+
| `terminal-card` (fade) | `whoosh` | `BUILD_CLOSE` | 0.50 | Two-band noise sweep — 1.1kHz body + 4–8kHz air, 700ms |
|
|
58
|
+
| _(transition)_ | `tension-riser` | `BUILD_CLOSE + 700` | 0.35 | 2.9s ascending low rumble + sub tone ramp — peaks at REVEAL_START |
|
|
59
|
+
| `reveal-hero` | `reveal-boom` | `REVEAL_START` (exact) | 0.88 | Sub (45Hz) + body (90Hz) + shimmer (4.5–11kHz) + 85ms echo — 900ms |
|
|
60
|
+
| `proof-stat` | `counter-tick` | `PROOF_START`, `+2200`, `+4400` | 0.32 → 0.20 | Harmonic click — 880Hz + 440Hz + 1760Hz + 2640Hz, 80ms. Decrescendo |
|
|
61
|
+
| `cta-card` | `cta-chime` | `CTA_START` (exact) | 0.62 | A major chord (440 + 554 + 659 + 880Hz) + aecho bell shimmer — 1.2s |
|
|
62
|
+
|
|
63
|
+
**Mixing rules:**
|
|
64
|
+
- If `--music` flag provided: music plays at 22% volume under SFX
|
|
65
|
+
- SFX do not duck background music — they sit on top
|
|
66
|
+
- `adelay` places each SFX at the exact millisecond — timing is frame-accurate
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 1. blackout-opener
|
|
71
|
+
|
|
72
|
+
**Purpose:** Silence before the storm. 1–2 seconds of pure darkness, then a single line
|
|
73
|
+
emerges from black. Sets cinematic tone immediately.
|
|
74
|
+
|
|
75
|
+
**CSS:**
|
|
76
|
+
```css
|
|
77
|
+
.blackout-opener {
|
|
78
|
+
background: #000; /* override preset bg — always black */
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
}
|
|
82
|
+
.blackout-opener .opener-line {
|
|
83
|
+
font-family: var(--font-body);
|
|
84
|
+
font-size: var(--body-size);
|
|
85
|
+
font-weight: 400;
|
|
86
|
+
letter-spacing: var(--tracking-wide);
|
|
87
|
+
text-transform: uppercase;
|
|
88
|
+
color: var(--text-secondary);
|
|
89
|
+
opacity: 0;
|
|
90
|
+
transform: translateY(8px);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**HTML template:**
|
|
95
|
+
```html
|
|
96
|
+
<div class="scene blackout-opener">
|
|
97
|
+
<div class="scene-inner" style="text-align:center">
|
|
98
|
+
<p class="opener-line">[one line — year, or a short evocative phrase, or empty for pure black]</p>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**renderFrame notes:**
|
|
104
|
+
- Scene fades in (standard sceneState opacity).
|
|
105
|
+
- `.opener-line` appears at 40% of scene duration: `opacity 0→1, translateY 8px→0, 600ms`.
|
|
106
|
+
- For pure black opener (no text): omit the `<p>` element entirely.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 2. tease-problem
|
|
111
|
+
|
|
112
|
+
**Purpose:** The problem. Revealed word-by-word in all-caps. Audience recognises themselves.
|
|
113
|
+
Never names the product. Ends on the pain, not the solution.
|
|
114
|
+
|
|
115
|
+
**CSS:**
|
|
116
|
+
```css
|
|
117
|
+
.tease-problem {
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
}
|
|
121
|
+
.tease-problem .problem-text {
|
|
122
|
+
font-family: var(--font-display);
|
|
123
|
+
font-size: 52px;
|
|
124
|
+
font-weight: 700;
|
|
125
|
+
letter-spacing: var(--tracking-wide);
|
|
126
|
+
text-transform: uppercase;
|
|
127
|
+
color: var(--text-primary);
|
|
128
|
+
text-align: center;
|
|
129
|
+
line-height: 1.1;
|
|
130
|
+
max-width: 800px;
|
|
131
|
+
}
|
|
132
|
+
.tease-problem .problem-text .word {
|
|
133
|
+
display: inline-block;
|
|
134
|
+
opacity: 0;
|
|
135
|
+
transform: translateY(20px);
|
|
136
|
+
margin-right: 0.2em;
|
|
137
|
+
}
|
|
138
|
+
.tease-problem .sub-line {
|
|
139
|
+
font-family: var(--font-body);
|
|
140
|
+
font-size: var(--body-size);
|
|
141
|
+
color: var(--text-secondary);
|
|
142
|
+
text-align: center;
|
|
143
|
+
margin-top: 24px;
|
|
144
|
+
letter-spacing: var(--tracking-wide);
|
|
145
|
+
text-transform: uppercase;
|
|
146
|
+
opacity: 0;
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**HTML template:**
|
|
151
|
+
```html
|
|
152
|
+
<div class="scene tease-problem">
|
|
153
|
+
<div class="scene-inner" style="text-align:center">
|
|
154
|
+
<p class="problem-text">
|
|
155
|
+
<!-- Agent: wrap each word in a span.word -->
|
|
156
|
+
<span class="word">You've</span>
|
|
157
|
+
<span class="word">been</span>
|
|
158
|
+
<span class="word">doing</span>
|
|
159
|
+
<span class="word">this</span>
|
|
160
|
+
<span class="word">the</span>
|
|
161
|
+
<span class="word">hard</span>
|
|
162
|
+
<span class="word">way.</span>
|
|
163
|
+
</p>
|
|
164
|
+
<p class="sub-line">[optional: short clarifying line, 4–6 words max]</p>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**renderFrame notes:**
|
|
170
|
+
```javascript
|
|
171
|
+
// Word-by-word reveal: stagger 180ms per word, starting at 15% of scene duration
|
|
172
|
+
function wordReveal(words, t, sceneStart, sceneEnd) {
|
|
173
|
+
const stagger = 180; // ms between words
|
|
174
|
+
const revealStart = sceneStart + (sceneEnd - sceneStart) * 0.15;
|
|
175
|
+
words.forEach((w, i) => {
|
|
176
|
+
const wStart = revealStart + i * stagger;
|
|
177
|
+
const wDur = 350;
|
|
178
|
+
const p = clamp((t - wStart) / wDur, 0, 1);
|
|
179
|
+
w.style.opacity = easeOutCubic(p);
|
|
180
|
+
w.style.transform = `translateY(${lerp(20, 0, easeOutCubic(p)).toFixed(2)}px)`;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// Sub-line appears after all words: delay = revealStart + wordCount * stagger + 200ms
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## 3. tension-build
|
|
189
|
+
|
|
190
|
+
**Purpose:** Rising action before the reveal. No product name. Visual energy converges
|
|
191
|
+
toward the center — particles, a filling bar, or a count-up to a tipping point.
|
|
192
|
+
|
|
193
|
+
**CSS (particles variant — default):**
|
|
194
|
+
```css
|
|
195
|
+
.tension-build {
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
}
|
|
199
|
+
.tension-build canvas.particles {
|
|
200
|
+
position: absolute;
|
|
201
|
+
inset: 0;
|
|
202
|
+
opacity: 0.8;
|
|
203
|
+
}
|
|
204
|
+
.tension-build .build-label {
|
|
205
|
+
font-family: var(--font-body);
|
|
206
|
+
font-size: var(--body-size);
|
|
207
|
+
letter-spacing: var(--tracking-wide);
|
|
208
|
+
text-transform: uppercase;
|
|
209
|
+
color: var(--text-secondary);
|
|
210
|
+
text-align: center;
|
|
211
|
+
position: relative;
|
|
212
|
+
z-index: 1;
|
|
213
|
+
opacity: 0;
|
|
214
|
+
}
|
|
215
|
+
.tension-build .build-counter {
|
|
216
|
+
font-family: var(--font-display);
|
|
217
|
+
font-size: 80px;
|
|
218
|
+
font-weight: 700;
|
|
219
|
+
color: var(--text-primary);
|
|
220
|
+
text-align: center;
|
|
221
|
+
position: relative;
|
|
222
|
+
z-index: 1;
|
|
223
|
+
margin-bottom: 16px;
|
|
224
|
+
opacity: 0;
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**HTML template:**
|
|
229
|
+
```html
|
|
230
|
+
<div class="scene tension-build">
|
|
231
|
+
<canvas class="particles" id="tension-canvas" width="[W]" height="[H]"></canvas>
|
|
232
|
+
<div class="scene-inner" style="text-align:center;pointer-events:none">
|
|
233
|
+
<p class="build-counter" id="build-counter">0</p>
|
|
234
|
+
<p class="build-label">[optional: short context, e.g. "teams wasting hours daily"]</p>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**renderFrame notes:**
|
|
240
|
+
```javascript
|
|
241
|
+
// Particle convergence: N particles start spread, converge to center over scene duration
|
|
242
|
+
// Initialise once, then animate position each frame
|
|
243
|
+
function renderParticles(ctx, t, sceneStart, sceneEnd, W, H) {
|
|
244
|
+
if (!window.__particles) {
|
|
245
|
+
window.__particles = Array.from({length: 60}, () => ({
|
|
246
|
+
x: Math.random() * W, y: Math.random() * H,
|
|
247
|
+
tx: W/2 + (Math.random()-0.5)*40, ty: H/2 + (Math.random()-0.5)*40,
|
|
248
|
+
r: Math.random() * 2 + 1,
|
|
249
|
+
}));
|
|
250
|
+
}
|
|
251
|
+
const prog = clamp((t - sceneStart) / (sceneEnd - sceneStart), 0, 1);
|
|
252
|
+
const ease = easeOutCubic(prog);
|
|
253
|
+
ctx.clearRect(0, 0, W, H);
|
|
254
|
+
ctx.fillStyle = 'var(--accent)'; // resolved at runtime; use literal hex from preset
|
|
255
|
+
window.__particles.forEach(p => {
|
|
256
|
+
const px = lerp(p.x, p.tx, ease);
|
|
257
|
+
const py = lerp(p.y, p.ty, ease);
|
|
258
|
+
ctx.beginPath();
|
|
259
|
+
ctx.arc(px, py, p.r, 0, Math.PI * 2);
|
|
260
|
+
ctx.globalAlpha = ease * 0.6;
|
|
261
|
+
ctx.fill();
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// Build counter: counts up from 0 to a number the agent specifies
|
|
265
|
+
// Appears at 10% of scene, completes at 70%
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 4. reveal-hero
|
|
271
|
+
|
|
272
|
+
**Purpose:** THE moment. Product name slams or materialises into view.
|
|
273
|
+
Everything before this built toward this frame. Make it feel earned.
|
|
274
|
+
|
|
275
|
+
**CSS:**
|
|
276
|
+
```css
|
|
277
|
+
.reveal-hero {
|
|
278
|
+
align-items: center;
|
|
279
|
+
justify-content: center;
|
|
280
|
+
}
|
|
281
|
+
.reveal-hero .flash-overlay {
|
|
282
|
+
position: absolute;
|
|
283
|
+
inset: 0;
|
|
284
|
+
background: #FFFFFF; /* cinematic/emotional: warm #FFF8F0; energetic: #FFFFFF; minimal: #FFFFFF */
|
|
285
|
+
opacity: 0;
|
|
286
|
+
pointer-events: none;
|
|
287
|
+
z-index: 2;
|
|
288
|
+
}
|
|
289
|
+
.reveal-hero .product-name {
|
|
290
|
+
font-family: var(--font-display);
|
|
291
|
+
font-size: var(--product-size); /* min 120px for 16:9, min 80px for 9:16 */
|
|
292
|
+
font-weight: 900;
|
|
293
|
+
letter-spacing: var(--tracking-tight);
|
|
294
|
+
color: var(--text-primary);
|
|
295
|
+
text-align: center;
|
|
296
|
+
line-height: 0.9;
|
|
297
|
+
position: relative;
|
|
298
|
+
z-index: 3;
|
|
299
|
+
opacity: 0;
|
|
300
|
+
transform-origin: center center;
|
|
301
|
+
}
|
|
302
|
+
.reveal-hero .tagline {
|
|
303
|
+
font-family: var(--font-body);
|
|
304
|
+
font-size: var(--tagline-size);
|
|
305
|
+
font-weight: 400;
|
|
306
|
+
letter-spacing: var(--tracking-wide);
|
|
307
|
+
color: var(--accent);
|
|
308
|
+
text-align: center;
|
|
309
|
+
margin-top: 28px;
|
|
310
|
+
position: relative;
|
|
311
|
+
z-index: 3;
|
|
312
|
+
opacity: 0;
|
|
313
|
+
text-transform: uppercase;
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**HTML template:**
|
|
318
|
+
```html
|
|
319
|
+
<div class="scene reveal-hero">
|
|
320
|
+
<div class="flash-overlay" id="reveal-flash"></div>
|
|
321
|
+
<div class="scene-inner" style="text-align:center">
|
|
322
|
+
<h1 class="product-name" id="product-name-el">[PRODUCT NAME]</h1>
|
|
323
|
+
<p class="tagline" id="tagline-el">[tagline — 4–6 words]</p>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**renderFrame notes:**
|
|
329
|
+
```javascript
|
|
330
|
+
// Flash fires at scene start: opacity 0→0.6→0 over 300ms
|
|
331
|
+
// Then product name: cinematic=materialise(700ms) | energetic=slam(120ms) | minimal=fade(400ms) | emotional=wordReveal
|
|
332
|
+
// Tagline: fades in 400ms after product name completes
|
|
333
|
+
|
|
334
|
+
function renderRevealHero(t, sceneStart) {
|
|
335
|
+
const flash = document.getElementById('reveal-flash');
|
|
336
|
+
const nameEl = document.getElementById('product-name-el');
|
|
337
|
+
const tagEl = document.getElementById('tagline-el');
|
|
338
|
+
|
|
339
|
+
// Flash: 0→peak→0 in 300ms at scene start
|
|
340
|
+
const flashDur = 300;
|
|
341
|
+
const fp = clamp(t - sceneStart, 0, flashDur) / flashDur;
|
|
342
|
+
flash.style.opacity = (fp < 0.4 ? fp / 0.4 : (1 - (fp - 0.4) / 0.6) * 0.6).toFixed(3);
|
|
343
|
+
|
|
344
|
+
// Product name reveal (example: materialise for cinematic)
|
|
345
|
+
const nameStart = sceneStart + 200; // 200ms after flash peak
|
|
346
|
+
const nameDur = 700;
|
|
347
|
+
const np = clamp((t - nameStart) / nameDur, 0, 1);
|
|
348
|
+
const blurVal = lerp(10, 0, easeOutCubic(np));
|
|
349
|
+
nameEl.style.opacity = easeOutCubic(np).toFixed(3);
|
|
350
|
+
nameEl.style.filter = `blur(${blurVal.toFixed(2)}px)`;
|
|
351
|
+
|
|
352
|
+
// Tagline: after name completes
|
|
353
|
+
const tagStart = nameStart + nameDur + 100;
|
|
354
|
+
const tagDur = 500;
|
|
355
|
+
const tp = clamp((t - tagStart) / tagDur, 0, 1);
|
|
356
|
+
tagEl.style.opacity = easeOutCubic(tp).toFixed(3);
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 5. tagline-card
|
|
363
|
+
|
|
364
|
+
**Purpose:** The product's promise stated alone. Large type, nothing else. Used when the
|
|
365
|
+
tagline is powerful enough to hold a scene solo — or as a second Reveal beat.
|
|
366
|
+
|
|
367
|
+
**CSS:**
|
|
368
|
+
```css
|
|
369
|
+
.tagline-card {
|
|
370
|
+
align-items: center;
|
|
371
|
+
justify-content: center;
|
|
372
|
+
}
|
|
373
|
+
.tagline-card .main-line {
|
|
374
|
+
font-family: var(--font-display);
|
|
375
|
+
font-size: 64px;
|
|
376
|
+
font-weight: 700;
|
|
377
|
+
letter-spacing: var(--tracking-tight);
|
|
378
|
+
color: var(--text-primary);
|
|
379
|
+
text-align: center;
|
|
380
|
+
line-height: 1.15;
|
|
381
|
+
max-width: 860px;
|
|
382
|
+
opacity: 0;
|
|
383
|
+
}
|
|
384
|
+
.tagline-card .accent-bar {
|
|
385
|
+
width: 48px;
|
|
386
|
+
height: 3px;
|
|
387
|
+
background: var(--accent);
|
|
388
|
+
margin: 32px auto 0;
|
|
389
|
+
transform: scaleX(0);
|
|
390
|
+
transform-origin: left center;
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**HTML template:**
|
|
395
|
+
```html
|
|
396
|
+
<div class="scene tagline-card">
|
|
397
|
+
<div class="scene-inner" style="text-align:center">
|
|
398
|
+
<p class="main-line" id="tagline-main">[The tagline — 4–6 words, no punctuation at end]</p>
|
|
399
|
+
<div class="accent-bar" id="tagline-bar"></div>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**renderFrame notes:**
|
|
405
|
+
- Text: materialise or fade-up at 10% of scene.
|
|
406
|
+
- Accent bar: `scaleX 0→1` after text completes (200ms, `ease-out`).
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## 6. proof-stat
|
|
411
|
+
|
|
412
|
+
**Purpose:** One number. One truth. The counter animation makes it feel earned.
|
|
413
|
+
No context list — one stat only. If you have multiple, pick the strongest.
|
|
414
|
+
|
|
415
|
+
**CSS:**
|
|
416
|
+
```css
|
|
417
|
+
.proof-stat {
|
|
418
|
+
align-items: center;
|
|
419
|
+
justify-content: center;
|
|
420
|
+
}
|
|
421
|
+
.proof-stat .stat-value {
|
|
422
|
+
font-family: var(--font-display);
|
|
423
|
+
font-size: var(--stat-size);
|
|
424
|
+
font-weight: 900;
|
|
425
|
+
letter-spacing: var(--tracking-tight);
|
|
426
|
+
color: var(--text-primary);
|
|
427
|
+
text-align: center;
|
|
428
|
+
line-height: 0.85;
|
|
429
|
+
opacity: 0;
|
|
430
|
+
transform-origin: center center;
|
|
431
|
+
}
|
|
432
|
+
.proof-stat .stat-suffix {
|
|
433
|
+
font-family: var(--font-display);
|
|
434
|
+
font-size: calc(var(--stat-size) * 0.45);
|
|
435
|
+
font-weight: 700;
|
|
436
|
+
color: var(--accent);
|
|
437
|
+
vertical-align: super;
|
|
438
|
+
font-size: 80px;
|
|
439
|
+
}
|
|
440
|
+
.proof-stat .stat-label {
|
|
441
|
+
font-family: var(--font-body);
|
|
442
|
+
font-size: var(--body-size);
|
|
443
|
+
letter-spacing: var(--tracking-wide);
|
|
444
|
+
text-transform: uppercase;
|
|
445
|
+
color: var(--text-secondary);
|
|
446
|
+
text-align: center;
|
|
447
|
+
margin-top: 20px;
|
|
448
|
+
opacity: 0;
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**HTML template:**
|
|
453
|
+
```html
|
|
454
|
+
<div class="scene proof-stat">
|
|
455
|
+
<div class="scene-inner" style="text-align:center">
|
|
456
|
+
<p class="stat-value" id="stat-number">
|
|
457
|
+
<span id="stat-counter">0</span><span class="stat-suffix">[+ or % or × — agent chooses]</span>
|
|
458
|
+
</p>
|
|
459
|
+
<p class="stat-label" id="stat-label">[3–6 word context, e.g. "teams ship 10× faster"]</p>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**renderFrame notes:**
|
|
465
|
+
```javascript
|
|
466
|
+
// Stat value: slam-in at scene start (all presets — number always feels energetic)
|
|
467
|
+
// Counter: counts from 0 to TARGET over 60% of scene duration
|
|
468
|
+
// Label: fades in after counter completes
|
|
469
|
+
|
|
470
|
+
function renderProofStat(t, sceneStart, sceneEnd, targetNum) {
|
|
471
|
+
const counterEl = document.getElementById('stat-counter');
|
|
472
|
+
const statEl = document.querySelector('.stat-value');
|
|
473
|
+
const labelEl = document.getElementById('stat-label');
|
|
474
|
+
|
|
475
|
+
// Appear at scene start
|
|
476
|
+
const appearDur = 180;
|
|
477
|
+
const ap = clamp((t - sceneStart) / appearDur, 0, 1);
|
|
478
|
+
statEl.style.opacity = easeOutCubic(ap).toFixed(3);
|
|
479
|
+
|
|
480
|
+
// Counter runs from 0 to targetNum over 60% of scene
|
|
481
|
+
const countDur = (sceneEnd - sceneStart) * 0.60;
|
|
482
|
+
const cp = clamp((t - sceneStart) / countDur, 0, 1);
|
|
483
|
+
counterEl.textContent = Math.round(easeOutCubic(cp) * targetNum).toLocaleString();
|
|
484
|
+
|
|
485
|
+
// Label appears when counter completes
|
|
486
|
+
const labelStart = sceneStart + countDur + 100;
|
|
487
|
+
const lp = clamp((t - labelStart) / 400, 0, 1);
|
|
488
|
+
labelEl.style.opacity = easeOutCubic(lp).toFixed(3);
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## 7. feature-bullet
|
|
495
|
+
|
|
496
|
+
**Purpose:** Single product capability with one line of context. Used in Proof section
|
|
497
|
+
when the proof is qualitative rather than numerical.
|
|
498
|
+
|
|
499
|
+
**CSS:**
|
|
500
|
+
```css
|
|
501
|
+
.feature-bullet {
|
|
502
|
+
align-items: center;
|
|
503
|
+
justify-content: center;
|
|
504
|
+
}
|
|
505
|
+
.feature-bullet .bullet-icon {
|
|
506
|
+
width: 48px;
|
|
507
|
+
height: 3px;
|
|
508
|
+
background: var(--accent);
|
|
509
|
+
margin: 0 auto 40px;
|
|
510
|
+
transform: scaleX(0);
|
|
511
|
+
transform-origin: left center;
|
|
512
|
+
}
|
|
513
|
+
.feature-bullet .bullet-main {
|
|
514
|
+
font-family: var(--font-display);
|
|
515
|
+
font-size: 60px;
|
|
516
|
+
font-weight: 700;
|
|
517
|
+
letter-spacing: var(--tracking-tight);
|
|
518
|
+
color: var(--text-primary);
|
|
519
|
+
text-align: center;
|
|
520
|
+
line-height: 1.1;
|
|
521
|
+
max-width: 800px;
|
|
522
|
+
opacity: 0;
|
|
523
|
+
}
|
|
524
|
+
.feature-bullet .bullet-sub {
|
|
525
|
+
font-family: var(--font-body);
|
|
526
|
+
font-size: var(--body-size);
|
|
527
|
+
color: var(--text-secondary);
|
|
528
|
+
text-align: center;
|
|
529
|
+
margin-top: 20px;
|
|
530
|
+
max-width: 640px;
|
|
531
|
+
line-height: 1.6;
|
|
532
|
+
opacity: 0;
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**HTML template:**
|
|
537
|
+
```html
|
|
538
|
+
<div class="scene feature-bullet">
|
|
539
|
+
<div class="scene-inner" style="text-align:center">
|
|
540
|
+
<div class="bullet-icon" id="bullet-icon"></div>
|
|
541
|
+
<p class="bullet-main" id="bullet-main">[the capability — 4–8 words]</p>
|
|
542
|
+
<p class="bullet-sub" id="bullet-sub">[supporting context — 1 sentence, 10–15 words]</p>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**renderFrame notes:**
|
|
548
|
+
- Icon bar: `scaleX 0→1` at scene start over 200ms.
|
|
549
|
+
- Main text: materialise/fade at 200ms.
|
|
550
|
+
- Sub text: fade after main completes.
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## 8. countdown-card
|
|
555
|
+
|
|
556
|
+
**Purpose:** Countdown to launch date. Visual tension device. Can appear in Tease OR as
|
|
557
|
+
a scene within CTA. Agent sets values at generation time based on current date vs launch_date.
|
|
558
|
+
|
|
559
|
+
**CSS:**
|
|
560
|
+
```css
|
|
561
|
+
.countdown-card {
|
|
562
|
+
align-items: center;
|
|
563
|
+
justify-content: center;
|
|
564
|
+
}
|
|
565
|
+
.countdown-card .countdown-label {
|
|
566
|
+
font-family: var(--font-body);
|
|
567
|
+
font-size: var(--body-size);
|
|
568
|
+
letter-spacing: var(--tracking-wide);
|
|
569
|
+
text-transform: uppercase;
|
|
570
|
+
color: var(--text-secondary);
|
|
571
|
+
text-align: center;
|
|
572
|
+
margin-bottom: 40px;
|
|
573
|
+
opacity: 0;
|
|
574
|
+
}
|
|
575
|
+
.countdown-card .countdown-grid {
|
|
576
|
+
display: flex;
|
|
577
|
+
gap: 48px;
|
|
578
|
+
justify-content: center;
|
|
579
|
+
align-items: flex-end;
|
|
580
|
+
opacity: 0;
|
|
581
|
+
}
|
|
582
|
+
.countdown-card .countdown-unit {
|
|
583
|
+
text-align: center;
|
|
584
|
+
}
|
|
585
|
+
.countdown-card .countdown-num {
|
|
586
|
+
font-family: var(--font-display);
|
|
587
|
+
font-size: 120px;
|
|
588
|
+
font-weight: 900;
|
|
589
|
+
letter-spacing: var(--tracking-tight);
|
|
590
|
+
color: var(--text-primary);
|
|
591
|
+
line-height: 0.9;
|
|
592
|
+
}
|
|
593
|
+
.countdown-card .countdown-sub {
|
|
594
|
+
font-family: var(--font-body);
|
|
595
|
+
font-size: 14px;
|
|
596
|
+
letter-spacing: var(--tracking-wide);
|
|
597
|
+
text-transform: uppercase;
|
|
598
|
+
color: var(--text-secondary);
|
|
599
|
+
margin-top: 12px;
|
|
600
|
+
}
|
|
601
|
+
.countdown-card .countdown-date {
|
|
602
|
+
font-family: var(--font-body);
|
|
603
|
+
font-size: var(--body-size);
|
|
604
|
+
color: var(--accent);
|
|
605
|
+
text-align: center;
|
|
606
|
+
margin-top: 40px;
|
|
607
|
+
letter-spacing: var(--tracking-wide);
|
|
608
|
+
text-transform: uppercase;
|
|
609
|
+
opacity: 0;
|
|
610
|
+
}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**HTML template:**
|
|
614
|
+
```html
|
|
615
|
+
<div class="scene countdown-card">
|
|
616
|
+
<div class="scene-inner" style="text-align:center">
|
|
617
|
+
<p class="countdown-label" id="cd-label">Launching in</p>
|
|
618
|
+
<div class="countdown-grid" id="cd-grid">
|
|
619
|
+
<div class="countdown-unit">
|
|
620
|
+
<p class="countdown-num">[DD]</p>
|
|
621
|
+
<p class="countdown-sub">Days</p>
|
|
622
|
+
</div>
|
|
623
|
+
<div class="countdown-unit">
|
|
624
|
+
<p class="countdown-num">[HH]</p>
|
|
625
|
+
<p class="countdown-sub">Hours</p>
|
|
626
|
+
</div>
|
|
627
|
+
<div class="countdown-unit">
|
|
628
|
+
<p class="countdown-num">[MM]</p>
|
|
629
|
+
<p class="countdown-sub">Minutes</p>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
<p class="countdown-date" id="cd-date">[formatted launch date, e.g. "June 12, 2025"]</p>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**renderFrame notes:**
|
|
638
|
+
- Label fades first (0–200ms of scene).
|
|
639
|
+
- Grid appears (200–500ms of scene): slam-in or materialise per preset.
|
|
640
|
+
- Date fades after grid (500–800ms).
|
|
641
|
+
- Values are static — agent calculates `DD`, `HH`, `MM` at generation time from `launch_date`.
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
## 9. cta-card
|
|
646
|
+
|
|
647
|
+
**Purpose:** The close. URL large. Action text above. Always the final scene.
|
|
648
|
+
Nothing else — no feature list, no secondary CTA.
|
|
649
|
+
|
|
650
|
+
**CSS:**
|
|
651
|
+
```css
|
|
652
|
+
.cta-card {
|
|
653
|
+
align-items: center;
|
|
654
|
+
justify-content: center;
|
|
655
|
+
}
|
|
656
|
+
.cta-card .cta-action {
|
|
657
|
+
font-family: var(--font-body);
|
|
658
|
+
font-size: var(--body-size);
|
|
659
|
+
letter-spacing: var(--tracking-wide);
|
|
660
|
+
text-transform: uppercase;
|
|
661
|
+
color: var(--text-secondary);
|
|
662
|
+
text-align: center;
|
|
663
|
+
margin-bottom: 24px;
|
|
664
|
+
opacity: 0;
|
|
665
|
+
}
|
|
666
|
+
.cta-card .cta-url {
|
|
667
|
+
font-family: var(--font-display);
|
|
668
|
+
font-size: 72px;
|
|
669
|
+
font-weight: 700;
|
|
670
|
+
letter-spacing: var(--tracking-tight);
|
|
671
|
+
color: var(--text-primary);
|
|
672
|
+
text-align: center;
|
|
673
|
+
line-height: 1;
|
|
674
|
+
opacity: 0;
|
|
675
|
+
}
|
|
676
|
+
.cta-card .cta-accent {
|
|
677
|
+
width: 64px;
|
|
678
|
+
height: 2px;
|
|
679
|
+
background: var(--accent);
|
|
680
|
+
margin: 32px auto 0;
|
|
681
|
+
transform: scaleX(0);
|
|
682
|
+
transform-origin: left center;
|
|
683
|
+
}
|
|
684
|
+
.cta-card .cta-sub {
|
|
685
|
+
font-family: var(--font-body);
|
|
686
|
+
font-size: 18px;
|
|
687
|
+
color: var(--text-secondary);
|
|
688
|
+
text-align: center;
|
|
689
|
+
margin-top: 24px;
|
|
690
|
+
opacity: 0;
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
**HTML template:**
|
|
695
|
+
```html
|
|
696
|
+
<div class="scene cta-card">
|
|
697
|
+
<div class="scene-inner" style="text-align:center">
|
|
698
|
+
<p class="cta-action" id="cta-action">[action phrase — "Available now" / "Join the waitlist" / "Try it free"]</p>
|
|
699
|
+
<p class="cta-url" id="cta-url">[url — no https:// prefix]</p>
|
|
700
|
+
<div class="cta-accent" id="cta-accent"></div>
|
|
701
|
+
<p class="cta-sub" id="cta-sub">[optional: one supporting line — "Free 14-day trial. No card required."]</p>
|
|
702
|
+
</div>
|
|
703
|
+
</div>
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
**renderFrame notes:**
|
|
707
|
+
- Action text: fades in at scene start (300ms).
|
|
708
|
+
- URL: materialise/slam after action (200ms delay, 500ms duration).
|
|
709
|
+
- Accent bar: `scaleX 0→1` after URL (200ms).
|
|
710
|
+
- Sub text: fades after accent bar (200ms).
|
|
711
|
+
- The URL stays fully visible through end of scene — no exit animation on cta-card (it holds on screen).
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## 10. tease-words ★ PRIMARY TEASE SCENE
|
|
716
|
+
|
|
717
|
+
**Purpose:** Each problem keyword owns the entire screen at massive scale. Punchy, rhythmic, unavoidable. Reference: Apple "Shot on iPhone" cadence. Replaces `tease-problem` for 60s and 90s videos.
|
|
718
|
+
|
|
719
|
+
**Timing:** Each word gets 1600–1800ms. 4 words = ~7200ms. Sub-line fades in at last 1500ms of tease. Reserve first 1500ms for `blackout-opener`.
|
|
720
|
+
|
|
721
|
+
**CSS:**
|
|
722
|
+
```css
|
|
723
|
+
.scene-tease { background: #000; }
|
|
724
|
+
.hero-word {
|
|
725
|
+
position: absolute; inset: 0;
|
|
726
|
+
display: flex; align-items: center; justify-content: center;
|
|
727
|
+
font-family: var(--font-display);
|
|
728
|
+
font-size: 210px; font-weight: 900;
|
|
729
|
+
letter-spacing: -0.05em; line-height: 1;
|
|
730
|
+
color: #fff;
|
|
731
|
+
opacity: 0; will-change: opacity, transform;
|
|
732
|
+
}
|
|
733
|
+
.tease-sub-line {
|
|
734
|
+
position: absolute; bottom: 140px; left: 0; right: 0; text-align: center;
|
|
735
|
+
font-size: 15px; font-weight: 400;
|
|
736
|
+
letter-spacing: 0.22em; text-transform: uppercase;
|
|
737
|
+
color: rgba(255,255,255,0.20);
|
|
738
|
+
opacity: 0; will-change: opacity;
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**HTML template:**
|
|
743
|
+
```html
|
|
744
|
+
<div class="scene scene-tease">
|
|
745
|
+
<p id="word-0" class="hero-word">[word 1 — e.g. Research.]</p>
|
|
746
|
+
<p id="word-1" class="hero-word">[word 2 — e.g. Write.]</p>
|
|
747
|
+
<p id="word-2" class="hero-word">[word 3 — e.g. Outreach.]</p>
|
|
748
|
+
<p id="word-3" class="hero-word">[word 4 — e.g. Repeat.]</p>
|
|
749
|
+
<p id="tease-sub" class="tease-sub-line">[pain statement — e.g. "Hours that don't scale"]</p>
|
|
750
|
+
</div>
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
**renderFrame notes:**
|
|
754
|
+
```javascript
|
|
755
|
+
// Word beats — each word is a separate element, one visible at a time
|
|
756
|
+
const WORD_BEATS = [
|
|
757
|
+
{ id: 'word-0', start: TEASE_START + 1500, end: TEASE_START + 3300 },
|
|
758
|
+
{ id: 'word-1', start: TEASE_START + 3300, end: TEASE_START + 5100 },
|
|
759
|
+
{ id: 'word-2', start: TEASE_START + 5100, end: TEASE_START + 6900 },
|
|
760
|
+
{ id: 'word-3', start: TEASE_START + 6900, end: TEASE_END },
|
|
761
|
+
];
|
|
762
|
+
const FADE = 170; // ms
|
|
763
|
+
|
|
764
|
+
WORD_BEATS.forEach(({ id, start, end }, i) => {
|
|
765
|
+
const el = document.getElementById(id);
|
|
766
|
+
if (!el) return;
|
|
767
|
+
if (t < start || t > end + 60) { el.style.opacity = '0'; el.style.transform = ''; return; }
|
|
768
|
+
if (t < start + FADE) {
|
|
769
|
+
const p = easeOutQuint((t - start) / FADE);
|
|
770
|
+
el.style.opacity = p.toFixed(3);
|
|
771
|
+
el.style.transform = `translateY(${lerp(32, 0, p).toFixed(2)}px)`;
|
|
772
|
+
} else if (i < 3 && t > end - FADE) {
|
|
773
|
+
const p = (t - (end - FADE)) / FADE;
|
|
774
|
+
el.style.opacity = (1 - p).toFixed(3);
|
|
775
|
+
el.style.transform = `translateY(${lerp(0, -22, p).toFixed(2)}px)`;
|
|
776
|
+
} else { el.style.opacity = '1'; el.style.transform = ''; }
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
const tsub = document.getElementById('tease-sub');
|
|
780
|
+
if (tsub) tsub.style.opacity = easeOutCubic(clamp((t - (TEASE_END - 1600)) / 600, 0, 1)).toFixed(3);
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
**Design rules:**
|
|
784
|
+
- Each word is centered, full-screen, no decoration.
|
|
785
|
+
- 4 words maximum. If description has more pain points, pick the 4 sharpest.
|
|
786
|
+
- Sub-line: 15px, all-caps, very low opacity. It echoes, not competes.
|
|
787
|
+
- Background: always `#000` — never white, even in minimal preset.
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## 11. terminal-card ★ PRIMARY BUILD SCENE
|
|
792
|
+
|
|
793
|
+
**Purpose:** Shows the audience's current reality — manual work being done, line by line, like a terminal output. 3 cards appear sequentially with a typewriter animation. Creates empathy before the reveal destroys that world. Inspired by Linear's "the old way vs new way" narrative pattern.
|
|
794
|
+
|
|
795
|
+
**CSS:**
|
|
796
|
+
```css
|
|
797
|
+
.scene-build {
|
|
798
|
+
background: #080808;
|
|
799
|
+
background-image: radial-gradient(circle, rgba(255,255,255,0.035) 1px, transparent 1px);
|
|
800
|
+
background-size: 60px 60px;
|
|
801
|
+
align-items: flex-start; justify-content: center;
|
|
802
|
+
padding: 0 200px;
|
|
803
|
+
}
|
|
804
|
+
.build-header {
|
|
805
|
+
font-size: 13px; font-weight: 500;
|
|
806
|
+
letter-spacing: 0.22em; text-transform: uppercase;
|
|
807
|
+
color: rgba(245,245,245,0.35);
|
|
808
|
+
margin-bottom: 48px;
|
|
809
|
+
opacity: 0; will-change: opacity;
|
|
810
|
+
}
|
|
811
|
+
.term-cards { width: 100%; max-width: 1000px; display: flex; flex-direction: column; gap: 20px; }
|
|
812
|
+
.term-card {
|
|
813
|
+
display: flex; align-items: center; gap: 18px;
|
|
814
|
+
border: 1px solid rgba(255,255,255,0.07);
|
|
815
|
+
border-radius: 14px; padding: 26px 32px;
|
|
816
|
+
background: rgba(255,255,255,0.018);
|
|
817
|
+
opacity: 0; will-change: opacity, transform;
|
|
818
|
+
}
|
|
819
|
+
.term-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
|
|
820
|
+
.term-text {
|
|
821
|
+
font-size: 22px; font-weight: 400; letter-spacing: -0.01em;
|
|
822
|
+
color: rgba(245,245,245,0.65);
|
|
823
|
+
}
|
|
824
|
+
.build-close-line {
|
|
825
|
+
position: absolute; bottom: 140px; left: 200px; right: 200px;
|
|
826
|
+
height: 1px; background: var(--accent);
|
|
827
|
+
transform: scaleX(0); transform-origin: left center; will-change: transform;
|
|
828
|
+
box-shadow: 0 0 18px rgba([accent-rgb],0.4);
|
|
829
|
+
}
|
|
830
|
+
.build-close-label {
|
|
831
|
+
position: absolute; bottom: 96px; left: 0; right: 0; text-align: center;
|
|
832
|
+
font-size: 13px; font-weight: 500; letter-spacing: 0.22em; text-transform: uppercase;
|
|
833
|
+
color: var(--accent); opacity: 0; will-change: opacity;
|
|
834
|
+
}
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
**HTML template:**
|
|
838
|
+
```html
|
|
839
|
+
<div class="scene scene-build">
|
|
840
|
+
<div style="width:100%;max-width:1000px">
|
|
841
|
+
<p id="build-header" class="build-header">Meanwhile, your team is spending hours on</p>
|
|
842
|
+
<div class="term-cards">
|
|
843
|
+
<div id="tc0" class="term-card"><div class="term-dot"></div><span id="tt0" class="term-text"></span></div>
|
|
844
|
+
<div id="tc1" class="term-card"><div class="term-dot"></div><span id="tt1" class="term-text"></span></div>
|
|
845
|
+
<div id="tc2" class="term-card"><div class="term-dot"></div><span id="tt2" class="term-text"></span></div>
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
<div id="close-line" class="build-close-line"></div>
|
|
849
|
+
<p id="close-label" class="build-close-label">Not anymore</p>
|
|
850
|
+
</div>
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
**renderFrame notes:**
|
|
854
|
+
```javascript
|
|
855
|
+
const CARDS = [
|
|
856
|
+
{ id: 0, start: BUILD_START_MS + 1500, text: 'Researching [N] competitor strategies...' },
|
|
857
|
+
{ id: 1, start: BUILD_START_MS + 5500, text: 'Writing [N] content drafts from briefs...' },
|
|
858
|
+
{ id: 2, start: BUILD_START_MS + 9500, text: 'Scheduling [N] personalised outreach emails...' },
|
|
859
|
+
];
|
|
860
|
+
const TYPE_DUR = 1800; // ms for full typewriter reveal
|
|
861
|
+
const BUILD_CLOSE = BUILD_END_MS - 3500; // fade cards out before build ends
|
|
862
|
+
const BUILD_LABEL = BUILD_END_MS - 2200;
|
|
863
|
+
|
|
864
|
+
CARDS.forEach(({ id, start, text }) => {
|
|
865
|
+
const card = document.getElementById(`tc${id}`);
|
|
866
|
+
const txt = document.getElementById(`tt${id}`);
|
|
867
|
+
if (!card || !txt) return;
|
|
868
|
+
|
|
869
|
+
if (t < start) { card.style.opacity = '0'; card.style.transform = ''; txt.textContent = ''; return; }
|
|
870
|
+
if (t >= BUILD_CLOSE) {
|
|
871
|
+
const fp = clamp((t - BUILD_CLOSE) / 550, 0, 1);
|
|
872
|
+
card.style.opacity = (1 - easeOutCubic(fp)).toFixed(3);
|
|
873
|
+
card.style.transform = `translateX(${lerp(0, -24, easeOutCubic(fp)).toFixed(2)}px)`;
|
|
874
|
+
} else {
|
|
875
|
+
const fi = clamp((t - start) / 380, 0, 1);
|
|
876
|
+
card.style.opacity = easeOutCubic(fi).toFixed(3);
|
|
877
|
+
card.style.transform = `translateX(${lerp(-24, 0, easeOutCubic(fi)).toFixed(2)}px)`;
|
|
878
|
+
}
|
|
879
|
+
const tp = clamp((t - (start + 180)) / TYPE_DUR, 0, 1);
|
|
880
|
+
const chars = Math.floor(tp * text.length);
|
|
881
|
+
const blink = t < (start + 180 + TYPE_DUR) && chars < text.length && Math.sin((t / 380) * Math.PI) > 0;
|
|
882
|
+
txt.textContent = text.slice(0, chars) + (blink ? '▋' : '');
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const closeLine = document.getElementById('close-line');
|
|
886
|
+
const closeLabel = document.getElementById('close-label');
|
|
887
|
+
if (closeLine) closeLine.style.transform = `scaleX(${easeOutCubic(clamp((t - BUILD_LABEL) / 650, 0, 1)).toFixed(3)})`;
|
|
888
|
+
if (closeLabel) closeLabel.style.opacity = easeOutCubic(clamp((t - (BUILD_LABEL + 500)) / 500, 0, 1)).toFixed(3);
|
|
889
|
+
```
|
|
890
|
+
|
|
891
|
+
**Design rules:**
|
|
892
|
+
- Card text must describe the actual manual work the target audience does — not generic placeholders.
|
|
893
|
+
- Tailor the numbers in the card text to the specific product's context (make them feel real).
|
|
894
|
+
- `build-header` sets the scene: "Meanwhile, your team is spending hours on" — always present.
|
|
895
|
+
- Cards fade out 3500ms before build ends, leaving a closing accent line + "Not anymore".
|
|
896
|
+
- Background: dark with dot-grid always — never white.
|