マウスで3Dモデルを回転する(クオータニオン)

http://jsdo.it/babu_baboo/EZt9
いつものようにライブラリなんか利用しないで書いた。
凄く勉強になるのだが、すぐに忘れてしまいそうだ。

<!DOCTYPE html>
<meta charset="UTF-8">
<title>N個の点を持つ球体を描画する</title>
<style>
body {
  color: #ccc; background: black;
}
canvas { border: 1px #666 solid; }
#CG ul, #CG li { margin: 0; padding: 0;}
#CG li { list-style: none; }
h4 { font-size: normal; margin: .5ex;font-weight: normal;}
#c1 {
  background: black;
  border: 1px #666 solid;
}
#CG {
  vertical-align: top;
  border: 1px #666 solid;
  width: 120px; height: 696px;
  overflow: auto;
  padding: 2px;
  display: inline-block;
}
#ctrl p {
  margin: 0; font-size: small;
  border-bottom: 1px #666 solid;
}
#CG input {
  background: transparent;
  color: #ff0;
  radius: 4px;
}
#CG input[type="button"] {
    width: 100%;
}
#CG input[type="number"] {
  width: 4em;
}
#c2 {
  width: 100%;
}
</style>

<body>
<div id="CG">
  <h4>光源</h4>
  <canvas width="100" height="100" id="c2"></canvas>

  <ul>
   <li>
    <label>半径</label><br>
    A:<input type="number" id="radius" value="100" size="5" min="30" step="30" max="210"><br>
    B:<input type="number" id="radius2" value="30" size="5" min="30" step="30" max="90">
   <li>
    <label>分割</label><br>
    A:<input type="number" id="N" value="10" min="3" max="100"><br>
    B:<input type="number" id="N2" value="10" min="3" max="100">
   <li>
    <label>面分割</label><br>
    A:<input type="number" id="split" value="0" size="5" min="0" max="5"><br>
  </ul>
  <h4>物体</h4>
  <ul>
   <li><input type="button" value="正二十面体" id="regularIcosahedron">
   <li><input type="button" value="正四面体" id="regularTetrahedron">
   <li><input type="button" value="多角錐" id="pyramid">
   <li><input type="button" value="ドーナツ" id="doughnut">

  </ul>
  <hr>

  <h4>Render</h4>
  <ul>
   <li><label><input type="checkbox" value="vertex" id="vertex">頂点</label>
   <li><label><input type="checkbox" value="wireFrame" id="wireFrame">線</label>
   <li><label><input type="checkbox" value="surface" id="surface" checked>面</label>
   <li><label><input type="checkbox" value="hiddenSurface" id="hiddenSurface" checked>隠面処理</label>
  </ul>

  <hr>
  <h4>Color</h4>
  <ul>
   <li><label><input type="checkbox" value="red" id="red" checked>赤</label>
   <li><label><input type="checkbox" value="green" id="green" checked>緑</label>
   <li><label><input type="checkbox" value="blue" id="blue" checked>青</label>
   <li><label><input type="checkbox" value="random" id="random" checked>ランダム</label>
  </ul>

</div>
<canvas width="800" height="700" id="c1"></canvas>

<script>

//___________________________________

{ //アニメーションの環境か?

  const
    win = this;

  const
    substitution =
      function (callBackFunc, that) {
        let tmpFunc = function () {
          let timestamp = +(new Date);
          callBackFunc (timestamp);
        };
        win.setTimeout (tmpFunc, Math.floor (1000/60));
      };

  if ('undefined' === typeof win.requestAnimationFrame)
    win.requestAnimationFrame =
      win.requestAnimationFrame ||
      win.webkitRequestAnimationFrame ||
      win.mozRequestAnimationFrame ||
      win.oRequestAnimationFrame ||
      win.msRequestAnimationFrame ||
      substitution;

  if ('undefined' === typeof win.cancelAnimationFrame)
    win.cancelAnimationFrame =
      win.cancelAnimationFrame ||
      win.mozCancelAnimationFrame ||
      win.webkitCancelAnimationFrame ||
      win.msCancelAnimationFrame;
}


//___________________________________

{ //色を定義する
  const
      MAX_COLOR   = 255,
      MAX_OPACITY = 1,
      MIN         = Math.min,
      MAX         = Math.max,
      INT         = Math.floor;


  class Color {

    constructor (r = MAX_COLOR, g = MAX_COLOR, b = MAX_COLOR, a = MAX_OPACITY) {
      this.r = MAX (0, MIN (r, MAX_COLOR));
      this.g = MAX (0, MIN (g, MAX_COLOR));
      this.b = MAX (0, MIN (b, MAX_COLOR));
      this.a = MAX (0, MIN (a, MAX_OPACITY));
    }

    //色の加算
    addColor (colorObj) {
      let {r, g, b, a} = colorObj;
      this.r = MAX (0, r * a + this.r);
      this.g = MAX (0, g * a + this.g);
      this.b = MAX (0, b * a + this.b);
    }

    //色のコピーを作る
    crone () {
      return new Color (this.r, this.g, this.b, this.a);
    }


    //乗算したコピーを作る
    multiplication (x) {
      return new Color (
        MAX (0, this.r * x),
        MAX (0, this.g * x),
        MAX (0, this.b * x),
        this.a
      );
    }


    //文字列で返す(透明度含む)
    toStringRGBA () {
      return 'rgba(' + [
        MIN (INT (this.r), MAX_COLOR),
        MIN (INT (this.g), MAX_COLOR),
        MIN (INT (this.b), MAX_COLOR),
        MIN (this.a, MAX_OPACITY)
      ].join (',') + ')';
    }


    //文字列で返す
    toSrtingRGB () {
      return 'rgb(' + [
        MIN (INT (this.r), MAX_COLOR),
        MIN (INT (this.g), MAX_COLOR),
        MIN (INT (this.b), MAX_COLOR)
      ].join (',') + ')';
    }
  }

  //__________

  this.Color = Color;
}


//___________________________________

{ //点を定義する

  const
    DEF_COLOR  = [0, 256, 0, .5],
    DEF_OPTION = {
      disabled: false,
      size    : 2
    };


  //単純な2次元の点として定義
  class Point2D {
    constructor (x = 0, y = 0) {
      this.x = x;
      this.y = y;
    }
  }


  //単純な3次元の点
  class Point3D extends Point2D {
    constructor (x, y, z = 0) {
      super (x, y);
      this.z = z;
    }
  }


  //空間に表現するための「点」
  class Point extends Point3D {

    constructor (x, y, z, color = new Color(...DEF_COLOR), option = DEF_OPTION) {
      super (x, y, z);
      this.color  = color;
      this.option = Object.assign ({ }, DEF_OPTION, option);
    }
  }

  //__________

  this.Point2D = Point2D;
  this.Point3D = Point3D;
  this.Point   = Point;
}


//___________________________________

{ //ベクトルの定義

  class Vector extends Point3D {

    constructor (x, y, z) {
      super (x, y, z);
    }


    //加算
    add ({x, y, z}) {
      this.x += x;
      this.y += y;
      this.z += z;
    }


    //内積
    innerProducts ({x, y, z}) {
      return this.x * x + this.y * y + this.z * z;
    }
  }

  //__________

  this.Vector = Vector;
}


//___________________________________

{ //面を定義する

  const
    DEF_COLOR  = [255, 255, 255,.5],
    DEF_OPTION = {
      disabled: false,
      lineColor: new Color (...DEF_COLOR)
    };


  class Surface {

    constructor (lineList = [ ], color = new Color(...DEF_COLOR), option = { }) {
      this.lineList  = lineList;
      this.lineColor = color;
      this.color     = color;
      this.option    = Object.assign ({ }, DEF_OPTION, option);
    }


    //面の3点を利用して面のベクトルを返す。面自体には点の座標の情報が無いので注意
    getVector (pointList) {
      let
        [i0, i1, i2] = this.lineList,
        {x: x0, y: y0, z: z0} = pointList[i0],
        {x: x1, y: y1, z: z1} = pointList[i1],
        {x: x2, y: y2, z: z2} = pointList[i2],

        [px, py, pz] = [x1 - x0, y1 - y0, z1 - z0], //p
        [qx, qy, qz] = [x2 - x1, y2 - y1, z2 - z1]; //q

      return new Vector (
        py * qz - pz * qy,
        pz * qx - px * qz,
        px * qy - py * qx
      );
    }


    getDistance (pointList) {
      let
        [i0, i1, i2] = this.lineList,
        {x: x0, y: y0, z: z0} = pointList[i0],
        {x: x1, y: y1, z: z1} = pointList[i1],
        {x: x2, y: y2, z: z2} = pointList[i2],

        x = (x0 + x1 + x2) / 3,
        y = (y0 + y1 + y2) / 3,
        z = (z0 + z1 + z2) / 3;

      return new Point3D (x, y, z);

    }
  }

  //__________

  this.Surface = Surface;
}


//___________________________________

{ //モデルを定義する

  const
    DEF_COLOR  = new Color (),
    DEF_OPTION = {
      disabled: false
    };


  class Model {

    constructor (vertex = [ ], surface = [ ], option = { }) {
      this.vertex  = vertex;  //頂点
      this.surface = surface; //面
      this.option  = Object.assign ({ }, DEF_OPTION, option);
    }


    //面を追加する
    addSurface (surface) {
      let len = this.surface.length;
      this.surface.push (surface);
      return len;
    }


    //頂点リストに追加する
    addVertex (vertex) {
      let len = this.vertex.length;
      this.vertex.push (vertex);
      return len;
    }


    //頂点の色をセットする
    setVertexColor (color, no = null) {
      let i, p;

      if (null === no)
        for (i = 0; p = this.vertex[i]; i += 1)
          Object.assign (p.color, color);
      else
        if (p = this.vertex[no])
          Object.assign (p.color, color);
    }


    //面の色をセットする
    setSurfaceColor (color, no = null) {
      let p;
      if (null === no)
        for (let i = 0; p = this.surface[i]; i += 1)
          Object.assign (p.color, color);
      else
        if (p = this.surface[no])
          Object.assign (p.color, color);
    }


    //物体の頂点の配列を返す
    getViewPoint () {
      return this.vertex.map (p => [p.x, p.y, p.z]);
    }
  }

  //__________

  this.Model = Model;
}


//___________________________________
// 焦点距離(Focal Length), 画角(Field Of View)
//http://www.cyber.t.u-tokyo.ac.jp/~tani/class/mech_enshu/enshu2011mi2.pdf

{ // カメラ

  const
    FOCAL_LENGTH = 50, //mm 標準レンズの焦点距離
    DEF_POSITION = [0, 0, 1000],
    DEF_OPTION   = {
      disabled: false,
      scale: 1
    };


  class Camera  {

    constructor (position = new Point3D (...DEF_POSITION), focalLength = FOCAL_LENGTH, option = { }) {
      this.position = position;
      this.FOV      = focalLength; //2 * Math.atan ((APERTURE_X / 2) / focalLength);
      this.option   = Object.assign ({ }, DEF_OPTION, option);
      this.Z        = position.z;
      this.f0       = this.option.scale * (focalLength / (1 + focalLength / position.z))
    }


    //3次から2次へ投影
    project ({x, y, z}) {
      let {Z, f0} = this, s = f0 / (Z - z);
      return [x * s, y * s];
    }
  }

  //__________

  this.Camera = Camera;
}


//___________________________________

{ //照明(将来的に点光源など複数の照明を使えるようにするべき)

  const
    DEF_COLOR   = [255, 255, 255, 1],
    DEF_VECTOR  = [1, 1, 1], //平行光なのでベクトルだけ
    DEF_OPTION  = {
      disabled     : false,
      brightness   : 1, //明るさ
      ambientLight : 0 //環境光
    };


  class Light {
    constructor (vector = new Vector (...DEF_VECTOR), color = new Color (...DEF_COLOR), option = { }) {
      let {x, y, z} = vector;

      this.vector       = vector;
      this.color        = color;
      this.option       = Object.assign ({ }, DEF_OPTION, option);
      this.distance     = Math.sqrt (x * x + y * y + z * z); //原点までの距離を算出
    }
  }

  //__________

  this.Light = Light;
}


//___________________________________

{ //マウスのドラッグ操作で配列の回転を制御(QUATERNIONによる回転)

  const
    SQRT            = Math.sqrt,
    SIN             = Math.sin,
    COS             = Math.cos,
    PI              = Math.PI,
    DEG             = PI / 180,

    TOUCH_EVENT     = ['touchstart', 'touchend', 'touchmove'],
    MOUSE_EVENT     = ['mousedown', 'mouseup', 'mousemove', 'mouseout'],
    DEF_OPTION      = {
      inertia     : true, //慣性モード有効
      sensitivity : 500,  //マウスの感度
      gain        : 0.999 //慣性移動の減速率
    };


  //ローテーションクラス本体
  class RotationController {

    constructor (element = document.body, model, option = {}) {
      this.target      = element;
      this.model       = model;
      this.option      = Object.assign ({}, DEF_OPTION, option);
      this.mouseX      = null;//マウス座標の基点
      this.mouseY      = null;//マウス座標の基点
      this.touched     = false; //ドラッグ中か?
      this.animeId     = 0;
      this.timeStamp   = null; //慣性を行うかどうかのもう一つの基準
      this.dx          = 0;//マウスの慣性移動量
      this.dy          = 0;//マウスの慣性移動量
      this.sensitivity = 1 / this.option.sensitivity; //mouse移動の感度
      this.inertia_min = 0.001; //最小の移動量で完成移動を止める

      //touchイベントがあるなら優先
      (window.TouchEvent ? TOUCH_EVENT: MOUSE_EVENT)
        .forEach (addEventType, this)
    }


    //各イベント処理
    handleEvent (event) {
      let e = event.target;
      switch (event.type) {

      // 制御終了
      case 'mouseup' :
      case 'mouseout' :
      case 'touchend' :
        if (this.touched){
          this.touched = false;
          if (event.timeStamp - this.timeStamp < 50) //mouseup から 50mm秒以内なら慣性モードへ
            if (this.option.inertia)
              inertia.call (this);//制御を慣性に移す
        }
        break;

      // 制御開始
      case 'mousedown' :
      case 'touchstart' :
        if (this.animeId) {
          cancelAnimationFrame (this.animeId);
          this.animeId = 0;
        }
        this.dx = 0;
        this.dy = 0;
        this.mouseX = event.pageX;
        this.mouseY = event.pageY;
        this.touched = true;
        break;

      // 回転制御中
      case 'mousemove' :
      case 'touchmove' :
        if (this.touched) {
          event.preventDefault ();//ipadなどでスクロールさせないため
          this.timeStamp = event.timeStamp;
          let
            {pageX, pageY} = event,
            {mouseX, mouseY, sensitivity} = this,
            sx = mouseX - pageX,
            sy = mouseY - pageY;
          if (sx || sy) {
            this.dx = sx * sensitivity;
            this.dy = sy * sensitivity;

            rotation.call (this);
            this.mouseX = pageX;
            this.mouseY = pageY;
          }
        }
        break;
      }
    }

    //quaternion rotation
    //回転の関数の独立した呼び出し
    static rotation (aryPoint3D, qx, qy, qz, qa) {
      let ql = qx * qx + qy * qy + qz * qz;

      if (ql) {
        let qh = qa * DEG * .5, s = SIN (qh) / SQRT (ql);
        product (aryPoint3D, qx * s, qy * s, qz * s, COS (qh));
      }
    }
  }


  const
    //イベントの登録
    addEventType =
      function (eventType) {
        this.target.addEventListener (eventType, this, false);
      },


    //慣性モードに移行
    inertia =
      function () {
        let gain = this.option.gain;
        this.dx *= gain;
        this.dy *= gain;
        if (this.inertia_min < rotation.call (this))
          this.animeId = requestAnimationFrame (inertia.bind (this));
      },


    //回転を行う
    rotation =
      function () {
        //画面の2次元移動量から3次元の回転量を求める
        let
          {dx, dy} = this,
          t = dx * dx + dy * dy;

        if (t) {
          // クオータニオンによる回転
          t = SQRT (t);
          let as = -SIN (t) / t;
          product (this.model.vertex, dy * as, dx * as, 0, COS (t));
        }
        return t; //移動距離を返す(慣性?)
      },


    //積(座標)の計算
    product =
      function (point, q0, q1, q2, q3) {
        for (let i = 0, p; p = point[i]; i++) {
          let
            {x, y, z} = p,
            a0 =  q3 * x + q1 * z - q2 * y,
            a1 =  q3 * y + q2 * x - q0 * z,
            a2 =  q3 * z + q0 * y - q1 * x,
            a3 = -q0 * x - q1 * y - q2 * z;

          point[i].x = a0 * q3 - a3 * q0 - a1 * q2 + a2 * q1;
          point[i].y = a1 * q3 - a3 * q1 - a2 * q0 + a0 * q2;
          point[i].z = a2 * q3 - a3 * q2 - a0 * q1 + a1 * q0;
        }
      };

  //__________

  this.RotationController = RotationController;
}


//___________________________________

{// Canvas コントローラー

  const
    DEF_OPTION = {
      surface     : true,
      hiddenSurface: true,
      lighting    : true,
      wireFrame   : false,
      vertex      : false,
      clsColor    : 'rgb(0,0,0)'
    },
    PI2 = Math.PI * 2;


  class Controller {

    constructor (canvas, camera, light, option = { }) {
      this.canvas = canvas;
      this.ctx    = canvas.getContext ('2d');
      this.camera = camera;
      this.light  = light;
      this.option = Object.assign ({}, DEF_OPTION, option);
      this.width  = canvas.width;
      this.height = canvas.height;
      this.centerX= canvas.width  * 0.5 + 0.5;
      this.centerY= canvas.height * 0.5 + 0.5;
    }


    //CANVASを塗りつぶす
    clsScreen (rgba = this.option.clsColor) {
      let {ctx, width, height} = this;
      ctx.fillStyle = rgba;
      ctx.fillRect (0, 0, width, height);
    }


    //model を描く
    draw (model) {
      let
        cos  = Math.cos,
        acos = Math.acos,
        sqrt = Math.sqrt,
        max  = Math.max,
        {ctx, centerX, centerY, light, camera, option} = this,
        {vertex, surface} = model,
        zbuf = [ ],
        {distance, option: {brightness: lb, ambientLight: la}, vector: {x: lx, y: ly, z: lz}} = light,
        distance2 = distance * distance;


      //カメラからの投影結果をxy[]に保存
      let xy = vertex.map (s => {
        let [px, py] = camera.project (s);
        return [centerX + px, centerY - py];
      });

      //面の構成が左回りを利用して2次元上で裏の向きを省く
      if (option.hiddenSurface) {
        let
          {x: cx, y: cy, z: cz} = camera.position,
          hbuf = surface.reduce ((a, b) => {
            let
              [p0, p2, p1] = b.lineList,//3点からベクトルの外積
              [[x0, y0], [x1, y1], [x2, y2]] = [xy[p0], xy[p1], xy[p2]];
            if ((x2 - x1) * (y0 - y1) - (y2 - y1) * (x0 - x1) > 0)
              a.push (b);
            return a;
          }, []);


        //距離でソート
        zbuf = hbuf.map (s => {
          let
            {x, y, z} = s.getDistance (vertex),
            dx = x - cx, dy = y - cy, dz = cz -z;
          return [dx*dx + dy*dy + dz*dz, s];
        })
          .sort ((a, b) => a[0] < b[0])
          .map ((a)=> a[1]);
      }
      else
        zbuf = surface;


      //面の描画
      if (option.surface) {
        let brightness = 1;
        zbuf.forEach (s => {
          let P = s.lineList;
          if (option.lighting) {
            let
              {x, y, z} = s.getVector (vertex),
              len = (x*lx + y*ly + z*lz) / (distance * sqrt (x*x + y*y + z*z));
            brightness = la + max (0, len * lb);
          }

          ctx.fillStyle = s.color.multiplication (brightness).toStringRGBA ();
          ctx.beginPath ();
          ctx.moveTo (...xy[P[0]]);
          P.forEach ((p, i) => { ctx.lineTo (...xy[P[i]]) });
          ctx.fill ();
        });
      }


      //線を表示する(ワイヤーフレーム)
      if (this.option.wireFrame) {
        zbuf.forEach (s => {
          let
            {lineList: P, color: C} = s,
            xy0 = xy[P[0]];
          if (C) {
            ctx.strokeStyle = C.toStringRGBA ();
            ctx.beginPath();
            ctx.moveTo (...xy0);
            P.forEach ((p) => ctx.lineTo (...xy[p]));
            ctx.lineTo (...xy0);
            ctx.stroke ();
          }
        });
      }


      //点を表示する
      if (this.option.vertex) {
        let pi2 = PI2;
        if (option.hiddenSurface)
          zbuf.forEach (s => {
            s.lineList.forEach ((k) => {
              let {color: C, option: O} = vertex[k];
              if (C) {
                ctx.fillStyle = C.toStringRGBA ();
                if (O) {
                  ctx.beginPath ();
                  ctx.arc (...xy[k], O.size, 0, pi2);
                  ctx.fill ();
              }}
            });
          });
        else
          //単純点描画処理
          vertex.forEach ((p, i) => {
            let {color: C, option: O} = p;
            if (C) {
              ctx.fillStyle = C.toStringRGBA ();
              if (O) {
                ctx.beginPath ();
                ctx.arc (...xy[i], O.size, 0, pi2);
                ctx.fill ();
            }}
          });
      }

    }
  }

  //__________

  this.CTX = Controller;
}





//___________________________________

{ //物体の定義
  const
    PI     = Math.PI,
    ACOS   = Math.acos,
    ATAN2  = Math.atan2,
    SQRT   = Math.sqrt,
    SIN    = Math.sin,
    COS    = Math.cos,

    createObject = function (arg) { return new this (...arg); },
    createObject2 = function (arg) { return new this (arg); },

    RegularIcosahedron_option = {
      pointColor: new Color ()
    }
    //正二十面体を定義する
    RegularIcosahedron = function (option) {
      let
        r  = option.radius,
        gr = (1 + SQRT (5)) / 2,
        a = r / SQRT (1 + gr * gr),
        b = a * gr,

        point = [ //物体の頂点
          [ 0,-a,-b], [ 0, a,-b], [ 0,-a, b], [0, a, b],
          [-b, 0,-a], [-b, 0, a], [ b, 0,-a], [b, 0, a],
          [-a,-b, 0], [ a,-b, 0], [-a, b, 0], [a, b, 0]
        ].map (a => new Point (...a, option.pointColor)),

        surface = [//辺の順序が面を現す
            [ 0, 1, 6], [ 1, 0, 4], [ 2, 3, 5], [ 3, 2, 7],
            [ 4, 5,10], [ 5, 4, 8], [ 6, 7, 9], [ 7, 6,11],
            [ 8, 9, 2], [ 9, 8, 0], [10,11, 1], [11,10, 3],
            [ 0, 6, 9], [ 0, 8, 4], [ 1, 4,10], [ 1,11, 6],
            [ 2, 5, 8], [ 2, 9, 7], [ 3, 7,11], [ 3,10, 5]
        ].map (createObject2, Surface);

      return new Model (point, surface, option);
    },


  //正四面体を定義する
  RegularTetrahedron =
    function (option = { radius: 1}) {
      let
        r  = option.radius,
        a = 1/SQRT(3/8) * r,
        h = SQRT(2/3)*a,
        b = -h + r,

        point = [ //物体の頂点
          [0, r, 0], [0, b, r], [a / 2, b, b], [-a/2, b, b]
        ].map (createObject, Point),

        surface = [//辺の順序が面を現す(先に登録された頂点の番号)
          [ 0, 1, 2], [ 0, 2, 3], [ 0, 3, 1], [ 3, 2, 1],
        ].map (createObject2, Surface);

      return new Model (point, surface, option);
    },


    //ドーナツの定義
    Doughnut = function (option) {
      let
        {radius, N, radius2, N2} = option,
        pi2 = PI + PI,
        ringStep = pi2 / N, cStep = pi2 / N2,
        all = N * N2,
        point = [ ], surface = [ ];

      for (let i = 0; i < N; i += 1) {
        let
          angle = ringStep * i,
          idx = i * N2,
          idx2 = (idx + N2) % all;

        for (let j = 0; j < N2; j += 1) {
          let
            cAngle = cStep * j,
            r = radius + SIN (cAngle) * radius2;

          point.push (new Point (
            SIN (angle) * r,
            COS (cAngle) * radius2,
            COS (angle) * r
          ));

          surface.push (new Surface ([
            idx + j,
            idx + (j + 1) % N2,
            idx2 + (j + 1) % N2,
            idx2 + j
          ]));
        }
      }

      return new Model (point, surface, option);
    },


     //多角推
     Pyramid_option = {
       N: 3, radius: 1, radius2: 1, color: new Color (255,0,0, .7),
       offsetX: 0, offsetY: 0, offsetZ: 0
     },

     Pyramid = function (option = { }) {
         option = Object.assign ({}, Pyramid_option, option);
       let
         {N, radius, radius2, color, offsetX, offsetY, offsetZ } = option;
         pi2 = PI + PI, cStep = pi2 / N,
         point = [ ], surface = [ ], base = [ ];

       for (let i = 0; i < N; i += 1) {
         let angle = cStep * i;
         point.push (new Point (
           offsetX + SIN (angle) * radius,
           offsetY,
           offsetZ + COS (angle) * radius
         ));
         surface.push (new Surface ([N, i, (i + 1) % N], color, option));
         base.push (i);
      }
      point.push (new Point (offsetX, radius2 + offsetY, offsetZ));
      surface.push (new Surface (base.reverse (), color, option));

      return new Model (point, surface, option);
    },


    //2つのベクトルの中間を返す
    createPoint = function (a, b, r) {
      let
        { x: ax, y: ay, z: az } = a,
        { x: bx, y: by, z: bz } = b;
        cx = (ax + bx) * .5, cy = (ay + by) * .5, cz = (az + bz) * .5,
        d = cx * cx + cy * cy + cz * cz, d2 = d * 2,
        e = SQRT (r * 2 * d2) / d2;
      return new Point (cx * e, cy * e, cz * e);
    },


    //三角形を4分割する
    SplitTriangle = function () {
      let
        {surface, vertex, option} = this,
        r = option.radius, rr = r * r;

      //三角形abc の 線分acの中点をd, 線分bcの中点をe, 線分caの中点をfとする
      surface.forEach (s => {
        let
          {lineList, color, option} = s,
          [p0, p1, p2] = lineList,
          [ a,  b,  c] = [vertex[p0], vertex[p1], vertex[p2]],
          [ d,  e,  f] = [createPoint (a, b, rr), createPoint (b, c, rr), createPoint (c, a, rr)],
          [dn, en, fn] = [this.addVertex (d), this.addVertex (e), this.addVertex (f)];

        //従来の三角形を4分割した中央の三角形にする
        s.lineList = [dn, en, fn];
        [vertex[dn], vertex[en], vertex[fn]] = [d, e, f];
        //他の3つの三角形の面を追加する
        this.addSurface (new Surface ([p0, dn, fn], color, option));
        this.addSurface (new Surface ([p1, en, dn], color, option));
        this.addSurface (new Surface ([p2, fn, en], color, option));
      });
    };

  //________________________

  const Modeler = function (type, option) {
    let model;

    switch (type) {
    case 'regularIcosahedron' : //正二十面体
      model = RegularIcosahedron (option);
      for (let i = 0; i < option.split; i += 1)
        SplitTriangle.call (model);
      break;

    case 'regularTetrahedron' : //正四面体
      model = RegularTetrahedron (option);
      for (let i = 0; i < option.split; i += 1)
        SplitTriangle.call (model);
      break;

    case 'doughnut' : //ドーナツ
      model = Doughnut (option);
      break;

    case 'pyramid' : //多角推
      model = Pyramid (option);
      break;
    }

    return model;
  }

  //__________

  this.Modeler = Modeler;
}


//___________________________________

{ //demo

  const
    R      = 130,
    CANVAS = document.querySelectorAll ('canvas'),
    CAMERA = new Camera (new Point3D (0, 0, 700), 50, {scale: 20})

  //光源用の設定
  const
    LCtrl_MODEL_OPT = { radius: 10, split: 1, N:8, radius2: 60, offsetY: -20},
    LCtrl_MODEL     = Modeler ('pyramid', LCtrl_MODEL_OPT),
    LCtrl_ROTE_OPT  = {sensitivity: 80, gain: 0};
    LCtrl_ROTE      = new RotationController (CANVAS[0], LCtrl_MODEL, LCtrl_ROTE_OPT),
    LCtrl_LIGHT_OPT = {ambientLight: 0.5},
    LCtrl_LIGHT     = new Light (new Vector (0, 1, 1), new Color (), LCtrl_LIGHT_OPT),
    LCtrl_CTX_OPT   = {vertex: false, wireFrame: true, surface: true, hiddenSurface: true},
    LCtrl_CTX       = new CTX (CANVAS[0], CAMERA, LCtrl_LIGHT, LCtrl_CTX_OPT);

  //モデル用の設定
  const
    MCtrl_MODEL_OPT = { radius: R, N: 36, radius2: 30, N2: 24},
    MCtrl_MODEL     = Modeler ('doughnut', MCtrl_MODEL_OPT),
    MCtrl_ROTE_OPT  = {gain:1},
    MCtrl_ROTE      = new RotationController (CANVAS[1], MCtrl_MODEL, MCtrl_ROTE_OPT),
    MCtrl_LIGHT_OPT = {ambientLight: 0.3},
    MCtrl_LIGHT     = new Light (new Vector (0,1, 0), new Color (), MCtrl_LIGHT_OPT),
    MCtrl_CTX_OPT   = {vertex: false, wireFrame: false, surface: true, hiddenSurface: true},
    MCtrl_CTX       = new CTX (CANVAS[1], CAMERA, MCtrl_LIGHT, MCtrl_CTX_OPT);



  const
    OPACITY = 0.7,

    rnd =
      function () { return Math.floor (Math.random() * 128+128); },


    setRandomColor =
      function (model) {
        let i, surface;

        for (i = 0; surface = model.surface[i]; i++)
          surface.color = new Color (rnd (), rnd (), rnd (), OPACITY);
      },


    setColor =
      function (model, doc) {
        inp = doc.querySelectorAll ('#red, #green, #blue, #random');
        inp = Array.prototype.slice.call (inp);
        if (inp[3].checked) {
          model.surface.forEach (s => s.color = new Color (
            (inp[0].checked ? rnd () : 0),
            (inp[1].checked ? rnd () : 0),
            (inp[2].checked ? rnd () : 0),
            OPACITY
          ));
        }
        else {
          let col = new Color (255*inp[0].checked, 255*inp[1].checked, 255*inp[2].checked, OPACITY);
          model.surface.forEach (s => s.color = col);
        }
      },


    handler =
      function (event) {
        let
          e     = event.target,
          model = null, inp;

        if ('INPUT' !== e.tagName) return;

        switch (e.id) {

        case 'regularIcosahedron' : //正二十面体
        case 'regularTetrahedron' : //正四面体
        case 'doughnut' : //ドーナツ
        case 'pyramid' : //多角推
          inp = e.ownerDocument.querySelectorAll ('#radius, #radius2, #N, #N2, #split');
          inp = Array.prototype.slice.call (inp);
          let model_opt = inp.reduce ((a,b) => {a[b.id]=parseInt(b.value,10); return a}, {});
          model = Modeler (e.id, model_opt);
          Object.assign (MCtrl_MODEL, model);
          setColor (MCtrl_MODEL, e.ownerDocument);
          break;



        case 'vertex' : case 'surface': case 'wireFrame' : case 'hiddenSurface' :
          inp = e.ownerDocument.querySelectorAll ('#vertex, #surface, #wireFrame, #hiddenSurface');
          inp = Array.prototype.slice.call (inp);
          let ctx_opt = inp.reduce ((a,b) => {a[b.id]=b.checked; return a}, {});
          Object.assign (MCtrl_CTX.option, ctx_opt);
          break;

        case 'red': case 'green' : case 'blue' : case 'random' :
          setColor (MCtrl_MODEL, e.ownerDocument);
          break;
        }

      },


    loop = function loop () {
      LCtrl_CTX.clsScreen ();
      LCtrl_CTX.draw (LCtrl_MODEL);

      MCtrl_CTX.clsScreen ();
      MCtrl_CTX.draw (MCtrl_MODEL);

      requestAnimationFrame (loop);
    };


  //モデル用の光源ベクトルを、光源用のモデルの頂点に追加する
  LCtrl_MODEL.addVertex (MCtrl_LIGHT.vector);

  //モデルの表面をランダムな色にする
  setRandomColor (MCtrl_MODEL);

  //イベントの登録
  document.addEventListener ('click',      handler, false);
  document.addEventListener ('touchStart', handler, false);

  this.demo = loop;
}

//___________________________________

demo ();

</script>