springs.ts

examples/fxedit/springs.ts · raw · open the example · all examples · site home

Exports: SpringAnimator

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;
  }
}