歩行アニメーションを作ってみる

解説っぽくしてみましたが文字にしたら分かりにくくなってしまいました。
何度か読み返したり、行ったり来たりして理解を深めてください。
もし質問や、何か気になることがあったら、Twitterからお気軽にご質問ください。
デモを用意しました。

<!DOCTYPE html>
<html lang="ja" dir="ltr">
    <head>
        <meta charset="utf-8">
        <title>walk anim</title>
        <link rel="stylesheet" href="style.css">
        <script type="text/javascript" src="main.js"></script>
        <script type="text/javascript" src="character.js"></script>
    </head>
    <body>
        <div class="wrap">
            <canvas id="canvas" width="300" height="300"></canvas><br>
            <button type="button" id="toUpButton" name="button">↑</button>
            <div style="display: flex; justify-content: center;">
                <button type="button" id="toLeftButton" name="button">←</button>
                <button type="button" id="toRightButton" name="button">→</button>
            </div>
            <button type="button" id="toDownButton" name="button">↓</button>
            <p>キャラクター画像は <a href="http://www.ob2.aitai.ne.jp/~owl/index.html">ヤマンチュゲーム研究所</a> 様からお借りしています。</p>
        </div>
    </body>
    <script type="text/javascript" src="../Loading.js"></script>
</html>
const Character = {
    canvas : null,
    ctx : null,
    pos : { x : 0, y : 0},      // キャラクターの描画位置
    img : null,                 // キャラクターの Image オブジェクト
    isLoaded : false,           // 読み込みが完了していたら true
    tic : 0,                    // 画像のモーション番号
    ticMax : 3,                 // モーション番号の最大値
    dirMax : 4,                 // キャラクターの方向数 左右上下の 4 種類
    // 初期処理
    init : function() {
        // HTML から canvas を取得する
        const canvasId = 'canvas';
        this.canvas = document.getElementById(canvasId);
        this.ctx = this.canvas.getContext('2d');
        this.reDraw();
        const s = 'characterChip.png';
        this.img = new Image();
        this.img.src = s;
        // 画像の読み込みが終わったら呼ばれる
        this.img.onload = () => {
            // 初期位置をキャンバスの中心にする
            // (キャンバスの幅 / 2) - (キャラクターの一方向の幅 / 2);
            this.pos.x = this.canvas.width / 2 - this.img.width / this.ticMax / 2;
            this.pos.y = this.canvas.height / 2 - this.img.height / this.dirMax / 2;
            this.drawCharacter(2);
            // 画像の読み込みが完了したらローディング画面を非表示にする
            hideLoading();
        }
    },

    // キャンバスを無地にする
    reDraw : function() {
        this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    },

    // 0  1  2  3
    // ↑ → ↓ ←
    // キャラクターを描画する
    drawCharacter : function(dir) {
        // 前回の描画を消してから再描画する
        this.reDraw();
        // this.ctx.drawImage(描画する画像, 画像の切り抜き位置 x, y, 切り抜くサイズ幅, 高さ, 描画する位置 x, y, 描画するサイズ幅, 高さ)
        this.ctx.drawImage(this.img, this.img.width / this.ticMax * this.tic, this.img.height / this.dirMax * dir, this.img.width / this.ticMax, this.img.height / this.dirMax, this.pos.x, this.pos.y, this.img.width / this.ticMax * 2, this.img.height / this.dirMax * 2);
    },

    // キャラクターの移動
    moveCharacter : function(dir) {
        // 0  1  2  3
        // ↑ → ↓ ←
        switch(dir) {
            case 0:
                this.pos.y -= 10;
            break;
            case 1:
                this.pos.x += 10;
            break;
            case 2:
                this.pos.y += 10;
            break;
            case 3:
                this.pos.x -= 10;
            break;
        }
        // 移動する毎に歩行モーション番号を増やす
        this.tic ++;
        if(this.ticMax <= this.tic) this.tic = 0;
        this.drawCharacter(dir);
    }
}
window.onload = () => {
    // キャラクターオブジェクトの初期処理
    Character.init();
    // ボタンの初期化処理
    const toLeftButtonId = 'toLeftButton';
    const toLeftButton = document.getElementById(toLeftButtonId);
    toLeftButton.addEventListener('click', () => {
        Character.moveCharacter(3);
    });
    const toUpButtonId = 'toUpButton';
    const toUpButton = document.getElementById(toUpButtonId);
    toUpButton.addEventListener('click', () => {
        Character.moveCharacter(0);
    });
    const toRightButtonId = 'toRightButton';
    const toRightButton = document.getElementById(toRightButtonId);
    toRightButton.addEventListener('click', () => {
        Character.moveCharacter(1);
    });
    const toDownButtonId = 'toDownButton';
    const toDownButton = document.getElementById(toDownButtonId);
    toDownButton.addEventListener('click', () => {
        Character.moveCharacter(2);
    });
}

walkAnim.html

canvas と キャラクターを操作するボタンを並べています。
画像はヤマンチュゲーム研究所様からお借りしています。

画像の読み込み img.onload

どこかでみたことのある、onload ですね。

window.onload = () => {
    // 読み込みが完了したら実行する処理
}

はい、ということでウィンドウが読み込まれた時に実行したい時に使うものでした。
今回は、img の src が読み込まれた時に実行されるものになります。
ウィンドウが読み込まれた時点では、JavaScript を読み込む準備が完了しただけです。
ですので、ソースコード上で画像を読み込む場合、画像を読み込む処理が非同期となり画像ファイルの読み込みを待つ必要があります。
今回はそんなに重たい画像ではないので、ローディング画面がほとんど表示されません。
容量の大きい画像や大量の画像を読み込む場合、読み込みが完了する前に処理が開始されてしまうとエラーになることがあります。

キャンバスのクリア context.clearRect

キャンバスをクリアする処理です。
これをしないと残像が残ります。
ちなみに、 context.fillRect を使用し、上から塗りつぶすことでクリア処理っぽく・・・・・・・・・もできますが、実際にはクリアできていないので、処理落ちの原因となります。
というのも、上から塗りつぶしたとしても、塗りつぶした下地も描画処理がされているからです。

画像の描画 context.drawImage

画像を描画する関数です。
引数の数に応じていろいろできます。

// this.ctx.drawImage(描画する画像, 描画する位置x, y);
this.ctx.drawImage(this.img, 0, 0);

(0, 0)地点に画像が描画されました。
3つの引数で、描画する画像を(x, y)の位置に描画できます。
アニメーションに利用している画像をそのまま描画してみました。

// this.ctx.drawImage(描画する画像, 描画する位置x, y, 描画する幅, 高さ)
this.ctx.drawImage(this.img, 0, 0, 144, 192);

先ほどよりも画像が大きく描画されました。
5つの引数で、描画する画像を(x, y)の位置から描画する大きさの幅と高さを指定できます。
今回は元の画像を1.5倍のサイズで描画するように、画像サイズ * 1.5を描画する幅と高さを大きさとして指定しています。

// this.ctx.drawImage(描画する画像, 画像の切り抜き位置 x, y, 切り抜くサイズ幅, 高さ, 描画する位置 x, y, 描画するサイズ幅, 高さ)
this.ctx.drawImage(this.img, 0, 0, 32, 32, 0, 0, 32, 32);
this.ctx.drawImage(this.img, 64, 0);

最後はキャラクター一つ分の画像が描画されました。(比較のために元の画像も描画しています。)
9つの引数で、描画する画像、画像の切り抜き開始位置(x, y)、切り抜く幅と高さ、描画開始位置(x, y)、描画する幅と高さを指定できます。
今回は元画像の左上(0, 0)から一人分のキャラクターサイズ(幅32, 高さ32)を切り抜き、(0, 0)の位置から幅32、高さ32の大きさで描画しています。
引数がたくさんあり分かりにくいですが、アニメーションを作成するには便利です。

キャラクターの移動

キャラクターは押下された矢印ボタンの向き方向に10移動させています。

アニメーション

やっとアニメーションの説明です。
今回は一枚の画像からアニメーションを作成しています。

一枚の画像を二次元配列のように分割して見ると分かりやすいです。
画像の行(tic)方向を見ていくと、同方向の別の絵柄になっています。
画像の列(dir)方向を見ていくと、各方向の絵柄になっています。

次に先ほど見た9つの引数を取る画像を描画する関数を思い出してみます。
元の画像から、切り抜きを開始する位置、切り抜く幅と高さ、描画を開始する位置、描画する幅と高さの大きさを指定することで、画像の一部分を切り抜くことができます。
今回は画像のサイズが(96×128)なので、キャラクター一人分のサイズは(32×32)であることがわかります。(96 / 3と128 / 4)
つまり、切り抜き開始位置(0, 0)で切り抜くサイズを(32, 32)とすることで、画像の左上一人分を切り抜くことができます。
同一方向に移動した場合は、隣のモーションを一人分を切り抜けばいいので、切り抜き開始位置(0, 32)で切り抜くサイズを(32, 32)とすることで、次の歩行アニメーションを切り抜くことができます。
方向転換した場合は、キャラクター一人分の高さだけずらすことで表現することができます。
つまり、上方向から右方向へ方向転換した場合は、切り抜き開始位置を(0, 32)で切り抜くサイズを(32, 32)とすることで、画像左上から2段目の右向きの一人分を切り抜くことができます。
もしも、上方向から下方向へ方向転換した場合は、切り抜き開始位置を(0, 64)で切り抜くサイズを(32, 32)とすることで表現できます。

一枚の画像をキャラクター一人分のサイズで切り抜き、切り抜く位置を tic と dir を使うことでアニメーションっぽさを出しているということです。