/* global React */ const { useEffect, useRef } = React; /* ========================================================== HERO SCENE — layered CSS/SVG matte ========================================================== */ // Distant ridge / mesa silhouette function HeroRidge() { return ( ); } // Buildings flanking the street — flat ochre planes with hard shadows function HeroBuildings({ parallaxY = 0 }) { return ( ); } // Receding street plane function HeroStreet() { return ( ); } // Tiny opponent silhouette in the distance function HeroOpponent() { return ( ); } /* ========================================================== HUD REVOLVER — single-action, leather grip, brass frame ========================================================== */ function HudRevolver({ hammerTwitch }) { return ( ); } /* ========================================================== DUST PARTICLES — canvas ========================================================== */ function DustCanvas({ scrollSpeed, reduced }) { const ref = useRef(null); const rafRef = useRef(0); const speedRef = useRef(scrollSpeed); useEffect(() => { speedRef.current = scrollSpeed; }, [scrollSpeed]); useEffect(() => { if (reduced) return; const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext("2d"); let w = canvas.width = canvas.offsetWidth * window.devicePixelRatio; let h = canvas.height = canvas.offsetHeight * window.devicePixelRatio; const dpr = window.devicePixelRatio; const N = 80; const particles = Array.from({ length: N }, () => ({ x: Math.random() * w, y: Math.random() * h, r: (0.5 + Math.random() * 1.8) * dpr, vx: (0.15 + Math.random() * 0.4) * dpr, vy: (Math.random() - 0.5) * 0.05 * dpr, a: 0.15 + Math.random() * 0.45, })); const onResize = () => { w = canvas.width = canvas.offsetWidth * window.devicePixelRatio; h = canvas.height = canvas.offsetHeight * window.devicePixelRatio; }; window.addEventListener("resize", onResize); const tick = () => { ctx.clearRect(0, 0, w, h); const mult = 1 + speedRef.current * 2.5; for (const p of particles) { p.x += p.vx * mult; p.y += p.vy; if (p.x > w + 10) { p.x = -10; p.y = Math.random() * h; } ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fillStyle = `rgba(236, 226, 200, ${p.a})`; ctx.fill(); } rafRef.current = requestAnimationFrame(tick); }; const start = () => { if (rafRef.current) return; // already running tick(); }; const stop = () => { if (rafRef.current) { cancelAnimationFrame(rafRef.current); rafRef.current = 0; } }; // Resume when page becomes visible after backgrounding const onVisibility = () => { if (document.visibilityState === "visible") start(); else stop(); }; document.addEventListener("visibilitychange", onVisibility); // Resume / pause when canvas enters / leaves viewport const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { if (entry.isIntersecting) start(); else stop(); } }, { threshold: 0 } ); observer.observe(canvas); start(); return () => { stop(); window.removeEventListener("resize", onResize); document.removeEventListener("visibilitychange", onVisibility); observer.disconnect(); }; }, [reduced]); return