orbiter
Version:
Orbiting quaternion camera
227 lines (187 loc) • 5.5 kB
JavaScript
'use strict'
module.exports = createOrbiter
var createController = require('orbit-camera-controller')
var mouseChange = require('mouse-change')
var mouseWheel = require('mouse-wheel')
var now = require('right-now')
function copyVec(a, b) {
a[0] = b[0]
a[1] = b[1]
a[2] = b[2]
}
function Orbiter(element, controller) {
this.element = element
this.controller = controller
this.matrix = new Array(16)
this.rotation = new Array(4)
this.up = new Array(3)
this.eye = new Array(3)
this.center = new Array(3)
this.distance = 0.0
this.delay = 40
this.inputEnabled = true
this.flipX = false
this.flipY = false
this.translateSpeed = 1
this.zoomSpeed = 1
this.rotateSpeed = 1
this.changed = true
}
var proto = Orbiter.prototype
proto.toJSON = function() {
var t = this.lastT()
controller.recalcMatrix(t)
return {
camera: this.controller.toJSON(),
delay: this.delay,
flipX: this.flipX,
flipY: this.flipY,
translateSpeed: this.translateSpeed,
zoomSpeed: this.zoomSpeed,
rotateSpeed: this.rotateSpeed
}
}
proto.fromJSON = function(options) {
if('camera' in options) {
this.controller.fromJSON(options.camera)
}
var self = this
function handleOption(prop) {
if(prop in options) {
self[prop] = options[prop]
}
}
handleOption('delay')
handleOption('inputEnabled')
handleOption('flipX')
handleOption('flipY')
handleOption('translateSpeed')
handleOption('zoomSpeed')
handleOption('rotateSpeed')
this.changed = true
}
proto.tick = function() {
var t = now()
var delay = this.delay
var controller = this.controller
controller.idle(t - 0.5 * delay)
controller.flush(t - (1000 * (1 + Math.min(delay,0))))
controller.recalcMatrix(t - delay)
var prevMat = this.matrix
var nextMat = controller.computedMatrix
var changed = this.changed
for(var i=0; i<16; ++i) {
changed = changed || (Math.abs(nextMat[i] - prevMat[i]) > 1e-5)
prevMat[i] = nextMat[i]
}
var prevRot = this.rotation
var nextRot = controller.computedRotation
for(var i=0; i<4; ++i) {
prevRot[i] = nextRot[i]
}
copyVec(this.eye, controller.computedEye)
copyVec(this.up, controller.computedUp)
copyVec(this.center, controller.computedCenter)
this.distance = Math.exp(controller.computedRadius[0])
//Clear change flag
this.changed = false
return changed
}
proto.lookAt = function(eye, center, up) {
this.controller.lookAt(this.lastT(), eye, center, up)
}
var scratchEye = [0,0,0]
var scratchCenter = [0,0,0]
var scratchUp = [0,1,0]
proto.box = function(lo, hi) {
var diam = 0
for(var i=0; i<3; ++i) {
eye[i] = center[i] = 0.5 * (lo[i]+hi[i])
diam = Math.max(diam, hi[i] - lo[i])
}
eye[1] = hi[1] + 2 * diam
this.controller.lookAt(now(), eye, center, up)
}
proto.setMatrix = function(mat) {
this.controller.setMatrix(this.lastT(), mat)
}
proto.setDistance = function(d) {
this.controller.setDistance(this.lastT(), d)
}
proto.setDistanceLimits = function(lo, hi) {
this.controller.setDistanceLimits(lo, hi)
}
proto.rotate = function(pitch, yaw, roll) {
this.controller.rotate(now(), pitch, yaw, roll)
}
proto.pan = function(dx, dy, dz) {
this.controller.pan(now(), dx, dy, dz)
}
proto.translate = function(dx, dy, dz) {
this.controller.translate(now(), dx, dy, dz)
}
proto.lastT = function() {
return this.controller.lastT()
}
function createOrbiter(element, options) {
//Create orbiter
var controller = createController(options)
var orbiter = new Orbiter(element, controller)
orbiter.fromJSON(options)
orbiter.tick()
orbiter.changed = true
//Hook input handlers
var lastX = 0, lastY = 0
mouseChange(element, function(buttons, x, y, mods) {
if(!orbiter.inputEnabled) {
return
}
var height = element.clientHeight
var width = element.clientWidth
var scale = 1.0 / height
var dx = scale * (x - lastX)
var dy = scale * (y - lastY)
var t = now()
var flipX = orbiter.flipX ? 1 : -1
var flipY = orbiter.flipY ? 1 : -1
var vrot = Math.PI * orbiter.rotateSpeed
var vpan = +orbiter.translateSpeed
var vzoom = +orbiter.zoomSpeed
var distance = orbiter.distance
if(buttons & 1) {
if(mods.shift) {
controller.rotate(t, 0, 0, -dx * drot)
} else {
controller.rotate(t, flipX * vrot * dx, -flipY * vrot * dy, 0)
}
} else if(buttons & 2) {
controller.pan(t, -vpan * dx * distance * height / width, vpan * dy * distance, 0)
} else if(buttons & 4) {
controller.pan(t, 0, 0, vzoom * dy * distance)
}
lastX = x
lastY = y
})
mouseWheel(element, function(dx, dy, dz) {
if(!orbiter.inputEnabled) {
return
}
var t = now()
var flipX = orbiter.flipX ? 1 : -1
var vrot = Math.PI * orbiter.rotateSpeed
var vzoom = +orbiter.zoomSpeed
var distance = orbiter.distance
if(Math.abs(dx) > Math.abs(dy)) {
controller.rotate(t, 0, 0, -dx * flipX * vrot / window.innerWidth)
} else {
controller.pan(t, 0, 0, vzoom * dy / window.innerHeight * distance)
}
}, true)
element.addEventListener('contextmenu', function(ev) {
if(orbiter.inputEnabled) {
ev.preventDefault()
return false
}
})
return orbiter
}