export interface SpringConfig {
stiffness: number;
damping: number;
mass: number;
}
export const CELL: SpringConfig = { stiffness: 2080, damping: 68, mass: 0.45 };
export const HOME: SpringConfig = { stiffness: 600, damping: 54, mass: 1 };
export const VEIL: SpringConfig = { stiffness: 1040, damping: 68, mass: 0.8 };
const SAMPLE = 0.005;
const solve = (config: SpringConfig, x0: number, v0: number): ((t: number) => number) => {
const omega = Math.sqrt(config.stiffness / config.mass);
const zeta = config.damping / (2 * Math.sqrt(config.stiffness * config.mass));
if (Math.abs(zeta - 1) < 1e-6)
return (t) => Math.exp(-omega * t) * (x0 + (v0 + omega * x0) * t);
if (zeta < 1) {
const wd = omega * Math.sqrt(1 - zeta * zeta);
return (t) =>
Math.exp(-zeta * omega * t) *
(x0 * Math.cos(wd * t) + ((v0 + zeta * omega * x0) / wd) * Math.sin(wd * t));
}
const spread = omega * Math.sqrt(zeta * zeta - 1);
const fast = -zeta * omega + spread;
const slow = -zeta * omega - spread;
const a = (v0 - slow * x0) / (fast - slow);
const b = x0 - a;
return (t) => a * Math.exp(fast * t) + b * Math.exp(slow * t);
};
export class Motion {
current = $state(0);
#velocity = 0;
#frame: number | null = null;
constructor(initial: number) {
this.current = initial;
}
stop(): void {
if (this.#frame === null) return;
cancelAnimationFrame(this.#frame);
this.#frame = null;
}
set(value: number): void {
this.stop();
this.#velocity = 0;
this.current = value;
}
animate(target: number, config: SpringConfig, done?: () => void): void {
this.stop();
const x0 = this.current - target;
if (x0 === 0 && this.#velocity === 0) {
done?.();
return;
}
const at = solve(config, x0, this.#velocity);
const restDelta = Math.max(Math.abs(x0), 1e-3) * 1e-3;
const restSpeed = restDelta * 30;
const start = performance.now();
const step = (now: number): void => {
const t = (now - start) / 1000;
const x = at(t);
const velocity = (at(t + SAMPLE) - x) / SAMPLE;
if (Math.abs(x) < restDelta && Math.abs(velocity) < restSpeed) {
this.#frame = null;
this.#velocity = 0;
this.current = target;
done?.();
return;
}
this.#velocity = velocity;
this.current = target + x;
this.#frame = requestAnimationFrame(step);
};
this.#frame = requestAnimationFrame(step);
}
}