Data URL使ってセーブ画面にサムネイル表示したった【RPGツクールMZ】

# 「セーブデータとセーブ画面・ロード画面の変更」プラグイン備忘録

通称「自分用セーブプラグイン」  
ソースコードは以下にて公開中。  
自分用なので他者環境で動作するかは不明。

https://github.com/kuroudo119/RPGMZ-Plugin/blob/master/KRD_MZ_Save.js

## サムネイル表示

2022年2月21日にサムネイル表示を追加した。  
本書はその際のやったことメモである。

![save.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373498/742ff934-3948-0006-4d9d-e9e791160428.png)

### 機能概要

セーブデータごとにセーブ時のマップ画像を表示する機能である。

### 必要な処理

サムネイル表示に必要な処理は以下である。

- 画面キャプチャする。
- 画面キャプチャを保存する。
- 画面キャプチャを読込する。
- 画面キャプチャを表示する。

---

### 動作確認編

まずは各処理を作りながら動作確認をしていく。

#### 画面キャプチャする

画面キャプチャは以下の関数がコアスクリプト（rmmz_managers.js）内で定義されているので、これを使用する。

```javascript
SceneManager.snap()
```

この snap 関数を使うことで画面キャプチャした画像（Bitmap クラスのインスタンス）が得られる。

#### 画面キャプチャを表示する

Bitmap クラスは blt 関数で表示することができる。  
blt 関数はコアスクリプト（rmmz_core.js）内で定義されている。

その blt 関数内では drawImage 関数が使用されている。  

```javascript
Bitmap.prototype.blt()
```

```javascript
CanvasRenderingContext2D.drawImage()
```

画面キャプチャした画像を、まずは試しに blt 関数を使用した。
問題なく表示できた。

#### 画面キャプチャを保存する

画面キャプチャできた、その画像の表示もできたとなると、次は画像の保存である。

今回は、セーブ画面でのサムネイル表示に使うため、SavefileInfo に含めることで保存する。  
以下の関数をオーバーライドし、 info に値を追加することとする。

```javascript
DataManager.makeSavefileInfo()
```

さて、ここで問題が起こる。

Bitmapクラスのインスタンスを追加した所、セーブできない状態となった。

詳細は未確認だが、推測では、Bitmapクラスのインスタンスが大きすぎる、インスタンスを保存できていない等が思いつく。  
つまり、Bitmapクラスのインスタンスを info 内に追加するのは難しいように感じる。

---

### 調査編

この時点で着目すべき点は以下である。

- SavefileInfo に含める必要がある。
- Bitmap は保存できないと思われる。

ここで、前述の blt 関数と内部の drawImage 関数を見たところ、drawImage 関数の引数として使うのは、canvas または image のデータである。  
canvas または image のデータは、Bitmap 内の一部データなので、Bitmapよりもデータサイズが小さいと考えられる。  
そこで、「Bitmap を blt 関数で表示する処理」を「canvas を drawImage 関数で表示する処理」に変え、これでも問題なく表示されることを確認した。

しかし、保存がうまくいかない。画像データが保存されていないような状況であった。

その時、Data URL という方法を知る。

> Data URL によって、画像を Base64 でエンコードした文字列として、コード内で完全に定義できます。

https://developer.mozilla.org/ja/docs/Web/API/Canvas_API/Tutorial/Using_images

https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement/toDataURL

```javascript
HTMLCanvasElement.toDataURL()
```

Base64エンコードとは、画像などのバイナリデータを文字列に変える方法である。  
つまり、文字列であれば、SavefileInfo に含めることができる可能性がある。

これを試した所、ファイルサイズが大きくなるが保存に成功した。

---

### 解決編

この時点で残る問題は以下である。

- セーブファイルサイズが大きい。

#### 画面キャプチャする

セーブファイルサイズを小さくするため、jpeg を使うこととし、画質も落とした。

尚、snap 関数は改造しているが内容は割愛する。

```javascript
Scene_Base.prototype.mapImage = function() {
	if (USE_MAP_IMAGE) {
		const scale = 0.125;
		const width = Math.floor(Graphics.boxWidth * scale);
		const height = Math.floor(Graphics.boxHeight * scale);
		const bitmap = SceneManager.snapWH(width, height);
		const image = new Image(width, height);
		image.src = bitmap.canvas.toDataURL("image/jpeg", 0.1);
		$gameTemp._mapImage = image;
	} else {
		$gameTemp._mapImage = null;
	}
};
```

画面キャプチャを行うタイミングは、Scene_Map クラスの stop 関数とした。
具体的には、Scene_Menu および Scene_Battle への遷移前である、

このタイミングで画面キャプチャしてもその後にセーブしない場合が多いと思われるが、事前に取得しておかないと取り逃してしまうので、このタイミングとした。

尚、場所移動時のオートセーブは、タイミングの都合上、真っ暗の画面キャプチャか、移動前マップの画面キャプチャとなるので、今回はキャプチャなしとした。

補足であるが、Scene クラスのライフサイクルは initialize → create → start → update → stop → terminate となっている。

```javascript
const KRD_Scene_Map_stop = Scene_Map.prototype.stop;
Scene_Map.prototype.stop = function() {
	if (!SceneManager.isNextScene(Scene_Map)) {
		this.mapImage();
	} else {
		$gameTemp._mapImage = null;
	}
	KRD_Scene_Map_stop.apply(this, arguments);
};
```

#### 画面キャプチャを保存する

画面キャプチャの保存は info への追加により行う。

画像データとして src, width, height の3つが必要なので、それぞれ値を設定する。

```javascript
const KRD_DataManager_makeSavefileInfo = DataManager.makeSavefileInfo;
DataManager.makeSavefileInfo = function() {
	const info = KRD_DataManager_makeSavefileInfo.apply(this, arguments);
	info.mapName = this.getMapName();
	info.version = this.getVersion();
	info.mapImage = this.getMapImage();
	return info;
};
```

```javascript
DataManager.getMapImage = function() {
	const result = $gameTemp._mapImage ? 
	{
		src: $gameTemp._mapImage.src,
		width: $gameTemp._mapImage.width,
		height: $gameTemp._mapImage.height,
	} : null;

	$gameTemp._mapImage = null;
	return result;
};
```

#### 画面キャプチャを読込する

画像の読込は非同期で行われるため、確実に表示するには待つ必要がある。

addEventListener がそのための処理である。

```javascript
Window_SavefileList.prototype.drawContents = function(info, rect) {
	if (USE_MAP_IMAGE && info.mapImage) {
		const image = new Image(info.mapImage.width, info.mapImage.height);
		image.src = info.mapImage.src;
		image.addEventListener("load", element => {
			this.drawMapImage(rect, image);
			this.drawMainContents(info, rect);
		});
	} else {
		this.drawMainContents(info, rect);
	}
};
```

#### 画面キャプチャを表示する

drawImage 関数を使って Image クラスの画像を表示する。

尚、try 文になっているのは、コアスクリプトを真似たためである。

```javascript
Window_SavefileList.prototype.drawMapImage = function(rect, image) {
	if (image) {
		try {
			const sx = 0;
			const sy = 0;
			const sw = image.width;
			const sh = image.height;
			const dw = sw;
			const dh = sh;
			const dx = rect.x + rect.width - dw;
			const dy = rect.y + Math.floor((rect.height - dh) / 2);

			this.contents.context.globalCompositeOperation = "source-over";
			this.contents.context.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
			this.contents._baseTexture.update();
		} catch (exception) {
			//
		}
	}
};
```

以上。