マウスでドラッグすると3D回転します

マウスでドラッグすると回転します

<canvas width="1024px" height="700px" id="D20101003b"></canvas>

<script>

class CG {
  constructor (canvas, offset = new P2) {
    this.canvas = canvas;
    this.offset = offset;
    this.ctx = canvas.getContext ('2d');
    canvas.width = canvas.clientWidth;
    canvas.height = canvas.clientHeight;
  }

  cls () {
    this.canvas.width = this.canvas.width;//高速な場合がある
    /*
    let {width, height}=this.canvas;
    this.ctx.clearRect(0,0,width,height)
    */
    return this;
  }

  line3D (obj, color = "black", d = 200, sc = 350) {
    let
      ctx = this.ctx,
      cx = this.canvas.width / 2 + this.offset.x,
      cy = this.canvas.height / 2 + this.offset.y;
    
    ctx.beginPath ();
    ctx.strokeStyle = color;

    for (let [s,e] of obj.line) {
      let
        {x,y,z} = obj.point[s],
        {x:X,y:Y,z:Z} = obj.point[e];
      let z0 = sc / (d - z), z1 = sc / (d - Z);
      ctx.moveTo (cx + x * z0, cy - y * z0);
      ctx.lineTo (cx + X * z1, cy - Y * z1);

    }
    ctx.stroke ();
    return this;
  }
}

class WireFrame {
  constructor () {
    this.point = [ ];
    this.line = [ ];
    this.attr = [ ];
    this.index = null;
  }
  addPoint (...p) {
    return this.index = this.point.push (...p) - 1;
  }
  addLine (...ps) {
    if (1 == ps.length) ps.unshift (this.index);
    for (let i = 0, I = ps.length - 1; i < I; i++) {
      this.line.push ([ps[i], ps[i+1]])
    }
    return this;
  }
  addAttr (...at) {
    this.attr.push (...at)
  }
}



//__________________________

class P2{
  constructor(x=0,y=0){this.x=x;this.y=y}
  get clone(){return new this.constructor(this.x, this.y)}//複写
  get toArray(){return[this.x,this.y]}
  add({x=0,y=0},{x:X,y:Y}=this){this.x=X+x;this.y=Y+y;return this}//加算
  sub({x=0,y=0},{x:X,y:Y}=this){this.x=X-X;this.y=Y-y;return this}//減算
  mul({x=0,y=0},{x:X,y:Y}=this){this.x=X*x;this.y=Y*y;return this}//乗算
  div({x=0,y=0},{x:X,y:Y}=this){this.x=X/x;this.y=Y/y;return this}//除算
  sMul(n=0,{x:X,y:Y}=this){this.x=X*n;this.y=Y*n;return this}//スカラー倍
}

class V2 extends P2{
  constructor(...p){super(...p)}
  get length(){return(this.x**2+this.y**2)**.5}//長さ
  get negate(){this.x=-this.x;this.y=-this.y;return this}//逆向き
  get normalize(){let L=1/(this.length||1);this.x*=L;this.y*=L;return this}//単位化
  get angle(){return Math.atan2(-this.y,-this.x)+Math.PI}//原点を基準にした角度
  dot({x=0,y=0},{x:X,y:Y}=this){return X*x+Y*y}//ドット積
  cross({x=0,y=0},{x:X,y:Y}=this){return X*y-Y*x}//クロス積
}

//__________________________

class P3{
  constructor(x=0,y=0,z=0){this.x=x,this.y=y,this.z=z}
  get clone(){return new this.constructor(this.x,this.y,this.z)}//複写
  get toArray(){return[this.x,this.y,this.z]}
  add({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){this.x=X+x;this.y=Y+y;this.z=Z+z;return this}//加算
  sub({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){this.x=X-x;this.y=Y-y;this.z=Z-z;return this}//減算
  mul({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){this.x=X*x;this.y=Y*y;this.z=Z*z;return this}//乗算
  div({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){this.x=X/x;this.y=Y/y;this.z=Z/z;return this}//除算
  sMul(n=0,{x:X,y:Y,z:Z}=this){this.x=X*n;this.y=Y*n;this.z=Z*n;return this}//スカラー倍
  applyQuaternion({x=0,y=0,z=0,w=1},{x:X,y:Y,z:Z}=this){
    let a=w*X+y*Z-z*Y,b=w*Y+z*X-x*Z,c=w*Z+x*Y-y*X,d=-x*X-y*Y-z*Z;
    return this.x=a*w+d*-x+b*-z-c*-y,this.y=b*w+d*-y+c*-x-a*-z,this.z=c*w+d*-z+a*-y-b*-x,this;
  }
}

class Vector extends P3{
  constructor(...p){super(...p)}
  get length(){return(this.x**2+this.y**2+this.z**2)**.5}//長さ
  get negate(){this.x=-this.x;this.y=-this.y;this.z=-this.z;return this}//逆向き
  get normalize(){let l=this.length||1;this.x/=l;this.y/=l;this.z/=l;return this}//単位化
  dot({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){return X*x+Y*y+Z*z}
    cross ({x=0,y=0,z=0},{x:X,y:Y,z:Z}=this){return this.x=y*Z-z*Y,this.y=z*X-x*Z,this.z=x*Y-y*X,this}
  rotation (angle0 = 0, angle1 = 0) {
    let q = new Quaternion (this.x, this.y, this.z, 1)
    const d = Math.PI / 180, sin = Math.sin, cos = Math.cos;
    let
      {x,y,z}=this,
      d0 = angle0 * d, sin0 = sin (d0), cos0 = cos (d0),
      d1 = angle1 * d, sin1 = sin (d1), cos1 = cos (d1),
      x0 = x * cos0 + y * sin0,
      y0 = x * -sin0 + y * cos0,
      x1 = x0 * cos1 + z * sin1,
      z1 = x0 * -sin1 + z * cos1;
    this.x = x1; this.y = y0; this.z = z1; return this;
  }
}

//__________________________

class Quaternion{
  constructor (x=0,y=0,z=0,w=1){this.x=x;this.y=y;this.z=z;this.w=w}
  get clone(){return new this.constructor(this.x,this.y,this.z,this.w)}//複写
  get length(){return(this.x**2+this.y**2+this.z**2+this.z**2)**.5}
    get normalize(){let L=this.length;return (L?(L=1/L,this.x*=L,this.y*=L,this.z*=L,this.w*=L):this.x=this.y=this.z=0,this.w=1),this}
  dot({x=0,y=0,z=0,w=1},{x:X,y:Y,z:Z,w:W}=this){return X*x+Y*y+Z*z+W*w}
  mul({x=0,y=0,z=0,w=1},{x:X,y:Y,z:Z,w:W}=this){return this.x=X*w+W*x+Y*z-Z*y,this.y=Y*w+W*y+Z*x-X*z,this.z=Z*w+W*z+X*y-Y*x,this.w=W*w-X*x-Y*y-Z*z,this}
  add({x,y,z,w}){return this.x+=x,this.y+=y,this.z+=z,this.w+=w,this}
  setFromAxisAngle(a=0,_){this.w*=Math.cos(_=a/2);this.x*=(_=Math.sin(_));this.y*=_;this.z*=_;return this}
}

/*__________________________________________________
マウスのドラッグ操作で配列の回転を制御(QUATERNIONによる回転)
const rc = new RotationController;
[P3,...].forEach (p=> p.applyQuaternion (rc.currentQuaternion));
*/
class RotationController {
  constructor (element = document.body, option = { }) {
    this.target      = element;//マウス操作の対象要素
    this.option      = Object.assign ({ },this.constructor.getDefaultOption (), option);
    this.touched     = false; //ドラッグ中か?
    this.distance    = [0, 0];//マウスの移動距離により回転のクオータニオンを生成する
    this.mousePoint  = [0, 0];//初期のマウスポインタ
    this.quaternion  = this.option.quaternion;//積算され続ける

    (window.TouchEvent //イベント登録
      ? ['touchstart', 'touchend', 'touchmove'] //touchイベントがあるなら優先
      : ['mousedown', 'mouseup', 'mousemove', 'mouseout']
    ).forEach (e=> element.addEventListener(e, this, false));
  }
  
  handleEvent (event) {//各イベント処理
    let { type, pageX, pageY } = event;
    switch (type) {
      case 'mousedown' : case 'touchstart': this.touched = true; break;// 制御開始
      case 'mouseup'   : case 'mouseout'  : case 'touchend' : this.touched = false; break;// 制御終了
      case 'mousemove' : case 'touchmove' :// 回転制御中
        event.preventDefault ();//ipadなどでスクロールさせないため
        this.touched && (this.moveMousePoint = [pageX, pageY]);
        break;
    }
    this.mousePoint = [pageX, pageY];
  }

  set moveMousePoint ([px, py]) {//マウスが動くことで差を求め距離をセットする
    let [mx, my] = this.mousePoint, s = this.option.sensitivity;
    this.distance = [(mx - px) * s, (my - py) * s];
  } 

  set distance ([x, y]) {//距離から回転クオータニオンを求める
    let d = (x**2 + y**2)**.5, s = -Math.sin (d) / (d || 1);//距離が0でも計算
    this._d = [x, y];
    this._q = new Quaternion (y * s, x * s, 0, Math.cos (d));
  }

  get currentQuaternion () {//現在の回転量
    let gain = this.option.gain;
    this.distance = this._d.map (n=> n * gain);
    this.quaternion.mul (this._q);
    return this._q;
  }

  static getDefaultOption (key = null) {
    const opt = {
      sensitivity : 1/300,  //マウスの感度
      gain        : .95, //慣性移動の減速率
      quaternion  : new Quaternion, //初期の回転量
    };
    return null === key ? opt: opt[key];
  }
}
//__________________________________________________



class Node {
  constructor (parent, attr, ...childs) {
    this.parent = parent;
    this.attr = attr;
    this.childs = childs;
  }
  append (...childs) { this.childs.push (...childs) }
  hasChild () { return !!this.childs.length;}
}


class Tree extends Node {
  constructor (parent, attr, iden, branch = 0) {
    super (parent, attr);
    this.iden = iden;

    if (branch) this.growup (branch);//枝分かれ
  }

  growup (cnt = 1) {
    const loop = (parent, cnt)=> {//再起呼び出し
      if (cnt--) {
        if (! parent.hasChild ())
          parent.append (...parent.iden (parent));//遺伝子を使て枝を生成
        parent.childs.forEach (t=> loop (t, cnt));
      }
    };

    loop (this, cnt);
    return this;
  }


  getTreeData (offset = new P3, wireFrame) {
    const loop = (e, position, cnt = 0, beginNo = null)=> {
      let
        vector = e.attr,
        p0 = position.clone,
        p1 = p0.clone.add (vector),
        endNo = null;

      if (null === beginNo) {
        beginNo = wireFrame.addPoint (p0);
      }
      endNo = wireFrame.addPoint (p1);
      wireFrame.addLine (beginNo, endNo);
      wireFrame.addAttr (cnt);

      e.childs.forEach (a=> loop (a, p1, cnt + 1, endNo));
    }

    loop (this, offset);
    return wireFrame;
  }


  makeTree (...attrs) {
    let
      { parent, attr:pvc, iden } = this,//parent
      rst = [ ];

    for (let [len, rot0, rot1] of attrs) {
      let p = pvc.clone.sMul(len), sin0, cos0, sin1, cos1;
//rot0=0;
//rot1=0;
      sin0 = Math.sin (rot0*Math.PI/180/2);
      cos0 = Math.cos (rot0*Math.PI/180/2);
      sin1 = Math.sin (rot1*Math.PI/180/2);
      cos1 = Math.cos (rot1*Math.PI/180/2);
      let Q = new Quaternion (0, 0, sin0, cos0);
//    Q.normalize;

      p.applyQuaternion (Q);
//      p.applyQuaternion ({x: sin1, y: 0, z: 0, w: cos1});

      rst.push (new Tree (this, p, iden));
    }
    return rst;
  }
}

//__________________________________________________

//__________________________________________________
const
  //canvas
  cg = new CG (document.querySelector ('#D20101003b'), new P2(0,100)),
  //遺伝子となる関数
  idenA = parent=> parent.makeTree ([.90,30,-45],[.6, -30,-45]),
  //木を作る  
  treeA = new Tree (null, new Vector (20, 30, 0), idenA, 11),
  //
  DATA = treeA.getTreeData (new P3(20,0,0), new WireFrame),
  PLACE0 = new WireFrame,
  PLACE1 = new WireFrame;

PLACE0.addPoint (new P3(-200,0,0), new P3(200,0,0), new P3(0,0,-200), new P3(0,0,200));
PLACE0.addLine (0, 1);
PLACE0.addLine (2, 3);

let no = 0;
for (let i = -100; i <= 100; i+=10) {
  if (i) {
    PLACE1.addPoint (new P3(-100,0,i), new P3(100,0,i)); PLACE1.addLine (no, no+1);
    PLACE1.addPoint (new P3(i,0,-100), new P3(i,0,100)); PLACE1.addLine (no+2, no+3);
    no+=4;
  }
}

const mc = new RotationController (document.body, DATA);

const
  demo = function loop () {
    cg.cls ();
    let cq = mc.currentQuaternion;
    PLACE0.point.forEach (p=> p.applyQuaternion (cq));
    PLACE1.point.forEach (p=> p.applyQuaternion (cq));
    DATA.point.forEach (p=> p.applyQuaternion (cq));
    cg.line3D (PLACE0, "rgba(255,0,0,.3)");
    cg.line3D (PLACE1, "rgba(0,0,0,.3)");
    cg.line3D (DATA, "green");
    requestAnimationFrame (loop);
  };

demo ();
</script>