Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…

/** | |
* @author qiao / https://github.com/qiao | |
* @author mrdoob / http://mrdoob.com | |
* @author alteredq / http://alteredqualia.com/ | |
* @author WestLangley / http://github.com/WestLangley | |
* @author erich666 / http://erichaines.com | |
*/ | |
/*global THREE, console */ | |
// This set of controls performs orbiting, dollying (zooming), and panning. It maintains | |
// the "up" direction as +Y, unlike the TrackballControls. Touch on tablet and phones is | |
// supported. | |
// | |
// Orbit - left mouse / touch: one finger move | |
// Zoom - middle mouse, or mousewheel / touch: two finger spread or squish | |
// Pan - right mouse, or arrow keys / touch: three finter swipe | |
THREE.OrbitControls = function ( object, domElement ) { | |
this.object = object; | |
this.domElement = ( domElement !== undefined ) ? domElement : document; | |
// API | |
// Set to false to disable this control | |
this.enabled = true; | |
// "target" sets the location of focus, where the control orbits around | |
// and where it pans with respect to. | |
this.target = new THREE.Vector3(); | |
// center is old, deprecated; use "target" instead | |
this.center = this.target; | |
// This option actually enables dollying in and out; left as "zoom" for | |
// backwards compatibility | |
this.noZoom = false; | |
this.zoomSpeed = 1.0; | |
// Limits to how far you can dolly in and out | |
this.minDistance = 0; | |
this.maxDistance = Infinity; | |
// Set to true to disable this control | |
this.noRotate = false; | |
this.rotateSpeed = 1.0; | |
// Set to true to disable this control | |
this.noPan = false; | |
this.keyPanSpeed = 7.0; // pixels moved per arrow key push | |
// Set to true to automatically rotate around the target | |
this.autoRotate = false; | |
this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 | |
// How far you can orbit vertically, upper and lower limits. | |
// Range is 0 to Math.PI radians. | |
this.minPolarAngle = 0; // radians | |
this.maxPolarAngle = Math.PI; // radians | |
// How far you can orbit horizontally, upper and lower limits. | |
// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. | |
this.minAzimuthAngle = - Infinity; // radians | |
this.maxAzimuthAngle = Infinity; // radians | |
// Set to true to disable use of the keys | |
this.noKeys = false; | |
// The four arrow keys | |
this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; | |
// Mouse buttons | |
this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; | |
//////////// | |
// internals | |
var scope = this; | |
var EPS = 0.000001; | |
var rotateStart = new THREE.Vector2(); | |
var rotateEnd = new THREE.Vector2(); | |
var rotateDelta = new THREE.Vector2(); | |
var panStart = new THREE.Vector2(); | |
var panEnd = new THREE.Vector2(); | |
var panDelta = new THREE.Vector2(); | |
var panOffset = new THREE.Vector3(); | |
var offset = new THREE.Vector3(); | |
var dollyStart = new THREE.Vector2(); | |
var dollyEnd = new THREE.Vector2(); | |
var dollyDelta = new THREE.Vector2(); | |
var theta; | |
var phi; | |
var phiDelta = 0; | |
var thetaDelta = 0; | |
var scale = 1; | |
var pan = new THREE.Vector3(); | |
var lastPosition = new THREE.Vector3(); | |
var lastQuaternion = new THREE.Quaternion(); | |
var STATE = { NONE : -1, ROTATE : 0, DOLLY : 1, PAN : 2, TOUCH_ROTATE : 3, TOUCH_DOLLY : 4, TOUCH_PAN : 5 }; | |
var state = STATE.NONE; | |
// for reset | |
this.target0 = this.target.clone(); | |
this.position0 = this.object.position.clone(); | |
// so camera.up is the orbit axis | |
var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); | |
var quatInverse = quat.clone().inverse(); | |
// events | |
var changeEvent = { type: 'change' }; | |
var startEvent = { type: 'start'}; | |
var endEvent = { type: 'end'}; | |
this.rotateLeft = function ( angle ) { | |
if ( angle === undefined ) { | |
angle = getAutoRotationAngle(); | |
} | |
thetaDelta -= angle; | |
}; | |
this.rotateUp = function ( angle ) { | |
if ( angle === undefined ) { | |
angle = getAutoRotationAngle(); | |
} | |
phiDelta -= angle; | |
}; | |
// pass in distance in world space to move left | |
this.panLeft = function ( distance ) { | |
var te = this.object.matrix.elements; | |
// get X column of matrix | |
panOffset.set( te[ 0 ], te[ 1 ], te[ 2 ] ); | |
panOffset.multiplyScalar( - distance ); | |
pan.add( panOffset ); | |
}; | |
// pass in distance in world space to move up | |
this.panUp = function ( distance ) { | |
var te = this.object.matrix.elements; | |
// get Y column of matrix | |
panOffset.set( te[ 4 ], te[ 5 ], te[ 6 ] ); | |
panOffset.multiplyScalar( distance ); | |
pan.add( panOffset ); | |
}; | |
// pass in x,y of change desired in pixel space, | |
// right and down are positive | |
this.pan = function ( deltaX, deltaY ) { | |
var element = scope.domElement === document ? scope.domElement.body : scope.domElement; | |
if ( scope.object.fov !== undefined ) { | |
// perspective | |
var position = scope.object.position; | |
var offset = position.clone().sub( scope.target ); | |
var targetDistance = offset.length(); | |
// half of the fov is center to top of screen | |
targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); | |
// we actually don't use screenWidth, since perspective camera is fixed to screen height | |
scope.panLeft( 2 * deltaX * targetDistance / element.clientHeight ); | |
scope.panUp( 2 * deltaY * targetDistance / element.clientHeight ); | |
} else if ( scope.object.top !== undefined ) { | |
// orthographic | |
scope.panLeft( deltaX * (scope.object.right - scope.object.left) / element.clientWidth ); | |
scope.panUp( deltaY * (scope.object.top - scope.object.bottom) / element.clientHeight ); | |
} else { | |
// camera neither orthographic or perspective | |
console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); | |
} | |
}; | |
this.dollyIn = function ( dollyScale ) { | |
if ( dollyScale === undefined ) { | |
dollyScale = getZoomScale(); | |
} | |
scale /= dollyScale; | |
}; | |
this.dollyOut = function ( dollyScale ) { | |
if ( dollyScale === undefined ) { | |
dollyScale = getZoomScale(); | |
} | |
scale *= dollyScale; | |
}; | |
this.update = function () { | |
var position = this.object.position; | |
offset.copy( position ).sub( this.target ); | |
// rotate offset to "y-axis-is-up" space | |
offset.applyQuaternion( quat ); | |
// angle from z-axis around y-axis | |
theta = Math.atan2( offset.x, offset.z ); | |
// angle from y-axis | |
phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y ); | |
if ( this.autoRotate && state === STATE.NONE ) { | |
this.rotateLeft( getAutoRotationAngle() ); | |
} | |
theta += thetaDelta; | |
phi += phiDelta; | |
// restrict theta to be between desired limits | |
theta = Math.max( this.minAzimuthAngle, Math.min( this.maxAzimuthAngle, theta ) ); | |
// restrict phi to be between desired limits | |
phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) ); | |
// restrict phi to be betwee EPS and PI-EPS | |
phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) ); | |
var radius = offset.length() * scale; | |
// restrict radius to be between desired limits | |
radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) ); | |
// move target to panned location | |
this.target.add( pan ); | |
offset.x = radius * Math.sin( phi ) * Math.sin( theta ); | |
offset.y = radius * Math.cos( phi ); | |
offset.z = radius * Math.sin( phi ) * Math.cos( theta ); | |
// rotate offset back to "camera-up-vector-is-up" space | |
offset.applyQuaternion( quatInverse ); | |
position.copy( this.target ).add( offset ); | |
this.object.lookAt( this.target ); | |
thetaDelta = 0; | |
phiDelta = 0; | |
scale = 1; | |
pan.set( 0, 0, 0 ); | |
// update condition is: | |
// min(camera displacement, camera rotation in radians)^2 > EPS | |
// using small-angle approximation cos(x/2) = 1 - x^2 / 8 | |
if ( lastPosition.distanceToSquared( this.object.position ) > EPS | |
|| 8 * (1 - lastQuaternion.dot(this.object.quaternion)) > EPS ) { | |
this.dispatchEvent( changeEvent ); | |
lastPosition.copy( this.object.position ); | |
lastQuaternion.copy (this.object.quaternion ); | |
} | |
}; | |
this.reset = function () { | |
state = STATE.NONE; | |
this.target.copy( this.target0 ); | |
this.object.position.copy( this.position0 ); | |
this.update(); | |
}; | |
this.getPolarAngle = function () { | |
return phi; | |
}; | |
this.getAzimuthalAngle = function () { | |
return theta | |
}; | |
function getAutoRotationAngle() { | |
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; | |
} | |
function getZoomScale() { | |
return Math.pow( 0.95, scope.zoomSpeed ); | |
} | |
function onMouseDown( event ) { | |
if ( scope.enabled === false ) return; | |
event.preventDefault(); | |
if ( event.button === scope.mouseButtons.ORBIT ) { | |
if ( scope.noRotate === true ) return; | |
state = STATE.ROTATE; | |
rotateStart.set( event.clientX, event.clientY ); | |
} else if ( event.button === scope.mouseButtons.ZOOM ) { | |
if ( scope.noZoom === true ) return; | |
state = STATE.DOLLY; | |
dollyStart.set( event.clientX, event.clientY ); | |
} else if ( event.button === scope.mouseButtons.PAN ) { | |
if ( scope.noPan === true ) return; | |
state = STATE.PAN; | |
panStart.set( event.clientX, event.clientY ); | |
} | |
if ( state !== STATE.NONE ) { | |
document.addEventListener( 'mousemove', onMouseMove, false ); | |
document.addEventListener( 'mouseup', onMouseUp, false ); | |
scope.dispatchEvent( startEvent ); | |
} | |
} | |
function onMouseMove( event ) { | |
if ( scope.enabled === false ) return; | |
event.preventDefault(); | |
var element = scope.domElement === document ? scope.domElement.body : scope.domElement; | |
if ( state === STATE.ROTATE ) { | |
if ( scope.noRotate === true ) return; | |
rotateEnd.set( event.clientX, event.clientY ); | |
rotateDelta.subVectors( rotateEnd, rotateStart ); | |
// rotating across whole screen goes 360 degrees around | |
scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); | |
// rotating up and down along whole screen attempts to go 360, but limited to 180 | |
scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); | |
rotateStart.copy( rotateEnd ); | |
} else if ( state === STATE.DOLLY ) { | |
if ( scope.noZoom === true ) return; | |
dollyEnd.set( event.clientX, event.clientY ); | |
dollyDelta.subVectors( dollyEnd, dollyStart ); | |
if ( dollyDelta.y > 0 ) { | |
scope.dollyIn(); | |
} else { | |
scope.dollyOut(); | |
} | |
dollyStart.copy( dollyEnd ); | |
} else if ( state === STATE.PAN ) { | |
if ( scope.noPan === true ) return; | |
panEnd.set( event.clientX, event.clientY ); | |
panDelta.subVectors( panEnd, panStart ); | |
scope.pan( panDelta.x, panDelta.y ); | |
panStart.copy( panEnd ); | |
} | |
if ( state !== STATE.NONE ) scope.update(); | |
} | |
function onMouseUp( /* event */ ) { | |
if ( scope.enabled === false ) return; | |
document.removeEventListener( 'mousemove', onMouseMove, false ); | |
document.removeEventListener( 'mouseup', onMouseUp, false ); | |
scope.dispatchEvent( endEvent ); | |
state = STATE.NONE; | |
} | |
function onMouseWheel( event ) { | |
if ( scope.enabled === false || scope.noZoom === true || state !== STATE.NONE ) return; | |
event.preventDefault(); | |
event.stopPropagation(); | |
var delta = 0; | |
if ( event.wheelDelta !== undefined ) { // WebKit / Opera / Explorer 9 | |
delta = event.wheelDelta; | |
} else if ( event.detail !== undefined ) { // Firefox | |
delta = - event.detail; | |
} | |
if ( delta > 0 ) { | |
scope.dollyOut(); | |
} else { | |
scope.dollyIn(); | |
} | |
scope.update(); | |
scope.dispatchEvent( startEvent ); | |
scope.dispatchEvent( endEvent ); | |
} | |
function onKeyDown( event ) { | |
if ( scope.enabled === false || scope.noKeys === true || scope.noPan === true ) return; | |
switch ( event.keyCode ) { | |
case scope.keys.UP: | |
scope.pan( 0, scope.keyPanSpeed ); | |
scope.update(); | |
break; | |
case scope.keys.BOTTOM: | |
scope.pan( 0, - scope.keyPanSpeed ); | |
scope.update(); | |
break; | |
case scope.keys.LEFT: | |
scope.pan( scope.keyPanSpeed, 0 ); | |
scope.update(); | |
break; | |
case scope.keys.RIGHT: | |
scope.pan( - scope.keyPanSpeed, 0 ); | |
scope.update(); | |
break; | |
} | |
} | |
function touchstart( event ) { | |
if ( scope.enabled === false ) return; | |
switch ( event.touches.length ) { | |
case 1: // one-fingered touch: rotate | |
if ( scope.noRotate === true ) return; | |
state = STATE.TOUCH_ROTATE; | |
rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); | |
break; | |
case 2: // two-fingered touch: dolly | |
if ( scope.noZoom === true ) return; | |
state = STATE.TOUCH_DOLLY; | |
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; | |
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; | |
var distance = Math.sqrt( dx * dx + dy * dy ); | |
dollyStart.set( 0, distance ); | |
break; | |
case 3: // three-fingered touch: pan | |
if ( scope.noPan === true ) return; | |
state = STATE.TOUCH_PAN; | |
panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); | |
break; | |
default: | |
state = STATE.NONE; | |
} | |
if ( state !== STATE.NONE ) scope.dispatchEvent( startEvent ); | |
} | |
function touchmove( event ) { | |
if ( scope.enabled === false ) return; | |
event.preventDefault(); | |
event.stopPropagation(); | |
var element = scope.domElement === document ? scope.domElement.body : scope.domElement; | |
switch ( event.touches.length ) { | |
case 1: // one-fingered touch: rotate | |
if ( scope.noRotate === true ) return; | |
if ( state !== STATE.TOUCH_ROTATE ) return; | |
rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); | |
rotateDelta.subVectors( rotateEnd, rotateStart ); | |
// rotating across whole screen goes 360 degrees around | |
scope.rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); | |
// rotating up and down along whole screen attempts to go 360, but limited to 180 | |
scope.rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); | |
rotateStart.copy( rotateEnd ); | |
scope.update(); | |
break; | |
case 2: // two-fingered touch: dolly | |
if ( scope.noZoom === true ) return; | |
if ( state !== STATE.TOUCH_DOLLY ) return; | |
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; | |
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; | |
var distance = Math.sqrt( dx * dx + dy * dy ); | |
dollyEnd.set( 0, distance ); | |
dollyDelta.subVectors( dollyEnd, dollyStart ); | |
if ( dollyDelta.y > 0 ) { | |
scope.dollyOut(); | |
} else { | |
scope.dollyIn(); | |
} | |
dollyStart.copy( dollyEnd ); | |
scope.update(); | |
break; | |
case 3: // three-fingered touch: pan | |
if ( scope.noPan === true ) return; | |
if ( state !== STATE.TOUCH_PAN ) return; | |
panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); | |
panDelta.subVectors( panEnd, panStart ); | |
scope.pan( panDelta.x, panDelta.y ); | |
panStart.copy( panEnd ); | |
scope.update(); | |
break; | |
default: | |
state = STATE.NONE; | |
} | |
} | |
function touchend( /* event */ ) { | |
if ( scope.enabled === false ) return; | |
scope.dispatchEvent( endEvent ); | |
state = STATE.NONE; | |
} | |
this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false ); | |
this.domElement.addEventListener( 'mousedown', onMouseDown, false ); | |
this.domElement.addEventListener( 'mousewheel', onMouseWheel, false ); | |
this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox | |
this.domElement.addEventListener( 'touchstart', touchstart, false ); | |
this.domElement.addEventListener( 'touchend', touchend, false ); | |
this.domElement.addEventListener( 'touchmove', touchmove, false ); | |
window.addEventListener( 'keydown', onKeyDown, false ); | |
// force an update at start | |
this.update(); | |
}; | |
THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); | |
THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; |