Setting the file. One moment. Index · Music To Video · heygen-com/hyperframes · Skills DocsGsap Min
(opens in a new tab)
references/templates/card-flyby/index.html
HTML·527 lines·20 KB
12 <head>
13 <meta charset="UTF-8" />
14 <meta name="viewport" content="width=1920, height=1080" />
15 <title>Card Flyby · accelerating 3D deck cascade — 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 width: 1920px;
28 height: 1080px;
29 background: #06070d;
30 overflow: hidden;
31 }
32 #root {
33 position: relative;
34 width: 1920px;
35 height: 1080px;
36 background: radial-gradient(1300px 820px at 50% 52%, #0d1018 0%, #06070d 70%, #04050a 100%);
37 overflow: hidden;
38 font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
39 }
40 /* .stage carries the perspective; .scene carries the static yaw. The cards' own
41 rotateX drives the tumble-forward — the scene only holds the side tilt. */
42 .stage {
43 position: absolute;
44 left: 0;
45 top: 0;
46 width: 1920px;
47 height: 1080px;
48 perspective: 2400px;
49 perspective-origin: 50% 55%;
50 }
51 .scene {
52 position: absolute;
53 left: 0;
54 top: 0;
55 width: 1920px;
56 height: 1080px;
57 transform-style: preserve-3d;
58 transform: rotateY(-12deg);
59 }
60 .card {
61 position: absolute;
62 left: calc(50% - 700px);
63 top: calc(50% - 395px);
64 width: 1400px;
65 height: 790px;
66 border-radius: 32px;
67 will-change: transform, opacity;
68 --wipe: 0;
69 --color: #444;
70 transform-origin: 50% 50%;
71 }
72 /* Wireframe (always present, opacity tracks the slot). */
73 .wire {
74 position: absolute;
75 inset: 0;
76 border-radius: 28px;
77 border: 1.5px solid rgba(255, 255, 255, 0.42);
78 box-shadow: inset 0 0 0 0.5px rgba(255, 255, 255, 0.18);
79 background: rgba(255, 255, 255, 0.012);
80 overflow: hidden;
81 }
82 .wire .wire-label {
83 position: absolute;
84 left: 68px;
85 top: 60px;
86 font-size: 26px;
87 font-weight: 500;
88 letter-spacing: 0.18em;
89 text-transform: uppercase;
90 color: rgba(255, 255, 255, 0.5);
91 }
92 .wire .wire-corner {
93 position: absolute;
94 right: 68px;
95 top: 60px;
96 width: 34px;
97 height: 34px;
98 border: 1.5px solid rgba(255, 255, 255, 0.32);
99 border-radius: 50%;
100 }
101 .wire .wire-bar {
102 position: absolute;
103 left: 68px;
104 right: 68px;
105 bottom: 68px;
106 height: 1.5px;
107 background: rgba(255, 255, 255, 0.22);
108 }
109 /* Solid colored face — revealed via an angled clip-path wipe driven by --wipe.
110 Polygon sweeps upper-left → lower-right at ~72°. wipe=0 collapsed off-element,
111 wipe=1 covers the full card. */
112 .solid {
113 position: absolute;
114 inset: 0;
115 border-radius: 28px;
116 /* gentle top→bottom tonal gradient (theme-derived shades) for depth */
117 background: linear-gradient(
118 158deg,
119 var(--c1, var(--color)) 0%,
120 var(--c2, var(--color)) 100%
121 );
122 box-shadow:
123 0 34px 100px rgba(0, 0, 0, 0.55),
124 inset 0 1px 0 rgba(255, 255, 255, 0.14);
125 overflow: hidden;
126 /* the title is the hero — centered in the card */
127 display: flex;
128 align-items: center;
129 justify-content: center;
130 padding: 90px 104px;
131 clip-path: polygon(
132 0% 0%,
133 calc(var(--wipe) * 200% - 30%) 0%,
134 calc(var(--wipe) * 200% - 80%) 100%,
135 0% 100%
136 );
137 }
138 .solid::before {
139 content: "";
140 position: absolute;
141 inset: 0;
142 border-radius: 28px;
143 background: linear-gradient(
144 135deg,
145 rgba(255, 255, 255, 0.13) 0%,
146 rgba(255, 255, 255, 0.03) 40%,
147 rgba(0, 0, 0, 0.14) 100%
148 );
149 pointer-events: none;
150 }
151 .solid .title {
152 font-size: 184px;
153 font-weight: 800;
154 letter-spacing: -0.035em;
155 line-height: 0.92;
156 color: var(--ink, #ffffff);
157 text-align: center;
158 max-width: 100%;
159 overflow-wrap: break-word;
160 text-wrap: balance;
161 }
162 .solid .play {
163 position: absolute;
164 right: 68px;
165 bottom: 70px;
166 width: 80px;
167 height: 80px;
168 border-radius: 50%;
169 background: rgba(0, 0, 0, 0.28);
170 display: flex;
171 align-items: center;
172 justify-content: center;
173 }
174 .solid .play::after {
175 content: "";
176 width: 0;
177 height: 0;
178 border-left: 22px solid #ffffff;
179 border-top: 14px solid transparent;
180 border-bottom: 14px solid transparent;
181 margin-left: 5px;
182 }
183 </style>
184 </head>
185 <body>
186 <!-- duration 6.0s = a 6-card accelerating cascade (1 card lands per beat, dwells shrinking) -->
187 <div
188 data-hf-id="hf-cf-root"
189 id="root"
190 data-composition-id="card-flyby"
191 data-width="1920"
192 data-height="1080"
193 data-start="0"
194 data-duration="6.0"
195 data-root="true"
196 >
197 <div data-hf-id="hf-cf-stage" class="stage">
198 <div data-hf-id="hf-cf-scene" class="scene" id="scene"></div>
199 </div>
200
201 <script>
202 (function () {
203 // ════════════════════════════════════════════════════════════════════
204 // CARD FLYBY · accelerating 3D deck cascade
205 // ────────────────────────────────────────────────────────────────────
206 // A depth column of N cards rolls FORWARD. On each landing beat the next
207 // card tumbles up into the front slot (s0) and WIPES in its solid colored
208 // face (angled clip-path), the previous front card tumbles forward + falls
209 // down toward the camera on exit, and every card behind advances one slot.
210 // Dwells shrink card-to-card → the deck reads as an accelerating rush into
211 // a held final card. Asset-free: wireframe + solid CSS cards, system fonts.
212 //
213 // COMPOSE BY DATA — `cards` (the deck) and `landings` (one beat per card)
214 // are the two data surfaces. Card count is data: 3/6/9 cards all "just
215 // work"; landings default to an accelerating schedule derived from count.
216 // ════════════════════════════════════════════════════════════════════
217
218 window.__timelines = window.__timelines || {};
219 // force3D is set per card tween/set below (NOT a global default — a global
220 // would leak onto the plain-object padding tween and warn). It keeps a 3D
221 // matrix on every transform so cards never collapse to 2D: a card whose
222 // rotX/Y/Z all hit 0 (the front s0 pose) would otherwise drop out of the 3D
223 // stacking context and paint in DOM order, letting back cards render on top.
224
225 // ── 1. Variables ──────────────────────────────────────────────────────
226 var defaults = { theme: "aurora", bgColor: "", cards: "", landings: "", yaw: -12 };
227 var vars = Object.assign(
228 {},
229 defaults,
230 window.__hyperframes && window.__hyperframes.getVariables
231 ? window.__hyperframes.getVariables()
232 : {},
233 );
234
235 // ── Palette themes — each is a COHESIVE ramp (one temperature/saturation
236 // family) so any card count reads as a designed set. card.color still
237 // overrides per card. White-on-card stays AA at the card type sizes. ──
238 var THEMES = {
239 // cool jewel arc: teal → ocean → indigo → violet → plum
240 aurora: {
241 bg: "#06060e",
242 ink: "#ffffff",
243 colors: ["#0e5f63", "#15517e", "#283f86", "#4a3a83", "#6a3380", "#8a2e63"],
244 },
245 // warm sunset arc: wine → brick → rust → amber → ochre → bronze
246 ember: {
247 bg: "#0e0805",
248 ink: "#fff6ee",
249 colors: ["#5c2030", "#7e2828", "#9c3b1c", "#a9551a", "#8a5a1c", "#5f4626"],
250 },
251 // sleek monochrome blue staircase (dark → mid)
252 mono: {
253 bg: "#060912",
254 ink: "#ffffff",
255 colors: ["#11243f", "#16314f", "#1b3d63", "#214a78", "#27568d", "#2d63a2"],
256 },
257 // muted cool slate — understated / editorial
258 slate: {
259 bg: "#0b0e12",
260 ink: "#eef2f6",
261 colors: ["#232a33", "#2b3540", "#33414e", "#3b4c5c", "#43576a", "#4b6278"],
262 },
263 };
264 var TH = THEMES[String(vars.theme || "").trim()] || THEMES.aurora;
265
266 // tiny hex helpers (deterministic, no deps) — derive shades + soft ink.
267 function parseHex(h) {
268 h = String(h || "")
269 .trim()
270 .replace(/^#/, "");
271 if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
272 if (h.length !== 6) return null;
273 var n = parseInt(h, 16);
274 return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
275 }
276 function clamp8(v) {
277 return Math.max(0, Math.min(255, Math.round(v)));
278 }
279 function toHex(c) {
280 return (
281 "#" +
282 [c.r, c.g, c.b]
283 .map(function (v) {
284 return clamp8(v).toString(16).padStart(2, "0");
285 })
286 .join("")
287 );
288 }
289 function mix(hex, t, amt) {
290 var c = parseHex(hex);
291 if (!c) return hex;
292 return toHex({
293 r: c.r + (t.r - c.r) * amt,
294 g: c.g + (t.g - c.g) * amt,
295 b: c.b + (t.b - c.b) * amt,
296 });
297 }
298 var BLACK = { r: 0, g: 0, b: 0 },
299 WHITE = { r: 255, g: 255, b: 255 };
300 function darken(hex, amt) {
301 return mix(hex, BLACK, amt);
302 }
303 function lighten(hex, amt) {
304 return mix(hex, WHITE, amt);
305 }
306
307 var DEFAULT_CARDS = [
308 { title: "FADEGLOW" },
309 { title: "SAUL BASS" },
310 { title: "EULER" },
311 { title: "HERMÈS" },
312 { title: "DOSSIER" },
313 { title: "BLACK HOLES" },
314 ];
315 // landings tuned by hand on the sandbox: accelerating dwells, final card holds ~1.6s.
316 var DEFAULT_LANDINGS = [0.4, 1.7, 2.7, 3.45, 4.0, 4.4];
317
318 var DUR = 6.0; // matches data-duration
319 var YAW = Number(vars.yaw);
320 if (!isFinite(YAW)) YAW = -12;
321
322 function parseJSON(v) {
323 if (!v) return null;
324 if (typeof v !== "string") return v;
325 try {
326 return JSON.parse(v);
327 } catch (e) {
328 return null;
329 }
330 }
331
332 var CARDS = parseJSON(vars.cards);
333 if (!CARDS || !CARDS.length) CARDS = DEFAULT_CARDS;
334 var N = CARDS.length;
335
336 // ── 2. Landing schedule (accelerating cascade) ─────────────────────────
337 // gaps between landings shrink geometrically; the final card lands early
338 // enough to hold a beat before the cut.
339 function genLandings(n, total) {
340 var start = 0.4;
341 var hold = Math.min(1.6, total * 0.27);
342 var end = Math.max(start + 0.3, total - hold);
343 if (n <= 1) return [start];
344 var ratio = 0.78,
345 weights = [],
346 sum = 0,
347 i;
348 for (i = 0; i < n - 1; i++) {
349 var w = Math.pow(ratio, i);
350 weights.push(w);
351 sum += w;
352 }
353 var span = end - start,
354 acc = start,
355 times = [start];
356 for (i = 0; i < n - 1; i++) {
357 acc += (span * weights[i]) / sum;
358 times.push(acc);
359 }
360 return times;
361 }
362 var LANDED = parseJSON(vars.landings);
363 if (!LANDED || LANDED.length !== N) {
364 LANDED = N === DEFAULT_CARDS.length ? DEFAULT_LANDINGS.slice() : genLandings(N, DUR);
365 }
366 // per-card transition duration derived from the gap into it (faster as we accelerate).
367 function transDur(i) {
368 var gap = i === 0 ? (N > 1 ? LANDED[1] - LANDED[0] : 0.4) : LANDED[i] - LANDED[i - 1];
369 return Math.max(0.12, Math.min(0.42, gap * 0.32));
370 }
371
372 // ── 3. Build the deck DOM from data ────────────────────────────────────
373 var scene = document.getElementById("scene");
374 scene.style.transform = "rotateY(" + YAW + "deg)";
375 // bg: explicit bgColor override wins; else the theme's field color. The radial
376 // lifts the centre slightly and sinks the edges, both derived from the bg.
377 var BG = vars.bgColor && String(vars.bgColor).trim() ? vars.bgColor : TH.bg;
378 document.documentElement.style.background = BG;
379 document.body.style.background = BG;
380 var rootEl = document.getElementById("root");
381 rootEl.style.background =
382 "radial-gradient(1300px 820px at 50% 52%, " +
383 lighten(BG, 0.1) +
384 " 0%, " +
385 BG +
386 " 70%, " +
387 darken(BG, 0.4) +
388 " 100%)";
389
390 function el(cls, parent) {
391 var d = document.createElement("div");
392 d.className = cls;
393 if (parent) parent.appendChild(d);
394 return d;
395 }
396 var cardEls = [];
397 for (var i = 0; i < N; i++) {
398 var c = CARDS[i] || {};
399 var card = el("card", scene);
400 card.id = "card-" + i;
401 // color: per-card override, else cycle the cohesive theme ramp. A gentle
402 // top→bottom shade pair (--c1/--c2) gives each face tonal depth.
403 var col = c.color || TH.colors[i % TH.colors.length];
404 card.style.setProperty("--color", col);
405 card.style.setProperty("--c1", lighten(col, 0.05));
406 card.style.setProperty("--c2", darken(col, 0.22));
407 card.style.setProperty("--ink", TH.ink);
408
409 var wire = el("wire", card);
410 var label = el("wire-label", wire);
411 label.textContent = String(i + 1).padStart(3, "0");
412 el("wire-corner", wire);
413 el("wire-bar", wire);
414
415 var solid = el("solid", card);
416 var title = el("title", solid);
417 title.textContent = c.title || "";
418 el("play", solid);
419
420 cardEls.push(card);
421 }
422
423 // ── 4. SLOT poses ───────────────────────────────────────────────────────
424 // rotX is the key. Cards behind tilt BACK (top edge away, +rotX); the front
425 // is upright (rotX 0); on exit a card continues forward into a face-down
426 // tumble (-rotX) while dropping down + toward camera. Depth from perspective
427 // (Z) + the cards' rotateX, not a camera move.
428 var SLOT = {
429 far: { x: 360, y: 380, z: -1080, rotX: 84, scale: 0.74, opacity: 0, zi: 60 },
430 s3: { x: 280, y: 290, z: -820, rotX: 66, scale: 0.8, opacity: 0.32, zi: 70 },
431 s2: { x: 195, y: 200, z: -560, rotX: 46, scale: 0.86, opacity: 0.58, zi: 80 },
432 s1: { x: 105, y: 105, z: -290, rotX: 22, scale: 0.93, opacity: 0.85, zi: 90 },
433 s0: { x: 0, y: 0, z: 0, rotX: 0, scale: 1.0, opacity: 1.0, zi: 100 },
434 exit: { x: -90, y: 620, z: 420, rotX: -88, scale: 1.1, opacity: 0, zi: 110 },
435 };
436 // pose for a card whose index is `rel` slots behind the current front card
437 // (rel 0 = front, +ve = behind, -ve = already exiting / gone).
438 function poseForRel(rel) {
439 if (rel <= -1) return SLOT.exit;
440 if (rel === 0) return SLOT.s0;
441 if (rel === 1) return SLOT.s1;
442 if (rel === 2) return SLOT.s2;
443 if (rel === 3) return SLOT.s3;
444 return SLOT.far;
445 }
446
447 var tl = gsap.timeline({ paused: true });
448
449 function poseTween(idx, slot, t, dur, ease) {
450 // z-index is set instantly at the swap start so paint order matches the
451 // slot — CSS can't reliably depth-sort non-intersecting planes.
452 tl.set("#card-" + idx, { zIndex: slot.zi }, t);
453 tl.to(
454 "#card-" + idx,
455 {
456 x: slot.x,
457 y: slot.y,
458 z: slot.z,
459 rotationX: slot.rotX,
460 scale: slot.scale,
461 opacity: slot.opacity,
462 force3D: true,
463 duration: dur,
464 ease: ease || "power2.inOut",
465 },
466 t,
467 );
468 }
469
470 // ── 5. Initial pose (front index = -1: nothing landed yet) ─────────────
471 // card j sits at rel = j + 1, so card0 starts at s1, card1 at s2, etc. Apply
472 // via gsap.set (so the paused render at t=0 shows the staged stack) AND via
473 // tl.set at 0 (so scrubbing back to 0 restores it).
474 for (var j = 0; j < N; j++) {
475 var s = poseForRel(j + 1);
476 var props = {
477 x: s.x,
478 y: s.y,
479 z: s.z,
480 rotationX: s.rotX,
481 scale: s.scale,
482 opacity: s.opacity,
483 zIndex: s.zi,
484 force3D: true,
485 "--wipe": 0,
486 };
487 gsap.set("#card-" + j, props);
488 tl.set("#card-" + j, props, 0);
489 }
490
491 // ── 6. Per-landing orchestration ───────────────────────────────────────
492 // On landing i, card i becomes front. Tween the live window [i-1 .. i+4]:
493 // i-1 tumbles to exit, i rolls into s0 (+ wipe), the rest advance toward
494 // the front. Cards outside the window are already at rest (far / gone).
495 for (var L = 0; L < N; L++) {
496 var dur = transDur(L);
497 var t = Math.max(0, LANDED[L] - dur);
498
499 for (var k = L - 1; k <= L + 4; k++) {
500 if (k < 0 || k >= N) continue;
501 var rel = k - L;
502 var ease = rel === 0 ? "power3.out" : rel < 0 ? "power2.in" : "power2.inOut";
503 poseTween(k, poseForRel(rel), t, dur, ease);
504 }
505
506 // the landing card wipes in its solid face as it rolls up.
507 tl.to(
508 "#card-" + L,
509 {
510 "--wipe": 1,
511 duration: dur * 0.85,
512 ease: "power2.inOut",
513 },
514 t + dur * 0.15,
515 );
516 }
517
518 // pad the timeline so duration() >= data-duration.
519 tl.to({}, { duration: DUR }, 0);
520 tl.seek(0);
521
522 window.__timelines["card-flyby"] = tl;
523 })();
524 </script>
525 </div>
526 </body>
527</html>