/* Veld β€” atmospheric magic Ember canvas, time-of-day greeting, countdown, moon phase, battlement divider, ink reveal, flicker word. */ const { useEffect: useEffect_m, useRef: useRef_m, useState: useState_m } = React; /* ---------- EMBER CANVAS (drifting fireflies, particle system) ---------- */ const EMBER_COLORS = ['#d8a86a', '#e07830', '#f0a040', '#c49b5c', '#ff8030', '#e8c994']; function EmberCanvas({ density = 0.04, opacity = 0.85 }) { const ref = useRef_m(null); useEffect_m(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; let raf = 0; let particles = []; let visible = true; const resize = () => { const dpr = Math.min(window.devicePixelRatio || 1, 1.5); const rect = canvas.parentElement.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; canvas.style.width = rect.width + 'px'; canvas.style.height = rect.height + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; const w = () => canvas.clientWidth; const h = () => canvas.clientHeight; const spawn = () => ({ x: Math.random() * w(), y: h() + 10, vx: (Math.random() - 0.5) * 0.5, vy: Math.random() * 1.1 + 0.35, r: Math.random() * 1.6 + 0.4, op: Math.random() * 0.55 + 0.3, life: 0, color: EMBER_COLORS[Math.floor(Math.random() * EMBER_COLORS.length)], }); const target = () => Math.max(8, Math.min(50, Math.floor(w() * density))); const init = () => { particles = []; const n = target(); for (let i = 0; i < n; i++) { const p = spawn(); p.y = Math.random() * h(); p.life = Math.random(); particles.push(p); } }; const draw = () => { if (!visible) { raf = 0; return; } ctx.clearRect(0, 0, w(), h()); for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; p.life += p.vy / h(); p.vx += (Math.random() - 0.5) * 0.07; p.vx *= 0.97; p.x += p.vx; p.y -= p.vy; const fade = 1 - p.life; ctx.save(); ctx.globalAlpha = p.op * fade; ctx.shadowColor = p.color; ctx.shadowBlur = 6; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fillStyle = p.color; ctx.fill(); ctx.restore(); if (p.life >= 1 || p.y < -10) particles[i] = spawn(); } while (particles.length < target()) particles.push(spawn()); raf = requestAnimationFrame(draw); }; // Pause when offscreen to save CPU const io = new IntersectionObserver( ([entry]) => { visible = entry.isIntersecting; if (visible && !raf) raf = requestAnimationFrame(draw); }, { threshold: 0 } ); if (canvas.parentElement) io.observe(canvas.parentElement); const onResize = () => { resize(); init(); }; window.addEventListener('resize', onResize); resize(); init(); if (visible) raf = requestAnimationFrame(draw); return () => { window.removeEventListener('resize', onResize); io.disconnect(); if (raf) cancelAnimationFrame(raf); }; }, [density]); return (