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

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

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

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

以下が requestAnimationFrame を使い改善を行ったコードになります。

var lastTimeStamp = performance.now();

function tick(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;
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;
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 を実装している最新のブラウザでしか利用できません。

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

WMClock の提供する機能

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

  • 描画の再現テストに必要な、ある地点を基準とした相対時間の設定と取得
    • options.baseTime
    • WMClock#setTime
    • WMClock#getTime
    • WMClock#now
  • ペースメイクされた理想的な経過時間の配給
    • options.pulse
  • 低クロックの配給
    • options.speed
  • ブラウザの描画サイクルと合致した低コストなクロックの配給
    • options.vsync
  • visibility change event への対応
    • options.suspend
Clone this wiki locally