Setting the file. One moment. Proof Logo Chain · Hyperframes Animation · heygen-com/hyperframes · Skills DocsBlueprints Index
(opens in a new tab)
examples/proof-logo-chain.html
HTML·861 lines·28 KB
8 <!--
9 HyperFrames-native version of the scene-03-anchor-chain-reveal concept.
10 Same 5-phase choreography, GSAP-driven, seek-safe.
11
12 Audio (assumed accompanying narration):
13 "Meet HyperFrames, the HTML video composition toolkit for building,
14 previewing, and rendering programmable motion."
15
16 Sub-shots (local time, 8s total):
17 0.0 – 1.2s "HyperFrames" Brand reveal (hacker-flip)
18 1.2 – 4.0s "HTML VIDEO" Text swap + rolling ticker
19 4.0 – 6.3s "60FPS" Counter + capability cloud
20 6.3 – 8.0s "Built for Motion" Capability strip
21
22 Placeholders (self-contained — no asset files needed):
23 inline-SVG "HF" mark — brand logo (swap for your image)
24 CSS initials circles — user avatars (edit CREATOR_INITIALS)
25 CSS text chips — partner brand logos (edit --brand-name)
26 Replace with real assets for production.
27 -->
28
29 <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
30
31 <style>
32 :root {
33 /* Palette — direct port of the the source COLORS constants */
34 --text-primary: #ffffff;
35 --text-muted: rgba(255, 255, 255, 0.45);
36 --brand-green: #00e676;
37 --bg-grad-from: #0a1a2e;
38 --bg-grad-to: #1a0a2e;
39 }
40
41 * {
42 margin: 0;
43 padding: 0;
44 box-sizing: border-box;
45 }
46 html,
47 body {
48 margin: 0;
49 width: 1920px;
50 height: 1080px;
51 overflow: hidden;
52 background: #000;
53 font-family:
54 "Inter",
55 system-ui,
56 -apple-system,
57 sans-serif;
58 color: var(--text-primary);
59 }
60
61 /* ============================================================
62 BACKGROUND (port of <FrostedGlassBackground />)
63 ============================================================ */
64 .bg {
65 position: absolute;
66 inset: 0;
67 background:
68 radial-gradient(ellipse at 30% 20%, rgba(0, 230, 118, 0.18), transparent 50%),
69 radial-gradient(ellipse at 70% 80%, rgba(120, 60, 220, 0.18), transparent 50%),
70 linear-gradient(135deg, var(--bg-grad-from), var(--bg-grad-to));
71 }
72
73 /* Camera wrapper — port of <CameraSystem /> */
74 .camera {
75 position: absolute;
76 inset: 0;
77 transform-origin: center center;
78 }
79
80 /* ============================================================
81 PHASES 1–3 (BrandReveal): logo + decode + swap + recenter
82 ============================================================ */
83 .brand-stage {
84 position: absolute;
85 inset: 0;
86 display: flex;
87 align-items: center;
88 justify-content: center;
89 z-index: 50; /* above cloud SVG (z:1), below logo (z:100) */
90 }
91 .anchor-shift {
92 display: flex;
93 align-items: center;
94 gap: 35px;
95 /* GSAP tweens .x on this element for Phase-2 recenter */
96 }
97 .anchor-logo {
98 position: relative;
99 width: 192px;
100 height: 192px;
101 flex-shrink: 0;
102 display: flex;
103 align-items: center;
104 justify-content: center;
105 z-index: 100; /* highest — sits above lines + avatars */
106 opacity: 1;
107 /* initial scale(0) set by GSAP fromTo — no CSS transform here */
108 }
109 .anchor-logo img {
110 width: 100%;
111 height: 100%;
112 object-fit: contain;
113 filter: drop-shadow(0 4px 24px rgba(0, 0, 0, 0.4));
114 }
115 .anchor-text {
116 position: relative;
117 display: flex;
118 align-items: center;
119 }
120 .phase1-text {
121 display: flex;
122 perspective: 800px; /* required for the per-glyph rotateX */
123 white-space: nowrap;
124 }
125 .flip-glyph {
126 position: relative;
127 display: inline-block;
128 font-size: 163px; /* 1920 × 0.085 — matches the source brandFontSize */
129 font-weight: 900;
130 letter-spacing: -0.02em;
131 }
132 .flip-glyph .ghost {
133 opacity: 0;
134 }
135 .flip-glyph .anim {
136 position: absolute;
137 left: 0;
138 top: 0;
139 width: 100%;
140 color: var(--text-primary);
141 opacity: 0;
142 transform: perspective(600px) rotateX(90deg);
143 transform-origin: bottom;
144 backface-visibility: hidden;
145 }
146 .flip-glyph.space {
147 min-width: 0.35em;
148 }
149
150 .phase2-claim {
151 position: absolute;
152 left: 0;
153 top: 50%;
154 transform: translateY(-50%);
155 opacity: 0;
156 white-space: nowrap;
157 display: flex;
158 align-items: center;
159 gap: 40px;
160 }
161 .claim-rank {
162 color: var(--brand-green);
163 font-size: 163px;
164 font-weight: 900;
165 letter-spacing: -0.02em;
166 }
167 .claim-ai,
168 .claim-video {
169 color: var(--text-primary);
170 font-size: 163px;
171 font-weight: 900;
172 letter-spacing: -0.02em;
173 }
174
175 /* Vertical ticker — port of <RollingWord /> */
176 .ticker-window {
177 height: 204px; /* fontSize × 1.25 = 163 × 1.25 ≈ 204 */
178 overflow: hidden;
179 display: inline-flex;
180 flex-direction: column;
181 vertical-align: bottom;
182 }
183 .ticker-stack {
184 display: flex;
185 flex-direction: column;
186 will-change: transform;
187 }
188 .ticker-item {
189 height: 204px;
190 display: flex;
191 align-items: center;
192 color: var(--text-primary);
193 font-weight: 900;
194 font-size: 163px;
195 letter-spacing: -0.02em;
196 line-height: 1;
197 }
198
199 /* ============================================================
200 PHASE 4 (CreatorCloud): counter + avatar cloud + lines
201 ============================================================ */
202 .cloud-stage {
203 position: absolute;
204 inset: 0;
205 z-index: 10; /* below brand-stage's logo (z:100), below counter */
206 opacity: 0; /* GSAP fades it in */
207 }
208 .counter-pin {
209 position: absolute;
210 top: 3%;
211 left: 50%;
212 transform: translateX(-50%);
213 text-align: center;
214 z-index: 60; /* above brand-stage so it overlays cleanly */
215 }
216 .count {
217 font-variant-numeric: tabular-nums;
218 font-weight: 900;
219 font-size: 115px; /* 1920 × 0.06 */
220 color: var(--brand-green);
221 display: inline-block;
222 min-width: 200px;
223 text-align: right;
224 }
225 .count-suffix {
226 font-weight: 900;
227 font-size: 115px;
228 color: var(--brand-green);
229 opacity: 0; /* GSAP fades in after count */
230 }
231 .cloud-lines {
232 position: absolute;
233 inset: 0;
234 pointer-events: none;
235 z-index: 1;
236 }
237 .cloud-avatars {
238 position: absolute;
239 inset: 0;
240 z-index: 10;
241 }
242 .cloud-avatar {
243 position: absolute;
244 border-radius: 50%;
245 overflow: hidden;
246 border: 3px solid rgba(255, 255, 255, 0.2);
247 opacity: 0;
248 /* initial scale(0) set by GSAP fromTo — no CSS transform here */
249 background: linear-gradient(135deg, #6b8cff, #a6b3ff);
250 display: flex;
251 align-items: center;
252 justify-content: center;
253 color: rgba(255, 255, 255, 0.8);
254 font-weight: 700;
255 font-size: 32px;
256 }
257
258 /* ============================================================
259 PHASE 5 (BrandShowcase): label + scrolling brand strip
260 ============================================================ */
261 .brand-strip {
262 position: absolute;
263 bottom: 8%;
264 left: 0;
265 right: 0;
266 opacity: 0; /* GSAP fades + translateY in via fromTo */
267 z-index: 70;
268 }
269 .brand-label {
270 text-align: center;
271 margin-bottom: 25px;
272 font-size: 27px; /* 1920 × 0.014 */
273 font-weight: 500;
274 color: var(--text-muted);
275 letter-spacing: 0.2em;
276 text-transform: uppercase;
277 }
278 .brand-strip-window {
279 display: flex;
280 justify-content: center;
281 align-items: center;
282 gap: 80px;
283 overflow: hidden;
284 }
285 .brand-strip-track {
286 display: flex;
287 gap: 110px;
288 }
289 .brand-logo {
290 width: 170px;
291 height: 52px;
292 display: flex;
293 align-items: center;
294 justify-content: center;
295 opacity: 0;
296 /* initial scale(0.5) set by GSAP fromTo — no CSS transform here */
297 }
298 .brand-logo::before {
299 content: var(--brand-name);
300 font-family: Inter, system-ui, sans-serif;
301 font-size: 30px;
302 font-weight: 800;
303 letter-spacing: 0.08em;
304 white-space: nowrap;
305 color: rgba(255, 255, 255, 0.72);
306 }
307 .brand-nvidia {
308 --brand-name: "NVIDIA";
309 }
310 .brand-visa {
311 --brand-name: "VISA";
312 }
313 .brand-zoominfo {
314 --brand-name: "ZOOMINFO";
315 }
316 .brand-github {
317 --brand-name: "GITHUB";
318 }
319
320 /* Vignette overlay */
321 .vignette {
322 position: absolute;
323 inset: 0;
324 background: radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.45) 100%);
325 pointer-events: none;
326 z-index: 200;
327 }
328 </style>
329 </head>
330 <body>
331 <div
332 id="root"
333 data-composition-id="main"
334 data-start="0"
335 data-duration="8"
336 data-width="1920"
337 data-height="1080"
338 >
339 <div class="bg"></div>
340
341 <div class="camera">
342 <!-- =====================================================
343 PHASES 1–3: BrandReveal (logo + hacker-flip + swap)
344 ===================================================== -->
345 <div
346 id="phase-anchor"
347 class="brand-stage clip"
348 data-start="0"
349 data-duration="8"
350 data-track-index="1"
351 >
352 <div class="anchor-shift" data-layout-allow-overflow>
353 <!-- Anchor logo — persists across all phases -->
354 <div class="anchor-logo">
355 <!-- Inline-SVG placeholder mark — swap for your logo image -->
356 <svg
357 class="logo-mark"
358 viewBox="0 0 100 100"
359 width="100%"
360 height="100%"
361 role="img"
362 aria-label="hyperframes logo"
363 >
364 <defs>
365 <linearGradient id="hfMark" x1="0" y1="0" x2="1" y2="1">
366 <stop offset="0" stop-color="#8b7bff" />
367 <stop offset="1" stop-color="#3ddc97" />
368 </linearGradient>
369 </defs>
370 <rect x="4" y="4" width="92" height="92" rx="22" fill="url(#hfMark)" />
371 <text
372 x="50"
373 y="63"
374 text-anchor="middle"
375 font-family="Inter, system-ui, sans-serif"
376 font-size="40"
377 font-weight="800"
378 fill="#fff"
379 >
380 HF
381 </text>
382 </svg>
383 </div>
384
385 <!-- Swappable text zone -->
386 <div class="anchor-text">
387 <!-- Phase 1: 'HyperFrames' decoded character by character -->
388 <div class="phase1-text" aria-label="HyperFrames">
389 <!-- Spans generated by the script below -->
390 </div>
391
392 <!-- Phase 2: 'HTML video render/ship' claim -->
393 <div class="phase2-claim">
394 <span class="claim-rank">HTML</span>
395 <span class="claim-ai">Video</span>
396 <div class="claim-ticker">
397 <div class="ticker-window" data-layout-allow-overflow>
398 <div class="ticker-stack">
399 <div class="ticker-item">render</div>
400 <div class="ticker-item">ship</div>
401 </div>
402 </div>
403 </div>
404 </div>
405 </div>
406 </div>
407 </div>
408
409 <!-- =====================================================
410 PHASE 4: CreatorCloud (counter + avatar ring + lines)
411 ===================================================== -->
412 <div
413 id="phase-cloud"
414 class="cloud-stage clip"
415 data-start="3.6"
416 data-duration="4.4"
417 data-track-index="2"
418 >
419 <div class="counter-pin">
420 <span class="count">60</span><span class="count-suffix">FPS</span>
421 </div>
422 <svg class="cloud-lines" width="1920" height="1080">
423 <defs>
424 <linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
425 <stop offset="0%" stop-color="#00e676" stop-opacity="0.8" />
426 <stop offset="100%" stop-color="#00e676" stop-opacity="0.2" />
427 </linearGradient>
428 </defs>
429 <!-- <line> elements generated by JS -->
430 </svg>
431 <div class="cloud-avatars">
432 <!-- .cloud-avatar elements generated by JS -->
433 </div>
434 </div>
435
436 <!-- =====================================================
437 PHASE 5: BrandShowcase (label + scrolling logos)
438 ===================================================== -->
439 <div
440 id="phase-brands"
441 class="brand-strip clip"
442 data-start="6.3"
443 data-duration="1.7"
444 data-track-index="3"
445 >
446 <div class="brand-label">Trusted by Leading Brands</div>
447 <div class="brand-strip-window" data-layout-allow-overflow>
448 <div class="brand-strip-track">
449 <div class="brand-logo brand-nvidia" aria-label="NVIDIA"></div>
450 <div class="brand-logo brand-visa" aria-label="Visa"></div>
451 <div class="brand-logo brand-zoominfo" aria-label="ZoomInfo"></div>
452 <div class="brand-logo brand-github" aria-label="GitHub"></div>
453 <div class="brand-logo brand-nvidia" aria-label="NVIDIA"></div>
454 <div class="brand-logo brand-visa" aria-label="Visa"></div>
455 <div class="brand-logo brand-zoominfo" aria-label="ZoomInfo"></div>
456 <div class="brand-logo brand-github" aria-label="GitHub"></div>
457 <div class="brand-logo brand-nvidia" aria-label="NVIDIA"></div>
458 <div class="brand-logo brand-visa" aria-label="Visa"></div>
459 <div class="brand-logo brand-zoominfo" aria-label="ZoomInfo"></div>
460 <div class="brand-logo brand-github" aria-label="GitHub"></div>
461 </div>
462 </div>
463 </div>
464 </div>
465 <!-- /.camera -->
466
467 <div class="vignette"></div>
468 </div>
469
470 <script>
471 /* ================================================================
472 CONSTANTS — all derived once, never recomputed at tween time.
473 Composition is 1920 × 1080, total duration 8.0s.
474 ================================================================ */
475 const W = 1920,
476 H = 1080;
477 const FPS_HASH = 60; // synthetic clock for the hacker-flip flicker hash only
478
479 // Self-contained placeholder data — swap for real asset paths in production.
480 const CREATOR_INITIALS = ["JC", "MK", "AR", "TS", "LP", "DV", "NW", "SB", "KH", "EM"];
481
482 const TIMING = {
483 // Phase 1 — logo pop + HyperFrames decode
484 logoPop: 0.32,
485 flipStart: 0.4, // logoPop + ~5/60
486 flipStagger: 0.033, // 2 frames at 60fps per glyph
487 flipDuration: 0.55,
488
489 // Phase 2 — slide-out + claim slide-in.
490 swapTrigger: 1.38,
491 swapDuration: 0.55,
492 claimFadeIn: 1.65, // swapTrigger + ~8/60
493 claimFadeDur: 0.27,
494
495 // Rolling ticker — render → ship.
496 tickerTrigger: 2.35,
497 tickerDuration: 0.45,
498
499 // Phase 3 — logo recenters (anchored ~0.25s before "trusted")
500 recenterTrigger: 3.87,
501 recenterDuration: 0.9,
502
503 // Phase 4 — cloud + counter.
504 cloudFadeIn: 4.12,
505 cloudFadeDur: 0.4,
506 avatarEntryStart: 4.3,
507 avatarStagger: 0.1,
508 avatarEntryDur: 0.55,
509 linesDelay: 0.2, // gap after last avatar
510 linesDur: 0.45,
511 lineStagger: 0.033,
512
513 counterStart: 4.92,
514 counterDuration: 0.85,
515 suffixDelay: 0.05,
516
517 // Phase 5 — brand logos
518 brandStripStart: 6.5,
519 brandStripFadeDur: 0.35,
520 brandLogoStagger: 0.1,
521 brandScrollDur: 1.4, // until end (8.0 - 6.5 - 0.1 buffer)
522 };
523
524 // Layout constants — shared center keeps logo, avatars, and lines aligned.
525 const RECENTER_OFFSET = -W * 0.12; // = -230.4 px — container shift in Phase 2
526 const CENTER_OFFSET = 800; // Phase 3: logo center aligns with cloud center.
527
528 // Cloud geometry — logo final position and line origins derive from this center.
529 const CLOUD_CENTER_X = W / 2;
530 const CLOUD_CENTER_Y = H * 0.47; // = 507.6 px
531 const VERTICAL_ADJUST = CLOUD_CENTER_Y - H / 2;
532 const CLOUD_RADIUS_X = W * 0.25; // = 480 px
533 const CLOUD_RADIUS_Y = H * 0.22; // = 237.6 px
534 const AVATAR_COUNT = 10;
535
536 /* ================================================================
537 BUILD DOM — flip glyphs, avatars, connection lines.
538 Runs synchronously before the timeline registration.
539 ================================================================ */
540
541 // ---- Hacker-flip glyphs for "HyperFrames" -------------------------
542 const phase1 = document.querySelector(".phase1-text");
543 const decodeText = "HyperFrames";
544 decodeText.split("").forEach((char, index) => {
545 const span = document.createElement("span");
546 span.className = "flip-glyph" + (char === " " ? " space" : "");
547 span.dataset.char = char;
548 span.dataset.index = String(index);
549 const ghost = document.createElement("span");
550 ghost.className = "ghost";
551 ghost.textContent = char === " " ? " " : char;
552 const anim = document.createElement("span");
553 anim.className = "anim";
554 anim.textContent = char === " " ? " " : char;
555 span.append(ghost, anim);
556 phase1.appendChild(span);
557 });
558
559 // ---- Avatar cloud + connection lines ------------------------------
560 const cloudLines = document.querySelector(".cloud-lines");
561 const cloudAvatars = document.querySelector(".cloud-avatars");
562 const avatarPositions = [];
563 const avatars = CREATOR_INITIALS.slice(0, AVATAR_COUNT);
564
565 for (let i = 0; i < avatars.length; i++) {
566 const angle = (i / avatars.length) * Math.PI * 2 - Math.PI / 2;
567 const x = CLOUD_CENTER_X + Math.cos(angle) * CLOUD_RADIUS_X;
568 const y = CLOUD_CENTER_Y + Math.sin(angle) * CLOUD_RADIUS_Y;
569 avatarPositions.push({ x, y });
570
571 const size = 90 + (i % 3) * 15;
572 const av = document.createElement("div");
573 av.className = "cloud-avatar";
574 av.style.left = x - size / 2 + "px";
575 av.style.top = y - size / 2 + "px";
576 av.style.width = size + "px";
577 av.style.height = size + "px";
578 av.dataset.index = String(i);
579 // Initials placeholder — deterministic hue from index (no asset files).
580 av.textContent = avatars[i];
581 av.style.background = `linear-gradient(135deg, hsl(${(220 + i * 36) % 360}, 70%, 62%), hsl(${(260 + i * 36) % 360}, 70%, 46%))`;
582 cloudAvatars.appendChild(av);
583
584 // SVG line — both endpoints start at center; will tween x2/y2 outward.
585 const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
586 line.setAttribute("x1", CLOUD_CENTER_X);
587 line.setAttribute("y1", CLOUD_CENTER_Y);
588 line.setAttribute("x2", CLOUD_CENTER_X);
589 line.setAttribute("y2", CLOUD_CENTER_Y);
590 line.setAttribute("stroke", "#00e676");
591 line.setAttribute("stroke-width", "2");
592 line.setAttribute("stroke-dasharray", "4 4");
593 line.setAttribute("stroke-opacity", "0");
594 cloudLines.appendChild(line);
595 }
596
597 /* ================================================================
598 TIMELINE
599 ================================================================ */
600 window.__timelines = window.__timelines || {};
601 const tl = gsap.timeline({ paused: true });
602
603 // ----------------------------------------------------------------
604 // Camera: gentle initial zoom + drift. Finite, computed from duration.
605 // ----------------------------------------------------------------
606 tl.fromTo(".camera", { scale: 0.95 }, { scale: 1.0, duration: 0.95, ease: "power2.out" }, 0);
607
608 // Subtle drift across the whole 8s. Finite yoyo cycles, not infinite.
609 // sin(frame * 0.003) cycle = 2π / 0.003 frames = 2094 frames ≈ 34.9s at 60fps.
610 // Over 8s we see roughly a quarter cycle — model as a single slow yoyo.
611 tl.fromTo(
612 ".camera",
613 { x: 0, y: 0 },
614 { x: 3, y: 2, duration: 4, ease: "sine.inOut", yoyo: true, repeat: 1 },
615 0,
616 );
617
618 // ----------------------------------------------------------------
619 // PHASE 1: Logo pop + HyperFrames decode
620 // ----------------------------------------------------------------
621 tl.fromTo(
622 ".anchor-logo",
623 { scale: 0 },
624 {
625 scale: 1,
626 duration: 0.6,
627 ease: "back.out(1.4)", // spring(stiffness:180, damping:12)
628 },
629 TIMING.logoPop,
630 );
631
632 // Hacker-flip per glyph. Each glyph has its own tween with onUpdate
633 // that derives the visible character from time.
634 const CHAR_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&";
635 const FLICKER = 3; // frames between glyph reshuffles
636 const REVEAL_AT = 0.6; // progress threshold to swap random → real
637
638 function pseudoHash(i, t) {
639 // Cheap deterministic 32-bit mix — no Math.random, no Date.now
640 return ((i * 374761393 + t * 668265263) >>> 0) % CHAR_POOL.length;
641 }
642
643 document.querySelectorAll(".flip-glyph").forEach((glyph) => {
644 const index = Number(glyph.dataset.index);
645 const real = glyph.dataset.char === " " ? " " : glyph.dataset.char;
646 const anim = glyph.querySelector(".anim");
647 const start = TIMING.flipStart + index * TIMING.flipStagger;
648
649 tl.fromTo(
650 anim,
651 { rotationX: 90, opacity: 0, "--p": 0 },
652 {
653 rotationX: 0,
654 opacity: 1,
655 "--p": 1,
656 duration: TIMING.flipDuration,
657 ease: "back.out(1.6)", // spring(stiffness:150, damping:14)
658 onUpdate: function () {
659 const p = Number(gsap.getProperty(anim, "--p"));
660 if (p >= REVEAL_AT) {
661 if (anim.textContent !== real) anim.textContent = real;
662 } else {
663 const localFrame = Math.floor((tl.time() - start) * FPS_HASH);
664 const bucket = Math.max(0, Math.floor(localFrame / FLICKER));
665 anim.textContent = CHAR_POOL[pseudoHash(index, bucket)];
666 }
667 },
668 },
669 start,
670 );
671 });
672
673 // ----------------------------------------------------------------
674 // PHASE 2: Swap — old text slides out, container shifts left,
675 // claim fades in at the old text's origin.
676 // ----------------------------------------------------------------
677 tl.to(
678 ".phase1-text",
679 {
680 x: 200,
681 opacity: 0,
682 duration: TIMING.swapDuration * 0.5,
683 ease: "power3.out",
684 },
685 TIMING.swapTrigger,
686 );
687 tl.set(".phase1-text .anim", { opacity: 0 }, TIMING.swapTrigger + TIMING.swapDuration * 0.5);
688
689 tl.to(
690 ".anchor-shift",
691 {
692 x: RECENTER_OFFSET,
693 duration: TIMING.swapDuration,
694 ease: "power3.out", // spring(stiffness:80, damping:18)
695 },
696 TIMING.swapTrigger,
697 );
698
699 tl.to(
700 ".phase2-claim",
701 {
702 opacity: 1,
703 duration: TIMING.claimFadeDur,
704 ease: "power2.out",
705 },
706 TIMING.claimFadeIn,
707 );
708
709 // Rolling ticker — render → ship.
710 tl.to(
711 ".ticker-stack",
712 {
713 y: "-=204", // one itemHeight
714 duration: TIMING.tickerDuration,
715 ease: "back.out(1.4)", // spring(stiffness:120, damping:14)
716 },
717 TIMING.tickerTrigger,
718 );
719
720 // ----------------------------------------------------------------
721 // PHASE 3: Logo recenters + text fades out.
722 // ----------------------------------------------------------------
723 tl.to(
724 ".anchor-text",
725 {
726 opacity: 0,
727 duration: 0.3,
728 ease: "power2.out",
729 },
730 TIMING.recenterTrigger,
731 );
732
733 tl.to(
734 ".anchor-logo",
735 {
736 x: CENTER_OFFSET,
737 y: VERTICAL_ADJUST,
738 duration: TIMING.recenterDuration,
739 ease: "power2.out", // spring(stiffness:45, damping:22) — gentle
740 },
741 TIMING.recenterTrigger,
742 );
743
744 // ----------------------------------------------------------------
745 // PHASE 4: Avatar cloud + counter.
746 // ----------------------------------------------------------------
747 tl.to(
748 ".cloud-stage",
749 {
750 opacity: 1,
751 duration: TIMING.cloudFadeDur,
752 ease: "power2.out",
753 },
754 TIMING.cloudFadeIn,
755 );
756
757 // Avatars cascade in.
758 tl.fromTo(
759 ".cloud-avatar",
760 { scale: 0, opacity: 0 },
761 {
762 scale: 1,
763 opacity: 1,
764 duration: TIMING.avatarEntryDur,
765 ease: "back.out(1.7)",
766 stagger: { each: TIMING.avatarStagger, from: "start" },
767 },
768 TIMING.avatarEntryStart,
769 );
770
771 // Connection lines draw outward from center after all avatars settle.
772 const lastAvatarEnd =
773 TIMING.avatarEntryStart + TIMING.avatarStagger * (AVATAR_COUNT - 1) + TIMING.avatarEntryDur;
774 const linesStart = lastAvatarEnd + TIMING.linesDelay;
775
776 document.querySelectorAll(".cloud-lines line").forEach((line, i) => {
777 const { x, y } = avatarPositions[i];
778 tl.to(
779 line,
780 {
781 attr: { x2: x, y2: y },
782 strokeOpacity: 0.6,
783 duration: TIMING.linesDur,
784 ease: "power2.out",
785 },
786 linesStart + i * TIMING.lineStagger,
787 );
788 });
789
790 const countEl = document.querySelector(".count");
791 const suffixEl = document.querySelector(".count-suffix");
792
793 // Counter pulse — text stays deterministic under timeline seeking.
794 tl.fromTo(
795 countEl,
796 {
797 scale: 0.88,
798 },
799 {
800 scale: 1,
801 duration: TIMING.counterDuration,
802 ease: "power2.out",
803 },
804 TIMING.counterStart,
805 );
806
807 tl.to(
808 suffixEl,
809 {
810 opacity: 1,
811 duration: 0.3,
812 ease: "back.out(1.6)",
813 },
814 TIMING.counterStart + TIMING.counterDuration + TIMING.suffixDelay,
815 );
816
817 // ----------------------------------------------------------------
818 // PHASE 5: Brand strip — label + scrolling partner logos.
819 // ----------------------------------------------------------------
820 tl.fromTo(
821 ".brand-strip",
822 { opacity: 0, y: 40 },
823 {
824 opacity: 1,
825 y: 0,
826 duration: TIMING.brandStripFadeDur,
827 ease: "power2.out",
828 },
829 TIMING.brandStripStart,
830 );
831
832 tl.fromTo(
833 ".brand-logo",
834 { scale: 0.5, opacity: 0 },
835 {
836 scale: 1,
837 opacity: 0.7,
838 duration: 0.5,
839 ease: "back.out(1.4)",
840 stagger: { each: TIMING.brandLogoStagger, from: "start" },
841 },
842 TIMING.brandStripStart,
843 );
844
845 // Finite horizontal scroll. Distance derived from remaining time × speed.
846 // Speed in original: 0.8 px/frame at 30fps = 24 px/s. Over 1.4s ≈ 33.6 px.
847 // For a more visible scroll, multiply by 4 → ~135 px.
848 tl.to(
849 ".brand-strip-track",
850 {
851 x: -135,
852 duration: TIMING.brandScrollDur,
853 ease: "none",
854 },
855 TIMING.brandStripStart,
856 );
857
858 window.__timelines["main"] = tl;
859 </script>
860 </body>
861</html>