iOS7 の MobileSafari でもサポートされた Canvas の blend-mode を試してみた

ゲームなど、何らかのアニメーションで画像の色を変える際に、RGB 値に0~1の値を掛けることがあるかと思います。少なくとも Flash (SWF) では一般的なはずです。1
Canvas でこれを実現しようとすると、今までは物凄く面倒くさい処理をするか、愚直に 1px ずつ処理を行う必要がありました。

これを簡単に実現できてしまうのが、iOS7 の MobileSafari でもサポートされた blend-mode の multiply です。
cf. Blending features in Canvas | Web Platform Team Blog

各オペレーションの仕様はおそらく SVG に定められているものと同じです。
cf. SVG Compositing Specification

夢が広がりますね!
というわけで少し試してみました。

色を反転させる

まずは簡単な例として色を反転させてみます。

20131018040500 20131018040501

色を反転させる場合も、今までは 1px ずつ処理するのが一般的だったかと思います。
次のような感じですね。

function invertColors(ctx) {
	var width = ctx.canvas.width;
	var height = ctx.canvas.height;
	var imageData = ctx.getImageData(0, 0, width, height);
	var data = imageData.data;
	for (var i = 0; i < data.length; i += 4) {
		data[i    ] = 255 - data[i    ];
		data[i + 1] = 255 - data[i + 1];
		data[i + 2] = 255 - data[i + 2];
	}
	ctx.putImageData(imageData, 0, 0);
}

blend-mode の difference を使うことで次のように簡潔に記述できます。

function invertColors(ctx) {
	var width = ctx.canvas.width;
	var height = ctx.canvas.height;
	ctx.globalCompositeOperation = 'difference';
	ctx.fillStyle = '#fff';
	ctx.fillRect(0, 0, width, height);
}

また、前者のように CPU で 1px ずつ処理するのとは違って、Hardware accelerated canvas の場合は GPU で高速に処理されるはずです。

次のように記述すると blend-mode がサポートされていないブラウザでも動いて良いかもしれません。

function invertColors(ctx) {
	var width = ctx.canvas.width;
	var height = ctx.canvas.height;

	ctx.globalCompositeOperation = 'difference';
	if (ctx.globalCompositeOperation === 'difference') {
		ctx.fillStyle = '#fff';
		ctx.fillRect(0, 0, width, height);
		return;
	}

	var imageData = ctx.getImageData(0, 0, width, height);
	var data = imageData.data;
	for (var i = 0; i < data.length; i += 4) {
		data[i    ] = 255 - data[i    ];
		data[i + 1] = 255 - data[i + 1];
		data[i + 2] = 255 - data[i + 2];
	}
	ctx.putImageData(imageData, 0, 0);
}

Flash の着色処理を実装してみる

Flash で着色処理を行う際、各チャンネルに対して乗算値と加算値を指定できます。
仕様には次のように定められています。

R’ = max(0, min(((R * RedMultTerm) / 256) + RedAddTerm, 255))
G’ = max(0, min(((G * GreenMultTerm) / 256) + GreenAddTerm, 255))
B’ = max(0, min(((B * BlueMultTerm) / 256) + BlueAddTerm, 255))

MultTerm に対する処理は blend-mode の multiply、AddTerm に対する処理は composite-mode の lighter で対処できそうです。
簡単のため、画像の α 値は考慮しないことにします。

Ming による着色処理

与えられた画像に対して R チャンネルのみ取り出す処理、R 値を +255 する処理、R 値を -255 する処理を行う SWF を Ming で作成してみました。

#!/usr/bin/env python
from ming import *

Ming_useSWFVersion(4)
movie = SWFMovie()
movie.setRate(1)
movie.setDimension(256, 256)

img = SWFBitmap('./favicon.png')

rect = SWFShape()
rect.setRightFill(rect.addBitmapFill(img, SWFFILL_BITMAP))
rect.movePenTo(0, 0)
rect.drawLine( 256,    0)
rect.drawLine(   0,  256)
rect.drawLine(-256,    0)
rect.drawLine(   0, -256)

item = movie.add(rect)
movie.nextFrame()

item.multColor(1, 0, 0);
movie.nextFrame()

item.remove()
item = movie.add(rect)
item.addColor(255, 0, 0);
movie.nextFrame()

item.remove()
item = movie.add(rect)
item.addColor(-255, 0, 0);
movie.nextFrame()

movie.save(__file__.replace('.py', '.swf'))

作成した SWF には次の URL からアクセスできます。
http://dev.abicky.net/hatena/color_transform/color_transform.swf

Canvas による着色処理

multColor や addColor に相当する関数を次のように定義してみました。
addColor 関数も subtractColor 関数も r, g, b が非負であることを前提にしていますが、少し工夫すればひとまとめにできるかと思います。

function multColor(ctx, r, g, b) {
  ctx.globalCompositeOperation = 'multiply';
  ctx.fillStyle = 'rgb(' + [r * 255 | 0, g * 255 | 0 , b * 255 | 0].join() + ')';
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}

function addColor(ctx, r, g, b) {
  ctx.globalCompositeOperation = 'lighter';
  ctx.fillStyle = 'rgb(' + [r, g, b].join() + ')';
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}

function subtractColor(ctx, r, g, b) {
  var width = ctx.canvas.width;
  var height = ctx.canvas.height;

  ctx.globalCompositeOperation = 'difference';
  ctx.fillStyle = 'rgb(255,255,255)';
  ctx.fillRect(0, 0, width, height);

  ctx.globalCompositeOperation = 'lighter';
  ctx.fillStyle = 'rgb(' + [r, g, b].join() + ')';
  ctx.fillRect(0, 0, width, height);

  ctx.globalCompositeOperation = 'difference';
  ctx.fillStyle = 'rgb(255,255,255)';
  ctx.fillRect(0, 0, width, height);
}

これらの関数を使ってデモを作成してみました。

Flash と同じ結果になってますね!!

  1. 仕様書の Color transform record の項目です