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

第23回マウスポインタの軌跡を滑らかな線で描きながら消す

つぎなるお題は、マウスポインタの動きに沿って、滑らかに描かれては消える線のアニメーションだ。マウスが大きく動くと、線は太くなる。また、線はマウスポインタの軌跡そのままではなく、動きに少し弾みがついて描かれる。CreateJSコミュニティエバンジェリストのSebastian DeRossi氏がつくられたサンプルSmooth Lineをもとに、表現は少し変えている。

マウスポインタの軌跡を滑らかな曲線で描く

はじめの一歩として、マウスポインタの軌跡を滑らかな曲線で描く。この連載をとおして読んでくださっている読者なら、どこかで聞覚えのある表現だと気づかれたかもしれない。第10回ドラッグの軌跡を滑らかな曲線で描くで扱ったネタだ。ただ、細かい仕込みが違ってくるため、今回は新たに書き下ろす。ライブラリの読込みと初期設定の関数、body要素からの関数呼出しやcanvas要素の配置はいつもどおりだ。

<script src="http://code.createjs.com/easeljs-0.7.1.min.js"></script>
<script>
function initialize() {
  // 初期設定
}
</script>
<body onLoad="initialize()">
  <canvas id="myCanvas" width="240" height="180"></canvas>
</body>

さて、はじめの一歩はつぎのコード1だ。スクリプティングの考え方については、第10回コード1ドラッグする軌跡でアルファマスクを描くを参考にしてほしい。この後、コードの組立てをかいつまんで説明しよう。

コード1 マウスポインタの軌跡を滑らかな曲線で描く
var stage;
var container;
var myShape = new createjs.Shape();
var lastMidPoint = new createjs.Point();
var currentPoint = new createjs.Point();
var lastPoint = new createjs.Point();
var currentLineThickness = 1;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  container = new createjs.Container();  
  stage.addChild(container);
  container.addChild(myShape);
  lastPoint.x = lastMidPoint.x = canvasElement.width / 2;
  lastPoint.y = lastMidPoint.y = canvasElement.height / 2;
  createjs.Ticker.addEventListener("tick", draw);   
}
function draw() {
  var moveX = (stage.mouseX - currentPoint.x);
  var moveY = (stage.mouseY - currentPoint.y);
  if (moveX * moveX + moveY * moveY > 0.1) {
    currentPoint.x += moveX;
    currentPoint.y += moveY;
    var midPoint = new createjs.Point((lastPoint.x + currentPoint.x) / 2,  (lastPoint.y + currentPoint.y) / 2);
    drawCurve(myShape.graphics, lastMidPoint, midPoint, lastPoint);
    lastPoint.initialize(currentPoint.x, currentPoint.y);
    lastMidPoint.initialize(midPoint.x, midPoint.y);
  }
  stage.update();
}
function drawCurve(myGraphics, oldPoint, newPoint, controlPoint) {
  setLineThickness(oldPoint, newPoint);
  myGraphics.beginStroke("black")
  .setStrokeStyle(currentLineThickness, "round", "round")
  .moveTo(oldPoint.x, oldPoint.y)
  .quadraticCurveTo(controlPoint.x, controlPoint.y, newPoint.x, newPoint.y);   
}
function setLineThickness(oldPoint, newPoint) {
  var distanceX = newPoint.x - oldPoint.x;
  var distanceY = newPoint.y - oldPoint.y;
  var distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
  var lineThickness = distance * 0.2;
  currentLineThickness = lineThickness;
}

線はShapeインスタンス(myShape)に描く。ただし、Stageオブジェクト(stage)には直に置かない。先に親となるContainerオブジェクト(container)を加えて、Shapeインスタンスはその入れ子にする。描いた線を後から消すために、Shapeオブジェクトをいくつも加えられるようにしたいからだ。もっとも、今はまだオブジェクトは一つしかつくっていない。

var stage;
var container;
var myShape = new createjs.Shape();

function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  container = new createjs.Container();  
  stage.addChild(container);
  container.addChild(myShape);

}

マウスポインタの軌跡を滑らかに描く考え方は、前出第10回「ドラッグの軌跡を滑らかな曲線で描く」と基本的に変わらない。つまり連続した座標は、すべてコントロールポイントにしてしまう。そして、座標の中点をアンカーポイントとして結ぶのだった第10回図4再掲⁠⁠。

第10回 図4 座標はコントロールポイントとして中点をアンカーポイントに定める(再掲)
第10回 図4 座標はコントロールポイントとして中点をアンカーポイントに定める(再掲)

Ticker.tickイベントのリスナー関数(draw())は、新しいマウスポインタ座標(Stage.mouseXとStage.mouseYプロパティ)と前のマウスポインタ座標(currentPoint)との差をとって、少しでも動きがあれば曲線を描く[1]⁠。そして、ポインタ座標の古い中点(lastMidPoint)と新たな中点(midPoint)を結び、古いマウス座標(lastPoint)がコントロールポイントとなる曲線を描く。そのための関数(drawCurve())は別に設け、ShapeインスタンスのGraphicsオブジェクトと3つの座標のPointオブジェクトを引数に渡した。

var lastMidPoint = new createjs.Point();
var currentPoint = new createjs.Point();
var lastPoint = new createjs.Point();
var currentLineThickness = 1;
function initialize() {

  lastPoint.x = lastMidPoint.x = canvasElement.width / 2;
  lastPoint.y = lastMidPoint.y = canvasElement.height / 2;

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

function draw() {
  var moveX = (stage.mouseX - currentPoint.x);
  var moveY = (stage.mouseY - currentPoint.y);
  if (moveX * moveX + moveY * moveY > 0.1) {{
    currentPoint.x += moveX;
    currentPoint.y += moveY;
    var midPoint = new createjs.Point((lastPoint.x + currentPoint.x) / 2,  (lastPoint.y + currentPoint.y) / 2);
    drawCurve(myShape.graphics, lastMidPoint, midPoint, lastPoint);
    lastPoint.initialize(currentPoint.x, currentPoint.y);
    lastMidPoint.initialize(midPoint.x, midPoint.y);
  }
  stage.update();
}
function drawCurve(myGraphics, oldPoint, newPoint, controlPoint) {

  myGraphics.beginStroke("black")
  .setStrokeStyle(currentLineThickness, "round", "round")
  .moveTo(oldPoint.x, oldPoint.y)
  .quadraticCurveTo(controlPoint.x, controlPoint.y, newPoint.x, newPoint.y);   
}

さらに、曲線を描く関数(drawCurve())は、マウスの動きの大きさに応じて線幅を変えている。ポインタの古い座標と新しい座標を引数に渡して呼出した関数(setLineThickness())が、線幅(currentLineThickness)を定める。ふたつの座標の距離(distance)を求めて、幅はその値に比例させた。

function drawCurve(myGraphics, oldPoint, newPoint, controlPoint) {
  setLineThickness(oldPoint, newPoint);

}
function setLineThickness(oldPoint, newPoint) {
  var distanceX = newPoint.x - oldPoint.x;
  var distanceY = newPoint.y - oldPoint.y;
  var distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
  var lineThickness = distance * 0.2;
  currentLineThickness = lineThickness;
}

このコード1に手を加えていく。だが、試してみると目指す表現からはほど遠い。インクフローがおかしくなったペンで描いた線のようだ図1⁠。線の太さが滑らかに変わっていない。

図1 マウスポインタの動きに沿って描いた線の太さが滑らかに変わらない
図1 マウスポインタの動きに沿って描いた線の太さが滑らかに変わらない

値の変化のさせ方を考える

コード1の線幅を滑らかに変えたい。だからといって、マウスの動きに比例させる係数を小さくしたのでは、線幅の差が小さくなってしまう。小さくしたいのは変化であって、線幅ではない。そこで、目標の線幅(lineThickness)と現在の値(currentLineThickness)との差を求め、そのまま加えるのでなく、一定割合(0.25)だけ変える。これで変化が小さくなる。

function setLineThickness(oldPoint, newPoint) {
  var distanceX = newPoint.x - oldPoint.x;
  var distanceY = newPoint.y - oldPoint.y;
  var distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
  var lineThickness = distance * 0.2;
  // currentLineThickness = lineThickness;
  currentLineThickness += (lineThickness - currentLineThickness) * 0.25;
}

今回のお題でとくに考えたいことのひとつが、値の変化のさせ方だ。そこで、マウスポインタの軌跡を描く関数(draw())の線の動きも少し変えてみた。新旧ポインタ座標の差(moveXとmoveY)を直ちに線で結ぶのではなく、ここでも一定割合(ease)だけ近づいた線を描く。つまり、マウスポインタの動きに、線が少し遅れてついていく。

var velocityX = 0;
var velocityY = 0;
var ease = 0.25; function draw() {   var moveX = (stage.mouseX - currentPoint.x);   var moveY = (stage.mouseY - currentPoint.y);   if (moveX * moveX + moveY * moveY > 0.1) {     velocityX = moveX * ease;     velocityY = moveY * ease;     // currentPoint.x += moveX;     // currentPoint.y += moveY;     currentPoint.x += velocityX;     currentPoint.y += velocityY;     var midPoint = new createjs.Point((lastPoint.x + currentPoint.x) / 2, (lastPoint.y + currentPoint.y) / 2);     drawCurve(myShape.graphics, lastMidPoint, midPoint, lastPoint);     lastPoint.initialize(currentPoint.x, currentPoint.y);     lastMidPoint.initialize(midPoint.x, midPoint.y);   }   stage.update(); }

これで何とか、マウスポインタの動きに少し遅れて、太さも滑らかに変わる線が描かれるようになる図2⁠。

図2マウスポインタの動きに少し遅れて線幅も滑らかに変わる線が描かれる
図2 マウスポインタの動きに少し遅れて線幅も滑らかに変わる線が描かれる

描かれた線を後から消してゆく

つぎは、描かれた線を後から少しずつ消してゆこう。とはいえ、Shapeインスタンスへの描画を一部消すというのは難しい。そこで、Ticker.tickイベントのリスナー関数(draw())は、呼び出されるたびに別のShapeインスタンスをつくって線描することにした。新たなShapeオブジェクトを返す関数(getNewChild())は別に定める。

そして、ステージに置くShapeインスタンスの最大値を予め変数(maxLines)に定めておく。線を描いたShapeインスタンスがその数を超えたら、古い方から順に消してゆく。Shapeオブジェクトを消す関数(removeOldChild())もこの後定める。また、マウスポインタは動かなくても、Shapeオブジェクトを消す関数は、親オブジェクト(container)の子が残りひとつになるまで呼び出し続ける。

// var myShape = new createjs.Shape();
var maxLines = 50;

function initialize() {

  // container.addChild(myShape);

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

function draw() {
  var moveX = (stage.mouseX - currentPoint.x);
  var moveY = (stage.mouseY - currentPoint.y);
  var numChildren = container.getNumChildren();
  if (moveX * moveX + moveY * moveY > 0.1) {

    var myShape = getNewChild();
    container.addChild(myShape);
    drawCurve(myShape.graphics, lastMidPoint, midPoint, lastPoint);

    if (numChildren >= maxLines){
      removeOldChild();
    }
  } else if (numChildren > 1) {
    removeOldChild();
  }
  stage.update();
}

Shapeオブジェクトをつくるには、Shape()コンストラクタを呼び出せば済む。関数(getNewChild())を設けるのは、つくったオブジェクトを使い回すためだ(第14回オブジェクトの使い回しとアニメーション素材の変更参照⁠⁠。使い回すオブジェクトを入れる配列はあらかじめ変数(children)に定めた。関数は配列にオブジェクトがあれば取り出し、なければコンストラクタで新たにつくって返す。なお、前に使ったとき描かれた線は、Graphics.clear()メソッドで消しておくのを忘れずに。

Shapeオブジェクトを消す関数(removeOldChild())は、親オブジェクト(container)から一番古い(インデックス0の)オブジェクトを表示リストから除き、配列(children)に加える。

var children = [];

function getNewChild() {
  var child;
  if (children.length) {
    child = children.pop();
    child.graphics.clear();
  } else {
    child = new createjs.Shape();
  }
  return child;
}
function removeOldChild() {
  var child = container.getChildAt(0);
  container.removeChildAt(0);
  children.push(child);
}

これらの手を加えると、マウスポインタの動きに沿って描かれた線が、後から順に消えてゆくようになる図3⁠。以下のコード2に全体をまとめた。今回は、ここまでとしよう。後にjsdo.itのサンプルも掲げた。次回は、線の動きやアニメーションにもう少しこだわろう。

図3描かれた線が後から順に消えてゆく
図3 描かれた線が後から順に消えてゆく
コード2 マウスポインタの動きに沿って描いた線を後から順に消してゆく
var stage;
var container;
var children = [];
var lastMidPoint = new createjs.Point();
var currentPoint = new createjs.Point();
var lastPoint = new createjs.Point();
var velocityX = 0;
var velocityY = 0;
var ease = 0.25;
var maxLines = 50;
var currentLineThickness = 1;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  container = new createjs.Container();  
  stage.addChild(container);
  lastPoint.x = lastMidPoint.x = canvasElement.width / 2;
  lastPoint.y = lastMidPoint.y = canvasElement.height / 2;
  createjs.Ticker.addEventListener("tick", draw);   
}
function draw() {
  var moveX = (stage.mouseX - currentPoint.x);
  var moveY = (stage.mouseY - currentPoint.y);
  var numChildren = container.getNumChildren();
  if (moveX * moveX + moveY * moveY > 0.1) {
    velocityX = moveX * ease;
    velocityY = moveY * ease;
    currentPoint.x += velocityX;
    currentPoint.y += velocityY;
    var midPoint = new createjs.Point((lastPoint.x + currentPoint.x) / 2,  (lastPoint.y + currentPoint.y) / 2);
    var myShape = getNewChild();
    container.addChild(myShape);
    drawCurve(myShape.graphics, lastMidPoint, midPoint, lastPoint);
    lastPoint.initialize(currentPoint.x, currentPoint.y);
    lastMidPoint.initialize(midPoint.x, midPoint.y);
    if (numChildren >= maxLines){
      removeOldChild();
    }
  } else if (numChildren > 1) {
    removeOldChild();
  }
  stage.update();
}
function getNewChild() {
  var child;
  if (children.length) {
    child = children.pop();
    child.graphics.clear();
  } else {
    child = new createjs.Shape();
  }
  return child;
}
function removeOldChild() {
  var child = container.getChildAt(0);
  container.removeChildAt(0);
  children.push(child);
}
function drawCurve(myGraphics, oldPoint, newPoint, controlPoint) {
  setLineThickness(oldPoint, newPoint);
  myGraphics.beginStroke("black")
  .setStrokeStyle(currentLineThickness, "round", "round")
  .moveTo(oldPoint.x, oldPoint.y)
  .quadraticCurveTo(controlPoint.x, controlPoint.y, newPoint.x, newPoint.y);   
}
function setLineThickness(oldPoint, newPoint) {
  var distanceX = newPoint.x - oldPoint.x;
  var distanceY = newPoint.y - oldPoint.y;
  var distance = Math.sqrt(distanceX * distanceX + distanceY * distanceY);
  var lineThickness = distance * 0.2;
  currentLineThickness += (lineThickness - currentLineThickness) * 0.25;
}

おすすめ記事

記事・ニュース一覧