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
夢が広がりますね!
というわけで少し試してみました。
色を反転させる
まずは簡単な例として色を反転させてみます。
色を反転させる場合も、今までは 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 と同じ結果になってますね!!
-
仕様書の Color transform record の項目です ↩