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

第9回ドラッグでアルファマスクを描く

前回は、BoxBlurFilterによりぼかしたイメージのインスタンスに、AlphaMaskFilterでマスクしたもとのイメージのインスタンスを重ね、マウスクリックした部分のアルファを上げていくという表現までつくった第8回コード3⁠。参考までに、jsdo.itのサンプルを掲げておく。

今回は、ドラッグでアルファマスクを描くようにしよう。目指すは、EaselJSサイトのデモALPHAMASK FILTERだ。

マウスポインタの座標を直線で結ぶ

ドラッグしている間マウスポインタの座標を調べ、アルファマスクのShapeインスタンスにその軌跡を描く。座標は直線で結ぶことにしよう。直線を引くのに使うメソッドは、Graphics.moveTo()Graphics.lineTo()だ。まず、描き始めの座標をGraphics.moveTo()メソッドによって決める。その後、Graphics.lineTo()メソッドで座標を直線で結んでいく。また、線のスタイルや色は、それぞれGraphics.setStrokeStyle()Graphics.beginStroke()メソッドで定める(⁠EaselJSのGraphicクラスで2次ベジエ曲線を描く参照⁠⁠。

Graphicsオブジェクト
.setStrokeStyle(線の太さ, 線の端, 線の角)
.beginStroke(線のカラー)
.moveTo(x座標, y座標)
.lineTo(x座標, y座標)
.lineTo(x座標, y座標)
…;

スクリプトは前回のコード3に手を入れていく。マウスイベントの扱いは、画像を読込み終えたLoadQueue.fileloadイベントのリスナー関数(draw())に加える。ステージ上でマウスボタンを押すStage.stagemousedownイベントでドラッグが始まり(startWipe()⁠⁠、ボタンを放すStage.stagemouseupイベントによりドラッグは終わる。ドラッグしている間のアルファマスクのアニメーションは、Ticker.tickイベントのリスナー(wipe())が行う。

function draw(eventObject) {

  // stage.addEventListener("stagemousedown", wipe);
  stage.addEventListener("stagemousedown", startWipe);
  stage.addEventListener("stagemouseup", stopWipe);

  createjs.Ticker.addEventListener("tick", wipe);
}

リスナー関数は前述3つのイベントごとに役割を分けて、以下のように書替える。Stage.stagemousedownイベントのリスナー(startWipe())は、描き始めのマウスポインタ座標をPointオブジェクトの変数(mousePoint)に納める。また、ドラッグが始まったことを示す変数(isDrawing)はtrueにした。そして、アルファマスクを描くGraphicsオブジェクトには、Graphics.setStrokeStyle()メソッドで線のスタイルだけ定めた。マスクそのものは、ドラッグの関数(wipe())が描く。なお、メソッドの第2および第3引数で、線の端と角を丸く("round")した(引数について詳しくは前出「EaselJSのGraphicクラスで2次ベジエ曲線を描く」参照⁠⁠。

ドラッグでマスクを描く関数(wipe())は、Ticker.tickイベントのリスナーに定めた。ドラッグしていることは、変数(isDrawing)if条件で確かめてから、描画に入る。マウスポインタの座標は予めローカル変数(mouseXとmouseY)にとっておく。そして、アルファマスクのGraphicsオブジェクトにアルファつきのカラーを定めて、マウスポインタの前の座標から今の座標までの直線を描いている。そして、つぎの線描のために、マウスポインタ座標はPointオブジェクトの変数(oldPoint)に納めた。

マウスボタンを放すStage.stagemouseupイベントでドラッグは終わる。そこで、ドラッグしていることを示す変数にfalseを与える。これら3つのイベントリスナーを定めることにより、アルファマスクがドラッグで描ける図1⁠。

var radius = 4;
var oldPoint = new createjs.Point();
var isDrawing;

/*
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 startWipe(eventObject) {
  var mousePoint = getMousePoint();
  oldPoint.x = mousePoint.x;
  oldPoint.y = mousePoint.y;
  isDrawing = true;
  wipingShape.graphics
  .setStrokeStyle(radius * 2, "round", "round");
}
function wipe(eventObject) {
  if (isDrawing) {
    var mousePoint = getMousePoint();
    var mouseX = mousePoint.x;
    var mouseY = mousePoint.y;
    wipingShape.graphics
    .beginStroke(createjs.Graphics.getRGB(0x0, 0.4))
    .moveTo(oldPoint.x, oldPoint.y)
    .lineTo(mouseX, mouseY);
    oldPoint.x = mouseX;
    oldPoint.y = mouseY;
    updateCacheImage(true);
  }
}
function stopWipe(event) {
  isDrawing = false;
}
図1 アルファマスクがドラッグで描かれる
図1 アルファマスクがドラッグで描かれる

script要素に書いたJavaScript全体は、つぎのコード1のとおりだ。ただし、前掲のコードではマスクに軌跡が正しく描かれているかを確かめるため、線は細くしてアルファも高めにした図1⁠。コード1は、目指す表現に合わせて線は太く、アルファも下げてある図2⁠。

コード1 ドラッグする軌跡でアルファマスクを描く
var stage;
var wipingShape;
var imageBitmap;
var blurBitmap;
var imageSize = new createjs.Point();
var radius = 10;
var bitmapPoint = new createjs.Point();
var oldPoint = new createjs.Point();
var isDrawing;
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", startWipe);
  stage.addEventListener("stagemouseup", stopWipe);
  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);
  createjs.Ticker.addEventListener("tick", wipe);  
}
function createBitmap(image, nX, nY) {
  var myBitmap = new createjs.Bitmap(image);
  myBitmap.x = nX;
  myBitmap.y = nY;
  stage.addChild(myBitmap);
  return myBitmap;
}
function startWipe(eventObject) {
  var mousePoint = getMousePoint();
  oldPoint.x = mousePoint.x;
  oldPoint.y = mousePoint.y;
  isDrawing = true;
  wipingShape.graphics
  .setStrokeStyle(radius * 2, "round", "round");
}
function wipe(eventObject) {
  if (isDrawing) {
    var mousePoint = getMousePoint();
    var mouseX = mousePoint.x;
    var mouseY = mousePoint.y;
    wipingShape.graphics
    .beginStroke(createjs.Graphics.getRGB(0x0, 0.15))
    .moveTo(oldPoint.x, oldPoint.y)
    .lineTo(mouseX, mouseY);
    oldPoint.x = mouseX;
    oldPoint.y = mouseY;
    updateCacheImage(true);
  }
}
function stopWipe(event) {
  isDrawing = false;
}
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);
  }
}
図2 マスクに描く線は太くしてアルファも下げた
図2 マスクに描く線は太くしてアルファも下げた

このコードでいいのか?

前掲のコード1を見て、このままでいいのか、もう少し手を加えたほうがすっきりするのでは、と考えた読者もいるだろう。最近のTVドラマの主人公がよく口にした台詞で答えるなら、いいね~と申し上げたい。ふたつ押さえておこう。

ひとつ目は、Ticker.tickイベントのリスナー(wipe())だ。マウスをドラッグしているかどうかは変数(wipe)にブール値を入れ(いわゆる「フラグ⁠⁠、ドラッグしていないときは何のアニメーションもしない。しかし、Ticker.tickイベントそのものは、何もしなくても起こり続ける。それなら、そもそもイベントリスナーを除いてしまって、マウスボタンが押されたときに改めてリスナーを加えれば無駄がない。

まったくそのとおりだ。けれど、実はこの後マウスポインタとともに動くカーソル替わりのインスタンスを加える。このインスタンスは、マウスボタンに関わりなくマウスポインタの後を追い続ける。そのため、Ticker.tickイベントのリスナー関数(wipe())は、そのまま残した。

もうひとつは、Graphicsオブジェクトに線を描く処理の切り分けだ。Stage.stagemousedownイベントのリスナー(startWipe())は、描く線のスタイルをGraphics.setStrokeStyle()メソッドで決めた。だが、Graphics.beginStroke()メソッドによるカラーの指定や、Graphics.moveTo()メソッドの座標決めも、Stage.stagemousedownイベントのリスナーで初めにやってしまえば済むのではないだろうか。すると、つぎのようにTicker.tickイベントのリスナー(wipe())は、Graphics.lineTo()メソッドでひたすら直線を引き続ければよい。

// var oldPoint = new createjs.Point();

function startWipe(eventObject) {
  var mousePoint = getMousePoint();
  // oldPoint.x = mousePoint.x;
  // oldPoint.y = mousePoint.y;
  isDrawing = true;
  wipingShape.graphics
  .setStrokeStyle(radius * 2, "round", "round")
  .beginStroke(createjs.Graphics.getRGB(0x0, 0.4))
  .moveTo(mousePoint.x, mousePoint.y);
}
function wipe(eventObject) {
  if (isDrawing) {
    var mousePoint = getMousePoint();
    var mouseX = mousePoint.x;
    var mouseY = mousePoint.y;
    wipingShape.graphics
    // .beginStroke(createjs.Graphics.getRGB(0x0, 0.4))
    // .moveTo(oldPoint.x, oldPoint.y)
    .lineTo(mouseX, mouseY);
    // oldPoint.x = mouseX;
    // oldPoint.y = mouseY;
    updateCacheImage(true);
  }
}

鋭い指摘だ。筆者も初めはそう考えた。しかし、このコードでは、アルファマスクにマウスポインタの軌跡は正しく描けるものの、表現が意図どおりにならなかった。具体的には、アルファのかかった線を塗り重ねてもアルファが高まらず、もとイメージが濃くならないのだ。重なりのアルファを上げるためには、Graphics.beginStroke()メソッドを呼出して仕切り直さないといけない。すると、描き始めの座標も、改めてGraphics.moveTo()メソッドで決めることになる。

ということで、前掲コード1はこのまま生かすことにする。ただ、書いたコードを見直して、推敲を重ねることはよい習慣といえる。

カーソル替わりのインスタンスをマウスポインタに合わせて動かす

つぎは、カーソル替わりのShapeインスタンスをつくり、マウスポインタに合わせて動かしたい。アルファマスクに描かれる線の太さと同じ円形にすれば、描かれるマスクが予想しやすくなる。また、マウスポインタそのものも、矢印からかたちを変えたい。

カーソル替わりのShapeインスタンス(cursor)は新たに定める関数(createCursor())でつくり、描画の関数(draw())から呼び出す。インスタンスのアルファは半透明にした。そして、インスタンスにポインタを重ねたときのカーソルのかたちは、DisplayObject.cursorプロパティで定める。"pointer"は、指差しカーソルだ。

var cursor;

function draw(eventObject) {

  createCursor();

}

function createCursor() {
  cursor = new createjs.Shape();
  cursor.graphics
  .beginFill("white")
  .drawCircle(0, 0, radius);
  cursor.cursor = "pointer";
  cursor.alpha = 0.3;
  stage.addChild(cursor);
}

インスタンス(cursor)のアニメーションは、Ticker.tickイベントのリスナー関数(wipe())で扱う。マウスポインタの座標を調べて、インスタンスの位置をそこに合わせれればよい。ただし、マウスをドラッグしないと、キャッシュを書替える関数(updateCacheImage())が呼ばれない。この関数はStage.update()メソッドの呼出しを含む。そこで、else文にStage.update()メソッドのステートメントを加えた。

function draw(eventObject) {

function wipe(event) {
  cursor.x = stage.mouseX;
  cursor.y = stage.mouseY;
  if (isDrawing) {

  } else {
    stage.update();
  }
}

これでカーソル替わりのインスタンス(cursor)は、つねにマウスポインタに合わせて動く。ところが、ポインタのカーソルが矢印のまま変わらない。実は、ポインタをインスタンスに重ねたとき、そのカーソルを変えるには、Stage.enableMouseOver()メソッドを呼び出さなければならない[1]⁠。

function draw(eventObject) {

  stage.enableMouseOver();

}

これで、カーソル替わりのインスタンス(cursor)はマウスポインタに合わせて動き、ポインタのかたちは矢印から指差しカーソルに変わる図3⁠。script要素に書いたJavaScriptの全体をコード2にまとめた。

図3 半透明のインスタンスがポインタに合わせて動き指差しカーソルに変わる
図3 半透明のインスタンスがポインタに合わせて動き指差しカーソルに変わる
コード2 インスタンスをポインタに合わせて動かして指差しカーソルに変える
var stage;
var wipingShape;
var imageBitmap;
var blurBitmap;
var imageSize = new createjs.Point();
var radius = 10;
var bitmapPoint = new createjs.Point();
var oldPoint = new createjs.Point();
var isDrawing;
var cursor;
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", startWipe);
  stage.addEventListener("stagemouseup", stopWipe);
  stage.enableMouseOver();
  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);
  createCursor();
  updateCacheImage(false);
  createjs.Ticker.addEventListener("tick", wipe);  
}
function createBitmap(image, nX, nY) {
  var myBitmap = new createjs.Bitmap(image);
  myBitmap.x = nX;
  myBitmap.y = nY;
  stage.addChild(myBitmap);
  return myBitmap;
}
function createCursor() {
  cursor = new createjs.Shape();
  cursor.graphics
  .beginFill("white")
  .drawCircle(0, 0, radius);
  cursor.cursor = "pointer";
  cursor.alpha = 0.3;
  stage.addChild(cursor);
}
function startWipe(eventObject) {
  var mousePoint = getMousePoint();
  oldPoint.x = mousePoint.x;
  oldPoint.y = mousePoint.y;
  isDrawing = true;
  wipingShape.graphics
  .setStrokeStyle(radius * 2, "round", "round");
}
function wipe(eventObject) {
  cursor.x = stage.mouseX;
  cursor.y = stage.mouseY;
  if (isDrawing) {
    var mousePoint = getMousePoint();
    var mouseX = mousePoint.x;
    var mouseY = mousePoint.y;
    wipingShape.graphics
    .beginStroke(createjs.Graphics.getRGB(0x0, 0.15))
    .moveTo(oldPoint.x, oldPoint.y)
    .lineTo(mouseX, mouseY);
    oldPoint.x = mouseX;
    oldPoint.y = mouseY;
    updateCacheImage(true);
  } else {
    stage.update();
  }
}
function stopWipe(event) {
  isDrawing = false;
}
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);
  }
}

これで、ひとまずお題の動きはできた。jsdo.itにもサンプルを掲げた。次回は、EaselJSサイトのデモ「ALPHAMASK FILTER」のコードと違いを見比べながら、さらに知識を深めよう。

おすすめ記事

記事・ニュース一覧