// Ambient background dust — many small, low-opacity phone-shaped particles (the "C" from the logo) // drifting slowly across the whole page. Purely decorative: pointer-events disabled, never grabs attention. const ParticlesFX = () => { const canvasRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const img = new Image(); img.src = (window.__resources && window.__resources.confirmariIcon) || "assets/confirmari_icon.png"; let particles = []; let raf = null; let dpr = Math.min(window.devicePixelRatio || 1, 2); let docHeight = document.documentElement.scrollHeight; const rand = (a, b) => a + Math.random() * (b - a); // y is in document space (0..docHeight) so particles stay put relative to the page as you scroll. const makeParticle = (w, h) => ({ x: rand(0, w), y: rand(0, h), size: rand(10, 26), ratio: 211 / 88, // source image height/width rot: rand(0, Math.PI * 2), rotSpeed: rand(-0.0025, 0.0025), vx: rand(-0.10, 0.10), vy: rand(-0.18, -0.04), // gentle upward drift, like dust opacity: rand(0.035, 0.11), }); const resizeCanvas = () => { const w = window.innerWidth, h = window.innerHeight; canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + "px"; canvas.style.height = h + "px"; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; const seedParticles = () => { docHeight = document.documentElement.scrollHeight; const count = window.innerWidth < 760 ? 90 : 165; particles = Array.from({ length: count }, () => makeParticle(window.innerWidth, docHeight)); }; const draw = () => { const w = window.innerWidth, h = window.innerHeight; const scrollY = window.scrollY; ctx.clearRect(0, 0, w, h); for (const p of particles) { if (!prefersReducedMotion) { p.x += p.vx; p.y += p.vy; p.rot += p.rotSpeed; if (p.y < -40) p.y = docHeight + 40; if (p.y > docHeight + 40) p.y = -40; if (p.x < -40) p.x = w + 40; if (p.x > w + 40) p.x = -40; } const screenY = p.y - scrollY; if (screenY < -40 || screenY > h + 40) continue; const pw = p.size, ph = p.size * p.ratio; ctx.save(); ctx.globalAlpha = p.opacity; ctx.translate(p.x, screenY); ctx.rotate(p.rot); ctx.drawImage(img, -pw / 2, -ph / 2, pw, ph); ctx.restore(); } if (!prefersReducedMotion) raf = requestAnimationFrame(draw); }; const onResize = () => { resizeCanvas(); seedParticles(); }; const start = () => { resizeCanvas(); seedParticles(); draw(); }; if (img.complete) start(); else img.onload = start; window.addEventListener("resize", onResize); window.addEventListener("scroll", () => { if (prefersReducedMotion) draw(); }, { passive: true }); return () => { window.removeEventListener("resize", onResize); if (raf) cancelAnimationFrame(raf); }; }, []); return (