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

第27回ランダムな楕円軌道を描くパーティクルの仕上げ

前回の第26回パーティクルを使い回しながらさまざまな楕円軌道で回すでは、速度からランダムに導いた楕円軌道でパーティクルを回してみた。今回は、そのランダムをさらにばらつかせる。そして、楕円運動の速度から位置を求める方程式についても、数学的な説明を補おう。

パーティクルの動き始めもランダムにする

前回できあがりの第26回コード4パーティクル100個を使い回しながらさまざまな楕円軌道で回すでは、すべてのパーティクルが一斉に動き出す。そのため、パーティクルの動きが揃って同じ位置に集まってしまう図1⁠。つぎのようにパーティクルの数(total)を増やしてみるとわかりやすい。ただし、オブジェクトを使い回して設定し直す関数(resetParticle())により、つぎの再設定時(lifetime)がランダムに定められるため、動きのばらつきはやがて広がる第26回100個のパーティクルを使い回して再設定する参照⁠⁠。

var total = 200;   // 100;

function resetParticle(particle) {

  var lifetime = Math.random()* total | 0;
  particle.reset(center.x, center.y, radius, angle, lifetime);
}
図1 初めパーティクルの動き始めが揃って同じ位置に集まる
図1 初めパーティクルの動き始めが揃って同じ位置に集まる

ここはやはり、動き始めについても、ランダムな時間差を与えることにする。クラス(Particle)のパーティクル設定のメソッド(reset())に加える引数(delay)で、動き出すまでにタメを与える。引数に与えられるランダムな正数値は、プロパティ(delay)にもつ。そして、カウンタのプロパティ(index)をアニメーションのたびに加算して、タメの値を超えたら動かす。カウントアップしながらふたつのプロパティ値を比べるメソッド(isMovable())は、クラスに新たに備えた。

// Particle.prototype.reset = function(x, y, radius, angle, lifetime) {
Particle.prototype.reset = function(x, y, radius, angle, lifetime, delay) {

  this.delay = delay;
  this.index = 0;
};

Particle.prototype.isMovable = function() {
  var movable = (++this.index > this.delay);
  return movable;
};

アニメーションの関数(tick())は以下のように、パーティクルを動かしてよいかどうか、上記で加えたメソッド(isMovable())により確かめたうえで動かすようにした。

タメの値(delay)は、パーティクルを設定する関数(resetParticle())から、ランダムに定めてオブジェクト(particle)のメソッド(reset())に渡す。ただし、もとになる値を関数が引数として受け取っている。これは初期設定の関数(initialize())がパーティクルを予め定めた数(total)つくるとき、forループのカウンタ変数(i)がもつ値だ。すると、ループの初めにつくられたオブジェクトは早めに動き出す。これで、パーティクルが揃ってタメすぎることを防げる。

function initialize() {

  for(var i = 0; i < total; i++) {

    // resetParticle(particle);
    resetParticle(particle, i);

  }

}
// function resetParticle(particle) {
function resetParticle(particle, delay) {

  delay *= Math.random();
  // particle.reset(center.x, center.y, radius, angle, lifetime);
  particle.reset(center.x, center.y, radius, angle, lifetime, delay);
}
function tick() {

  for(var i = 0; i < count; i++) {
    var particle = particles[i];
    // moveParticle(particle);
    if (particle.isMovable()) {
      moveParticle(particle, i);
    }
  }

}
// function moveParticle(particle) {
function moveParticle(particle, index) {
  if (particle.lifetime > 0) {

  } else {
    // resetParticle(particle);
    resetParticle(particle, index);
  }
}

これらの書替えをおこなったパーティクルのクラス(Particle)と楕円軌道でアニメーションさせるJavaScriptコードは、それぞれコード1およびコード2としてまとめた。これで、パーティクルの動き始めにそれぞれランダムなタメが加わる図2⁠。オブジェクトが使い回されて再設定されるときにも、このタメは利いているはずだ。わかりづらいだろうが、画面の真ん中に一瞬留まるパーティクルができている。

図2 パーティクルの動き始めにランダムなタメが加わってから楕円軌道を回り出す
図2 パーティクルの動き始めにランダムなタメが加わってから楕円軌道を回り出す
コード1 楕円軌道の動き始めにタメが加わるパーティクルのクラス
function Particle(radius, color) {
  this.initialize();
  this.graphics.beginFill(color)
  .drawCircle(0, 0, radius)
  .endFill();
  this.compositeOperation = "lighter";
}
Particle.prototype = new createjs.Shape();
Particle.prototype.reset = function(x, y, radius, angle, lifetime, delay) {
  this.x = x;
  this.y = y;
  this.speedX = radius * Math.cos(angle);
  this.speedY = radius * Math.sin(angle);
  this.lifetime = lifetime;
  this.angle = 0;
  this.delay = delay;
  this.index = 0;
};
Particle.prototype.move = function(advance) {
  var angle = this.angle + advance;
  var velocityX = this.speedX * Math.sin(angle);
  var velocityY = this.speedY * Math.cos(angle);
  this.x += velocityX;
  this.y += velocityY;
  this.angle = angle;
  this.lifetime--;
};
Particle.prototype.isMovable = function() {
  var movable = (++this.index > this.delay);
  return movable;
};
コード2 パーティクルの動き始めにランダムなタメを加えてから楕円軌道で回す
var stage;
var total = 200;
var center = new createjs.Point();
var particles = [];
var fading = 0.04;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  var stageWidth = canvasElement.width;
  var stageHeight = canvasElement.height;
  stage = new createjs.Stage(canvasElement);
  stage.autoClear = false;
  center.x = stageWidth / 2;
  center.y = stageHeight / 2;
  for(var i = 0; i < total; i++) {
    var radius = 1 + Math.random() * 4;
    var particle = new Particle(radius, "#0016E9");
    resetParticle(particle, i);
    particles.push(particle);
    stage.addChild(particle);
  }
  addBackground(stageWidth, stageHeight, fading);
  createjs.Ticker.timingMode = createjs.Ticker.RAF;
  createjs.Ticker.addEventListener("tick", tick);
}
function resetParticle(particle, delay) {
  var radius = 1 + Math.random();
  var angle = Math.random() * Math.PI * 2;
  var lifetime = Math.random()* total | 0;
  delay *= Math.random();
  particle.reset(center.x, center.y, radius, angle, lifetime, delay);
}
function tick() {
  var count = particles.length;
  for(var i = 0; i < count; i++) {
    var particle = particles[i];
    if (particle.isMovable()) {
      moveParticle(particle, i);
    }
  }
  stage.update();
}
function moveParticle(particle, index) {
  if (particle.lifetime > 0) {
    particle.move(Math.PI / 90);
  } else {
    resetParticle(particle, index);
  }
}
function addBackground(width, height, alpha) {
  var background = new createjs.Shape();
  background.graphics.beginFill("black")
  .drawRect(0, 0, width, height)
  .endFill();
  stage.addChild(background);
  background.alpha = alpha;
}

メソッドを動的に切換える

パーティクルのアニメーション表現そのものは、これで決まりとする。その代わりにもうひと手間加えたいのは、処理の組立ての改善だ。前掲コード1では、それぞれのパーティクルを動かし始めてよいかどうかは、ブール値を返すクラス(Particle)のメソッド(isMovable())として備えた。そして、前掲コード2のパーティクルをアニメーションさせる関数(tick())で、メソッドの戻り値を確かめて動かした。

Particle.prototype.isMovable = function() {
  var movable = (++this.index > this.delay);
  return movable;
};
function tick() {

    if (particle.isMovable()) {
      moveParticle(particle, i);
    }

}

しかし、ひとたびタメ終わったら、その後もいちいち動かしていいかメソッド(isMovable())に尋ねるのは煩わしい。動かし始めるかどうか教えて済ますのでなく、実際にパーティクルを楕円軌道で回すところまで、クラス(Particle)で扱ってしまえないだろうか。つまり、前掲コード2のアニメーションの関数(tick())から、つぎのようにパーティクルを動かす関数(moveParticle())につないでオブジェクト(particle)のメソッド(move())を呼び出してしまい、メソッド側でタメるか回すかを切り換えたい。

function tick() {

    // if (particle.isMovable()) {
    moveParticle(particle, i);
    // }

}
function moveParticle(particle, index) {

    particle.move(Math.PI / 90);

}

さて、前掲コード1のパーティクルのクラス(Particle)を書き替えよう。上記のパーティクルを動かす関数(moveParticle())から呼び出すメソッド名は、以下のようにプロパティ(move)とし、メソッド名(_move())は変えた。そして、パーティクル設定のメソッド(reset)で、プロパティにはカウンタプロパティ(index)加算のメソッド(_movable)を代入した。

そして、カウントアップした値がタメのプロパティ値(delay)を超えたら、上記パーティクルを動かす関数(moveParticle())から呼出すメソッド(move)の参照先は、楕円軌道を回るメソッド(_move)に置き換える。これで、動き始めはタメてたうえで、楕円軌道を回るパーティクルのふるまいがクラス(Particle)で扱えるようになった。

Particle.prototype.reset = function(x, y, radius, angle, lifetime, delay) {

  this.move = this._movable;
};
// Particle.prototype.move = function(advance) {
Particle.prototype._move = function(advance) {

};
// Particle.prototype.isMovable = function() {
Particle.prototype._movable = function(advance) {
  // var movable = (++this.index > this.delay);
  if (++this.index > this.delay) {
    this.move = this._move;
  }
  // return movable;
};

これらの手直しをしたパーティクルのクラスの定めとそれを用いたJavaScriptコードは、それぞれコード3コード4にまとめた。あわせて、同じコードをjsdo.itにもサンプルとして掲げた。

コード3 パーティクルが動くメソッドを動的に切換えるクラス
function Particle(radius, color) {
  this.initialize();
  this.graphics.beginFill(color)
  .drawCircle(0, 0, radius)
  .endFill();
  this.compositeOperation = "lighter";
}
Particle.prototype = new createjs.Shape();
Particle.prototype.reset = function(x, y, radius, angle, lifetime, delay) {
  this.x = x;
  this.y = y;
  this.speedX = radius * Math.cos(angle);
  this.speedY = radius * Math.sin(angle);
  this.lifetime = lifetime;
  this.angle = 0;
  this.delay = delay;
  this.index = 0;
  this.move = this._movable;
};
Particle.prototype._move = function(advance) {
  var angle = this.angle + advance;
  var velocityX = this.speedX * Math.sin(angle);
  var velocityY = this.speedY * Math.cos(angle);
  this.x += velocityX;
  this.y += velocityY;
  this.angle = angle;
  this.lifetime--;
};
Particle.prototype._movable = function(advance) {
  if (++this.index > this.delay) {
    this.move = this._move;
  }
};
コード4 アニメーションはパーティクルの同じメソッドを呼び続けてふるまいはクラスに任せる
var stage;
var total = 200;
var center = new createjs.Point();
var particles = [];
var fading = 0.04;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  var stageWidth = canvasElement.width;
  var stageHeight = canvasElement.height;
  stage = new createjs.Stage(canvasElement);
  stage.autoClear = false;
  center.x = stageWidth / 2;
  center.y = stageHeight / 2;
  for(var i = 0; i < total; i++) {
    var radius = 1 + Math.random() * 4;
    var particle = new Particle(radius, "#0016E9");
    resetParticle(particle, i);
    particles.push(particle);
    stage.addChild(particle);
  }
  addBackground(stageWidth, stageHeight, fading);
  createjs.Ticker.timingMode = createjs.Ticker.RAF;
  createjs.Ticker.addEventListener("tick", tick);
}
function resetParticle(particle, delay) {
  var radius = 1 + Math.random();
  var angle = Math.random() * Math.PI * 2;
  var lifetime = Math.random()* total | 0;
  delay *= Math.random();
  particle.reset(center.x, center.y, radius, angle, lifetime, delay);
}
function tick() {
  var count = particles.length;
  for(var i = 0; i < count; i++) {
    var particle = particles[i];
    moveParticle(particle, i);
  }
  stage.update();
}
function moveParticle(particle, index) {
  if (particle.lifetime > 0) {
    particle.move(Math.PI / 90);
  } else {
    resetParticle(particle, index);
  }
}
function addBackground(width, height, alpha) {
  var background = new createjs.Shape();
  background.graphics.beginFill("black")
  .drawRect(0, 0, width, height)
  .endFill();
  stage.addChild(background);
  background.alpha = alpha;
}

等速円運動と速度

結びとして、前回楕円軌道を速度から導くやり方としてご紹介したつぎの式について、数学的な説明を補おう。興味がある読者向けの解説なので、数学が苦手あるいは結論だけわかれば理屈は気にしないという方はここまでお読みいただいただけで構わない。

現行角度 += 角速度
水平速度 = -半径×角速度×sin(現行角度)
垂直速度 = 半径×角速度×cos(現行角度)
水平座標(x) += 水平速度
垂直座標(y) += 垂直速度

速度とは、時間あたりで動く位置の変化だ。たとえば秒速が与えられたら、1秒後の位置は速度の値を加えれば求まる。等速直線運動なら、毎秒つぎのように一定値(定数)を加えればアニメーションが表せる。なお、水平(x)も垂直(y)も考え方は同じなので、ひとつの式で示した(ベクトルと考えればよい⁠⁠。

速度 = 定数
位置 += 速度

等速直線運動でない場合、速度は時々刻々変わる。そのときは、時間の関数として表す。たとえば、自由落下運動(初速は0とされる)であれば、時間が経つにしたがって重力加速度が加わる[1]⁠。なお、重力加速度は定数だ。

速度 = 重力加速度×時間
位置 += 速度

数学では、自由落下は位置(x)の時間(t)に対する2次方程式で表される。そして、速度(v)は一般に、位置の方程式を時間で微分した導関数(x')として求める。したがって、自由落下の速度は時間に比例する。比例係数が加速度だ。

x = at2
v = x' = 2at

第25回「楕円軌道に残像を描きながら回るパーティクル」楕円軌道を描いて動かすでご紹介した円軌道の位置(x, y)は、半径(r)と時間(t)あたりに回る角度(ω)にもとづいてつぎの方程式で与えられる(原点を中心とする⁠⁠。

x = r cos(ωt)
y = r sin(ωt)

これらの方程式を微分すれば速度(x', y')が求まる。これが第26回「パーティクルを使い回しながらさまざまな楕円軌道で回す」楕円軌道を速度から導くでご紹介した速度の式だ。さらに詳しく知りたい読者は、等速円運動を三角関数の微分で表すをお読みいただきたい。

x' = -rω sin(ωt)
y' = rω cos(ωt)

時間の単位をフレームとし※1参照⁠⁠、フレームあたりの回転角を角速度と定めれば、フレームごとに現在位置に速度を加えることで円運動のアニメーションが表せる。それが本項の初めに示した式だ。

速度 += フレームあたりの加速度
位置 += 速度

おすすめ記事

記事・ニュース一覧