import { lerpView, type PanZoomState } from '$lib/person/genealogy/panZoom'; /** Fit / recentre animation duration (US-PAN-004 AC2: ≤ 300 ms). */ export const VIEW_ANIM_MS = 300; const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3); /** Snapshot of the user's reduced-motion preference (non-reactive, browser-only). */ export function prefersReducedMotion(): boolean { return typeof window !== 'undefined' && typeof window.matchMedia === 'function' ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false; } /** * Tween the view from `from` to `to`, calling `onFrame` with each interpolated * state. Honors reduced motion by snapping straight to the target (NFR-A11Y-003, * REQ-PAN-005). Returns a cancel function. */ export function animateView( from: PanZoomState, to: PanZoomState, onFrame: (state: PanZoomState) => void, opts: { reducedMotion?: boolean; durationMs?: number } = {} ): () => void { if (opts.reducedMotion) { onFrame(to); return () => {}; } const duration = opts.durationMs ?? VIEW_ANIM_MS; const start = performance.now(); let raf = requestAnimationFrame(function step(now: number) { const t = Math.min(1, (now - start) / duration); onFrame(lerpView(from, to, easeOutCubic(t))); if (t < 1) raf = requestAnimationFrame(step); }); return () => cancelAnimationFrame(raf); }