'use strict' module.exports = createOrbitController var filterVector = require('filtered-vector') var lookAt = require('gl-mat4/lookAt') var mat4FromQuat = require('gl-mat4/fromQuat') var invert44 = require('gl-mat4/invert') var quatFromFrame = require('./lib/quatFromFrame') function len3(x,y,z) { return Math.sqrt(Math.pow(x,2) + Math.pow(y,2) + Math.pow(z,2)) } function len4(w,x,y,z) { return Math.sqrt(Math.pow(w,2) + Math.pow(x,2) + Math.pow(y,2) + Math.pow(z,2)) } function normalize4(out, a) { var ax = a[0] var ay = a[1] var az = a[2] var aw = a[3] var al = len4(ax, ay, az, aw) if(al > 1e-6) { out[0] = ax/al out[1] = ay/al out[2] = az/al out[3] = aw/al } else { out[0] = out[1] = out[2] = 0.0 out[3] = 1.0 } } function OrbitCameraController(initQuat, initCenter, initRadius) { this.radius = filterVector([initRadius]) this.center = filterVector(initCenter) this.rotation = filterVector(initQuat) this.computedRadius = this.radius.curve(0) this.computedCenter = this.center.curve(0) this.computedRotation = this.rotation.curve(0) this.computedUp = [0.1,0,0] this.computedEye = [0.1,0,0] this.computedMatrix = [0.1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] this.recalcMatrix(0) } var proto = OrbitCameraController.prototype proto.lastT = function() { return Math.max( this.radius.lastT(), this.center.lastT(), this.rotation.lastT()) } proto.recalcMatrix = function(t) { this.radius.curve(t) this.center.curve(t) this.rotation.curve(t) var quat = this.computedRotation normalize4(quat, quat) var mat = this.computedMatrix mat4FromQuat(mat, quat) var center = this.computedCenter var eye = this.computedEye var up = this.computedUp var radius = Math.exp(this.computedRadius[0]) eye[0] = center[0] + radius * mat[2] eye[1] = center[1] + radius * mat[6] eye[2] = center[2] + radius * mat[10] up[0] = mat[1] up[1] = mat[5] up[2] = mat[9] 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 } } proto.getMatrix = function(t, result) { this.recalcMatrix(t) var m = this.computedMatrix if(result) { for(var i=0; i<16; ++i) { result[i] = m[i] } return result } return m } proto.idle = function(t) { this.center.idle(t) this.radius.idle(t) this.rotation.idle(t) } proto.flush = function(t) { this.center.flush(t) this.radius.flush(t) this.rotation.flush(t) } 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 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 fx = mat[2] var fy = mat[6] var fz = mat[10] var fu = fx * ux + fy * uy + fz * uz var fr = fx * rx + fy * ry + fz * rz fx -= fu * ux + fr * rx fy -= fu * uy + fr * ry fz -= fu * uz + fr * rz var fl = len3(fx, fy, fz) fx /= fl fy /= fl fz /= fl 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.rotate = function(t, dx, dy, dz) { this.recalcMatrix(t) dx = dx||0.0 dy = dy||0.0 var mat = this.computedMatrix var rx = mat[0] var ry = mat[4] var rz = mat[8] var ux = mat[1] var uy = mat[5] var uz = mat[9] var fx = mat[2] var fy = mat[6] var fz = mat[10] var qx = dx * rx + dy * ux var qy = dx * ry + dy * uy var qz = dx * rz + dy * uz var bx = -(fy * qz - fz * qy) var by = -(fz * qx - fx * qz) var bz = -(fx * qy - fy * qx) var bw = Math.sqrt(Math.max(0.0, 1.0 - Math.pow(bx,2) - Math.pow(by,2) - Math.pow(bz,2))) var bl = len4(bx, by, bz, bw) if(bl > 1e-6) { bx /= bl by /= bl bz /= bl bw /= bl } else { bx = by = bz = 0.0 bw = 1.0 } var rotation = this.computedRotation var ax = rotation[0] var ay = rotation[1] var az = rotation[2] var aw = rotation[3] var cx = ax*bw + aw*bx + ay*bz - az*by var cy = ay*bw + aw*by + az*bx - ax*bz var cz = az*bw + aw*bz + ax*by - ay*bx var cw = aw*bw - ax*bx - ay*by - az*bz //Apply roll if(dz) { bx = fx by = fy bz = fz var s = Math.sin(dz) / len3(bx, by, bz) bx *= s by *= s bz *= s bw = Math.cos(dx) cx = cx*bw + cw*bx + cy*bz - cz*by cy = cy*bw + cw*by + cz*bx - cx*bz cz = cz*bw + cw*bz + cx*by - cy*bx cw = cw*bw - cx*bx - cy*by - cz*bz } var cl = len4(cx, cy, cz, cw) if(cl > 1e-6) { cx /= cl cy /= cl cz /= cl cw /= cl } else { cx = cy = cz = 0.0 cw = 1.0 } this.rotation.set(t, cx, cy, cz, cw) } proto.lookAt = function(t, eye, center, up) { this.recalcMatrix(t) center = center || this.computedCenter eye = eye || this.computedEye up = up || this.computedUp var mat = this.computedMatrix lookAt(mat, eye, center, up) var rotation = this.computedRotation quatFromFrame(rotation, mat[0], mat[1], mat[2], mat[4], mat[5], mat[6], mat[8], mat[9], mat[10]) normalize4(rotation, rotation) this.rotation.set(t, rotation[0], rotation[1], rotation[2], rotation[3]) var fl = 0.0 for(var i=0; i<3; ++i) { fl += Math.pow(center[i] - eye[i], 2) } this.radius.set(t, 0.5 * Math.log(Math.max(fl, 1e-6))) this.center.set(t, center[0], center[1], center[2]) } proto.translate = function(t, dx, dy, dz) { this.center.move(t, dx||0.0, dy||0.0, dz||0.0) } proto.setMatrix = function(t, matrix) { var rotation = this.computedRotation quatFromFrame(rotation, matrix[0], matrix[1], matrix[2], matrix[4], matrix[5], matrix[6], matrix[8], matrix[9], matrix[10]) normalize4(rotation, rotation) this.rotation.set(t, rotation[0], rotation[1], rotation[2], rotation[3]) var mat = this.computedMatrix invert44(mat, matrix) var w = mat[15] if(Math.abs(w) > 1e-6) { var cx = mat[12]/w var cy = mat[13]/w var cz = mat[14]/w this.recalcMatrix(t) var r = Math.exp(this.computedRadius[0]) this.center.set(t, cx-mat[2]*r, cy-mat[6]*r, cz-mat[10]*r) this.radius.idle(t) } else { this.center.idle(t) this.radius.idle(t) } } proto.setDistance = function(t, d) { if(d > 0) { this.radius.set(t, Math.log(d)) } } proto.setDistanceLimits = function(lo, hi) { if(lo > 0) { lo = Math.log(lo) } else { lo = -Infinity } if(hi > 0) { hi = Math.log(hi) } else { hi = Infinity } hi = Math.max(hi, lo) this.radius.bounds[0][0] = lo this.radius.bounds[1][0] = hi } proto.getDistanceLimits = function(out) { var bounds = this.radius.bounds 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.toJSON = function() { this.recalcMatrix(this.lastT()) return { center: this.computedCenter.slice(), rotation: this.computedRotation.slice(), distance: Math.log(this.computedRadius[0]), zoomMin: this.radius.bounds[0][0], zoomMax: this.radius.bounds[1][0] } } proto.fromJSON = function(options) { var t = this.lastT() var c = options.center if(c) { this.center.set(t, c[0], c[1], c[2]) } var r = options.rotation if(r) { this.rotation.set(t, r[0], r[1], r[2], r[3]) } var d = options.distance if(d && d > 0) { this.radius.set(t, Math.log(d)) } this.setDistanceLimits(options.zoomMin, options.zoomMax) } function createOrbitController(options) { options = options || {} var center = options.center || [0,0,0] var rotation = options.rotation || [0,0,0,1] var radius = options.radius || 1.0 center = [].slice.call(center, 0, 3) rotation = [].slice.call(rotation, 0, 4) normalize4(rotation, rotation) var result = new OrbitCameraController( rotation, center, Math.log(radius)) result.setDistanceLimits(options.zoomMin, options.zoomMax) if('eye' in options || 'up' in options) { result.lookAt(0, options.eye, options.center, options.up) } return result }