以前、canvasで画像を表示する方法についての記事を投稿しました。
その記事で、複数の画像をcanvasに描画する方法を記載したのですが、その際for...of文で1枚ずつ読み込むという方法を紹介しました。
今回はより効率的な方法を勉強したので、備忘録としてまとめていきたいと思います。
サンプルコードはこちらからどうぞ。
※サンプルで使用している画像は、非商用に限りご自由にお使いください。
環境
- OS:Windows 11
- ブラウザ:Google Chrome (ver. 150.0.7871.187)
- エディタ:Visual Studio Code
- 使用言語:HTML / CSS / JavaScript
複数の画像を一枚ずつ読み込む
冒頭で説明した記事はこちらとなります:
この記事では、複数枚の画像の読み込みに以下のようなコードを使用していました。
const canvasEl = document.getElementById('canvas02');
const ctxt = canvasEl.getContext('2d');
// 画像のパスをまとめたリスト
const imageList = [
'./image/minicat-brawn.jpg',
'./image/minicat-black.jpg'
];
// 画像の読み込み
async function loadImage(path) {
const image = new Image();
image.src = path; // 画像のパス(htmlファイルからの相対パス)
await image.decode();
return image;
}
// 画像の描画
async function drawImage() {
// for文で一枚ずつ読み込む
for (const path of imageList) {
try {
const img = await loadImage(path);
let index = 0;
const x = img.width * index;
const y = (canvasEl.height - img.height) / 2;
ctxt.drawImage(img, x, y);
index += 1;
} catch (error) {
console.log('表示に失敗しました', error);
}
}
}
drawImage();
上記のコードは、for...ofで画像を一枚ずつ順番に読み込んでいます。そのため、画像の枚数が増える度、処理時間が積み重なってしまいます。
この問題の解決策の一つに、Promis.allSettled()を使い、画像の読み込みを並行して読み込む方法があります。
Promis.allSettled() を使った並列処理
Promise.allSettled()は、結果の成否にかかわらず、複数の非同期処理がすべて完了するまで待機し、すべての処理の結果をまとめて取得します。
画像読み込みのイメージを図にすると、for...ofを使う方法では、
画像1: 読み込み開始ーーー▷完了
画像2: 読み込み開始ーーー▷完了
画像3: 読み込み開始ーーー▷完了
に対し、Promis.allSettled()を使うと、
画像1: 読み込み開始ーーー▷完了
画像2: 読み込み開始ーーーーー▷完了
画像3: 読み込み開始ーーーー▷完了
となります。読み込みのタイミングが同時になるというイメージです。
そのため、for...ofで1枚ずつ読み込む場合と比べて、画像の枚数に応じて読み込み時間が単純に積み重なることを抑えられます。
Promis.allSettled();を使ったコードは以下の通りです。
const canvasEl = document.getElementById('canvas');
const ctx = canvasEl.getContext('2d');
const imagePathList = [
'./image/---.png',
'./image/---.png',
]
async function loadImage(path) {
const img = new Image();
img.src = path;
await img.decode();
return img;
}
async function preloadImage() {
const entries = imagePathList.map((path, index) => loadImage(path));
const results = await Promise.allSettled(entries);
results.forEach((result, index) => {
if(result.status === 'fulfilled') {
const image = result.value;
const x = image.width * index;
const y = (canvasEl.height - image.height) / 2;
ctx.drawImage(image, x, y);
} else {
console.error(`読み込み時にエラーが起きました:${imagePathList[path]}`, result.reason);
}
});
}
preloadImage();
画像の読み込みの処理が始まるタイミングは
const entries = imagePathList.map((path, index) => loadImage(path));
です。
map()もfor...of文と同じように配列の要素を順に処理しますが、ここではloadImage()が返すPromiseの完了を待たずに次々と処理を始めます。そのため、厳密には同時ではないものの、次々とloadImage()が呼び出され、複数の画像読み込みが並行して進みます。
imagePathList.map()を実行すると、loadImage()が画像の数だけ呼び出され、それぞれのPromiseオブジェクトがentriesに格納されます。
この時点で各画像の読み込み処理は開始されており、一つの画像の読み込み完了を待つことなく、次の画像の読み込みが開始されます。
その後、Promise.allSettled(entries)によって、すべてのPromiseが完了するまで待機します。処理が完了すると、それぞれのPromiseが成功したか失敗したかを表す結果がresultsに配列として格納されます。
resultに格納されるPromiseオブジェクトは下記のようなイメージです。
{
status: 'fulfilled',
value: {name: --, image: img}
},
{
status: 'rejected',
reason: DOMExpection
}
成功ならstatus=fulfilled, 失敗ならstatus=rejectedとなります。
上記のコードはこのstatusを分岐の条件にしています。
最後に
本記事では、Promise.allSettled()を用いた画像の読み込み方法についてまとめました。
今回紹介した方法は、あくまでも一つの手段であり、複数の画像を効率よく読み込む・表示させる方法は他にもたくさんあると思います。
それらの方法をちゃんと使い分けるようになれるようになりたいです。