'use strict' module.exports = createTurntableController var filterVector = require('filtered-vector') var invert44 = require('gl-mat4/invert') var rotateM = require('gl-mat4/rotate') var cross = require('gl-vec3/cross') var normalize3 = require('gl-vec3/normalize') var dot3 = require('gl-vec3/dot') function len3(x, y, z) { return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2)) } function clamp1(x) { return Math.min(1.0, Math.max(-1.0, x)) } function findOrthoPair(v) { var vx = Math.abs(v[0]) var vy = Math.abs(v[1]) var vz = Math.abs(v[2]) var u = [0,0,0] if(vx > Math.max(vy, vz)) { u[2] = 1 } else if(vy > Math.max(vx, vz)) { u[0] = 1 } else { u[1] = 1 } var vv = 0 var uv = 0 for(var i=0; i<3; ++i ) { vv += v[i] * v[i] uv += u[i] * v[i] } for(var i=0; i<3; ++i) { u[i] -= (uv / vv) * v[i] } normalize3(u, u) return u } function TurntableController(zoomMin, zoomMax, center, up, right, radius, theta, phi) { this.center = filterVector(center) this.up = filterVector(up) this.right = filterVector(right) this.radius = filterVector([radius]) this.angle = filterVector([theta, phi]) this.angle.bounds = [[-Infinity,-Math.PI/2], [Infinity,Math.PI/2]] this.setDistanceLimits(zoomMin, zoomMax) this.computedCenter = this.center.curve(0) this.computedUp = this.up.curve(0) this.computedRight = this.right.curve(0) this.computedRadius = this.radius.curve(0) this.computedAngle = this.angle.curve(0) this.computedToward = [0,0,0] this.computedEye = [0,0,0] this.computedMatrix = new Array(16) for(var i=0; i<16; ++i) { this.computedMatrix[i] = 0.5 } this.recalcMatrix(0) } var proto = TurntableController.prototype proto.setDistanceLimits = function(minDist, maxDist) { if(minDist > 0) { minDist = Math.log(minDist) } else { minDist = -Infinity } if(maxDist > 0) { maxDist = Math.log(maxDist) } else { maxDist = Infinity } maxDist = Math.max(maxDist, minDist) this.radius.bounds[0][0] = minDist this.radius.bounds[1][0] = maxDist } proto.getDistanceLimits = function(out) { var bounds = this.radius.bounds[0] if(out) { out[0] = Math.exp(bounds[0][0]) out[1] = Math.exp(bounds[1][0]) return out } return [ Math.exp(bounds[0][0]), Math.exp(bounds[1][0]) ] } proto.recalcMatrix = function(t) { //Recompute curves this.center.curve(t) this.up.curve(t) this.right.curve(t) this.radius.curve(t) this.angle.curve(t) //Compute frame for camera matrix var up = this.computedUp var right = this.computedRight var uu = 0.0 var ur = 0.0 for(var i=0; i<3; ++i) { ur += up[i] * right[i] uu += up[i] * up[i] } var ul = Math.sqrt(uu) var rr = 0.0 for(var i=0; i<3; ++i) { right[i] -= up[i] * ur / uu rr += right[i] * right[i] up[i] /= ul } var rl = Math.sqrt(rr) for(var i=0; i<3; ++i) { right[i] /= rl } //Compute toward vector var toward = this.computedToward cross(toward, up, right) normalize3(toward, toward) //Compute angular parameters var radius = Math.exp(this.computedRadius[0]) var theta = this.computedAngle[0] var phi = this.computedAngle[1] var ctheta = Math.cos(theta) var stheta = Math.sin(theta) var cphi = Math.cos(phi) var sphi = Math.sin(phi) var center = this.computedCenter var wx = ctheta * cphi var wy = stheta * cphi var wz = sphi var sx = -ctheta * sphi var sy = -stheta * sphi var sz = cphi var eye = this.computedEye var mat = this.computedMatrix for(var i=0; i<3; ++i) { var x = wx * right[i] + wy * toward[i] + wz * up[i] mat[4*i+1] = sx * right[i] + sy * toward[i] + sz * up[i] mat[4*i+2] = x mat[4*i+3] = 0.0 } var ax = mat[1] var ay = mat[5] var az = mat[9] var bx = mat[2] var by = mat[6] var bz = mat[10] var cx = ay * bz - az * by var cy = az * bx - ax * bz var cz = ax * by - ay * bx var cl = len3(cx, cy, cz) cx /= cl cy /= cl cz /= cl mat[0] = cx mat[4] = cy mat[8] = cz for(var i=0; i<3; ++i) { eye[i] = center[i] + mat[2+4*i]*radius } for(var i=0; i<3; ++i) { var rr = 0.0 for(var j=0; j<3; ++j) { rr += mat[i+4*j] * eye[j] } mat[12+i] = -rr } mat[15] = 1.0 } proto.getMatrix = function(t, result) { this.recalcMatrix(t) var mat = this.computedMatrix if(result) { for(var i=0; i<16; ++i) { result[i] = mat[i] } return result } return mat } var zAxis = [0,0,0] proto.rotate = function(t, dtheta, dphi, droll) { this.angle.move(t, dtheta, dphi) if(droll) { this.recalcMatrix(t) var mat = this.computedMatrix zAxis[0] = mat[2] zAxis[1] = mat[6] zAxis[2] = mat[10] var up = this.computedUp var right = this.computedRight var toward = this.computedToward for(var i=0; i<3; ++i) { mat[4*i] = up[i] mat[4*i+1] = right[i] mat[4*i+2] = toward[i] } rotateM(mat, mat, droll, zAxis) for(var i=0; i<3; ++i) { up[i] = mat[4*i] right[i] = mat[4*i+1] } this.up.set(t, up[0], up[1], up[2]) this.right.set(t, right[0], right[1], right[2]) } } proto.pan = function(t, dx, dy, dz) { dx = dx || 0.0 dy = dy || 0.0 dz = dz || 0.0 this.recalcMatrix(t) var mat = this.computedMatrix var dist = Math.exp(this.computedRadius[0]) var ux = mat[1] var uy = mat[5] var uz = mat[9] var ul = len3(ux, uy, uz) ux /= ul uy /= ul uz /= ul var rx = mat[0] var ry = mat[4] var rz = mat[8] var ru = rx * ux + ry * uy + rz * uz rx -= ux * ru ry -= uy * ru rz -= uz * ru var rl = len3(rx, ry, rz) rx /= rl ry /= rl rz /= rl var vx = rx * dx + ux * dy var vy = ry * dx + uy * dy var vz = rz * dx + uz * dy this.center.move(t, vx, vy, vz) //Update z-component of radius var radius = Math.exp(this.computedRadius[0]) radius = Math.max(1e-4, radius + dz) this.radius.set(t, Math.log(radius)) } proto.translate = function(t, dx, dy, dz) { this.center.move(t, dx||0.0, dy||0.0, dz||0.0) } //Recenters the coordinate axes proto.setMatrix = function(t, mat, axes, noSnap) { //Get the axes for tare var ushift = 1 if(typeof axes === 'number') { ushift = (axes)|0 } if(ushift < 0 || ushift > 3) { ushift = 1 } var vshift = (ushift + 2) % 3 var fshift = (ushift + 1) % 3 //Recompute state for new t value if(!mat) { this.recalcMatrix(t) mat = this.computedMatrix } //Get right and up vectors var ux = mat[ushift] var uy = mat[ushift+4] var uz = mat[ushift+8] if(!noSnap) { var ul = len3(ux, uy, uz) ux /= ul uy /= ul uz /= ul } else { var ax = Math.abs(ux) var ay = Math.abs(uy) var az = Math.abs(uz) var am = Math.max(ax,ay,az) if(ax === am) { ux = (ux < 0) ? -1 : 1 uy = uz = 0 } else if(az === am) { uz = (uz < 0) ? -1 : 1 ux = uy = 0 } else { uy = (uy < 0) ? -1 : 1 ux = uz = 0 } } var rx = mat[vshift] var ry = mat[vshift+4] var rz = mat[vshift+8] var ru = rx * ux + ry * uy + rz * uz rx -= ux * ru ry -= uy * ru rz -= uz * ru var rl = len3(rx, ry, rz) rx /= rl ry /= rl rz /= rl var fx = uy * rz - uz * ry var fy = uz * rx - ux * rz var fz = ux * ry - uy * rx var fl = len3(fx, fy, fz) fx /= fl fy /= fl fz /= fl this.center.jump(t, ex, ey, ez) this.radius.idle(t) this.up.jump(t, ux, uy, uz) this.right.jump(t, rx, ry, rz) var phi, theta if(ushift === 2) { var cx = mat[1] var cy = mat[5] var cz = mat[9] var cr = cx * rx + cy * ry + cz * rz var cf = cx * fx + cy * fy + cz * fz if(tu < 0) { phi = -Math.PI/2 } else { phi = Math.PI/2 } theta = Math.atan2(cf, cr) } else { var tx = mat[2] var ty = mat[6] var tz = mat[10] var tu = tx * ux + ty * uy + tz * uz var tr = tx * rx + ty * ry + tz * rz var tf = tx * fx + ty * fy + tz * fz phi = Math.asin(clamp1(tu)) theta = Math.atan2(tf, tr) } this.angle.jump(t, theta, phi) this.recalcMatrix(t) var dx = mat[2] var dy = mat[6] var dz = mat[10] var imat = this.computedMatrix invert44(imat, mat) var w = imat[15] var ex = imat[12] / w var ey = imat[13] / w var ez = imat[14] / w var gs = Math.exp(this.computedRadius[0]) this.center.jump(t, ex-dx*gs, ey-dy*gs, ez-dz*gs) } proto.lastT = function() { return Math.max( this.center.lastT(), this.up.lastT(), this.right.lastT(), this.radius.lastT(), this.angle.lastT()) } proto.idle = function(t) { this.center.idle(t) this.up.idle(t) this.right.idle(t) this.radius.idle(t) this.angle.idle(t) } proto.flush = function(t) { this.center.flush(t) this.up.flush(t) this.right.flush(t) this.radius.flush(t) this.angle.flush(t) } proto.setDistance = function(t, d) { if(d > 0) { this.radius.set(t, Math.log(d)) } } proto.lookAt = function(t, eye, center, up) { this.recalcMatrix(t) eye = eye || this.computedEye center = center || this.computedCenter up = up || this.computedUp var ux = up[0] var uy = up[1] var uz = up[2] var ul = len3(ux, uy, uz) if(ul < 1e-6) { return } ux /= ul uy /= ul uz /= ul var tx = eye[0] - center[0] var ty = eye[1] - center[1] var tz = eye[2] - center[2] var tl = len3(tx, ty, tz) if(tl < 1e-6) { return } tx /= tl ty /= tl tz /= tl var right = this.computedRight var rx = right[0] var ry = right[1] var rz = right[2] var ru = ux*rx + uy*ry + uz*rz rx -= ru * ux ry -= ru * uy rz -= ru * uz var rl = len3(rx, ry, rz) if(rl < 0.01) { rx = uy * tz - uz * ty ry = uz * tx - ux * tz rz = ux * ty - uy * tx rl = len3(rx, ry, rz) if(rl < 1e-6) { return } } rx /= rl ry /= rl rz /= rl this.up.set(t, ux, uy, uz) this.right.set(t, rx, ry, rz) this.center.set(t, center[0], center[1], center[2]) this.radius.set(t, Math.log(tl)) var fx = uy * rz - uz * ry var fy = uz * rx - ux * rz var fz = ux * ry - uy * rx var fl = len3(fx, fy, fz) fx /= fl fy /= fl fz /= fl var tu = ux*tx + uy*ty + uz*tz var tr = rx*tx + ry*ty + rz*tz var tf = fx*tx + fy*ty + fz*tz var phi = Math.asin(clamp1(tu)) var theta = Math.atan2(tf, tr) var angleState = this.angle._state var lastTheta = angleState[angleState.length-1] var lastPhi = angleState[angleState.length-2] lastTheta = lastTheta % (2.0 * Math.PI) var dp = Math.abs(lastTheta + 2.0 * Math.PI - theta) var d0 = Math.abs(lastTheta - theta) var dn = Math.abs(lastTheta - 2.0 * Math.PI - theta) if(dp < d0) { lastTheta += 2.0 * Math.PI } if(dn < d0) { lastTheta -= 2.0 * Math.PI } this.angle.jump(this.angle.lastT(), lastTheta, lastPhi) this.angle.set(t, theta, phi) } function createTurntableController(options) { options = options || {} var center = options.center || [0,0,0] var up = options.up || [0,1,0] var right = options.right || findOrthoPair(up) var radius = options.radius || 1.0 var theta = options.theta || 0.0 var phi = options.phi || 0.0 center = [].slice.call(center, 0, 3) up = [].slice.call(up, 0, 3) normalize3(up, up) right = [].slice.call(right, 0, 3) normalize3(right, right) if('eye' in options) { var eye = options.eye var toward = [ eye[0]-center[0], eye[1]-center[1], eye[2]-center[2] ] cross(right, toward, up) if(len3(right[0], right[1], right[2]) < 1e-6) { right = findOrthoPair(up) } else { normalize3(right, right) } radius = len3(toward[0], toward[1], toward[2]) var ut = dot3(up, toward) / radius var rt = dot3(right, toward) / radius phi = Math.acos(ut) theta = Math.acos(rt) } //Use logarithmic coordinates for radius radius = Math.log(radius) //Return the controller return new TurntableController( options.zoomMin, options.zoomMax, center, up, right, radius, theta, phi) }