Files
portfolio/lib/momentum.ts
T

71 lines
1.9 KiB
TypeScript

export const THROW_MIN_DISTANCE = 14;
export const THROW_GRACE_MS = 24;
export const THROW_STALE_MS = 90;
export function freshness(idleMs: number): number {
if (idleMs <= THROW_GRACE_MS) return 1;
return Math.max(0, 1 - (idleMs - THROW_GRACE_MS) / (THROW_STALE_MS - THROW_GRACE_MS));
}
export type Vec = { x: number; y: number };
export function createVelocityTracker() {
let last: { x: number; y: number; t: number } | null = null;
let vx = 0;
let vy = 0;
return {
reset(x: number, y: number) {
last = { x, y, t: performance.now() };
vx = 0;
vy = 0;
},
sample(x: number, y: number) {
const now = performance.now();
if (last) {
const dt = now - last.t;
if (dt > 0) {
vx = vx * 0.7 + ((x - last.x) / dt) * 0.3;
vy = vy * 0.7 + ((y - last.y) / dt) * 0.3;
}
}
last = { x, y, t: now };
},
release(): Vec {
const idle = last ? performance.now() - last.t : Infinity;
const f = freshness(idle);
return { x: vx * f, y: vy * f };
},
};
}
export function runInertia(
velocity: Vec,
onStep: (dx: number, dy: number) => void,
opts: { minSpeed?: number; stopSpeed?: number; decayPerFrame?: number } = {},
): () => void {
const minSpeed = opts.minSpeed ?? 0.04;
const stopSpeed = opts.stopSpeed ?? 0.012;
const decayPerFrame = opts.decayPerFrame ?? 0.94;
let { x: vx, y: vy } = velocity;
if (Math.hypot(vx, vy) < minSpeed) return () => { /* nothing started */ };
let raf = 0;
let last = performance.now();
const tick = (now: number) => {
const dt = Math.min(40, now - last);
last = now;
onStep(vx * dt, vy * dt);
const decay = Math.pow(decayPerFrame, dt / 16);
vx *= decay;
vy *= decay;
if (Math.hypot(vx, vy) > stopSpeed) {
raf = requestAnimationFrame(tick);
} else {
raf = 0;
}
};
raf = requestAnimationFrame(tick);
return () => { if (raf) cancelAnimationFrame(raf); };
}