Fit-to-screen tweens to the default view over 300ms via animateView (eased, lerpView-driven) and snaps instantly when prefers-reduced-motion is set (US-PAN-004 AC2, NFR-A11Y-003). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
39 lines
1.3 KiB
TypeScript
39 lines
1.3 KiB
TypeScript
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);
|
|
}
|