Setting the file. One moment. Index · Music To Video · heygen-com/hyperframes · Skills DocsGsap Min
(opens in a new tab)
references/templates/split-anchor-word-slot/index.html
HTML·594 lines·24 KB
12
<
head
>
13 <meta charset="utf-8" />
14 <meta name="viewport" content="width=1920, height=1080" />
15 <title>Split-Anchor Word-Slot · beat-synced kinetic typography — group_6 reference impl</title>
16 <script src="../../motion-primitives/assets/gsap.min.js"></script>
17 <style>
18 *,
19 *::before,
20 *::after {
21 margin: 0;
22 padding: 0;
23 box-sizing: border-box;
24 }
25 html,
26 body {
27 background: #0a84ff;
28 overflow: hidden;
29 font-family: "Helvetica Neue", "Arial Black", Arial, sans-serif;
30 }
31 #root {
32 position: relative;
33 width: 1920px;
34 height: 1080px;
35 overflow: hidden;
36 background: #0a84ff;
37 }
38 /* #frame = the CENTERING wrapper. CSS positions it slightly ABOVE center (the source
39 sits high-center); GSAP never touches #frame, so the CSS transform here is safe. */
40 #frame {
41 position: absolute;
42 left: 50%;
43 top: 43%;
44 transform: translate(-50%, -50%);
45 }
46 /* #lockup = the thing GSAP jitters/tilts (shake) — NO CSS transform, so GSAP owns
47 x/y/rotation cleanly. Holds the base tilt the whole brutalist lockup leans at. */
48 #lockup {
49 position: relative;
50 display: flex;
51 align-items: center;
52 gap: 0;
53 will-change: transform;
54 }
55 /* anchors LEFT-aligned (ragged right) so the slot box clears the shorter words and
56 never hides the held text. */
57 #anchors {
58 display: flex;
59 flex-direction: column;
60 align-items: flex-start;
61 }
62 .anchor {
63 font-weight: 900;
64 line-height: 0.96;
65 letter-spacing: -0.045em;
66 text-transform: lowercase;
67 white-space: nowrap;
68 will-change: transform, opacity;
69 }
70 /* the inner .t span carries the faux-EXTENDED width (scaleX) for the ANCHORS — they
71 have no background, so nothing overflows. GSAP animates the outer element; the
72 span's scaleX is never touched by GSAP, so the two never fight (asset-free
73 Druk-Wide / Extended-Black look from a system font). */
74 .t {
75 display: inline-block;
76 transform: scaleX(1.12);
77 transform-origin: left center;
78 }
79 /* slot TAPES carry their extend via the LINE's GSAP scaleX instead, so the tape
80 BACKGROUND stretches WITH the text and the word never overflows the strip. */
81 .sline .t {
82 transform: scaleX(1);
83 }
84 /* #slotbox = a TRANSPARENT container sitting just RIGHT of the anchors (small positive
85 margin → it never covers the held words). Holds the 3 stacked cards + the word tapes. */
86 /* small NEGATIVE margin = the source's tight collage TUCK: the tape kisses / lightly
87 presses over the tail of the longest anchor without hiding the glyph (the tape's own
88 text starts after its left padding, so words never collide). Tune this for press amount. */
89 #slotbox {
90 position: relative;
91 margin-left: -0.12em;
92 will-change: transform, opacity;
93 backface-visibility: hidden;
94 transform-origin: 50% 42%;
95 }
96 /* THREE stacked, offset, torn CARDS = the brutalist collage. Transparent during word
97 phases; `bg` turns them black for the empty box + the expand zoom (they scale as one
98 with #slotbox, staying staggered). Siblings — NOT parent-clipped — so the offsets show. */
99 .card {
100 position: absolute;
101 inset: 0;
102 background: transparent;
103 will-change: transform;
104 }
105 #card1 {
106 z-index: -1;
107 clip-path: polygon(0% 4%, 97% 0%, 100% 92%, 3% 100%);
108 }
109 #card2 {
110 z-index: -2;
111 transform: translate(22px, 17px) rotate(-2.8deg);
112 clip-path: polygon(0% 2%, 100% 0%, 98% 100%, 2% 96%);
113 }
114 #card3 {
115 z-index: -3;
116 transform: translate(-15px, 10px) rotate(2.1deg);
117 clip-path: polygon(0% 6%, 96% 3%, 100% 95%, 4% 100%);
118 }
119 .lines {
120 position: relative;
121 z-index: 1;
122 display: grid;
123 }
124 .sgroup {
125 grid-area: 1 / 1;
126 display: flex;
127 flex-direction: column;
128 align-items: flex-start;
129 gap: 3px;
130 }
131 /* each .sline is its OWN torn TAPE — sized to its word, its GSAP scaleX stretches
132 bg+text TOGETHER (no overflow), its own micro-rotation + stair offset + shadow. */
133 .sline {
134 font-weight: 900;
135 line-height: 0.96;
136 letter-spacing: -0.045em;
137 white-space: nowrap;
138 text-align: left;
139 background: #f4f4f4;
140 padding: 0.03em 0.22em 0.08em 0.16em;
141 clip-path: polygon(0% 6%, 98% 0%, 100% 90%, 3% 100%);
142 box-shadow: 5px 6px 0 rgba(0, 0, 0, 0.16);
143 transform-origin: 0% 50%; /* GSAP scaleX stretches RIGHTward only — never back over the anchors */
144 will-change: transform, opacity;
145 }
146 </style>
147 </head>
148 <body>
149 <!-- duration 5.41s = the full group_6 span (lockup → 2 slot cycles → bg flip + shake → expand wipe) -->
150 <div
151 data-hf-id="hf-g6rt"
152 id="root"
153 data-composition-id="group-six-recreate"
154 data-width="1920"
155 data-height="1080"
156 data-start="0"
157 data-duration="5.41"
158 data-root="true"
159 >
160 <div data-hf-id="hf-g6frame" id="frame">
161 <div data-hf-id="hf-g6lock" id="lockup">
162 <div data-hf-id="hf-g6anch" id="anchors"></div>
163 <div data-hf-id="hf-g6box" id="slotbox">
164 <div data-hf-id="hf-g6c1" id="card1" class="card"></div>
165 <div data-hf-id="hf-g6c2" id="card2" class="card"></div>
166 <div data-hf-id="hf-g6c3" id="card3" class="card"></div>
167 <div class="lines" id="lines"></div>
168 </div>
169 </div>
170 </div>
171 </div>
172
173 <script>
174 (function () {
175 // ════════════════════════════════════════════════════════════════════
176 // SPLIT-ANCHOR WORD-SLOT · ONE base lockup, FIVE beat-synced operators
177 // ────────────────────────────────────────────────────────────────────
178 // BASE : a kinetic-typography LOCKUP — a HELD left "anchor" column of R rows
179 // (fixed words) beside a torn-paper word-SLOT box on the right. The
180 // whole lockup LEANS at a base tilt (brutalist/acid attitude). R is
181 // data (the `anchors` comma-list). Asset-free: text + colored boxes.
182 // Reversed from group_6 + tuned against a Gemini side-by-side pass.
183 //
184 // OP lockup : pop the R anchor words in (punchy scale, faux motion-blur squash)
185 // + reveal the empty slot box. The held stage. (source f3–f12)
186 // OP slot : one word-GROUP cycle — lines WHIP in from the right (expo.out +
187 // scaleX stretch = fake motion blur), hold, then arc out down-left.
188 // own color per line. repeatable. (source f24–f44, f49–f66)
189 // OP bg : HARD-CUT flip the whole scene to another theme on a beat (bg +
190 // anchor ink + box fill + slot ink invert, 0ms). 0/1/2× = changes
191 // none / once / twice. (source f83 blue→green)
192 // OP shake : global per-beat JOLT of the lockup (x/y + rotation around the base
193 // tilt) with a back-ease recoil — one punch per beat. (source f52–f100)
194 // OP expand : EXIT wipe — fade text, then the slot box ZOOMS (expo.in, blur) from
195 // its own centre until it swallows the frame. (source f106–f117)
196 //
197 // ARCHITECTURE: every operator writes GSAP tweens at ABSOLUTE audio seconds onto
198 // ONE paused timeline → order-independent, may overlap (shake runs under slot/bg).
199 // A data PROGRAM lists which ops run on which beats; §7 dispatches it. Compose by
200 // editing DATA (the PROGRAM / `program` var / program.json), never operator code.
201 // ════════════════════════════════════════════════════════════════════
202
203 // ── 1. Content variables ──
204 var defaults = {
205 bgColor: "#0a84ff",
206 anchors: "studio,contre,courant",
207 theme: 0,
208 showText: true,
209 program: "",
210 };
211 var vars = Object.assign(
212 {},
213 defaults,
214 window.__hyperframes && window.__hyperframes.getVariables
215 ? window.__hyperframes.getVariables()
216 : {},
217 );
218 var ANCHORS = String(vars.anchors)
219 .split(",")
220 .map(function (s) {
221 return s.trim();
222 })
223 .filter(function (s) {
224 return s.length;
225 });
226 if (!ANCHORS.length) ANCHORS = ["studio", "contre", "courant"];
227 var BASE_THEME = Math.max(0, parseInt(vars.theme, 10) || 0);
228 var SHOW_TEXT = vars.showText !== false && vars.showText !== "false";
229
230 // base attitude: the whole lockup leans; the box leans extra the other way.
231 var BASE_TILT = -3.5; // deg, the lockup's resting lean
232 var BOX_TILT = 2.6; // deg, the slot box's extra lean vs the anchors
233
234 // ── 2. THEME LIBRARY — brighter electric/fluoro to match the source ──
235 var THEMES = [
236 { bg: "#0a84ff", ink: "#ffffff", box: "#f4f4f4", boxInk: "#0a84ff" }, // 0 electric blue
237 { bg: "#5cf03a", ink: "#0a0a0a", box: "#0a0a0a", boxInk: "#5cf03a" }, // 1 lime/acid green (invert)
238 { bg: "#0a0a0a", ink: "#ffffff", box: "#ffffff", boxInk: "#0a0a0a" }, // 2 ink
239 { bg: "#ffe400", ink: "#0a0a0a", box: "#0a0a0a", boxInk: "#ffe400" }, // 3 amber
240 ];
241 var ACCENTS = ["#0a84ff", "#39e024", "#ff3bd0", "#ffe400", "#ffffff", "#0a0a0a"];
242 function theme(i) {
243 return THEMES[((i % THEMES.length) + THEMES.length) % THEMES.length];
244 }
245 function accent(i) {
246 return ACCENTS[((i % ACCENTS.length) + ACCENTS.length) % ACCENTS.length];
247 }
248
249 // ── 3. THE PROGRAM (data) — see program.json ──
250 // group_6: ~92 BPM · downbeats 2.322(f56)/4.923(f118) · hihat fill 2.16–4.18s.
251 var DEFAULT_PROGRAM = [
252 { op: "lockup", enter: [0.125, 0.333, 0.5], box: 0.292, theme: 0 },
253 {
254 op: "slot",
255 words: ["I bring", "ideas", "to life"],
256 enter: [1.0, 1.167, 1.292],
257 exit: [1.667, 1.75, 1.833],
258 },
259 {
260 op: "slot",
261 words: ["you", "wanna", "see?"],
262 enter: [2.042, 2.208, 2.375],
263 exit: [2.75, 2.75, 2.75],
264 colors: [0, 1, 1],
265 },
266 {
267 op: "shake",
268 amp: 26,
269 rot: 2.4,
270 settle: 0.16,
271 beats: [
272 2.16, 2.3, 2.44, 2.62, 2.76, 2.88, 3.06, 3.27, 3.41, 3.53, 3.71, 3.85, 4.09, 4.18,
273 ],
274 },
275 { op: "bg", at: 3.458, theme: 1 },
276 { op: "expand", fade: 4.18, at: 4.4, to: 4.82 },
277 ];
278 var DUR = 5.41;
279
280 var PROGRAM = DEFAULT_PROGRAM;
281 if (vars.program && String(vars.program).trim()) {
282 try {
283 var parsed = typeof vars.program === "string" ? JSON.parse(vars.program) : vars.program;
284 if (parsed && parsed.length) PROGRAM = parsed;
285 } catch (e) {
286 PROGRAM = DEFAULT_PROGRAM;
287 }
288 }
289
290 // ── 4. Build the LOCKUP ──────────────────────────────────────────────────
291 var R = ANCHORS.length;
292 var slotSteps = PROGRAM.filter(function (s) {
293 return s.op === "slot";
294 });
295 var maxSlot = slotSteps.reduce(function (m, s) {
296 return Math.max(m, (s.words || []).length);
297 }, 0);
298 var maxRows = Math.max(R, maxSlot, 1);
299 var fontPx = maxRows <= 3 ? 116 : maxRows === 4 ? 92 : 74;
300 var rowH = Math.round(fontPx * 0.99);
301
302 var root = document.getElementById("root");
303 var lockupEl = document.getElementById("lockup");
304 var anchorsBox = document.getElementById("anchors");
305 var slotbox = document.getElementById("slotbox");
306 var card1 = document.getElementById("card1");
307 var card2 = document.getElementById("card2");
308 var card3 = document.getElementById("card3");
309 var cards = [card1, card2, card3];
310 var linesBox = document.getElementById("lines");
311
312 var TH0 = theme(BASE_THEME);
313 document.documentElement.style.background = vars.bgColor || TH0.bg;
314 document.body.style.background = vars.bgColor || TH0.bg;
315 root.style.background = vars.bgColor || TH0.bg;
316
317 function mkWord(text, px, h) {
318 // word node with an inner faux-extended span
319 var d = document.createElement("div");
320 d.style.fontSize = px + "px";
321 d.style.height = h + "px";
322 var sp = document.createElement("span");
323 sp.className = "t";
324 sp.textContent = SHOW_TEXT ? text : "";
325 d.appendChild(sp);
326 return { row: d, span: sp };
327 }
328
329 // left anchor column (held words). The faux-extend scaleX renders the glyphs WIDER
330 // than their layout box (origin-left → overflow to the right); pin each row's width
331 // to the SCALED width so the column — and the slot box to its right — accounts for
332 // it and never overlaps the held words.
333 var anchorEls = [];
334 for (var a = 0; a < R; a++) {
335 var w = mkWord(ANCHORS[a], fontPx, rowH);
336 w.row.className = "anchor";
337 w.span.style.color = TH0.ink;
338 anchorsBox.appendChild(w.row);
339 w.row.style.width = Math.ceil(w.span.getBoundingClientRect().width + 2) + "px";
340 anchorEls.push(w.row);
341 }
342
343 // the slot box is a TRANSPARENT backing during word phases (so only the per-line
344 // tapes show on the bg — no dead space). `bg` turns it into a solid torn card for
345 // the empty state + the expand zoom. ONE .sgroup per slot step, stacked in one grid
346 // cell. Each .sline is its own TAPE (own width/rotation/offset). No runtime text edit.
347 slotbox.style.background = "transparent";
348 cards.forEach(function (c) {
349 c.style.background = "transparent";
350 });
351 var groups = []; // [{ lines:[rowEls], spans:[spanEls] }]
352 slotSteps.forEach(function (step) {
353 var g = document.createElement("div");
354 g.className = "sgroup";
355 var lines = [],
356 spans = [];
357 (step.words || []).forEach(function (word, li) {
358 var lw = mkWord(word, fontPx, rowH);
359 lw.row.className = "sline";
360 lw.row.style.background = TH0.box; // each line = its own tape
361 lw.row.style.marginLeft = [0, 16, 8, 24][li % 4] + "px"; // rightward stair offset (collage)
362 lw.span.style.color = TH0.boxInk;
363 g.appendChild(lw.row);
364 lines.push(lw.row);
365 spans.push(lw.span);
366 });
367 linesBox.appendChild(g);
368 groups.push({ lines: lines, spans: spans });
369 step.__g = groups.length - 1; // remember which group this step drives
370 });
371 var allSlotLines = groups.reduce(function (acc, g) {
372 return acc.concat(g.lines);
373 }, []);
374 var allSlotSpans = groups.reduce(function (acc, g) {
375 return acc.concat(g.spans);
376 }, []);
377
378 // ── 5. Timeline + helpers ────────────────────────────────────────────────
379 window.__timelines = window.__timelines || {};
380 var tl = gsap.timeline({ paused: true });
381
382 function beatAt(i, n, beats) {
383 if (!beats || !beats.length) return 0;
384 return beats[Math.min(beats.length - 1, Math.floor((i * beats.length) / n))];
385 }
386 function jolt(k) {
387 // deterministic per-beat shake offset (no Math.random)
388 return { x: ((k * 37) % 11) - 5, y: ((k * 53) % 9) - 4, r: ((k * 29) % 7) - 3 };
389 }
390 function lineRot(i) {
391 return [-2.0, 1.6, -1.0, 2.2][i % 4];
392 } // per-line lean
393
394 // rest pose: lockup leans at BASE_TILT, box leans extra, all content hidden.
395 gsap.set(lockupEl, { rotation: BASE_TILT });
396 gsap.set(slotbox, { rotation: BOX_TILT });
397 gsap.set(anchorEls, { autoAlpha: 0, y: 18, scale: 0.9, scaleX: 0.9 });
398 gsap.set(slotbox, { autoAlpha: 0, scale: 0.86 });
399 allSlotLines.forEach(function (el, i) {
400 gsap.set(el, { autoAlpha: 0, rotation: lineRot(i) });
401 });
402
403 // ── 6. THE FIVE OPERATORS ────────────────────────────────────────────────
404
405 // lockup — punchy anchor pop-in (scale + faux-blur squash) + box reveal.
406 function lockup(P) {
407 var th = theme(P.theme != null ? P.theme : BASE_THEME);
408 tl.set(lockupEl, { rotation: BASE_TILT }, 0);
409 anchorEls.forEach(function (el, i) {
410 tl.set(el.firstChild, { color: th.ink }, 0);
411 tl.fromTo(
412 el,
413 { autoAlpha: 0, y: 26, scale: 0.84, scaleX: 0.7 },
414 {
415 autoAlpha: 1,
416 y: 0,
417 scale: 1,
418 scaleX: 1,
419 duration: 0.2,
420 ease: "expo.out",
421 overwrite: "auto",
422 immediateRender: false,
423 },
424 beatAt(i, R, P.enter),
425 );
426 });
427 var boxAt = P.box != null ? P.box : beatAt(R - 1, R, P.enter);
428 tl.fromTo(
429 slotbox,
430 { autoAlpha: 0, scale: 0.6, rotation: BOX_TILT - 6 },
431 {
432 autoAlpha: 1,
433 scale: 1,
434 rotation: BOX_TILT,
435 duration: 0.22,
436 ease: "back.out(2.2)",
437 overwrite: "auto",
438 immediateRender: false,
439 },
440 boxAt,
441 );
442 }
443
444 // slot — word-GROUP cycle: WHIP in from the right (expo.out + scaleX stretch),
445 // hold, arc out down-left. each line its own colour + lean.
446 function slot(P) {
447 var g = groups[P.__g != null ? P.__g : 0];
448 if (!g) return;
449 var th = theme(P.theme != null ? P.theme : BASE_THEME);
450 var n = g.lines.length;
451 g.lines.forEach(function (el, i) {
452 var col = P.colors && P.colors[i] != null ? accent(P.colors[i]) : th.boxInk;
453 var tIn = beatAt(i, n, P.enter);
454 tl.set(g.spans[i], { color: col }, tIn);
455 tl.fromTo(
456 el,
457 { autoAlpha: 0, x: 150, scaleX: 1.7, rotation: lineRot(i) },
458 {
459 autoAlpha: 1,
460 x: 0,
461 scaleX: 1.12,
462 rotation: lineRot(i),
463 duration: 0.24,
464 ease: "expo.out",
465 overwrite: "auto",
466 immediateRender: false,
467 }, // rest scaleX 1.12 = faux-extend (bg stretches with text)
468 tIn,
469 );
470 });
471 if (!P.exit || !P.exit.length) return;
472 g.lines.forEach(function (el, i) {
473 tl.to(
474 el,
475 {
476 autoAlpha: 0,
477 x: -120,
478 y: 46,
479 scaleX: 1.4,
480 duration: 0.16,
481 ease: "power3.in",
482 overwrite: "auto",
483 },
484 beatAt(i, n, P.exit),
485 );
486 });
487 }
488
489 // bg — hard-cut scene flip (0ms invert). 0/1/2 of these = bg changes 0/1/2×.
490 function bg(P) {
491 var th = theme(P.theme != null ? P.theme : 0);
492 tl.set(root, { backgroundColor: th.bg }, P.at);
493 tl.set(
494 anchorEls.map(function (e) {
495 return e.firstChild;
496 }),
497 { color: th.ink },
498 P.at,
499 );
500 tl.set(cards, { backgroundColor: th.box }, P.at); // 3 stacked torn cards = the EMPTY box + zoom target
501 tl.set(allSlotLines, { backgroundColor: th.box }, P.at); // recolor any visible tapes
502 tl.set(allSlotSpans, { color: th.boxInk }, P.at);
503 }
504
505 // shake — per-beat JOLT around the base tilt, with a back-ease recoil.
506 function shake(P) {
507 var amp = P.amp != null ? P.amp : 22;
508 var rot = P.rot != null ? P.rot : 2.2;
509 var settle = P.settle != null ? P.settle : 0.15;
510 (P.beats || []).forEach(function (t, k) {
511 var j = jolt(k);
512 tl.fromTo(
513 lockupEl,
514 { x: (j.x * amp) / 5, y: (j.y * amp) / 4, rotation: BASE_TILT + (j.r * rot) / 3 },
515 {
516 x: 0,
517 y: 0,
518 rotation: BASE_TILT,
519 duration: settle,
520 ease: "back.out(3)",
521 overwrite: "auto",
522 immediateRender: false,
523 },
524 t,
525 );
526 });
527 }
528
529 // expand — EXIT wipe: fade text, then the box ZOOMS (expo.in + blur) to swallow
530 // the frame. keeps the lockup lean; clip-path off so the scaled box fully covers.
531 function expand(P) {
532 var fadeAt = P.fade != null ? P.fade : P.at;
533 tl.to(
534 anchorEls,
535 { autoAlpha: 0, duration: 0.16, ease: "power2.in", overwrite: "auto" },
536 fadeAt,
537 );
538 tl.to(
539 allSlotLines,
540 { autoAlpha: 0, duration: 0.14, ease: "power2.in", overwrite: "auto" },
541 fadeAt,
542 );
543 var startAt = P.at,
544 endAt = P.to != null ? P.to : P.at + 0.46;
545 tl.set(lockupEl, { x: 0, y: 0 }, startAt); // clear shake residual (keep the lean)
546 if (P.theme != null) tl.set(cards, { backgroundColor: theme(P.theme).box }, startAt);
547 var mid = Math.max(startAt + 0.04, endAt - 0.12);
548 // stage A — the TORN mass grows but stays SUB-frame (≈half-screen, decelerating),
549 // so the eye reads "the box is swelling" across most of the window (matches the
550 // source, whose mass is only ~half-frame as late as f113). A big rest box covers
551 // the frame at a low scale, so cap stage A low and let stage B do the swallow.
552 tl.fromTo(
553 slotbox,
554 { scale: 1, filter: "blur(0px)" },
555 {
556 scale: 2.2,
557 filter: "blur(4px)",
558 duration: mid - startAt,
559 ease: "power2.out",
560 overwrite: "auto",
561 immediateRender: false,
562 },
563 startAt,
564 );
565 // stage B — SWALLOW the frame in the last beats (accelerating), then flatten to solid.
566 tl.to(
567 slotbox,
568 {
569 scale: 34,
570 filter: "blur(12px)",
571 duration: endAt - mid,
572 ease: "power2.in",
573 overwrite: "auto",
574 },
575 mid,
576 );
577 tl.set(card1, { clipPath: "none" }, endAt - 0.03); // card1 (inset 0) flattens to a solid full-frame black-out
578 }
579
580 var OPS = { lockup: lockup, slot: slot, bg: bg, shake: shake, expand: expand };
581
582 // ── 7. RUN THE PROGRAM ───────────────────────────────────────────────────
583 PROGRAM.forEach(function (step) {
584 var fn = step && OPS[step.op];
585 if (fn) fn(step);
586 });
587
588 tl.to({}, { duration: 0.001 }, DUR);
589 tl.seek(0);
590 window.__timelines["group-six-recreate"] = tl;
591 })();
592 </script>
593 </body>
594</html>