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

第34回パーティクルの弾けるような動きをつくる

この連載を始めてから、はや1年半が過ぎた。実は、今回のお題が連載の締めとなる。そこで最近の解説のおさらいも兼ねて、つぎのjsdo.itのようなパーティクルを選んだ。先頃大ヒットしたディズニーアニメの雪(サウンドあり)を想い起こさせる表現だ。池田泰延(clockmaker)氏によるParticle 3000を下敷きにした。このコードがCanvas APIを使っているのに対し、本解説ではもちろんCreateJSを用いる。今回を含めた都合3回で仕上げるつもりだ。

クラスでつくったパーティクルをステージに置く

まず、アニメーションは後に回して、パーティクルをひとつステージの真ん中に置こう図1⁠。CreateJSのライブラリは、EaselJSを使う。そして、body要素からonload属性で、script要素に定めた初期設定の関数(initialize())を呼び出すのは毎度おなじみのとおりだ。なお、パーティクルを白くするので、Canvasの背景色は黒に定めておく。

<script src="http://code.createjs.com/easeljs-0.7.1.min.js"></script>
<script>
var stage;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  // ...[初期設定]...
}
</script>
<body onLoad="initialize()">
  <canvas id="myCanvas" width="400" height="300"></canvas>
</body>
図1 ステージの真ん中にパーティクルをひとつ置く
図1 ステージの真ん中にパーティクルをひとつ置く

パーティクルはクラス(Particle)で定める。コンストラクタには、つぎのように位置のxy座標を渡すものとする。先に、このクラスからつくったパーティクル(Particle)のオブジェクトを、ステージに置くJavaScriptコードから書いてしまおうコード1⁠。パーティクルをつくる関数(createParticle())が、オブジェクト(particle)をつくってステージ(stage)に加えている。

new Particle(x座標, y座標)
コード1 クラスからつくったパーティクルをひとつステージに置く
var stage;
var stageWidth;
var stageHeight;
var particle;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stageWidth = canvasElement.width;
  stageHeight = canvasElement.height;
  stage = new createjs.Stage(canvasElement);
  createParticle();
  stage.update();
}
function createParticle() {
  particle = new Particle(stageWidth / 2, stageHeight / 2);
  stage.addChild(particle);
}

パーティクルのクラス(Particle)は、Shapeクラスをつぎのように継承する。これは、第18回「クラスの継承と透視投影」Shapeクラスを継承する3次元座標のクラス定義のおさらいだ。CreateJSのクラスを継承するときは、コンストラクタからinitialize()メソッドを忘れずに呼び出そう。

function Particle(x, y) {
  this.initialize();
  // ...[初期設定]...
}
Particle.prototype = new createjs.Shape();

まだアニメーションはさせないので、パーティクルのクラス(Particle)のコンストラクタはプロパティを定めて、自身のGraphicsオブジェクトに描画するメソッド(drawParticle())を呼び出すだけだ。これらコード1コード2で、ステージの真ん中に矩形のパーティクルがひとつ描かれる(前掲図1⁠。

コード2 パーティクルを定めるクラス
function Particle(x, y) {
  this.initialize();
  this.x = x;
  this.y = y;
  this.radius = 4;
  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)にアニメーションさせるためのメソッド(accelerateTo())を定めよう。つぎのように、マウスポインタのxy座標を引数に与えて、弾みのついた動きにする。

Particleオブジェクト.accelerateTo(x座標, y座標)

といって、いきなりお題の動きを式に書いてもわかりづらいだろう。第24回マウスポインタの動きに弾みがついた曲線を滑らかに描くで似たようなアニメーションをつくった。このときと同じ、バネのような弾む動きから始めてみる。

とりあえず、クラス(Particle)のアニメーションのメソッド(accelerateTo())はできてしまったことにして、オブジェクトをアニメーションさせるJavaScriptコードから書き加える。Ticker.tickイベントのリスナー関数(updateAnimation())で、マウスポインタの座標に向けて、アニメーションのメソッドが呼出される。そして、マウスポインタの座標は、Stage.stagemousemoveイベントのリスナー関数(recordMousePoint())で変数(mousePoint)にPointオブジェクトとして与えることとした。なお、Pointオブジェクトの初期xy座標値は、ステージ中央だ。

var mousePoint = new createjs.Point();

function initialize() {

  mousePoint.x = stageWidth / 2;
  mousePoint.y = stageHeight / 2;

  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 mouseX = mousePoint.x;
  var mouseY = mousePoint.y;
  particle.accelerateTo(mouseX, mouseY);
  stage.update();
}

クラス(Particle)のメソッド(accelerateTo())は、つぎのように定めた。移動先のxy座標は、第24回「マウスポインタの動きに弾みがついた曲線を滑らかに描く」弾みのついた軌跡を描くと基本的に同じ式で求めている。マウスポインタとパーティクルのxy座標のそれぞれの差(differenceXとdifferenceY)に減速率(0.1)を乗じて、xyの加速度(accelerationXとaccelerationY)とする。それらをxyの速度(_velocityXと_velocityY)に足し込んで減衰率(friction)で割り引く。その速度を、それぞれxy座標(_xと_y)に加えればよい。

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 accelerationX = differenceX * 0.1;
  var accelerationY = differenceY * 0.1;
  _velocityX += accelerationX;
  _velocityY += accelerationY;
  _velocityX *= this.friction;
  _velocityY *= this.friction;
  _x += _velocityX;
  _y += _velocityY;
  this.x = _x;
  this.y = _y;
  this.velocityX = _velocityX;
  this.velocityY = _velocityY;
};

前掲コード1コード2にこれらの手直しを加えたのが、以下のコード3およびコード4だ。マウスポインタの座標をパーティクルが、弾みのついたバネのような動きで追いかける。jsdo.itのコードも併せて掲げたので、アニメーションを確かめてほしい。例によって、クラス(Particle)は[HTML]の欄に定めている。

コード3 ステージのパーティクルにバネのようなアニメーションを与える
var stage;
var stageWidth;
var stageHeight;
var mousePoint = new createjs.Point();
var particle;
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;
  createParticle();
  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 mouseX = mousePoint.x;
  var mouseY = mousePoint.y;
  particle.accelerateTo(mouseX, mouseY);
  stage.update();
}
function createParticle() {
  particle = new Particle(stageWidth / 2, stageHeight / 2);
  stage.addChild(particle);
}
コード4 バネのように弾みをつけて動くメソッドが備わったパーティクルのクラス
function Particle(x, y) {
  this.initialize();
  this.x = x;
  this.y = y;
  this.velocityX = 0;
  this.velocityY = 0;
  this.friction = 0.95;
  this.radius = 4;
  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 accelerationX = differenceX * 0.1;
  var accelerationY = differenceY * 0.1;
  _velocityX += accelerationX;
  _velocityY += accelerationY;
  _velocityX *= this.friction;
  _velocityY *= this.friction;
  _x += _velocityX;
  _y += _velocityY;
  this.x = _x;
  this.y = _y;
  this.velocityX = _velocityX;
  this.velocityY = _velocityY;
};

吸込まれて弾けるパーティクルのアニメーション

前掲jsdo.itのアニメーションは、マウスポインタの座標に向かって弾むように動く。これに手を加えて、お題のマウスポインタの座標に吸い込まれるような動きにしたい。さらにいうと、お題のパーティクルは、マウスポインタに追いつくと、弾けるように反発している。どのような式にすれば、この動きになるのか。

前掲コード4のクラス(Particle)では、アニメーションのメソッド(accelerateTo())で用いた加速度の係数が固定(0.1)だった。この係数(ratio)をつぎのように距離の2乗(square)に反比例させる。つまり、マウスポインタに近づくほど、加速度の係数が高まり、ブラックホールに吸込まれるような動きになるのだ。


Particle.prototype.accelerateTo = function (targetX, targetY) {

  var square = differenceX * differenceX + differenceY * differenceY;
  var ratio;
  if (square > 0) {
    ratio = 50 / square;
  } else {
    ratio = 0;
  }
  /*
  var accelerationX = differenceX * 0.1;
  var accelerationY = differenceY * 0.1;
  */
  var accelerationX = differenceX * ratio;
  var accelerationY = differenceY * ratio;
  _velocityX += accelerationX;
  _velocityY += accelerationY;
  _velocityX *= this.friction;
  _velocityY *= this.friction;
  _x += _velocityX;
  _y += _velocityY;

};

もちろん、距離の2乗(square)が0になってしまうと、正しい係数値が求まらない。その場合は、条件判定により係数(ratio)を0と定めた。しかしこれだけでは、0に近い小さな値のとき、加速度が大きくなりすぎて、パーティクルがとんでもない遠くに飛んでしまう。

そこで、ステージの外に飛び去ったパーティクルは、反対の橋にスクロールしてステージ内に戻すことにした。そのためには、ステージの幅と高さをクラス(Particle)のオブジェクトが知っていなければならない。これらの値は、つぎのようにコンストラクタの引数(rightとbottom)に加えた。したがって、パーティクルをつくる関数(createParticle())にも、以下のようにコンストラクタの呼出しにこれらの引数を与える。

// function Particle(x, y) {
function Particle(x, y, right, bottom) {

  this.right = right;
  this.bottom = bottom;

}

Particle.prototype.accelerateTo = function (targetX, targetY) {

  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;
  }

};
function createParticle() {
  // particle = new Particle(stageWidth / 2, stageHeight / 2);
  particle = new Particle(stageWidth / 2, stageHeight / 2, stageWidth, stageHeight);

}

前掲コード3コード4にこれらの手を加えると、パーティクルはマウスポインタの後を吸込まれるように追いかけ、ポインタに追いつくと弾けるように反発する。パーティクルがまだひとつとはいえ、動きは一応でき上がりだ。以下にコード5およびコード6としてまとめ、jsdo.itにサンプルも掲げた。今回はここまでとする。残すところあと2回の本連載、最後までおつき合いいただきたい。

コード5 吸込まれるように動いて弾けるメソッドに書替えたパーティクルのクラス
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 = 4;
  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;
};
コード6 パーティクルがマウスポインタの後を吸込まれるように追いかけて弾けるアニメーション
var stage;
var stageWidth;
var stageHeight;
var mousePoint = new createjs.Point();
var particle;
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;
  createParticle();
  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 mouseX = mousePoint.x;
  var mouseY = mousePoint.y;
  particle.accelerateTo(mouseX, mouseY);
  stage.update();
}
function createParticle() {
  particle = new Particle(stageWidth / 2, stageHeight / 2, stageWidth, stageHeight);
  stage.addChild(particle);
}

おすすめ記事

記事・ニュース一覧