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

第32回弾力のある多角形を放物線状に落とす

前回の第31回位置座標の相互作用で弾力を表すでは、ふたつの点を棒でつなぎ、つぎのjsdo.itのコードのように放物線状に落としてみた。複数のオブジェクトをつなげた弾力のある動きが、座標の移動と位置関係や動く範囲を定めるだけの四則演算で表せた。今回は、点と棒の数を増やして多角形の動きをつくりたい。

点と棒で組上げた三角形を落とす

前回、点と棒のオブジェクトは、それぞれのクラス(VerletPointとVerletStick)からつくった。これらふたつのクラスは、今回そのまま用いる。多角形になっても、クラスの仕組みは変わらないということだ。参照しやすいように、それぞれのクラスを定めた第31回のコード3コード6を以下に改めて掲げておく。

第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);
};

そして、前回のコード5ふたつの点を棒でつないで放物線状に落とすを、以下の抜書きのように書き替える。まず、(VerletStickオブジェクト)も複数になるので、変数(_sticks)の配列に納めることにした。新たに加えた関数(makeSticks())で点(VerletPointオブジェクト)の数に応じてインスタンスをつくり、この配列に加える。なお、forループを抜けてから、終わりの点と初めの点を棒で結ばないと多角形が閉じないことに注意したい。

そして、多角形の頂点数が自由に決められるように、点をつぎの関数により生成して、やはり変数(_points)の配列に納めることにした。始めは、三角形をつくる。

makePoints(中心x座標, 中心y座標, 半径, 頂点数)

多角形の頂点をつくるこの関数は、-π/2ラジアンつまり時計の12時の角度(angle)から、引数の頂点数に応じた中心角(theta)と引数の半径(radius)で点のインスタンスをつくって、配列(_points)に加えている。この計算の考え方については、第15回「Matrix2Dクラスで座標を回す」星形を頂点座標と直線で描くが参考になるだろう。

// var _stick;
var _sticks = [];

var _radius = 50;
function initialize() {

  // _points.push(new VerletPoint(70, 100));
  // _points.push(new VerletPoint(50, 25));
  makePoints(100, 70, 50, 3);
  // _stick = new VerletStick(_points[0], _points[1]);
  makeSticks();

}

function makePoints(centerX, centerY, radius, vertices) {
  var angle = -Math.PI / 2;
  var theta = 2 * Math.PI / vertices;
  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++) {
    _sticks.push(new VerletStick(_points[i], _points[i + 1]));
  }
  _sticks.push(new VerletStick(_points[i], _points[0]));
}

Ticker.tickイベントのリスナー関数(draw())は、扱う棒のオブジェクトが複数になったので、更新と描画はそれぞれ新たに設けた関数(updateSticks()とrenderSticks())の呼出しにより行う。どちらの関数も、棒が納められた配列(_sticks)からオブジェクト(stick)を取り出して、それぞれに応じたメソッド(update()とrender())を呼び出している。

function draw(eventObject) {

  // _stick.update();
  updateSticks();

  // _stick.render(drawingGraphics);
  renderSticks();

}

function updateSticks() {
  var count = _sticks.length;
  for (var i = 0; i < count; i++) {
    var stick = _sticks[i];
    stick.update();
  }
}

function renderSticks() {
  var count = _sticks.length;
  for (var i = 0; i < count; i++) {
    var stick = _sticks[i];
    stick.render(drawingGraphics);
  }
}

これらの書き替えを終えたJavaScriptが以下のコード1だ。3つの点と棒で三角形をつくって、放物線状に落としている。アニメーションはつぎのjsdo.itのコードで確かめてほしい。三角形になると、Canvasの下端でも弾むような動きが見られる。

コード1 点と棒で三角形をつくって放物線状に落とす
var stage;
var drawingGraphics;
var _points = [];
var _sticks = [];
var _stageRect;
var velocityX = 5;
var velocityY = 0.25;
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, 3);
  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;
  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++) {
    _sticks.push(new VerletStick(_points[i], _points[i + 1]));
  }
  _sticks.push(new VerletStick(_points[i], _points[0]));
}
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);
  }
}

4つの点から四角形をつくって落とす

三角形ができたら、つぎは四角形だ。もっとも、前掲コード1の頂点をつくる関数(makePoints())には、頂点数を引数で与えられるようにした。したがって、初期化の関数(initialize())内から呼び出すとき渡す頂点数の引数値を書き替えるだけで、四角形がつくれる。つくれるけれども、残念ながらでき上がるアニメーションは情けない。Canvas下端に落ちたときかたちを保てず、四角形がつぶれてしまうのだ図1⁠。

function initialize() {

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

}
図1 四角形が落ちるとかたちはつぶれる
図1 四角形が落ちるとかたちはつぶれる 図1 四角形が落ちるとかたちはつぶれる

問題は、4つの点の隣同士の間にしか棒が加えられていないことだ。それでは、いくら4つの棒の長さを保っても、つぶれてしまう。姑息(ことばの意味を取り違える人が増えているそうなので辞書にリンクした)解決法として、筋交い⁠すじかい)を加えることが考えられる図2⁠。前掲コード1の棒をつくる関数(makeSticks())に、以下のように1行書込めば済む。

図2 筋交いを加えてつぶれないようにする
図2 筋交いを加えてつぶれないようにする
function makeSticks() {
  var count = _points.length;
  for (var i = 0; i < count - 1; i++) {
    _sticks.push(new VerletStick(_points[i], _points[i + 1]));
  }
  _sticks.push(new VerletStick(_points[i], _points[0]));
  _sticks.push(new VerletStick(_points[0], _points[2]));   // 追加
}

しかし、これで四角形はかたちが保てても、さらに頂点数を増やせば、また筋交いの棒をどこに加えるか考えるはめになる。それでは、せっかく頂点数を関数(makePoints())の引数で自由に決められるようにした意味がない。そこで、すべてのふたつの頂点の組を棒で結ぶことにする。棒をつくる関数(makeSticks())for文をつぎのように二重にして、外側のループで始めの点を選び、内側のループは選んだ点のインデックスより後の点を順に棒でつないでいる。

function makeSticks() {
  var count = _points.length;
  for (var i = 0; i < count - 1; i++) {
    // _sticks.push(new VerletStick(_points[i], _points[i + 1]));

    for (var j = i + 1; j < count; j++) {
      _sticks.push(new VerletStick(_points[i], _points[j], null, 0.1));
    }
  }
  // _sticks.push(new VerletStick(_points[i], _points[0]));
}

この修正を前掲コード1に加えたのが、以下のコード2だ。動きを確かめるためにjsdo.itのサンプルも掲げよう。これで、頂点をつくる関数(makePoints())の引数で任意の多角形が定められる。もちろん、落ちてもかたちはつぶれない。

コード2 点と棒でつくった四角形を放物線状に落とす
var stage;
var drawingGraphics;
var _points = [];
var _sticks = [];
var _stageRect;
var velocityX = 5;
var velocityY = 0.25;
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, 4);
  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;
  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]));
    }
  }

}
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);
  }
}

パラメータを変えて試してみよう

コードの解説は今回はここまでとする。後は、ぜひ関数の引数や変数の値など、パラメータをいろいろ変えて試してみてほしい。たとえば、以下のように頂点をつくる関数(makePoints())の引数さえ変えれば、すぐに四角形が六角形になる。さらに、2点の組を棒でつなぐ関数(makeSticks())から呼出すVerletStick()コンストラクタの引数により、棒の固さも変えてみよう[1]⁠。

makePoints(中心x座標, 中心y座標, 半径, 頂点数)

VerletStick(始点, 終点, 長さ, 固さ)
function initialize() {

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

}

function makeSticks() {

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

}

前回のふたつの点を棒でつなぐでご説明したとおり、固さ0.5というのは、棒に定められた長さとの誤差をふたつの点に半分ずつ割振って、直ちに補正するということだ。ところが、頂点が6つになると、誤差が一発では直せない。それどころか、その歪みが別の点にしわ寄せされる。続くアニメーションでは、その誤差がまた別の点に飛火する。その繰返しで、六角形は頂点があちこち乱れて動き、かたちが定まらなくなってしまうのだ図3⁠。

図3 六角形のかたちが歪んで定まらない
図3 六角形のかたちが歪んで定まらない 図3 六角形のかたちが歪んで定まらない

そのため、VerletStick()コンストラクタでは、棒の固さのプロパティ(elasticity)のデフォルト値をもっと小さな数値(0.2)にしてある。すると、1回のアニメーションでは、誤差は一気になくならない。イージングの動きのように、何度か繰返すうちに、目的の値にたどり着くようになる。その場合、残った誤差の分、棒は弾力をもって伸び縮みするように見える。

たとえば、パラメータを以下のように書き替えてみる。頂点数を8に増やして八角形とし、棒の固さはデフォルト値よりさらに柔らかく(0.05)した。そして、重力に当たる垂直方向の力(velocityY)を和らげて(0.05)いる。落ち方がゆっくりになるとともに、弾力は空気の甘いボールのようになる。前掲コード2にこの書替えを加えて、ご参考までにjsdo.itのサンプルとして以下に掲げた。

var velocityY = 0.05;   // 0.25;

function initialize() {

  makePoints(100, 70, 50, 8);

}

function makeSticks() {

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

}

jsdo.itには[Fork]という便利な機能がある図4⁠。このボタンを押すと、コードのコピーがつくられ、自由に手が加えられる。そして、もとのコードと書き替わった行は、[View Diff]というボタンで比較表示して確かめられる。ぜひ、これらの機能を使って、試してみてほしい。次回は、もう少し処理を加えたうえで、お題を仕上げたい。

図4 jsdo.itの[Fork]と[View Diff]ボタン
図4 jsdo.itの[Fork]と[View Diff]ボタン

おすすめ記事

記事・ニュース一覧