phina.jsで画像を変換して新しく登録する

初めまして、結城ゆきです。VTuberやってます。phina.jsアドベントカレンダーがあんまり賑わっていなかったので急遽Qiitaを始めてみました。

# なにができる
アセットに登録された画像のピクセル情報の取得と、拡大した画像を新しく登録することが出来ます。応用すれば色反転とかぼかしとか色々使えます。
なお、[phina.jsでSpriteの輪郭を強調](https://github.dev7.jp/b/2015/10/10/outline/)を参考にしました。

# 実装
コメントで説明します。使用例も書いておいたので使い方もわかるかと思います。

```javascript

	// 読み込み済みの画像アセットからピクセル情報とサイズを取得します。
	// アセットにある名前をこの関数に渡します。
	// 取得できるオブジェクトはreturnを参照
	function getImageData(name)
	{
		var pixelData = new Array();	// 返却するピクセル情報の保管場所
		var texture = phina.asset.AssetManager.get("image", name);	// アセットから画像を読み込む
		var w = texture.domElement.width;	// 
		var h = texture.domElement.height;	// この後何回か使うので1文字変数に入れる

		var src = phina.graphics.Canvas().setSize(w, h);	// 画像サイズのキャンバスを用意
		src.context.drawImage(texture.domElement, 0, 0);	// 色情報を得るために画像を描く

		var srcData = src.context.getImageData(0, 0, w, h).data; 	// キャンバスからrgba情報を取得
		
		var isAlpha = srcData.length / w / h === 4;	// 
		var dataLength = isAlpha?4:3	// 透過画像であれば1ピクセル当たりのデータ長は4、でなければ3とする
		
		for(var i = 0; i < srcData.length; i += dataLength)	// 各ピクセルをたどるループ
		{
			var rgba = 0;
			for(var j = 0; j < dataLength; j++)	// rgb(a)を取得するループ
			{
				rgba |= srcData[i+j] << (8 * j);
			}
			pixelData.push({
				r: rgba & 0xff,
				g: rgba >>> 8 & 0xff,
				b: rgba >>> 16 & 0xff,
				a: rgba >>> 24 & 0xff,
			});	// rgbaを分解して配列に追加する
		}
		return ({
			data: pixelData,
			width: w,
			height: h,
		});
	}
	
	// ニアレストネイバーで拡大した画像をアセットに登録します。
	// 上の関数で取得した画像データと登録するアセット名と拡大率をこの関数に渡します。
	function createScaledImage(imageData, name, scale)
	{
		var newImage = phina.graphics.Canvas().setSize(imageData.width * scale, imageData.height * scale);	// 新しく追加する画像を描くキャンバスを用意
		for(var y=0; y<imageData.height; y++)	// 
		{
			for(var x=0;x<imageData.width; x++)	// 縦横をたどる
			{
				newImage.strokeStyle = newImage.fillStyle = "rgba({r},{g},{b},{a})".format(imageData.data[x + y * imageData.width]);	// 対応するピクセルの色をセット
				newImage.fillRect(x * scale, y * scale, scale, scale);	// 
				newImage.strokeRect(x * scale, y * scale, scale, scale);	// 拡大率を適用して四角く塗りつぶす
			}
		}
		phina.asset.AssetManager.set("image", name, newImage);
	}
	
	// LoadingSceneを上書きします。
	phina.define("LoadingScene",
	{
		superClass: "DisplayScene",
		init: function(options)
		{
			this.superInit(options);
			this.backgroundColor = "black";
			
			var loader = AssetLoader();	// アセットを読み込むのに使う
			
			var label = Label("読み込み中...")
				.setPosition(this.gridX.center(), this.gridY.center())
				.addChildTo(this);
			label.fill = "white";

			// アセットの読み込みが完了したら実行される
			loader.onload = function()
			{
				// 同じ名前を指定するので上書きされる
				createScaledImage(getImageData("hiya0"), "hiya0", FIELD.scale);
				createScaledImage(getImageData("hiya1"), "hiya1", FIELD.scale);
				createScaledImage(getImageData("hiya2"), "hiya2", FIELD.scale);
				
				this.flare('loaded');
			}.bind(this);

			loader.load(options.assets);
		},
	});
	
	phina.define("MainScene"),
		// 他の処理を省略
		
		// ブロックを初期化します。
		initBlocks: function()
		{
			this.background.setImage("hiya0");	// 脱衣状態の画像を背景にセットする
			var blockPixel = getImageData("wear0");	// 着衣状態の画像データを取得する
			
			for(var i = 0; i < blockPixel.data.length; i++)
			{
				if(blockPixel.data[i].a != 255)continue;	// そのピクセルが透明ならブロックを置かない
				
				var block = Block();	// ブロック生成()
				block.addChildTo(this.blocks);	// ブロック設置
				// ブロックサイズや位置等の設定は省略
			}
		},
	});
```

# いきさつ
第二回V-1という企画に参加するためにゲームを作ろうとしていました。お題が「南国」だったので、脱衣ブロック崩しを作りました。服を着ている部分と着ていない部分を定義するために透過画像を使いたかったので実装されました。
動画はこちら[【第2回V-1】あのコの水着を見せて！脱衣ブロック崩し](https://www.youtube.com/watch?v=L_xj5wrY8O8)