開発
動機
マウストレーサー開発時に発見
- マウスの軌跡をトレースするのに正確にかつ数多くリズムを刻む関数が必要だった
以前、mouseTracerを開発しているときに正確に等間隔のイベントを発生させる関数を探していました。
setIntervalやsetTimeoutは解説にも間隔が不正確と記載されていますので当初から候補にはありません。
その時に見つけたのがRequestAnimationFrameです。
現在時刻を元に、
正確に一定間隔のイベントを
発生するスクリプト
以前、mouseTracerを開発しているときに正確に等間隔のイベントを発生させる関数を探していました。
setIntervalやsetTimeoutは解説にも間隔が不正確と記載されていますので当初から候補にはありません。
その時に見つけたのがRequestAnimationFrameです。
Request Animation Frameは仕様上から考えて非常に正確にリズムを刻むと予想できます。
理由としてはコールバック関数が現在時刻をパラメータとして受け取る仕様だからです。
メソッドを入れ子にし、そのパラメータの差分を取得することでミリ秒単位で時間を計測できます。
他の関数たとえば、setTimeoutやsetIntervalなどの関数を使用する場合は別途date関数などと併用する必要があります。
以下はMDNによるRequest Animation Frameの仕様説明です。
※Request Animation Frameコールバック:次の再描画でアニメーションを更新する時に呼び出す関数を指定します。コールバック関数は 1 個の引数 DOMHighResTimeStamp を受け取ります。この引数は、requestAnimationFrame がコールバックの呼び出しを開始した時点の時刻、すなわち performance.now() から返された時刻を示します。
※performance.now() 現在時刻を1msに丸めずマイクロ秒までの精度を持った浮動小数点の値で時刻を表します。
Request Animation Frameは回数を設定することはできません。
そのため、最初に1秒間に何回イベントが発生するのかを調べます。
調べる方法は、コールバック関数が現在時刻を取得する仕様を利用し、2秒間で何回イベントが発生するか調べます。
(1秒でもいいのですが、統計的にはより分母が多い方が正確なので。もちろん10秒でもいいのですが)
具体的には、直前のタイムスタンプと現在時刻の差分が指定時間を超えると停止するように作成します。
その間に何回イベントが発生するか計測すればいいことになります。
またコンソールログにイベントの時刻を記録し随時書き出します。
これによりイベントの間隔が何ミリセコンドがわかります。
/* 説明が重複しますが、コールバックメソッドには、1 個の引数 DOMHighResTimeStamp が渡されます。
これは現在の時刻を(time originからの経過ミリ秒数で)示します。
つまり、下記プログラムのtimestampには現在時刻が格納されます。
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回の間隔でイベント発生といえます。
date関数と組み合わせてSetIntervalもどのくらいの間隔でイベントが発生するか調べます。
基本は前と同様に2秒間に何回イベントが発生するか検証しますが、setIntervalは回数を設定できるので、requestAnimationFrameにあわせて60回にします。
コンソールログには同様にイベント間隔の時刻と回数を記録します。
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を使いのがいいですね。
回数もコールバックのパラメータに任意の係数を掛け算することで制御可能ですね。
例えば1秒間に20回にしたい場合は、最大上限の60に対して3掛けてあげれば正確に1秒間に20回のイベントを発生させることが可能です。
このページで紹介しているプログラムやビジュアルなどご依頼いただければ実装を賜ります。
お問い合わせはこちら