マウスでドラッグすると回転します
<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>