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

第6回アニメーションする粒子間に引合う力を加える

今回のお題は、ランダムに動く粒子の間に相互作用を加えたアニメーションだ。ランダムな動きにオブジェクト同士のインタラクションを交えると、有機的なアニメーションになってくる。Jared Tarbell氏がかつてNode Gardenという作品を発表された。Keith Peters氏はそのノード間にバネ運動を加えて、著書ActionScript 3.0アニメーションで解説している。本稿はその表現をCreateJSで書いてみようと思う。

ステージのランダムな位置に、ランダムな方向と速さで動く小さなオブジェクトをたくさん置く。それらのオブジェクトの間に引っぱり合うバネのような力を与え、さらに互いを線で結ぶ図1⁠。できあがりのコードは、jsdo.itに上げてある(例によって後で書き直すかもしれないが⁠⁠。

図1 ランダムに動くオブジェクトの間に引合う力を与えて線で結ぶ
図1 ランダムに動くオブジェクトの間に引合う力を与えて線で結ぶ

ランダムに置いたオブジェクトをランダムな向きと速さで動かす

取りあえず、オブジェクトの互いの間にかかる力は脇に置いておこう。ステージ上のランダムなところから、ランダムな向きと速さで等速に動かす。まず、予め定めた数のオブジェクトをステージ領域のランダムな位置につくるのが、次のscript要素だ。

body要素のonload属性で呼出す初期化の関数(initialize())は、for文であらかじめ定めた数(ballCount)のインスタンス(ball)をステージ領域のランダムな位置に置く。円形のShapeインスタンスをつくる関数(createBall())は別に定め、引数に半径と色、およびxy座標を渡している。また、インスタンスのShape.graphicsプロパティに円を描くのも別関数(drawBall())とした。引数には、Graphicsオブジェクトと半径、および色を渡す。


<script src="http://code.createjs.com/easeljs-0.6.0.min.js"></script>
<script>
var stage;
var stageWidth;
var stageHeight;

var ballCount = 25;
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);
    // ...[中略]...
    stage.addChild(ball);
  }
  stage.update();
}
function createBall(radius, color, nX, nY) {
  var ball = new createjs.Shape();
  drawBall(ball.graphics, radius, color);
  ball.x = nX;
  ball.y = nY;
  // ...[中略]...
  return ball;
}
function drawBall(myGraphics, radius, color) {
  myGraphics.beginFill(color);
  myGraphics.drawCircle(0, 0, radius);
}
</script>

つぎに、オブジェクトをランダムな向きと速さで動かす。つくったオブジェクトは変数の配列(balls)に入れ、Ticker.tickイベントのリスナー関数(move())でアニメーションさせる。xy軸方向に進む向きと速さは、インスタンスごとに変えるので、速度のプロパティ(velocityXとvelocityY)としてもたせる。そのため、インスタンスをつくる関数(createBall())の引数に、これらの値を加えた。

Ticker.tickイベントのリスナーに定めたアニメーションの関数(move())は、オブジェクト(ball)の速度のプロパティ値(velocityXとvelocityY)をそれぞれの座標に加えて動かす。ただし、座標値がステージの端を超えたら反対側の端に戻るよう、周期化する関数(roll())で補正している。


var balls = [];

function initialize() {
  // ...[中略]...
  for (var i = 0; i < ballCount; i++) {
    // ...[中略]...
    // var ball = createBall(3, "black", nX, nY);
    var ball = createBall(3, "black", nX, nY, velocityX, velocityY);
    balls.push(ball);
    // ...[中略]...
  }
  // stage.update();
  createjs.Ticker.addEventListener("tick", move);
}
// function createBall(radius, color, nX, nY) {
function createBall(radius, color, nX, nY, velocityX, velocityY) {
  // ...[中略]...
  ball.velocityX = velocityX;
  ball.velocityY = velocityY;
  return ball;
}

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);
  }
  stage.update();
}
function roll(value, length) {
  if (value > length) {
    value -= length;
  } else if (value < 0) {
    value += length;
  }
  return value;
}

これらのsctipt要素をまとめたのが、次のコード1だ。ステージにランダムに散らばったオブジェクトは、ランダムな向きと方向に等速で動き続ける図2⁠。オブジェクトがステージの外に出ると、反対側の端から表れる。オブジェクトの間には、とくに相互作用はない。

図2 ステージにランダムに散らばったオブジェクトがランダムな向きと速さで動く
図2 ステージにランダムに散らばったオブジェクトがランダムな向きと速さで動く
コード1 ステージにランダムに置いたオブジェクトをランダムな向きと速さで等速に動かす

<script src="http://code.createjs.com/easeljs-0.6.0.min.js"></script>
<script>
var stage;
var stageWidth;
var stageHeight;
var balls = [];
var ballCount = 25;
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);
  }
  stage.update();
}
function roll(value, length) {
  if (value > length) {
    value -= length;
  } else if (value < 0) {
    value += length;
  }
  return value;
}
</script>

オブジェクト同士の間にバネのような引合う力を加える

いよいよ、オブジェクト同士がバネでつながれているように、互いに引合う力を加えよう。そのためには、バネにかかる力の性質を知らなければならない。もっとも、その法則はきわめて単純だ。バネの力は伸びに比例する。これをフックの法則と呼ぶ。

力は物体の加速度となる。したがって、ふたつのオブジェクトの間の距離に比例した加速度を、それぞれのオブジェクトの速度(velocityXとvelocityY)に加えればよい。そのための関数(spring())を次のように定めた。引数は、加速度を与えるふたつのオブジェクトだ。アニメーションの関数(move())からfor文で、すべてのオブジェクトの組合わせを引数にして呼出す。

速度(velocityXとvelocityY)と同じく、加速度(accelXとaccelY)もxy座標に分けて計算できる。比例係数はグローバルな変数(ratio)に宣言した。オブジェクト同士が引合う力なので、加速度の正負はふたつのオブジェクトで逆にしている。

var ratio = 1 / 2000;

function move(eventObject) {
  // ...[中略]...
  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);
    }
  }
  // ...[中略]...
}

function spring(object0, object1) {
  var distanceX = object1.x - object0.x;
  var distanceY = object1.y - object0.y;
  var accelX = distanceX * ratio;
  var accelY = distanceY * ratio;
  object0.velocityX += accelX;
  object0.velocityY += accelY;
  object1.velocityX -= accelX;
  object1.velocityY -= accelY;
}

加速度はオブジェクト同士の総当たりで決まるので、互いに打ち消したり、強めたりする。ただ全体としては、ひとつの固まりに集まっては、離れることの繰返しになる図3⁠。手を加えたJavaScriptコードの全体は、次のコード2のとおりだ。

図3 オブジェクトがひと固まりに集まっては離れる
図3 オブジェクトがひと固まりに集まっては離れる 図3 オブジェクトがひと固まりに集まっては離れる
コード2 オブジェクト同士の間にバネのような引合う力を加える

var stage;
var stageWidth;
var stageHeight;
var balls = [];
var ballCount = 25;
var ratio = 1 / 2000;
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 accelX = distanceX * ratio;
  var accelY = distanceY * ratio;
  object0.velocityX += accelX;
  object0.velocityY += accelY;
  object1.velocityX -= accelX;
  object1.velocityY -= accelY;
}

この動きは、それなりに面白い。ただ、しばらく放っておくと、多くのオブジェクトが激しく行き来するようになる。それは、仲間から外れたオブジェクトが、より大きな加速度を得るからだ。ひとたび仲間外れになると、集まりには戻りにくくなる。そうして、次第にはぐれ者が増えてしまうのだ。

そこで次回は、はぐれ者をむやみに増やさないよう、加速度が加わる範囲をかぎる。そして、オブジェクト同士を線で結びつけてみよう。

まだ未完成ではあるが、ここまでのコードを参考までにjsdo.itに掲げておく。

おすすめ記事

記事・ニュース一覧