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

第26回パーティクルを使い回しながらさまざまな楕円軌道で回す

前回の第25回「楕円軌道に残像を描きながら回るパーティクル」では、黒い背景に残像が残るパーティクルを楕円軌道で重ねた(再掲第25回図3⁠。そして、楕円軌道の中心はCanvasの真ん中に定めた。今回は、違った方程式で楕円軌道を描きたい。

第25回図3 楕円軌道の残像に重なるインスタンスが明るさを増す(再掲)
第25回図3 楕円軌道の残像に重なるインスタンスが明るさを増す(再掲)

楕円軌道を速度から導く

前回のサンプルの楕円軌道は、第25回コード1パーティクルを定めるクラスにメソッド(move())としてつぎのように定めた。

Particle.prototype.move = function(advance) {
  var angle = this.angle + advance;
  this.x = this.centerX + this.radius * Math.cos(angle);
  this.y = this.centerY + this.radius * 0.8 * Math.sin(angle);
  this.angle = angle;
};

このコードでインスタンスが楕円軌道を動くのは、つぎのような円周上の座標を導く方程式にもとづく。円の中心座標と半径が与えられると、現行の角度を変えれば座標が円周上を動く。そして、半径を水平と垂直で違う長さにすると、軌道が楕円になった。なお、角速度は描画のたびに加える定数の角度だ。

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

上記の方程式は、現行角度から直ちに軌道上の座標を求めた。しかし、一般に物体の運動は、現在の座標に速度を加えても導ける。円軌道を速度から定めたのが、つぎの方程式だ。この場合、中心座標が要らないことに注目してほしい。また、半径を水平と垂直で変えれば、やはり楕円軌道が描ける。

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

細かい説明より先に、この方程式でパーティクルのクラス(Particle)に定めるメソッドを書き替えてみよう。クラスの定め全体は後にコード1としてまとめた。抜き書きしたJavaScriptコードがつぎのとおりだ。アニメーションのメソッド(move())だけでなく、パーティクルのインスタンスにプロパティを定めるメソッド(reset())にも手が加わっている。

Particle.prototype.reset = function(x, y, radius, angle) {
  // this.centerX = x;
  // this.centerY = y;
  this.x = x;
  this.y = y;
  this.speedX = radius * Math.cos(angle);
  this.speedY = radius * Math.sin(angle);
  // this.radius = radius;
  this.angle = 0;    // angle;
};
Particle.prototype.move = function(advance) {
  var angle = this.angle + advance;
  // this.x = this.centerX + this.radius * Math.cos(angle);
  // this.y = this.centerY + this.radius * 0.8 * Math.sin(angle);
  var velocityX = this.speedX * Math.sin(angle);
  var velocityY = this.speedY * Math.cos(angle);
  this.x += velocityX;
  this.y += velocityY;
  this.angle = angle;
};

先に、パーティクルのアニメーションのメソッド(move())から見よう。ほぼ上記の方程式にしたがって書き替えただけだ。ただ、sinとcosの値に乗じる「半径×角速度」の値は、アニメーションを始めたら変わることがない。そこで、プロパティを定めるメソッド(reset())で、値はプロパティ(speedXとspeedY)とした(後述ランダムな楕円軌道のバランスをとる参照⁠⁠。

さらに、プロパティを定めるメソッド(reset())では、前述のとおり軌道の中心座標をプロパティにもたなくて済む。また、軌道の半径も、速度の係数に使うだけなので、プロパティには要らない。また、現行角度(angle)は0から始めることにした。

クラス(Particle)からパーティクルをつくってアニメーションさせる第25回コード4楕円軌道の残像に重なったパーティクルが輝きながら消えてゆくにもひとつ手が加わる。インスタンスにプロパティを与える関数(resetParticle())から渡すランダムな半径(radius)の値の範囲はつぎのように変えた。楕円軌道の半径そのものでなく、速度を変える係数として用いられるからだ。

function resetParticle(particle) {
  // var radius = 40 + Math.random() * 50;
  var radius = 1 + Math.random();
  var angle = Math.random() * Math.PI * 2;
  particle.reset(center.x, center.y, radius, angle);
}

楕円軌道で動くパーティクルをつくるクラス(Particle)は、つぎのコード1のように書き替えた。初めにインスタンスが置かれた座標から、速度を決めて、それぞれの楕円軌道で回る図1⁠。

コード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) {
  this.x = x;
  this.y = y;
  this.speedX = radius * Math.cos(angle);
  this.speedY = radius * Math.sin(angle);

  this.angle = 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;
};
図1 パーティクルが初めに置かれたCanvasの中心からそれぞれの楕円軌道を描いて回る
図1 パーティクルが初めに置かれたCanvasの中心からそれぞれの楕円軌道を描いて回る

前掲コード1のクラス(Particle)からパーティクルのインスタンスを10個つくって、ランダムな楕円軌道で回すJavaScriptコードは、前述のとおり1行インスタンスに与えるランダムな係数値が変わるだけだ。だが、動きの違いが確かめやすいように、つぎのコード2にスクリプト全体を掲げた。

コード2 クラスでつくったパーティクル10個をそれぞれの楕円軌道で回す
var stage;
var total = 10;
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);
    particles.push(particle);
    stage.addChild(particle);
  }
  addBackground(stageWidth, stageHeight, fading);
  createjs.Ticker.timingMode = createjs.Ticker.RAF;
  createjs.Ticker.addEventListener("tick", tick);
}
function resetParticle(particle) {
  var radius = 1 + Math.random();
  var angle = Math.random() * Math.PI * 2;
  particle.reset(center.x, center.y, radius, angle);
}
function tick() {
  var count = particles.length;
  for(var i = 0; i < count; i++) {
    var particle = particles[i];
    moveParticle(particle);
  }
  stage.update();
}
function moveParticle(particle) {
  particle.move(Math.PI / 90);
}
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;
}

ランダムな楕円軌道のバランスをとる

前記の速度を求める方程式の中で「半径×角速度」の値には、とくに三角関数は含まれない。クラス(Particle)のプロパティを定めるメソッド(reset())で用いた三角関数Math.cos()Math.sin()メソッド)が果たすのは、ランダムな引数(angle)値から±1の範囲の係数を定めるだけの役割だ。

したがって、引数値は使わずに、たとえばつぎのようにランダムに±1の範囲の値を決めても構わない。ただ、この式から得られるパーティクル10個の楕円軌道は、縮こまった感じになる図2⁠。

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

  // this.speedX = radius * Math.cos(angle);
  // this.speedY = radius * Math.sin(angle);
  this.speedX = radius * (Math.random() * 2 - 1);
  this.speedY = radius * (Math.random() * 2 - 1);

};
図2 10個のパーティクルの楕円軌道が縮こまる
図2 10個のパーティクルの楕円軌道が縮こまる

xとy方向それぞれ独立に±1の範囲のランダムな値を決めると、両方とも0に近い値になることが起こりうる。それに対して、ランダムな引数(angle)からcosとsinで求める値は、一方が0に寄ると、他方は1または-1に近づく。軌道の長さが縮こまることはない。つまり、ランダムに定めた楕円軌道のバランスがとれるのだ。

100個のパーティクルを使い回して再設定する

パーティクルの数をさらに増やそう。ただ、インスタンスを新たにつくり続けるのではなく、ある程度経ったら設定を変えることにして、オブジェクトは使い回す。そこで、アニメーションが描かれる(move())たびにカウントダウンするプロパティ(lifetime)を、つぎのようにパーティクルのクラス(Particle)に加える。値はプロパティを定めるメソッド(reset())の引数とした。

// Particle.prototype.reset = function(x, y, radius, angle) {
Particle.prototype.reset = function(x, y, radius, angle, lifetime) {
  this.x = x;
  this.y = y;
  this.speedX = radius * Math.cos(angle);
  this.speedY = radius * Math.sin(angle);
  this.lifetime = lifetime;
  this.angle = 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)からパーティクルをつくってアニメーションさせるJavaScriptコードもつぎのように手直しする。インスタンスの数(total)は大幅に増やす。また、パーティクルにプロパティを与えるメソッド(reset())呼出しのとき、前述のカウントダウンの値を引数(lifetime)として加えた。この数値はインスタンス数の範囲までのランダムな整数だ[1]⁠。

そして、インスタンスをアニメーションさせる関数(moveParticle())は、カウントダウンのプロパティ(lifetime)値が0になっていなければ、アニメーションのメソッド(move())を呼び出して回す。値が0までカウントダウンされたインスタンスは、プロパティを設定す関数(resetParticle())の呼出しによって使い回している。

var total = 100;    // = 10;

function resetParticle(particle) {
  var radius = 1 + Math.random();
  var angle = Math.random() * Math.PI * 2;
  var lifetime = Math.random()* total | 0;
  // particle.reset(center.x, center.y, radius, angle);
  particle.reset(center.x, center.y, radius, angle, lifetime);
}

function moveParticle(particle) {
  if (particle.lifetime > 0) {
    particle.move(Math.PI / 90);
  } else {
    resetParticle(particle);
  }
}

これで、100個のパーティクルがつくられ、Canvasの真ん中からさまざまな楕円軌道で残像を描きながら回る図3⁠。そして、ランダムに定めたカウントダウンのプロパティ値が0になるたび、インスタンスは設定し直される。ここまで書き替えたクラス(Particle)の定めが以下のコード3で、クラスのインスタンスをつくってアニメーションさせるスクリプトはコード4にまとめて掲げた。

図3 100個のパーティクルがCanvasの中心からさまざまな楕円軌道で残像を描く
図3 100個のパーティクルがCanvasの中心からさまざまな楕円軌道で残像を描く
コード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) {
  this.x = x;
  this.y = y;
  this.speedX = radius * Math.cos(angle);
  this.speedY = radius * Math.sin(angle);
  this.lifetime = lifetime;
  this.angle = 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--;
};
コード4 パーティクル100個を使い回しながらさまざまな楕円軌道で回す
var stage;
var total = 100;
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);
    particles.push(particle);
    stage.addChild(particle);
  }
  addBackground(stageWidth, stageHeight, fading);
  createjs.Ticker.timingMode = createjs.Ticker.RAF;
  createjs.Ticker.addEventListener("tick", tick);
}
function resetParticle(particle) {
  var radius = 1 + Math.random();
  var angle = Math.random() * Math.PI * 2;
  var lifetime = Math.random()* total | 0;
  particle.reset(center.x, center.y, radius, angle, lifetime);
}
function tick() {
  var count = particles.length;
  for(var i = 0; i < count; i++) {
    var particle = particles[i];
    moveParticle(particle);
  }
  stage.update();
}
function moveParticle(particle) {
  if (particle.lifetime > 0) {
    particle.move(Math.PI / 90);
  } else {
    resetParticle(particle);
  }
}
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;
}

jsdo.itにもサンプルとしてコードを掲載した。アニメーションの見た目は、ほぼでき上がったといってよい。だが、さらにパーティクルを増やすと、込み入った感じになる。もう少し、パーティクルの動きにばらつきを加えたい。それは次回としよう。また、楕円運動の速度を用いた方程式についても、少し数学的な説明を補うつもりだ。

おすすめ記事

記事・ニュース一覧