Skip to content
uupaa edited this page Dec 20, 2014 · 17 revisions

WMClock.js は、タイムラインアニメーションに最適なクロックと、描画テストに必須となる仕組みを提供します。

より良いタイムラインアニメーションと解決すべき課題

タイムラインアニメーションは、ゼロから始まる時間軸と、軸上にマップされたイベントから構成されています。

0ms   200ms  400ms  600ms  800ms 1000ms 1200ms 1400ms 1600ms 1800ms
+------+------+------+------+------+------+------+------+------+
|      |      |      |      |      |      |      |      |      |
+------+------+------+------+------+------+------+------+------+

         ^
         スプライトA を表示する

              ^
              スプライトB を表示する

                  ^
                  スプライトA を移動させる

潤沢なCPU/GPU性能があれば、これらの全てのイベントを正確に再生することも可能ですが、 モバイルブラウザにおいては、それはまだまだ夢の様な話になります。

また、レンダリングのテストを全て人の手で行うという考え方は、QA/テスト工学的にナンセンスです。
日々増え続ける iOS, Android デバイスに対応しきれず、いずれテスト工数が爆発し、テスト体制が崩壊してしまうでしょう。

低スペックなデバイスでも無駄なくアニメーションをドライブし、 かつ、可能な限り自動テストが可能なようにするためには、 ブラウザとJavaScriptが抱える以下の課題を全てクリアする必要があります。

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

以下は、よくあるアニメーションを再生するコードです。

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回以上画面を更新しようとしてしまいます。

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

この課題を解決するための API が 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() の省略も同時に行っており、さらなる負荷の軽減も同時に施しています。

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

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

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

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

var pulse = 100; // 100ms 毎にパルスを発生させる
var lastTimeStamp = -1;

function tick() {
    requestAnimationFrame(tick);

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

    if (lastTimeStamp < 0) {
        timeStamp = 0;
        deltaTime = pulse;
    } else {
        timeStamp = pulse + lastTimeStamp;
        deltaTime = pulse;
    }
    lastTimeStamp = timeStamp;

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

また、60fpsで再描画される画面を目視で確認することは中々できません。 ゆっくりとアニメーションさせつつ描画を確認できるようにするには以下のようなコードも必要でしょう。

var speed = 1000; // コールバックの速度
var pulse = 100;  // 100ms 毎にパルスを発生させる
var lastTimeStamp = -1;

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

    if (lastTimeStamp < 0) {
        timeStamp = 0;
        deltaTime = pulse;
    } else {
        timeStamp = pulse + lastTimeStamp;
        deltaTime = pulse;
    }
    lastTimeStamp = timeStamp;

    render(timeStamp, deltaTime);
}
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);
}

上記のような理想的でシンプルなコードは visibility change event を実装している最新のブラウザでしか利用できません。

おそらくは PageVisibilityEvent.js のような polyfill を含むライブラリを利用する必要があるでしょう。

課題4. 連携ミスによるフレーム遅延

動的なアニメーションを生成するシステムは、恐らく幾つかのサブシステムから構成されているはずです。

しかし、以下のようなバラバラのクロックで駆動する(連携がとれていない)サブシステムでは、 恐らく2〜3フレームもの遅延が発生してしまっているはずです。

  • ユーザの入力をモデルに反映させるサブシステムA
  • setIntetval で駆動し、モデルの情報変更をレンダリングに反映される準備を行うサブシステムB
  • サブシステムB から呼ばれ、setTimeout で動作し関連情報を収集するサブシステムC
  • requestAnimationFrame で駆動するサブシステムC(レンダラー)

フレーム遅延を避けようとするならば、足並みを揃える、マスタークロックを供給する仕組みが必要でしょう。

WMClock の提供する機能

WMClock.js はこれらの課題を解決する機能を提供します。