Files
familienarchiv/frontend/src/lib/person/genealogy/animateView.ts
Marcel 396c87f8ab feat(stammbaum): animate fit-to-screen, snap under reduced motion (#692)
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>
2026-05-29 16:54:34 +02:00

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);
}