マウスで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>