// AP2 Fraction Opening Movie — clean center-only // Geometric motion leading to the AP2 FRACTION logo. 8-second loop. // No kicker, no HUD, no subhead, no index — just the reveal. const CC = { base: '#F4F1EA', baseDim:'rgba(244,241,234,0.55)', baseFaint:'rgba(244,241,234,0.12)', ink: '#0A0A09', ink2: '#141311', green: '#1F3A2E', greenL: '#2E5142', brass: '#A08256', brassL: '#C4A172', }; const TAU = Math.PI * 2; const lerp = (a, b, t) => a + (b - a) * t; // ─────────────────────────────────────────────── // Scene (8s loop): // 0.0 – 1.2 grid fades in; scan line sweeps; central axis draws // 1.2 – 3.0 particles rise from horizon, align to a ring // 3.0 – 4.8 ring forms; brass orbit hairline rotates; ticks pulse // 4.8 – 6.0 ring splits vertically, halves slide apart // 5.2 – 6.8 logo emerges between the halves; green rule draws // 6.8 – 7.1 logo flash / punch // 7.1 – 7.9 BURST — ring & logo shatter into particles flying outward // 7.9 – 8.0 black, ready to loop // ─────────────────────────────────────────────── function Scene({ isPortrait = false }) { const t = useTime(); const { duration } = useTimeline(); const W = 1920, H = 1080; const cx = W / 2, cy = H / 2; // Portrait-mode sizing overrides — stage is still 1920×1080 but the host // scales & crops sides, so we shrink the circle & enlarge the text in // stage-space so content reads correctly on phones. const R = isPortrait ? 130 : 280; const splitMaxX = isPortrait ? 130 : 280; const logoWidthPct = isPortrait ? '14%' : '22%'; const subFontSize = isPortrait ? 16 : 24; const subLetterSpacing = isPortrait ? 4 : 10; const subMarginTop = isPortrait ? 36 : 56; // global fade: very gradual outro so ending never feels abrupt const inFade = clamp(t / 0.6, 0, 1); const outFade = 1 - Easing.easeInOutCubic(clamp((t - 7.8) / 2.0, 0, 1)); const globalA = Math.min(inFade, outFade); const gridA = interpolate([0.0, 1.2, 6.2, 7.0], [0, 0.08, 0.08, 0], Easing.easeOutCubic)(t); const scanY = interpolate([0.2, 1.2], [-40, H + 40], Easing.easeInOutCubic)(t); const scanA = interpolate([0.2, 0.6, 1.2], [0, 0.7, 0], Easing.linear)(t); const partIn = clamp((t - 0.6) / 1.4, 0, 1); const partSettle = clamp((t - 1.6) / 1.4, 0, 1); const partOut = 1 - clamp((t - 4.6) / 0.4, 0, 1); // Ambient stars (static) — breathing in with soft delay const starsA = interpolate([0.0, 1.6, 5.5, 7.0], [0, 0.9, 0.9, 0], Easing.easeInOutCubic)(t); const breathe = 0.85 + 0.15 * Math.sin(t * TAU * 0.25); const N = 72; // Ring draws slowly from the start — no particles, just pure circle drawing const ringDraw = Easing.easeInOutCubic(clamp(t / 3.2, 0, 1)); const ringA = Easing.easeOutCubic(clamp(t / 0.4, 0, 1)); const splitP = Easing.easeInOutCubic(clamp((t - 3.2) / 1.0, 0, 1)); const splitX = splitP * splitMaxX; // ring + split halves vanish at burst moment const splitA = 1 - clamp((t - 6.9) / 0.08, 0, 1); // Both monogram (A/P) and wordmark (AP2 | FRACTION) use the same smooth left-to-right sweep // Slower than before for a more elegant pen-trace feel const monoA = Easing.easeInOutCubic(clamp((t - 3.6) / 1.4, 0, 1)); const wordA = Easing.easeInOutCubic(clamp((t - 5.0) / 1.8, 0, 1)); const h1A = Math.max(monoA, wordA); // used elsewhere for fade gating const h1Y = 0; // logo doesn't move — stays still until burst // ─── Burst phase ─── // 6.85–7.05: anticipation punch-up // 7.05: flash // 7.05–9.55: SLOW shatter — 2.5s so pieces are clearly visible const BURST_START = 6.9; const BURST_DUR = 2.5; const burstT = clamp((t - BURST_START) / BURST_DUR, 0, 1); const burstE = Easing.easeOutCubic(burstT); // Logo stays at scale 1 until burst moment, then snaps to 0 (it has already shattered into pieces) const logoScale = 1; const h1Out = 1 - clamp((t - BURST_START) / 0.05, 0, 1); const ruleW = Easing.easeInOutCubic(clamp((t - 5.9) / 0.8, 0, 1)); const ruleOut = 1 - clamp((t - 6.85) / 0.1, 0, 1); const axisW = Easing.easeOutCubic(clamp((t - 0.1) / 0.8, 0, 1)); return (
{!isPortrait && (
)} {/* Ambient stars — soft romantic backdrop */} {Array.from({ length: 70 }, (_, i) => { const sx = ((i * 2654435761) >>> 0) / 4294967296; const sy = ((i * 1597334677) >>> 0) / 4294967296; const sr = ((i * 3266489917) >>> 0) / 4294967296; const x = sx * W; const y = sy * H; // per-star slow twinkle const tw = 0.6 + 0.4 * Math.sin(t * 1.4 + i * 0.7); const isBrass = i % 11 === 0; const r = 0.4 + sr * 1.2; return ( ); })} {/* particles removed — ring draws on its own */} 0.02 ? 1 : 0)}> {/* ─── BURST — only the things that existed actually shatter ─── */} {burstT > 0 && burstT < 1 && (() => { const GRAVITY = 380; const time = burstT * BURST_DUR; const fadeT = clamp((burstT - 0.6) / 0.4, 0, 1); const pieceA = 1 - fadeT; // Helper: deterministic pseudo-random const rand = (n) => ((n * 2654435761) >>> 0) / 4294967296; // ── WIND: lateral gust that ramps in, with gentle oscillation // Accelerates pieces sideways over time → "blown away" feel const WIND_X = 520; // rightward push strength (px/s²-ish) const WIND_Y = -180; // slight upward lift from wind const windRamp = Easing.easeOutCubic(clamp(time / 0.6, 0, 1)); // Per-piece wind multiplier (some pieces catch more wind than others) const windMul = (seed) => 0.55 + seed * 0.9 + Math.sin(time * 3 + seed * 10) * 0.15; // ── Ring shatters into many small arc fragments const ringChunks = []; { let a = 0; let idx = 0; while (a < TAU - 0.005) { const span = 0.06 + rand(idx * 7 + 3) * 0.09; // small: ~3–9° per chunk, ~40–60 chunks const aEnd = Math.min(a + span, TAU); const midA = (a + aEnd) / 2; const seed = rand(idx * 13 + 5); const seed2 = rand(idx * 31 + 17); const speed = 160 + seed * 260; const vx = Math.cos(midA) * speed; const vy = Math.sin(midA) * speed - 40; const wm = windMul(seed); const x = cx + vx * time + 0.5 * WIND_X * wm * time * time * windRamp; const y = cy + vy * time + 0.5 * GRAVITY * time * time + 0.5 * WIND_Y * wm * time * time * windRamp; const spin = (seed2 * 360 + (320 + seed * 480) * time * (seed < 0.5 ? 1 : -1)); const x1 = Math.cos(a) * R; const y1 = Math.sin(a) * R; const x2 = Math.cos(aEnd) * R; const y2 = Math.sin(aEnd) * R; ringChunks.push( ); a = aEnd; idx++; } } // ── Inner ring (r-24) also shatters into small pieces const innerChunks = []; { const Ri = R - 24; let a = 0.2; let idx = 0; while (a < TAU + 0.2 - 0.005) { const span = 0.07 + rand(idx * 11 + 9) * 0.11; const aEnd = Math.min(a + span, TAU + 0.2); const midA = (a + aEnd) / 2; const seed = rand(idx * 19 + 23); const seed2 = rand(idx * 41 + 37); const speed = 130 + seed * 230; const vx = Math.cos(midA) * speed; const vy = Math.sin(midA) * speed - 30; const wm = windMul(seed2); const x = cx + vx * time + 0.5 * WIND_X * wm * time * time * windRamp; const y = cy + vy * time + 0.5 * GRAVITY * time * time + 0.5 * WIND_Y * wm * time * time * windRamp; const spin = (seed2 * 360 + (280 + seed * 440) * time * (seed < 0.5 ? 1 : -1)); const x1 = Math.cos(a) * Ri; const y1 = Math.sin(a) * Ri; const x2 = Math.cos(aEnd) * Ri; const y2 = Math.sin(aEnd) * Ri; innerChunks.push( ); a = aEnd; idx++; } } // ── Green rule — breaks into many small pieces const greenPieces = []; { const totalW = 280; const startX = cx - 140; const startY = cy + 200; let x0 = 0; let idx = 0; while (x0 < totalW - 1) { const segLen = 8 + rand(idx * 73 + 11) * 14; // small: 8–22px each const segEnd = Math.min(x0 + segLen, totalW); const midX = (x0 + segEnd) / 2; const seed = rand(idx * 53 + 7); const seed2 = rand(idx * 89 + 41); const ang = ((midX / totalW) - 0.5) * 1.2 + (seed - 0.5) * 0.6; const speed = 140 + seed * 180; const vx = Math.sin(ang) * speed; const vy = -120 - seed2 * 90; const wm = windMul(seed); const dx = vx * time + 0.5 * WIND_X * wm * 1.15 * time * time * windRamp; const dy = vy * time + 0.5 * GRAVITY * time * time + 0.5 * WIND_Y * wm * time * time * windRamp; const spin = (seed * 360) + (360 + seed * 480) * time * (seed < 0.5 ? 1 : -1); const pieceLen = segEnd - x0; greenPieces.push( ); x0 = segEnd; idx++; } } return {ringChunks}{innerChunks}{greenPieces}; })()} {/* Center logo — two-stage pen-trace reveal */}
{/* Upper band: A→P monogram (top 67% of image — includes full monogram, stops before divider), left→right wipe driven by monoA */} {/* Pen cursor for monogram */} {monoA > 0 && monoA < 1 && (
)} {/* Lower band: vertical divider + AP2 | FRACTION wordmark (below monogram), left→right wipe driven by wordA */} AP2 FRACTION {/* Pen cursor for wordmark */} {wordA > 0 && wordA < 1 && (
)}
{/* Logo shatter layer — only visible during burst */} {burstT > 0 && burstT < 1 && (() => { const time = burstT * BURST_DUR; const fadeT = clamp((burstT - 0.6) / 0.4, 0, 1); const pieceA = 1 - fadeT; if (pieceA <= 0.01) return null; const rand = (n) => ((n * 2654435761) >>> 0) / 4294967296; // Logo box dims (matches the rendered above) const logoW = (isPortrait ? 0.14 : 0.22) * W; const logoH = logoW * (741 / 891); const logoL = cx - logoW / 2; const logoT = cy - logoH / 2; // Grid subdivision — balanced for visual density vs perf const COLS = isPortrait ? 24 : 32; const ROWS = isPortrait ? 18 : 22; const cellW = logoW / COLS; const cellH = logoH / ROWS; const GRAVITY = 380; const WIND_X = 520; const WIND_Y = -180; const windRamp = Easing.easeOutCubic(clamp(time / 0.6, 0, 1)); const windBase = 0.5 * WIND_X * time * time * windRamp; const windBaseY = 0.5 * WIND_Y * time * time * windRamp; const gravTerm = 0.5 * GRAVITY * time * time; const pieces = []; const bgSize = `${logoW}px ${logoH}px`; for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { const idx = r * COLS + c; const seed = rand(idx * 13 + 7); const seed2 = rand(idx * 41 + 19); // Piece original center in scene coords const px = logoL + c * cellW + cellW / 2; const py = logoT + r * cellH + cellH / 2; // Direction outward from logo center, with jitter const dx0 = px - cx; const dy0 = py - cy; const baseAng = Math.atan2(dy0, dx0); const ang = baseAng + (seed - 0.5) * 0.8; const speed = 180 + seed * 320; const wm = 0.55 + seed2 * 0.9 + Math.sin(time * 3 + seed2 * 10) * 0.15; const dx = Math.cos(ang) * speed * time + windBase * wm; const dy = Math.sin(ang) * speed * time - 60 * time + gravTerm + windBaseY * wm; const spin = (seed2 - 0.5) * 720 * time * (1 + seed); pieces.push(
); } } return
{pieces}
; })()}
); } Object.assign(window, { Scene });