Skip to content
uupaa edited this page Apr 23, 2015 · 10 revisions

Clock.js は、タイムラインアニメーションに適したクロックを供給するライブラリです。

タイムラインアニメーションと解決すべき幾つかの課題

タイムラインアニメーションは、ゼロから始まる時間軸と軸上にマップされたイベントで構成されています。 これらのイベントを時間どおりにドライブするには、正確なクロックが必要になります。

0   200  400  600  800 1000 1200 1400 1600 1800 (ms)
+----+----+----+----+----+----+----+----+----+----->
       ^    ^    ^
       1    2    3

(1) スプライトA を表示
(2) スプライトB を表示
(3) スプライトA を移動

よくあるアニメーション再生コードが抱える課題

以下は、ありがちなアニメーション再生コードです。

var lastTimeStamp = Date.now();

function tick() {
    setTimeout(tick, 1000 / 60);

    var timeStamp = Date.now();
    var deltaTime = timeStamp - lastTimeStamp; // 前回の呼び出しからの経過時間

    render(timeStamp, deltaTime);
}
setTimeout(tick, 1000 / 60);

このコードは幾つかの課題を抱えています。

課題1. setTimout による無駄な負荷

setTimeout(tick, 1000 / 60) は16.666ms毎にコールバックが発生し、1秒間に60回画面を更新できそうに見えますが、実際の動作はかなり異なります。
実際に試した事がある方は、このようなコードでは安定して60fpsを出せない事をご存知でしょう。

簡単に改善しようと setTimeout(tick, 1000 / 70) としたとしても、こんどは一秒間に60回以上画面を更新しようとしてしまいます。

ブラウザは独自のリフレッシュレートとタイミングで画面を更新します。過剰な再描画命令は実際には画面に反映されず、ただただ無駄な負荷となります。
滑らかなアニメーションを実現するには、ブラウザに余計な負荷をかけずに、リフレッシュレートに合わせて再描画を行う工夫が必要になります。

この課題を解決するには requestAnimationFrame と performance.now を使います。
requestAnimationFrame はブラウザのリフレッシュタイミングでコールバックを発生させるため、無駄な描画負荷を無くす事ができます。

requestAnimationFrame で改善したコード

var lastTimeStamp = performance.now();

function tick(timeStamp) { // requestAnimationFrame は timeStamp を引数に渡す
    requestAnimationFrame(tick);

    var deltaTime = timeStamp - lastTimeStamp; // 前回の呼び出しからの経過時間

    render(timeStamp, deltaTime);
}
requestAnimationFrame(tick);

このコードでは Date.now() の省略も同時に行っており、さらなる負荷の軽減も同時に施しています。
requestAnimationFrame and Date.now in WebKit も参照してください。

課題2. 不整脈とテスタビリティ

setTimeout の代わりに requestAnimationFrame を使ったコードにも、アニメーションの描画テストができないという課題が残っています。

描画テストは安定したクロックの供給を必要としますが、 requestAnimationFrame は負荷に応じてコールバックタイミングが異なるため、現在時刻(timeStamp)と経過時間(deltaTime)が安定せず、 そのままではアニメーションのテスト結果が毎回異なってしまいます。

この課題を解決するのが以下のコードです。
timeStamp と deltaTime を 100ms 刻みの理想的な数値に整形しています。

var init = false; // 初期化済みで true
var pulse = 100;  // 100ms 毎にパルスを発生させる
var lastTimeStamp = -1;

function tick() {
    requestAnimationFrame(tick);

    var timeStamp = 0; // 現在時刻(相対時間)
    var deltaTime = 0; // 前回の呼び出しからの経過時間

    if (!init) {
        init      = true;
        timeStamp = 0;
        deltaTime = pulse;
    } else {
        timeStamp = pulse + lastTimeStamp; // 正確に 100ms づつ増えるようにする
        deltaTime = pulse;
    }
    lastTimeStamp = timeStamp; // 更新

    render(timeStamp, deltaTime);
}
requestAnimationFrame(tick);

また、60fpsで再描画される画面を目視で確認することは中々できません。 ゆっくりとアニメーションさせつつ描画を確認できるようにするには、requestAnimationFrame の代わりに setInterval を使い、ゆっくりと動作するデバッグ用のモードも必要でしょう。

var init = false; // 初期化済みで true
var speed = 1000; // コールバックの速度
var pulse = 100;  // 100ms 毎にパルスを発生させる
var lastTimeStamp = -1;

function tick() {
    var timeStamp = 0; // 現在時刻(相対時間)
    var deltaTime = 0; // 前回の呼び出しからの経過時間

    if (!init) {
        init      = true;
        timeStamp = 0;
        deltaTime = pulse;
    } else {
        timeStamp = pulse + lastTimeStamp; // 正確に 100ms づつ増えるようにする
        deltaTime = pulse;
    }
    lastTimeStamp = timeStamp;

    render(timeStamp, deltaTime);
}
// requestAnimationFrame(tick);
setInterval(tick, speed);

課題3. visibility change event への対応

タブが inactive (background) になった場合はクロックの供給をストップし、アニメーションや音楽を停止するのが望ましい場合もあるでしょう。

visibility change event のハンドリングは以下のようにします。

document.addEventListener("visibilitychange", handlePageVisibility);

function handlePageVisibility(event) {
    var hidden = document.hidden;

    callback(hidden);
}

課題4. 複数のサブフレームワーク間の連携ミスによるフレーム遅延

ゲームエンジンのようなアニメーションを多用するプログラムは、役割ごとに分割された複数のサブシステムで構成されています。

  • サブシステムA
    • setInterval(, 1000/60) で駆動しユーザの入力を読み取る
  • サブシステムB
    • setTimeout(, 1000/60) で駆動しモデルの情報の変更を元にレンダリングのための準備を行う
  • サブシステムC
    • requestAnimationFrame で画面の再描画を行う

上記のように、それぞれが勝手なタイミングで動作しているとしたら、一体どうなるのでしょう?

連携がとれていないサブシステムでは、滑らかなアニメーションは望めません。
フレームスキップが頻繁に発生してしまいます。

Clock の提供する機能

Clock.js が 課題1,2,4を解決するコードを提供し、PageVisibilityEvent.js が課題3を解決するコードを提供します。