HTML5のCanvasでつくるダイナミックな表現―CreateJSを使う

第8回ぼかしフィルタとアルファマスク

今回からまた新たなお題を、EaselJSサイトのデモから頂戴しよう。ALPHAMASK FILTERというサンプルで、アルファチャネルのマスクが動的につくられる図1⁠。EaselJSのDisplayObjectインスタンスには、ShapeオブジェクトでベクターマスクがかけられるDisplayObject.maskプロパティ⁠。アルファマスクは、ベクターマスクとは異なり、AlphaMaskFilterクラスで扱われるフィルタだ。⁠ALPHAMASK FILTER」では、さらにBoxBlurFilterクラスで、インスタンスのイメージにぼかしを加えている。今回は、まずこのふたつのフィルタの使い方を学ぼう。

図1 EaselJSサイトのデモ「ALPHAMASK FILTER」
図1 EaselJSサイトのデモ「ALPHAMASK FILTER」

外部画像ファイルから読込んだイメージをインスタンスにしてステージに置く

フィルタをかけるには、ステージにその対象となるインスタンスが置かれていなければならない。外部画像ファイルを読込んでステージに加えるには、ライブラリとしてEaselJSのほかにPreloadJSを用いる。これらをscript要素に定めておく[1]⁠。

<script src="http://code.createjs.com/easeljs-0.6.1.min.js"></script>
<script src="http://code.createjs.com/preloadjs-0.3.1.min.js"></script>

画像(PNG)ファイル(image.png)は、HTMLドキュメントと同じ場所のフォルダ(images)に納められているものとする。CreateJSによる画像ファイルの読込みは、すでに第2回コード1で解説した。今回のお題がつくりやすいように、JavaScriptコードをつぎのように書いてみた。

var stage;
var blurBitmap;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  var loader = new createjs.LoadQueue(false);
  loader.addEventListener("fileload", draw);
  loader.loadFile("images/image.png");
}
function draw(eventObject) {
  var image = eventObject.result;
  blurBitmap = createBitmap(image);
  stage.update();
}
function createBitmap(image) {
  var myBitmap = new createjs.Bitmap(image);
  stage.addChild(myBitmap);
  return myBitmap;
}

Bitmapインスタンスをつくって返す関数(createBitmap())は分けて定めた。Imageオブジェクトを引数として渡す。これで、Bitmapインスタンスがいくつでもできる。まだ、インスタンスの位置を決めていないので、親であるStageオブジェクトの基準点に置かれる図2⁠。

図2 インスタンスはステージの左上角に置かれる
図2 インスタンスはステージの左上角に置かれる

Bitmapインスタンスはステージの真ん中に置きたい。そのためには、読込んだ画像の大きさを調べなければならない。ということは、LoadQueue.fileloadイベントのリスナー関数(draw())で扱うべきだ。さらに、ステージの真ん中はCanvasの大きさから求める。Canvasの参照(canvasElement)は、初期化の関数(initialize())で得ている。これを、どうやってイベントリスナーに伝えるか。

簡単なのは、Canvasの参照あるいは大きさ(幅と高さ)をグローバルな変数に与えることだ。だが、インスタンスの座標は始めに定めたら、その後動かすことはない。つまり、変数が使われるのは1度きりだ。そのままメモリに残すのは、無駄に感じられる。

そこで、LoadQueueクラスの仕組みを使って、値をイベントリスナーに渡そう。LoadQueue.loadFile()メソッドの引数に与えるファイルは、Objectインスタンスでも定められる。読込むファイルのURLは、オブジェクトにsrcというプロパティで加える。このオブジェクトに納められるプロパティは、他にもいくつかある(詳しくはPreloadJSで外部ファイルを読込む参照⁠⁠。今回は、任意の値が入れられるdataプロパティを使う。

LoadQueueオブジェクト.loadFile({src:ファイルのURL, 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だ。

図3 インスタンスがステージの真ん中に置かれた
図3 インスタンスがステージの真ん中に置かれた
コード1 外部画像のイメージをBitmapインスタンスに与えてステージの真ん中に置く
var stage;
var blurBitmap;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  var canvasSize = new createjs.Point(canvasElement.width, canvasElement.height);
  stage = new createjs.Stage(canvasElement);
  var loader = new createjs.LoadQueue(false);
  loader.addEventListener("fileload", draw);
  loader.loadFile({
    src: "images/image.png", 
    data: canvasSize
    });
}
function draw(eventObject) {
  var image = eventObject.result;
  var canvasSize = eventObject.item.data;
  var imageWidth = image.width;
  var imageHeight = image.height;
  var nX = (canvasSize.x - imageWidth) / 2;
  var nY = (canvasSize.y - imageHeight) / 2;
  blurBitmap = createBitmap(image, nX, nY);
  stage.update();
}
function createBitmap(image, nX, nY) {
  var myBitmap = new createjs.Bitmap(image);
  myBitmap.x = nX;
  myBitmap.y = nY;
  stage.addChild(myBitmap);
  return myBitmap;
}

BoxBlurFilterクラスで画像イメージをぼかす

では、早速BoxBlurFilterクラスでBitmapインスタンスのイメージをぼかしてみたい。だがその前に、ひとつ重要な注意を覚えていただきたい。EaselJSのコンパクト版JavaScript(JS)ファイル(easeljs-X.X.X.min.js)にはフィルタのクラスが基本的に含まれないということだ[2]⁠。つまり、使うフィルタのクラスはscript要素でひとつひとつ読込まなければならない。

<script src="easeljs/filters/BoxBlurFilter.js"></script>

フィルタをかけるには、大きく3つの手順を踏む。第1に、フィルタのインスタンスをつくる。お約束どおり、コンストラクタの呼出しだ。BoxBlurFilter()コンストラクタには3つの引数が渡せる。初めのふたつは、それぞれ水平と垂直のぼかし幅で、単位はピクセルだ。3つ目は、ぼかしのきめ細かさを1から3の整数を目安として与える。この整数は、内部的にぼかしを重ねる回数になるので、増やせば負荷が高まる。

new BoxBlurFilter(水平ぼかし, 垂直ぼかし, ぼかし品質)

手順の第2は、フィルタオブジェクトをDisplayObject.filtersプロパティに定める。ただし、プロパティにはフィルタオブジェクトを直に与えるのでなく、配列に入れたうえで、その配列を代入する。これは、フィルタを複数かけられるように考えられた仕組みだ。

そして第3に、設定されたフィルタをDisplayObject.cache()メソッドで描画する。内部的には、Canvasが新たにつくられ、そこでフィルタの演算と描画を行った後、ステージにその結果が加えられる。引数は矩形領域を示す4つの数値で、その範囲にフィルタがかかる。

DisplayObjectオブジェクト.cache(x座標, y座標, 幅, 高さ)

LoadQueue.fileloadイベントのリスナーに定めた描画の関数(draw())には、つぎのようなJavaScriptコードを加えよう。BoxBlurFilterのコンストラクタに渡すぼかし幅は大きめにした。そして、DisplayObject.cache()メソッドを呼出す矩形領域は、Bitmapインスタンスのイメージの領域となる。

function draw(eventObject) {

  blurBitmap = createBitmap(image, nX, nY);
  blurBitmap.filters = [new createjs.BoxBlurFilter(15, 15, 2)];
  blurBitmap.cache(0, 0, imageWidth, imageHeight);
  stage.update();
}

ぼかし幅をかなり大きくしたため、インスタンスのもとイメージがわからないくらいぼかされる図4⁠。script要素に書いたJavaScript全体は、つぎのコード2のとおりだ。

図4 インスタンスのイメージがかなりぼけた
図4 インスタンスのイメージがかなりぼけた
コード2 BoxBlurFilterクラスでインスタンスにぼかしフィルタをかける
var stage;
var blurBitmap;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  var canvasSize = new createjs.Point(canvasElement.width, canvasElement.height);
  stage = new createjs.Stage(canvasElement);
  var loader = new createjs.LoadQueue(false);
  loader.addEventListener("fileload", draw);
  loader.loadFile({
    src: "images/image.png", 
    data: canvasSize
    });
}
function draw(eventObject) {
  var image = eventObject.result;
  var canvasSize = eventObject.item.data;
  var imageWidth = image.width;
  var imageHeight = image.height;
  var nX = (canvasSize.x - imageWidth) / 2;
  var nY = (canvasSize.y - imageHeight) / 2;
  blurBitmap = createBitmap(image, nX, nY);
  blurBitmap.filters = [new createjs.BoxBlurFilter(15, 15, 2)];
  blurBitmap.cache(0, 0, imageWidth, imageHeight);
  stage.update();
}
function createBitmap(image, nX, nY) {
  var myBitmap = new createjs.Bitmap(image);
  myBitmap.x = nX;
  myBitmap.y = nY;
  stage.addChild(myBitmap);
  return myBitmap;
}

AlphaMaskFilterクラスでつくったアルファマスクをかける

つぎは、AlphaMaskFilterクラスを使う。インスタンスにアルファマスクをかける手順は、フィルタのお約束どおり3つだ。また、フィルタのクラスAlphaMaskFilterは、あらかじめscript要素に読込んでおく。

<script src="easeljs/filters/AlphaMaskFilter.js"></script>

もっとも、アルファマスクをかけるにはマスクが要る。そのため、マスク用のShapeインスタンスをつくる。そして、マスクする相手は、ぼかしたBitmapインスタンスではなく、新たにもうひとつオブジェクトを加える。そこで、コードにはつぎのような手直しをする。マスク用のShapeインスタンスと追加のBitmapオブジェクトは、それぞれ変数(wipingShapeとimageBitmap)に納めた。そして、Shapeインスタンスに描画する関数(wipe())を新たに定め、半透明(アルファ0.5)の塗りで円を描いている。

var imageBitmap;
var wipingShape;
var radius = 40;

function draw(eventObject) {

  wipingShape = new createjs.Shape();
  blurBitmap = createBitmap(image, nX, nY);
  blurBitmap.filters = [new createjs.BoxBlurFilter(15, 15, 2)];
  blurBitmap.cache(0, 0, imageWidth, imageHeight);
  imageBitmap = createBitmap(image, nX, nY);
  wipe();
}

function wipe(eventObject) {
  wipingShape.graphics
  .beginFill(createjs.Graphics.getRGB(0x0, 0.5))
  .drawCircle(50, 50, radius);

}

もう少し細かく見ていこう。Shapeインスタンスはアルファチャネルをマスクとして使うだけなので、ステージには置かない。そして、新たに加えたBitmapオブジェクトには、ぼかしたインスタンスと同じ画像イメージを与えた。しかも、後からStageオブジェクトの子として加えたため、ぼかしたインスタンスのうえに、もとのイメージが重ねられたことになる。

アルファマスクは、上に重ねたもとイメージのインスタンスにかける。すると、マスクのShapeオブジェクトは初め透明なので、上のBitmapインスタンスは見えなくなる。そこで、マスクのオブジェクトに半透明の円を描けば、上のインスタンスのその部分が半透明で表れるという段取りだ。

Shapeインスタンスに円を描く関数についても、少し補っておく。Shape.graphicsプロパティで描画の対象となるGraphicsオブジェクトが得られる。その参照を変数に入れて、Graphicsクラスのメソッドを呼び出すことにより、さまざまなベクターシェイプが描ける。第4回コード1は、この書き方でリングのシェイプをつくった。

var myGraphics = DisplayObjectオブジェクト.graphics;
myGraphics.Graphicsクラスのメソッド;
myGraphics.Graphicsクラスのメソッド;
…;

ところが、前掲コードではGraphicsオブジェクトを変数に入れず、そのまま続けてGraphics.beginFill()メソッドを呼び出した。実は、Graphicsクラスのメソッドは、基本的に呼出したGraphicsオブジェクトを返す。そのため、さらに続けてGraphics.drawCircle()メソッドが呼び出せる。このように、メソッドをいくらでも書連ねることができるのだ。

DisplayObjectオブジェクト.graphics.Graphicsクラスのメソッド.Graphicsクラスのメソッド.…;

ただ、1行で書くと何をやっているのか見にくいので、前掲のステートメントはドット(.)の前で改行している。なお、Graphics.getRGB()メソッドを用いると、カラーの整数とアルファのふたつの引数値でカラーを定める文字列が得られる。

アルファマスクをかける関数(updateCacheImage())は、つぎのように別に定めた。また、DisplayObject.cache()メソッドでフィルタの演算結果をキャッシュに描画する関数(updateCache())も分けた。引数にはブール(論理)値とキャッシュするインスタンスを渡す。

AlphaMaskFilter()コンストラクタは、マスクするイメージを引数にとる。具体的にはImageオブジェクトまたはCanvasで与える。つまり、Shapeオブジェクトはそのままではアルファマスクにはできない。そこで、DisplayObject.cache()メソッドを呼び出して(updateCache()⁠⁠、Canvasにキャッシュする。すると、その参照がShape.cacheCanvasプロパティで得られるので、それをAlphaMaskFilter()コンストラクタの引数に渡せばよい。

new AlphaMaskFilter(マスクイメージ)

あとは、フィルタオブジェクト(maskFilter)を配列に入れてDisplayObject.filtersプロパティに加え、別に定めた関数(updateCache())からDisplayObject.cache()メソッドでキャッシュするという手順どおりだ。なお、キャッシュする矩形領域の幅と高さはPointオブジェクトで変数(imageSize)に納めた。

var imageSize = new createjs.Point();

function draw(eventObject) {
  var image = eventObject.result;

  var imageWidth = imageSize.x = image.width;
  var imageHeight = imageSize.y = image.height;

}

function wipe(eventObject) {

  updateCacheImage(false);
}

function updateCacheImage(update) {
  updateCache(update, wipingShape);
  var maskFilter = new createjs.AlphaMaskFilter(wipingShape.cacheCanvas);
  imageBitmap.filters = [maskFilter];
  updateCache(update, imageBitmap);
  stage.update();
}
function updateCache(update, instance) {
  if (update) {

  } else {
    instance.cache(0, 0, imageSize.x, imageSize.y);
  }
}

これで、半透明のアルファマスクがShapeインスタンス(wipingShape)にもとづいてつくられ、AlphaMaskFilterクラスによりもとイメージをもつ前面のBitmapインスタンスに加えられる。その結果、半透明の円形の領域でぼかしが拭われたような表現になった図5⁠。

図5 半透明の円形でアルファマスクがかかってぼけが薄らぐ
図5 半透明の円形でアルファマスクがかかってぼけが薄らぐ

クリックした位置にアルファマスクを加える

もう一歩進めて、インタラクションを加えよう。円形のアルファマスクを、マウスクリックした位置に加える。つぎのコードのように、マスクを描く関数(wipe())Stage.stagemousedownイベントのリスナーに定める。リスナー関数は、Stage.mouseXStage.mouseYプロパティでクリックした座標を調べ、その座標が中心となるように円形のマスクを描く。

クリックするたびに、マスクには円形の塗りが加わる。すると、アルファマスクのキャッシュも描き直さなければならない。だが、新たなCanvasを増やすには及ばない。そういうときは、DisplayObject.updateCache()メソッドを呼び出すと、すでにあるCanvasの描画が改められる。

Canvasにキャッシュする関数(updateCache())は、第1引数のブール値でDisplayObject.updateCache()DisplayObject.cache()のどちらのメソッドを呼び出すのかを選べるようにした。したがって、始めは引数falseで新たなCanvasをつくり、その後はtrueにして描画を更新すればよい。

function draw(eventObject) {

  stage.addEventListener("stagemousedown", wipe);
  wipingShape = new createjs.Shape();

  // wipe();
  updateCacheImage(false);
}

function wipe(eventObject) {
  var mouseX = stage.mouseX;
  var mouseY = stage.mouseY;
  wipingShape.graphics
  .beginFill(createjs.Graphics.getRGB(0x0, 0.5))
  // .drawCircle(50, 50, radius);
  .drawCircle(mouseX, mouseY, radius);
  // updateCacheImage(false);
  updateCacheImage(true);
}

function updateCacheImage(update) {
  updateCache(update, wipingShape);
  var maskFilter = new createjs.AlphaMaskFilter(wipingShape.cacheCanvas);
  imageBitmap.filters = [maskFilter];
  updateCache(update, imageBitmap);
  stage.update();
}
function updateCache(update, instance) {
  if (update) {
    instance.updateCache();
  } else {
    instance.cache(0, 0, imageSize.x, imageSize.y);
  }
}

前述のコードで試してみると、クリックした座標と円形のマスクの中心がずれてしまう図6⁠。なぜなら、マウス座標はステージの基準点にもとづいて定められる。ところが、フィルタはインスタンスの中の座標で演算されるからだ。インスタンスをステージの真ん中に置くため、座標を動かした。その座標の差がマスクの位置をずらしてしまう。

図6 ステージの基準点とインスタンスの座標の差がマスクの位置をずらす
図6 ステージの基準点とインスタンスの座標の差がマスクの位置をずらす

そこで、インスタンスを描く関数(draw())の中で、真ん中に位置合わせした座標をPointオブジェクトにして変数(bitmapPoint)に納めた。そして、マスクを描く関数(wipe())は、円の中心座標をその分ずらす。新たに定めた関数(getMousePoint())は、インスタンスの基準点から見たマウスポインタの座標を返すようにした。

var bitmapPoint = new createjs.Point();

function draw(eventObject) {

  var nX = bitmapPoint.x = (canvasSize.x - imageWidth) / 2;
  var nY = bitmapPoint.y = (canvasSize.y - imageHeight) / 2;
  stage.addEventListener("stagemousedown", wipe);

}

function wipe(eventObject) {
  // var mouseX = stage.mouseX;
  // var mouseY = stage.mouseY;
  var mousePoint = getMousePoint();
  var mouseX = mousePoint.x;
  var mouseY = mousePoint.y;
  wipingShape.graphics
  .beginFill(createjs.Graphics.getRGB(0x0, 0.5))
  .drawCircle(mouseX, mouseY, radius);

}
function getMousePoint() {
  var mouseX = stage.mouseX - bitmapPoint.x;
  var mouseY = stage.mouseY - bitmapPoint.y;
  return new createjs.Point(mouseX, mouseY);
}

これで、クリックした座標を中心に円形のアルファマスクが加えられる図7⁠。script要素全体は、つぎのコード3のとおりだ。半透明の塗りを重ねると、アルファ値は高まる。それがわかりやすいように、塗りのアルファを少し下げた。また、ぼけたイメージのインスタンス(blurBitmap)も、アルファを下げている。そうすると、手前のインスタンス(imageBitmap)が濃くなるにつれ、アルファも高まって見えるからだ。

図7 クリックした座標を中心に円形のアルファマスクが重なる
図7 クリックした座標を中心に円形のアルファマスクが重なる
コード3 クリックした座標を中心に円形のアルファマスクを塗り重ねてインスタンスに加える
<script src="http://code.createjs.com/easeljs-0.6.1.min.js"></script>
<script src="http://code.createjs.com/preloadjs-0.3.1.min.js"></script>
<script src="easeljs/filters/AlphaMaskFilter.js"></script>
<script src="easeljs/filters/BoxBlurFilter.js"></script>
<script>
var stage;
var wipingShape;
var imageBitmap;
var blurBitmap;
var imageSize = new createjs.Point();
var radius = 40;
var bitmapPoint = new createjs.Point();
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  var canvasSize = new createjs.Point(canvasElement.width, canvasElement.height);
  stage = new createjs.Stage(canvasElement);
  var loader = new createjs.LoadQueue(false);
  loader.addEventListener("fileload", draw);
  loader.loadFile({
    src: "images/image.png", 
    data: canvasSize
    });
}
function draw(eventObject) {
  var image = eventObject.result;
  var canvasSize = eventObject.item.data;
  var imageWidth = imageSize.x = image.width;
  var imageHeight = imageSize.y = image.height;
  var nX = bitmapPoint.x = (canvasSize.x - imageWidth) / 2;
  var nY = bitmapPoint.y = (canvasSize.y - imageHeight) / 2;
  stage.addEventListener("stagemousedown", wipe);
  wipingShape = new createjs.Shape();
  blurBitmap = createBitmap(image, nX, nY);
  blurBitmap.filters = [new createjs.BoxBlurFilter(15, 15, 2)];
  blurBitmap.cache(0, 0, imageWidth, imageHeight);
  blurBitmap.alpha = 0.8;
  imageBitmap = createBitmap(image, nX, nY);
  updateCacheImage(false);
}
function createBitmap(image, nX, nY) {
  var myBitmap = new createjs.Bitmap(image);
  myBitmap.x = nX;
  myBitmap.y = nY;
  stage.addChild(myBitmap);
  return myBitmap;
}
function wipe(eventObject) {
  var mousePoint = getMousePoint();
  var mouseX = mousePoint.x;
  var mouseY = mousePoint.y;
  wipingShape.graphics
  .beginFill(createjs.Graphics.getRGB(0x0, 0.15))
  .drawCircle(mouseX, mouseY, radius);
  updateCacheImage(true);
}
function getMousePoint() {
  var mouseX = stage.mouseX - bitmapPoint.x;
  var mouseY = stage.mouseY - bitmapPoint.y;
  return new createjs.Point(mouseX, mouseY);
}
function updateCacheImage(update) {
  updateCache(update, wipingShape);
  var maskFilter = new createjs.AlphaMaskFilter(wipingShape.cacheCanvas);
  imageBitmap.filters = [maskFilter];
  updateCache(update, imageBitmap);
  stage.update();
}
function updateCache(update, instance) {
  if (update) {
    instance.updateCache();
  } else {
    instance.cache(0, 0, imageSize.x, imageSize.y);
  }
}
</script>

今回はここまでにしよう。コードはjsdo.itにも掲げた。次回は、お題と同じように、ドラッグでアルファマスクを描く。

おすすめ記事

記事・ニュース一覧