canvas : 画像の合成方法いろいろ
本日は、ほったらかしになっていた canvas ネタを再開したいと思います。だいぶ時間が経ってしまったので、概要を軽く説明しておきましょうか。 canvas は JavaScript で高度なグラフィック処理ができる新しい HTML 要素です。 Safari や Firefox, Opera が標準でサポートしており、 IE でも excanvas という JavaScript ライブラリを読み込むことで基本機能が利用できます。 W3C 標準にはなっていないものの、現時点では最も汎用性のある Web グラフィックソリューションかと思います。これまでの記事では基本的な描画メソッドとさまざまな描画スタイルをみてきました。過去記事はこちらからご参照いただけます。
今回は、描画時の合成方法の変更を試してみたいと思います。具体的には、図形を描画する際に背景画像と合成する方法をいくつか選ぶことができますので、その指定方法と効果についてご紹介していきます。ドローツールなどでよくある「パスの合成」のような機能が実現でき、使いこなせばかなり面白い効果が出せそうです。ただ、残念ながらこの機能は IE + excanvas ではサポートされないようです。
使い方
それでは、まずは使い方からご紹介しましょう。今回は実に簡単です。
合成モードの指定
合成モードの指定は、描画コンテキストの globalCompositeOperation プロパティーに合成モードを表す文字列を設定するだけです。例えば、以下のようにすると xor モードになります。
context.globalCompositeOperation = "xor";
後は、普通に図形や画像を描画するだけで指定した合成モードが反映されます。合成モードは矩形、パス、ビットマップ画像のいずれの描画にも適用されます。
利用可能な合成方法
globalCompositeOperation に設定可能な値には以下のものがあります。ここで「ソース画像」と言っているのはこれから描画する図形・画像、「ディスティネーション画像」はすでに canvas 上に描画されている画像です。なお、現在のドラフトでは半透明(α値が 0 でも 1 でもない)の場合の挙動が明確ではないため、ここでも半透明は考慮していません。
- source-atop
- ディスティネーション画像の透明部分をマスクしたソース画像を、ディスティネーション画像の上に描画します。
- source-in
- ディスティネーション画像の透明部分をマスクしてソース画像を描画します。ディスティネーション画像はマスクのみに使われ、表示されません。
- source-out
- ディスティネーション画像の非透明部分をマスクしてソース画像を描画します。ディスティネーション画像はマスクのみに使われ、表示されません。
- source-over
- ディスティネーション画像の上にソース画像を描画します。デフォルトの合成モードです。
- destination-atop 〜 destination-over
- ソース画像とディスティネーション画像を入れ替えて、対応する source-??? のモードで描画したのと同じ結果になります。
- darker
- ドラフトの文面がいまいち理解できないのですが、たぶん減算合成です。
- lighter
- 同じく理解できていませんが、たぶん加算合成です。
- copy
- 単純にソース画像を上書きします。透明度に関わらず、デスティネーション画像は表示されません。ただし、 Firefox 2.0 では図形の外の部分にはディスティネーション画像が表示されます。
- xor
- ソース画像のみが不透明の領域はソース画像が、ディスティネーション画像のみが不透明の領域はディスティネーション画像が、両方とも不透明、もしくは両方とも透明の領域は透明色が描画されます。
表にまとめると、以下のようになるでしょうか。
合成モード | 0,0 | 1,0 | 0,1 | 1,1 |
---|---|---|---|---|
source-atop | 0 | 0 | D | SxD |
source-in | 0 | 0 | 0 | S |
source-out | 0 | S | 0 | 0 |
source-over | 0 | S | D | SxD |
destination-atop | 0 | D | 0 | DxS |
destination-in | 0 | 0 | 0 | D |
destination-out | 0 | 0 | D | 0 |
destination-over | 0 | S | D | DxS |
darker | 0 | 0 | D | D-S |
lighter | 0 | S | D | S+D |
copy | 0 | S | 0 | S |
xor | 0 | S | D | 0 |
見出しの "(s,d)" は s がソース画像のα値、 d がディスティネーション画像のα値です。描画結果の記号の意味は以下のようになっています。
記号 | 意味 |
---|---|
0 | 背景色 |
S | ソース画像 |
D | ディスティネーション画像 |
SxD | α合成 |
S+D | 加算合成 |
D-S | 減算合成 |
その他、ドラフトでは "ベンダー名-操作" という名前でベンダー拡張の合成モードを定義してもよいことになっています。 Opera や Firefox がなんらかの拡張を定義しているかどうかは不明です。
実装のバグ
合成モードに関しては Opera 9, Firefox 2.0 ともに Web Application 1.0 のドラフトとは挙動の異なる部分が散見されます。私が確認した範囲での不具合を以下に挙げておきます。
- Opera 9
- darker, lighter がそれぞれ min(s,d), max(s,d) として扱われます。
- Firefox 2.0
- darker が destination-over として扱われています。また、ソースがビットマップ画像の場合、画像の外の領域は必ずディスティネーション画像が表示されるようです。
これらは将来修正される可能性が高いので、利用する際はご注意ください。
サンプル
使い方がわかったところで、実際に試してみましょう。以下のサンプルでは、合成モードを変更して効果の違いを確かめられます。ついでに描画する図形(パス or ビットマップ画像)、透明度、描画色なども変更できますので、それぞれを組み合わせてどのような効果が得られるかも確認できるようにしておきました。
※ペンギンの画像は Image * After で公開されていたものを Flickr に保存して利用しています。
ディスティネーション画像の下半分は半透明で描画しています。ただし Opera では画像に対する半透明指定は機能しません。また、透明部分が確かめやすいように背景画像を設定していますが、この背景は CSS の background-image プロパティーによるものであるため、 globalCompositeOperation の影響を受けずに描画されます。ソースは以下のようになっています(簡略化のため、未対応ブラウザ用のコードは省いています)。
<script type="text/javascript"> var sample_image; function badge(dev, cx, cy, radius) { var outerRadius = radius; var innerRadius = radius * 0.8; var num_pricks = 20; dev.beginPath(); for(var i = 0 ; i < num_pricks ; ++i) { var a = Math.PI / num_pricks * i * 2; var x = Math.sin(a) * outerRadius + cx; var y = Math.cos(a) * outerRadius + cy; if(i == 0) dev.moveTo(x, y); else dev.lineTo(x, y); a = Math.PI / num_pricks * (i * 2 + 1); dev.lineTo(Math.sin(a) * innerRadius + cx, Math.cos(a) * innerRadius + cy); } dev.closePath(); } function page_onload() { sample_image = new Image; sample_image.onload = function() { page_draw(); }; sample_image.src = "http://farm1.static.flickr.com/139/362556361_93fe264eaf_o.jpg"; } function page_draw() { var canvas = document.getElementById("page_composite_sample"); if(canvas.getContext) { var dev = canvas.getContext("2d"); dev.fillStyle = "white"; dev.globalAlpha = 1.0; dev.globalCompositeOperation = "source-over"; dev.clearRect(0, 0, canvas.width, canvas.height); dev.drawImage(sample_image, 100, 0, 100, 100); badge(dev, 50, 50, 50); dev.fillStyle = "gray"; dev.fill(); dev.globalAlpha = 0.5; dev.drawImage(sample_image, 0, 100, 100, 100); badge(dev, 150, 150, 50); dev.fillStyle = "gray"; dev.fill(); var form = document.forms["page_composite"]; dev.globalCompositeOperation = form.type.value; dev.globalAlpha = parseFloat(form.alpha.value); if(form.shape[0].checked) { badge(dev, 100, 100, 80); dev.fillStyle = form.color.value; dev.fill(); } else { dev.drawImage(sample_image, 50, 50, 100, 100); } } } window.onload = page_onload; </script> <style> #page_composite_sample { position:relative; background-image:url(http://farm1.static.flickr.com/43/362377239_6b498923ec_o.jpg); } </style> <canvas id="page_composite_sample" width="200" height="200"></canvas> <form id="page_composite" name="page_composite"> ソース図形 : <input type="radio" name="shape" checked="checked" onclick="page_draw();">Path</input> <input type="radio" name="shape" onclick="page_draw();">Image</input><br/> globalCompositeOperation : <select name="type" onchange="page_draw();"> <option value="source-atop">source-atop</option> <option value="source-in">source-in</option> <option value="source-out">source-out</option> <option value="source-over" selected="selected">source-over</option> <option value="destination-atop">destination-atop</option> <option value="destination-in">destination-in</option> <option value="destination-out">destination-out</option> <option value="destination-over">destination-over</option> <option value="darker">darker</option> <option value="lighter">lighter</option> <option value="copy">copy</option> <option value="xor">xor</option> </select><br/> globalAlpha : <input type="text" name="alpha" value="1.0" onkeyup="page_draw();"/><br/> fillStyle : <input type="text" name="color" value="#0000ff" onkeyup="page_draw();"/> </form>
以上、本日は canvas のさまざまな合成方法についてご紹介しました。この方法を使えば、画像を任意形状で切り取ったり、シルエットの形状はそのままで内部を別の画像で差し替えたりと、いろいろな効果が出せます。画像ビュアーなどで画像を切り替えるときのエフェクトとして使うのも面白いかもしれませんね。皆さんなりの活用方法を考えてみてください。
詳しくはこちらの記事をどうぞ!
この記事にコメントする