springs.ts
examples/fxedit/springs.ts · raw · open the example · all examples · site home
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
// Springs for everything that moves in the editor, and the one animation
// loop, which sleeps once everything has settled.
export class Spring {
value: number;
target: number;
velocity = 0;
// Settled when this close to the target and this slow (units of value)
readonly epsilon: number;
constructor(value: number, epsilon = 0.05) {
this.value = value;
this.target = value;
this.epsilon = epsilon;
}
// Semi-implicit Euler in fixed substeps, stable for stiff springs; true
// while still moving
step(dt: number, stiffness: number, damping: number): boolean {
if (this.value === this.target && this.velocity === 0) {
return false;
}
const c = 2 * Math.sqrt(stiffness) * damping;
for (let t = dt; t > 0; t -= 1 / 240) {
const h = Math.min(t, 1 / 240);
this.velocity += (stiffness * (this.target - this.value) - c * this.velocity) * h;
this.value += this.velocity * h;
}
if (Math.abs(this.target - this.value) < this.epsilon && Math.abs(this.velocity) < this.epsilon * 10) {
this.snap();
return false;
}
return true;
}
snap(value = this.target) {
this.value = value;
this.target = value;
this.velocity = 0;
}
}
// Calls every ticker each animation frame while any of them reports it is
// still moving; wake() after changing a target
export class Animator {
private tickers: ((dt: number) => boolean)[] = [];
private frame = 0;
private last = 0;
add(ticker: (dt: number) => boolean) {
this.tickers.push(ticker);
this.wake();
}
wake() {
if (this.frame === 0) {
this.last = performance.now();
this.frame = requestAnimationFrame((now) => this.loop(now));
}
}
private loop(now: number) {
const dt = Math.min(0.05, Math.max(0, (now - this.last) / 1000));
this.last = now;
let active = false;
for (const ticker of this.tickers) {
active = ticker(dt) || active;
}
this.frame = active ? requestAnimationFrame((time) => this.loop(time)) : 0;
}
}