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

第18回クラスの継承と透視投影

前回の第17回簡単なクラスを定義するで予告したとおり、今回のお題は引続きクラス定義と透視投影を使ったコードだ。Keith Peters氏が著書ActionScript 3.0 アニメーションでつくられたサンプルの動きをもとに、CreateJSでスクリプティングした。ユーザーインタラクションは含まれていない。

Shapeクラスを継承する3次元座標のクラス定義

早速、3次元座標のクラスを定める。前回と違うのは、座標だけでなくボールのシェイプも、このクラスでつくってしまうことだ。EaselJSなら、Shapeクラスを使えば円形のシェイプが描ける。では、この機能を丸ごといただこう。こういうとき、プログラミング言語では、Shapeクラスを「継承」して3次元座標のクラスを定義する。

JavaScriptの継承は他の言語と少し変わっている。継承したいクラス(⁠⁠スーパークラス⁠⁠)のオブジェクトを、継承するクラス(⁠⁠サブクラス⁠⁠)のFunction.prototypeプロパティに代入してしまう。すると、スーパークラスのプロトタイプオブジェクトが、サブクラスのインスタンスから参照できる[1]⁠。そして、そのプロトタイプオブジェクトに備わるメソッドやプロパティが、インスタンスに定められたかのように扱える仕組みなのである。

// サブクラスのコンストラクタ関数の定義
function クラス名() {
  // インスタンスの初期化
}
// スーパークラスの継承
クラス名.prototype = スーパークラスのオブジェクト;

今回、3次元座標のクラス(Ball3D)は、つぎのようにFunction.prototypeプロパティにShapeオブジェクトを与えて継承した。したがって、コンストラクタが受取った引数の半径とカラーで、自らのGraphicsオブジェクトにメソッド(drawBall())で円が描ける。そして、前回のクラスと同じく、コンストラクタで3次元座標をプロパティとして定める。ただし、Shapeクラスにはすでにxyというプロパティが備わっている。そこで、xyの2次元に透視投影した座標をこのプロパティに与えることとし、3次元空間における座標のプロパティは別の名前(realX/realY/realZ)にした。

function Ball3D(radius, color) {
  this.initialize();
  this.radius = radius;
  this.color = color;
  this.realX = 0;
  this.realY = 0;
  this.realZ = 0;
  this.velocityX = 0;
  this.velocityY = 0;
  this.velocityZ = 0;
  this.drawBall(radius, color);
}
Ball3D.prototype = new createjs.Shape();
Ball3D.prototype.move = function(gravity) {
  this.realX += this.velocityX;
  this.realY += this.velocityY;
  this.velocityY += gravity;
};
Ball3D.prototype.drawBall = function(radius, color) {
  this.graphics.beginFill(color)
  .drawCircle(0, 0, radius);
};

前掲クラス(Ball3D)のインスタンスを落とすメソッド(move())は、第11回「マウスポインタの動きに合わせてインスタンスをランダムに落とす」インスタンスの動きに水平方向の初速を加えるから抜き出した、つぎのコードにならった。ただし、メソッドはクラスのインスタンス自身を落とすので、this参照が操作の対象となる。まずは、2次元平面で落としてみる。その速さはxyzのそれぞれの向きごとに、コンストラクタでプロパティ(velocityX/velocityY/velocityZ)に定めた。

なお、クラスのコンストラクタ(Ball3D())が初めに呼び出しているメソッドinitialize()は、Shapeクラスに内部的に定められている初期化のメソッドだ。インスタンスがもつGraphicsオブジェクトも、このメソッドでつくられる。

function animate(eventObject) {
  var count = stage.getNumChildren() - 1;
  for (var i = count; i > -1; i--) {

    var newY = child.y + child.velocityY;

    child.x += child.velocityX;
    child.y = newY;

    child.velocityY += 2;

  }

}

3次元空間のボールのオブジェクトを100個つくって、とりあえず2次元平面でランダムな向きに落とすスクリプトが以下のコード1だ。まず、初期化の関数(initialize())の中のforループでボールのクラスのコンストラクタ(Ball3D())を呼ぶ。ボールのオブジェクトの塗り色(color)とxy方向の速さ(velocityXとvelocityY)は、ランダムに与える。また、つくったオブジェクトは、後で扱いやすいように変数(balls)の配列に入れておく。なお、アニメーションはステージの真ん中を原点とするため、そのxy座標値を変数(centerXとcenterY)に納めた。

アニメーションは、お約束どおりTicker.tickイベントのリスナー(animate())で行う。リスナー関数は、配列(balls)からすべてのオブジェクト(ball)を順に取り出し、ボールの動きを定める関数(move())に渡す。その関数はボールのオブジェクトの3次元座標をクラス(Ball3D)のメソッド(move())で落下させ、その結果を2次元のxy座標Shape.xShape.yプロパティ)に与えている。この2次元のxy座標は、後で3次元座標から透視投影して定めるつもりだ。

コード1 Shapeのサブクラスのオブジェクトを100個つくって2次元平面でランダムな向きに落とす
function Ball3D(radius, color) {
  this.initialize();
  this.radius = radius;
  this.color = color;
  this.realX = 0;
  this.realY = 0;
  this.realZ = 0;
  this.velocityX = 0;
  this.velocityY = 0;
  this.velocityZ = 0;
  this.drawBall(radius, color);
}
Ball3D.prototype = new createjs.Shape();
Ball3D.prototype.move = function(gravity) {
  this.realX += this.velocityX;
  this.realY += this.velocityY;
  this.velocityY += gravity;
};
Ball3D.prototype.drawBall = function(radius, color) {
  this.graphics.beginFill(color)
  .drawCircle(0, 0, radius);
};
var stage;
var balls = [];
var numBalls = 100;
var stageWidth;
var centerX;
var centerY;
var gravity = 0.2;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  stageWidth = canvasElement.width;
  centerX = stageWidth / 2;
  centerY = canvasElement.height / 2;
  for (var i = 0; i < numBalls; i++) {
    var color = createjs.Graphics.getRGB(getRandom(0, 0xFFFFFF));
    var ball = new Ball3D(3, color);
    balls.push(ball);
    ball.realY = -50;
    ball.velocityX = getRandom(-3, 3);
    ball.velocityY = getRandom(-6, 0);
    stage.addChild(ball);
  }
  createjs.Ticker.addEventListener("tick", animate);
}
function animate(eventObject) {
  for (var i = balls.length - 1; i > -1; i--) {
    var ball = balls[i];
    move(ball);
  }
  stage.update();
}
function move(ball) {
  ball.move(gravity);
  ball.x = centerX + ball.realX;
  ball.y = centerY + ball.realY;
}
function getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}

これで、Shapeを継承したクラス(Ball3D)でつくられた100個のボールが、2次元平面でランダムな向きに落ちていく図1⁠。まだオブジェクトの3次元座標を透視投影しておらず、そもそもz座標値が0のまま動かないため、すべてのボールが同じ大きさのまま下に向かって散らばるだけだ。

図1 100個のボールが2次元平面でランダムな向きに落ちる
図1 100個のボールが2次元平面でランダムな向きに落ちる 図1 100個のボールが2次元平面でランダムな向きに落ちる

z軸方向の速さと透視投影のメソッドを加える

2次元平面でボールを落とせたら、つぎはz軸方向の動きを加えて、透視投影する。つまり、3次元で動くボールに、z軸の向きにも速さ(velocityZ)を加える。初期化の関数(initialize())でz軸方向の速さをランダムに定め、ボールのクラス(Ball3D)のオブジェクトを落とすメソッド(move())で、z軸の向きの動きも与える。

Ball3D.prototype.move = function(gravity) {
  this.realX += this.velocityX;
  this.realY += this.velocityY;
  this.realZ += this.velocityZ;
  this.velocityY += gravity;
};

function initialize() {

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

    var ball = new Ball3D(3, color);

    ball.velocityX = getRandom(-3, 3);
    ball.velocityY = getRandom(-6, 0);
    ball.velocityZ = getRandom(-3, 3);

  }

}

3次元で動くボールのクラス(Ball3D)には、さらに透視投影のメソッド(getProjectedData())をつぎのように定める。焦点距離(focalLength)を引数に受け取って、xy座標(xおよびy)とオブジェクトの伸縮率(scale)をObjectインスタンスのプロパティに納めて返す。

Ball3D.prototype.getProjectedData = function(focalLength) {
    var scale = focalLength / (focalLength + this.realZ);
    var x = this.realX * scale;
    var y = this.realY * scale;
    return {x:x, y:y, scale:scale};
};

参考にしたのは第16回「3次元空間で座標を回す」3次元空間座標を透視投影するから抜き出したつぎの関数だ。ただし、前掲コードは大きさをもつボールのクラス(Ball3D)のメソッド(getProjectedData())なので、透視投影の比率(焦点距離 / ⁠焦点距離 + z位置))を戻り値のオブジェクトに伸縮率のプロパティ(scale)として加えた。

function getProjetedPoint(focalLength, _point3D) {
  var point2D = new createjs.Point();
  var w = focalLength / (focalLength + _point3D.z);
  point2D.x = _point3D.x * w;
  point2D.y = _point3D.y * w;
  return point2D;
}

前掲透視投影のメソッド(getProjetedPoint())は、ボールの動きを2次元平面に定める関数(move())から呼び出す。メソッドから返されたObjectインスタンスのプロパティ(scaleおよびxとy)を、ボールの(スーパークラスShapeの)xy座標と水平・垂直伸縮率に与えて透視投影する。なお、透視投影のメソッドに渡す焦点距離は、あらかじめ変数(focalLength)に定めた。

var focalLength = 200;

function move(ball) {
  ball.move(gravity);
  var data = ball.getProjectedData(focalLength);
  ball.scaleX = ball.scaleY = data.scale;
  // ball.x = centerX + ball.realX;
  ball.x = centerX + data.x;
  // ball.y = centerY + ball.realY;
  ball.y = centerY + data.y;
}

ボール100個が落ちる動きを3次元に拡げたのがつぎのコード2だ。ボールのオブジェクトはz座標値に応じて透視投影され、遠ざかれば小さく、近づけば大きく表示される(図2⁠。もっとも、今の表現では3次元の動きだといわれなければ、ただオブジェクトが伸縮しながら落ちているだけに見えるかもしれない。

図2 100個のボールが3次元空間でランダムな向きに落ちる
図2 100個のボールが3次元空間でランダムな向きに落ちる
コード2 100個つくったオブジェクトの3次元座標をを透視投影してランダムな向きに落とす
function Ball3D(radius, color) {
  this.initialize();
  this.radius = radius;
  this.color = color;
  this.realX = 0;
  this.realY = 0;
  this.realZ = 0;
  this.velocityX = 0;
  this.velocityY = 0;
  this.velocityZ = 0;
  this.drawBall(radius, color);
}
Ball3D.prototype = new createjs.Shape();
Ball3D.prototype.move = function(gravity) {
  this.realX += this.velocityX;
  this.realY += this.velocityY;
  this.realZ += this.velocityZ;
  this.velocityY += gravity;
};
Ball3D.prototype.getProjectedData = function(focalLength) {
    var scale = focalLength / (focalLength + this.realZ);
    var x = this.realX * scale;
    var y = this.realY * scale;
    return {x:x, y:y, scale:scale};
};
Ball3D.prototype.drawBall = function(radius, color) {
  this.graphics.beginFill(color)
  .drawCircle(0, 0, radius);
};
var stage;
var balls = [];
var numBalls = 100;
var stageWidth;
var centerX;
var centerY;
var gravity = 0.2;
var focalLength = 200;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  stageWidth = canvasElement.width;
  centerX = stageWidth / 2;
  centerY = canvasElement.height / 2;
  for (var i = 0; i < numBalls; i++) {
    var color = createjs.Graphics.getRGB(getRandom(0, 0xFFFFFF));
    var ball = new Ball3D(3, color);
    balls.push(ball);
    ball.realY = -50;
    ball.velocityX = getRandom(-3, 3);
    ball.velocityY = getRandom(-6, 0);
    ball.velocityZ = getRandom(-3, 3);
    stage.addChild(ball);
  }
  createjs.Ticker.addEventListener("tick", animate);
}
function animate(eventObject) {
  for (var i = balls.length - 1; i > -1; i--) {
    var ball = balls[i];
    move(ball);
  }
  stage.update();
}
function move(ball) {
  ball.move(gravity);
  var data = ball.getProjectedData(focalLength);
  ball.scaleX = ball.scaleY = data.scale;
  ball.x = centerX + data.x;
  ball.y = centerY + data.y;
}
function getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}

できあがったコードはjsdo.itに掲げた。次回は、3つ手を加えてお題を仕上げるつもりだ。まず、ボールを床で弾ませる。つぎに、ステージから見えなくなったオブジェクトは、メモリから片づけたい。そして、3次元の表現ではもうひとつ忘れてならない処理がある。それは何か。年をまたいでしまうので、お正月の宿題としよう(つぎのjsdo.itのアニメーションをよく見ていただくとヒントがある⁠⁠。では、よいお年をお迎えいただきたい!

おすすめ記事

記事・ニュース一覧