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

第35回たくさんのパーティクルに弾けるようなアニメーションをさせる

前回の第34回パーティクルの弾けるような動きをつくるは、ステージにパーティクルをひとつ置き、マウスポインタの座標に向けて弾けるように動かした。今回は、この数を増やして、パーティクルらしいアニメーションにしたい。パーティクルのような多くのオブジェクトを、配列でどう扱うかにも触れたい。

たくさんのパーティクルをステージのランダムな位置に置く

前回の第34回コード6に手を加えよう。パーティクルのクラス(Particle)を定めた第34回コード5ほぼそのまま用いる。まず、パーティクルをひとつだけ置いた関数(createParticle())は、引数の数だけステージのランダムな位置に描く関数(createParticles())に書き替える。初期化の関数(initialize())から、以下のように新たな関数にパーティクル数(numParticles)を渡して呼び出す。

// var particle;
var particles = new Array();
var numParticles = 3000;
function initialize() {

  // createParticle();
  createParticles(numParticles);

}

// function createParticle() {
function createParticles(amount) {
  for (var i = 0; i < amount; i++) {
    var _x = Math.random() * stageWidth;
    var _y = Math.random() * stageHeight;
    // particle = new Particle(stageWidth / 2, stageHeight / 2, stageWidth, stageHeight);
    var particle = new Particle(_x, _y, stageWidth, stageHeight);
    particles.push(particle);
    stage.addChild(particle);
  }
}

呼び出された関数(createParticles())は、上記のようにforループで引数(amount)の数だけパーティクルのインスタンスをつくって、ステージのランダムな位置に置く。そして、インスタンスは変数(particles)に定める配列に納めた。これで、ステージのランダムな位置にたくさんのパーティクルが描かれる図1⁠。ただし、まだアニメーションの処理を書き替えていないので、このまま試したのではエラーになる。確かめたいときは、Ticker.tickイベントのリスナーは一時的に外しておかなければならない。

// createjs.Ticker.addEventListener("tick", updateAnimation);
図1 ステージのランダムな位置にたくさんのパーティクルが描かれる
図1 ステージのランダムな位置にたくさんのパーティクルが描かれる

すべてのパーティクルを弾けるように動かす

つぎは、いよいよすべてのパーティクルに弾ける動きを与える。前項でTicker.tickイベントのリスナーを外して試した人は、必ず戻そう。こういうつまらない誤りにかぎってハマりやすいものだ。スクリプトはとくに難しいことはない。つぎのように、アニメーションの関数(updateAnimation())に手を加える。パーティクルが納められた配列(particles)からforループで取り出したすべてのオブジェクト(particle)に対して、引数のマウス座標(mouseXとmouseY)をアニメーションのためのメソッド(accelerateTo())に渡せばよい。

function updateAnimation(eventObject) {
  var count = particles.length;

  // particle.accelerateTo(mouseX, mouseY);
  for (var i = 0; i < count; i++) {
    var particle = particles[i];
    particle.accelerateTo(mouseX, mouseY);
  }

}

クラス(Particle)からたくさんのパーティクルをつくって、弾けるようにアニメーションさせるスクリプトは、まとめると以下のコード1のとおりだ。また、粉雪が舞い散るような表現にするため、クラス(Particle)のコンストラクタで定めるパーティクルの大きさ(radius)を縮めて、1辺1ピクセル(= 0.5×2)にした図2⁠。他は、前回と変わっていない。それも、コード2として併せて掲げた。また、jsdo.itのサンプルも添えよう。

図2 たくさんのパーティクルがマウスポインタを追って粉雪のように舞い散る
図2 たくさんのパーティクルがマウスポインタを追って粉雪のように舞い散る
コード1 クラスからつくったたくさんのパーティクルを弾けるようにアニメーションさせる
var stage;
var stageWidth;
var stageHeight;
var mousePoint = new createjs.Point();
var particles = [];
var numParticles = 3000;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stageWidth = canvasElement.width;
  stageHeight = canvasElement.height;
  stage = new createjs.Stage(canvasElement);
  mousePoint.x = stageWidth / 2;
  mousePoint.y = stageHeight / 2;
  createParticles(numParticles);
  stage.update();
  stage.addEventListener("stagemousemove", recordMousePoint);
  createjs.Ticker.timingMode = createjs.Ticker.RAF;
  createjs.Ticker.addEventListener("tick", updateAnimation);
}
function recordMousePoint(eventObject) {
  mousePoint.x = eventObject.stageX;
  mousePoint.y = eventObject.stageY;
}
function updateAnimation(eventObject) {
  var count = particles.length;
  var mouseX = mousePoint.x;
  var mouseY = mousePoint.y;
  for (var i = 0; i < count; i++) {
    var particle = particles[i];
    particle.accelerateTo(mouseX, mouseY);
  }
  stage.update();
}
function createParticles(amount) {
  for (var i = 0; i < amount; i++) {
    var _x = Math.random() * stageWidth;
    var _y = Math.random() * stageHeight;
    var particle = new Particle(_x, _y, stageWidth, stageHeight);
    particles[i] = particle;
    stage.addChild(particle);
  }
}
コード2 マウスポインタの後を追って弾けるように動くパーティクルのクラス
function Particle(x, y, right, bottom) {
  this.initialize();
  this.x = x;
  this.y = y;
  this.right = right;
  this.bottom = bottom;
  this.velocityX = 0;
  this.velocityY = 0;
  this.friction = 0.95;
  this.radius = 0.5;
  this.drawParticle();
}
Particle.prototype = new createjs.Shape();
Particle.prototype.drawParticle = function () {
  var size = this.radius * 2;
  this.graphics.beginFill("white")
  .drawRect(-this.radius, -this.radius, size, size);
};
Particle.prototype.accelerateTo = function (targetX, targetY) {
  var _x = this.x;
  var _y = this.y;
  var _velocityX = this.velocityX;
  var _velocityY = this.velocityY;
  var differenceX = targetX - _x;
  var differenceY = targetY - _y;
  var square = differenceX * differenceX + differenceY * differenceY;
  var ratio;
  if (square > 0) {
    ratio = 50 / square;
  } else {
    ratio = 0;
  }
  var accelerationX = differenceX * ratio;
  var accelerationY = differenceY * ratio;
  _velocityX += accelerationX;
  _velocityY += accelerationY;
  _velocityX *= this.friction;
  _velocityY *= this.friction;
  _x += _velocityX;
  _y += _velocityY;
  if (_x < 0) {
    _x += this.right;
  } else if (_x > this.right) {
    _x -= this.right;
  }
  if (_y < 0) {
    _y += this.bottom;
  } else if (_y > this.bottom) {
    _y -= this.bottom;
  }
  this.x = _x;
  this.y = _y;
  this.velocityX = _velocityX;
  this.velocityY = _velocityY;
};

配列の扱いを最適化する

前掲コード1は、前述「たくさんのパーティクルをステージのランダムな位置に置く」の項で抜書きして解説したJavaScriptコードと実は2か所だけ微妙に違っている。別に間違い探しではなく、どちらでも正しく動く。コード1が異なるのはつぎのふたつのステートメントだ。

// var particles = new Array();
var particles = [];

function createParticles(amount) {
  for (var i = 0; i < amount; i++) {

    var particle = new Particle(_x, _y, stageWidth, stageHeight);
    // particles.push(particle);
    particles[i] = particle;

  }
}

第1に、空の配列インスタンスをつくるとき、Array()コンストラクタでなく[]リテラルとして書いてもよい。第2に、配列エレメントを加えるには、Array.push()メソッドの替わりに、やはり[]アクセスが使える。コード1は、どちらも[]演算子を用いた。ちなみに、[]「ブラケット」bracketと呼ばれる。たまに、⁠ブランケット」blanketと間違える人もいるが、それはピーナッツのライナス(Linus)が抱えているあれだ。

配列の扱いは、どちらもブラケットを使った方がお得だ。なぜなら、処理が速くなる。なるほど、ブラケットを使うと配列の読み書きが速いのか、と覚えるのは少しばかり早とちりだろう。ブラケットのアクセスが速い訳では決してない。配列をつくるときと、エレメントを加える場合について、それぞれ順に解説しよう。

まず、Array()コンストラクタでインスタンスをつくるとき、渡す引数はつぎの表1のようにエレメントと長さのふたつの意味があり得る。引数が複数なら、つくられた配列インスタンスのエレメントとして納められる。しかし、正の整数ひとつを引数に渡したときは、その長さの配列がつくられる。それ以外のデータが引数であれば、そのひとつの値はやはりエレメントになる[1]⁠。

表1 コンストラクタメソッドの引数によってつくられる配列の違い
コンストラクタ呼出し引数の意味つくられる配列
new Array()エレメント(なし)[]
new Array(3)長さ[undefined, undefined, undefined]
new Array(0, 1, 2)エレメント[0, 1, 2]
new Array("a")エレメント["a"]

このようにArray()コンストラクタは、引数の数とデータ型によって処理が変わる[2]⁠。したがって、コンストラクタは呼び出されたとき、引数の数とデータ型から、値をどう扱うのか選ばなければならない。そのタメが一瞬生じる図3⁠。ところが、[]を用いたリテラルで書けば、配列の長さは決められないので、中身はすべてエレメントだ。つまり、ブラケットなら一切の迷いがない。そのスタートの差が生じる。

図3 引数の数とデータ型から値の扱いを選ばなければならない
図3 引数の数とデータ型から値の扱いを選ばなければならない

つぎに、Array.push()メソッドは、引数のエレメントを配列の最後に加える。それはつまり、配列の最後尾を調べなければならないということだ。つい先頃も、ビッグウェーブに乗った行列が話題になった図4⁠。長い行列の最後尾を探すのはそれなりに手間だ。だが、前掲コード1のパーティクルをつくる関数(createParticles())のように、加える配列のインデックスがわかっているときは、それをブラケットで与えれば手間が省ける。

図4 配列の最後尾を探さないとけいない
図4 配列の最後尾を探さないとけいない

結局、ふたつの場合において、ブラケットを使うとひと手間省けることが速さにつながっている。ブラケットを用いたとしても、たとえば長さがわからない配列の最後にエレメントを加えようとしたら、つぎのようにArray.lengthプロパティで長さを調べるしかない。こうなると、手間はArray.push()メソッドを使うのと変わらなくなってしまう。⁠ブラケットのアクセスが速い訳では決してない」と述べたのはこういう意味だ。

配列[配列.length] = エレメント

では、ブラケットを用いたとき速さがどれだけ稼げるかというと、100万回処理を繰返して数ミリから数百ミリ秒、つまりせいぜいコンマ数秒といったところだ。しかも、オペレーティングシステムやブラウザおよびそのバージョンによってばらつきも大きい。ブラウザがJavaScriptの最適化を十分に施していれば、実質的な差が認められないこともある。

しかも、今回のコード1では、配列インスタンス(particles)をたったひとつしかつくっていない。配列にエレメントを加えるのも、パーティクルをつくる関数(createParticles())が初めに1度行うだけだ。書き替えたコンテンツの動きに違いはほとんどない。とはいえ、ちりも積もれば山となる。ブラケットを使う不利益はない。コードを書くのも簡単だし、見やすいだろう。⁠よい習慣」best practiceとしてお勧めする所以だ[3]⁠。

配列はたくさんのエレメントをまとめて、すべてに処理を加えるときよく用いられる。だがそれだけでなく、他のさまざまな用途にも幅広く対応している。つまり、汎用性が高い。けれど、あえて使わない機能を削れば、速さの稼げる余地が生まれる。次回は、エレメントすべてを取出して処理する機能に絞り込んだ「連結リスト」⁠linked list)について解説しよう。

おすすめ記事

記事・ニュース一覧