chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:35 +08:00
commit 85453da49f
4031 changed files with 710987 additions and 0 deletions
@@ -0,0 +1,861 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=1920, height=1080" />
<title>Scene 03 — HyperFrames Anchor Chain Reveal</title>
<!--
HyperFrames-native version of the scene-03-anchor-chain-reveal concept.
Same 5-phase choreography, GSAP-driven, seek-safe.
Audio (assumed accompanying narration):
"Meet HyperFrames, the HTML video composition toolkit for building,
previewing, and rendering programmable motion."
Sub-shots (local time, 8s total):
0.0 1.2s "HyperFrames" Brand reveal (hacker-flip)
1.2 4.0s "HTML VIDEO" Text swap + rolling ticker
4.0 6.3s "60FPS" Counter + capability cloud
6.3 8.0s "Built for Motion" Capability strip
Placeholders (self-contained — no asset files needed):
inline-SVG "HF" mark — brand logo (swap for your image)
CSS initials circles — user avatars (edit CREATOR_INITIALS)
CSS text chips — partner brand logos (edit --brand-name)
Replace with real assets for production.
-->
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
:root {
/* Palette — direct port of the the source COLORS constants */
--text-primary: #ffffff;
--text-muted: rgba(255, 255, 255, 0.45);
--brand-green: #00e676;
--bg-grad-from: #0a1a2e;
--bg-grad-to: #1a0a2e;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
margin: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000;
font-family:
"Inter",
system-ui,
-apple-system,
sans-serif;
color: var(--text-primary);
}
/* ============================================================
BACKGROUND (port of <FrostedGlassBackground />)
============================================================ */
.bg {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse at 30% 20%, rgba(0, 230, 118, 0.18), transparent 50%),
radial-gradient(ellipse at 70% 80%, rgba(120, 60, 220, 0.18), transparent 50%),
linear-gradient(135deg, var(--bg-grad-from), var(--bg-grad-to));
}
/* Camera wrapper — port of <CameraSystem /> */
.camera {
position: absolute;
inset: 0;
transform-origin: center center;
}
/* ============================================================
PHASES 13 (BrandReveal): logo + decode + swap + recenter
============================================================ */
.brand-stage {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 50; /* above cloud SVG (z:1), below logo (z:100) */
}
.anchor-shift {
display: flex;
align-items: center;
gap: 35px;
/* GSAP tweens .x on this element for Phase-2 recenter */
}
.anchor-logo {
position: relative;
width: 192px;
height: 192px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 100; /* highest — sits above lines + avatars */
opacity: 1;
/* initial scale(0) set by GSAP fromTo — no CSS transform here */
}
.anchor-logo img {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 4px 24px rgba(0, 0, 0, 0.4));
}
.anchor-text {
position: relative;
display: flex;
align-items: center;
}
.phase1-text {
display: flex;
perspective: 800px; /* required for the per-glyph rotateX */
white-space: nowrap;
}
.flip-glyph {
position: relative;
display: inline-block;
font-size: 163px; /* 1920 × 0.085 — matches the source brandFontSize */
font-weight: 900;
letter-spacing: -0.02em;
}
.flip-glyph .ghost {
opacity: 0;
}
.flip-glyph .anim {
position: absolute;
left: 0;
top: 0;
width: 100%;
color: var(--text-primary);
opacity: 0;
transform: perspective(600px) rotateX(90deg);
transform-origin: bottom;
backface-visibility: hidden;
}
.flip-glyph.space {
min-width: 0.35em;
}
.phase2-claim {
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
opacity: 0;
white-space: nowrap;
display: flex;
align-items: center;
gap: 40px;
}
.claim-rank {
color: var(--brand-green);
font-size: 163px;
font-weight: 900;
letter-spacing: -0.02em;
}
.claim-ai,
.claim-video {
color: var(--text-primary);
font-size: 163px;
font-weight: 900;
letter-spacing: -0.02em;
}
/* Vertical ticker — port of <RollingWord /> */
.ticker-window {
height: 204px; /* fontSize × 1.25 = 163 × 1.25 ≈ 204 */
overflow: hidden;
display: inline-flex;
flex-direction: column;
vertical-align: bottom;
}
.ticker-stack {
display: flex;
flex-direction: column;
will-change: transform;
}
.ticker-item {
height: 204px;
display: flex;
align-items: center;
color: var(--text-primary);
font-weight: 900;
font-size: 163px;
letter-spacing: -0.02em;
line-height: 1;
}
/* ============================================================
PHASE 4 (CreatorCloud): counter + avatar cloud + lines
============================================================ */
.cloud-stage {
position: absolute;
inset: 0;
z-index: 10; /* below brand-stage's logo (z:100), below counter */
opacity: 0; /* GSAP fades it in */
}
.counter-pin {
position: absolute;
top: 3%;
left: 50%;
transform: translateX(-50%);
text-align: center;
z-index: 60; /* above brand-stage so it overlays cleanly */
}
.count {
font-variant-numeric: tabular-nums;
font-weight: 900;
font-size: 115px; /* 1920 × 0.06 */
color: var(--brand-green);
display: inline-block;
min-width: 200px;
text-align: right;
}
.count-suffix {
font-weight: 900;
font-size: 115px;
color: var(--brand-green);
opacity: 0; /* GSAP fades in after count */
}
.cloud-lines {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 1;
}
.cloud-avatars {
position: absolute;
inset: 0;
z-index: 10;
}
.cloud-avatar {
position: absolute;
border-radius: 50%;
overflow: hidden;
border: 3px solid rgba(255, 255, 255, 0.2);
opacity: 0;
/* initial scale(0) set by GSAP fromTo — no CSS transform here */
background: linear-gradient(135deg, #6b8cff, #a6b3ff);
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.8);
font-weight: 700;
font-size: 32px;
}
/* ============================================================
PHASE 5 (BrandShowcase): label + scrolling brand strip
============================================================ */
.brand-strip {
position: absolute;
bottom: 8%;
left: 0;
right: 0;
opacity: 0; /* GSAP fades + translateY in via fromTo */
z-index: 70;
}
.brand-label {
text-align: center;
margin-bottom: 25px;
font-size: 27px; /* 1920 × 0.014 */
font-weight: 500;
color: var(--text-muted);
letter-spacing: 0.2em;
text-transform: uppercase;
}
.brand-strip-window {
display: flex;
justify-content: center;
align-items: center;
gap: 80px;
overflow: hidden;
}
.brand-strip-track {
display: flex;
gap: 110px;
}
.brand-logo {
width: 170px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
/* initial scale(0.5) set by GSAP fromTo — no CSS transform here */
}
.brand-logo::before {
content: var(--brand-name);
font-family: Inter, system-ui, sans-serif;
font-size: 30px;
font-weight: 800;
letter-spacing: 0.08em;
white-space: nowrap;
color: rgba(255, 255, 255, 0.72);
}
.brand-nvidia {
--brand-name: "NVIDIA";
}
.brand-visa {
--brand-name: "VISA";
}
.brand-zoominfo {
--brand-name: "ZOOMINFO";
}
.brand-github {
--brand-name: "GITHUB";
}
/* Vignette overlay */
.vignette {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.45) 100%);
pointer-events: none;
z-index: 200;
}
</style>
</head>
<body>
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="8"
data-width="1920"
data-height="1080"
>
<div class="bg"></div>
<div class="camera">
<!-- =====================================================
PHASES 13: BrandReveal (logo + hacker-flip + swap)
===================================================== -->
<div
id="phase-anchor"
class="brand-stage clip"
data-start="0"
data-duration="8"
data-track-index="1"
>
<div class="anchor-shift" data-layout-allow-overflow>
<!-- Anchor logo — persists across all phases -->
<div class="anchor-logo">
<!-- Inline-SVG placeholder mark — swap for your logo image -->
<svg
class="logo-mark"
viewBox="0 0 100 100"
width="100%"
height="100%"
role="img"
aria-label="hyperframes logo"
>
<defs>
<linearGradient id="hfMark" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#8b7bff" />
<stop offset="1" stop-color="#3ddc97" />
</linearGradient>
</defs>
<rect x="4" y="4" width="92" height="92" rx="22" fill="url(#hfMark)" />
<text
x="50"
y="63"
text-anchor="middle"
font-family="Inter, system-ui, sans-serif"
font-size="40"
font-weight="800"
fill="#fff"
>
HF
</text>
</svg>
</div>
<!-- Swappable text zone -->
<div class="anchor-text">
<!-- Phase 1: 'HyperFrames' decoded character by character -->
<div class="phase1-text" aria-label="HyperFrames">
<!-- Spans generated by the script below -->
</div>
<!-- Phase 2: 'HTML video render/ship' claim -->
<div class="phase2-claim">
<span class="claim-rank">HTML</span>
<span class="claim-ai">Video</span>
<div class="claim-ticker">
<div class="ticker-window" data-layout-allow-overflow>
<div class="ticker-stack">
<div class="ticker-item">render</div>
<div class="ticker-item">ship</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- =====================================================
PHASE 4: CreatorCloud (counter + avatar ring + lines)
===================================================== -->
<div
id="phase-cloud"
class="cloud-stage clip"
data-start="3.6"
data-duration="4.4"
data-track-index="2"
>
<div class="counter-pin">
<span class="count">60</span><span class="count-suffix">FPS</span>
</div>
<svg class="cloud-lines" width="1920" height="1080">
<defs>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#00e676" stop-opacity="0.8" />
<stop offset="100%" stop-color="#00e676" stop-opacity="0.2" />
</linearGradient>
</defs>
<!-- <line> elements generated by JS -->
</svg>
<div class="cloud-avatars">
<!-- .cloud-avatar elements generated by JS -->
</div>
</div>
<!-- =====================================================
PHASE 5: BrandShowcase (label + scrolling logos)
===================================================== -->
<div
id="phase-brands"
class="brand-strip clip"
data-start="6.3"
data-duration="1.7"
data-track-index="3"
>
<div class="brand-label">Trusted by Leading Brands</div>
<div class="brand-strip-window" data-layout-allow-overflow>
<div class="brand-strip-track">
<div class="brand-logo brand-nvidia" aria-label="NVIDIA"></div>
<div class="brand-logo brand-visa" aria-label="Visa"></div>
<div class="brand-logo brand-zoominfo" aria-label="ZoomInfo"></div>
<div class="brand-logo brand-github" aria-label="GitHub"></div>
<div class="brand-logo brand-nvidia" aria-label="NVIDIA"></div>
<div class="brand-logo brand-visa" aria-label="Visa"></div>
<div class="brand-logo brand-zoominfo" aria-label="ZoomInfo"></div>
<div class="brand-logo brand-github" aria-label="GitHub"></div>
<div class="brand-logo brand-nvidia" aria-label="NVIDIA"></div>
<div class="brand-logo brand-visa" aria-label="Visa"></div>
<div class="brand-logo brand-zoominfo" aria-label="ZoomInfo"></div>
<div class="brand-logo brand-github" aria-label="GitHub"></div>
</div>
</div>
</div>
</div>
<!-- /.camera -->
<div class="vignette"></div>
</div>
<script>
/* ================================================================
CONSTANTS — all derived once, never recomputed at tween time.
Composition is 1920 × 1080, total duration 8.0s.
================================================================ */
const W = 1920,
H = 1080;
const FPS_HASH = 60; // synthetic clock for the hacker-flip flicker hash only
// Self-contained placeholder data — swap for real asset paths in production.
const CREATOR_INITIALS = ["JC", "MK", "AR", "TS", "LP", "DV", "NW", "SB", "KH", "EM"];
const TIMING = {
// Phase 1 — logo pop + HyperFrames decode
logoPop: 0.32,
flipStart: 0.4, // logoPop + ~5/60
flipStagger: 0.033, // 2 frames at 60fps per glyph
flipDuration: 0.55,
// Phase 2 — slide-out + claim slide-in.
swapTrigger: 1.38,
swapDuration: 0.55,
claimFadeIn: 1.65, // swapTrigger + ~8/60
claimFadeDur: 0.27,
// Rolling ticker — render → ship.
tickerTrigger: 2.35,
tickerDuration: 0.45,
// Phase 3 — logo recenters (anchored ~0.25s before "trusted")
recenterTrigger: 3.87,
recenterDuration: 0.9,
// Phase 4 — cloud + counter.
cloudFadeIn: 4.12,
cloudFadeDur: 0.4,
avatarEntryStart: 4.3,
avatarStagger: 0.1,
avatarEntryDur: 0.55,
linesDelay: 0.2, // gap after last avatar
linesDur: 0.45,
lineStagger: 0.033,
counterStart: 4.92,
counterDuration: 0.85,
suffixDelay: 0.05,
// Phase 5 — brand logos
brandStripStart: 6.5,
brandStripFadeDur: 0.35,
brandLogoStagger: 0.1,
brandScrollDur: 1.4, // until end (8.0 - 6.5 - 0.1 buffer)
};
// Layout constants — shared center keeps logo, avatars, and lines aligned.
const RECENTER_OFFSET = -W * 0.12; // = -230.4 px — container shift in Phase 2
const CENTER_OFFSET = 800; // Phase 3: logo center aligns with cloud center.
// Cloud geometry — logo final position and line origins derive from this center.
const CLOUD_CENTER_X = W / 2;
const CLOUD_CENTER_Y = H * 0.47; // = 507.6 px
const VERTICAL_ADJUST = CLOUD_CENTER_Y - H / 2;
const CLOUD_RADIUS_X = W * 0.25; // = 480 px
const CLOUD_RADIUS_Y = H * 0.22; // = 237.6 px
const AVATAR_COUNT = 10;
/* ================================================================
BUILD DOM — flip glyphs, avatars, connection lines.
Runs synchronously before the timeline registration.
================================================================ */
// ---- Hacker-flip glyphs for "HyperFrames" -------------------------
const phase1 = document.querySelector(".phase1-text");
const decodeText = "HyperFrames";
decodeText.split("").forEach((char, index) => {
const span = document.createElement("span");
span.className = "flip-glyph" + (char === " " ? " space" : "");
span.dataset.char = char;
span.dataset.index = String(index);
const ghost = document.createElement("span");
ghost.className = "ghost";
ghost.textContent = char === " " ? " " : char;
const anim = document.createElement("span");
anim.className = "anim";
anim.textContent = char === " " ? " " : char;
span.append(ghost, anim);
phase1.appendChild(span);
});
// ---- Avatar cloud + connection lines ------------------------------
const cloudLines = document.querySelector(".cloud-lines");
const cloudAvatars = document.querySelector(".cloud-avatars");
const avatarPositions = [];
const avatars = CREATOR_INITIALS.slice(0, AVATAR_COUNT);
for (let i = 0; i < avatars.length; i++) {
const angle = (i / avatars.length) * Math.PI * 2 - Math.PI / 2;
const x = CLOUD_CENTER_X + Math.cos(angle) * CLOUD_RADIUS_X;
const y = CLOUD_CENTER_Y + Math.sin(angle) * CLOUD_RADIUS_Y;
avatarPositions.push({ x, y });
const size = 90 + (i % 3) * 15;
const av = document.createElement("div");
av.className = "cloud-avatar";
av.style.left = x - size / 2 + "px";
av.style.top = y - size / 2 + "px";
av.style.width = size + "px";
av.style.height = size + "px";
av.dataset.index = String(i);
// Initials placeholder — deterministic hue from index (no asset files).
av.textContent = avatars[i];
av.style.background = `linear-gradient(135deg, hsl(${(220 + i * 36) % 360}, 70%, 62%), hsl(${(260 + i * 36) % 360}, 70%, 46%))`;
cloudAvatars.appendChild(av);
// SVG line — both endpoints start at center; will tween x2/y2 outward.
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", CLOUD_CENTER_X);
line.setAttribute("y1", CLOUD_CENTER_Y);
line.setAttribute("x2", CLOUD_CENTER_X);
line.setAttribute("y2", CLOUD_CENTER_Y);
line.setAttribute("stroke", "#00e676");
line.setAttribute("stroke-width", "2");
line.setAttribute("stroke-dasharray", "4 4");
line.setAttribute("stroke-opacity", "0");
cloudLines.appendChild(line);
}
/* ================================================================
TIMELINE
================================================================ */
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// ----------------------------------------------------------------
// Camera: gentle initial zoom + drift. Finite, computed from duration.
// ----------------------------------------------------------------
tl.fromTo(".camera", { scale: 0.95 }, { scale: 1.0, duration: 0.95, ease: "power2.out" }, 0);
// Subtle drift across the whole 8s. Finite yoyo cycles, not infinite.
// sin(frame * 0.003) cycle = 2π / 0.003 frames = 2094 frames ≈ 34.9s at 60fps.
// Over 8s we see roughly a quarter cycle — model as a single slow yoyo.
tl.fromTo(
".camera",
{ x: 0, y: 0 },
{ x: 3, y: 2, duration: 4, ease: "sine.inOut", yoyo: true, repeat: 1 },
0,
);
// ----------------------------------------------------------------
// PHASE 1: Logo pop + HyperFrames decode
// ----------------------------------------------------------------
tl.fromTo(
".anchor-logo",
{ scale: 0 },
{
scale: 1,
duration: 0.6,
ease: "back.out(1.4)", // spring(stiffness:180, damping:12)
},
TIMING.logoPop,
);
// Hacker-flip per glyph. Each glyph has its own tween with onUpdate
// that derives the visible character from time.
const CHAR_POOL = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%&";
const FLICKER = 3; // frames between glyph reshuffles
const REVEAL_AT = 0.6; // progress threshold to swap random → real
function pseudoHash(i, t) {
// Cheap deterministic 32-bit mix — no Math.random, no Date.now
return ((i * 374761393 + t * 668265263) >>> 0) % CHAR_POOL.length;
}
document.querySelectorAll(".flip-glyph").forEach((glyph) => {
const index = Number(glyph.dataset.index);
const real = glyph.dataset.char === " " ? " " : glyph.dataset.char;
const anim = glyph.querySelector(".anim");
const start = TIMING.flipStart + index * TIMING.flipStagger;
tl.fromTo(
anim,
{ rotationX: 90, opacity: 0, "--p": 0 },
{
rotationX: 0,
opacity: 1,
"--p": 1,
duration: TIMING.flipDuration,
ease: "back.out(1.6)", // spring(stiffness:150, damping:14)
onUpdate: function () {
const p = Number(gsap.getProperty(anim, "--p"));
if (p >= REVEAL_AT) {
if (anim.textContent !== real) anim.textContent = real;
} else {
const localFrame = Math.floor((tl.time() - start) * FPS_HASH);
const bucket = Math.max(0, Math.floor(localFrame / FLICKER));
anim.textContent = CHAR_POOL[pseudoHash(index, bucket)];
}
},
},
start,
);
});
// ----------------------------------------------------------------
// PHASE 2: Swap — old text slides out, container shifts left,
// claim fades in at the old text's origin.
// ----------------------------------------------------------------
tl.to(
".phase1-text",
{
x: 200,
opacity: 0,
duration: TIMING.swapDuration * 0.5,
ease: "power3.out",
},
TIMING.swapTrigger,
);
tl.set(".phase1-text .anim", { opacity: 0 }, TIMING.swapTrigger + TIMING.swapDuration * 0.5);
tl.to(
".anchor-shift",
{
x: RECENTER_OFFSET,
duration: TIMING.swapDuration,
ease: "power3.out", // spring(stiffness:80, damping:18)
},
TIMING.swapTrigger,
);
tl.to(
".phase2-claim",
{
opacity: 1,
duration: TIMING.claimFadeDur,
ease: "power2.out",
},
TIMING.claimFadeIn,
);
// Rolling ticker — render → ship.
tl.to(
".ticker-stack",
{
y: "-=204", // one itemHeight
duration: TIMING.tickerDuration,
ease: "back.out(1.4)", // spring(stiffness:120, damping:14)
},
TIMING.tickerTrigger,
);
// ----------------------------------------------------------------
// PHASE 3: Logo recenters + text fades out.
// ----------------------------------------------------------------
tl.to(
".anchor-text",
{
opacity: 0,
duration: 0.3,
ease: "power2.out",
},
TIMING.recenterTrigger,
);
tl.to(
".anchor-logo",
{
x: CENTER_OFFSET,
y: VERTICAL_ADJUST,
duration: TIMING.recenterDuration,
ease: "power2.out", // spring(stiffness:45, damping:22) — gentle
},
TIMING.recenterTrigger,
);
// ----------------------------------------------------------------
// PHASE 4: Avatar cloud + counter.
// ----------------------------------------------------------------
tl.to(
".cloud-stage",
{
opacity: 1,
duration: TIMING.cloudFadeDur,
ease: "power2.out",
},
TIMING.cloudFadeIn,
);
// Avatars cascade in.
tl.fromTo(
".cloud-avatar",
{ scale: 0, opacity: 0 },
{
scale: 1,
opacity: 1,
duration: TIMING.avatarEntryDur,
ease: "back.out(1.7)",
stagger: { each: TIMING.avatarStagger, from: "start" },
},
TIMING.avatarEntryStart,
);
// Connection lines draw outward from center after all avatars settle.
const lastAvatarEnd =
TIMING.avatarEntryStart + TIMING.avatarStagger * (AVATAR_COUNT - 1) + TIMING.avatarEntryDur;
const linesStart = lastAvatarEnd + TIMING.linesDelay;
document.querySelectorAll(".cloud-lines line").forEach((line, i) => {
const { x, y } = avatarPositions[i];
tl.to(
line,
{
attr: { x2: x, y2: y },
strokeOpacity: 0.6,
duration: TIMING.linesDur,
ease: "power2.out",
},
linesStart + i * TIMING.lineStagger,
);
});
const countEl = document.querySelector(".count");
const suffixEl = document.querySelector(".count-suffix");
// Counter pulse — text stays deterministic under timeline seeking.
tl.fromTo(
countEl,
{
scale: 0.88,
},
{
scale: 1,
duration: TIMING.counterDuration,
ease: "power2.out",
},
TIMING.counterStart,
);
tl.to(
suffixEl,
{
opacity: 1,
duration: 0.3,
ease: "back.out(1.6)",
},
TIMING.counterStart + TIMING.counterDuration + TIMING.suffixDelay,
);
// ----------------------------------------------------------------
// PHASE 5: Brand strip — label + scrolling partner logos.
// ----------------------------------------------------------------
tl.fromTo(
".brand-strip",
{ opacity: 0, y: 40 },
{
opacity: 1,
y: 0,
duration: TIMING.brandStripFadeDur,
ease: "power2.out",
},
TIMING.brandStripStart,
);
tl.fromTo(
".brand-logo",
{ scale: 0.5, opacity: 0 },
{
scale: 1,
opacity: 0.7,
duration: 0.5,
ease: "back.out(1.4)",
stagger: { each: TIMING.brandLogoStagger, from: "start" },
},
TIMING.brandStripStart,
);
// Finite horizontal scroll. Distance derived from remaining time × speed.
// Speed in original: 0.8 px/frame at 30fps = 24 px/s. Over 1.4s ≈ 33.6 px.
// For a more visible scroll, multiply by 4 → ~135 px.
tl.to(
".brand-strip-track",
{
x: -135,
duration: TIMING.brandScrollDur,
ease: "none",
},
TIMING.brandStripStart,
);
window.__timelines["main"] = tl;
</script>
</body>
</html>