Setting the file. One moment. Index · Music To Video · heygen-com/hyperframes · Skills DocsGsap Min
(opens in a new tab)
references/templates/poster-tile-mosaic/index.html
HTML·589 lines·24 KB
{"id":"program", "type":"string", "label":"Program JSON (empty = built-in group_5 score)", "default":""}
12 ]'
13>
14 <head>
15 <meta charset="utf-8" />
16 <meta name="viewport" content="width=1920, height=1080" />
17 <title>Poster-Tile Wall · 3 beat-synced operations — group_5 reference impl</title>
18 <script src="../../motion-primitives/assets/gsap.min.js"></script>
19 <style>
20 *,
21 *::before,
22 *::after {
23 margin: 0;
24 padding: 0;
25 box-sizing: border-box;
26 }
27 html,
28 body {
29 background: var(--bg, #e9e9e9);
30 overflow: hidden;
31 font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
32 }
33 #root {
34 position: relative;
35 width: 1920px;
36 height: 1080px;
37 overflow: hidden;
38 background: var(--bg, #e9e9e9);
39 }
40 /* the wall = the BASE TEMPLATE. all three operations act on this one grid. */
41 #wall {
42 position: absolute;
43 inset: 0;
44 }
45 .gf-tile {
46 position: absolute;
47 display: flex;
48 align-items: flex-end;
49 overflow: hidden;
50 will-change: transform, opacity;
51 backface-visibility: hidden;
52 }
53 .gf-label {
54 font-weight: 800;
55 line-height: 0.95;
56 letter-spacing: -0.01em;
57 padding: 0.5em 0.55em;
58 text-transform: uppercase;
59 word-break: break-word;
60 }
61 /* overlay "cutouts" (phase C) — drawn, asset-free silhouette + label */
62 .gf-ov {
63 position: absolute;
64 display: flex;
65 flex-direction: column;
66 align-items: center;
67 justify-content: flex-end;
68 border-radius: 6px;
69 will-change: transform, opacity;
70 box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
71 }
72 .gf-ov .sil {
73 position: relative;
74 width: 60%;
75 flex: 1 1 auto;
76 margin-top: 14%;
77 display: flex;
78 align-items: flex-end;
79 justify-content: center;
80 }
81 .gf-ov .sil::before {
82 /* head */
83 content: "";
84 position: absolute;
85 top: 0;
86 width: 34%;
87 padding-top: 34%;
88 border-radius: 50%;
89 background: currentColor;
90 }
91 .gf-ov .sil::after {
92 /* shoulders / body */
93 content: "";
94 width: 100%;
95 height: 56%;
96 border-radius: 48% 48% 14% 14% / 60% 60% 14% 14%;
97 background: currentColor;
98 }
99 .gf-ov .cap {
100 align-self: stretch;
101 text-align: center;
102 font-weight: 800;
103 font-size: 26px;
104 letter-spacing: 0.02em;
105 padding: 10px 6px;
106 text-transform: uppercase;
107 }
108 </style>
109 </head>
110 <body>
111 <!-- duration 5.75s = the full group_5 span (3 operations back to back, ends frozen) -->
112 <div
113 data-hf-id="hf-g5rt"
114 id="root"
115 data-composition-id="group-five-recreate"
116 data-width="1920"
117 data-height="1080"
118 data-start="0"
119 data-duration="5.75"
120 data-root="true"
121 >
122 <div data-hf-id="hf-g5wall" id="wall"></div>
123 </div>
124
125 <script>
126 (function () {
127 // ════════════════════════════════════════════════════════════════════
128 // POSTER-TILE MOSAIC · ONE base template, THREE beat-synced operations
129 // ────────────────────────────────────────────────────────────────────
130 // BASE : a packed MOSAIC of DIFFERENT-SIZED colored tiles (asset-free:
131 // color + label). The tiles are NOT a uniform grid — they vary in
132 // size (tall / wide / small) and tessellate to fill the frame with
133 // no overlap (thin seams only), exactly like the source's 错落 wall.
134 // "adding a box" = a new image; "changing a box color" = a new
135 // asset/recolor. Everything below just schedules these tiles on
136 // the beat. Reverse-engineered from group_5 of the golden sample.
137 //
138 // OP A staggerInOut : tiles enter staggered (top-right→bottom-left waterfall)
139 // → hold on a downbeat → exit staggered. each in/out
140 // lands on one onset. mixed sizes packing to full frame.
141 // OP B holdRecolor : all tiles snap on at once, positions LOCKED, the
142 // whole palette recolors as a unit on each beat.
143 // OP C snakeFill+overlay : tiles reveal in a snake path (band by band,
144 // L→R then R→L) → a full-grid recolor on the hard-stop
145 // → extra "cutout" tiles pop on top, one per beat.
146 //
147 // ARCHITECTURE: the operators (§6) are pure, parameterized, order-independent
148 // functions. A data PROGRAM (§3) lists which operators run, in what order,
149 // on which beats — and §7 just dispatches it. So you compose by editing DATA
150 // (the PROGRAM array, or the `program` render variable / program.json), never
151 // the operator code. Combine freely, repeat, or run one solo.
152 // ════════════════════════════════════════════════════════════════════
153
154 // ── 1. Content variables (what a user changes; everything else is timing)
155 var defaults = {
156 bgColor: "#e9e9e9",
157 tiles: 12,
158 bands: 3,
159 gap: 10,
160 showText: true,
161 labels:
162 "DARE TO BE BOLD,NEW DROP,WALK THE TALK,STAND OUT,PUCKER UP," +
163 "SUPER SONIC,IN TECHNICOLOUR,BOLD PRINTS,NEW SCENTS,TIME TO MOVE," +
164 "CRAZY FUNKY COOL,BEAUTY INSIDE",
165 program: "", // empty = built-in group_5 score (DEFAULT_PROGRAM, §3)
166 };
167 var vars = Object.assign(
168 {},
169 defaults,
170 window.__hyperframes && window.__hyperframes.getVariables
171 ? window.__hyperframes.getVariables()
172 : {},
173 );
174 var N = Math.max(2, parseInt(vars.tiles, 10) || 12);
175 var BANDS = Math.max(1, parseInt(vars.bands, 10) || 3);
176 var GAP = Math.max(0, parseFloat(vars.gap) || 0);
177 var SHOW_TEXT = vars.showText !== false && vars.showText !== "false";
178 var LABELS = String(vars.labels)
179 .split(",")
180 .map(function (s) {
181 return s.trim();
182 });
183
184 // ── 2. PALETTE LIBRARY — each THEME is a "look" the whole wall can wear.
185 // A recolor (OP B) / a swap (OP C) just points the wall at another theme.
186 // Tile i takes theme[i % theme.length], so one theme dresses the grid in a
187 // coordinated multi-color scheme. Add themes freely — more beats, more looks.
188 var THEMES = [
189 ["#c8f53a", "#101010", "#ffffff", "#1d1d1d"], // 0 lime / ink (default + OP A)
190 ["#b6f23c", "#0c2a14", "#ffffff", "#0c2a14"], // 1 green
191 ["#2f5cff", "#dce8ff", "#0a1748", "#9fc0ff"], // 2 blue
192 ["#ff5ab3", "#23d6a3", "#1a0a14", "#ffd1ec"], // 3 pink / mint
193 ["#ff3b30", "#ffd60a", "#0a84ff", "#30d158"], // 4 saturated DROP look
194 ];
195 var FONTS = [
196 // typographic variety per tile (the "字体" inside each box)
197 '"Helvetica Neue", Arial, sans-serif',
198 'Georgia, "Times New Roman", serif',
199 '"Arial Narrow", "Helvetica Neue", sans-serif',
200 '"Courier New", Courier, monospace',
201 ];
202 function contrast(hex) {
203 // readable text color for a given fill
204 var h = hex.replace("#", "");
205 var r = parseInt(h.substr(0, 2), 16),
206 g = parseInt(h.substr(2, 2), 16),
207 b = parseInt(h.substr(4, 2), 16);
208 return 0.299 * r + 0.587 * g + 0.114 * b > 150 ? "#0b0b0b" : "#ffffff";
209 }
210 function themeColor(t, slot) {
211 var pal = THEMES[((t % THEMES.length) + THEMES.length) % THEMES.length];
212 return pal[slot % pal.length];
213 }
214
215 // ── 3. THE PROGRAM (data) — the composable surface ──────────────────────
216 // An ordered list of operator steps {op, ...beats/opts}. THIS is what you
217 // recompose: reorder, drop, repeat, or swap a step's beats — no operator code
218 // changes. An agent can override the whole list at render time via the
219 // `program` variable (a JSON string), so it composes by emitting DATA, not by
220 // editing JS. Beats are absolute audio seconds from <group>.audiomap.json.
221 // Mirrors program.json. group_5: 129.2 BPM · downbeats 1.324/3.181/5.039 ·
222 // hard_stop 4.0s. (See program.json for each op's fields.)
223 var DEFAULT_PROGRAM = [
224 {
225 op: "staggerInOut", // build → hold (downbeat) → exit
226 theme: 0,
227 enter: [0.23, 0.37, 0.46, 0.6, 0.7, 0.84, 0.91, 1.07, 1.14, 1.28],
228 hold: 1.324,
229 exit: [1.37, 1.51, 1.62, 1.74, 1.83, 1.97, 2.07],
230 },
231 {
232 op: "holdRecolor", // snap on, lock, recolor per snare
233 on: 2.35,
234 onTheme: 1,
235 recolor: [2.42, 2.53, 2.81, 2.93],
236 themes: [2, 3, 1, 2],
237 off: 2.98,
238 },
239 {
240 op: "snakeFillOverlay", // snake fill → swap → overlay pops
241 baseTheme: 0,
242 fill: [3.04, 3.181, 3.44, 3.65, 3.79, 3.9], // 3.181 = downbeat f76 (accel)
243 swap: 4.0,
244 swapTheme: 4, // hard_stop "一键换装"
245 overlay: [4.11, 4.34, 4.48, 4.81, 4.97, 5.25, 5.48],
246 }, // 7 onsets, 1 cutout each
247 ];
248 var DUR = 5.75; // = root data-duration; re-timing a new track edits both + the beats
249
250 // Resolve the active program: a non-empty `program` variable (JSON) overrides.
251 var PROGRAM = DEFAULT_PROGRAM;
252 if (vars.program && String(vars.program).trim()) {
253 try {
254 var parsed = typeof vars.program === "string" ? JSON.parse(vars.program) : vars.program;
255 if (parsed && parsed.length) PROGRAM = parsed;
256 } catch (e) {
257 PROGRAM = DEFAULT_PROGRAM;
258 } // malformed → fall back, render stays valid
259 }
260 // Pre-build enough overlay "cutout" elements for the hungriest step.
261 var maxOverlays = 0;
262 PROGRAM.forEach(function (s) {
263 if (s.overlay && s.overlay.length > maxOverlays) maxOverlays = s.overlay.length;
264 });
265
266 // ── 4. Build the MOSAIC (the base template — DIFFERENT-sized packed tiles) ─
267 var root = document.getElementById("root");
268 document.documentElement.style.setProperty("--bg", vars.bgColor);
269 var wall = document.getElementById("wall");
270
271 var MARGIN = 56;
272 var innerW = 1920 - MARGIN * 2,
273 innerH = 1080 - MARGIN * 2;
274
275 // Pack the frame into N DIFFERENT-sized rects that tessellate with NO overlap:
276 // recursively split the largest region along its longer side. Deterministic —
277 // the split ratio is a fixed function of the step index, so it renders the same
278 // every time (no Math.random). This is the "错落 but gap-filling" look of OP A.
279 function packMosaic(n, W, H) {
280 var rects = [{ x: 0, y: 0, w: W, h: H }];
281 while (rects.length < n) {
282 var bi = 0;
283 for (var i = 1; i < rects.length; i++) {
284 if (rects[i].w * rects[i].h > rects[bi].w * rects[bi].h) bi = i;
285 }
286 var r = rects.splice(bi, 1)[0];
287 var ratio = 0.4 + ((rects.length * 37) % 21) / 100; // 0.40..0.60, deterministic
288 if (r.w >= r.h) {
289 var w1 = Math.round(r.w * ratio);
290 rects.push({ x: r.x, y: r.y, w: w1, h: r.h });
291 rects.push({ x: r.x + w1, y: r.y, w: r.w - w1, h: r.h });
292 } else {
293 var h1 = Math.round(r.h * ratio);
294 rects.push({ x: r.x, y: r.y, w: r.w, h: h1 });
295 rects.push({ x: r.x, y: r.y + h1, w: r.w, h: r.h - h1 });
296 }
297 }
298 return rects;
299 }
300 var rects = packMosaic(N, innerW, innerH);
301
302 var tiles = [];
303 for (var idx = 0; idx < N; idx++) {
304 var R = rects[idx];
305 var el = document.createElement("div");
306 el.className = "gf-tile";
307 el.style.left = MARGIN + R.x + GAP / 2 + "px";
308 el.style.top = MARGIN + R.y + GAP / 2 + "px";
309 el.style.width = Math.max(1, R.w - GAP) + "px";
310 el.style.height = Math.max(1, R.h - GAP) + "px";
311 el.style.backgroundColor = themeColor(0, idx);
312
313 var lab = document.createElement("span");
314 lab.className = "gf-label";
315 lab.textContent = SHOW_TEXT ? LABELS[idx % LABELS.length] || "" : "";
316 lab.style.color = contrast(themeColor(0, idx));
317 lab.style.fontFamily = FONTS[idx % FONTS.length];
318 lab.style.fontSize = Math.round(Math.min(R.w, R.h) * 0.16) + "px";
319 el.appendChild(lab);
320
321 wall.appendChild(el);
322 tiles.push({
323 el: el,
324 label: lab,
325 idx: idx,
326 cx: R.x + R.w / 2,
327 cy: R.y + R.h / 2,
328 rect: R,
329 });
330 }
331
332 // enter/exit order (OP A): top-right → bottom-left waterfall (matches source)
333 var enterOrder = tiles
334 .map(function (t) {
335 return t.idx;
336 })
337 .sort(function (a, b) {
338 return tiles[b].cx - tiles[b].cy - (tiles[a].cx - tiles[a].cy);
339 });
340 // snake order (OP C): band by band top→bottom, alternating L→R / R→L
341 var bandH = innerH / BANDS;
342 var snakeOrder = tiles
343 .map(function (t) {
344 return t.idx;
345 })
346 .sort(function (a, b) {
347 var ba = Math.min(BANDS - 1, Math.floor(tiles[a].cy / bandH));
348 var bb = Math.min(BANDS - 1, Math.floor(tiles[b].cy / bandH));
349 if (ba !== bb) return ba - bb;
350 return (ba % 2 === 0 ? 1 : -1) * (tiles[a].cx - tiles[b].cx);
351 });
352
353 // overlay "cutouts" (OP C) — drawn silhouettes at scattered spots
354 var OV_POS = [
355 // % of the inner stage
356 { l: 60, t: 50 },
357 { l: 14, t: 56 },
358 { l: 78, t: 12 },
359 { l: 38, t: 26 },
360 { l: 50, t: 58 },
361 { l: 70, t: 66 },
362 { l: 26, t: 40 },
363 ];
364 var overlays = [];
365 for (var k = 0; k < maxOverlays; k++) {
366 var pos = OV_POS[k % OV_POS.length];
367 var ov = document.createElement("div");
368 ov.className = "gf-ov";
369 var ow = innerW * 0.2,
370 oh = innerH * 0.62;
371 ov.style.width = ow + "px";
372 ov.style.height = oh + "px";
373 ov.style.left = MARGIN + innerW * (pos.l / 100) - ow / 2 + "px";
374 ov.style.top = MARGIN + innerH * (pos.t / 100) - oh / 2 + "px";
375 var fill = themeColor(4, k);
376 ov.style.backgroundColor = fill;
377 var sil = document.createElement("div");
378 sil.className = "sil";
379 sil.style.color = contrast(fill);
380 var cap = document.createElement("div");
381 cap.className = "cap";
382 cap.textContent = SHOW_TEXT ? "MODEL " + (k + 1) : "";
383 cap.style.color = contrast(fill);
384 ov.appendChild(sil);
385 ov.appendChild(cap);
386 wall.appendChild(ov);
387 overlays.push(ov);
388 }
389
390 // ── 5. Timeline + small helpers ─────────────────────────────────────────
391 window.__timelines = window.__timelines || {};
392 var tl = gsap.timeline({ paused: true });
393
394 // overwrite:"auto" lets a later move on a tile cleanly supersede an earlier
395 // one (and keeps the static linter from cross-flagging dynamically-targeted tweens).
396 // immediateRender:false — a from/fromTo MUST NOT bake its from-state at creation,
397 // or an operator scheduled LATER (e.g. staggerInOut's jitter) would park the tiles
398 // off-grid during an EARLIER operator (e.g. holdRecolor). Keeps operators order-independent.
399 var POP = {
400 duration: 0.12,
401 ease: "back.out(1.7)",
402 overwrite: "auto",
403 immediateRender: false,
404 };
405 function setTheme(t, idx, themeIdx) {
406 // 0ms recolor on the beat (a hit, not a fade)
407 var col = themeColor(themeIdx, idx);
408 tl.set(tiles[idx].el, { backgroundColor: col }, t);
409 tl.set(tiles[idx].label, { color: contrast(col) }, t);
410 }
411 function jitter(i) {
412 // deterministic scatter offset (no Math.random — must render identically)
413 return { x: (((i * 37) % 7) - 3) * 16, y: (((i * 53) % 7) - 3) * 16 };
414 }
415 function beatAt(i, n, beats) {
416 // distribute n items across a beat list, monotonic
417 if (!beats || !beats.length) return 0;
418 return beats[Math.min(beats.length - 1, Math.floor((i * beats.length) / n))];
419 }
420 function orderOf(name) {
421 // resolve a reveal order (string preset or explicit array)
422 if (Array.isArray(name)) return name;
423 if (name === "snake") return snakeOrder;
424 if (name === "index")
425 return tiles.map(function (t) {
426 return t.idx;
427 });
428 return enterOrder; // default = top-right→bottom-left waterfall
429 }
430
431 gsap.set(
432 tiles.map(function (t) {
433 return t.el;
434 }),
435 { autoAlpha: 0 },
436 );
437 if (overlays.length) gsap.set(overlays, { autoAlpha: 0 });
438
439 // ── 6. THE THREE OPERATORS — pure, parameterized, order-independent ──────
440 // Each takes ONE program step P and schedules onto `tl`. Each "shows" tiles
441 // via fromTo(from hidden) and sets its own theme, so operators compose in ANY
442 // order, repeat safely, and can run solo. Add a new operator = add a function
443 // here + a key in OPS; the PROGRAM (§3 / the `program` var) can then call it.
444
445 // A — staggered enter (waterfall) → hold → staggered exit. one move per beat.
446 // P: { enter:[s], exit?:[s], theme?:i, order?:"waterfall"|"snake"|"index"|[idx] }
447 function staggerInOut(P) {
448 var order = orderOf(P.order),
449 theme = P.theme || 0;
450 order.forEach(function (i, k) {
451 setTheme(beatAt(k, N, P.enter), i, theme);
452 var d = jitter(i);
453 tl.fromTo(
454 tiles[i].el,
455 { autoAlpha: 0, scale: 0.78, x: d.x, y: d.y },
456 Object.assign({ autoAlpha: 1, scale: 1, x: 0, y: 0 }, POP),
457 beatAt(k, N, P.enter),
458 );
459 });
460 // hold = the wall simply stays put until the exit begins (no extra tween)
461 if (!P.exit || !P.exit.length) return; // no exit → leave the wall standing
462 order.forEach(function (i, k) {
463 tl.to(
464 tiles[i].el,
465 {
466 autoAlpha: 0,
467 scale: 0.9,
468 y: 46,
469 duration: 0.12,
470 ease: "power2.in",
471 overwrite: "auto",
472 },
473 beatAt(k, N, P.exit),
474 );
475 });
476 }
477
478 // B — snap whole wall on, LOCK positions, recolor as a unit on each beat.
479 // P: { on:s, recolor?:[s], themes?:[i], onTheme?:i, off?:s }
480 function holdRecolor(P) {
481 tiles.forEach(function (_, i) {
482 setTheme(P.on, i, P.onTheme || 0);
483 });
484 // x:0,y:0 resets any transform a prior operator left behind → clean LOCKED grid
485 tl.fromTo(
486 tiles.map(function (t) {
487 return t.el;
488 }),
489 { autoAlpha: 0, scale: 0.96, x: 0, y: 0 },
490 {
491 autoAlpha: 1,
492 scale: 1,
493 x: 0,
494 y: 0,
495 duration: 0.1,
496 ease: "power2.out",
497 overwrite: "auto",
498 immediateRender: false,
499 },
500 P.on,
501 );
502 (P.recolor || []).forEach(function (t, ri) {
503 var th = (P.themes || [0])[ri % (P.themes || [0]).length];
504 tiles.forEach(function (_, i) {
505 setTheme(t, i, th);
506 });
507 });
508 if (P.off != null) {
509 tl.to(
510 tiles.map(function (t) {
511 return t.el;
512 }),
513 { autoAlpha: 0, scale: 0.97, duration: 0.1, ease: "power2.in", overwrite: "auto" },
514 P.off,
515 );
516 }
517 }
518
519 // C — snake reveal → optional full-grid swap → overlay cutouts pop on top.
520 // P: { fill:[s], baseTheme?:i, swap?:s, swapTheme?:i, overlay?:[s], order?:… }
521 function snakeFillOverlay(P) {
522 var order = orderOf(P.order || "snake"),
523 base = P.baseTheme || 0;
524 var f0 = P.fill && P.fill.length ? P.fill[0] : 0;
525 order.forEach(function (i) {
526 setTheme(Math.max(0, f0 - 0.01), i, base);
527 });
528 order.forEach(function (i, k) {
529 tl.fromTo(
530 tiles[i].el,
531 { autoAlpha: 0, scale: 0.85, x: 0, y: 0 }, // x/y reset → fills on the clean grid
532 {
533 autoAlpha: 1,
534 scale: 1,
535 x: 0,
536 y: 0,
537 duration: 0.1,
538 ease: "back.out(1.6)",
539 overwrite: "auto",
540 immediateRender: false,
541 },
542 beatAt(k, N, P.fill),
543 );
544 });
545 if (P.swap != null) {
546 // hard-stop "一键换装" — recolor the whole grid at once
547 tiles.forEach(function (_, i) {
548 setTheme(P.swap, i, P.swapTheme != null ? P.swapTheme : 4);
549 });
550 }
551 (P.overlay || []).forEach(function (t, i) {
552 // one cutout per beat; final holds
553 if (!overlays[i]) return;
554 tl.fromTo(
555 overlays[i],
556 { autoAlpha: 0, scale: 0.6, y: 30 },
557 {
558 autoAlpha: 1,
559 scale: 1,
560 y: 0,
561 duration: 0.16,
562 ease: "back.out(2)",
563 overwrite: "auto",
564 immediateRender: false,
565 },
566 t,
567 );
568 });
569 }
570
571 var OPS = {
572 staggerInOut: staggerInOut,
573 holdRecolor: holdRecolor,
574 snakeFillOverlay: snakeFillOverlay,
575 };
576
577 // ── 7. RUN THE PROGRAM — dispatch each step to its operator, in order ────
578 PROGRAM.forEach(function (step) {
579 var fn = step && OPS[step.op];
580 if (fn) fn(step);
581 });
582
583 tl.to({}, { duration: 0.001 }, DUR); // pin the timeline length to the full span
584 tl.seek(0);
585 window.__timelines["group-five-recreate"] = tl;
586 })();
587 </script>
588 </body>
589</html>