/* store-anim.jsx — motion & interaction primitives for the store. Reveal / CountUp (scroll), CursorGlow, useTilt, useParallax, useCountdown. One shared pointer source (FxPointer) drives glow + parallax. Everything honors prefers-reduced-motion. Exported to window. */ const _fxReduce = () => typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches; /* ---- shared pointer ---- */ const FxPointer = (function () { let s = { x: 0, y: 0, nx: 0, ny: 0 }; const subs = new Set(); if (typeof window !== "undefined") { window.addEventListener("mousemove", (e) => { s = { x: e.clientX, y: e.clientY, nx: (e.clientX / window.innerWidth) * 2 - 1, ny: (e.clientY / window.innerHeight) * 2 - 1, }; subs.forEach((fn) => fn(s)); }, { passive: true }); } return { subscribe(fn) { subs.add(fn); return () => subs.delete(fn); }, get() { return s; } }; })(); /* ---- Reveal (scroll fade + rise) ---- */ function Reveal({ children, y = 26, delay = 0, dur = 720, className = "", style = {}, as: Tag = "div" }) { const ref = React.useRef(null); const [shown, setShown] = React.useState(false); React.useEffect(() => { const el = ref.current; if (!el) return; if (_fxReduce()) { setShown(true); return; } const io = new IntersectionObserver((es) => { es.forEach((e) => { if (e.isIntersecting) { setShown(true); io.unobserve(el); } }); }, { threshold: 0.14, rootMargin: "0px 0px -6% 0px" }); io.observe(el); return () => io.disconnect(); }, []); const ease = "cubic-bezier(.21,.68,.24,1)"; return ( {children} ); } /* ---- CountUp ---- */ function CountUp({ to, from = 0, dur = 1500, prefix = "", suffix = "", decimals = 0, className = "", style = {} }) { const ref = React.useRef(null); const started = React.useRef(false); const [val, setVal] = React.useState(from); React.useEffect(() => { const el = ref.current; if (!el) return; if (_fxReduce()) { setVal(to); return; } const io = new IntersectionObserver((es) => { es.forEach((e) => { if (e.isIntersecting && !started.current) { started.current = true; const t0 = performance.now(); const tick = (now) => { const p = Math.min(1, (now - t0) / dur); const eased = 1 - Math.pow(1 - p, 3); setVal(from + (to - from) * eased); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); io.unobserve(el); } }); }, { threshold: 0.5 }); io.observe(el); return () => io.disconnect(); }, []); return {prefix}{val.toFixed(decimals)}{suffix}; } /* ---- CursorGlow — canvas cursor system: breathing glow + trail + sparkles. One rAF loop, transform/opacity only, capped particle pool. ---- */ function CursorGlow() { const cvsRef = React.useRef(null); React.useEffect(() => { if (_fxReduce()) return; const cvs = cvsRef.current; const ctx = cvs.getContext("2d"); const dpr = Math.min(window.devicePixelRatio || 1, 2); let w = 0, h = 0; const resize = () => { w = window.innerWidth; h = window.innerHeight; cvs.width = w * dpr; cvs.height = h * dpr; cvs.style.width = w + "px"; cvs.style.height = h + "px"; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }; resize(); window.addEventListener("resize", resize); let mx = -600, my = -600, ex = -600, ey = -600, lx = -600, ly = -600, present = 0; const trail = [], sparks = []; const rand = (a, b) => a + Math.random() * (b - a); const unsub = FxPointer.subscribe(({ x, y }) => { mx = x; my = y; }); let raf; const loop = () => { ex += (mx - ex) * 0.18; ey += (my - ey) * 0.18; const speed = Math.hypot(mx - lx, my - ly); lx = mx; ly = my; present += ((mx < 0 ? 0 : 1) - present) * 0.07; trail.push({ x: ex, y: ey }); if (trail.length > 20) trail.shift(); if (speed > 5 && mx > 0) { const n = Math.min(3, 1 + Math.floor(speed / 12)); for (let i = 0; i < n; i++) sparks.push({ x: mx + rand(-7, 7), y: my + rand(-7, 7), vx: rand(-0.7, 0.7), vy: rand(-1, -0.2), life: 0, max: rand(46, 84), size: rand(1.1, 2.6), hue: rand(250, 320) }); } if (sparks.length > 100) sparks.splice(0, sparks.length - 100); ctx.clearRect(0, 0, w, h); const t = performance.now() / 1000; const breathe = 1 + Math.sin(t * 1.7) * 0.07; // large soft glow const R = 210 * breathe; let g = ctx.createRadialGradient(ex, ey, 0, ex, ey, R); g.addColorStop(0, `rgba(122,86,255,${0.17 * present})`); g.addColorStop(0.4, `rgba(150,112,255,${0.08 * present})`); g.addColorStop(1, "rgba(150,112,255,0)"); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(ex, ey, R, 0, 7); ctx.fill(); // inner lavender core g = ctx.createRadialGradient(ex, ey, 0, ex, ey, 95); g.addColorStop(0, `rgba(196,158,255,${0.16 * present})`); g.addColorStop(1, "rgba(196,158,255,0)"); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(ex, ey, 95, 0, 7); ctx.fill(); // trail for (let i = 0; i < trail.length; i++) { const p = trail[i], a = i / trail.length, rr = 30 * a; g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, rr); g.addColorStop(0, `rgba(140,100,255,${0.06 * a * present})`); g.addColorStop(1, "rgba(140,100,255,0)"); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(p.x, p.y, rr, 0, 7); ctx.fill(); } // bright core dot at real cursor g = ctx.createRadialGradient(mx, my, 0, mx, my, 15); g.addColorStop(0, `rgba(83,58,253,${0.5 * present})`); g.addColorStop(1, "rgba(83,58,253,0)"); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(mx, my, 15, 0, 7); ctx.fill(); // sparkles for (let i = sparks.length - 1; i >= 0; i--) { const s = sparks[i]; s.life++; s.x += s.vx; s.y += s.vy; s.vy += 0.012; const lt = s.life / s.max; if (lt >= 1) { sparks.splice(i, 1); continue; } const a = Math.sin(lt * Math.PI) * present; g = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.size * 3.4); g.addColorStop(0, `hsla(${s.hue},100%,82%,${0.85 * a})`); g.addColorStop(1, `hsla(${s.hue},100%,82%,0)`); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(s.x, s.y, s.size * 3.4, 0, 7); ctx.fill(); ctx.fillStyle = `hsla(${s.hue},100%,94%,${a})`; ctx.beginPath(); ctx.arc(s.x, s.y, s.size * 0.7, 0, 7); ctx.fill(); } raf = requestAnimationFrame(loop); }; loop(); return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); unsub(); }; }, []); return