• requestAnimationFrame()は描画ごとに次の更新を予約してアニメーションを動かす

requestAnimationFrame()は描画ごとに次の更新を予約してアニメーションを動かす

公開日: 執筆:佐藤

requestAnimationFrame()は、関数を一定間隔で繰り返す命令ではありません。一回呼ぶと、スクリプトによるアニメーションの次の更新を一回だけ予約します。動きを続ける場合は、呼び出されたコールバックの中で、次のrequestAnimationFrame()を予約します。

停止するときは、予約時に返されたhandleをcancelAnimationFrame()へ渡します。更新、次の予約、handleの保存を一つの流れとして組み立ててください。

一回の呼び出しは一回の更新を予約する

W3CのAnimation Timing仕様は、requestAnimationFrame(callback)が、コールバックとhandle(識別子)の組をアニメーション用のリストへ追加すると定めています。返されるhandleは、その予約を識別する正の整数です。

同仕様は、requestAnimationFrame()が一回の更新だけを予約することも明記しています。コールバックを一度登録しただけでは、アニメーションは自動で繰り返されません。

function update(time) {
  // timeを使って、この描画での位置を計算する

  window.requestAnimationFrame(update);
}

window.requestAnimationFrame(update);

最初の呼び出しが一回目を予約し、update()の末尾にある呼び出しが二回目を予約します。この形が続くため、描画ごとに次の更新が一つずつつながります。

コールバックへ渡される時刻から位置を計算する

コールバックは、DOMHighResTimeStampのtimeを受け取ります。更新回数を数えて位置を増やすより、開始時刻との差から、その描画での位置を計算します。

次は、要素を一秒かけて200px動かす仕組みを示す仮例です。対象要素には、leftが働く位置指定がCSSで設定されているものとします。

var requestId = 0;
var startTime = null;
var box = document.getElementById('animated-box');

function update(time) {
  if (startTime === null) {
    startTime = time;
  }

  var progress = Math.min((time - startTime) / 1000, 1);
  box.style.left = String(progress * 200) + 'px';

  if (progress < 1) {
    requestId = window.requestAnimationFrame(update);
  } else {
    requestId = 0;
  }
}

function startAnimation() {
  if (requestId !== 0) {
    return;
  }

  startTime = null;
  requestId = window.requestAnimationFrame(update);
}

一回の更新ごとに、経過時間から位置を求めます。描画の間隔が変わっても、フレーム数を経過時間だとみなさない構造です。終了条件へ達したら、次の予約を作らず、handleを0へ戻します。

cancelAnimationFrame()へ最新のhandleを渡す

cancelAnimationFrame(handle)は、先に作ったアニメーション更新の予約を取り消します。呼び出し時に一致するhandleがなければ、処理は行われません。

先ほどの仮例へ停止処理を加えるなら、次の形です。

function stopAnimation() {
  if (requestId !== 0) {
    window.cancelAnimationFrame(requestId);
  }

  requestId = 0;
  startTime = null;
}

コールバック内で次の予約を作るたびに、返された最新のhandleをrequestIdへ保存します。最初のhandleだけを残すと、すでに実行済みの予約を取り消すことになり、次に待っている更新を止められません。

cancelAnimationFrame()が取り消すのは、予約したコールバックです。すでに書き換えた要素の位置を元へ戻す処理ではありません。停止時に初期位置へ戻す必要があるなら、その処理は別に書きます。

同じcallbackを重ねて予約しない

W3C仕様によれば、処理される前に同じコールバックを複数回requestAnimationFrame()へ渡すと、リストには複数の予約が入り、その描画でコールバックも複数回呼ばれます。

開始ボタンを押すたびに新しいループを作ると、同じ要素を複数の処理が更新します。仮例のrequestId !== 0という判定は、動作中に二重の開始予約を作らないためのものです。

開始、停止、再開を実装するときは、次の状態を分けます。

  • 次の更新を予約しているか
  • アニメーションの開始時刻は何か
  • 停止後に位置を維持するか、初期化するか

handleの有無だけで、表示位置や経過時間まで自動で戻るとは考えないでください。

更新間隔はブラウザーが決める

requestAnimationFrame()は、毎秒何回の実行を保証するAPIではありません。W3C仕様は、ページがforegroundかbackgroundか、CPUの負荷などを考慮して、ブラウザーなどのuser agentが更新頻度を制御できると説明しています。

ページが表示されていない場合、アニメーションは大きく間引かれます。そのため、「一回のコールバックを一フレームとして60回数えれば一秒」とは決められません。コールバックへ渡された時刻から経過時間を計算し、非表示から戻った場合も終了条件を越えて更新し続けないようにします。

これは、backgroundでも同じ頻度でタイマー処理を続けるためのAPIではありません。画面に描く必要がある更新をuser agentへ予約するAPIです。

一つの動きで開始と停止を確認する

最初に、位置、透明度、寸法など、数値を一つだけ変える動きを作ります。開始操作を一回行い、コールバック内で次の予約が一つだけ作られることを確認します。

次に、動作中の停止、停止後の再開、開始操作の連続実行、ページを非表示にしてから戻した場合を試します。確認記録には、更新する値、開始条件、終了条件、現在のhandle、停止後の表示状態を残してください。

requestAnimationFrame()を呼べたことと、意図した時間、位置、停止条件で動いたことは別です。まず一つの要素について、開始から終了までの状態を確認してから、複数の要素や複雑な計算へ広げます。

戻る