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

第17回簡単なクラスを定義する

前回の第16回3次元空間で座標を回すは、星形の3次元座標をy軸で水平に回し、2次元平面に透視投影したうえでアニメーションにした第16回コード2星形の3次元座標に遠近法を加えてy軸で回すアニメーション⁠。この表現はもうこのまま変えない。今回は、スクリプトの書き方に磨きをかける。具体的には、簡単なクラスを定義してみよう。

透視投影のメソッドをオブジェクトに定める

オブジェクトをクラスからつくると、プロパティとして備わったデータをそのオブジェクトのメソッドで操作できる(IT用語辞典クラス参照⁠⁠。今回のお題で、3次元空間座標をObjectインスタンスでつくった。これをクラスとして定義し直し、透視投影座標はそのメソッドで求めることにする。3次元空間座標のクラス(Point3D)と透視投影のメソッド(getProjetedPoint())が定められた暁には、3次元座標のオブジェクトをつくって、2次元座標に透視投影するステートメントはつぎのようになる。

// 3次元座標のオブジェクトをつくる
var point3D = new Point3D(x座標, y座標, z座標);

// 3次元座標を2次元座標に透視投影
var point2D = point3D.getProjetedPoint(焦点距離);

もっとも、オブジェクトを参照してメソッドを呼ぶには、クラスが必ずしも定義されていなくてもよい。まず、こちらから先に進めよう。第16回コード2は、Ticker.tickイベントのリスナー関数(rotate())から、別の関数(getProjetedPoint())の引数に焦点距離(focalLength)と3次元座標のオブジェクト(point)を与えて、2次元座標に透視投影した。そのステートメントは、つぎのようにオブジェクトを参照した同じ名前のメソッド(getProjetedPoint())の呼出しに書き替える。

function rotate(eventObject) {
  var count = points.length;
  points2D.length = 0;
  matrix.identity().rotate(angle);
  for (var i = 0; i < count; i++) {
    var point = points[i];
    matrix.transformPoint(point.x, point.z, _point);
    point.x = _point.x;
    point.z = _point.y;
    // points2D[i] = getProjetedPoint(focalLength, point);
    points2D[i] = point.getProjetedPoint(focalLength);
  }
  draw(points2D);

つぎに手を加えるのは、3次元座標のオブジェクトをつくる関数(newPoint3D())だ。つくったオブジェクト(point3D)にプロパティのかたちでメソッド名(getProjetedPoint)を与え、定義した関数(getProjetedPoint())の参照を代入する。これで、このプロパティをオブジェクトのメソッドとして呼び出せる。

function newPoint3D(x, y, z) {
  var point3D = {x:x, y:y, z:z};
  point3D.getProjetedPoint = getProjetedPoint;   // 関数をメソッドとして定める
  return point3D;
}

そして、オブジェクトのメソッドに定めた関数(getProjetedPoint())をつぎのように書き直す。3次元座標のオブジェクトにメソッドとして定めたのだから、引数にオブジェクト(_point3D)は受け取らなくてよい。オブジェクトのプロパティ(getProjetedPoint)に関数が与えられ、メソッドとして呼び出すとき、その関数内ではオブジェクトをthisキーワードで参照できる。したがって、引数のオブジェクトを参照していた記述は、すべてthisに書き替える。

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

これで、透視投影の関数は3次元座標のオブジェクトにメソッド(getProjetedPoint())として備わった。書き上がったスクリプトは、コード1のとおりだ。ワイヤーフレームの星形がy軸で水平に回るアニメーションは、第16回コード2と変わらない図1⁠。マウスポインタの水平位置に応じて、星形の回る向きと速さが変わる。

図1 星形の3次元座標がy軸で水平に回って透視投影される
図1 星形の3次元座標がy軸で水平に回って透視投影される 図1 星形の3次元座標がy軸で水平に回って透視投影される
コード1 星形の3次元座標をy軸で回して透視投影するアニメーション
var stage;
var drawGraphics;
var points;
var angle = Math.PI / 36;
var matrix = new createjs.Matrix2D();
var stageCenterX;
var _point = new createjs.Point();
var points2D = [];
var focalLength = 300;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  stageCenterX = canvasElement.width / 2;
  drawGraphics = createGraphics(stageCenterX, canvasElement.height / 2);
  points = createStarPoints(5, 65, 25);
  draw(points);
  createjs.Ticker.addEventListener("tick", rotate);
  stage.addEventListener("stagemousemove", setAngle);
}
function setAngle(eventObject) {
  var mouseX = eventObject.stageX;
  angle = (mouseX - stageCenterX) * 1 / 300;
}
function rotate(eventObject) {
  var count = points.length;
  points2D.length = 0;
  matrix.identity().rotate(angle);
  for (var i = 0; i < count; i++) {
    var point = points[i];
    matrix.transformPoint(point.x, point.z, _point);
    point.x = _point.x;
    point.z = _point.y;
    points2D[i] = point.getProjetedPoint(focalLength);
  }
  draw(points2D);
}
function createGraphics(x, y) {
  var drawShape = new createjs.Shape();
  drawShape.x = x;
  drawShape.y = y;
  stage.addChild(drawShape);
  return drawShape.graphics;
}
function draw(points) {
  var count = points.length;
  var point = points[count - 1];
  drawGraphics.clear()
  .beginStroke("mediumblue")
  .setStrokeStyle(3)
  .moveTo(point.x, point.y);
  for (var i = 0; i < count; i++) {
    point = points[i];
    drawGraphics.lineTo(point.x, point.y);
  }
  stage.update();
}
function createStarPoints(numVertices, longRadius, shortRadius) {
  var starPoints = [];
  var angle = Math.PI;
  var theta = angle / numVertices;
  angle /= -2;
  for (var i = 0; i < numVertices; i++) {
    starPoints.push(newPoint3D(longRadius * Math.cos(angle), longRadius * Math.sin(angle), 0));
    angle += theta;
    starPoints.push(newPoint3D(shortRadius * Math.cos(angle), shortRadius * Math.sin(angle), 0));
    angle += theta;
  }
  return starPoints;
}
function newPoint3D(x, y, z) {
  var point3D = {x:x, y:y, z:z};
  point3D.getProjetedPoint = getProjetedPoint;
  return point3D;
}
function getProjetedPoint(focalLength) {
  var point2D = new createjs.Point();
  var w = focalLength / (focalLength + this.z);
  point2D.x = this.x * w;
  point2D.y = this.y * w;
  return point2D;
}

コンストラクタ関数を定める

つぎは、コンストラクタ関数を定義して呼び出す。それ自体はとても簡単だ。ただ関数を定めて、new演算子で呼び出しさえすればよい。

// コンストラクタ関数の定義
function クラス名() {}

// new演算子で呼出す
var 変数 = new クラス名();

コンストラクタ関数には値を返すreturnステートメントは書かない。new演算子で呼び出せば、新たにつくられたインスタンスが返る決まりだ。今回は、引数に渡したxyz座標をプロパティとして定めたい。そのときは、コンストラクタ内のthis参照にプロパティを与えればよい。コンストラクタ内でつくられるインスタンスは、thisで参照されるからだ。

すると、前掲コード1の3次元座標オブジェクトをつくって返す関数(newPoint3D())は、つぎのようにコンストラクタ関数として書き替えられる。なお、クラス名(Point3D)はそれらしく変えてみた。

// function newPoint3D(x, y, z) {
function Point3D(x, y, z) {
  /*
  var point3D = {x:x, y:y, z:z};
  point3D.getProjetedPoint = getProjetedPoint;
  return point3D;
  */
  this.x = x;
  this.y = y;
  this.z = z;
  this.getProjetedPoint = getProjetedPoint;
}

3次元座標をつくって配列に納める関数(createStarPoints())からは、前掲のコンストラクタ関数(Point3D())new演算子で呼出すように書き替える。

function createStarPoints(numVertices, longRadius, shortRadius) {
  var starPoints = [];
  var angle = Math.PI;
  var theta = angle / numVertices;
  angle /= -2;
  for (var i = 0; i < numVertices; i++) {
    // starPoints.push(newPoint3D(longRadius * Math.cos(angle), longRadius * Math.sin(angle), 0));
    starPoints.push(new Point3D(longRadius * Math.cos(angle), longRadius * Math.sin(angle), 0));
    angle += theta;
    // starPoints.push(newPoint3D(shortRadius * Math.cos(angle), shortRadius * Math.sin(angle), 0));
    starPoints.push(new Point3D(shortRadius * Math.cos(angle), shortRadius * Math.sin(angle), 0));
    angle += theta;
  }
  return starPoints;
}

これで取りあえずクラス(Point3D)は定義でき、クラスにメソッド(getProjetedPoint())も備わった。試してみれば、星形はマウスポインタの水平位置に合わせてy軸で回る。ただし、クラスへのメソッドの定め方が標準的ではない。

コンストラクタのプロトタイプオブジェクトにメソッドを定める

前項のクラスへのメソッドの定め方には、ふたつ問題がある。ひとつは、メソッドを本格的に加え始めると、コンストラクタの中がメソッド設定の羅列で見づらくなる。もうひとつは、メソッドに与えた関数が、メソッドでなく直に関数としても呼べてしまうことだ[1]⁠。関数が直に呼ばれると、その中のthis参照はグローバルオブジェクトになってしまうので正しく動かない。

そこで、クラスに備えるメソッドは、コンストラクタ関数のFunction.prototypeプロパティに定める。このプロパティはコンストラクタとして呼出された関数に備わるプロトタイプオブジェクトを参照する。コンストラクタからつくられたインスタンスを参照すると、コンストラクタのthis参照などをとおしてインスタンスに与えられたプロパティやメソッドにアクセスできる。けれど、その中に見当たらないときは、コンストラクタのプロトタイプオブジェクトを探し始め、見つかればインスタンスに定められたのと同じに扱えるのだ。

コンストラクタのFunction.prototypeプロパティにメソッドを定めるときは、名前のない関数で与える。そうすれば、直に関数を呼ばれるおそれがなくなるからだ。

// コンストラクタ関数の定義
function クラス名() {}
// メソッドの定義
クラス名.prototype.メソッド名 = function(引数) {
  // メソッドの処理
}

そこで、前項で定義したコンストラクタ(Point3D())とメソッド(getProjetedPoint())を、つぎのように書き直す。コンストラクタ関数のFunction.prototypeプロパティに定められた関数(getProjetedPoint())は、すべてのインスタンスから参照でき、自身のメソッドとして呼び出せる。

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

メソッド(getProjetedPoint())をひとつだけもつクラス(Point3D)がこれで定義できた。ここでひとつ、大切な注意がある。メソッドは名前のない関数を代入して定めている。そのため、このコンストラクタを呼び出すステートメントは、代入文の後に書かなければメソッドが備わらない。つまり、クラス定義はJavaScriptコードの冒頭に置くのがよい。そうしてまとめたスクリプト全体がつぎのコード2だ。

コード2 冒頭に3次元座標のクラスと透視投影のメソッドを定義したy軸で星形が回るアニメーション
function Point3D(x, y, z) {
  this.x = x;
  this.y = y;
  this.z = z;
}
Point3D.prototype.getProjetedPoint = function(focalLength) {
  var point2D = new createjs.Point();
  var w = focalLength / (focalLength + this.z);
  point2D.x = this.x * w;
  point2D.y = this.y * w;
  return point2D;
};
var stage;
var drawGraphics;
var points;
var angle = Math.PI / 36;
var matrix = new createjs.Matrix2D();
var stageCenterX;
var _point = new createjs.Point();
var points2D = [];
var focalLength = 300;
function initialize() {
  var canvasElement = document.getElementById("myCanvas");
  stage = new createjs.Stage(canvasElement);
  stageCenterX = canvasElement.width / 2;
  drawGraphics = createGraphics(stageCenterX, canvasElement.height / 2);
  points = createStarPoints(5, 65, 25);
  draw(points);
  createjs.Ticker.addEventListener("tick", rotate);
  stage.addEventListener("stagemousemove", setAngle);
}
function setAngle(eventObject) {
  var mouseX = eventObject.stageX;
  angle = (mouseX - stageCenterX) * 1 / 300;
}
function rotate(eventObject) {
  var count = points.length;
  points2D.length = 0;
  matrix.identity().rotate(angle);
  for (var i = 0; i < count; i++) {
    var point = points[i];
    matrix.transformPoint(point.x, point.z, _point);
    point.x = _point.x;
    point.z = _point.y;
    points2D[i] = point.getProjetedPoint(focalLength);
  }
  draw(points2D);
}
function createGraphics(x, y) {
  var drawShape = new createjs.Shape();
  drawShape.x = x;
  drawShape.y = y;
  stage.addChild(drawShape);
  return drawShape.graphics;
}
function draw(points) {
  var count = points.length;
  var point = points[count - 1];
  drawGraphics.clear()
  .beginStroke("mediumblue")
  .setStrokeStyle(3)
  .moveTo(point.x, point.y);
  for (var i = 0; i < count; i++) {
    point = points[i];
    drawGraphics.lineTo(point.x, point.y);
  }
  stage.update();
}
function createStarPoints(numVertices, longRadius, shortRadius) {
  var starPoints = [];
  var angle = Math.PI;
  var theta = angle / numVertices;
  angle /= -2;
  for (var i = 0; i < numVertices; i++) {
    starPoints.push(new Point3D(longRadius * Math.cos(angle), longRadius * Math.sin(angle), 0));
    angle += theta;
    starPoints.push(new Point3D(shortRadius * Math.cos(angle), shortRadius * Math.sin(angle), 0));
    angle += theta;
  }
  return starPoints;
}

このお題は、これででき上がりとする。jsdo.itにコードを掲げた。次回はお題を変えて、クラス定義と透視投影にもう少し取り組んでみたい。このようなサンプルを予定している(なお、このサンプルにはインタラクションはない⁠⁠。

おすすめ記事

記事・ニュース一覧