requestAnimationFrame でフレームと再描画更新を制御する

Posted :

Canvas や SVG などを用いて、アニメーションを行う場合の描画繰り返しについて考えます。

描画の繰り返しアニメーションには requestAnimationFrame や setTimeout など利用し、そのタイミングで再描画を繰り返すという手法が一般的です。

これまで、 setTimeout を利用することが主流で、60 FPS のアニメーションの場合は例えば以下の様なコードで実現することも多かったかと思います。

var x = 0;
( function loop(){
    setTimeout( loop, 1000 / 60 );
    x += 1;
    console.log( x );
} )();

ただし、setTimeout や setInterval は、ブラウザー側で再描画の準備が整っているか否かにかかわらず、必ず実行されてしまいます。また、ブラウザーのタブが非表示 (バックグラウンド) の場合でも常に実行し続けます。

一方で、requestAnimationFrame はブラウザーの負荷に合わせては 60 FPS 以内で再描画の準備が整ったタイミングで実行され、また、ブラウザーのタブが非表示 (バックグラウンド) にある場合は、発火頻度が自動で低下します。これにより、メモリーの消費を抑えることができます。

こういった理由で requestAnimationFrame はタイマーメソッドより、アニメーションの表現に向いていると言えます。一方で、正確な FPS を制御することはできません。そのため、次のようなコードは、アニメーションの精度はあまり良くないといえます。

var x = 0;
( function loop(){
    requestAnimationFrame( loop );
    x += 1;
    console.log( x );
} )();

上記のコードは、

  • 任意の FPS で繰り返されないこと (例えば、このままでは 24 FPS で繰り返すことはできません)
  • FPS にばらつきがあること ( 負荷が軽い場合には 62 FPS から 58 FPS で不正確に繰り返される)

という問題点があります。

次の demo で FPS のばらつきを確認することができます。IE10 以降あるはその他のブラウザーで動作します。(同様に setTimeout などのタイマーメソッドも正確ではありませんが。)

demo

ですので、requestAnimationFrame のタイミングにたよって値を変えるするというのではなく、経過時間を管理し再描画のタイミングで「経過時間に合わせたフレーム」を表示してやればよさそうです。

時間とフレームの対応例として 6 FPS で 6 コマの繰り返しの demo を用意しました。

時間は、performance.now() や Date.getTime() などを利用し管理することができます。時間取得のための関数は以下のように用意することができます。

ちなみに performance.now() は Date.getTime() や Date.now() よりも詳細な時間を取得することができます。

var now = window.performance && (
    performance.now || 
    performance.mozNow || 
    performance.msNow || 
    performance.oNow || 
    performance.webkitNow );

var getTime = function() {
    return ( now && now.call( performance ) ) || ( new Date().getTime() );
}

そして、開始時間を一度だけ保存しておきます。

var startTime = getTime();

requestAnimationFrame のタイミングで経過時間を知るには、以下で可能です。

( function loop(){
    requestAnimationFrame( loop );
    var lastTime = getTime();
    console.log( startTime - lastTime );
} )();

上記の方法を利用し、経過時間から対応するフレームを求めるなら

var fps = 30;
var frameLength = 6;
( function loop(){
    requestAnimationFrame( loop );
    var frame = Math.floor( ( getTime() - startTime ) / ( 1000 / fps ) % frameLength );
    console.log( frame );
} )();

のような方法で可能です。

最終的に、ポリーフィルを含めて次のコードで「経過時間に合わせたフレームの表示」が可能です。requestAnimationFrame に FPS が追いつかない場合は、コマ落ちすることになりますが、他のアニメーションやタイムラインと連携する場合にはこの方法がいいのかなと思ってます。

var requestAnimationFrame = ( function(){
    return  window.requestAnimationFrame       || 
            window.webkitRequestAnimationFrame || 
            window.mozRequestAnimationFrame    || 
            window.oRequestAnimationFrame      || 
            window.msRequestAnimationFrame     || 
            function( callback ){
                window.setTimeout( callback, 1000.0 / 60.0 );
            };
} )();

var now = window.performance && (
    performance.now || 
    performance.mozNow || 
    performance.msNow || 
    performance.oNow || 
    performance.webkitNow );

var getTime = function() {
    return ( now && now.call( performance ) ) || ( new Date().getTime() );
}

var startTime = getTime();
var fps = 30.0;
var frameLength = 6.0;

( function loop(){
    requestAnimationFrame( loop );
    var lastTime = getTime();
    var frame = Math.floor( ( lastTime - startTime ) / ( 1000.0 / fps ) % frameLength );
    console.log( frame );
} )();

上記のコードはこの demo で確認できます。

あとは適当に使うかもしれなさそうなのを試してみました。