three-trackballcontrols-ts
Version:
ThreeJS Trackball Controls as a standalone and typescript compatible npm module
441 lines (440 loc) • 20.2 kB
JavaScript
import * as THREE from 'three';
const STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
const CHANGE_EVENT = { type: 'change' };
const START_EVENT = { type: 'start' };
const END_EVENT = { type: 'end' };
const EPS = 0.000001;
const LAST_POSITION = new THREE.Vector3();
const LAST_ZOOM = { value: 1 };
export class TrackballControls extends THREE.EventDispatcher {
constructor(camera, domElement, domWindow) {
super();
this.getMouseOnScreen = (pageX, pageY) => {
const vector = new THREE.Vector2();
return vector.set((pageX - this.screen.left) / this.screen.width, (pageY - this.screen.top) / this.screen.height);
};
this.getMouseOnCircle = (pageX, pageY) => {
const vector = new THREE.Vector2();
return vector.set((pageX - this.screen.width * 0.5 - this.screen.left) / (this.screen.width * 0.5), (this.screen.height + 2 * (this.screen.top - pageY)) / this.screen.width);
};
this.rotateCamera = () => {
const axis = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const eyeDirection = new THREE.Vector3();
const cameraUpDirection = new THREE.Vector3();
const cameraSidewaysDirection = new THREE.Vector3();
const moveDirection = new THREE.Vector3();
let angle;
moveDirection.set(this._moveCurr.x - this._movePrev.x, this._moveCurr.y - this._movePrev.y, 0);
angle = moveDirection.length();
if (angle) {
this._eye.copy(this.camera.position).sub(this.target);
eyeDirection.copy(this._eye).normalize();
cameraUpDirection.copy(this.camera.up).normalize();
cameraSidewaysDirection.crossVectors(cameraUpDirection, eyeDirection).normalize();
cameraUpDirection.setLength(this._moveCurr.y - this._movePrev.y);
cameraSidewaysDirection.setLength(this._moveCurr.x - this._movePrev.x);
moveDirection.copy(cameraUpDirection.add(cameraSidewaysDirection));
axis.crossVectors(moveDirection, this._eye).normalize();
angle *= this.rotateSpeed;
quaternion.setFromAxisAngle(axis, angle);
this._eye.applyQuaternion(quaternion);
this.camera.up.applyQuaternion(quaternion);
this._lastAxis.copy(axis);
this._lastAngle = angle;
}
else if (!this.staticMoving && this._lastAngle) {
this._lastAngle *= Math.sqrt(1.0 - this.dynamicDampingFactor);
this._eye.copy(this.camera.position).sub(this.target);
quaternion.setFromAxisAngle(this._lastAxis, this._lastAngle);
this._eye.applyQuaternion(quaternion);
this.camera.up.applyQuaternion(quaternion);
}
this._movePrev.copy(this._moveCurr);
};
this.zoomCamera = () => {
let factor = 0;
if (this._state === STATE.TOUCH_ZOOM_PAN) {
factor = this._touchZoomDistanceStart / this._touchZoomDistanceEnd;
this._touchZoomDistanceStart = this._touchZoomDistanceEnd;
if (this.camera['isPerspectiveCamera']) {
this._eye.multiplyScalar(factor);
}
else if (this.camera['isOrthographicCamera']) {
this.camera.zoom *= factor;
this.camera.updateProjectionMatrix();
}
else {
console.warn('THREE.TrackballControls: Unsupported camera type');
}
}
else {
factor = 1.0 + (this._zoomEnd.y - this._zoomStart.y) * this.zoomSpeed;
if (factor !== 1.0 && factor > 0.0) {
if (this.camera['isPerspectiveCamera']) {
this._eye.multiplyScalar(factor);
}
else if (this.camera['isOrthographicCamera']) {
this.camera.zoom /= factor;
this.camera.updateProjectionMatrix();
}
else {
console.warn('THREE.TrackballControls: Unsupported camera type');
}
}
if (this.staticMoving) {
this._zoomStart.copy(this._zoomEnd);
}
else {
this._zoomStart.y += (this._zoomEnd.y - this._zoomStart.y) * this.dynamicDampingFactor;
}
}
};
this.panCamera = () => {
const mouseChange = new THREE.Vector2();
const cameraUp = new THREE.Vector3();
const pan = new THREE.Vector3();
mouseChange.copy(this._panEnd).sub(this._panStart);
if (mouseChange.lengthSq()) {
if (this.camera['isOrthographicCamera']) {
const scale_x = (this.camera.right - this.camera.left) /
this.camera.zoom /
this.domElement.clientWidth;
const scale_y = (this.camera.top - this.camera.bottom) /
this.camera.zoom /
this.domElement.clientWidth;
mouseChange.x *= scale_x;
mouseChange.y *= scale_y;
}
mouseChange.multiplyScalar(this._eye.length() * this.panSpeed);
pan.copy(this._eye).cross(this.camera.up).setLength(mouseChange.x);
pan.add(cameraUp.copy(this.camera.up).setLength(mouseChange.y));
this.camera.position.add(pan);
this.target.add(pan);
if (this.staticMoving) {
this._panStart.copy(this._panEnd);
}
else {
this._panStart.add(mouseChange.subVectors(this._panEnd, this._panStart).multiplyScalar(this.dynamicDampingFactor));
}
}
};
if (domElement === undefined)
console.warn('THREE.TrackballControls: The second parameter "domElement" is now mandatory.');
this.camera = camera;
this.domElement = domElement;
this.window = domWindow !== undefined ? domWindow : window;
// Set to false to disable this control
this.enabled = true;
this.screen = { left: 0, top: 0, width: 0, height: 0 };
this.rotateSpeed = 1.0;
this.zoomSpeed = 1.2;
this.panSpeed = 0.3;
this.noRotate = false;
this.noZoom = false;
this.noPan = false;
this.staticMoving = false;
this.dynamicDampingFactor = 0.2;
// How far you can dolly in and out ( PerspectiveCamera only )
this.minDistance = 0;
this.maxDistance = Infinity;
this.keys = [65 /*A*/, 83 /*S*/, 68 /*D*/];
// Replace ZOOM by DOLLY (threejs r111)
this.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.PAN };
// "target" sets the location of focus, where the camera orbits around
this.target = new THREE.Vector3();
this._state = STATE.NONE;
this._keyState = STATE.NONE;
this._eye = new THREE.Vector3();
this._movePrev = new THREE.Vector2();
this._moveCurr = new THREE.Vector2();
this._lastAxis = new THREE.Vector3();
this._lastAngle = 0;
this._zoomStart = new THREE.Vector2();
this._zoomEnd = new THREE.Vector2();
this._touchZoomDistanceStart = 0;
this._touchZoomDistanceEnd = 0;
this._panStart = new THREE.Vector2();
this._panEnd = new THREE.Vector2();
this.target0 = this.target.clone();
this.position0 = this.camera.position.clone();
this.up0 = this.camera.up.clone();
this.zoom0 = this.camera.zoom;
// event handlers - FSM: listen for events and reset state
this.keydown = (event) => {
if (this.enabled === false)
return;
this.window.removeEventListener('keydown', this.keydown);
if (this._keyState !== STATE.NONE) {
return;
}
else if (event.keyCode === this.keys[STATE.ROTATE] && !this.noRotate) {
this._keyState = STATE.ROTATE;
}
else if (event.keyCode === this.keys[STATE.ZOOM] && !this.noZoom) {
this._keyState = STATE.ZOOM;
}
else if (event.keyCode === this.keys[STATE.PAN] && !this.noPan) {
this._keyState = STATE.PAN;
}
};
this.keyup = () => {
if (this.enabled === false) {
return;
}
this._keyState = STATE.NONE;
this.window.addEventListener('keydown', this.keydown, false);
};
this.mousedown = (event) => {
if (this.enabled === false) {
return;
}
event.preventDefault();
event.stopPropagation();
if (this._state === STATE.NONE) {
switch (event.button) {
case this.mouseButtons.LEFT:
this._state = STATE.ROTATE;
break;
case this.mouseButtons.MIDDLE:
this._state = STATE.ZOOM;
break;
case this.mouseButtons.RIGHT:
this._state = STATE.PAN;
break;
default:
this._state = STATE.NONE;
}
}
const state = this._keyState !== STATE.NONE ? this._keyState : this._state;
if (state === STATE.ROTATE && !this.noRotate) {
this._moveCurr.copy(this.getMouseOnCircle(event.pageX, event.pageY));
this._movePrev.copy(this._moveCurr);
}
else if (state === STATE.ZOOM && !this.noZoom) {
this._zoomStart.copy(this.getMouseOnScreen(event.pageX, event.pageY));
this._zoomEnd.copy(this._zoomStart);
}
else if (state === STATE.PAN && !this.noPan) {
this._panStart.copy(this.getMouseOnScreen(event.pageX, event.pageY));
this._panEnd.copy(this._panStart);
}
document.addEventListener('mousemove', this.mousemove, false);
document.addEventListener('mouseup', this.mouseup, false);
this.dispatchEvent(START_EVENT);
};
this.mousemove = (event) => {
if (this.enabled === false) {
return;
}
event.preventDefault();
event.stopPropagation();
const state = this._keyState !== STATE.NONE ? this._keyState : this._state;
if (state === STATE.ROTATE && !this.noRotate) {
this._movePrev.copy(this._moveCurr);
this._moveCurr.copy(this.getMouseOnCircle(event.pageX, event.pageY));
}
else if (state === STATE.ZOOM && !this.noZoom) {
this._zoomEnd.copy(this.getMouseOnScreen(event.pageX, event.pageY));
}
else if (state === STATE.PAN && !this.noPan) {
this._panEnd.copy(this.getMouseOnScreen(event.pageX, event.pageY));
}
};
this.mouseup = (event) => {
if (this.enabled === false) {
return;
}
event.preventDefault();
event.stopPropagation();
this._state = STATE.NONE;
document.removeEventListener('mousemove', this.mousemove);
document.removeEventListener('mouseup', this.mouseup);
this.dispatchEvent(END_EVENT);
};
this.mousewheel = (event) => {
if (this.enabled === false) {
return;
}
if (this.noZoom === true)
return;
event.preventDefault();
event.stopPropagation();
switch (event.deltaMode) {
case 2:
// Zoom in pages
this._zoomStart.y -= event.deltaY * 0.025;
break;
case 1:
// Zoom in lines
this._zoomStart.y -= event.deltaY * 0.01;
break;
default:
// undefined, 0, assume pixels
this._zoomStart.y -= event.deltaY * 0.00025;
break;
}
this.dispatchEvent(START_EVENT);
this.dispatchEvent(END_EVENT);
};
this.touchstart = (event) => {
if (this.enabled === false) {
return;
}
event.preventDefault();
switch (event.touches.length) {
case 1:
this._state = STATE.TOUCH_ROTATE;
this._moveCurr.copy(this.getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY));
this._movePrev.copy(this._moveCurr);
break;
default:
// 2 or more
this._state = STATE.TOUCH_ZOOM_PAN;
const dx = event.touches[0].pageX - event.touches[1].pageX;
const dy = event.touches[0].pageY - event.touches[1].pageY;
this._touchZoomDistanceEnd = this._touchZoomDistanceStart = Math.sqrt(dx * dx + dy * dy);
const x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
const y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
this._panStart.copy(this.getMouseOnScreen(x, y));
this._panEnd.copy(this._panStart);
break;
}
this.dispatchEvent(START_EVENT);
};
this.touchmove = (event) => {
if (this.enabled === false) {
return;
}
event.preventDefault();
event.stopPropagation();
switch (event.touches.length) {
case 1:
this._movePrev.copy(this._moveCurr);
this._moveCurr.copy(this.getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY));
break;
default:
// 2 or more
const dx = event.touches[0].pageX - event.touches[1].pageX;
const dy = event.touches[0].pageY - event.touches[1].pageY;
this._touchZoomDistanceEnd = Math.sqrt(dx * dx + dy * dy);
const x = (event.touches[0].pageX + event.touches[1].pageX) / 2;
const y = (event.touches[0].pageY + event.touches[1].pageY) / 2;
this._panEnd.copy(this.getMouseOnScreen(x, y));
break;
}
};
this.touchend = (event) => {
if (this.enabled === false) {
return;
}
switch (event.touches.length) {
case 0:
this._state = STATE.NONE;
break;
case 1:
this._state = STATE.TOUCH_ROTATE;
this._moveCurr.copy(this.getMouseOnCircle(event.touches[0].pageX, event.touches[0].pageY));
this._movePrev.copy(this._moveCurr);
break;
}
this.dispatchEvent(END_EVENT);
};
this.contextmenu = (event) => {
if (this.enabled === false) {
return;
}
event.preventDefault();
};
this.domElement.addEventListener('contextmenu', this.contextmenu, false);
this.domElement.addEventListener('mousedown', this.mousedown, false);
this.domElement.addEventListener('wheel', this.mousewheel, false);
this.domElement.addEventListener('touchstart', this.touchstart, false);
this.domElement.addEventListener('touchend', this.touchend, false);
this.domElement.addEventListener('touchmove', this.touchmove, false);
this.window.addEventListener('keydown', this.keydown, false);
this.window.addEventListener('keyup', this.keyup, false);
this.handleResize();
// force an update at start
this.update();
}
dispose() {
this.domElement.removeEventListener('contextmenu', this.contextmenu, false);
this.domElement.removeEventListener('mousedown', this.mousedown, false);
this.domElement.removeEventListener('wheel', this.mousewheel, false);
this.domElement.removeEventListener('touchstart', this.touchstart, false);
this.domElement.removeEventListener('touchend', this.touchend, false);
this.domElement.removeEventListener('touchmove', this.touchmove, false);
document.removeEventListener('mousemove', this.mousemove, false);
document.removeEventListener('mouseup', this.mouseup, false);
this.window.removeEventListener('keydown', this.keydown, false);
this.window.removeEventListener('keyup', this.keyup, false);
}
// ------------------------------------------------
handleResize() {
const box = this.domElement.getBoundingClientRect();
// adjustments come from similar code in the jquery offset() function
const d = this.domElement.ownerDocument.documentElement;
this.screen.left = box.left + this.window.pageXOffset - d.clientLeft;
this.screen.top = box.top + this.window.pageYOffset - d.clientTop;
this.screen.width = box.width;
this.screen.height = box.height;
}
checkDistances() {
if (!this.noZoom || !this.noPan) {
if (this._eye.lengthSq() > this.maxDistance * this.maxDistance) {
this.camera.position.addVectors(this.target, this._eye.setLength(this.maxDistance));
this._zoomStart.copy(this._zoomEnd);
}
if (this._eye.lengthSq() < this.minDistance * this.minDistance) {
this.camera.position.addVectors(this.target, this._eye.setLength(this.minDistance));
this._zoomStart.copy(this._zoomEnd);
}
}
}
update() {
this._eye.subVectors(this.camera.position, this.target);
if (!this.noRotate) {
this.rotateCamera();
}
if (!this.noZoom) {
this.zoomCamera();
}
if (!this.noPan) {
this.panCamera();
}
this.camera.position.addVectors(this.target, this._eye);
if (this.camera['isPerspectiveCamera']) {
this.checkDistances();
this.camera.lookAt(this.target);
if (LAST_POSITION.distanceToSquared(this.camera.position) > EPS) {
this.dispatchEvent(CHANGE_EVENT);
LAST_POSITION.copy(this.camera.position);
}
}
else if (this.camera['isOrthographicCamera']) {
this.camera.lookAt(this.target);
if (LAST_POSITION.distanceToSquared(this.camera.position) > EPS || LAST_ZOOM.value !== this.camera.zoom) {
this.dispatchEvent(CHANGE_EVENT);
LAST_POSITION.copy(this.camera.position);
LAST_ZOOM.value = this.camera.zoom;
}
else {
console.warn('THREE.TrackballControls: Unsupported camera type');
}
}
}
reset() {
this._state = STATE.NONE;
this._keyState = STATE.NONE;
this.target.copy(this.target0);
this.camera.position.copy(this.position0);
this.camera.up.copy(this.up0);
this.camera.zoom = this.zoom0;
this._eye.subVectors(this.camera.position, this.target);
this.camera.lookAt(this.target);
this.dispatchEvent(CHANGE_EVENT);
LAST_POSITION.copy(this.camera.position);
LAST_ZOOM.value = this.camera.zoom;
}
}