import { useState, useRef, useEffect } from 'react'; // Frame-rate-independent easing of a scalar toward a moving target (alpha from // dt + a time constant). The rAF loop runs continuously so the value keeps // gliding between the bridge's discrete value updates — turning a stuttery // ~10–20 Hz stream into a smooth 60 fps motion. To avoid needless React work it // only re-renders the consumer while the value is actually moving: once settled, // the functional setState returns the same number and React bails (Object.is). export function useEased(target, tau = 0.08) { const [, force] = useState(0); const cur = useRef(target), tg = useRef(target); tg.current = target; useEffect(() => { let raf, last = 0; const loop = (now) => { const dt = last ? Math.min(0.05, (now - last) / 1000) : 0.016; last = now; const k = 1 - Math.exp(-dt / tau); const next = cur.current + (tg.current - cur.current) * k; const settled = Math.abs(tg.current - next) < 0.02; const val = settled ? tg.current : next; if (val !== cur.current) { cur.current = val; force((n) => (n + 1) & 0xffff); } raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); return () => cancelAnimationFrame(raf); }, [tau]); return cur.current; } // As above but eases along the shortest arc across the 0↔360 wrap (headings). export function useEasedAngle(target, tau = 0.08) { const [, force] = useState(0); const cur = useRef(target), tg = useRef(target); tg.current = target; useEffect(() => { let raf, last = 0; const loop = (now) => { const dt = last ? Math.min(0.05, (now - last) / 1000) : 0.016; last = now; const k = 1 - Math.exp(-dt / tau); const d = ((tg.current - cur.current + 540) % 360) - 180; // shortest signed arc const next = Math.abs(d) < 0.05 ? tg.current : cur.current + d * k; const val = ((next % 360) + 360) % 360; if (val !== cur.current) { cur.current = val; force((n) => (n + 1) & 0xffff); } raf = requestAnimationFrame(loop); }; raf = requestAnimationFrame(loop); return () => cancelAnimationFrame(raf); }, [tau]); return cur.current; }