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

第7回粒子同士が引き合う力を直線の濃淡で示す

前回は、ランダムに動く粒子それぞれの間にバネのような力を与えてアニメーションさせた。すると、粒子は互いに集まっては離れることを繰り返す、という面白い動きになった(再掲第6回図3⁠。ここまでのコードは、以下のjsdo.itに掲げてある第6回コード2⁠。

第6回 図3 オブジェクトがひと固まりに集まっては離れる(再掲)
第6回 図3 オブジェクトがひと固まりに集まっては離れる 第6回 図3 オブジェクトがひと固まりに集まっては離れる

オブジェクトが引合う力の範囲をかぎる

互いに離れれば離れるほど、引き戻す力は強まるというのがバネの性質だ。ただし、それを単純に当てはめたために、仲間から外れた粒子の激しい動きが止められず、しかも仲間はずれは次第に増えてしまうというのが前回の問題だった。

もともと、バネはいくらでも伸び縮みするものではない。むしろ、その動きを正しく保てる範囲はかぎられる。そこで、この粒子のアニメーションでも、互いの間に引合う力が及ぶ距離を決めてしまおう。2点の座標からその間の距離を導くには、三平方の定理を用いる図1⁠。2点の水平・垂直の各座標の差をそれぞれ2乗し、足し合わせてから平方根を求めればよい。

図1 2点の座標から三平方の定理で距離を求める
</span>図1 2点の座標から三平方の定理で距離を求める </span>図1 2点の座標から三平方の定理で距離を求める

そこで、以下のように力の働く最大距離を変数(limit)で定める。そして、オブジェクト間にバネの力を加える関数(spring())の中で、その距離より近いオブジェクト同士だけ引合うようにする。

水平・垂直それぞれの座標の差(distanceXとdistanceY)は、もともと求めていた。だから、それぞれを2乗して(squareXとsquareY)加え、平方根を計算すれば、距離(distance)が導ける。平方根はMath.sqrt()メソッドで求まる。なお、累乗にはMath.pow()メソッドが使える。だが、Mathクラスの演算は遅くなりがちだ。2乗くらいなら、同じ値を2度掛けたほうが速い。

function spring(object0, object1) {
  var distanceX = object1.x - object0.x;
  var distanceY = object1.y - object0.y;
  var squareX = distanceX * distanceX;
  var squareY = distanceY * distanceY;
  var distance = Math.sqrt(squareX + squareY);
  if (distance < limit) {
    var accelX = distanceX * ratio;
    var accelY = distanceY * ratio;
    object0.velocityX += accelX;
    object0.velocityY += accelY;
    object1.velocityX -= accelX;
    object1.velocityY -= accelY;
  }
}

さて、処理の速さを考えるなら、もう一歩踏み込みたい。オブジェクト間の距離を条件に、互いに引合うかどうかを決めた。けれども、距離(distance)の値そのものは計算に使っていない。単に、if条件であらかじめ定めた制限値(limit)と比べただけだ。そうであれば、平方根など求めることはない。比べる制限値の方を2乗しておく。これで、またひとつ計算が減り、Mathクラスのメソッドも使わずに済んだ。

// var limit = 100;
var limit = 100 * 100;

function spring(object0, object1) {
  var distanceX = object1.x - object0.x;
  var distanceY = object1.y - object0.y;
  var squareX = distanceX * distanceX;
  var squareY = distanceY * distanceY;
  // var distance = Math.sqrt(squareX + squareY);
  // if (distance < limit) {
  if (squareX + squareY < limit) {
    var accelX = distanceX * ratio;
    var accelY = distanceY * ratio;
    object0.velocityX += accelX;
    object0.velocityY += accelY;
    object1.velocityX -= accelX;
    object1.velocityY -= accelY;
  }
}

書き直したJavaScript全体は、以下のコード1のとおりだ。仲間から大きく外れると力が加えられなくなり、近づけば引き戻される。激しくやんちゃな動きをする粒子はめっきり減るはずだ図2⁠。

図2 オブジェクトの集まりから激しく飛び出す粒子は減る
</span>図2 オブジェクトの集まりから激しく飛び出す粒子は減る
コード1 オブジェクト同士が引合う力の及ぶ範囲をかぎった
var stage;
var stageWidth;
var stageHeight;
var balls = [];
var ballCount = 25;
var ratio = 1 / 2000;
var limit = 100 * 100;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  stageWidth = canvasElement.width;
  stageHeight = canvasElement.height;
  for (var i = 0; i < ballCount; i++) {
    var nX = Math.random() * stageWidth;
    var nY = Math.random() * stageHeight;
    var velocityX = (Math.random() - 0.5) * 5;
    var velocityY = (Math.random() - 0.5) * 5;
    var ball = createBall(3, "black", nX, nY, velocityX, velocityY);
    balls.push(ball);
    stage.addChild(ball);
  }
  createjs.Ticker.addEventListener("tick", move);
}
function createBall(radius, color, nX, nY, velocityX, velocityY) {
  var ball = new createjs.Shape();
  drawBall(ball.graphics, radius, color);
  ball.x = nX;
  ball.y = nY;
  ball.velocityX = velocityX;
  ball.velocityY = velocityY;
  return ball;
}
function drawBall(myGraphics, radius, color) {
  myGraphics.beginFill(color);
  myGraphics.drawCircle(0, 0, radius);
}
function move(eventObject) {
  for (var i = 0; i < ballCount; i++) {
    var ball = balls[i];
    var nX = ball.x;
    var nY = ball.y;
    nX += ball.velocityX;
    nY += ball.velocityY;
    ball.x = roll(nX, stageWidth);
    ball.y = roll(nY, stageHeight);
  }
  for (i = 0; i < ballCount - 1; i++) {
    var ball0 = balls[i];
    for (var j = i + 1; j < ballCount; j++) {
      var ball1 = balls[j];
      spring(ball0, ball1);
    }
  }
  stage.update();
}
function roll(value, length) {
  if (value > length) {
    value -= length;
  } else if (value < 0) {
    value += length;
  }
  return value;
}
function spring(object0, object1) {
  var distanceX = object1.x - object0.x;
  var distanceY = object1.y - object0.y;
  var squareX = distanceX * distanceX;
  var squareY = distanceY * distanceY;
  if (squareX + squareY < limit) {
    var accelX = distanceX * ratio;
    var accelY = distanceY * ratio;
    object0.velocityX += accelX;
    object0.velocityY += accelY;
    object1.velocityX -= accelX;
    object1.velocityY -= accelY;
  }
}

引合うオブジェクト同士の直線で結んでみる

つぎは、引合う力が働いているオブジェクト同士を線で結んでみよう。その下ごしらえとして、線を描くためのオブジェクトをステージに置く。つぎのように、Shapeオブジェクト(background)をつくって、Stageオブジェクトの子に加えた。また、粒子や線の色が変えやすいように、カラー値は変数(colorInt)に定めた。なお、Graphicsオブジェクトに描画するときの色は文字列で定めるため、カラー値を静的メソッドGraphics.getRGB()で変換する。

var colorInt = 0x0;
var background = new createjs.Shape();
function initialize() {
  var color = createjs.Graphics.getRGB(colorInt);

  stage.addChild(background);
  for (var i = 0; i < ballCount; i++) {

    // var ball = createBall(3, "black", nX, nY, velocityX, velocityY);
    var ball = createBall(3, color, nX, nY, velocityX, velocityY);

  }

}

線の描画は、以下のようにTicker.tickイベントのリスナー関数(move())から始める。描画用のオブジェクト(background)については、まず前のイベントのとき描かれた線をGraphics.clear()メソッドで消しておかなければならない。

線は力の働くオブジェクトの間に引くので、引合う力を計算する関数(spring())の仕事になる。ふたつのオブジェクトの座標は力の計算と線描に使うため、変数(_0x、_0y、_1x、_1y)にとっておく。そして、力が働くかどうかif条件で確かめた後、新たに定める線描の関数(drawLine())を呼出す。この関数(drawLine())は、線の太さと色、2点のxy座標という引数にもとづいて直線を描く。

function move(eventObject) {

  background.graphics.clear();
  for (i = 0; i < ballCount - 1; i++) {

    for (var j = i + 1; j < ballCount; j++) {

      spring(ball0, ball1);
    }
  }
  stage.update();
}

function spring(object0, object1) {
  var _0x = object0.x;
  var _0y = object0.y;
  var _1x = object1.x;
  var _1y = object1.y;
  // var distanceX = object1.x - object0.x;
  // var distanceY = object1.y - object0.y;
  var distanceX = _1x - _0x;
  var distanceY = _1y - _0y;

  if (squareX + squareY < limit) {
    var color = createjs.Graphics.getRGB(colorInt);

    drawLine(1, color, _0x, _0y, _1x, _1y);
  }
}
function drawLine(stroke, color, beginX, beginY, endX, endY) {
  var myGraphics = background.graphics;
  myGraphics.setStrokeStyle(stroke);
  myGraphics.beginStroke(color);
  myGraphics.moveTo(beginX, beginY);
  myGraphics.lineTo(endX, endY);
}

これで、引合う力が働くオブジェクト同士が直線で結ばれる図3⁠。JavaScript全体をまとめたのが以下のコード2だ。線が加わることで、アニメーションの表現が有機的になった。ただ、直線が強すぎて粒子は見えにくくなってしまった。

図3 引合う粒子の間が線で結ばれる
</span>図3 引合う粒子の間が線で結ばれる
コード2 引合うオブジェクト同士を直線で結ぶ
var stage;
var stageWidth;
var stageHeight;
var balls = [];
var ballCount = 25;
var ratio = 1 / 2000;
var limit = 100 * 100;
var colorInt = 0x0;
var background = new createjs.Shape();
function initialize() {
  var color = createjs.Graphics.getRGB(colorInt);
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  stageWidth = canvasElement.width;
  stageHeight = canvasElement.height;
  stage.addChild(background);
  for (var i = 0; i < ballCount; i++) {
    var nX = Math.random() * stageWidth;
    var nY = Math.random() * stageHeight;
    var velocityX = (Math.random() - 0.5) * 5;
    var velocityY = (Math.random() - 0.5) * 5;
    var ball = createBall(3, color, nX, nY, velocityX, velocityY);
    balls.push(ball);
    stage.addChild(ball);
  }
  createjs.Ticker.addEventListener("tick", move);
}
function createBall(radius, color, nX, nY, velocityX, velocityY) {
  var ball = new createjs.Shape();
  drawBall(ball.graphics, radius, color);
  ball.x = nX;
  ball.y = nY;
  ball.velocityX = velocityX;
  ball.velocityY = velocityY;
  return ball;
}
function drawBall(myGraphics, radius, color) {
  myGraphics.beginFill(color);
  myGraphics.drawCircle(0, 0, radius);
}
function move(eventObject) {
  for (var i = 0; i < ballCount; i++) {
    var ball = balls[i];
    var nX = ball.x;
    var nY = ball.y;
    nX += ball.velocityX;
    nY += ball.velocityY;
    ball.x = roll(nX, stageWidth);
    ball.y = roll(nY, stageHeight);
  }
  background.graphics.clear();
  for (i = 0; i < ballCount - 1; i++) {
    var ball0 = balls[i];
    for (var j = i + 1; j < ballCount; j++) {
      var ball1 = balls[j];
      spring(ball0, ball1);
    }
  }
  stage.update();
}
function roll(value, length) {
  if (value > length) {
    value -= length;
  } else if (value < 0) {
    value += length;
  }
  return value;
}
function spring(object0, object1) {
  var _0x = object0.x;
  var _0y = object0.y;
  var _1x = object1.x;
  var _1y = object1.y;
  var distanceX = _1x - _0x;
  var distanceY = _1y - _0y;
  var squareX = distanceX * distanceX;
  var squareY = distanceY * distanceY;
  if (squareX + squareY < limit) {
    var color = createjs.Graphics.getRGB(colorInt);
    var accelX = distanceX * ratio;
    var accelY = distanceY * ratio;
    object0.velocityX += accelX;
    object0.velocityY += accelY;
    object1.velocityX -= accelX;
    object1.velocityY -= accelY;
    drawLine(1, color, _0x, _0y, _1x, _1y);
  }
}
function drawLine(stroke, color, beginX, beginY, endX, endY) {
  var myGraphics = background.graphics;
  myGraphics.setStrokeStyle(stroke);
  myGraphics.beginStroke(color);
  myGraphics.moveTo(beginX, beginY);
  myGraphics.lineTo(endX, endY);
}

オブジェクトが離れるほど線を薄くする

粒子を見やすくするには、直線を細くするかアルファでも掛ければよい。だが、もう一歩進めて、オブジェクトが離れるほどアルファを下げてみよう。

すると改めて、ふたつのオブジェクトの距離を求めることになる。そこで、以下のように、まず力の働く最大距離を変数(max)にとった。そして、引き合うかどうかのif条件は平方(2乗)のまま比べ、線で結ぶことになってから平方根で距離(distance)を求める。無駄な計算は避けるためだ。

アルファは、Graphics.getRGB()メソッドの第2引数で定める。オブジェクト同士がくっついたら1、最大の距離(max)で0になるように比率を定めた。

var max = 100;
// var limit = 100 * 100;
var limit = max * max;

function spring(object0, object1) {

  if (squareX + squareY < limit) {
    var distance = Math.sqrt(squareX + squareY);
    var color = createjs.Graphics.getRGB(colorInt, (max - distance) / max);

    drawLine(1, color, _0x, _0y, _1x, _1y);
  }
}

これで、オブジェクト同士が離れるほど、結ばれる直線は薄くなり、最大距離で線は消える。オブジェクトが引合う力は距離に比例させているので、アルファが見た目で示していることにもなる。JavaScriptの全体は、以下のコード3のとおりだ。

図4 粒子が離れるほど線は薄くなる
</span>図4 粒子が離れるほど線は薄くなる
コード3 オブジェクト同士の距離に比例して線を薄くした
var stage;
var stageWidth;
var stageHeight;
var balls = [];
var ballCount = 25;
var ratio = 1 / 2000;
var max = 100;
var limit = max * max;
var colorInt = 0x0;
var background = new createjs.Shape();
function initialize() {
  var color = createjs.Graphics.getRGB(colorInt);
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  stageWidth = canvasElement.width;
  stageHeight = canvasElement.height;
  stage.addChild(background);
  for (var i = 0; i < ballCount; i++) {
    var nX = Math.random() * stageWidth;
    var nY = Math.random() * stageHeight;
    var velocityX = (Math.random() - 0.5) * 5;
    var velocityY = (Math.random() - 0.5) * 5;
    var ball = createBall(3, color, nX, nY, velocityX, velocityY);
    balls.push(ball);
    stage.addChild(ball);
  }
  createjs.Ticker.addEventListener("tick", move);
}
function createBall(radius, color, nX, nY, velocityX, velocityY) {
  var ball = new createjs.Shape();
  drawBall(ball.graphics, radius, color);
  ball.x = nX;
  ball.y = nY;
  ball.velocityX = velocityX;
  ball.velocityY = velocityY;
  return ball;
}
function drawBall(myGraphics, radius, color) {
  myGraphics.beginFill(color);
  myGraphics.drawCircle(0, 0, radius);
}
function move(eventObject) {
  for (var i = 0; i < ballCount; i++) {
    var ball = balls[i];
    var nX = ball.x;
    var nY = ball.y;
    nX += ball.velocityX;
    nY += ball.velocityY;
    ball.x = roll(nX, stageWidth);
    ball.y = roll(nY, stageHeight);
  }
  background.graphics.clear();
  for (i = 0; i < ballCount - 1; i++) {
    var ball0 = balls[i];
    for (var j = i + 1; j < ballCount; j++) {
      var ball1 = balls[j];
      spring(ball0, ball1);
    }
  }
  stage.update();
}
function roll(value, length) {
  if (value > length) {
    value -= length;
  } else if (value < 0) {
    value += length;
  }
  return value;
}
function spring(object0, object1) {
  var _0x = object0.x;
  var _0y = object0.y;
  var _1x = object1.x;
  var _1y = object1.y;
  var distanceX = _1x - _0x;
  var distanceY = _1y - _0y;
  var squareX = distanceX * distanceX;
  var squareY = distanceY * distanceY;
  if (squareX + squareY < limit) {
    var distance = Math.sqrt(squareX + squareY);
    var color = createjs.Graphics.getRGB(colorInt, (max - distance) / max);
    var accelX = distanceX * ratio;
    var accelY = distanceY * ratio;
    object0.velocityX += accelX;
    object0.velocityY += accelY;
    object1.velocityX -= accelX;
    object1.velocityY -= accelY;
    drawLine(1, color, _0x, _0y, _1x, _1y);
  }
}
function drawLine(stroke, color, beginX, beginY, endX, endY) {
  var myGraphics = background.graphics;
  myGraphics.setStrokeStyle(stroke);
  myGraphics.beginStroke(color);
  myGraphics.moveTo(beginX, beginY);
  myGraphics.lineTo(endX, endY);
}

「Node Garden」のお題はでき上がりだ。jsdo.itでは背景は黒くして、粒子と線を白で描いた。このアニメーションも長く再生していると、粒子の動きはかなり激しくなる。それを避けるには、パラメータを調整してもよいし、簡単なのは制限速度を設けることだろう。これらは、読者のみなさんそれぞれがお試しいただきたい。次回から、また新たなお題に取組む。

おすすめ記事

記事・ニュース一覧