// Splash — LIGHT-THEME variant (cream paper substrate, dark gold accents). // // Same cinematic beats as the dark version, color-inverted for the bright // tablet experience: ignition → cosmos → particles converge to the LOGO IMAGE // pixel positions → real client logo materialises over the constellation → // subtitle → venus credit → warp out into the EXPLORE puzzle. // // The dark version is preserved verbatim at `splash-dark.jsx` if you ever // want to switch back — just swap the script tag in the HTML. // // Particles sample the actual `universe-logo-transparent.png` so the // constellation traces the real client logo silhouette before the image // itself fades in. function ensureSplashKeys() { if (document.getElementById('splash-light-keys')) return; const s = document.createElement('style'); s.id = 'splash-light-keys'; s.textContent = ` @keyframes spOrbitSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes spOrbitSpinRev { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } } @keyframes spHudPulse { 0%,100% { opacity: 0.55; } 50% { opacity: 0.95; } } @keyframes spDotBreatheLight { 0%,100% { transform: scale(1); filter: drop-shadow(0 0 14px rgba(176,138,63,0.65)); } 50% { transform: scale(1.18); filter: drop-shadow(0 0 28px rgba(176,138,63,0.92)); } } `; document.head.appendChild(s); } // Logo display geometry inside the canvas (2560×1600). Centred slightly above middle. const LOGO_W = 720; const LOGO_H = LOGO_W * (787/1400); // ≈ 405 const LOGO_CX = 1280; const LOGO_CY = 760; const LOGO_LFT = LOGO_CX - LOGO_W/2; const LOGO_TOP = LOGO_CY - LOGO_H/2; function Splash() { const t = useLoop(); const [navigated, setNavigated] = React.useState(false); // Sample logo image pixels → particle target positions in canvas coords. const [logoTargets, setLogoTargets] = React.useState(null); React.useEffect(() => { ensureSplashKeys(); const img = new Image(); img.onload = () => { const cv = document.createElement('canvas'); cv.width = img.naturalWidth; cv.height = img.naturalHeight; const ctx = cv.getContext('2d'); ctx.drawImage(img, 0, 0); const data = ctx.getImageData(0, 0, cv.width, cv.height).data; const stride = 7; const out = []; for (let y = 0; y < cv.height; y += stride) { for (let x = 0; x < cv.width; x += stride) { const a = data[(y * cv.width + x) * 4 + 3]; if (a > 180) { out.push({ x: LOGO_LFT + (x / cv.width) * LOGO_W, y: LOGO_TOP + (y / cv.height) * LOGO_H, }); } } } for (let i = out.length - 1; i > 0; i--) { const j = (Math.random() * (i + 1)) | 0; [out[i], out[j]] = [out[j], out[i]]; } setLogoTargets(out); }; img.src = 'assets/brand/universe-logo-transparent.png'; }, []); React.useEffect(() => { // Once the splash settles, hand off to the EXPLORE puzzle screen. const id = setTimeout(() => { setNavigated(true); navigate('explore'); }, 7800); return () => clearTimeout(id); }, []); // ---------- timing schedule ---------- const ignite = clamp((t - 0.0) / 0.5); const bloomFlash = clamp((t - 0.45)/0.18) * (1 - clamp((t - 0.63)/0.5)); const cosmosP = clamp((t - 0.6) / 1.2); const horizonP = clamp((t - 0.7) / 1.1); const hudP = clamp((t - 1.0) / 1.0); const convergeP = clamp((t - 1.8) / 1.4); const bracketsP = clamp((t - 2.2) / 0.6) * (1 - clamp((t - 3.6)/0.6)); const ringsP = clamp((t - 2.6) / 1.0); const logoP = clamp((t - 3.4) / 1.2); const dispersP = clamp((t - 4.2) / 0.9); const subP = clamp((t - 4.7) / 0.9); const credP = clamp((t - 5.4) / 1.0); const warpP = clamp((t - 7.1) / 0.7); const fade = navigated ? clamp((t - 7.3) / 0.5) : 0; // ---------- particle field ---------- const N = 220; const particles = React.useMemo(() => Array.from({length:N}).map((_,i) => { const a = (i * 137.5) * Math.PI / 180; const r = 520 + (i*47 % 980); return { ox: LOGO_CX + Math.cos(a) * r, oy: LOGO_CY + Math.sin(a) * r * 0.7, seed: i, r: 0.6 + (i%4) * 0.45, }; }), []); const targetFor = (p, i) => { if (logoTargets && logoTargets.length) { return logoTargets[i % logoTargets.length]; } const a = (i * 137.5) * Math.PI / 180; return { x: LOGO_CX + Math.cos(a)*60, y: LOGO_CY + Math.sin(a)*60 }; }; const positions = particles.map((p, i) => { const tgt = targetFor(p, i); let cx, cy; if (t < 1.8) { const k = clamp((t - 0.6) / 1.2); const baseX = lerp(p.ox + (p.seed%7)*8, p.ox + Math.sin(t*0.7 + p.seed)*22, k); const baseY = lerp(p.oy + (p.seed%5)*6, p.oy + Math.cos(t*0.6 + p.seed)*18, k); cx = baseX; cy = baseY; } else if (t < 3.2) { const k = ease.inOutCubic((t - 1.8) / 1.4); cx = lerp(p.ox, tgt.x, k); cy = lerp(p.oy, tgt.y, k); } else if (t < 4.2) { cx = tgt.x + Math.sin(t*1.6 + p.seed) * 1.6; cy = tgt.y + Math.cos(t*1.4 + p.seed) * 1.6; } else { const k = ease.outQuart(dispersP); const a = Math.atan2(tgt.y - LOGO_CY, tgt.x - LOGO_CX) + (p.seed%7)*0.07; const dx = tgt.x + Math.cos(a) * 220 * k; const dy = tgt.y + Math.sin(a) * 220 * k; cx = dx; cy = dy; } return { x: cx, y: cy, r: p.r, seed: p.seed }; }); const streaks = (t > 1.8 && t < 3.2) ? particles.map((p, i) => { const tgt = targetFor(p, i); const k = ease.inOutCubic((t - 1.8) / 1.4); const cx = lerp(p.ox, tgt.x, k); const cy = lerp(p.oy, tgt.y, k); const back = Math.max(0, k - 0.12); const bx = lerp(p.ox, tgt.x, back); const by = lerp(p.oy, tgt.y, back); return { x1: bx, y1: by, x2: cx, y2: cy, op: (1 - Math.abs(k - 0.5) * 1.4) * 0.55 }; }) : []; const particleAlpha = 1 - logoP * 0.55; const tw = (text, p) => text.slice(0, Math.round(text.length * clamp(p))); const warpScale = 1 + warpP * 0.42; // === LIGHT theme palette ================================================ const PARTICLE_COL = '#b08a3f'; // dark gold dots on cream const STREAK_COL = '#c9a05e'; const LINE_COL = '#b08a3f'; const HUD_COL = 'rgba(10,10,10,0.55)'; const HUD_DOT = '#b08a3f'; return (