85453da49f
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
862 lines
28 KiB
HTML
862 lines
28 KiB
HTML
<!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 1–3 (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 1–3: 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>
|