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

第19回 3次元空間で弾むオブジェクトとz座標による重ね順の並べ替え

この記事を読むのに必要な時間:およそ 7 分

オブジェクトの重なりをz座標値の順に並べ替える

CreateJSでは,インスタンスは親オブジェクトの表示リストに加えられた順に手前に重なる。重ね順はあえて動かさないかぎり,ずっとそのままだ。重なりのインデックスは親インスタンス(今回はStageオブジェクト)Container.setChildIndex()メソッドで変えられる。

親オブジェクト.setChildIndex(子オブジェクト, インデックス)

すると,各ボールのz座標値の順序をどうやって調べるかだ。実は,そのためにオブジェクトを配列(balls)に納めておいた。配列にはArray.sort()という並べ替えのメソッドが備わっている。もちろん,私たちが定義したクラス(Ball3D)のプロパティ(realZ)で,何もいわずに都合よく並べ替えてはくれはしない。どう並べ替えるのかは比較関数というかたちで決めて,Array.sort()メソッドに渡す。

配列.sort(比較関数)

Array.sort()メソッドは比較関数を渡されると,並べ替えるエレメントをふたつ取り出しては関数にその順序を尋ねる※1)。比較関数はそのふたつのエレメントを引数に受け取って,どちらが先になるのかを数値で返す。戻り値は,順序を入替えるなら正数,そのままでよければ負数,等しい(つまりそのままの)ときは0とする。

function 比較関数(a, b) {
  if (aがbより大きい) {
    return 1;
  } else if (aがbより小さい) {
    return -1;
  } else {   // aとbは等しい
    return 0;
  }
}

オブジェクトの重なりをz座標値(realZ)の順に整える関数(sortZ())は,配列(balls)を参照してArray.sort()メソッドでつぎのような比較関数(compare())により並べ替えればよい。配列エレメントは,z座標値の大きい(奥からの)順に揃う。

function sortZ() {

  balls.sort(compare);

}
function compare(a, b) {
  if (a.realZ > b.realZ) {
    return 1;
  } else if (a.realZ < b.realZ) {
    return -1;
  } else {
    return 0;
  }
}

そのうえで,オブジェクトの重なりを整える関数(sortZ())は,forループで配列(balls)のエレメントを順にすべて取り出して表示リストの中のインデックスをContainer.setChildIndex()メソッドで並べ替える。配列エレメントはz座標値が大きい順なので,表示リストのインデックスはいずれも0に定めれば,後から加えたオブジェクトほど奥に置かれていく。

function animate(eventObject) {

  sortZ();
  stage.update();
}

function sortZ() {
  var count = balls.length;
  balls.sort(compare);
  for (var i = 0; i < count; i++) {
    var ball = balls[i];
    stage.setChildIndex(ball, 0);
  }
}

これで,ボールのオブジェクトの重なりはz座標値の順に整えられる。手を加えたscript要素のJavaScript全体は,つぎのコード2のとおりだ。アニメーションが目で確かめづらいときは,ボールのインスタンスにアウトラインを描き,焦点距離は短くして遠近感を強め,フレームレートも落としてみるといいだろう図2)。

図1 ボールにアウトラインを加えて遠近感も強めたアニメーション

図1 ボールにアウトラインを加えて遠近感も強めたアニメーション

コード2 3次元座標空間に100個つくってランダムに落としたオブジェクトをz座標値の順に並べ替えたアニメーション

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;
var floor = 50;
var bounce = -0.6;
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);
  }
  sortZ();
  stage.update();
}
function move(ball) {
  ball.move(gravity);
  var x = ball.x;
  var realY = ball.realY;
  if (realY > floor) {
    ball.velocityY *= bounce;
    ball.realY = floor - realY % floor;
  }
  if (ball.realZ < -focalLength || x < 0 || stageWidth < x) {
    var index = createjs.indexOf(balls, ball);
    stage.removeChild(ball);
    balls.splice(index, 1);
console.log([stage.getNumChildren(), balls.length]);
  } else {
    var data = ball.getProjectedData(focalLength);
    ball.scaleX = ball.scaleY = data.scale;
    ball.x = centerX + data.x;
    ball.y = centerY + data.y;
  }
}
function sortZ() {
  var count = balls.length;
  balls.sort(compare);
  for (var i = 0; i < count; i++) {
    var ball = balls[i];
    stage.setChildIndex(ball, 0);
  }
}
function compare(a, b) {
  if (a.realZ > b.realZ) {
    return 1;
  } else if (a.realZ < b.realZ) {
    return -1;
  } else {
    return 0;
  }
}
function getRandom(min, max) {
  var randomNumber = Math.random() * (max - min) + min;
  return randomNumber;
}

jsdo.itにもコード2のサンプルを掲げた。ただし,背景色は黒に定めてある。「Fireworks」(花火)というタイトルにイメージが近づいたのではないか。

※1
並べ替えの仕方には,さまざまなロジックがある。いずれにしても,順に取出すふたつの値から並び方を決める。クイックソートを表すつぎの映像からイメージがつかめるかもしれない。

著者プロフィール

野中文雄(のなかふみお)

ソフトウェアトレーナー,テクニカルライター,オーサリングエンジニア。上智大学法学部卒,慶応義塾大学大学院経営管理研究科修士課程修了(MBA)。独立系パソコン販売会社で,総務・人事,企画,外資系企業担当営業などに携わる。その後,マルチメディアコンテンツ制作会社に転職。ソフトウェアトレーニング,コンテンツ制作などの業務を担当する。2001年11月に独立。Web制作者に向けた情報発信プロジェクトF-siteにも参加する。株式会社ロクナナ取締役(非常勤)。

URLhttp://www.FumioNonaka.com/

著書

コメント

コメントの記入