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

第33回弾力のある多角形をドラッグして落とす

前回の第32回弾力のある多角形を放物線状に落とすの解説では、弾力のある四角形をつくり、さらにパラメータを変えて八角形なども試してみた。第31回にお題として示したのは、つぎのjsdo.itのコードだった。違いは、中心に点が加えられていることだ。だが、これだけではおもしろくない。今回は、弾力のある多角形を、ドラッグして放れるようにしよう。

描画をドラッグするには

画面に見えるものをドラッグさせようとするとき、そのオブジェクトへのマウス操作(イベント)で扱うのがお約束だ。たとえば、図形を描いたShapeオブジェクトであれば、その上でマウスボタンを押して、マウスを動かし、ボタンを放すという操作に応じてオブジェクトの位置が変わればよい。

ところが、今回Shapeオブジェクトはあくまでかたちを描く対象なので、位置は動かない(動いても意味がない⁠⁠。Shapeオブジェクトはそのままで、絵をドラッグしたいというのが、今回の課題になる。幸いお題で描くのは、正多角形だ。頂点数がある程度あれば、円に近いとみなせる。

そうであれば、図形の中心とマウスポインタとの距離で、マウス操作がその上で行われているかどうか調べられる。近似した円の半径より距離が小さければ、ポインタは図形に重なっていると決めればよい図1⁠。ゲームの当たり判定などでも、よく使われる考え方だ。計算が簡単なので、処理の速さも稼げる。

図1 中心からマウスポインタまでの距離と近似した円の半径で重なりを決める
図1 中心からマウスポインタまでの距離と近似した円の半径で重なりを決める

八角形に中心点を加えて放物線状に落とす

そこで、中心にVerletPointオブジェクトを加えたうえで、正八角形をつくろう。第32回コード2点と棒でつくった四角形を放物線状に落とすに手を加える。中心点のオブジェクトを加えるには、頂点をつくる関数(makePoints())に1行書き足すだけだ。頂点をつくるforループに入る前に、つぎのように頂点の配列(_points)に中心点のVerletPointオブジェクトを納めればよい。そして、四角形から八角形に頂点数を増やすのに併せて、パラメータも少しいじった(第32回のパラメータを変えて試してみようの項参照⁠⁠。

var velocityY = 0.05;   // 0.25;

function initialize() {

  makePoints(100, 70, 50, 8);   // 4);

}

function makePoints(centerX, centerY, radius, vertices) {
  var angle = -Math.PI / 2;
  var theta = 2 * Math.PI / vertices;
  _points.push(new VerletPoint(centerX, centerY));   // 追加
  for (var i = 0; i < vertices; i++) {
    var x = centerX + radius * Math.cos(angle);
    var y = centerY + radius * Math.sin(angle);
    _points.push(new VerletPoint(x, y));
    angle += theta;
  }
}
function makeSticks() {

  // _sticks.push(new VerletStick(_points[i], _points[j]));
  _sticks.push(new VerletStick(_points[i], _points[j], null, 0.05));

}

第32回コード2にまだ少しの手直ししかしていない。しかし、参照しやすいようにコード1としてまとめておこう。また、ふたつのクラスVerletPointとVerletStickは第31回のままだ。これらも併せて掲げておく。このコードが、基本的に第31回にお題として示した動きだ。ただし、細かいコードは少し気分で変えているため、確認の意味も含めてjsdo.itのサンプルも添えた。

第31回コード3 点を定めるクラスVerletPointにメソッドを追加(再掲)
function VerletPoint(x, y) {
  this.x = this._oldX = x;
  this.y = this._oldY = y;
}
VerletPoint.prototype.update = function() {
  var tempX = this.x;
  var tempY = this.y;
  var velocity = this.getVelocity();
  this.addCoordinates(velocity.x, velocity.y);
  this._oldX = tempX;
  this._oldY = tempY;
};
VerletPoint.prototype.constrain = function(rect) {
  var left = rect.x;
  var right = left + rect.width;
  var top = rect.y;
  var bottom = top + rect.height;
  if (this.x < left) {
    this.x = left;
  } else if (this.x > right) {
    this.x = right;
  }
  if (this.y < top) {
    this.y = top;
  } else if (this.y > bottom) {
    this.y = bottom;
  }
};
VerletPoint.prototype.getVelocity = function() {
  var velocity = new createjs.Point(this.x - this._oldX, this.y - this._oldY);
  return velocity;
};
VerletPoint.prototype.render = function(graphics) {
  graphics.beginFill("black")
  .drawCircle(this.x, this.y, 2.5)
  .endFill();
};
VerletPoint.prototype.addCoordinates = function(x, y) {
  this.x += x;
  this.y += y;
};
VerletPoint.prototype.subtract = function(_point) {
  var subtractedPoint = new VerletPoint(this.x - _point.x, this.y - _point.y);
  return subtractedPoint;
};
VerletPoint.prototype.getLength = function() {
  var dx = this.x;
  var dy = this.y;
  var length = Math.sqrt(dx * dx + dy * dy);
  return length;
};
VerletPoint.prototype.getDistance = function(_point) {
  var distancePoint = this.subtract(_point);
  return distancePoint.getLength();
};
第31回コード6 棒を定めるクラスVerletStickのコンストラクタに引数追加(再掲)
function VerletStick(point0, point1, length, elasticity) {
  if (!elasticity || elasticity > 0.5 || 0 > elasticity) {
    this.elasticity = 0.2;
  } else {
    this.elasticity = elasticity;
  }
  this._point0 = point0;
  this._point1 = point1;
  if (!length || length < 0) {
    this._length = point0.getDistance(point1);
  } else {
    this._length = length;
  }
}
VerletStick.prototype.update = function() {
  var delta = this._point1.subtract(this._point0);
  var distance = delta.getLength();
  var difference = this._length - distance;
  var offsetX = (difference * delta.x / distance)  * this.elasticity;
  var offsetY = (difference * delta.y / distance)  * this.elasticity;
  this._point0.addCoordinates(-offsetX, -offsetY);
  this._point1.addCoordinates(offsetX, offsetY);
};
VerletStick.prototype.render = function(graphics) {
  graphics.beginStroke("black")
  .setStrokeStyle(0.5)
  .moveTo(this._point0.x, this._point0.y)
  .lineTo(this._point1.x, this._point1.y);
};
コード1 中心点の加わった八角形を放物線状に落とす
var stage;
var drawingGraphics;
var _points = [];
var _sticks = [];
var _stageRect;
var velocityX = 5;
var velocityY = 0.05;
var _radius = 50;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  var shape = new createjs.Shape();
  stage = new createjs.Stage(canvasElement);
  stage.addChild(shape);
  drawingGraphics = shape.graphics;
  _stageRect = new createjs.Rectangle(
    _radius / 8,
    _radius / 8,
    canvasElement.width - _radius / 4,
    canvasElement.height - _radius / 4
  );
  makePoints(100, 70, 50, 8);
  makeSticks();
  _points[0].x += velocityX;
  createjs.Ticker.timingMode = createjs.Ticker.RAF;
  createjs.Ticker.addEventListener("tick", draw);
}
function draw(eventObject) {
  updatePoints();
  updateSticks();
  drawingGraphics.clear();
  renderPoints();
  renderSticks();
  stage.update();
}
function makePoints(centerX, centerY, radius, vertices) {
  var angle = -Math.PI / 2;
  var theta = 2 * Math.PI / vertices;
  _points.push(new VerletPoint(centerX, centerY));
  for (var i = 0; i < vertices; i++) {
    var x = centerX + radius * Math.cos(angle);
    var y = centerY + radius * Math.sin(angle);
    _points.push(new VerletPoint(x, y));
    angle += theta;
  }
}
function makeSticks() {
  var count = _points.length;
  for (var i = 0; i < count - 1; i++) {
    for (var j = i + 1; j < count; j++) {
      _sticks.push(new VerletStick(_points[i], _points[j], null, 0.05));
    }
  }
}
function updatePoints() {
  var count = _points.length;
  for (var i = 0; i < count; i++) {
    var point = _points[i];
    point.y += velocityY;
    point.update();
    point.constrain(_stageRect);
  }
}
function updateSticks() {
  var count = _sticks.length;
  for (var i = 0; i < count; i++) {
    var stick = _sticks[i];
    stick.update();
  }
}
function renderPoints() {
  var count = _points.length;
  for (var i = 0; i < count; i++) {
    var point = _points[i];
    point.render(drawingGraphics);
  }
}
function renderSticks() {
  var count = _sticks.length;
  for (var i = 0; i < count; i++) {
    var stick = _sticks[i];
    stick.render(drawingGraphics);
  }
}

図形をドラッグして放る

それでは、弾力のある八角形をドラッグして放り投げられるようにしたい。ドラッグは、ステージへの操作として捉え、前述のとおり図形の中心からマウスポインタの距離によって、それが図形の上で行われたかどうか判定する(前掲図1参照⁠⁠。ステージにおけるドラッグは、つぎのように3つのマウスイベントで扱う[1]⁠。

ドラッグを3つのマウスイベントで扱う
  1. Stage.stagemousedownイベント
    • ドラッグを始める
    • ふたつのイベントリスナーを登録
      • Stage.stagemousemove
      • Stage.stagemouseup
  2. Stage.stagemousemoveイベント
    • インスタンスの位置をマウス座標に合わせて動かす
  3. Stage.stagemouseupイベント
    • ドラッグを終える
    • ふたつのイベントリスナーを削除
      • Stage.stagemousemove
      • Stage.stagemouseup

図形をマウスでドラッグするために加えるJavaScriptコードは、以下に抜書きしたとおりだ。Stage.stagemousemoveイベントのリスナー関数(startDrag())は、初期化の関数(initialize())で加える。

このドラッグを始めるリスナー関数(startDrag())では、マウスボタンを押したポインタの座標(mousePoint)と中心点(centerPoint)との距離(distance)が多角形の頂点をつくった半径(_radius)より小さいときに、図形上の操作とみなしてStage.stagemousemoveStage.stagemouseupイベントのリスナー関数(drag()とstopDrag())をそれぞれ加える。

ドラッグのリスナー関数(drag())は、図形の中心点(centerPoint)をポインタ座標(mousePoint)に合わせて動かす。ただし、今回使っている仕組み(ベレ法)では、点の移動は力を加えることになる。あまり大きな力をかけると、アニメーションが乱れてしまう。そのため、ここでもマウスポインタが図形の上にある(距離が半径より小さい)ことを条件とした。ポインタが図形から外れたら、ドラッグ終了の関数(stopDrag())を呼んでいる。

ドラッグを終えるリスナー関数(stopDrag())では、前述のとおりStage.stagemousemoveStage.stagemouseupイベントのリスナー関数(drag()とstopDrag())が除かれている。

var offset;
function initialize() {

  stage.addEventListener("stagemousedown", startDrag);
}

function startDrag(eventObject) {
  var mousePoint = new VerletPoint(eventObject.stageX, eventObject.stageY);
  var centerPoint = _points[0];
  var distance = mousePoint.getDistance(centerPoint);
  if (distance < _radius) {
    offset = mousePoint.subtract(centerPoint);
    stage.addEventListener("stagemousemove", drag);
    stage.addEventListener("stagemouseup", stopDrag);
  }
}
function drag(eventObject) {
  var mousePoint = new VerletPoint(eventObject.stageX, eventObject.stageY);
  var centerPoint = _points[0];
  if (mousePoint.getDistance(centerPoint) < _radius) {
    var movePoint = mousePoint.subtract(offset);
    centerPoint.x = movePoint.x;
    centerPoint.y = movePoint.y;
  } else {
    stopDrag(null);
  }
}
function stopDrag(eventObject) {
  stage.removeEventListener("stagemousemove", drag);
  stage.removeEventListener("stagemouseup", stopDrag);
}

細かいので後に回した説明が、ひとつ残っている。ドラッグを始めるリスナー関数(startDrag())の中で、変数(offset)に納めたマウスポインタと中心点の差の使い途だ。図形の中心点をそのままポインタ座標に合わせると、マウスポインタがつねに図形の中心になるように動く。しかし、図形の端からドラッグを始めたら、ポインタが端にあるまま図形を動かしたい。そこで、ドラッグするリスナー関数(drag())では、マウスポインタの座標(mousePoint)から変数にとった差の値を差し引いて、ポインタと図形の位置関係を保っている。

図形のドラッグを加えたscript要素は、つぎのコード2にまとめた。点と棒のクラスVerletPointとVerletStickは、前掲第31回コード3第31回コード6のまま変えていない。jsdo.itにもサンプルを掲げた。これで、弾力のある八角形を、ドラッグで放り投げることができる。なお、前述のとおり仕様として、ドラッグするマウスポインタが図形の外に出ると、マウスボタンを放したのと同じようにドラッグは終わってしまう。勢いをつけたかったら、マウスは徐々に速く動かすのがコツだ。

コード2 弾力のある八角形をドラッグで放る
var stage;
var drawingGraphics;
var _points = [];
var _sticks = [];
var _stageRect;
var velocityX = 5;
var velocityY = 0.05;
var _radius = 50;
var offset;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  var shape = new createjs.Shape();
  stage = new createjs.Stage(canvasElement);
  stage.addChild(shape);
  drawingGraphics = shape.graphics;
  _stageRect = new createjs.Rectangle(
    _radius / 8,
    _radius / 8,
    canvasElement.width - _radius / 4,
    canvasElement.height - _radius / 4
  );
  makePoints(100, 70, 50, 8);
  makeSticks();
  _points[0].x += velocityX;
  createjs.Ticker.timingMode = createjs.Ticker.RAF;
  createjs.Ticker.addEventListener("tick", draw);
  stage.addEventListener("stagemousedown", startDrag);
}
function draw(eventObject) {
  updatePoints();
  updateSticks();
  drawingGraphics.clear();
  renderPoints();
  renderSticks();
  stage.update();
}
function makePoints(centerX, centerY, radius, vertices) {
  var angle = -Math.PI / 2;
  var theta = 2 * Math.PI / vertices;
  _points.push(new VerletPoint(centerX, centerY));
  for (var i = 0; i < vertices; i++) {
    var x = centerX + radius * Math.cos(angle);
    var y = centerY + radius * Math.sin(angle);
    _points.push(new VerletPoint(x, y));
    angle += theta;
  }
}
function makeSticks() {
  var count = _points.length;
  for (var i = 0; i < count - 1; i++) {
    for (var j = i + 1; j < count; j++) {
      _sticks.push(new VerletStick(_points[i], _points[j], null, 0.05));
    }
  }
}
function updatePoints() {
  var count = _points.length;
  for (var i = 0; i < count; i++) {
    var point = _points[i];
    point.y += velocityY;
    point.update();
    point.constrain(_stageRect);
  }
}
function updateSticks() {
  var count = _sticks.length;
  for (var i = 0; i < count; i++) {
    var stick = _sticks[i];
    stick.update();
  }
}
function renderPoints() {
  var count = _points.length;
  for (var i = 0; i < count; i++) {
    var point = _points[i];
    point.render(drawingGraphics);
  }
}
function renderSticks() {
  var count = _sticks.length;
  for (var i = 0; i < count; i++) {
    var stick = _sticks[i];
    stick.render(drawingGraphics);
  }
}
function startDrag(eventObject) {
  var mousePoint = new VerletPoint(eventObject.stageX, eventObject.stageY);
  var centerPoint = _points[0];
  var distance = mousePoint.getDistance(centerPoint);
  if (distance < _radius) {
    offset = mousePoint.subtract(centerPoint);
    stage.addEventListener("stagemousemove", drag);
    stage.addEventListener("stagemouseup", stopDrag);
  }
}
function drag(eventObject) {
  var mousePoint = new VerletPoint(eventObject.stageX, eventObject.stageY);
  var centerPoint = _points[0];
  if (mousePoint.getDistance(centerPoint) < _radius) {
    var movePoint = mousePoint.subtract(offset);
    centerPoint.x = movePoint.x;
    centerPoint.y = movePoint.y;
  } else {
    stopDrag(null);
  }
}
function stopDrag(eventObject) {
  stage.removeEventListener("stagemousemove", drag);
  stage.removeEventListener("stagemouseup", stopDrag);
}

見た目をボールにしてみる

今回まで取り組んできたお題は、とりあえずこれでできあがりとしたい。後は、読者のみなさんがそれぞれ試していただきたい。ひとつの例として、見た目をボールにしてみよう。前掲コード2からのおもな書替えはふたつだ。第1に、丸く見えるように頂点の数を増やす。第2は、点と棒ではなく、輪郭と塗りで描く。なお、それにともなって、パラメータも少し変えた方がよいかもしれない。

筆者は、つぎのように手直ししてみた。jsdo.itにもForkしたコードを掲げたので、興味があったら比べてみてほしい。頂点数を増やすと円に近づくものの、計算量も増えるし、点と棒の互いの影響が強まって、動きが緩慢になる。棒の固さや重力など、試しながら気に入った値を選ぶとよいだろう。

var velocityX = 50;   // 5;
var velocityY = 0.2;   // 0.05;
function initialize() {

  makePoints(100, 70, 50, 24);   // 8);
}
function draw(eventObject) {

  // renderPoints();
  // renderSticks();
  renderShape();

}

/*
function renderPoints() {
  // ...[中略]...
}
function renderSticks() {
  // ...[中略]...
}
*/
function renderShape() {
  var count = _points.length;
  var point = _points[count - 1];
  drawingGraphics.beginFill("cyan")
  .beginStroke("blue")
  .setStrokeStyle(1)
  .moveTo(point.x, point.y);
  for (var i = 1; i < count; i++) {
    point = _points[i];
    drawingGraphics.lineTo(point.x, point.y);
  }
  drawingGraphics.endFill();
}

おすすめ記事

記事・ニュース一覧