<canvas>を画面中央に表示する

<canvas>タグに幅・高さをなにも設定しないデフォルトの状態では、自動的に幅300px・高さ150pxで表示されるらしいことを知りました。PCやスマートフォンで遊べるブラウザゲームを作りたいので、まずは<canvas>をcssやJavascriptで調整してレスポンシブ対応や画面中央表示に対応するようなものを考えてみることにしました。

正方形のcanvasを描く

PC・スマートフォン・タブレット…など、どんなデバイスでも同じようにゲーム画面を表示するとしたら、「正方形」のキャンバスを用意するのが一番ラクそうだなと考えました。ブラウザウィンドウが縦長・横長のいずれであっても内容がハミ出さずに表示される正方形のキャンバスを用意してみます。

「幅と高さ」の小さい方にあわせて描く正方形

<script>

const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");

// Math.minで幅か高さの小さい方を取得
const size = Math.min( window.innerWidth, window.innerHeight ); 
// 幅か高さの小さい方を正方形の1辺のサイズにする
canvas.width = size;                                            
canvas.height = size;

ctx.fillStyle = "green";
ctx.fillRect(0, 0, size, size);    

</script>

実行結果 

※ブラウザウィンドウのサイズを変更後、再読み込み(windows:f5キー / Mac:Command (⌘) + R) をすると緑色のキャンバスが正方形になる

<script>

const IMG_URL = "img/map.png";
let IMG = new Image();
IMG.src = IMG_URL;           

IMG.onload = function() {    

    const canvas = document.createElement('canvas');
    document.body.appendChild(canvas);
    const ctx = canvas.getContext("2d");
    
    // Math.minで幅か高さの小さい方を取得
    const size = Math.min( window.innerWidth, window.innerHeight ); 
    // 幅か高さの小さい方を正方形の1辺のサイズにする
    canvas.width = size;                                            
    canvas.height = size;

    ctx.clearRect(0, 0, size, size);
    ctx.fillStyle = ctx.createPattern( IMG, 'repeat' );
    ctx.fillRect(0, 0, size, size);   
    
};  

IMG.onerror = function() {
    console.error("画像の読み込みに失敗しました:", IMG_URL);
};

</script>

実行結果 

※はじめのコードで背景に画像を読み込んだバージョン

はじめのコードのfillStyle属性のあたりだけを調整したところ、画像の読み込みができていないエラーが出るようになりました。そのためコード全体をonloadイベントの中に入れて画像の読み込み完了を待つ形にしてみたら動くようになりました。

画像の読み込み完了を待つImage.decode()メソッド

上のコード(2つ目のコード)の書き方を調べていたところ、「もう2024年なんだからImage.onloadはやめてImage.decodeを使うべき」といった内容の記事を見つけてそうか…そうなのか…と思いました。上手に書き換える知恵がないのでAIにすがったところ、次のようなコードを提案してくれました。

Image.onloadの部分は「古いブラウザ向けフォールバック」として残してくれていますが、Image.decodeに対応していない古いブラウザというのは2022年にサポート終了したInternetExplorerあたりのもの。いまはEdgeもあるしIEにどのくらい需要があるのだろう…というのが気になるところです。

<script>
const IMG_URL = "img/map.png";
const IMG = new Image();

function draw() {
  const canvas = document.createElement('canvas');
  document.body.appendChild(canvas);
  const ctx = canvas.getContext("2d");
  
  const size = Math.min(window.innerWidth, window.innerHeight);
  canvas.width = size;
  canvas.height = size;
  
  ctx.clearRect(0, 0, size, size);
  ctx.fillStyle = ctx.createPattern(IMG, 'repeat');
  ctx.fillRect(0, 0, size, size);
}

window.onload = function () {
  IMG.src = IMG_URL;
  
  // decode が利用可能かを判定
  if (typeof IMG.decode === 'function') {
  // Promise ベースの処理
  IMG.decode().then(() => {
      draw();
  }).catch((err) => {
      console.error("画像の読み込みに失敗しました(decode使用):", IMG_URL, err);
  });
  } else {
  // 古いブラウザ向けフォールバック
  IMG.onload = function () {
      draw();
  };
  
  IMG.onerror = function () {
      console.error("画像の読み込みに失敗しました(onload使用):", IMG_URL);
  };
  }
};
</script>

実行結果


ウィンドウサイズの変更に対応する

正方形のキャンバスを表示できました。

今度はこれをブラウザウィンドウが縦長・横長のいずれであっても中央に表示されるようにしたいし、ウィンドウサイズの変更になめらかに対応する感じにしたいと思います。

「中央表示とリサイズの設定」「キャンバスの描画設定」「ウィンドウサイズの変化を取得(アニメーション)」「読み込み完了で実行」「ウィンドウリサイズ時にキャンバスのリサイズを実行」の5つに分けて作ってみます。

<script>
    const IMG_URL = "img/map.png";
    let Frame = 0;
    let IMG = new Image();

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext("2d");        
    document.body.appendChild(canvas);
    
    // 中央表示とリサイズの設定
    function CenterResize() {
        const size = Math.min(window.innerWidth, window.innerHeight);
        canvas.width = size;
        canvas.height = size;  
        canvas.style.position = "absolute";
        canvas.style.left = ((window.innerWidth - size) / 2) + "px";
        canvas.style.top = ((window.innerHeight - size) / 2) + "px";                      
    }

    // キャンバスの描画設定
    function draw() {
        const size = Math.min(window.innerWidth, window.innerHeight);
        canvas.width = size;
        canvas.height = size;

        ctx.clearRect(0, 0, size, size);
        ctx.fillStyle = ctx.createPattern(IMG, 'repeat');
        ctx.fillRect(0, 0, size, size);
    }

    // ウィンドウサイズの変化を取得(アニメーション)
    function animationLoop() {
        Frame++;
        draw();
        requestAnimationFrame(animationLoop);           
    }

    // 読み込み完了で実行
    window.onload = async function () {
        IMG.src = IMG_URL;

        try {
            await IMG.decode(); // 読み込み完了まで待つ
            CenterResize();
            requestAnimationFrame(animationLoop);
        } catch (err) {
            console.error("画像の読み込みに失敗しました:", IMG_URL, err);
        }
    };

    // ウィンドウリサイズ時にキャンバスのリサイズを実行
    window.addEventListener("resize", () => {
        if (IMG.complete) {
        CenterResize();
        }
    });
</script>

実行結果


レイアウト制御の最適化を考える

だいぶ理想に近いものができてきました。

ここまで<canvas>の画面中央表示をjavascript内で実装してきましたが、AIに助言を求める中で、Aiが『中央表示関連のコードをCSSで提案してくる』ケースが出てきました。気になって「どうしてそのように書き直したのか」と尋ねると、AIは「このようなケースではCSSを使用するのが適している」との助言と下記の解説を提案してくれました。

CSS でレイアウトを制御するメリット

1. コードの責務が分離されて明確になる(Separation of Concerns)

  • CSS は「見た目」や「レイアウト」の責任を持つ。
  • JavaScript は「動作」「ロジック」の責任を持つ。

➡ これにより、コードの保守性・可読性が向上します。


2. 再描画やリサイズのパフォーマンスが向上する

  • JavaScript でスタイル(style.left / style.top)を毎回変更すると、ブラウザが再レイアウト(reflow)を繰り返すことになりがち。
  • CSS の position: absoluteflexbox, grid を使うことで、ブラウザが最適化されたレイアウトエンジンを使って処理します。

3. リサイズ対応が簡潔になる

  • CSS を使えば、ウィンドウサイズに応じたレイアウト調整がメディアクエリやパーセンテージ指定で簡単に行えます。
  • JavaScript でサイズと位置を手動で毎回再計算する必要がなくなり、バグの温床が減ります。

4. アニメーション・レスポンシブ対応がしやすくなる

  • CSS に移行しておくと、例えば canvas を中央にしつつ ズーム・フェード・スライドインといったアニメーションを CSS だけで制御できます。
  • また、将来的に **スマホ対応(レスポンシブ)**が必要になった際にも CSS の方が柔軟です。

JavaScript による中央寄せのデメリット

デメリット説明
冗長なコードになるサイズと位置の再計算が毎回必要
CSS と重複する可能性があるleft, top, position などが競合
バグを生みやすいスクロールやズーム、dpiの違いに弱い
保守性が低いスタイル変更時にJSも修正が必要

結論

CSS によって canvas を中央配置することで:

  • レイアウト処理を最適化
  • 可読性と拡張性が向上
  • バグを減らし、リサイズ対応も容易

になります。

ただし、細かい位置制御や **アニメーション的な動き(たとえば動的にスライドさせる等)**が必要であれば、JavaScriptで制御する方が適しているケースもあります


中央表示とリサイズをCSSで行う

上記のようなAIからの説明を受けて、「ウィンドウサイズの変更に対応」までで作った内容のうちレイアウト部分をCSSで制御するバージョンを作成することにしました。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Script-Type" content="text/javascript">
    <style>
        html, body {
        margin: 0;
        padding: 0;
        height: 100%;
        }
        body {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        background: #333;
        }
    </style>
</head>
<body>
    <script>
        const IMG_URL = "img/map.png";
        let Frame = 0;
        let IMG = new Image();

        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext("2d");        
        document.body.appendChild(canvas);    

        function CenterResize() {
            const size = Math.min(window.innerWidth, window.innerHeight);
            canvas.width = size;
            canvas.height = size;                    
        }

        function draw() {
            const size = Math.min(window.innerWidth, window.innerHeight);
            canvas.width = size;
            canvas.height = size;

            ctx.clearRect(0, 0, size, size);
            ctx.fillStyle = ctx.createPattern(IMG, 'repeat');
            ctx.fillRect(0, 0, size, size);
        }

        function animationLoop() {
            Frame++;
            draw();
            requestAnimationFrame(animationLoop);           
        }

        window.onload = async function () {
            IMG.src = IMG_URL;

            try {
                await IMG.decode(); // 読み込み完了まで待つ
                CenterResize();
                requestAnimationFrame(animationLoop);
            } catch (err) {
                console.error("画像の読み込みに失敗しました:", IMG_URL, err);
            }
        };

        window.addEventListener("resize", () => {
            if (IMG.complete) {
            CenterResize();
            }
        });
    </script>

    <noscript>
        <p>ご使用の環境はJavascriptに非対応です</p>
    </noscript>
</body>
</html>

実行結果

いちばんはじめに作ったシンプルな緑色の正方形と比べると、かなり便利で汎用性の高そうなブラッシュアップが加えられました。

ここにいろいろなグラフィックや動くキャラクターを置いたり、シーンが遷移したりするコードを足していったりしてみたいと思います。

By

Posted in

Reply

メールアドレスが公開されることはありません。 が付いている欄は必須項目です