DOM

【JavaScript】ブラウザアニメーションに最適なタイマー処理をする方法

更新日:2023/04/11

JavaScriptでアニメーションをおこなうとき、方法の一つとしてsetTimeout()またはsetInterval()で一定時間ごとのタイマー処理が考えられます。
しかしこれらの関数はブラウザでは効率が悪いので、Window.requestAnimationFrame()が推奨されます。

そこで今回は、このメソッドについてお伝えします。

 

Window.requestAnimationFrame()

Window.requestAnimationFrame()は、ブラウザが描画をする前に呼び出されるコールバック関数を登録するメソッドです。

プログラマー側から見るとDOM要素等のスタイルを変更した直後にブラウザが描画しているように感じますが、実際は一定の周期で描画をおこなっています。

そのためsetTimeout()またはsetInterval()は、描画と描画の間に複数回のタイマー処理がおこなわれる可能性があります。

ムダなタイマー割込みが発生している

可能性なのでムダな割込みがないかもしれませんが、確実性を求めたいですね。

そこでブラウザの描画直前に通知を受け取ることができる、Window.requestAnimationFrame()を使用します。

■お詫び
タイマー処理とは、一定時間後に処理を行うことを指します。
タイトルが"タイマー処理"となっていますが、Window.requestAnimationFrame()はタイマー処理ではありませんでした。
ごめんなさい。

 

Window.requestAnimationFrame()の使い方の概要

Window.requestAnimationFrame()は、引数を一つのみ受け付けます。

Window.requestAnimationFrame()の構文

Window.requestAnimationFrame( コールバック関数 )

コールバック関数の呼び出しは、1回だけです。
継続的に処理するなら、コールバック関数内でWindow.requestAnimationFrame()をもう一度実行する必要があります。

コールバック関数は、引数を一つ受け取ります。

コールバック関数の構文

コールバック関数( DOMHighResTimeStamp )

DOMHighResTimeStampは、コールバック関数呼び出し時のperformance.now() の結果です。
つまり、ミリ秒単位のタイムスタンプです。
ただしブラウザ設定等によりミリ秒以下が小数にセットされている可能性があります。

performance.now() については次のページを読んでみてください。
【JavaScript】 所要時間などを高精度でパフォーマンス計測する方法

ブラウザでタイマー的なアニメショーン処理をおこなう場合、スタート時点のタイムスタンプを記憶しておき、描画時のタイムスタンプとの差分で描画内容を決定します。

これはsetTimeout()とsetInterval()でも、必要な処理です。

スタート時点のタイムスタンプは、自分でperformance.now() を呼び出すのではなく、最初のコールバック関数の引数を使用します。

 

Window.requestAnimationFrame()のテンプレート的コード

概要で説明した内容を元にすると、次のようなコードの流れになります。

window.requestAnimationFrame()のテンプレート的コード

let startTimeStamp = null;
const callback = timestamp =>{
        // 開始タイムスタンプのセット
    if( startTimeStamp === null ) startTimeStamp = timestamp;
        // 経過時間の取得
    const elapsedTime = timestamp - startTimeStamp;
        // 経過時間からDOM要素変更やcanvas描画などを行う
        // 前回と同じなら何もしない
          ・・・

       // アニメーションを継続するなら、 window.requestAnimationFrame()を実行
    if( 継続条件 )
        window.requestAnimationFrame(callback);
};

window.requestAnimationFrame(callback);

ブラウザは描画を待っている状態なので、コールバック関数はできるかぎる速く処理を終わらせる必要があります。

描画内容をあらかじめ計算して配列にセットしておくなどの工夫をしましょう。

 

window.requestAnimationFrame()処理をクラス化してみる

蛇足的なおまけで、クラス化してみる。

const MyAnime = class{
    #startTimeStamp=null; // 開始時間
    #userCallBack;      // ユーザー指定コールバック
    #bindCallBack;      // #callBackにthisをバインド
    #stop=false;        // 停止フラグ
    #pause=false;       // 一時停止フラグ
    #pauseStart=null;   // 一時停止開始時間
    #pauseTime=0;       // 一時停止していた時間

    constructor(callBack){
        this.#userCallBack = callBack;
        this.#bindCallBack = this.#callBack.bind(this);
    }
    #reset(){ // プライベート変数の初期化
        this.#startTimeStamp = this.#pauseStart = null;
        this.#stop = this.#pause = false;
        this.#pauseTime = 0;
    }
    #callBack(timestamp){ // コールバック処理
        if( this.#startTimeStamp === null ) this.#startTimeStamp = timestamp;
        if( this.#stop ) return; // 停止

        if( this.#pauseStart !== null ) { // 一時停止復帰後の処理
                // 停止時間の加算
            this.#pauseTime += (timestamp-this.#pauseStart);
            this.#pauseStart = null;
        }
        const time = timestamp - this.#startTimeStamp - this.#pauseTime;
            // ユーザーコールバック関数の実行
        const result = this.#userCallBack( time );
            // アニメーション終了
        if( result === false ) { this.#reset(); return;}
        
            // 一時停止処理
        if( this.#pause ){
            this.#pauseStart = timestamp;return;
        }
        window.requestAnimationFrame(this.#bindCallBack);
    }
    start(){ // アニメーション開始
        this.#reset();
        window.requestAnimationFrame(this.#bindCallBack);
        return this;
    }
    stop(){
        if( this.#startTimeStamp !== null ) { // アニメーション中
            this.#stop = true;this.#pause = false;
        }
        return this;
    }
    pause(){
        if( this.#startTimeStamp === null || !this.#stop ) // 停止が優先
            this.#pause = true;
        return this;
    }
    resume(){
        if( this.#pauseStart === null ) return; // 一時停止中ではない
        this.#pause = false;
        window.requestAnimationFrame(this.#bindCallBack);
    }
};

作成したクラスは、開始からの経過時間算出とwindow.requestAnimationFrame()の呼び出しをラップしています。

使用例として、div要素のボーターの太さを変化させる(だけ)のアニメーションを作成しました。

デモ



html

<style>
#box{
    border-radius: 50%;
    width: 100px;
    height: 100px;
    border: 1px solid red;
}
</style>

<button id="start">開始</button><button id="stop">停止</button>
<button id="pause">一時停止</button><button id="resume">再開</button>
<div id="box"></div>

JavaScript

const div = document.getElementById("box");

const animeFunc = time =>{
        const width = Math.floor((time % 3000) / 200) + 1;
        div.style.borderWidth = width + "px";
}
const myAnime = new MyAnime(animeFunc);

document.getElementById("start").addEventListener("click",()=>myAnime.start());
document.getElementById("stop").addEventListener("click",()=>myAnime.stop());
document.getElementById("pause").addEventListener("click",()=>myAnime.pause());
document.getElementById("resume").addEventListener("click",()=>myAnime.resume());

 

DOM要素のアニメーション

DOM要素のアニメーションはWeb Animation APIが使用できます。
これは、cssの@keyframesによるスタイル遷移を動的に制御できるAPIです。

単純なアニメーションなら、このAPIの方が効率がいい可能性があります。
詳しくは次のページを読んでみてください。

更新日:2023/04/11

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。