[requestAnimationFrame]ループするときのfpsの求め方とfpsに依存しないアニメーション


## 初めに
はじめまして。

requestAnimationFrameでループを作るときの、fpsの求め方とfpsに依存しない処理の仕方を記します。

あくまでもオリジナルです。ベストなのかは知りません。
この記事ではrequestAnimationFrameを**rAF**と略します。長いからね。

## fpsについて

### fpsとは
fpsはframes per secondの略で、1秒あたりのフレーム数です。
$$\frac{フレーム}{秒}$$
と表せます。

### fpsの求め方
**rAF**が、1秒で何回呼び出されるのかを数えても求められますが、今回は数学的なアプローチで求めます。

$$\frac{フレーム}{秒} = 1 \div \frac{秒}{フレーム}$$
とします。逆数からfpsを求めています。fpsの逆数
$$\frac{秒}{フレーム}$$
はunity等ではdeltaTimeと呼ばれるもので、1フレームあたりの秒数です。

ということでdeltaTimeを求めて逆数にすれば、fpsが求められます。

## 実際に求める
### deltaTimeを求める
1フレームあたりの秒数ということで、前回の**rAF**の呼び出しとの時間差です。
ループ全体のコードを示します。

```javascript:loop.js
const loop = (func) => {

  let then = 0;

  const _loop = (timeStamp) => {

    id = requestAnimationFrame(_loop);
    const now = timeStamp * 0.001;
    if (then === 0) {

      then = now;
      return;
    }
    const delta = now - then;

    try {

      func(delta);
    } catch (error) {

      cancelAnimationFrame(id);
      throw new Error('In animationLoop, ' + error);
    }

    then = now;
  };
  let id = requestAnimationFrame(_loop);

  return () => cancelAnimationFrame(id);
};
```

色々と情報量が多いですが一つずつ。

**rAF**のコールバックに渡される引数(timeStamp)は、[windowが読み込まれた時刻](https://developer.mozilla.org/ja/docs/Web/API/DOMHighResTimeStamp#The_time_origin)からのミリ秒で、0.001を掛けて秒にします。(変数now)
一回目をスキップするのは、計算に前回の時刻が必要だからです。

現在の時刻から前回の時刻を引いて、1フレームの時間であるdeltaTimeを求めます。

ループ内でエラーが出たらキャンセルします。(これが無いと、毎秒60個のエラーが発生します。)

次回の計算のために現在時刻を保存します。(これが前回の時刻として次回に使用されます)(変数then)

loop関数の返り値は、停止をする関数です。

### fpsを求める

```javascript
const stop = loop((delta) => {

  const fps = 1 / delta;
  console.log(Math.round(fps));
});
```

ご覧の通り数式そのものでfpsが求められました。適当に丸めて表示しています。

## fpsに依存しないように
**rAF**は環境によって呼び出されるタイミングが異なります。60fpsを前提とした(fpsに依存した)実装では、意図した通りに動作しない場合があります。

```html
<canvas></canvas>
```

```javascript
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
```
### 等速直線運動
fpsに依存しないためには、1フレームでどのくらい進むのかを計算する必要があります。そこでdeltaTimeを使います。

```javascript
const velocity = 100;

let x = 0;
let y = canvas.height / 2;

const stop = loop((delta) => {

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  x += velocity * delta;

  if (x >= canvas.width) x -= canvas.width;
 
  ctx.beginPath();
  ctx.arc(x, y, 20, 0, 2 * Math.PI);
  ctx.fill();
});
```

こんな感じです。
velocityは秒速です。
$$\frac{ピクセル}{秒}$$
このように、一秒に100ピクセル進みます。
300を超えると300を引くことで、左端に戻るようにしています。

数式にするとイメージしやすいと思います。
xの単位をピクセルとして

$$\frac{ピクセル}{フレーム} = \frac{ピクセル}{秒} \times \frac{秒}{フレーム}$$

ということで、秒速(一秒で何ピクセル進むか)にdeltaTime(1フレームで何秒かかるか)を掛けると1フレームで何ピクセル進むかが求められます。これを座標に足して行くことでfpsに依存せずアニメーションできます。

### 等加速度直線運動
徐々に加速するときはこのようにします。

```javascript
const acceleration = 50;
let velocity = 100;

let x = 0;
let y = canvas.height / 2;

const stop = loop((delta) => {

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  velocity += acceleration * delta; 
  x += velocity * delta;

  if (x >= canvas.width) x -= canvas.width;

  ctx.beginPath();
  ctx.arc(x, y, 20, 0, 2 * Math.PI);
  ctx.fill();
});
```

加速度(一秒間にどのくらい加速するか)にdeltaTime(1フレームに何秒かかるか)を掛けることで、1フレームでどのくらい加速するかを求め、速度に足します。(velocityが定数から変数になったことに注意)

$$\frac{速度}{フレーム} = \frac{速度}{秒} \times \frac{秒}{フレーム}$$

更に、先の等速直線運動と同じように計算することで、等加速度直線運動を実現します。

$$\frac{ピクセル}{フレーム} = \frac{ピクセル}{秒} \times \frac{秒}{フレーム}$$

## 終わりに
駆け足感のある記事になってしまいました。
初投稿なので間違いがあるかもしれません。もし問題がありましたら連絡ください。