/* global React */ const { useState, useEffect, useRef, useCallback } = React; /* ─── Atmosphere: cursor, grain, whispers, sound ──────────────────────── */ function Atmosphere({ grainOpacity, whispersOn, sound }) { const cursorRef = useRef(null); const haloRef = useRef(null); const [whispers, setWhispers] = useState([]); const [cursorMode, setCursorMode] = useState("normal"); // cursor tracking useEffect(() => { let raf; let x = window.innerWidth / 2, y = window.innerHeight / 2; let cx = x, cy = y, hx = x, hy = y; const onMove = (e) => { x = e.clientX; y = e.clientY; }; const tick = () => { cx += (x - cx) * 0.25; cy += (y - cy) * 0.25; hx += (x - hx) * 0.06; hy += (y - hy) * 0.06; if (cursorRef.current) cursorRef.current.style.transform = `translate(${cx}px, ${cy}px) translate(-50%, -50%)`; if (haloRef.current) haloRef.current.style.transform = `translate(${hx}px, ${hy}px) translate(-50%, -50%)`; raf = requestAnimationFrame(tick); }; window.addEventListener("mousemove", onMove); tick(); const onOver = (e) => { const tag = e.target.closest("button, a, [data-cursor]"); setCursorMode(tag ? "dot" : "normal"); }; document.addEventListener("mouseover", onOver); return () => { cancelAnimationFrame(raf); window.removeEventListener("mousemove", onMove); document.removeEventListener("mouseover", onOver); }; }, []); // drifting whispers useEffect(() => { if (!whispersOn) return; const arr = window.WHISPERS || []; let id = 0; const spawn = () => { const text = arr[Math.floor(Math.random() * arr.length)]; const top = 18 + Math.random() * 64; // % const left = 10 + Math.random() * 70; // % const newW = { id: id++, text, top, left }; setWhispers((ws) => [...ws.slice(-3), newW]); setTimeout(() => setWhispers((ws) => ws.filter(w => w.id !== newW.id)), 22000); }; const t1 = setTimeout(spawn, 4000); const iv = setInterval(spawn, 11000); return () => { clearTimeout(t1); clearInterval(iv); }; }, [whispersOn]); return ( <>
{whispers.map(w => (
⌇ {w.text}
))}
); } window.Atmosphere = Atmosphere;