Setting the file. One moment. Index · Music To Video · heygen-com/hyperframes · Skills DocsGsap Min
(opens in a new tab)
references/templates/intro-kinetic-cascade/index.html
HTML·610 lines·22 KB
<
head
>
12 <meta charset="UTF-8" />
13 <meta name="viewport" content="width=1920, height=1080" />
14 <title>Intro Kinetic Cascade · beat-synced kinetic typography — reference impl</title>
15 <script src="../../motion-primitives/assets/gsap.min.js"></script>
16 <style>
17 *,
18 *::before,
19 *::after {
20 margin: 0;
21 padding: 0;
22 box-sizing: border-box;
23 }
24 html,
25 body {
26 width: 1920px;
27 height: 1080px;
28 overflow: hidden;
29 background: #f5f5f7;
30 }
31 #root {
32 position: relative;
33 width: 1920px;
34 height: 1080px;
35 overflow: hidden;
36 background: #f5f5f7;
37 color: #1d1d1f;
38 font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
39 }
40 #stage {
41 position: absolute;
42 inset: 0;
43 }
44
45 /* A PHRASE = a stacked, left-aligned editorial poster, vertically centered.
46 Word-by-word reveal, then a hard cut-out. */
47 .phrase {
48 position: absolute;
49 left: 0;
50 right: 0;
51 top: 50%;
52 transform: translateY(-50%);
53 display: flex;
54 flex-direction: column;
55 align-items: flex-start;
56 opacity: 0;
57 will-change: opacity;
58 }
59 .line {
60 font-weight: 900;
61 letter-spacing: -0.045em;
62 line-height: 0.88;
63 white-space: nowrap;
64 }
65 .line + .line {
66 margin-top: -0.04em;
67 }
68 /* the HERO line's elegant serif-italic contrast (offline system fallback) */
69 .line.serif {
70 font-family: "Playfair Display", Georgia, "Times New Roman", serif;
71 font-style: italic;
72 letter-spacing: -0.02em;
73 }
74 .w {
75 display: inline-block;
76 opacity: 0;
77 will-change: opacity;
78 }
79 .w.grad {
80 background: var(--grad);
81 background-size: 200% 100%;
82 background-position: 0% 50%;
83 -webkit-background-clip: text;
84 background-clip: text;
85 -webkit-text-fill-color: transparent;
86 color: transparent;
87 }
88
89 /* The CLIMAX phrase SLIDES in, holds to the end, and may carry an inline
90 animated icon (the {icon} token). */
91 .climax {
92 position: absolute;
93 left: 50%;
94 top: 50%;
95 display: flex;
96 align-items: center;
97 gap: 0.32em;
98 white-space: nowrap;
99 opacity: 0;
100 font-weight: 900;
101 letter-spacing: -0.035em;
102 line-height: 1;
103 will-change: transform, opacity;
104 }
105 .cword {
106 flex-shrink: 0;
107 }
108 .icon {
109 display: inline-flex;
110 align-items: center;
111 justify-content: center;
112 position: relative;
113 flex-shrink: 0;
114 opacity: 0;
115 overflow: visible;
116 line-height: 1;
117 }
118 .icon-glyph {
119 position: relative;
120 display: flex;
121 align-items: center;
122 justify-content: center;
123 transform-origin: 50% 14%;
124 }
125 .icon-glyph svg {
126 display: block;
127 width: 100%;
128 height: 100%;
129 }
130 .ring {
131 position: absolute;
132 left: 50%;
133 top: 50%;
134 border-radius: 50%;
135 border: 3px solid var(--ring1);
136 opacity: 0;
137 transform: translate(-50%, -50%) scale(0.4);
138 }
139 .ring.ring2 {
140 border-color: var(--ring2);
141 }
142 </style>
143 </head>
144 <body>
145 <div
146 data-hf-id="hf-ikc-root"
147 id="root"
148 data-composition-id="intro-kinetic-cascade"
149 data-width="1920"
150 data-height="1080"
151 data-start="0"
152 data-duration="7"
153 data-root="true"
154 >
155 <div data-hf-id="hf-ikc-stage" id="stage"></div>
156
157 <script>
158 (function () {
159 // ════════════════════════════════════════════════════════════════════
160 // INTRO KINETIC CASCADE · beat-synced editorial kinetic typography
161 // ────────────────────────────────────────────────────────────────────
162 // A spoken/anchored line laid out as a sequence of big editorial PHRASES,
163 // each revealed word-by-word on its anchors (VO word-times OR audio onsets),
164 // hard cut-out between them, climaxing on a phrase that SLIDES in with a
165 // swappable, ringing ICON. Asset-free: system type, inline-SVG icon library,
166 // bundled GSAP. Reversed from the VO intro `act0-intro-bell`.
167 //
168 // COMPOSE BY DATA — `phrases` (the cascade) + `climax` (the finale) + `icon`
169 // (which glyph rings) + `theme` (palette). The icon is its own knob so it can
170 // match the scene: bell for timing, cursor for a click, sparkle for AI, …
171 // ════════════════════════════════════════════════════════════════════
172
173 window.__timelines = window.__timelines || {};
174
175 // ── 1. Variables ──
176 var defaults = { theme: "light", icon: "bell", phrases: "", climax: "" };
177 var vars = Object.assign(
178 {},
179 defaults,
180 window.__hyperframes && window.__hyperframes.getVariables
181 ? window.__hyperframes.getVariables()
182 : {},
183 );
184
185 // ── 2. Palette themes (accents are AA-tuned on each bg) ──
186 var THEMES = {
187 light: {
188 bg: "#f5f5f7",
189 ink: "#1d1d1f",
190 accents: ["#0b8a3e", "#0a78b4", "gradient"],
191 gradFrom: "#0a78b4",
192 gradTo: "#0b8a3e",
193 },
194 dark: {
195 bg: "#0d0e12",
196 ink: "#f2f3f5",
197 accents: ["#3be06b", "#37c6ff", "gradient"],
198 gradFrom: "#37c6ff",
199 gradTo: "#3be06b",
200 },
201 bold: {
202 bg: "#0a1228",
203 ink: "#ffffff",
204 accents: ["#ffd23f", "#ff5da2", "gradient"],
205 gradFrom: "#ff5da2",
206 gradTo: "#ffd23f",
207 },
208 };
209 var TH = THEMES[String(vars.theme || "").trim()] || THEMES.light;
210
211 function hexRgba(hex, a) {
212 var h = String(hex).replace(/^#/, "");
213 if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
214 var n = parseInt(h, 16);
215 return (
216 "rgba(" + ((n >> 16) & 255) + "," + ((n >> 8) & 255) + "," + (n & 255) + "," + a + ")"
217 );
218 }
219
220 // ── 3. Inline-SVG icon library (asset-free; fill follows the text color) ──
221 var ICONS = {
222 bell: '<path d="M12 2.2a1.9 1.9 0 0 1 1.9 1.9v.7a6.2 6.2 0 0 1 4.3 5.9v3.5l1.9 2.1v1.1H3.9v-1.1l1.9-2.1V10.7a6.2 6.2 0 0 1 4.3-5.9v-.7A1.9 1.9 0 0 1 12 2.2zM9.6 19.6h4.8a2.4 2.4 0 0 1-4.8 0z"/>',
223 cursor: '<path d="M5.5 2.6l13.4 7.9-5.9 1.2-1.1 5.9-6.4-15z"/>',
224 sparkle: '<path d="M12 1.8l2.1 6.9 6.9 2.1-6.9 2.1L12 19.8l-2.1-6.9L3 10.8l6.9-2.1z"/>',
225 bolt: '<path d="M13.2 1.8L4.4 13.6h5.6l-1.2 8.6 9.2-12.4h-5.8z"/>',
226 play: '<path d="M12 2.4a9.6 9.6 0 1 0 0 19.2 9.6 9.6 0 0 0 0-19.2zm-2.1 5.1l6 4.5-6 4.5z"/>',
227 heart:
228 '<path d="M12 21.2S2.8 15.4 2.8 8.9C2.8 5.8 5.1 4 7.6 4c1.9 0 3.4 1.1 4.4 2.6C13 5.1 14.5 4 16.4 4c2.5 0 4.8 1.8 4.8 4.9 0 6.5-9.2 12.3-9.2 12.3z"/>',
229 check: '<path d="M9.6 16.7L4.8 11.9l1.7-1.7 3.1 3.1 7.9-7.9 1.7 1.7z"/>',
230 star: '<path d="M12 2.2l2.9 6.4 7 .6-5.3 4.6 1.6 6.8L12 17.4 5.8 20.6l1.6-6.8L2.1 9.2l7-.6z"/>',
231 };
232 function iconSVG(name) {
233 return (
234 '<svg viewBox="0 0 24 24" fill="currentColor" width="100%" height="100%">' +
235 ICONS[name] +
236 "</svg>"
237 );
238 }
239 // Insert SVG markup as SANITIZED parsed nodes — never innerHTML, and never
240 // a raw appendChild. DOMParser builds the tree, then cleanSvg() hard-strips
241 // everything that isn't inert drawing: only allow-listed shape elements +
242 // presentation attributes survive, so <script>, <image>/<use>/<foreignObject>,
243 // javascript: hrefs and every on* handler are removed before the nodes ever
244 // enter the live document. A custom icon SVG therefore can't smuggle active
245 // content (CWE-79 DOM-XSS). Non-SVG / malformed input falls back to text.
246 var SVG_OK_TAGS = {
247 svg: 1,
248 g: 1,
249 path: 1,
250 line: 1,
251 polyline: 1,
252 polygon: 1,
253 rect: 1,
254 circle: 1,
255 ellipse: 1,
256 defs: 1,
257 lineargradient: 1,
258 radialgradient: 1,
259 stop: 1,
260 clippath: 1,
261 title: 1,
262 desc: 1,
263 text: 1,
264 tspan: 1,
265 };
266 var SVG_OK_ATTRS = {
267 viewbox: 1,
268 xmlns: 1,
269 width: 1,
270 height: 1,
271 fill: 1,
272 "fill-rule": 1,
273 "fill-opacity": 1,
274 stroke: 1,
275 "stroke-width": 1,
276 "stroke-linecap": 1,
277 "stroke-linejoin": 1,
278 "stroke-dasharray": 1,
279 "stroke-dashoffset": 1,
280 "stroke-opacity": 1,
281 opacity: 1,
282 d: 1,
283 x: 1,
284 y: 1,
285 x1: 1,
286 y1: 1,
287 x2: 1,
288 y2: 1,
289 cx: 1,
290 cy: 1,
291 r: 1,
292 rx: 1,
293 ry: 1,
294 points: 1,
295 transform: 1,
296 offset: 1,
297 "stop-color": 1,
298 "stop-opacity": 1,
299 gradientunits: 1,
300 gradienttransform: 1,
301 "clip-path": 1,
302 "clip-rule": 1,
303 class: 1,
304 id: 1,
305 };
306 function cleanSvg(node) {
307 var attrs = Array.prototype.slice.call(node.attributes || []);
308 for (var a = 0; a < attrs.length; a++) {
309 if (!SVG_OK_ATTRS[attrs[a].name.toLowerCase()]) node.removeAttribute(attrs[a].name);
310 }
311 var kids = Array.prototype.slice.call(node.childNodes);
312 for (var k = 0; k < kids.length; k++) {
313 var c = kids[k];
314 if (c.nodeType === 1) {
315 if (SVG_OK_TAGS[(c.localName || c.nodeName).toLowerCase()]) cleanSvg(c);
316 else node.removeChild(c);
317 } else if (c.nodeType !== 3) {
318 node.removeChild(c);
319 }
320 }
321 }
322 function setSvg(el, markup) {
323 el.textContent = "";
324 var doc = new DOMParser().parseFromString(String(markup), "image/svg+xml");
325 var root = doc.documentElement;
326 if (
327 !root ||
328 root.nodeName.toLowerCase() !== "svg" ||
329 doc.getElementsByTagName("parsererror").length
330 ) {
331 el.textContent = String(markup);
332 return;
333 }
334 cleanSvg(root);
335 el.appendChild(document.importNode(root, true));
336 }
337
338 // ── 4. Default program (reversed from act0-intro-bell) ──
339 var DEFAULT_PHRASES = [
340 {
341 out: 1.36,
342 lines: [
343 { text: "If you've", size: 220, x: 140 },
344 { text: "ever", size: 520, x: 240, hero: true, font: "serif" },
345 { text: "used AI", size: 280, x: 780 },
346 ],
347 times: [0.14, 0.3, 0.55, 0.87, 1.06],
348 },
349 {
350 out: 3.06,
351 lines: [
352 { text: "for", size: 220, x: 160 },
353 { text: "video", size: 500, x: 220, hero: true },
354 { text: "editing before,", size: 200, x: 220 },
355 ],
356 times: [1.36, 1.58, 1.92, 2.48],
357 },
358 {
359 out: 3.79,
360 lines: [
361 { text: "you", size: 240, x: 200 },
362 { text: "know", size: 500, x: 260, hero: true, accent: "gradient" },
363 { text: "that", size: 340, x: 820 },
364 ],
365 times: [3.06, 3.3, 3.54],
366 },
367 ];
368 var DEFAULT_CLIMAX = {
369 text: "timing is {icon} tricky.",
370 in: 3.79,
371 iconAt: 4.9,
372 hold: 1.3,
373 size: 150,
374 };
375
376 var WORD_FADE = 0.18,
377 CUT = 0.1,
378 SLIDE = 0.42,
379 OFFSCREEN_R = 1400;
380
381 function parseJSON(v, fallback) {
382 if (!v) return fallback;
383 if (typeof v !== "string") return v;
384 try {
385 var p = JSON.parse(v);
386 return p || fallback;
387 } catch (e) {
388 return fallback;
389 }
390 }
391 var PHRASES = parseJSON(vars.phrases, DEFAULT_PHRASES);
392 if (!PHRASES.length) PHRASES = DEFAULT_PHRASES;
393 var C = Object.assign({}, DEFAULT_CLIMAX, parseJSON(vars.climax, {}));
394
395 var words = function (s) {
396 return String(s).trim().split(/\s+/).filter(Boolean);
397 };
398
399 // ── 5. Theme the field ──
400 var root = document.getElementById("root");
401 var stage = document.getElementById("stage");
402 root.style.background = TH.bg;
403 root.style.color = TH.ink;
404 document.documentElement.style.background = TH.bg;
405 document.body.style.background = TH.bg;
406 var GRAD =
407 "linear-gradient(90deg," +
408 TH.gradFrom +
409 " 0%," +
410 TH.gradTo +
411 " 50%," +
412 TH.gradFrom +
413 " 100%)";
414
415 // ── 6. Build phrase DOM, collect word spans in reading order ──
416 PHRASES.forEach(function (p, pi) {
417 var pEl = document.createElement("div");
418 pEl.className = "phrase";
419 var accent = p.accent || TH.accents[pi % TH.accents.length];
420 p._spans = [];
421 p._grad = [];
422 p.lines.forEach(function (ln) {
423 var lEl = document.createElement("div");
424 lEl.className = "line" + (ln.font === "serif" ? " serif" : "");
425 lEl.style.fontSize = ln.size + "px";
426 lEl.style.marginLeft = (ln.x != null ? ln.x : 0) + "px";
427 var heroColor = ln.hero ? ln.accent || accent : null;
428 words(ln.text).forEach(function (wtext) {
429 var w = document.createElement("span");
430 w.className = "w";
431 w.textContent = wtext;
432 if (heroColor === "gradient") {
433 w.classList.add("grad");
434 w.style.setProperty("--grad", GRAD);
435 p._grad.push(w);
436 } else if (heroColor) w.style.color = heroColor;
437 lEl.appendChild(w);
438 lEl.appendChild(document.createTextNode(" "));
439 p._spans.push(w);
440 });
441 pEl.appendChild(lEl);
442 });
443 stage.appendChild(pEl);
444 p._el = pEl;
445 });
446
447 // ── 7. Build the climax (slide-in + swappable icon) ──
448 var cEl = document.createElement("div");
449 cEl.className = "climax";
450 cEl.style.fontSize = C.size + "px";
451 cEl.style.setProperty("--ring1", hexRgba(TH.ink, 0.25));
452 cEl.style.setProperty("--ring2", hexRgba(TH.ink, 0.1));
453 var iconPx = Math.round(C.size * 1.7);
454 var iconEl = null,
455 rings = [];
456 var iconSpec = String(vars.icon == null ? "bell" : vars.icon).trim();
457
458 words(C.text).forEach(function (tok) {
459 if (tok === "{icon}") {
460 if (!iconSpec || iconSpec === "none") return; // drop the slot → pure-text climax
461 iconEl = document.createElement("span");
462 iconEl.className = "icon";
463 iconEl.style.width = iconPx + "px";
464 iconEl.style.height = iconPx + "px";
465 var r1 = document.createElement("span");
466 r1.className = "ring";
467 var r2 = document.createElement("span");
468 r2.className = "ring ring2";
469 [r1, r2].forEach(function (r) {
470 var rs = Math.round(iconPx * 1.3);
471 r.style.width = rs + "px";
472 r.style.height = rs + "px";
473 iconEl.appendChild(r);
474 });
475 rings = [r1, r2];
476 var glyph = document.createElement("span");
477 glyph.className = "icon-glyph";
478 glyph.style.width = iconPx + "px";
479 glyph.style.height = iconPx + "px";
480 glyph.style.color = TH.ink;
481 if (ICONS[iconSpec]) {
482 setSvg(glyph, iconSVG(iconSpec)); // library icon
483 } else if (iconSpec.slice(0, 4).toLowerCase() === "<svg") {
484 setSvg(glyph, iconSpec); // custom inline SVG
485 } else {
486 glyph.style.fontSize = Math.round(iconPx * 0.86) + "px"; // emoji / text glyph
487 glyph.textContent = iconSpec;
488 }
489 iconEl.appendChild(glyph);
490 cEl.appendChild(iconEl);
491 } else {
492 var w = document.createElement("span");
493 w.className = "cword";
494 w.textContent = tok;
495 cEl.appendChild(w);
496 }
497 });
498 stage.appendChild(cEl);
499
500 // ── 8. Timeline ──
501 var tl = gsap.timeline({ paused: true });
502
503 PHRASES.forEach(function (p) {
504 gsap.set(p._el, { opacity: 0 });
505 gsap.set(p._spans, { opacity: 0 });
506 });
507 gsap.set(cEl, { xPercent: -50, yPercent: -50, x: OFFSCREEN_R, opacity: 0 });
508 if (iconEl) {
509 gsap.set(iconEl, { opacity: 0 });
510 gsap.set(iconEl.querySelector(".icon-glyph"), { scale: 0.4, rotation: -8, opacity: 0 });
511 gsap.set(rings, { opacity: 0, scale: 0.4 });
512 }
513
514 PHRASES.forEach(function (p, pi) {
515 var n = p._spans.length;
516 var prevOut = pi === 0 ? 0.15 : PHRASES[pi - 1].out;
517 var times = p.times && p.times.length === n ? p.times : null;
518 if (!times) {
519 var start = p.start != null ? p.start : prevOut;
520 var end = p.out - Math.max(0.25, (p.out - start) * 0.18);
521 times = p._spans.map(function (_, i) {
522 return n <= 1 ? start : start + ((end - start) * i) / (n - 1);
523 });
524 }
525 tl.to(p._el, { opacity: 1, duration: 0.01, overwrite: "auto" }, times[0]);
526 p._spans.forEach(function (w, i) {
527 tl.to(
528 w,
529 { opacity: 1, duration: WORD_FADE, ease: "power2.out", overwrite: "auto" },
530 times[i],
531 );
532 });
533 if (p._grad.length) {
534 tl.fromTo(
535 p._grad,
536 { backgroundPosition: "0% 50%" },
537 {
538 backgroundPosition: "-200% 50%",
539 duration: Math.max(0.6, p.out - times[0]),
540 ease: "none",
541 },
542 times[0],
543 );
544 }
545 tl.to(
546 p._el,
547 { opacity: 0, duration: CUT, ease: "power2.in", overwrite: "auto" },
548 p.out - CUT,
549 );
550 });
551
552 // climax slide-in
553 tl.to(
554 cEl,
555 { x: 0, opacity: 1, duration: SLIDE, ease: "power3.out", overwrite: "auto" },
556 C.in,
557 );
558
559 // icon pop + ring
560 if (iconEl) {
561 var glyph = iconEl.querySelector(".icon-glyph");
562 tl.to(
563 iconEl,
564 { opacity: 1, duration: 0.3, ease: "power3.out", overwrite: "auto" },
565 C.iconAt,
566 );
567 tl.fromTo(
568 glyph,
569 { scale: 0.4, rotation: -8, opacity: 0 },
570 {
571 scale: 1,
572 rotation: 0,
573 opacity: 1,
574 duration: 0.35,
575 ease: "back.out(2.4)",
576 overwrite: "auto",
577 },
578 C.iconAt + 0.05,
579 );
580 tl.to(
581 glyph,
582 { rotation: 13, duration: 0.1, ease: "power2.out", overwrite: "auto" },
583 C.iconAt + 0.4,
584 );
585 tl.to(
586 glyph,
587 { rotation: 0, duration: 0.85, ease: "elastic.out(1, 0.25)", overwrite: "auto" },
588 C.iconAt + 0.5,
589 );
590 [0, 0.28].forEach(function (off) {
591 tl.fromTo(
592 rings,
593 { scale: 0.4, opacity: 0.55 },
594 { scale: 1.5, opacity: 0, duration: 0.8, ease: "power2.out", stagger: 0.08 },
595 C.iconAt + 0.42 + off,
596 );
597 });
598 }
599
600 var TOTAL = Math.round((C.iconAt + C.hold) * 100) / 100;
601 tl.to({}, { duration: TOTAL }, 0);
602 root.setAttribute("data-duration", String(TOTAL));
603 tl.seek(0);
604
605 window.__timelines["intro-kinetic-cascade"] = tl;
606 })();
607 </script>
608 </div>
609 </body>
610</html>