/* dust.jsx — site-wide falling gold-dust background + Tweaks panel. Self-contained: owns the tweak state, renders a fixed full-viewport canvas behind all content, and the Tweaks panel that drives it. */ const { useRef: useRefD, useEffect: useEffectD } = React; const DUST_DEFAULTS = /*EDITMODE-BEGIN*/{ "dustOn": true, "dustDirection": "falling", "dustAmount": 80, "dustSpeed": 1, "dustSize": 1, "dustGlow": true, "dustPalette": ["#c19a51", "#e8cf94", "#fff4d8", "#b3893f", "#d99a52"] }/*EDITMODE-END*/; const DUST_PALETTES = [ ["#c19a51", "#e8cf94", "#fff4d8", "#b3893f", "#d99a52"], // classic gold ["#e8cf94", "#fff4d8", "#fffaf0", "#f0dca8"], // champagne ["#d98c6a", "#e7c2a0", "#f6e3cf", "#c47044"], // warm copper ["#dfe7ef", "#ffffff", "#cdd8e6", "#aebfd4"], // silver / snow ]; const reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* build a soft round sprite per color once, reuse via drawImage (fast) */ function makeSprite(color, glow) { const S = 64; const c = document.createElement('canvas'); c.width = c.height = S; const ctx = c.getContext('2d'); const g = ctx.createRadialGradient(S / 2, S / 2, 0, S / 2, S / 2, S / 2); g.addColorStop(0, color); g.addColorStop(glow ? 0.35 : 0.5, color); g.addColorStop(1, 'rgba(0,0,0,0)'); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(S / 2, S / 2, S / 2, 0, Math.PI * 2); ctx.fill(); return c; } function GoldDustCanvas({ on, direction, amount, speed, size, glow, palette }) { const canvasRef = useRefD(null); const sim = useRefD({ parts: [], sprites: [], raf: 0, w: 0, h: 0, dpr: 1 }); // (re)build sprites when palette / glow changes useEffectD(() => { sim.current.sprites = palette.map((c) => makeSprite(c, glow)); }, [palette, glow]); useEffectD(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const s = sim.current; const resize = () => { s.dpr = Math.min(window.devicePixelRatio || 1, 2); s.w = window.innerWidth; s.h = window.innerHeight; canvas.width = s.w * s.dpr; canvas.height = s.h * s.dpr; canvas.style.width = s.w + 'px'; canvas.style.height = s.h + 'px'; ctx.setTransform(s.dpr, 0, 0, s.dpr, 0, 0); }; resize(); const spawn = (atEdge) => { const falling = direction === 'falling'; const drift = direction === 'drift'; const r = (1.1 + Math.random() * 3.4) * size; return { x: Math.random() * s.w, y: atEdge ? (falling ? -10 - Math.random() * s.h * 0.3 : s.h + 10 + Math.random() * s.h * 0.3) : Math.random() * s.h, r, // base vertical velocity (px/frame @60fps); drift is gentle both ways vy: (drift ? (Math.random() - 0.5) * 0.25 : (0.18 + Math.random() * 0.5)) * (falling ? 1 : -1) * (1 + (r - 2) * 0.18), sway: 0.3 + Math.random() * 0.9, swayPhase: Math.random() * Math.PI * 2, swaySpeed: 0.006 + Math.random() * 0.012, baseAlpha: 0.35 + Math.random() * 0.5, twPhase: Math.random() * Math.PI * 2, twSpeed: 0.01 + Math.random() * 0.02, spriteI: (Math.random() * Math.max(1, s.sprites.length)) | 0, }; }; const build = () => { const N = reduceMotion ? Math.min(20, amount) : amount; s.parts = Array.from({ length: N }, () => spawn(false)); }; build(); let last = performance.now(); const tick = (now) => { const dt = Math.min(2.4, (now - last) / 16.67); // frames elapsed, capped last = now; ctx.clearRect(0, 0, s.w, s.h); const falling = direction === 'falling'; const drift = direction === 'drift'; for (const p of s.parts) { p.y += p.vy * speed * dt; p.swayPhase += p.swaySpeed * dt; p.x += Math.sin(p.swayPhase) * p.sway * speed * dt; p.twPhase += p.twSpeed * dt; // recycle off-screen if (falling && p.y - p.r > s.h) { Object.assign(p, spawn(true), { y: -10 - Math.random() * 40 }); } else if (!falling && !drift && p.y + p.r < 0) { Object.assign(p, spawn(true), { y: s.h + 10 + Math.random() * 40 }); } else if (drift) { if (p.y - p.r > s.h) p.y = -10; if (p.y + p.r < 0) p.y = s.h + 10; } if (p.x < -20) p.x = s.w + 20; else if (p.x > s.w + 20) p.x = -20; const tw = 0.6 + 0.4 * Math.sin(p.twPhase); ctx.globalAlpha = Math.max(0, Math.min(1, p.baseAlpha * tw)); const spr = s.sprites[p.spriteI % s.sprites.length]; if (spr) { const d = p.r * (glow ? 4.2 : 2.6); ctx.drawImage(spr, p.x - d / 2, p.y - d / 2, d, d); } } ctx.globalAlpha = 1; s.raf = requestAnimationFrame(tick); }; if (on) s.raf = requestAnimationFrame(tick); window.addEventListener('resize', resize); return () => { cancelAnimationFrame(s.raf); window.removeEventListener('resize', resize); }; }, [on, direction, amount, speed, size, glow, palette]); if (!on) return null; return (