今回からまた新たなお題を、EaselJSサイトのデモから頂戴しよう。「ALPHAMASK FILTER」というサンプルで、アルファチャネルのマスクが動的につくられる(図1)。EaselJSのDisplayObjectインスタンスには、Shapeオブジェクトでベクターマスクがかけられる(DisplayObject.maskプロパティ)。アルファマスクは、ベクターマスクとは異なり、AlphaMaskFilterクラスで扱われるフィルタだ。「ALPHAMASK FILTER」では、さらにBoxBlurFilterクラスで、インスタンスのイメージにぼかしを加えている。今回は、まずこのふたつのフィルタの使い方を学ぼう。
外部画像ファイルから読込んだイメージをインスタンスにしてステージに置く
フィルタをかけるには、ステージにその対象となるインスタンスが置かれていなければならない。外部画像ファイルを読込んでステージに加えるには、ライブラリとしてEaselJSのほかにPreloadJSを用いる。これらをscript要素に定めておく[1]。
画像(PNG)ファイル(image.png)は、HTMLドキュメントと同じ場所のフォルダ(images)に納められているものとする。CreateJSによる画像ファイルの読込みは、すでに第2回コード1で解説した。今回のお題がつくりやすいように、JavaScriptコードをつぎのように書いてみた。
Bitmapインスタンスをつくって返す関数(createBitmap())は分けて定めた。Imageオブジェクトを引数として渡す。これで、Bitmapインスタンスがいくつでもできる。まだ、インスタンスの位置を決めていないので、親であるStageオブジェクトの基準点に置かれる(図2)。
Bitmapインスタンスはステージの真ん中に置きたい。そのためには、読込んだ画像の大きさを調べなければならない。ということは、LoadQueue.fileloadイベントのリスナー関数(draw())で扱うべきだ。さらに、ステージの真ん中はCanvasの大きさから求める。Canvasの参照(canvasElement)は、初期化の関数(initialize())で得ている。これを、どうやってイベントリスナーに伝えるか。
簡単なのは、Canvasの参照あるいは大きさ(幅と高さ)をグローバルな変数に与えることだ。だが、インスタンスの座標は始めに定めたら、その後動かすことはない。つまり、変数が使われるのは1度きりだ。そのままメモリに残すのは、無駄に感じられる。
そこで、LoadQueueクラスの仕組みを使って、値をイベントリスナーに渡そう。LoadQueue.loadFile()メソッドの引数に与えるファイルは、Objectインスタンスでも定められる。読込むファイルのURLは、オブジェクトにsrcというプロパティで加える。このオブジェクトに納められるプロパティは、他にもいくつかある(詳しくは「PreloadJSで外部ファイルを読込む」参照)。今回は、任意の値が入れられるdataプロパティを使う。
LoadQueue.loadFile()メソッドの引数に渡すオブジェクトにdataプロパティを定めると、LoadQueue.fileloadイベントのリスナー関数で受け取れる。リスナーの引数に渡されるイベントオブジェクトには、itemというプロパティが与えられる。その中のdataプロパティに、LoadQueue.loadFile()メソッドの引数に渡したdataプロパティの値が納められるのだ。
そこで、初期化の関数(initialize())の中でCanvasの幅と高さをPointオブジェクト(canvasSize)で定め、LoadQueue.loadFile()メソッドの引数に渡すオブジェクトのdataプロパティに与える。LoadQueue.fileloadイベントのリスナー関数(draw())は、イベントオブジェクトのitemプロパティからdataプロパティとしてPointオブジェクトが取り出せるため、その値と読込んだ画像イメージの大きさからBitmapインスタンスをステージの真ん中に置くことができる(図3)。これらの処理を加えたのが、つぎのコード1だ。
BoxBlurFilterクラスで画像イメージをぼかす
では、早速BoxBlurFilterクラスでBitmapインスタンスのイメージをぼかしてみたい。だがその前に、ひとつ重要な注意を覚えていただきたい。EaselJSのコンパクト版JavaScript(JS)ファイル(easeljs-X.X.X.min.js)にはフィルタのクラスが基本的に含まれないということだ[2]。つまり、使うフィルタのクラスはscript要素でひとつひとつ読込まなければならない。
フィルタをかけるには、大きく3つの手順を踏む。第1に、フィルタのインスタンスをつくる。お約束どおり、コンストラクタの呼出しだ。BoxBlurFilter()コンストラクタには3つの引数が渡せる。初めのふたつは、それぞれ水平と垂直のぼかし幅で、単位はピクセルだ。3つ目は、ぼかしのきめ細かさを1から3の整数を目安として与える。この整数は、内部的にぼかしを重ねる回数になるので、増やせば負荷が高まる。
手順の第2は、フィルタオブジェクトをDisplayObject.filtersプロパティに定める。ただし、プロパティにはフィルタオブジェクトを直に与えるのでなく、配列に入れたうえで、その配列を代入する。これは、フィルタを複数かけられるように考えられた仕組みだ。
そして第3に、設定されたフィルタをDisplayObject.cache()メソッドで描画する。内部的には、Canvasが新たにつくられ、そこでフィルタの演算と描画を行った後、ステージにその結果が加えられる。引数は矩形領域を示す4つの数値で、その範囲にフィルタがかかる。
LoadQueue.fileloadイベントのリスナーに定めた描画の関数(draw())には、つぎのようなJavaScriptコードを加えよう。BoxBlurFilterのコンストラクタに渡すぼかし幅は大きめにした。そして、DisplayObject.cache()メソッドを呼出す矩形領域は、Bitmapインスタンスのイメージの領域となる。
ぼかし幅をかなり大きくしたため、インスタンスのもとイメージがわからないくらいぼかされる(図4)。script要素に書いたJavaScript全体は、つぎのコード2のとおりだ。
AlphaMaskFilterクラスでつくったアルファマスクをかける
つぎは、AlphaMaskFilterクラスを使う。インスタンスにアルファマスクをかける手順は、フィルタのお約束どおり3つだ。また、フィルタのクラスAlphaMaskFilterは、あらかじめscript要素に読込んでおく。
もっとも、アルファマスクをかけるにはマスクが要る。そのため、マスク用のShapeインスタンスをつくる。そして、マスクする相手は、ぼかしたBitmapインスタンスではなく、新たにもうひとつオブジェクトを加える。そこで、コードにはつぎのような手直しをする。マスク用のShapeインスタンスと追加のBitmapオブジェクトは、それぞれ変数(wipingShapeとimageBitmap)に納めた。そして、Shapeインスタンスに描画する関数(wipe())を新たに定め、半透明(アルファ0.5)の塗りで円を描いている。
もう少し細かく見ていこう。Shapeインスタンスはアルファチャネルをマスクとして使うだけなので、ステージには置かない。そして、新たに加えたBitmapオブジェクトには、ぼかしたインスタンスと同じ画像イメージを与えた。しかも、後からStageオブジェクトの子として加えたため、ぼかしたインスタンスのうえに、もとのイメージが重ねられたことになる。
アルファマスクは、上に重ねたもとイメージのインスタンスにかける。すると、マスクのShapeオブジェクトは初め透明なので、上のBitmapインスタンスは見えなくなる。そこで、マスクのオブジェクトに半透明の円を描けば、上のインスタンスのその部分が半透明で表れるという段取りだ。
Shapeインスタンスに円を描く関数についても、少し補っておく。Shape.graphicsプロパティで描画の対象となるGraphicsオブジェクトが得られる。その参照を変数に入れて、Graphicsクラスのメソッドを呼び出すことにより、さまざまなベクターシェイプが描ける。第4回コード1は、この書き方でリングのシェイプをつくった。
ところが、前掲コードではGraphicsオブジェクトを変数に入れず、そのまま続けてGraphics.beginFill()メソッドを呼び出した。実は、Graphicsクラスのメソッドは、基本的に呼出したGraphicsオブジェクトを返す。そのため、さらに続けてGraphics.drawCircle()メソッドが呼び出せる。このように、メソッドをいくらでも書連ねることができるのだ。
ただ、1行で書くと何をやっているのか見にくいので、前掲のステートメントはドット(.)の前で改行している。なお、Graphics.getRGB()メソッドを用いると、カラーの整数とアルファのふたつの引数値でカラーを定める文字列が得られる。
アルファマスクをかける関数(updateCacheImage())は、つぎのように別に定めた。また、DisplayObject.cache()メソッドでフィルタの演算結果をキャッシュに描画する関数(updateCache())も分けた。引数にはブール(論理)値とキャッシュするインスタンスを渡す。
AlphaMaskFilter()コンストラクタは、マスクするイメージを引数にとる。具体的にはImageオブジェクトまたはCanvasで与える。つまり、Shapeオブジェクトはそのままではアルファマスクにはできない。そこで、DisplayObject.cache()メソッドを呼び出して(updateCache())、Canvasにキャッシュする。すると、その参照がShape.cacheCanvasプロパティで得られるので、それをAlphaMaskFilter()コンストラクタの引数に渡せばよい。
あとは、フィルタオブジェクト(maskFilter)を配列に入れてDisplayObject.filtersプロパティに加え、別に定めた関数(updateCache())からDisplayObject.cache()メソッドでキャッシュするという手順どおりだ。なお、キャッシュする矩形領域の幅と高さはPointオブジェクトで変数(imageSize)に納めた。
これで、半透明のアルファマスクがShapeインスタンス(wipingShape)にもとづいてつくられ、AlphaMaskFilterクラスによりもとイメージをもつ前面のBitmapインスタンスに加えられる。その結果、半透明の円形の領域でぼかしが拭われたような表現になった(図5)。
クリックした位置にアルファマスクを加える
もう一歩進めて、インタラクションを加えよう。円形のアルファマスクを、マウスクリックした位置に加える。つぎのコードのように、マスクを描く関数(wipe())はStage.stagemousedownイベントのリスナーに定める。リスナー関数は、Stage.mouseXとStage.mouseYプロパティでクリックした座標を調べ、その座標が中心となるように円形のマスクを描く。
クリックするたびに、マスクには円形の塗りが加わる。すると、アルファマスクのキャッシュも描き直さなければならない。だが、新たなCanvasを増やすには及ばない。そういうときは、DisplayObject.updateCache()メソッドを呼び出すと、すでにあるCanvasの描画が改められる。
Canvasにキャッシュする関数(updateCache())は、第1引数のブール値でDisplayObject.updateCache()とDisplayObject.cache()のどちらのメソッドを呼び出すのかを選べるようにした。したがって、始めは引数falseで新たなCanvasをつくり、その後はtrueにして描画を更新すればよい。
前述のコードで試してみると、クリックした座標と円形のマスクの中心がずれてしまう(図6)。なぜなら、マウス座標はステージの基準点にもとづいて定められる。ところが、フィルタはインスタンスの中の座標で演算されるからだ。インスタンスをステージの真ん中に置くため、座標を動かした。その座標の差がマスクの位置をずらしてしまう。
そこで、インスタンスを描く関数(draw())の中で、真ん中に位置合わせした座標をPointオブジェクトにして変数(bitmapPoint)に納めた。そして、マスクを描く関数(wipe())は、円の中心座標をその分ずらす。新たに定めた関数(getMousePoint())は、インスタンスの基準点から見たマウスポインタの座標を返すようにした。
これで、クリックした座標を中心に円形のアルファマスクが加えられる(図7)。script要素全体は、つぎのコード3のとおりだ。半透明の塗りを重ねると、アルファ値は高まる。それがわかりやすいように、塗りのアルファを少し下げた。また、ぼけたイメージのインスタンス(blurBitmap)も、アルファを下げている。そうすると、手前のインスタンス(imageBitmap)が濃くなるにつれ、アルファも高まって見えるからだ。
今回はここまでにしよう。コードはjsdo.itにも掲げた。次回は、お題と同じように、ドラッグでアルファマスクを描く。