File size: 2,309 Bytes
020d2dc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
// Fixed-timestep game loop with render interpolation.
//
// Logic advances in exact 1/60s steps so behaviour is frame-rate independent and
// reproducible. Rendering happens once per animation frame and interpolates between
// the previous and current logic states using `alpha`, so motion stays smooth even
// when the display refresh and the logic step do not line up.

export function createLoop({ step = 1 / 60, update, render, maxFrameTime = 0.25, maxSteps = 8 }) {
  let accumulator = 0;
  let previousTime = 0;
  let rafId = 0;
  let running = false;

  // Debug readout. Owned here because this is the only place that sees real frame times.
  const stats = { fps: 0, steps: 0, frames: 0, panics: 0 };
  let fpsElapsed = 0;
  let fpsFrames = 0;

  function tick(now) {
    // A frame can already be queued when stop() runs, so cancelling the handle
    // is not enough on its own — check the flag before doing any work or
    // scheduling the next one.
    if (!running) return;
    rafId = requestAnimationFrame(tick);

    let frameTime = (now - previousTime) / 1000;
    previousTime = now;
    if (!Number.isFinite(frameTime) || frameTime < 0) frameTime = 0;

    // A tab that was backgrounded returns a huge delta. Clamp it rather than
    // simulating thousands of steps at once.
    if (frameTime > maxFrameTime) frameTime = maxFrameTime;

    accumulator += frameTime;

    let steps = 0;
    while (accumulator >= step && steps < maxSteps) {
      update(step);
      accumulator -= step;
      steps++;
      stats.steps++;
    }

    // If we hit the step ceiling we are running slower than real time. Drop the
    // backlog instead of spiralling.
    if (steps === maxSteps && accumulator >= step) {
      accumulator = 0;
      stats.panics++;
    }

    render(accumulator / step, frameTime);

    stats.frames++;
    fpsElapsed += frameTime;
    fpsFrames++;
    if (fpsElapsed >= 0.5) {
      stats.fps = Math.round(fpsFrames / fpsElapsed);
      fpsElapsed = 0;
      fpsFrames = 0;
    }
  }

  return {
    stats,
    start() {
      if (running) return;
      running = true;
      previousTime = performance.now();
      accumulator = 0;
      rafId = requestAnimationFrame(tick);
    },
    stop() {
      running = false;
      cancelAnimationFrame(rafId);
    },
  };
}