hyperframes 0.1.15 → 0.2.0-alpha.1

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.
Files changed (41) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +3968 -3297
  3. package/dist/skills/hyperframes-captions/SKILL.md +1 -1
  4. package/dist/skills/hyperframes-compose/SKILL.md +1 -0
  5. package/dist/studio/assets/index-Bkp9HQbo.css +1 -0
  6. package/dist/studio/assets/index-DfhSlTti.js +93 -0
  7. package/dist/studio/index.html +2 -2
  8. package/dist/templates/_shared/CLAUDE.md +1 -1
  9. package/package.json +3 -2
  10. package/dist/studio/assets/index-CLmYRLY-.css +0 -1
  11. package/dist/studio/assets/index-CRvFpc0E.js +0 -84
  12. package/dist/templates/decision-tree/compositions/decision_tree.html +0 -420
  13. package/dist/templates/decision-tree/index.html +0 -56
  14. package/dist/templates/generators.ts +0 -32
  15. package/dist/templates/kinetic-type/compositions/main-graphics.html +0 -588
  16. package/dist/templates/kinetic-type/index.html +0 -61
  17. package/dist/templates/nyt-graph/compositions/nyt-chart.html +0 -450
  18. package/dist/templates/nyt-graph/index.html +0 -51
  19. package/dist/templates/play-mode/compositions/captions.html +0 -163
  20. package/dist/templates/play-mode/compositions/intro.html +0 -107
  21. package/dist/templates/play-mode/compositions/stats.html +0 -388
  22. package/dist/templates/play-mode/index.html +0 -239
  23. package/dist/templates/product-promo/assets/figma-cursors.svg +0 -30
  24. package/dist/templates/product-promo/assets/figma-logo-pieces.svg +0 -15
  25. package/dist/templates/product-promo/assets/figma-logo-pills.svg +0 -6
  26. package/dist/templates/product-promo/compositions/scene1-logo-intro.html +0 -189
  27. package/dist/templates/product-promo/compositions/scene2-4-canvas.html +0 -570
  28. package/dist/templates/product-promo/compositions/scene5-logo-outro.html +0 -226
  29. package/dist/templates/product-promo/index.html +0 -158
  30. package/dist/templates/swiss-grid/assets/swiss-grid.svg +0 -116
  31. package/dist/templates/swiss-grid/compositions/captions.html +0 -148
  32. package/dist/templates/swiss-grid/compositions/graphics.html +0 -207
  33. package/dist/templates/swiss-grid/compositions/intro.html +0 -131
  34. package/dist/templates/swiss-grid/index.html +0 -234
  35. package/dist/templates/vignelli/compositions/captions.html +0 -181
  36. package/dist/templates/vignelli/compositions/overlays.html +0 -323
  37. package/dist/templates/vignelli/index.html +0 -190
  38. package/dist/templates/warm-grain/compositions/captions.html +0 -147
  39. package/dist/templates/warm-grain/compositions/graphics.html +0 -164
  40. package/dist/templates/warm-grain/compositions/intro.html +0 -86
  41. package/dist/templates/warm-grain/index.html +0 -294
@@ -1,450 +0,0 @@
1
- <template id="nyt-chart-template">
2
- <div data-composition-id="nyt-chart" data-width="1920" data-height="1080" data-duration="15">
3
- <!-- Google Fonts Import -->
4
- <link rel="preconnect" href="https://fonts.googleapis.com" />
5
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
6
- <link
7
- href="https://fonts.googleapis.com/css2?family=Libre+Baskerville:wght@400;700&family=Libre+Franklin:wght@300;400;600&display=block"
8
- rel="stylesheet"
9
- />
10
-
11
- <div class="chart-container">
12
- <!-- Header Section -->
13
- <div class="header">
14
- <h1 class="headline">Monthly Revenue vs. Conversion Rate</h1>
15
- <p class="subtitle">Jan–Jun 2024, in thousands</p>
16
- <div class="key">
17
- <div class="key-item">
18
- <div class="key-box revenue-box"></div>
19
- <span class="key-text">Revenue</span>
20
- </div>
21
- <div class="key-item">
22
- <div class="key-line conversion-line"></div>
23
- <span class="key-text">Conversion Rate</span>
24
- </div>
25
- </div>
26
- </div>
27
-
28
- <!-- SVG Chart Area -->
29
- <svg class="chart-svg" viewBox="0 0 1600 700" preserveAspectRatio="xMidYMid meet">
30
- <!-- Gridlines Group -->
31
- <g class="gridlines"></g>
32
-
33
- <!-- X-Axis Labels -->
34
- <g class="x-axis-labels"></g>
35
-
36
- <!-- Bars Group (Revenue) -->
37
- <g class="bars-group"></g>
38
-
39
- <!-- Bar Labels Group -->
40
- <g class="bar-labels-group"></g>
41
-
42
- <!-- Line Group (Conversion Rate) -->
43
- <g class="line-group">
44
- <path class="conversion-path" fill="none" stroke="#326FA8" stroke-width="1.5" />
45
- <circle class="line-dot" r="5" fill="#326FA8" style="opacity: 0" />
46
- </g>
47
-
48
- <!-- Line Labels Group -->
49
- <g class="line-labels-group"></g>
50
-
51
- <!-- Baseline -->
52
- <line x1="100" y1="600" x2="1500" y2="600" stroke="black" stroke-width="1" />
53
- </svg>
54
-
55
- <!-- Source Line -->
56
- <div class="source">Source: Internal analytics</div>
57
- </div>
58
-
59
- <style>
60
- [data-composition-id="nyt-chart"] {
61
- width: 1920px;
62
- height: 1080px;
63
- background-color: #faf9f6;
64
- font-family: "Libre Franklin", sans-serif;
65
- color: #333;
66
- display: flex;
67
- justify-content: center;
68
- align-items: center;
69
- overflow: hidden;
70
- }
71
-
72
- [data-composition-id="nyt-chart"] .chart-container {
73
- width: 1600px;
74
- height: 900px;
75
- display: flex;
76
- flex-direction: column;
77
- position: relative;
78
- transform: scale(1.15);
79
- transform-origin: center center;
80
- }
81
-
82
- [data-composition-id="nyt-chart"] .header {
83
- margin-bottom: 40px;
84
- text-align: left;
85
- }
86
-
87
- [data-composition-id="nyt-chart"] .headline {
88
- font-family: "Libre Baskerville", serif;
89
- font-size: 42px;
90
- margin: 0;
91
- padding: 0;
92
- clip-path: inset(0 100% 0 0);
93
- white-space: nowrap;
94
- }
95
-
96
- [data-composition-id="nyt-chart"] .subtitle {
97
- font-size: 16px;
98
- color: #d4d4d4;
99
- margin: 8px 0 16px 0;
100
- clip-path: inset(0 100% 0 0);
101
- white-space: nowrap;
102
- }
103
-
104
- [data-composition-id="nyt-chart"] .key {
105
- display: flex;
106
- gap: 24px;
107
- opacity: 0;
108
- }
109
-
110
- [data-composition-id="nyt-chart"] .key-item {
111
- display: flex;
112
- align-items: center;
113
- gap: 8px;
114
- }
115
-
116
- [data-composition-id="nyt-chart"] .key-box {
117
- width: 10px;
118
- height: 10px;
119
- background-color: #5c5c5c;
120
- }
121
-
122
- [data-composition-id="nyt-chart"] .key-line {
123
- width: 16px;
124
- height: 1.5px;
125
- background-color: #326fa8;
126
- }
127
-
128
- [data-composition-id="nyt-chart"] .key-text {
129
- font-size: 13px;
130
- color: #666;
131
- }
132
-
133
- [data-composition-id="nyt-chart"] .chart-svg {
134
- width: 100%;
135
- height: 600px;
136
- overflow: visible;
137
- }
138
-
139
- [data-composition-id="nyt-chart"] .source {
140
- position: absolute;
141
- bottom: 0;
142
- left: 0;
143
- font-size: 11px;
144
- color: #999;
145
- opacity: 0;
146
- }
147
-
148
- [data-composition-id="nyt-chart"] .grid-line {
149
- stroke: #e8e8e8;
150
- stroke-width: 0.5px;
151
- opacity: 0;
152
- }
153
-
154
- [data-composition-id="nyt-chart"] .axis-label {
155
- font-size: 14px;
156
- fill: #666;
157
- text-anchor: middle;
158
- }
159
-
160
- [data-composition-id="nyt-chart"] .bar {
161
- fill: #5c5c5c;
162
- }
163
-
164
- [data-composition-id="nyt-chart"] .bar-label {
165
- font-size: 14px;
166
- fill: #333;
167
- text-anchor: middle;
168
- opacity: 0;
169
- }
170
-
171
- [data-composition-id="nyt-chart"] .line-label {
172
- font-size: 14px;
173
- fill: #326fa8;
174
- font-weight: 600;
175
- text-anchor: middle;
176
- opacity: 0;
177
- }
178
- </style>
179
-
180
- <script>
181
- (function initChart() {
182
- // Wait for DOM — the runtime may not have cloned the template content yet
183
- const svg = document.querySelector(`[data-composition-id="nyt-chart"] .chart-svg`);
184
- if (!svg) {
185
- setTimeout(initChart, 50);
186
- return;
187
- }
188
- const tl = gsap.timeline({ paused: true });
189
-
190
- // Data
191
- const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];
192
- const revenueData = [8, 12, 15, 11, 18, 22]; // in thousands
193
- const conversionData = [2.1, 2.8, 3.2, 2.9, 3.8, 4.2]; // in percentage
194
-
195
- // Chart Dimensions
196
- const chartWidth = 1400;
197
- const chartHeight = 500;
198
- const startX = 100;
199
- const startY = 600;
200
- const barWidth = 80;
201
- const spacing = (chartWidth - barWidth * months.length) / (months.length + 1);
202
-
203
- // Scales
204
- const maxRevenue = 25;
205
- const maxConversion = 5;
206
-
207
- const getBarY = (val) => startY - (val / maxRevenue) * chartHeight;
208
- const getLineY = (val) => startY - (val / maxConversion) * chartHeight;
209
- const getX = (i) => startX + spacing + i * (barWidth + spacing) + barWidth / 2;
210
-
211
- // 1. Setup Elements (svg already found above)
212
- const gridGroup = svg.querySelector(".gridlines");
213
- const xAxisGroup = svg.querySelector(".x-axis-labels");
214
- const barsGroup = svg.querySelector(".bars-group");
215
- const barLabelsGroup = svg.querySelector(".bar-labels-group");
216
- const lineLabelsGroup = svg.querySelector(".line-labels-group");
217
- const conversionPath = svg.querySelector(".conversion-path");
218
- const lineDot = svg.querySelector(".line-dot");
219
-
220
- // Create Gridlines (5 lines)
221
- for (let i = 1; i <= 5; i++) {
222
- const y = startY - i * (chartHeight / 5);
223
- const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
224
- line.setAttribute("x1", startX);
225
- line.setAttribute("y1", y);
226
- line.setAttribute("x2", startX + chartWidth);
227
- line.setAttribute("y2", y);
228
- line.setAttribute("class", "grid-line");
229
- gridGroup.appendChild(line);
230
- }
231
-
232
- // Create X-Axis Labels & Bars & Bar Labels
233
- const linePoints = [];
234
- months.forEach((month, i) => {
235
- const x = getX(i);
236
-
237
- // X-Axis Label
238
- const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
239
- label.setAttribute("x", x);
240
- label.setAttribute("y", startY + 30);
241
- label.setAttribute("class", "axis-label");
242
- label.textContent = month;
243
- xAxisGroup.appendChild(label);
244
-
245
- // Bar
246
- const barHeight = (revenueData[i] / maxRevenue) * chartHeight;
247
- const bar = document.createElementNS("http://www.w3.org/2000/svg", "rect");
248
- bar.setAttribute("x", x - barWidth / 2);
249
- bar.setAttribute("y", startY);
250
- bar.setAttribute("width", barWidth);
251
- bar.setAttribute("height", 0);
252
- bar.setAttribute("class", "bar");
253
- barsGroup.appendChild(bar);
254
-
255
- // Bar Label
256
- const barLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
257
- barLabel.setAttribute("x", x);
258
- barLabel.setAttribute("y", getBarY(revenueData[i]) - 10);
259
- barLabel.setAttribute("class", "bar-label");
260
- barLabel.textContent = `$${revenueData[i]}K`;
261
- barLabelsGroup.appendChild(barLabel);
262
-
263
- // Line Point
264
- linePoints.push(`${x},${getLineY(conversionData[i])}`);
265
-
266
- // Line Label
267
- const lineLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
268
- lineLabel.setAttribute("x", x);
269
- lineLabel.setAttribute("y", getLineY(conversionData[i]) - 15);
270
- lineLabel.setAttribute("class", "line-label");
271
- lineLabel.textContent = `${conversionData[i]}%`;
272
- lineLabelsGroup.appendChild(lineLabel);
273
- });
274
-
275
- // Set Path Data
276
- conversionPath.setAttribute("d", `M ${linePoints.join(" L ")}`);
277
- const pathLength = conversionPath.getTotalLength();
278
- conversionPath.style.strokeDasharray = pathLength;
279
- conversionPath.style.strokeDashoffset = pathLength;
280
-
281
- // --- ANIMATION TIMELINE ---
282
-
283
- // 2. Headline & Subtitle (Draw on left-to-right)
284
- tl.to(
285
- `[data-composition-id="nyt-chart"] .headline`,
286
- {
287
- clipPath: "inset(0 0% 0 0)",
288
- duration: 1.2,
289
- ease: "power1.inOut",
290
- },
291
- 0,
292
- );
293
-
294
- tl.to(
295
- `[data-composition-id="nyt-chart"] .subtitle`,
296
- {
297
- clipPath: "inset(0 0% 0 0)",
298
- duration: 1.2,
299
- ease: "power1.inOut",
300
- },
301
- 0.2,
302
- );
303
-
304
- // 3. Inline Key
305
- tl.to(
306
- `[data-composition-id="nyt-chart"] .key`,
307
- {
308
- opacity: 1,
309
- duration: 0.4,
310
- ease: "power2.out",
311
- },
312
- 1.0,
313
- );
314
-
315
- // 4. Gridlines (Stagger bottom to top)
316
- tl.to(
317
- `[data-composition-id="nyt-chart"] .grid-line`,
318
- {
319
- opacity: 1,
320
- duration: 0.5,
321
- stagger: 0.25,
322
- ease: "power2.out",
323
- },
324
- 0.5,
325
- );
326
-
327
- // 5. Bars (Revenue)
328
- const bars = barsGroup.querySelectorAll(".bar");
329
- const barLabels = barLabelsGroup.querySelectorAll(".bar-label");
330
-
331
- bars.forEach((bar, i) => {
332
- const barHeight = (revenueData[i] / maxRevenue) * chartHeight;
333
- const startTime = 1.5 + i * 0.5; // 0.8s duration, 0.3s overlap (0.8 - 0.3 = 0.5 step)
334
-
335
- tl.to(
336
- bar,
337
- {
338
- attr: {
339
- height: barHeight,
340
- y: startY - barHeight,
341
- },
342
- duration: 0.8,
343
- ease: "power2.out",
344
- },
345
- startTime,
346
- );
347
-
348
- // 6. Bar Labels
349
- tl.to(
350
- barLabels[i],
351
- {
352
- opacity: 1,
353
- duration: 0.3,
354
- ease: "power2.out",
355
- },
356
- startTime + 0.8,
357
- );
358
- });
359
-
360
- // 7. Line (Conversion Rate)
361
- // Starts 0.5s after the last bar begins
362
- const lastBarStartTime = 1.5 + (months.length - 1) * 0.5;
363
- const lineStartTime = lastBarStartTime + 0.5;
364
-
365
- tl.to(
366
- conversionPath,
367
- {
368
- strokeDashoffset: 0,
369
- duration: 3,
370
- ease: "none",
371
- },
372
- lineStartTime,
373
- );
374
-
375
- // 8. Line Dot & 9. Line Labels
376
- // We need to sync the dot and labels with the path drawing
377
- tl.set(
378
- lineDot,
379
- { opacity: 1, attr: { cx: getX(0), cy: getLineY(conversionData[0]) } },
380
- lineStartTime,
381
- );
382
-
383
- const lineLabels = lineLabelsGroup.querySelectorAll(".line-label");
384
- months.forEach((_, i) => {
385
- if (i > 0) {
386
- const x = getX(i);
387
- const y = getLineY(conversionData[i]);
388
- tl.to(
389
- lineDot,
390
- {
391
- attr: {
392
- cx: x,
393
- cy: y,
394
- },
395
- duration: 3 / (months.length - 1),
396
- ease: "none",
397
- },
398
- lineStartTime + ((i - 1) / (months.length - 1)) * 3,
399
- );
400
- }
401
-
402
- const labelTime = lineStartTime + (i / (months.length - 1)) * 3 + 0.2;
403
-
404
- // Set initial text to 0.0%
405
- lineLabels[i].textContent = "0.0%";
406
-
407
- // Fade in
408
- tl.to(
409
- lineLabels[i],
410
- {
411
- opacity: 1,
412
- duration: 0.1,
413
- ease: "power2.out",
414
- },
415
- labelTime,
416
- );
417
-
418
- // Animate value
419
- const dummy = { val: 0 };
420
- tl.to(
421
- dummy,
422
- {
423
- val: conversionData[i],
424
- duration: 0.8,
425
- ease: "power2.out",
426
- onUpdate: () => {
427
- lineLabels[i].textContent = dummy.val.toFixed(1) + "%";
428
- },
429
- },
430
- labelTime,
431
- );
432
- });
433
-
434
- // 11. Source Line
435
- tl.to(
436
- `[data-composition-id="nyt-chart"] .source`,
437
- {
438
- opacity: 1,
439
- duration: 0.5,
440
- ease: "power2.out",
441
- },
442
- ">+0.5",
443
- );
444
-
445
- // Register timeline
446
- window.__timelines["nyt-chart"] = tl;
447
- })();
448
- </script>
449
- </div>
450
- </template>
@@ -1,51 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=1920, height=1080" />
6
- <title>NYT Graph</title>
7
- <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
8
- <style>
9
- * {
10
- margin: 0;
11
- padding: 0;
12
- box-sizing: border-box;
13
- }
14
- html,
15
- body {
16
- margin: 0;
17
- width: 1920px;
18
- height: 1080px;
19
- overflow: hidden;
20
- background-color: #faf9f6;
21
- }
22
- </style>
23
- </head>
24
- <body>
25
- <div
26
- id="root"
27
- data-composition-id="main"
28
- data-start="0"
29
- data-duration="15"
30
- data-width="1920"
31
- data-height="1080"
32
- >
33
- <div
34
- id="nyt-chart-comp"
35
- data-composition-id="nyt-chart"
36
- data-composition-src="compositions/nyt-chart.html"
37
- data-start="0"
38
- data-duration="15"
39
- data-track-index="0"
40
- data-width="1920"
41
- data-height="1080"
42
- ></div>
43
- </div>
44
-
45
- <script>
46
- window.__timelines = window.__timelines || {};
47
- const tl = gsap.timeline({ paused: true });
48
- window.__timelines["main"] = tl;
49
- </script>
50
- </body>
51
- </html>
@@ -1,163 +0,0 @@
1
- <template id="captions-template">
2
- <div
3
- data-composition-id="captions"
4
- data-width="1920"
5
- data-height="1080"
6
- data-duration="__VIDEO_DURATION__"
7
- >
8
- <div id="captions-container"></div>
9
-
10
- <style>
11
- [data-composition-id="captions"] {
12
- width: 1920px;
13
- height: 1080px;
14
- pointer-events: none;
15
- }
16
-
17
- [data-composition-id="captions"] #captions-container {
18
- position: absolute;
19
- bottom: 100px; /* Position in bottom 1/3 */
20
- left: 50%;
21
- transform: translateX(-50%);
22
- display: flex;
23
- justify-content: center;
24
- align-items: center;
25
- width: 100%;
26
- height: 150px; /* Fixed height for the caption box */
27
- }
28
-
29
- .caption-box {
30
- position: absolute; /* Stacked in the same place */
31
- background-color: #ff2d8a;
32
- color: white;
33
- padding: 20px 40px;
34
- border-radius: 16px;
35
- font-family: "Nunito", sans-serif;
36
- font-weight: 900; /* Nunito Black */
37
- font-size: 64px;
38
- text-align: center;
39
- display: inline-block;
40
- white-space: nowrap;
41
- opacity: 0;
42
- visibility: hidden;
43
- transform: scale(0);
44
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
45
- }
46
- </style>
47
-
48
- <script>
49
- (function () {
50
- const TRANSCRIPT = [
51
- { text: "We", start: 0.119, end: 0.259 },
52
- { text: "asked", start: 0.319, end: 0.479 },
53
- { text: "what", start: 0.519, end: 0.659 },
54
- { text: "you", start: 0.699, end: 0.819 },
55
- { text: "needed.", start: 0.859, end: 1.819 },
56
- { text: "Forty-seven", start: 1.839, end: 2.299 },
57
- { text: "percent", start: 2.399, end: 2.679 },
58
- { text: "of", start: 2.7, end: 2.799 },
59
- { text: "you", start: 2.839, end: 2.939 },
60
- { text: "said", start: 3.039, end: 3.179 },
61
- { text: "motion", start: 3.24, end: 3.559 },
62
- { text: "graphics.", start: 3.579, end: 4.639 },
63
- { text: "Sixty-two", start: 4.659, end: 5.179 },
64
- { text: "percent", start: 5.299, end: 5.759 },
65
- { text: "said", start: 5.859, end: 5.98 },
66
- { text: "static", start: 6.099, end: 6.399 },
67
- { text: "content", start: 6.46, end: 6.879 },
68
- { text: "was", start: 6.92, end: 7.079 },
69
- { text: "costing", start: 7.099, end: 7.48 },
70
- { text: "you", start: 7.5, end: 7.579 },
71
- { text: "attention,", start: 7.679, end: 8.659 },
72
- { text: "and", start: 8.699, end: 8.86 },
73
- { text: "three", start: 8.88, end: 9.06 },
74
- { text: "out", start: 9.079, end: 9.18 },
75
- { text: "of", start: 9.199, end: 9.34 },
76
- { text: "four", start: 9.38, end: 9.799 },
77
- { text: "said", start: 9.84, end: 10.0 },
78
- { text: "you", start: 10.019, end: 10.159 },
79
- { text: "know", start: 10.179, end: 10.36 },
80
- { text: "the", start: 10.38, end: 10.479 },
81
- { text: "look", start: 10.52, end: 10.699 },
82
- { text: "you", start: 10.739, end: 10.859 },
83
- { text: "want", start: 10.98, end: 11.34 },
84
- { text: "but", start: 11.359, end: 11.52 },
85
- { text: "don't", start: 11.56, end: 11.779 },
86
- { text: "have", start: 11.819, end: 11.94 },
87
- { text: "the", start: 11.96, end: 12.06 },
88
- { text: "editing", start: 12.079, end: 12.4 },
89
- { text: "skills", start: 12.52, end: 12.86 },
90
- { text: "to", start: 12.88, end: 13.0 },
91
- { text: "get", start: 13.019, end: 13.18 },
92
- { text: "there.", start: 13.22, end: 14.22 },
93
- { text: "So", start: 14.239, end: 14.399 },
94
- { text: "we", start: 14.42, end: 14.52 },
95
- { text: "built", start: 14.619, end: 14.88 },
96
- { text: "Hyperframes", start: 15.079, end: 15.42 },
97
- ];
98
- const container = document.getElementById("captions-container");
99
- const tl = gsap.timeline({ paused: true });
100
-
101
- // Group transcript into lines (max 5 words)
102
- const lines = [];
103
- for (let i = 0; i < TRANSCRIPT.length; i += 5) {
104
- lines.push(TRANSCRIPT.slice(i, i + 5));
105
- }
106
-
107
- lines.forEach((lineWords, index) => {
108
- const lineText = lineWords.map((w) => w.text).join(" ");
109
- const startTime = lineWords[0].start;
110
- const endTime = lineWords[lineWords.length - 1].end;
111
-
112
- // Create element
113
- const el = document.createElement("div");
114
- el.className = "caption-box";
115
- el.textContent = lineText;
116
- el.id = `caption-line-${index}`;
117
- container.appendChild(el);
118
-
119
- // Animation: Pop in with scale-up bounce (0% to 110% to 100%)
120
- tl.to(
121
- el,
122
- {
123
- autoAlpha: 1,
124
- scale: 1.1,
125
- duration: 0.2,
126
- ease: "power2.out",
127
- },
128
- startTime,
129
- );
130
-
131
- tl.to(
132
- el,
133
- {
134
- scale: 1,
135
- duration: 0.1,
136
- ease: "power2.inOut",
137
- },
138
- startTime + 0.2,
139
- );
140
-
141
- // Stay visible until next line or end of its duration
142
- // We hide it when the next line starts or at its own end time
143
- const hideTime =
144
- index < lines.length - 1 ? Math.min(endTime, lines[index + 1][0].start) : endTime;
145
-
146
- tl.to(
147
- el,
148
- {
149
- autoAlpha: 0,
150
- scale: 0,
151
- duration: 0.15,
152
- ease: "power2.in",
153
- },
154
- hideTime,
155
- );
156
- });
157
-
158
- window.__timelines = window.__timelines || {};
159
- window.__timelines["captions"] = tl;
160
- })();
161
- </script>
162
- </div>
163
- </template>