import { useEffect, useRef } from "react"; /** Floating atmospheric dust — pure CSS-cheap canvas pass. */ export function AmbientParticles() { const ref = useRef(null); useEffect(() => { const c = ref.current!; const ctx = c.getContext("2d")!; let raf = 0, w = 0, h = 0; const dpr = Math.min(window.devicePixelRatio || 1, 2); const particles: { x: number; y: number; vx: number; vy: number; r: number; a: number }[] = []; function resize() { const r = c.getBoundingClientRect(); w = r.width; h = r.height; c.width = w * dpr; c.height = h * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); particles.length = 0; const n = Math.floor((w * h) / 28000); for (let i = 0; i < n; i++) { particles.push({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 0.15, vy: (Math.random() - 0.5) * 0.15 - 0.05, r: 0.4 + Math.random() * 1.2, a: 0.1 + Math.random() * 0.3, }); } } resize(); const ro = new ResizeObserver(resize); ro.observe(c); function tick() { raf = requestAnimationFrame(tick); ctx.clearRect(0, 0, w, h); for (const p of particles) { p.x += p.vx; p.y += p.vy; if (p.x < 0) p.x = w; if (p.x > w) p.x = 0; if (p.y < 0) p.y = h; if (p.y > h) p.y = 0; ctx.fillStyle = `hsla(220, 80%, 90%, ${p.a})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); } } raf = requestAnimationFrame(tick); return () => { cancelAnimationFrame(raf); ro.disconnect(); }; }, []); return ; }