// Explore — auto-assembling cosmic handoff between Splash and Home. // // Light theme. The 8 module pieces start scattered around the canvas. As soon // as the screen loads they AUTO-FLY one-by-one into their orbital slots // (timed sequence driven by useLoop). After the last piece lands the U // monogram drops into the centre. Then a big gold "EXPLORE NOW →" CTA // appears in the centre — a single tap takes the user to /home. // // The user is a spectator: pieces auto-place, no per-piece taps required. // The headline morphs through three states (assembling → welcome → ready). // Skip → CTA bottom-right is preserved. const EXPLORE_KEYS_ID = 'uni-explore-puzzle-keys'; function ensureExploreKeys() { if (typeof document === 'undefined') return; if (document.getElementById(EXPLORE_KEYS_ID)) return; const s = document.createElement('style'); s.id = EXPLORE_KEYS_ID; s.textContent = ` @keyframes uniSlotPulse { 0%, 100% { box-shadow: 0 0 0 1px rgba(176,138,63,0.22), 0 0 14px rgba(232,215,168,0.30); } 50% { box-shadow: 0 0 0 1px rgba(176,138,63,0.55), 0 0 30px rgba(232,215,168,0.75); } } @keyframes uniPieceWiggle { 0%, 100% { transform: rotate(var(--wig, 0deg)) translateY(0); } 50% { transform: rotate(calc(var(--wig, 0deg) * -0.7)) translateY(-6px); } } @keyframes uniPieceLock { 0% { box-shadow: 0 18px 36px rgba(50,32,12,0.20); } 30% { box-shadow: 0 0 0 12px rgba(232,215,168,0.32), 0 0 70px 20px rgba(232,215,168,0.55); } 60% { box-shadow: 0 0 0 22px rgba(232,215,168,0); } 100% { box-shadow: 0 18px 36px rgba(50,32,12,0.22); } } @keyframes uniCenterDrop { 0% { transform: translate(-50%, -50%) translateY(-220px) scale(0.4) rotate(-22deg); opacity: 0; } 55% { transform: translate(-50%, -50%) translateY(10px) scale(1.10) rotate(2deg); opacity: 1; } 75% { transform: translate(-50%, -50%) translateY(-4px) scale(0.97) rotate(0deg); opacity: 1; } 100% { transform: translate(-50%, -50%) translateY(0) scale(1) rotate(0deg); opacity: 1; } } @keyframes uniCtaPulse { 0%, 100% { box-shadow: 0 17px 38px rgba(176,138,63,0.47), 0 0 0 4px rgba(232,215,168,0.22), 0 0 60px rgba(232,215,168,0.36); } 50% { box-shadow: 0 17px 38px rgba(176,138,63,0.55), 0 0 0 8px rgba(232,215,168,0.34), 0 0 100px rgba(232,215,168,0.70); } } @keyframes uniCtaIn { 0% { opacity: 0; transform: translate(-50%, -50%) translateY(20px) scale(0.85); filter: blur(8px); } 100% { opacity: 1; transform: translate(-50%, -50%) translateY(0) scale(1); filter: blur(0); } } @keyframes uniHintArrow { 0%, 100% { transform: translateX(0); opacity: 0.7; } 50% { transform: translateX(8px); opacity: 1; } } @keyframes uniReadyHeadlineIn { 0% { opacity: 0; transform: translateY(10px); filter: blur(6px); } 100% { opacity: 1; transform: translateY(0); filter: blur(0); } } `; document.head.appendChild(s); } function Explore() { ensureExploreKeys(); const t = useLoop(); const [exiting, setExiting] = React.useState(false); const [bursts, fireBurst] = useBurst(900); const firedRef = React.useRef({}); // piece-landing burst dedupe const modules = PROJECT.modules; const UIcons = window.UIcons || {}; const Apartment10 = window.HomeApartment10; const CosmicDust = window.HomeCosmicDust; // Geometry — match Home so the transition feels like the puzzle settling. // CY tracks the LIVE canvas height so the cosmos sits optically centred and the // screen reads full at BOTH 16:10 (H 1600 → CY ≈ 832, unchanged) and 4:3 iPad // (H 1920 → CY ≈ 998) with no empty bottom band. const CANVAS_H = (typeof window !== 'undefined' && window.UNIVERSE_CANVAS && window.UNIVERSE_CANVAS.H) || 1600; const CX = 1280, CY = Math.round(CANVAS_H * 0.52); const SAT_R = 460; const TILE = 172; const CIRCLE_R = 220; // Auto-placement timing const FIRST_AT = 0.5; // first piece starts flying at 0.5s const STEP = 0.30; // stagger between pieces const FLIGHT_DUR = 0.85; // tween length const LAST_LAND = FIRST_AT + 7 * STEP + FLIGHT_DUR; // ≈ 3.45s const CENTER_AT = LAST_LAND + 0.05; // monogram drops const CENTER_DUR = 0.92; const READY_AT = CENTER_AT + CENTER_DUR + 0.25; // ≈ 4.67s // Scatter positions (manually balanced) const SCATTER = React.useMemo(() => ([ { sx: 240, sy: 350, rot: -8 }, { sx: 2120, sy: 360, rot: 10 }, { sx: 2350, sy: 880, rot: -6 }, { sx: 2200, sy: 1320, rot: 7 }, { sx: 1280, sy: 1430, rot: 0 }, { sx: 360, sy: 1320, rot: -7 }, { sx: 220, sy: 880, rot: 8 }, { sx: 760, sy: 220, rot: -5 }, ]), []); // Per-module geometry const pieces = React.useMemo(() => modules.map((m, i) => { const angle = (i * Math.PI / 4) - Math.PI / 2; const tx = CX + SAT_R * Math.cos(angle); const ty = CY + SAT_R * Math.sin(angle); return { m, i, tx, ty, angle, ...SCATTER[i] }; }), []); // Compute live placement progress for each piece const placement = pieces.map((p) => { const startAt = FIRST_AT + p.i * STEP; const local = clamp((t - startAt) / FLIGHT_DUR); const e = ease.outBack(local); const flying = local > 0 && local < 1; const placed = local >= 1; return { ...p, startAt, local, e, flying, placed }; }); const placedCount = placement.reduce((acc, p) => acc + (p.placed ? 1 : 0), 0); const allPlaced = placedCount === 8; const centerLanded = t >= CENTER_AT + CENTER_DUR; const ready = t >= READY_AT; // Fire a burst as each piece lands (once) React.useEffect(() => { placement.forEach((p) => { if (p.placed && !firedRef.current[p.m.id]) { firedRef.current[p.m.id] = true; fireBurst(p.tx, p.ty, { count: 12 }); } }); // eslint-disable-next-line }, [placedCount]); // Auto-transition to home shortly after the centre logo lands. The home // page detects the `uni-from-explore` flag and slides the entire orbital // cluster from EXPLORE-CENTRE → HOME-RIGHT with a CSS transform — that's // where the user sees the icons + monogram glide into place. Doing the // slide on the home side avoids fighting the route-transition cross-fade. const handoffRef = React.useRef(false); React.useEffect(() => { if (!centerLanded || handoffRef.current) return; handoffRef.current = true; setTimeout(() => { try { sessionStorage.setItem('uni-from-explore', '1'); } catch(_) {} navigate('home'); }, 720); // brief settling pause so the user sees the formed cosmos }, [centerLanded]); // No-op on explore — slide happens on home const handoff = false; const SLIDE_X = 0; const handleSkip = () => { try { sessionStorage.setItem('uni-from-explore', '1'); } catch(_) {} setExiting(true); setTimeout(() => navigate('home'), 320); }; const titleP = clamp((t - 0.0) / 0.6); const slotsP = clamp((t - 0.4) / 0.6); // Three headline states (no CTA gate — last state plays during auto-handoff) let eyebrow, headlineLead, headlineGold; if (!allPlaced) { eyebrow = 'ASSEMBLING YOUR UNIVERSE'; headlineLead = <>Assembling your ; headlineGold = <>universe…; } else if (!centerLanded) { eyebrow = 'CENTRE LOCKING IN'; headlineLead = <>Welcome to ; headlineGold = <>The Universe.; } else { eyebrow = 'ENTERING THE UNIVERSE'; headlineLead = <>Welcome to ; headlineGold = <>The Universe.; } return (
{/* Light cosmic backdrop — same visual language as Home */} {CosmicDust && }
{/* TOP — eyebrow + headline. Fades out when the handoff begins so the cluster slide can dominate visually. */}
{eyebrow}
{headlineLead}{headlineGold}
{/* SLOT OUTLINES — pulsing rings at each orbital position; fade once filled */} {placement.map((p) => { const isPlaced = p.placed; return (
); })} {/* Centre slot — pulses harder once the 8 are in, then yields to monogram */}
{/* === CLUSTER WRAPPER ============================================ When `handoff` flips, this whole group glides 600px to the right so the centre logo + 8 orbital pieces + spokes land in EXACTLY the same screen position the home page paints them — perfect continuity across the route change. */}
{/* Constellation spokes — draw in once each piece lands */} {placement.map((p, i) => { if (!p.placed) return null; const innerX = CX + (CIRCLE_R + 23) * Math.cos(p.angle); const innerY = CY + (CIRCLE_R + 23) * Math.sin(p.angle); const outerX = CX + (SAT_R - TILE/2 - 17) * Math.cos(p.angle); const outerY = CY + (SAT_R - TILE/2 - 17) * Math.sin(p.angle); return ( ); })} {/* CENTRE PIECE — drops in once all 8 satellites are placed */} {allPlaced && (
{/* outer halo */}
{/* main circle */}
{/* tick marks at 8 satellite positions */} {Array.from({length:8}).map((_, i) => { const a = (i * Math.PI/4) - Math.PI/2; const r1 = 50, r2 = 56; const x1 = 50 + r1*Math.cos(a), y1 = 50 + r1*Math.sin(a); const x2 = 50 + r2*Math.cos(a), y2 = 50 + r2*Math.sin(a); return ; })} {/* apartment sketch backdrop */}
{Apartment10 && }
{/* monogram */}
)} {/* No CTA — once centre lands the screen auto-transitions to /home. */} {/* PUZZLE PIECES — auto-fly from scatter to slot */} {placement.map((p) => { const Glyph = UIcons[p.m.id]; const { e, placed: isPlaced, flying } = p; // Tween position from scatter → slot using outBack(e) const x = p.sx + (p.tx - p.sx) * e; const y = p.sy + (p.ty - p.sy) * e; // Subtle wiggle until it starts flying const wig = (e === 0) ? p.rot : 0; // Scale: slight pop while flying, settle to 1 once placed const scale = isPlaced ? 1 : (flying ? (0.92 + 0.08 * e) : 1); return (
{/* keystone tick */}
{/* number badge */}
0{p.i + 1}
{/* glyph */}
{Glyph && }
{/* base hairline */}
{/* label below — fades in once placed */}
{p.m.label}
); })}
{/* === END CLUSTER WRAPPER === */} {/* PROGRESS METER — bottom centre. Fades during handoff. */}
{placedCount}/8
{Math.round((placedCount/8)*100)}%
{!allPlaced ? 'PIECES LOCKING INTO ORBIT…' : (centerLanded ? 'ENTERING THE UNIVERSE…' : 'CENTRE LOCKING IN…')}
{/* SKIP CTA bottom-right (still requires a tap). Fades during handoff. */}
); } window.Explore = Explore;