JavaScriptとcanvasで3Dなポリゴンを回転させる。

Safari


四角くてシフォン色のポリゴンを回転させるデモです。(お腹ぺこぺこなんで、食べられそうな色にしました)
Firefox, Safari, IE用です。とあるブラウザだと謎のモノリスがぐりぐり回転します。
なぜかOperaではうまく動作させることができませんでした。canvasの指定方法に問題があるのかもしれません。
IEで動作させるためには、excanvas.js が必要になります。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>polygon test</title>
<!--[if IE]><script type="text/javascript" src="excanvas.js"></script><![endif]-->
</head>
<body>

<canvas id="canvas0" width="640" height="480"></canvas>

<script type="text/javascript">
window.onload = function() {
  var polySize = { x: 200, y: 200 };
  var poly = new polygon();
  poly.initialize(document.getElementById("canvas0"), 50, polySize, 800);
  poly.draw();
}
</script>
<script type="text/javascript">
function polygon() {
}
polygon.prototype = {
  /** 初期化
   *
   * @param element   elm   <canvas>要素を指定します。
   * @param number    fps   1秒間に表示するフレーム数の指定です。
   *                        フレーム数を多くすると滑らかになりますが負荷も増えます。
   *                        25を指定すると約40msで1回描画します。
   * @param pos       pos   ポリゴンの座標を { x, y } で指定します。
   * @param number    zoom  ポリゴンの拡大倍率を数値で指定します。
   */
  initialize: function(elm, fps, pos, zoom) {
    this.ctx = elm.getContext("2d");
    this.fps = fps;
    this.pos = pos;
    this.zoom = zoom;
    this.poly     = [[], [], [], [], [], []]; // polygon data
    this.theta    = 0.5; // vertical
    this.phi      = 0.5; // horizontal
    // create polygon data
    var i = 0, v1, v2;
    for (; i < 5; ++i) {
      v1 = (!i) ? 0 : Math.SQRT2 * Math.cos((0.5 * i - 0.25) * Math.PI);
      v2 = (!i) ? 0 : Math.SQRT2 * Math.sin((0.5 * i - 0.25) * Math.PI);
      this.poly[0].push([ v1,  v2,   1]);
      this.poly[1].push([  1,  v1,  v2]);
      this.poly[2].push([ v2,   1,  v1]);
      this.poly[3].push([-v1, -v2,  -1]);
      this.poly[4].push([ -1, -v1, -v2]);
      this.poly[5].push([-v2,  -1, -v1]);
    }
  },
  draw: function() {
    var me = this;
    var phi   = Math.PI / 100; // 0.03141592653589793
    var theta = Math.PI / 80;
    window.setInterval(
      function() {
        me.theta += theta;
        me.phi += phi;
        me.__drawPolygon(me.theta, me.phi);
      }, 1000 / this.fps
    );
  },
  __drawPolygon: function(theta, phi) {
    var sinP = Math.sin(phi), cosP = Math.cos(phi);
    var sinT = Math.sin(theta), cosT = Math.cos(theta);

    // vector data
    var vX = [-sinP,         cosP,            0];
    var vY = [-cosT * cosP, -cosT * sinP,  sinT];
    var vZ = [-sinT * cosP, -sinT * sinP, -cosT];

    var info = [];
    var x, y, z;
    var i, j;
    var surface; // 2D bitmap surface

    for (i = 0; i < this.poly.length; ++i) {
      surface = [0, -(vZ[0] * this.poly[i][0][0] +
                      vZ[1] * this.poly[i][0][1] +
                      vZ[2] * this.poly[i][0][2])];
      for (j = 1; j < this.poly[i].length; ++j) {
        z = vZ[0] * this.poly[i][j][0] +
            vZ[1] * this.poly[i][j][1] +
            vZ[2] * this.poly[i][j][2];
        surface.push([vX[0] * this.poly[i][j][0] +
                      vX[1] * this.poly[i][j][1] +
                      vX[2] * this.poly[i][j][2],
                      vY[0] * this.poly[i][j][0] +
                      vY[1] * this.poly[i][j][1] +
                      vY[2] * this.poly[i][j][2], z]);
        surface[0] += z;
      }
      info.push(surface);
    }
    info.sort(function(a, b) {
      if (a[0] === b[0]) { return 0; }
      return a[0] < b[0] ? 1: -1;
    });

    // canvas clear
    this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);

    var light;
    for (i = 0; i < info.length; ++i) {
      info[i].shift();
      light = info[i].shift();
      if (light >= 0) {
        for (j = 0; j < info[i].length; ++j) {
          x = this.zoom * info[i][j][0] / (10 + info[i][j][2]);
          y = this.zoom * info[i][j][1] / (10 + info[i][j][2]);
          if (!j) {
            this.ctx.beginPath();
            this.ctx.moveTo(this.pos.x + x, this.pos.y + -y);
            this.ctx.fillStyle = this.__rgba(parseInt(0x3f * light + 360) * 0x0101, 1.0);
          } else {
            this.ctx.lineTo(this.pos.x + x, this.pos.y + -y);
          }
        }
        this.ctx.closePath();
        this.ctx.fill();
      }
    }
  },
  __rgba: function(color, alpha) {
    var rv = [color & 0xff0000, color & 0xff00, color & 0xff, alpha];
    return "rgba(" + rv.join(",") + ")";
  }
};
</script>

</body>
</html>

独自のメソッドの追加独自コンテキストの追加も試みたんですが、以下のようにCanvasRenderingContext2Dを拡張したところ、Firefox以外で動かなくなっちゃったので無かったことになりました。

CanvasRenderingContext2D.prototype.rgba = function(color, alpha) {
  var rv = [color & 0xff0000, color & 0xff00, color & 0xff, alpha];
  return "rgba(" + rv.join(",") + ")";
}