summaryrefslogtreecommitdiff
path: root/client/assets/javascripts/mx/extensions
diff options
context:
space:
mode:
Diffstat (limited to 'client/assets/javascripts/mx/extensions')
-rw-r--r--client/assets/javascripts/mx/extensions/mx.movements.js285
-rw-r--r--client/assets/javascripts/mx/extensions/mx.rotationControl.js265
-rw-r--r--client/assets/javascripts/mx/extensions/mx.scene.js165
-rw-r--r--client/assets/javascripts/mx/extensions/mx.scrubber.js191
4 files changed, 906 insertions, 0 deletions
diff --git a/client/assets/javascripts/mx/extensions/mx.movements.js b/client/assets/javascripts/mx/extensions/mx.movements.js
new file mode 100644
index 0000000..074ca9a
--- /dev/null
+++ b/client/assets/javascripts/mx/extensions/mx.movements.js
@@ -0,0 +1,285 @@
+
+
+MX.Movements = function (cam, viewHeight) {
+
+ var moveForward,
+ moveLeft,
+ moveBackward,
+ moveRight,
+ moveUp,
+ moveDown,
+ turnLeft,
+ turnRight,
+ turnUp,
+ turnDown,
+ jumping = false,
+ creeping = false,
+ locked = false,
+ gravity = false,
+ rotationX_min = PI/-2,
+ rotationX_max = PI/2
+
+ var v = 12,
+ vr = Math.PI * 0.012,
+ jumpV = 23,
+ vx = vy = vz = 0,
+ creepFactor = 0.3
+
+ var DEFAULT_SCALE = scale = 1.0
+
+ var pos = { x: 0, y: 0, z: 0, rotationX: 0, rotationY: 0 }
+
+ return {
+
+ init: function () {
+
+ document.addEventListener('keydown', function (e) {
+ // console.log(e.keyCode)
+ if (locked) return;
+ switch ( e.keyCode ) {
+
+ case 16: // shift
+ creeping = true
+ break
+
+ case 38: // up
+ case 87: // w
+ moveForward = true
+ break
+
+ case 37: // left
+ case 65: // a
+ moveLeft = true
+ break
+
+ case 40: // down
+ case 83: // s
+ moveBackward = true
+ break
+
+ case 39: // right
+ case 68: // d
+ moveRight = true
+ break
+
+ case 81: // q
+ turnLeft = true
+ break
+
+ case 69: // e
+ turnRight = true
+ break
+
+ case 82: // r
+ turnUp = true
+ break
+
+ case 70: // f
+ turnDown = true
+ break
+
+ case 32: // space
+ if (gravity) {
+ jumping = true
+ vy = abs(vy) + jumpV * scale
+ if (e.shiftKey) {
+ vy *= -1
+ }
+ }
+ else {
+ if (e.shiftKey) {
+ moveDown = true
+ }
+ else {
+ moveUp = true
+ }
+ }
+ break
+
+ case 27: // esc
+ map.toggle()
+ break
+ }
+ })
+
+ document.addEventListener('keyup', function (e) {
+ if (locked) return;
+ switch ( e.keyCode ) {
+
+ case 16: // shift
+ creeping = false
+ break
+
+ case 38: // up
+ case 87: // w
+ moveForward = false
+ break
+
+ case 37: // left
+ case 65: // a
+ moveLeft = false
+ break
+
+ case 40: // down
+ case 83: // s
+ moveBackward = false
+ break
+
+ case 39: // right
+ case 68: // d
+ moveRight = false
+ break
+
+ case 81: // q
+ turnLeft = false
+ break
+
+ case 69: // e
+ turnRight = false
+ break
+
+ case 82: // r
+ turnUp = false
+ break
+
+ case 70: // f
+ turnDown = false
+ break
+
+ case 32: // space
+ moveUp = moveDown = false
+ break
+
+ case 48: // 0
+ cam.rotationX = 0
+ cam.rotationY = 0
+ cam.x = 0
+ cam.y = viewHeight
+ cam.z = 0
+ break
+ }
+ })
+
+ var mouseX, mouseY, dx, dy, rotX, rotY, dragging = false
+ document.addEventListener('mousedown', function (e) {
+ if (locked) return;
+ mouseX = e.pageX
+ mouseY = e.pageY
+ rotX = cam.rotationX
+ rotY = cam.rotationY
+ dragging = true
+ })
+
+ document.addEventListener('mousemove', function (e) {
+ if (locked || ! dragging || app.dragging) return
+ var dx = (e.pageX - mouseX) / window.innerWidth * Math.PI/3
+ var dy = (e.pageY - mouseY) / window.innerHeight * Math.PI/3
+ cam.rotationY = rotY + dx
+ cam.rotationX = clamp( rotX - dy, rotationX_min, rotationX_max )
+ })
+
+ document.addEventListener('mouseup', function (e) {
+ app.dragging = dragging = false
+ })
+
+ window.addEventListener('blur', reset)
+ window.addEventListener('focus', reset)
+ function reset(){
+ moveForward = moveLeft = moveBackward = moveRight = moveUp = moveDown = turnLeft = turnRight = jumping = dragging = creeping = false
+ }
+
+ },
+
+ update: function () {
+
+ if (locked) return;
+
+ var ry = cam.rotationY
+ var s = creeping ? scale * creepFactor : scale
+ var vrrrr = creeping ? vr * creepFactor * 5 : vr
+ var moving = moveForward || moveBackward || moveRight || moveLeft || moveUp || moveDown || turnLeft || turnRight || turnUp || turnDown
+ vx = vz = 0
+
+ pos.x = cam.x
+ pos.z = cam.z
+
+ if (moving) {
+
+ if (moveForward) {
+ vx += v * Math.cos(ry + Math.PI / 2) * s
+ vz += v * Math.sin(ry + Math.PI / 2) * s
+ }
+ if (moveBackward) {
+ vx -= v * Math.cos(ry + Math.PI / 2) * s
+ vz -= v * Math.sin(ry + Math.PI / 2) * s
+ }
+ if (moveLeft) {
+ vx -= v * Math.cos(ry) * s
+ vz -= v * Math.sin(ry) * s
+ }
+ if (moveRight) {
+ vx += v * Math.cos(ry) * s
+ vz += v * Math.sin(ry) * s
+ }
+ if (moveUp) {
+ pos.y += v * scale
+ }
+ if (moveDown) {
+ pos.y -= v * scale
+ }
+
+ if (turnUp) {
+ cam.rotationX = clamp( cam.rotationX - vrrrr*s, rotationX_min, rotationX_max)
+ }
+ if (turnDown) {
+ cam.rotationX = clamp( cam.rotationX + vrrrr*s, rotationX_min, rotationX_max)
+ }
+ if (turnLeft) {
+ cam.rotationY += vrrrr*s
+ }
+ if (turnRight) {
+ cam.rotationY -= vrrrr*s
+ }
+
+ pos.x += vx
+ pos.z += vz
+ }
+
+ if (gravity) {
+ vy -= 1 * scale
+
+ pos.y += vy
+
+ if (vy) {
+ moving = true
+ }
+ if (pos.y <= viewHeight) {
+ pos.y = viewHeight
+ vy = 0
+ jumping = false
+ vz = vz || 1
+ }
+
+ var ceiling = (Rooms.mover.room ? Rooms.mover.room.height : 5000)
+
+ if (pos.y >= ceiling) {
+ vy = 0
+ pos.y = ceiling
+ vz = vz || 1
+ }
+ }
+
+ if (moving) {
+ app.tube("move", pos)
+ }
+ },
+
+ lock: function(){ locked = true },
+ unlock: function(){ locked = false },
+ scale: function(n){ if (n) scale = n; return scale },
+ resetScale: function(n){ scale = DEFAULT_SCALE },
+ gravity: function(g){ return typeof g == "boolean" ? gravity = g : gravity },
+ velocity: function(n){ v = clamp(n, 1, 50) },
+ jumpVelocity: function(n){ jumpV = clamp(n, 1, 50) },
+ }
+}
diff --git a/client/assets/javascripts/mx/extensions/mx.rotationControl.js b/client/assets/javascripts/mx/extensions/mx.rotationControl.js
new file mode 100644
index 0000000..9adb627
--- /dev/null
+++ b/client/assets/javascripts/mx/extensions/mx.rotationControl.js
@@ -0,0 +1,265 @@
+// Usage:
+//
+// var control = new MX.RotationControl()
+// control.init( object{MX.Object3D} [, listener{HTMLElement}] )
+//
+// In animation loop:
+//
+// control.update()
+//
+// The above code will register handler functions on `listener`
+// and will be updating `object`s rotationX and rotationY
+// If no `listener` is provided, will default to `object`s el.
+
+MX.RotationControl = function () {
+
+ var object,
+ locked = false
+
+ var down = false,
+ active = false,
+ lastX,
+ lastY
+
+ var pointerLockPrefix =
+ 'pointerLockElement' in document ? '' :
+ 'mozPointerLockElement' in document ? 'moz' :
+ 'webkitPointerLockElement' in document ? 'webkit' : null,
+ hasPointerLock = !(pointerLockPrefix === null)
+ pointerLockEnabled = false
+
+ var pub = {
+
+ sensitivity : .5,
+ ease : 10,
+ drag : true,
+
+ inverseX : false,
+ inverseY : false,
+
+ disableX : false,
+ disableY : false,
+
+ rotationX : 0,
+ rotationY : 0,
+
+ upperBoundX : undefined,
+ lowerBoundX : undefined,
+
+ upperBoundY : undefined,
+ lowerBoundY : undefined,
+
+ usePreset: function (name) {
+ var ops = presets[name]
+ if (ops) {
+ if (currentPreset && presets[currentPreset].teardown) {
+ presets[currentPreset].teardown()
+ }
+ for (var op in ops) {
+ if (op !== 'setup' && op !== 'teardown') {
+ pub[op] = ops[op]
+ }
+ }
+ if (op.setup) ops.setup()
+ }
+ }
+ }
+
+ var currentPreset
+ var presets = {
+ firstPerson: {
+ drag: false,
+ ease: 2,
+ sensitivity: .18,
+ inverseX: true,
+ inverseY: true,
+ upperBoundX: MX.rotationUnit === 'deg' ? 90 : Math.PI / 2,
+ lowerBoundX: MX.rotationUnit === 'deg' ? -90 : -Math.PI / 2
+ },
+ skybox: {
+ sensitivity: .18,
+ inverseX: true,
+ inverseY: true,
+ upperBoundX: MX.rotationUnit === 'deg' ? 90 : Math.PI / 2,
+ lowerBoundX: MX.rotationUnit === 'deg' ? -90 : -Math.PI / 2
+ }
+ }
+
+ function init (obj, lis) {
+ if (active) return
+
+ object = obj
+ pub.rotationX = object.rotationX
+ pub.rotationY = object.rotationY
+
+ if (lis instanceof HTMLElement) {
+ listener = lis
+ } else if (lis instanceof MX.Object3D) {
+ listener = lis.el
+ } else {
+ listener = window.document
+ }
+
+ listener.addEventListener('mousedown', onDown)
+ listener.addEventListener('mousemove', onMove)
+ listener.addEventListener('mouseup', onUp)
+ listener.addEventListener('touchstart', onDown)
+ listener.addEventListener('touchmove', onMove)
+ listener.addEventListener('touchend', onUp)
+
+ active = true
+ }
+
+ function changeObject (obj) {
+ object = obj
+ pub.rotationX = object.rotationX
+ pub.rotationY = object.rotationY
+ }
+
+ function changeListener (lis) {
+ remove()
+ active = false
+ init(object, lis)
+ if (pointerLockEnabled) {
+ initPointerLock()
+ }
+ }
+
+ function remove () {
+ if (!active) return
+ listener.removeEventListener('mousedown', onDown)
+ listener.removeEventListener('mousemove', onMove)
+ listener.removeEventListener('mouseup', onUp)
+ listener.removeEventListener('touchstart', onDown)
+ listener.removeEventListener('touchmove', onMove)
+ listener.removeEventListener('touchend', onUp)
+
+ if (hasPointerLock) {
+ document.removeEventListener(pointerLockPrefix + 'pointerlockchange', onPointerLockChange)
+ document.removeEventListener('mousemove', onPointerLockMove)
+ document.body[pointerLockPrefix + (pointerLockPrefix ? 'E' : 'e') + 'xitPointerLock']()
+ }
+ active = false
+ }
+
+ function onDown (e) {
+ e = normalizeEvent(e)
+ if (!e) return
+ down = true
+ lastX = e.pageX
+ lastY = e.pageY
+ }
+
+ function onMove (e) {
+ if (e.type = 'touchmove') {
+ e.preventDefault()
+ }
+ if (pub.drag && !down) return
+ e = normalizeEvent(e)
+ if (!e) return
+ lastX = lastX || e.pageX
+ lastY = lastY || e.pageY
+ var dx = e.pageX - lastX,
+ dy = e.pageY - lastY
+ lastX = e.pageX
+ lastY = e.pageY
+ updateTarget(dx, dy)
+ }
+
+ function onUp () {
+ down = false
+ }
+
+ function initPointerLock () {
+
+ if (pointerLockEnabled) return
+
+ document.addEventListener(pointerLockPrefix + 'pointerlockchange', onPointerLockChange)
+ document.addEventListener('mousemove', onPointerLockMove)
+
+ document.body[pointerLockPrefix + (pointerLockPrefix ? 'R' : 'r') + 'equestPointerLock']()
+ }
+
+ function onPointerLockChange () {
+ var el = document.body
+ if (document[pointerLockPrefix + (pointerLockPrefix ? 'P' : 'p') + 'ointerLockElement'] === el) {
+ pointerLockEnabled = true
+ } else {
+ pointerLockEnabled = false
+ }
+ }
+
+ function onPointerLockMove (e) {
+ if (!pointerLockEnabled) return
+ var dx = e[pointerLockPrefix + (pointerLockPrefix ? 'M' : 'm') + 'ovementX'],
+ dy = e[pointerLockPrefix + (pointerLockPrefix ? 'M' : 'm') + 'ovementY']
+ updateTarget(dx, dy)
+ }
+
+ function normalizeEvent (e) {
+ if (e.touches) {
+ return e.touches.length > 1 ? false : e.touches[0]
+ } else {
+ return e
+ }
+ }
+
+ function updateTarget (dx, dy) {
+ if (pub.inverseX) dx = -dx
+ if (pub.inverseY) dy = -dy
+ if (MX.rotationUnit !== 'deg') {
+ dx = MX.toRad(dx)
+ dy = MX.toRad(dy)
+ }
+
+ if (!pub.disableX) {
+ pub.rotationX -= dy * pub.sensitivity
+ if (pub.upperBoundX) pub.rotationX = Math.min(pub.rotationX, pub.upperBoundX)
+ if (pub.lowerBoundX) pub.rotationX = Math.max(pub.rotationX, pub.lowerBoundX)
+ }
+
+ if (!pub.disableY) {
+ pub.rotationY += dx * pub.sensitivity
+ if (pub.upperBoundY) pub.rotationY = Math.min(pub.rotationY, pub.upperBoundY)
+ if (pub.lowerBoundY) pub.rotationY = Math.max(pub.rotationY, pub.lowerBoundY)
+ }
+ }
+
+ function update () {
+ if (!object || locked) return
+ var dx = pub.rotationX - object.rotationX,
+ dy = pub.rotationY - object.rotationY
+ if (Math.abs(dx) < 0.0001) {
+ object.rotationX = pub.rotationX
+ } else {
+ object.rotationX += dx / pub.ease
+ }
+ if (Math.abs(dy) < 0.0001) {
+ object.rotationY = pub.rotationY
+ } else {
+ object.rotationY += dy / pub.ease
+ }
+ }
+
+ function lock () {
+ locked = true
+ }
+
+ function unlock () {
+ pub.rotationX = object.rotationX
+ pub.rotationY = object.rotationY
+ locked = false
+ }
+
+ pub.init = init
+ pub.remove = remove
+ pub.update = update
+ pub.lock = lock
+ pub.unlock = unlock
+ pub.initPointerLock = initPointerLock
+ pub.changeObject = changeObject
+ pub.changeListener = changeListener
+
+ return pub
+
+} \ No newline at end of file
diff --git a/client/assets/javascripts/mx/extensions/mx.scene.js b/client/assets/javascripts/mx/extensions/mx.scene.js
new file mode 100644
index 0000000..8f11fb0
--- /dev/null
+++ b/client/assets/javascripts/mx/extensions/mx.scene.js
@@ -0,0 +1,165 @@
+// NOTE
+//
+// This is not a fully functional 3d scene as you might expect.
+// The camera can only do pitch (rotationX) and yaw (rotationY), but no roll (rotationZ)
+// because I haven't implemented alternative euler orders or quaternions.
+//
+// For serious 3D scenes with more functionalities you should use
+// THREE.js with CSS3D Renderer.
+
+MX.Camera = MX.Object3D.extend({
+
+ init: function(){
+ this.el = null
+ this.type = "Camera"
+ },
+
+ move: function(s){
+ for (var i in s) {
+ this[i] = s[i]
+ }
+ },
+
+ toString: function(){
+ var params = "x y z rotationX rotationY".split(" ")
+ return this.__toString(params, "scene.camera.move")
+ },
+
+ getCameraEuler: function (target) {
+ var dx = target.x - this.x,
+ dy = target.y - this.y,
+ dz = target.z - this.z
+ r = {}
+ r.y = Math.atan2(-dx, dz)
+ r.x = Math.atan2(-dy, Math.sqrt(dx*dx + dz*dz))
+ r.z = 0
+ if (MX.rotationUnit === 'deg') {
+ r.x = MX.toDeg(r.x)
+ r.y = MX.toDeg(r.y)
+ }
+ return r
+ }
+
+})
+
+MX.Scene = (function () {
+
+ var add = MX.Object3D.prototype.add,
+ remove = MX.Object3D.prototype.remove
+
+ function Scene () {
+
+ this.el = document.createElement('div')
+ this.el.classList.add('mx-scene')
+
+ var s = this.el.style
+
+ s[MX.transformProp] = 'preserve-3d'
+
+ s.webkitPerspectiveOrigin = '50% 50%'
+ s.mozPerspectiveOrigin = '50% 50%'
+ s.perspectiveOrigin = '50% 50%'
+
+ s.webkitUserSelect = 'none'
+ s.mozUserSelect = 'none'
+ s.userSelect = 'none'
+
+ s.overflow = 'hidden'
+
+ this.inner = new MX.Object3D().addTo(this.el)
+ this.inner.el.style.width = '0'
+ this.inner.el.style.height = '0'
+
+ var self = this
+ var width, height, perspective
+
+ Object.defineProperty(this, 'width', {
+ get: function () {
+ return width
+ },
+ set: function (val) {
+ width = val
+ self.el.style.width = val + 'px'
+ }
+ })
+
+ Object.defineProperty(this, 'height', {
+ get: function () {
+ return height
+ },
+ set: function (val) {
+ height = val
+ self.el.style.height = val + 'px'
+ }
+ })
+
+ Object.defineProperty(this, 'perspective', {
+ get: function () {
+ return perspective
+ },
+ set: function (val) {
+ perspective = val
+ self.el.style[MX.perspectiveProp] = val + 'px'
+ self.inner.z = -val - self.camera.z
+ self.inner.rotationOrigin.z = -val
+ }
+ })
+
+ var cam = this.camera = new MX.Camera()
+
+ this.inner.rotationOrigin = { x:0, y:0, z:0 }
+
+ this.perspective = 0
+ }
+
+ Scene.prototype = {
+
+ constructor: Scene,
+
+ add: function () {
+ add.apply(this.inner, arguments)
+ return this
+ },
+
+ remove: function () {
+ remove.apply(this.inner, arguments)
+ return this
+ },
+
+ addTo: function (target) {
+ if (typeof target === 'string') {
+ target = document.querySelector(target)
+ }
+ if (target instanceof HTMLElement && target.appendChild) {
+ target.appendChild(this.el)
+ } else {
+ console.warn('You can only add a Scene to an HTML element.')
+ }
+ return this
+ },
+
+ update: function () {
+ // update inner based on camera
+
+ var i = this.inner,
+ c = this.camera
+
+ c.update()
+
+ i.z = -this.perspective - c.z
+ i.x = -c.x
+ i.y = -c.y
+
+ i.rotationX = -c.rotationX
+ i.rotationY = -c.rotationY
+ //i.rotationZ = -c.rotationZ
+
+ i.update()
+ return this
+ },
+
+ }
+
+ return Scene
+
+})() \ No newline at end of file
diff --git a/client/assets/javascripts/mx/extensions/mx.scrubber.js b/client/assets/javascripts/mx/extensions/mx.scrubber.js
new file mode 100644
index 0000000..54612f2
--- /dev/null
+++ b/client/assets/javascripts/mx/extensions/mx.scrubber.js
@@ -0,0 +1,191 @@
+/*
+ Use the scrollwheel to tween between different points and orientations
+
+ scrubber = new MX.Scrubber(cam, [
+ {
+ position: [0, viewHeight, -1000],
+ rotation: [0, 0]
+ },
+ {
+ position: [0, 1000, 1000],
+ rotation: [0, Math.PI]
+ },
+ {
+ position: [0, viewHeight, -1000],
+ rotation: [0, 2*Math.PI]
+ },
+ {
+ position: [0, viewHeight, -2000],
+ rotation: [0, 0]
+ }
+ ])
+
+ // in your animate function:
+ scrubber.update()
+
+*/
+
+MX.Scrubber = function (obj, points) {
+
+ obj = obj || {}
+ points = points || {}
+
+ var reversible = true, loop = false;
+
+ var total = points.length * 100,
+ distance = 0
+ destination = 0,
+ last_index = -1,
+ last_name = null,
+ locked = false,
+ point_count = points.length + (loop+0)
+
+ var avg_speed = scroll_avg_speed = 5,
+ click_avg_speed = 20,
+ webkit_ratio = 0.02
+
+ if (points[0].position) {
+ points.forEach(function(p){
+ p.x = p.position[0]
+ p.y = p.position[1]
+ p.z = p.position[2]
+ p.rotationX = p.rotation[0]
+ p.rotationY = p.rotation[1]
+ })
+ }
+
+ document.addEventListener('touchstart', next, false);
+ document.addEventListener('mousedown', next, false);
+ document.addEventListener('mousewheel', onDocumentMouseWheel, false);
+ document.addEventListener('DOMMouseScroll', onDocumentMouseWheel, false);
+ function onDocumentMouseWheel (e) {
+
+ if (locked) return
+
+ var delta = 0;
+
+ // WebKit
+ if ( event.wheelDeltaY ) {
+ delta -= event.wheelDeltaY * webkit_ratio
+ }
+ // Opera / Explorer 9
+ else if ( event.wheelDelta ) {
+ delta -= event.wheelDelta * webkit_ratio
+ }
+ // Firefox
+ else if ( event.detail ) {
+ delta += event.detail * 2
+ }
+ if (! reversible && delta < 0) return;
+
+ if (destination < total-100 || delta < 0) {
+ e.preventDefault()
+ }
+ else {
+ return
+ }
+
+ destination += delta
+
+ avg_speed = scroll_avg_speed
+ }
+
+ function add_point(point){
+ if (point.type == "Camera") {
+ point = {
+ position: [ point.x, point.y, point.z ],
+ rotation: [ point.rotationX, point.rotationY ],
+ callback: noop
+ }
+ }
+ points.push(point)
+ total = points.length * 100
+ }
+
+ function reset(){
+ distance = destination = 0
+ last_index = -1
+ last_name = null
+ }
+
+ function next(){
+ destination = ~~(destination/100) * 100
+ destination += 100
+ avg_speed = click_avg_speed
+ }
+
+ function update(){
+ if (locked) return
+
+ if (destination > total-100) destination = total-100
+
+ distance = avg(distance, destination, avg_speed)
+ var ratio = distance / total
+
+ if (! loop) {
+ if (ratio < 0) {
+ destination = 0
+ ratio = 0
+ }
+ else if (ratio > 1) {
+ destination = total
+ ratio = 1
+ }
+ }
+
+ var diff = ratio * point_count
+ var step = (distance % 100) / 100
+ var src = ~~clamp(diff, 0, point_count-1)
+ var halfway = ~~clamp(diff + 0.5, 0, point_count-1)
+ var dest = ~~clamp(diff + 1, 0, point_count-1)
+
+ if (halfway != last_index) {
+ last_index = halfway
+ if (points[last_index].name != last_name) {
+ last_name = points[last_index].name
+ }
+ $("#info .active").removeClass("active")
+ $("#info div[data-name='" + last_name + "']").addClass("active")
+ points[halfway].callback && points[halfway].callback()
+ }
+
+ var ry0 = points[src].rotationY
+ var ry1 = points[dest].rotationY
+ if (abs(ry0 - ry1) == TWO_PI) {
+ ry0 = ry1
+ }
+
+ obj.x = lerp(step, points[src].x, points[dest].x)
+ obj.y = lerp(step, points[src].y, points[dest].y)
+ obj.z = lerp(step, points[src].z, points[dest].z)
+ obj.rotationX = lerp(step, points[src].rotationX, points[dest].rotationX)
+ obj.rotationY = lerp(step, ry0, ry1)
+ if (obj.rotationY > PI) { obj.rotationY -= TWO_PI }
+ }
+
+ var scrubber = {
+ init: function(){
+ app && app.movements && app.movements.lock()
+ },
+ lock: function(){
+ app && app.movements && app.movements.unlock()
+ locked = true
+ },
+ unlock: function(){
+ app && app.movements && app.movements.lock()
+ locked = false
+ },
+ step: function(n){
+ distance = destination = n * 100
+ },
+ add_point: add_point,
+ reset: reset,
+ next: next,
+ update: update,
+
+ obj: obj,
+ points: points
+ }
+
+ return scrubber;
+}