JavaScriptでゲームを作る!
今回は画像をフェードイン、フェードアウトする方法を記します。
フェードインとフェードアウトとは?
この2つは、滑らかなトランジションを表現するために、ゲームやアニメーション、スライドショーなどでよく使われます。
ファイルとそれを入れるフォルダーを以下のような構成します。
前回の「画像を移動する方法」と同じです。
今回のコードは全体的に前回の「画像を移動する方法」と同じです。
そのため、変更を行ったフェードイン、フェードアウトの処理について記したいと思います。
どのように動くのか:
フェードの効果を実現するには、不透明度(透明度の逆)を段階的に変化させる必要があります。
変更を行った「sample.js」と「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) {
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();
});
})
}
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);
const 画像情報リスト = [
{ id: '草原', src: 'img/bg01.jpg' },
{ id: '人物01', src: 'img/chara01.png' },
{ id: '炎竜', src: 'img/mon01.png' },
{ id: 'デビル', src: 'img/mon02.png' }
];
await 画像管理.画像を読み込む(画像情報リスト);
const 背景01 = new スプライトクラス(レイヤー, 画像管理.取得する('草原'), 0, 0, 640, 640);
const 人物01 = new スプライトクラス(レイヤー, 画像管理.取得する('人物01'), 0, 100, 640, 640);
await 人物01.フェードインする();
プロミスリスト = [];
プロミスリスト.push(人物01.フェードアウトする(2.5));
プロミスリスト.push(モンスター01.フェードインする(4));
プロミスリスト.push(モンスター02.フェードインする(3));
プロミスリスト.push(モンスター03.フェードインする(1.5));
await Promise.all(プロミスリスト);
await サンプル.一時停止(2000);