nmi.jp Twitter → @tkihira
自分がフリーランスの時に気をつけていた3つのこと JSXでCanvasを使う方法

JavaScriptでアニメーションを書く初歩の初歩


2013-08-17
Takuo Kihira

JavaScriptを使ってアニメーションを書くときに有用なテクニックの、基本中の基本をご紹介します。おそらく、このブログを見ている人のほとんどにとっては釈迦に説法だと思います。今回、requestAnimationFrameの話すらしません。その点、ご留意ください。

まず、JavaScriptでアニメーションをする場合に気をつけないといけないのが、一度JavaScriptの実行(Context)を抜けないとブラウザに描画が反映されないということです。簡単に言うと、

<html><head><title>bad sample</title><script>
onload = function() {
    var e = document.getElementById("e");
    for(var i = 0; i <= 100; i += 5) {
        e.style.left = e.style.top = i + "px";
    }
};
</script></head><body>
<div id="e" style="position:absolute;background-color:red;width:50px;height:50px"></div>
</body>

このようなコードを書いても、途中経過は全く描画に反映されず、最終的に(100px, 100px)の位置に赤い四角が表示されておしまいです。

ゆっくりと移動させたい場合、setIntervalを使うこともできます。

<html><head><title>normal sample</title><script>
onload = function() {
    var e = document.getElementById("e");
    var i = 0;
    var id = setInterval(function() {
        e.style.left = e.style.top = i + "px";
        i += 5;
        if(i > 100) {
            clearInterval(id);
        }
    }, 100);
};
</script></head><body>
<div id="e" style="position:absolute;background-color:red;width:50px;height:50px"></div>
</body>

このように、setIntervalを使うことで定期的に実行する関数を容易して、その関数の中で少しずつ動かします。

ただsetIntervalをキャンセルする場合には、設定時に返値のidを保存して、clearIntervalを利用して消してやらねばいけません。ちょっとしたアニメーションだと問題にはならないですが、複雑なアニメーションになると管理が大変にめんどくさいのです。

というわけで、setIntervalではなくてsetTimeoutを使って書く方法があります(以下、script部分だけ書きます)。

onload = function() {
    var e = document.getElementById("e");
    var i = 0;
    var move = function() {
        if(i <= 100) {
            setTimeout(move, 100);
        }
        e.style.left = e.style.top = i + "px";
        i += 5;
    };
    move();
};

こう書くことによって、iが100以下の場合のみ再度move関数が呼ばれるようになります。setTimeoutを先頭で実行しているのには意味があります。この場合、setTimeoutが呼ばれてから100ミリ秒後にmove関数呼ばれるのですが、関数の先頭で呼ぶことで、その後の処理の重さに関わらず一定間隔で呼び続けることが出来ます。

ちなみに、上のコードはもうちょっとシンプルに、このように書くことが出来ます。

onload = function() {
    var e = document.getElementById("e");
    var i = 0;
    (function move() {
        if(i <= 100) {
            setTimeout(move, 100);
        }
        e.style.left = e.style.top = i + "px";
        i += 5;
    })();
};

名前付き関数式を使うことで、moveのスコープをmove関数内のみに限定して書くことが出来ます。大変すっきりしますね。

さて、このやり方でも良いのですが、これだと「きっかり2秒間、可能な限り滑らかにアニメーションしたい!」という要求を満たすことが出来ません。そういうときには、次のように書くときれいにかけます。

onload = function() {
    var e = document.getElementById("e");
    var i = 0;
    (function move() {
        move.endTime || (move.endTime = Date.now() + 2000);
        var ratio = Math.min(1, 1 - (move.endTime - Date.now()) / 2000);
        e.style.left = e.style.top = 100 * ratio + "px";
        if(ratio < 1) {
            setTimeout(move, 10);
        }
    })();
};

初回実行時に終了時間をセットし、現在の時間との差分をアニメーションの長さで割ることで今アニメーションが何割進んでいるのかを計算し、その結果を元に描画をします。この場合のsetTimeoutは、別にゼロを指定してもよいのですが、人間の目は60fps以上反応出来ないので、ここでは10を指定しています。

なおこのやり方は、今回は解説しませんが、requestAnimationFrameとの相性が大変良いです。また、イージングをつけるのも大変簡単です。たとえば100 * ratioを100 * ratio * ratioにするだけでイーズアウトになりますね。

なお、Math.minを使っているのは、ある種常套句です。

if(value < 0) { value = 0; }
if(value > 1) { value = 1; }
// ↓
value = Math.max(0, Math.min(1, value));

このような感じです。しかし、計ったことはないですが、たぶん速度も遅くなっている気がしますので、まあ存在を知っておく程度の構文だと思ってください。仕様書などで見かけることがあります。

さて、このようなやり方でアニメーションを定義すると、複数のキーフレームアニメーションもそこそこ簡単に書くことが出来るようになります。

onload = function() {
    var e = document.getElementById("e");
    var i = 0;
    (function move() {
        move.endTime || (move.endTime = Date.now() + 2000);
        var ratio = Math.min(1, 1 - (move.endTime - Date.now()) / 2000);
        e.style.left = e.style.top = 100 * ratio + "px";
        if(ratio < 1) {
            setTimeout(move, 10);
        } else {
            disappear();
        }
    })();
    var disappear = function () {
        disappear.endTime || (disappear.endTime = Date.now() + 2000);
        var ratio = Math.max(0, (disappear.endTime - Date.now()) / 2000);
        e.style.opacity = ratio;
        if(ratio > 0) {
            setTimeout(disappear, 10);
        }
    };
};

このように、赤い四角が右下に移動して、その後消えていくアニメーションを書くことが出来ます。後はこれをうまく一般化するだけで、そこそこ簡潔にHTMLのアニメーションを書くことが出来るようになると思います。