サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

鉛筆と消しゴムによる描画

ID: FN1508001 Technique: HTML5 and JavaScript Library: EaselJS 0.8.1

Grant Skinner氏がjsfiddle.netに、CreateJSによる鉛筆ツールと消しゴムツールのような描画の仕方の作例を公開されました(サンプル001)。このコードをもとにして、仕組みを解説するとともに、若干の改善を加えてみます。

サンプル001▪️CreateJS Draw/erase example


01 鉛筆でドラッグしたときに線を描く

まずは、マウスドラッグで線を描く鉛筆のコードから書きましょう。ドラッグの処理は、マウスの(1)ボタンを押して始まり、(2)押したまま動かすとドラッグ、(3)ボタンを放して終わることになります。これら3つのマウスイベントのリスナーを使って、つぎのように組立てるのがお約束です。

  1. マウスボタンを押したイベントリスナー登録
  2. ボタンを押したイベントリスナーの処理
    • ドラッグのイベントリスナー登録
    • ボタンを放したイベントリスナー登録
    • ドラッグ開始設定
  3. ドラッグのイベントリスナーの処理
    • マウス座標の取得
    • ドラッグ開始設定にもとづくドラッグの処理
  4. ボタンを放したイベントリスナーの処理
    1. ドラッグのイベントリスナー削除
    2. ボタンを放したイベントリスナー削除

ステージ(Stageオブジェクト)に対する3つのマウスイベントは、Stage.stagemousedownStage.stagemousemoveおよびStage.stagemouseupになります。鉛筆でドラッグの軌跡に線を描くクスリプトは、つぎのコード001のとおりです(なお、body要素のonload属性にコールバックとしてinitializeを加え、Canvas要素にはid属性"canvas"が与えてあります)。ドラッグ開始設定としては、マウスボタンが押されたときのマウス座標を変数(lastPoint)に収めます。そして、ドラッグのイベントリスナー(draw())により、変数のマウス座標と今のマウス座標との間に線を引き、変数の座標値を改めました。

コード001■鉛筆でドラッグする軌跡に線を描く
  1. var stage;
  2. var art;
  3. var lastPoint = new createjs.Point();
  4. var color = createjs.Graphics.getHSL(85, 50, 50);
  5. function initialize() {
  6.   var canvas = document.getElementById("canvas");
  7.   stage = new createjs.Stage(canvas);
  8.   art = stage.addChild(new createjs.Shape());
  9.   stage.addEventListener("stagemousedown", startDraw);
  10. }
  11. function startDraw(eventObject) {
  12.   stage.addEventListener("stagemousemove", draw);
  13.   stage.addEventListener("stagemouseup", endDraw);
  14.   lastPoint.setValues(eventObject.stageX, eventObject.stageY);
  15. }
  16. function draw(eventObject) {
  17.   art.graphics
      .setStrokeStyle(10, 1)
      .beginStroke(color)
      .moveTo(lastPoint.x, lastPoint.y)
      .lineTo(eventObject.stageX, eventObject.stageY);
  18.   lastPoint.setValues(eventObject.stageX, eventObject.stageY);
  19.   stage.update();
  20. }
  21. function endDraw(eventObject) {
  22.   stage.removeEventListener("stagemousemove", draw);
  23.   stage.removeEventListener("stagemouseup", endDraw);
  24. }

02 オブジェクトへの描画をキャッシュする

DisplayObject.cache()メソッドを用いると、オブジェクトがもつ描画イメージを内部的に新たなCanvasにキャッシュしてくれます。メソッドの4つの引数は矩形領域の値です。とくに複雑なイメージが描かれていたり子オブジェクトを多く含んでいて、それが変わらないときは、オブジェクトの描画を改めずに済むので負荷が減らせます。

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

オブジェクトのもつイメージが変わったときは、改めてDisplayObject.cache()DisplayObject.updateCache()メソッドを呼び出します。DisplayObject.updateCache()は、すでにDisplayObject.cache()メソッドでキャッシュされた領域を描き直します。引数がないと、矩形領域の描画は予め消されます。引数に渡すのは、Canvasに定めることのできるglobalCompositeOperationプロパティ値の文字列です。

DisplayObjectオブジェクト.updateCache(合成処理)

前掲コード001の作例にキャッシュを用いるなら、書き足す線を加えていけばよいでしょう(後掲コード002)。このとき、DisplayObject.updateCache()メソッドに渡す引数は"source-over"です(第23行目)。それまで描いた線はキャッシュに残っていますので、Graphicsオブジェクトの描画はGraphics.clear()メソッドでその都度消して、書き足す線だけ描きます(第18行目)。

  1. function initialize() {
  1.   art.cache(0, 0, canvas.width, canvas.height);
  1. }
  1. function draw(eventObject) {
  2.   art.graphics.clear()
      .setStrokeStyle(10, 1)
      .beginStroke(color)
      .moveTo(lastPoint.x, lastPoint.y)
      .lineTo(eventObject.stageX, eventObject.stageY);
  3.   art.updateCache("source-over");
  1. }

前掲コード001をキャッシュから描くように書き換えたのが、つぎのコード002です。ドラッグで線を描くという動きは、コード001とまったく変わりません。けれど、DisplayObject.updateCache()メソッドには引数に合成処理が与えられるので、その値により描画のされ方も変わってきます。

コード002■ドラッグした軌跡にキャッシュで線を描く
  1. var stage;
  2. var art;
  3. var lastPoint = new createjs.Point();
  4. var color = createjs.Graphics.getHSL(85, 50, 50);
  5. function initialize() {
  6.   var canvas = document.getElementById("canvas");
  7.   stage = new createjs.Stage(canvas);
  8.   art = stage.addChild(new createjs.Shape());
  9.   art.cache(0, 0, canvas.width, canvas.height);
  10.   stage.addEventListener("stagemousedown", startDraw);
  11. }
  12. function startDraw(eventObject) {
  13.   stage.addEventListener("stagemousemove", draw);
  14.   stage.addEventListener("stagemouseup", endDraw);
  15.   lastPoint.setValues(eventObject.stageX, eventObject.stageY);
  16. }
  17. function draw(eventObject) {
  18.   art.graphics.clear()
      .setStrokeStyle(10, 1)
      .beginStroke(color)
      .moveTo(lastPoint.x, lastPoint.y)
      .lineTo(eventObject.stageX, eventObject.stageY);
  19.   art.updateCache("source-over");
  20.   lastPoint.setValues(eventObject.stageX, eventObject.stageY);
  21.   stage.update();
  22. }
  23. function endDraw(eventObject) {
  24.   stage.removeEventListener("stagemousemove", draw);
  25.   stage.removeEventListener("stagemouseup", endDraw);
  26. }

03 消しゴムでドラッグした跡を消す

DisplayObject.updateCache()メソッドの引数に"destination-out"を渡すと、描き加えるイメージは透明になりつつ、キャッシュに描かれたイメージと重なる部分が除かれます。つまり、まさに消しゴムの描画になるのです。鉛筆と消しゴムが切り替えられるように、body要素にはつぎのようにふたつのinput要素を加えます。type属性を"radio"にしましたので、ふたつはラジオボタンとして働きます。

<input type="radio" name="action" value="draw" checked="checked"> draw
<input type="radio" name="action" value="erase" id="erase"> erase

ラジオボタンによりドラッグ操作を鉛筆と消しゴムに切り替えるよう書き替えたのが、後掲コード003です。どちらのラジオボタンが選ばれているか確かめて、合成処理の変数(compositeOperation)値を決めています(第20行目)。そして、その変数をDisplayObject.updateCache()メソッドの引数に渡しました(第21行目)。

  1. function draw(eventObject) {
  2.   var compositeOperation = erase.checked ? "destination-out" : "source-over";
      // art.updateCache("source-over");
  1.   art.updateCache(compositeOperation);
  1. }

さらにもうひとつ、Grant Skinner氏の作例にならって、鉛筆の線の色をドラッグし始めるたびに切り替えました(getNextColor())。HSLカラーの文字列から色相(hue)の数値を取り出して一定値(85)を加え、改めてHSLの文字列にしています(第35〜36行目)。

    // var color = createjs.Graphics.getHSL(85, 50, 50);
  1. var color = createjs.Graphics.getHSL(0, 50, 50);
  1. function startDraw(eventObject) {
  1.   color = getNextColor();
  2. }
  1. function getNextColor() {
  2.   var hue = parseInt(color.split("(")[1].split(",")[0]) + 85;
  3.   return createjs.Graphics.getHSL(hue, 50, 50);
  4. }

これで、ラジオボタンにより鉛筆と消しゴムが切り替わり、ドラッグで描画できるようになりました。また、ドラッグするたびに鉛筆の線の色が変わります。Grant Skinner氏の作例と同じ動きができ上がりました。

コード003■ドラッグで鉛筆と消しゴムの描画を行う
  1. var stage;
  2. var art;
  3. var lastPoint = new createjs.Point();
  4. var color = createjs.Graphics.getHSL(0, 50, 50);
  5. function initialize() {
  6.   var canvas = document.getElementById("canvas");
  7.   stage = new createjs.Stage(canvas);
  8.   art = stage.addChild(new createjs.Shape());
  9.   art.cache(0, 0, canvas.width, canvas.height);
  10.   stage.addEventListener("stagemousedown", startDraw);
  11. }
  12. function startDraw(eventObject) {
  13.   stage.addEventListener("stagemousemove", draw);
  14.   stage.addEventListener("stagemouseup", endDraw);
  15.   lastPoint.setValues(eventObject.stageX, eventObject.stageY);
  16.   color = getNextColor();
  17. }
  18. function draw(eventObject) {
  19.   var compositeOperation = erase.checked ? "destination-out" : "source-over";
  20.   art.graphics.clear()
      .setStrokeStyle(10, 1)
      .beginStroke(color)
      .moveTo(lastPoint.x, lastPoint.y)
      .lineTo(eventObject.stageX, eventObject.stageY);
  21.   art.updateCache(compositeOperation);
  22.   lastPoint.setValues(eventObject.stageX, eventObject.stageY);
  23.   stage.update();
  24. }
  25. function endDraw(eventObject) {
  26.   stage.removeEventListener("stagemousemove", draw);
  27.   stage.removeEventListener("stagemouseup", endDraw);
  28. }
  29. function getNextColor() {
  30.   var hue = parseInt(color.split("(")[1].split(",")[0]) + 85;
  31.   return createjs.Graphics.getHSL(hue, 50, 50);
  32. }

04 ウィンドウ外でドラッグを終えたときの問題

Grant Skinner氏の前掲サンプル001をしばらく試していると、気になる動きがあります。ドラッグで線を描いて、マウスボタンをウィンドウ(フレーム)の外で放すと、描画が終わりません(図001)。マウスボタンを放したままCanvasの中にマウスポインタを戻しても、線が描かれてしまいます。これは、CreateJSでStage.stagemouseupイベントがウィンドウの外では取れないからです。そのため、Stage.stagemousemoveイベントのリスナーが削除されず、線描され続けることになります。

図001■ウィンドウの外でマウスボタンを放すと線描が終わらない
図001左
図001右

もうひとつ別の問題もあります。前述の線描が終わらない状態で改めてドラッグし直すと、Stage.stagemousedownイベントのリスナーが呼び出されて線の色は変わります。けれど、今度はステージ上でマウスボタンを放しても、そのまま線が描かれ続けてしまうのです。実はこのとき、Stage.stagemouseupイベントは起こっています。けれど、サンプル001は、つぎのようにEventDispatcher.on()メソッドでイベントリスナーを加えていることに注意しなければなりません。

var x, y, listener, color, hue=0;

function startDraw(evt) {
  listener = stage.on("stagemousemove", draw, this);

}

function endDraw(evt) {
  stage.off("stagemousemove", listener);

}

図002■ステージ上でドラッグし直しても線が描かれ続ける
図002

EventDispatcher.on()メソッドは、第3引数のオブジェクトをスコープとした新たな関数をつくり、それをイベントリスナーに加えます。ですから、ステージ上で改めてドラッグし直すとまた別のリスナー関数がつくられ、それが変数(listener)に収められます。ドラッグを終えたときリスナーから除かれるのは、その新しい方の関数です。ウィンドウ外でマウスを放したことによって残った古いリスナー関数はそのまま居座ってしまうのです。

この作例では、あえてEventDispatcher.on()メソッドを用いなければならない理由はありません。そのため、前掲コード003はEventDispatcher.addEventListener()メソッドでイベントリスナーを加えました(第13〜14行目)。これなら、Stage.stagemouseupイベントさえ起これば、リスナー関数は確実に除かれます。

初めの問題に戻ります。ウィンドウの外でマウスボタンを放すイベントは受け取るのが難しそうです。そこで、マウスポインタがステージ外にでたら、描画を終えてしまうことにします。ただ、ステージの外ではStage.stagemousemoveイベントも起こりません。けれども、document.onmousemoveイベントなら受け取れます。それなら、Stage.stagemousemovedocument.onmousemoveイベントの起こった回数を比べれば、ステージの外かどうかが確かめられます。

後掲コード004は、ふたつのイベントの回数を配列の変数(counter)に収めました(第5行目)。そして、それぞれのイベントが起こるたびに値を加えます(第27および第40行目)。そのうえで、document.onmousemoveイベントハンドラ(checkOutside())は、ふたつの回数の差が一定値(1)を超えたら、Stage.stagemouseupイベントのリスナー関数(endDraw())を直に呼び出しています(第41〜43行目)。

  1. var counter = [0, 0];
  1. function startDraw(eventObject) {
  1.   counter[0] = 0;
  2.   counter[1] = 0;
  3.   document.onmousemove = checkOutside;
  4. }
  1. function draw(eventObject) {
  1.   counter[0]++;
  1. }
  2. function endDraw(eventObject) {
  1.   document.onmousemove = null;
  2. }
  1. function checkOutside(eventObject) {
  2.   counter[1]++;
  3.   if (counter[1] - counter[0] > 1) {
  4.     endDraw(null);
  5.   }
  6. }

書き上がったJavaScriptは、以下のコード004にまとめました。これで、ステージの外にドラッグすると、描画は止まることになります。また、サンプル002としてjsdo.itに掲げましたので、前掲サンプル001との動きの違いをお確かいただくとよいでしょう。

サンプル002▪️EaselJS 0.8.1: Draw/erase example

コード004■ステージ外にドラッグすると描画を終える
  1. var stage;
  2. var art;
  3. var lastPoint = new createjs.Point();
  4. var color = createjs.Graphics.getHSL(0, 50, 50);
  5. var counter = [0, 0];
  6. function initialize() {
  7.   var canvas = document.getElementById("canvas");
  8.   stage = new createjs.Stage(canvas);
  9.   art = stage.addChild(new createjs.Shape());
  10.   art.cache(0, 0, canvas.width, canvas.height);
  11.   stage.addEventListener("stagemousedown", startDraw);
  12. }
  13. function startDraw(eventObject) {
  14.   stage.addEventListener("stagemousemove", draw);
  15.   stage.addEventListener("stagemouseup", endDraw);
  16.   lastPoint.setValues(eventObject.stageX, eventObject.stageY);
  17.   color = getNextColor();
  18.   counter[0] = 0;
  19.   counter[1] = 0;
  20.   document.onmousemove = checkOutside;
  21. }
  22. function draw(eventObject) {
  23.   var compositeOperation = erase.checked ? "destination-out" : "source-over";
  24.   art.graphics.clear()
      .setStrokeStyle(10, 1)
      .beginStroke(color)
      .moveTo(lastPoint.x, lastPoint.y)
      .lineTo(eventObject.stageX, eventObject.stageY);
  25.   art.updateCache(compositeOperation);
  26.   lastPoint.setValues(eventObject.stageX, eventObject.stageY);
  27.   counter[0]++;
  28.   stage.update();
  29. }
  30. function endDraw(eventObject) {
  31.   stage.removeEventListener("stagemousemove", draw);
  32.   stage.removeEventListener("stagemouseup", endDraw);
  33.   document.onmousemove = null;
  34. }
  35. function getNextColor() {
  36.   var hue = parseInt(color.split("(")[1].split(",")[0]) + 85;
  37.   return createjs.Graphics.getHSL(hue, 50, 50);
  38. }
  39. function checkOutside(eventObject) {
  40.   counter[1]++;
  41.   if (counter[1] - counter[0] > 1) {
  42.     endDraw(null);
  43.   }
  44. }


作成者: 野中文雄
作成日: 2015年8月14日


Copyright © 2001-2015 Fumio Nonaka.  All rights reserved.