忍者ブログ

JavaScriptゲーム

JavaScriptでゲームを作る!

フェードイン/アウトする方法

今回は画像をフェードイン、フェードアウトする方法を記します。

フェードインとフェードアウトとは?

  • フェードイン: 画像やオブジェクトが徐々に現れ、不透明になる効果のこと。
  • フェードアウト: 画像やオブジェクトが徐々に透明になり、消えるような効果のこと。

この2つは、滑らかなトランジションを表現するために、ゲームやアニメーション、スライドショーなどでよく使われます。

01 ファイル構成

ファイルとそれを入れるフォルダーを以下のような構成します。
前回の「画像を移動する方法」と同じです。

02 ファイル全体の流れ

今回のコードは全体的に前回の「画像を移動する方法」と同じです。
そのため、変更を行ったフェードイン、フェードアウトの処理について記したいと思います。

どのように動くのか:

フェードの効果を実現するには、不透明度(透明度の逆)を段階的に変化させる必要があります。

  1. 不透明度(Alpha値) は 1.0 で完全に見える状態を表します。
  2. 0.0 は完全に透明な状態を表します。
  3. フェードアウトでは、1.0 → 0.0 へ徐々に値を下げます。
  4. フェードインでは、0.0 → 1.0 へ徐々に値を上げます。

03 各部分の詳細解説

変更を行った「sample.js」と「sprite.js」のファイルについて解説していきます。

a. sprite.js - スプライトクラス

export class スプライトクラス {
    constructor(レイヤー, 画像要素, x1, y1, x2, y2) {
        this.レイヤー = レイヤー;
        this.ctx = レイヤー.ctx;
        this.画像要素 = 画像要素;
        this.座標を設定する(x1, y1, x2, y2);
    }

    座標を設定する(x1, y1, x2, y2) {
        const 画像要素 = this.画像要素;
        const 表示幅 = x2 - x1;
        const 表示高さ = y2 - y1;

        const 画像縦横比 = 画像要素.width / 画像要素.height;
        const 表示縦横比 = 表示幅 / 表示高さ;

        let 描画幅, 描画高さ, 位置調整x, 位置調整y;

        if (画像縦横比 > 表示縦横比) {
            描画幅 = 表示幅;
            描画高さ = 表示幅 / 画像縦横比;
            位置調整x = 0;
            位置調整y = (表示高さ - 描画高さ) / 2; // 垂直中央揃え
        } else {
            描画高さ = 表示高さ;
            描画幅 = 表示高さ * 画像縦横比;
            位置調整x = (表示幅 - 描画幅) / 2; // 水平中央揃え
            位置調整y = 0;
        }

        this.x = x1 + 位置調整x;
        this.y = y1 + 位置調整y;
        this.幅 = 描画幅;
        this.高さ = 描画高さ;
    }

    描画する() {
        this.ctx.drawImage(this.画像要素, this.x, this.y, this.幅, this.高さ); // 画像を描画
    }

    移動して描画する(移動幅x, 移動幅y, 停止位置x, 停止位置y) {
        return new Promise(resolve => {
            this.レイヤー.処理を追加する(this, () => {
                if (移動幅x) this.x += 移動幅x;
                if (移動幅y) this.y += 移動幅y;
                this.描画する();

                let 停止フラグ = false;
                if (移動幅x) {
                    if (移動幅x > 0) {
                        if (this.x >= 停止位置x) 停止フラグ = true;
                    } else if (移動幅x < 0) {
                        if (this.x <= 停止位置x) 停止フラグ = true;
                    }
                }
                if (移動幅y) {
                    if (移動幅y > 0) {
                        if (this.y >= 停止位置y) 停止フラグ = true;
                    } else if (移動幅y < 0) {
                        if (this.y <= 停止位置y) 停止フラグ = true;
                    }
                }
                if (!停止フラグ) return;

                // 停止位置に来たら描画だけの処理に切替える
                this.レイヤー.処理を追加する(this, this.描画する.bind(this));
                resolve();
            });
        })
    }

    フェードアウトする(フェード時間 = 1) {
        const 減少値 = 1 / (60 * フェード時間); // フェード時間は秒単位とする
        let 不透明度 = 1.0;
        return new Promise(resolve => {
            this.レイヤー.処理を追加する(this, () => {
                this.ctx.save(); // 現在の描画状態を保存
                this.ctx.globalAlpha = 不透明度;
                this.描画する();
                this.ctx.restore(); // 元の描画状態を復元
                不透明度 -= 減少値;
                if (不透明度 > 0) return;

                // 不透明度が0%になったら描画処理を削除する
                this.レイヤー.処理を削除する(this);
                resolve();
            });
        })
    }

    フェードインする(フェード時間 = 1) {
        const 増加値 = 1 / (60 * フェード時間); // フェード時間は秒単位とする
        let 不透明度 = 0.0;
        return new Promise(resolve => {
            this.レイヤー.処理を追加する(this, () => {
                this.ctx.save(); // 現在の描画状態を保存
                this.ctx.globalAlpha = 不透明度;
                this.描画する();
                this.ctx.restore(); // 元の描画状態を復元
                不透明度 += 増加値;
                if (不透明度 < 1) return;

                // 不透明度が100%になったら描画だけの処理に切替える
                this.レイヤー.処理を追加する(this, this.描画する.bind(this));
                resolve();
            });
        })
    }
}

追加したコードについて

フェードアウト:

    フェードアウトする(フェード時間 = 1) {
        const 減少値 = 1 / (60 * フェード時間); // フェード時間は秒単位とする
        let 不透明度 = 1.0;
        return new Promise(resolve => {
            this.レイヤー.処理を追加する(this, () => {
                this.ctx.save(); // 現在の描画状態を保存
                this.ctx.globalAlpha = 不透明度;
                this.描画する();
                this.ctx.restore(); // 元の描画状態を復元
                不透明度 -= 減少値;
                if (不透明度 > 0) return;

                // 不透明度が0%になったら描画処理を削除する
                this.レイヤー.処理を削除する(this);
                resolve();
            });
        })
    }
  1. 処理の流れ:
    • 最初は画像が完全に表示されています(不透明度 = 1.0)。
    • 徐々に 不透明度 を減少(1フレームごとに少しずつ)させます。
    • 完全に透明になったら処理を終了します。
  2. ポイント:
    • ctx.globalAlpha を使って、描画時の透明度を設定しています。
    • this.ctx.save() と this.ctx.restore() を使用することで、他の描画操作に影響が出ないようにしています。

フェードイン:

    フェードインする(フェード時間 = 1) {
        const 増加値 = 1 / (60 * フェード時間); // フェード時間は秒単位とする
        let 不透明度 = 0.0;
        return new Promise(resolve => {
            this.レイヤー.処理を追加する(this, () => {
                this.ctx.save(); // 現在の描画状態を保存
                this.ctx.globalAlpha = 不透明度;
                this.描画する();
                this.ctx.restore(); // 元の描画状態を復元
                不透明度 += 増加値;
                if (不透明度 < 1) return;

                // 不透明度が100%になったら描画だけの処理に切替える
                this.レイヤー.処理を追加する(this, this.描画する.bind(this));
                resolve();
            });
        })
    }
  1. 処理の流れ:
    • 最初は画像が完全に透明な状態から始まります(不透明度 = 0.0)。
    • 徐々に 不透明度 を増加させ、見える状態にしていきます。
    • 不透明度が 1.0 になったら処理を終了します。
  2. ポイント:
    • フェードアウトと同様に、ctx.globalAlpha を使用して不透明度を段階的に変更しています。

初心者向けのポイント

  1. 不透明度は時間でコントロール:
    • フェード効果は、「何秒以内に透明度を変える」といった具合に、秒で指定できるようにしています(小数点も可)。
    • 1フレーム(このコードでは、約1/60秒)ごとに透明度を少しずつ調整しています。
  2. 非同期処理:
    • フェード効果を非同期で行う仕組み(Promise)を使うことで、他の処理と平行して行えるようになっています。

b. sample.js - サンプルクラス

import { 画像管理クラス } from './imageManager.js'; // クラスをインポート
import { スプライトクラス } from './sprite.js'; // クラスをインポート
import { レイヤークラス } from './layer.js'; // クラスをインポート
import { ループ } from './loop.js'; // クラスをインポート

class サンプル {
    static async main() {
        const レイヤー = new レイヤークラス('canvas.レイヤー1');
        const 画像管理 = new 画像管理クラス();
        ループ.初期化する(1000/60);
        ループ.開始する();

        // 事前に読み込む画像の配列
        const 画像情報リスト = [
            { id: '草原', src: 'img/bg01.jpg' },
            { id: '人物01', src: 'img/chara01.png' },
            { id: '炎竜', src: 'img/mon01.png' },
            { id: 'デビル', src: 'img/mon02.png' }
        ];

        // ページ初期化時に画像をプリロード
        if (!await 画像管理.画像を読み込む(画像情報リスト)) return;

        // 画像の描画位置を設定
        const 背景01 = new スプライトクラス(レイヤー, 画像管理.取得する('草原'), 0, 0, 640, 640);
        const 人物01 = new スプライトクラス(レイヤー, 画像管理.取得する('人物01'), 0, 100, 640, 640);
        const モンスター01 = new スプライトクラス(レイヤー, 画像管理.取得する('炎竜'), 0, 0, 640, 540);
        const モンスター02 = new スプライトクラス(レイヤー, 画像管理.取得する('デビル'), 0, 320, 320, 640);
        const モンスター03 = new スプライトクラス(レイヤー, 画像管理.取得する('デビル'), 320, 320, 640, 640);

        ループ.処理を追加する(レイヤー, レイヤー.描画する.bind(レイヤー));
        背景01.レイヤー.処理を追加する(背景01, 背景01.描画する.bind(背景01));

        let プロミスリスト;
        while (1) {
            await 人物01.フェードインする();
            await サンプル.一時停止(2000);

            プロミスリスト = [];
            プロミスリスト.push(人物01.フェードアウトする(2.5));
            プロミスリスト.push(モンスター01.フェードインする(4));
            プロミスリスト.push(モンスター02.フェードインする(3));
            プロミスリスト.push(モンスター03.フェードインする(1.5));
            await Promise.all(プロミスリスト);
            await サンプル.一時停止(2000);

            await モンスター02.フェードアウトする();
            await サンプル.一時停止(2000);

            プロミスリスト = [];
            プロミスリスト.push(モンスター01.フェードアウトする(1.5));
            プロミスリスト.push(モンスター03.フェードアウトする(3));
            await Promise.all(プロミスリスト);
            await サンプル.一時停止(2000);
        }
    }

    static 一時停止(時間) {
        return new Promise(resolve => setTimeout(resolve, 時間));
    }
}

addEventListener('load', サンプル.main);

このコードの全体の流れ

  1. 画像を読み込む:
    • 必要な画像を事前にメモリへロードします(画像管理クラスを使用)。
    • ロード後に各画像をスプライトクラスとして設定し、描画を準備します。
  2. アニメーションの管理:
    • ループを使って、画面内の描画やアニメーション(フェードイン/アウト)を繰り返します。
  3. フェード処理を実行:
    • 人物 や モンスター 画像をフェードインまたはフェードアウトさせ、アニメーションを構成します。
  4. 一時停止(待機):
    • 指定時間だけ処理を止めることで、画面切り替えのタイミングを調整します。

主要なポイントの解説

  1. 画像のプリロード:
    • 目的:画面をスムーズに表示するため、使用する画像を事前に読み込みます。
      const 画像情報リスト = [
          { id: '草原', src: 'img/bg01.jpg' },
          { id: '人物01', src: 'img/chara01.png' },
          { id: '炎竜', src: 'img/mon01.png' },
          { id: 'デビル', src: 'img/mon02.png' }
      ];
      await 画像管理.画像を読み込む(画像情報リスト);
      
    • ポイント:
      • src は画像ファイルのパス、id はその画像を区別するための名前です。
      • この処理が完了するまで次の処理には進みません(await の利用)。
  2. 画像の描画と配置:
    • 目的:画像の表示サイズや位置を設定します。
      const 背景01 = new スプライトクラス(レイヤー, 画像管理.取得する('草原'), 0, 0, 640, 640);
      const 人物01 = new スプライトクラス(レイヤー, 画像管理.取得する('人物01'), 0, 100, 640, 640);
      
    • ポイント:
      • スプライトクラス は画像をキャンバス上に描画するためのクラスです。
      • x1, y1, x2, y2 は画像を配置する位置とサイズを指定しています。
  3. フェード処理:
    • 目的:画像の表示サイズや位置を設定します。
      await 人物01.フェードインする();
      
    • ポイント:
      • await を使うことで、フェードインが完全に終わるまで待機します。
      • フェードアウトも同様の構文で実現できます。
  4. 同時処理の実行:
    • 目的:複数の画像が同時にフェードするアニメーションを作ります。
      プロミスリスト = [];
      プロミスリスト.push(人物01.フェードアウトする(2.5));
      プロミスリスト.push(モンスター01.フェードインする(4));
      プロミスリスト.push(モンスター02.フェードインする(3));
      プロミスリスト.push(モンスター03.フェードインする(1.5));
      await Promise.all(プロミスリスト);
      
    • ポイント:
      • Promise.all を使うことで、すべてのフェード処理を並行して実行します。
      • フェードの時間はそれぞれ異なります(例: 2.5秒 や 4秒)。
  5. 一時停止:
    • 目的:動作の切り替わりが分かるように、少しだけ一時停止しています。
      await サンプル.一時停止(2000);
      
    • ポイント:
      • 単純に指定したミリ秒(2000ミリ秒 = 2秒)待機するだけの処理です。

拍手[0回]

PR

コメント

P R

プロフィール

HN:
No Name Ninja
性別:
非公開