Studio Happyvalley

一定間隔で
イベントを
正確に
発生させる
スクリプト

現在時刻を元に、
正確に一定間隔のイベントを
発生するスクリプト

image: 一定間隔でイベントを正確に発生させるスクリプト
Toplabo
Generate Events At Regular Intervals

RequestAnimationFrameは現在時刻を元に実行する正確なJavascript関数。

 | 2023/06/19

開発
動機

マウストレーサー開発時に発見
  • マウスの軌跡をトレースするのに正確にかつ数多くリズムを刻む関数が必要だった

    以前、mouseTracerを開発しているときに正確に等間隔のイベントを発生させる関数を探していました。
    setIntervalやsetTimeoutは解説にも間隔が不正確と記載されていますので当初から候補にはありません。
    その時に見つけたのがRequestAnimationFrameです。
    ちなみに、requestAnimationFrameはES6やES7などのECMAScriptのバージョンとは直接関係なく、HTML5の仕様としてブラウザによって実装されたもののようです。
    HTML5仕様書:https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#dom-animationframeprovider-requestanimationframe

概要

RequestAnimationFrameの概要
  • パラメータが現在時刻を取得する仕様なので正確

    Request Animation Frameは仕様上から考えて非常に正確にリズムを刻むと予想できます。
    理由としてはコールバック関数が現在時刻をパラメータとして受け取る仕様だからです。
    メソッドを入れ子にし、そのパラメータの差分を取得することでミリ秒単位で時間を計測できます。
    他の関数たとえば、setTimeoutやsetIntervalなどの関数を使用する場合は別途date関数などと併用する必要があります。
    以下はMDNによるRequest Animation Frameの仕様説明です。

    ※Request Animation Frameコールバック:次の再描画でアニメーションを更新する時に呼び出す関数を指定します。コールバック関数は 1 個の引数 DOMHighResTimeStamp を受け取ります。この引数は、requestAnimationFrame がコールバックの呼び出しを開始した時点の時刻、すなわち performance.now() から返された時刻を示します。
    ※performance.now() 現在時刻を1msに丸めずマイクロ秒までの精度を持った浮動小数点の値で時刻を表します。

調査
探求

RequestAnimationFrameの仕様を調べます。
  • デフォルトでは回数を設定できないためイベントの間隔を調べます

    Request Animation Frameは回数を設定することはできません。
    そのため、最初に1秒間に何回イベントが発生するのかを調べます。

    調べる方法は、コールバック関数が現在時刻を取得する仕様を利用し、2秒間で何回イベントが発生するか調べます。
    (1秒でもいいのですが、統計的にはより分母が多い方が正確なので。もちろん10秒でもいいのですが)
    具体的には、直前のタイムスタンプと現在時刻の差分が指定時間を超えると停止するように作成します。
    その間に何回イベントが発生するか計測すればいいことになります。
    またコンソールログにイベントの時刻を記録し随時書き出します。
    これによりイベントの間隔が何ミリセコンドがわかります。

    /* 説明が重複しますが、コールバックメソッドには、1 個の引数 DOMHighResTimeStamp が渡されます。
    これは現在の時刻を(time originからの経過ミリ秒数で)示します。
    つまり、下記プログラムのtimestampには現在時刻が格納されます。

    ベースコードはMDN|Window.requestAnimationFrame()::https://developer.mozilla.org/ja/docs/Web/API/window/requestAnimationFrame

    const raf = document.getElementById('some-element-you-want-to-animate');
    let start, previousTimeStamp;
    let done = false
    let rafcount= 0;
    
    function step(timestamp) {
    	/* コールバックメソッドには、1 個の引数 DOMHighResTimeStamp が渡されます。これは現在の時刻を(time originからの経過ミリ秒数で)示します。 */
    		console.log(`
    raf: ${rafcount}
    timestamp: ${timestamp}
    		`);
    	if (start === undefined) {
    		start = timestamp;
    	}
    	const elapsed = timestamp - start;
    	if (previousTimeStamp !== timestamp) {
    		// ここで Math.min() を使用して、要素がちょうど 200px で止まるようにします。
    		const count = Math.min(0.1 * elapsed, 200);
    		raf.style.transform = `translateX(${count}px)`;
    		if (count === 200) done = true;
    	}
    	if (elapsed < 2000) { // Stop the animation after 2 seconds
    		rafcount++;
    		previousTimeStamp = timestamp;
    		if (!done) {
    			window.requestAnimationFrame(step);raf.textContent= rafcount;
    		}
    	}else{
    		raf.textContent= rafcount;
    	}
    }
    
    window.requestAnimationFrame(step);

結果

検証結果
  • コンソールログの結果。

    Request Animation Frameのイベントの間隔はおおよそ16msといえそうです。

    count: 0
    timestamp: 83.38

    count: 1
    timestamp: 100.06

    count: 2
    timestamp: 116.74

    count: 3
    timestamp: 133.42

    count: 4
    timestamp: 150.1

    count: 5
    timestamp: 166.8

    2秒間のイベント数は120、なので1秒間に60回

    2秒間で停止するスクリプトのイベント数は120です。なので1秒に60回の間隔でイベント発生といえます。

    requestAnimationFrameでの2秒間に最大何回イベントが発生するかの検証:https://labo.studio-happyvalley.com/RequestAnimationFrame/raf/

補足

比較用にsetIntervalも調べます
  • setIntervalのイベント発生間隔

    date関数と組み合わせてSetIntervalもどのくらいの間隔でイベントが発生するか調べます。
    基本は前と同様に2秒間に何回イベントが発生するか検証しますが、setIntervalは回数を設定できるので、requestAnimationFrameにあわせて60回にします。
    コンソールログには同様にイベント間隔の時刻と回数を記録します。
    setIntervalでの2秒間に最大何回イベントが発生するかの検証:https://labo.studio-happyvalley.com/RequestAnimationFrame/sto/

    const raf = document.getElementById('some-element-you-want-to-animate');
    const start = Date.now();
    let count= 0;
    const timer= setInterval(() => {
      count++;
      const s = Math.floor((Date.now() - start)*1000/ 1000);
    	raf.textContent= count;
    	if(2000 >= s){
    		raf.style.transform += `translateX(1px)`;
    		console.log(`
    		count: ${count}
    		time: ${s }
    		`);
    	}else{clearInterval(timer);}
    }, 1000/60);
    下はコンソールログの結果です。

    2秒間のイベント数はバラバラで、firefoxではおよそ80回前後、chromではかなり正確で125~6回となりました。
    ブラウザによってここまで差があるとは予想してませんでした。

    firefox:
    Google chrome:


    count: 1
    count: 1


    time: 28
    time: 17


    count: 2
    count: 2


    time: 59
    time: 34


    count: 3
    count: 3


    time: 78
    time: 50


    count: 4
    count: 4


    time: 112
    time: 65


    count: 5
    count: 5


    time: 138
    time: 81


    微妙に毎回違うのとブラウザ別でここまで差があるとは思ってなかったな。
    setIntervalは回数を設定できるのが便利だけど、こういう結果をみると、一定のリズムを刻む関数はこれからはRequest Animation Frameを使いのがいいですね。

    date関数を使って実際の間隔を計測:https://labo.studio-happyvalley.com/RequestAnimationFrame/date/

    回数もコールバックのパラメータに任意の係数を掛け算することで制御可能ですね。
    例えば1秒間に20回にしたい場合は、最大上限の60に対して3掛けてあげれば正確に1秒間に20回のイベントを発生させることが可能です。

    requestAnimationFrameでイベント回数を設定する:https://labo.studio-happyvalley.com/RequestAnimationFrame/rafplus/

『一定間隔でイベントを正確に発生させるスクリプト』関連のお薦め

このページで紹介しているプログラムやビジュアルなどご依頼いただければ実装を賜ります。
お問い合わせはこちら

Permanent Exhibition