JavaScript:マウスのドラッグ操作により、3次元の物体の回転を制御する

/*
 マウスのドラッグ操作で、3次元の物体の回転を制御する

*/

(function () {

  var INIT_QUATERNION = [1, 0, 0, 0];

  function RotationController (element) {
    this.target = element;
    this.mouseX = null;//マウス座標の基点
    this.mouseY = null;//マウス座標の基点
    this.touchF = false; //ドラッグ中か?
    this.Qnow = INIT_QUATERNION; //今回のマウスのドラッグ中のクォータニオン
    this.Qbef = INIT_QUATERNION; //前回のクォータニオン
    this.rots = INIT_QUATERNION; //今回と前回のクォータニオンの積(これが重要)
    this.gain = 1 / element.offsetWidth ; // mouse移動の感度
    this.dx = 0;//マウスの慣性移動量
    this.dy = 0;//マウスの慣性移動量
    this.timerId = null;//慣性移動中のタイマーID
    this.miniInertia = 1e-7;//慣性移動量の最小値
  }
  
  
  //画面の2次元移動量から3次元の回転量を求める
  function rotation (dx, dy) {
    var a, b, a0, a1, a2, a3, b0, b1, b2, b3, r, t, as;

    if (t = dx * dx + dy * dy) {
      r = Math.sqrt (t);
      as = Math.sin (r) / r;
      a = this.Qnow;
      a0 = a[0]; a1 = a[1]; a2 = a[2]; a3 = a[3];
      b0 = dy * as; b1 = dx * as; b3 = Math.cos (r);

      // クオータニオンによる回転
      a = this.Qbef;
      b = this.Qnow = [
        a0 * b3 - a3 * b0           - a2 * b1,
        a1 * b3 + a3 * b1 - a2 * b0,
        a2 * b3           + a0 * b1 + a1 * b0,
        a3 * b3 + a0 * b0 - a1 * b1
      ];

      //前回(a)と今回(b)のクォータニオンの積
      a0 = a[0]; a1 = a[1]; a2 = a[2]; a3 = a[3];
      b0 = b[0]; b1 = b[1]; b2 = b[2]; b3 = b[3];
      
      this.rots = [
        a0 * b0 - a1 * b1 - a2 * b2 - a3 * b3,
        a0 * b1 + a1 * b0 + a2 * b3 - a3 * b2,
        a0 * b2 - a1 * b3 + a2 * b0 + a3 * b1,
        a0 * b3 + a1 * b2 - a2 * b1 + a3 * b0
      ];
      this.dx = dx;
      this.dy = dy;
    }
    return t;
  }
  
  
  //慣性
  function inertia () {
    var distance = rotation.call (
      this,
      this.dx - this.dx / 40,
      this.dy - this.dy / 40
    );
    
    if (this.miniInertia < distance)
      this.timerId = setTimeout (inertia.bind (this), 33);
  }


  //クォータニオンによる座標群の回転
  function quaternionRotation (material) {
    
    var i, j, x, y, z;
    var surface, vertex;
    var q = this.rots;
    var q0 = q[0], q1 = q[1], q2 = q[2], q3 = q[3];
    var a0, a1, a2, a3;
    var s = [], rst = [];

    for (i = 0; surface = material[i]; i++) {
      for (j = 0, s = []; vertex = surface[j]; j++) {
        x = vertex[0], y = vertex[1], z = vertex[2];
        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;
        s[j] = [
          a0 * q3 - a3 * q0 - a1 * q2 + a2 * q1,
          a1 * q3 - a3 * q1 - a2 * q0 + a0 * q2,
          a2 * q3 - a3 * q2 - a0 * q1 + a1 * q0
        ];
      }
      rst[i] = s;
    }
    return rst;
  }

  
  //各イベント処理
  function handleEvent (event) {
    var e, x, y, dx, dy, a, b, c, e, r, t;
    var a0, a1, a2, a3, b0, b1, b2, b3, as;
    
    switch (event.type) {

    // 制御終了
    case 'mouseup' :
    case 'mouseout' :
    case 'touchend' :
      this.touchF = false;
      inertia.call (this);//制御を慣性にする
      break;
    
    // 制御開始
    case 'mousedown' :
    case 'touchstart' :
      if (this.timerId) {//慣性を解除
        clearTimeout (this.timerId);
        this.timerId = null;
      }
      this.touchF = true;
      this.Qnow = INIT_QUATERNION;
      this.Qbef = this.rots;
      e = event.target.getBoundingClientRect ();
      this.mouseX = event.pageX - e.left;
      this.mouseY = event.pageY - e.top;
      break;

    // 回転制御中
    case 'mousemove' :
    case 'touchmove' :
      event.preventDefault ();//ipadなどでスクロールさせないため
      e = event.target.getBoundingClientRect ();
      x = event.pageX - e.left;
      y = event.pageY - e.top;

      if (this.touchF){
        dx = (x - this.mouseX) * this.gain;
        dy = (y - this.mouseY) * this.gain;
        rotation.call (this, dx, dy);
      }

      this.mouseX = x;
      this.mouseY = y;
      break;
    }
  
  }
  
  
  // 要素にイベントを追加する
  function addEvent (event_type) {
    this.target.addEventListener (event_type, this, false);
  }
  
  
  // オブジェクトの生成
  function create (target) {
    if (1 > arguments.length)
      throw new Error ('引数がない');

    var obj = new RotationController (target);
    var event_list = window.TouchEvent //touchイベントがあるなら優先
      ? ['touchstart', 'touchend', 'touchmove']
      : ['mousedown', 'mouseup', 'mousemove', 'mouseout'];
      
    canvas = null;// メモリーリークパターンを断ち切る
    event_list.forEach (addEvent, obj);

    return obj;
  }
  
  //__
  
  RotationController.prototype.handleEvent = handleEvent;
  RotationController.prototype.quaternionRotation = quaternionRotation;
  //__
  RotationController.create = create;
  
  this.RotationController = RotationController;

}) ();

/*
使い方

  var loop = function () {
    var target = document.querySelector ('canvas');
    var ctl = RotationController.create (target);
    var ps = [ [x0, y0, z0], .., [xn, yn, zn] ];//3次元座標の配列
    
    return function () {
      var ps_ = ctl.quaternionRotation (ps);
      //ps_ には、回転後の3次元座標が代入される
      draw (ps_);//ps_を利用して線や点を描く
    };
  };
  
  setInterval (loop, 1000/60); //タイマーで呼び出す
*/