~james/core

core/web/src/lib/motion/value.svelte.ts -rw-r--r-- 2.3 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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);
	}
}