71 lines
1.9 KiB
TypeScript
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); };
|
|
}
|