Home Reference Source

scripts/three/first-person-controls.js

import * as THREE from 'three';

/**
 * FirstPersonControls class
 * 
 * @author mrdoob / http://mrdoob.com/
 * @author alteredq / http://alteredqualia.com/
 * @author paulirish / http://paulirish.com/
 */
class FirstPersonControls {
	/**
	 * Constructor
	 * 
	 * @param {object}
	 *            object Object
	 * @param {object}
	 *            domElement Dom element
	 */
	constructor( object, domElement = document ) {
		this.object = object;
		this.target = new THREE.Vector3( 0, 0, 0 );

		this.domElement = domElement;

		this.enabled = true;

		this.movementSpeed = 1.0;
		this.lookSpeed = 0.005;

		this.lookVertical = true;
		this.autoForward = false;

		this.activeLook = true;

		this.heightSpeed = false;
		this.heightCoef = 1.0;
		this.heightMin = 0.0;
		this.heightMax = 1.0;

		this.constrainVertical = false;
		this.verticalMin = 0;
		this.verticalMax = Math.PI;

		this.autoSpeedFactor = 0.0;

		this.mouseX = 0;
		this.mouseY = 0;

		this.lat = 0;
		this.lon = 0;
		this.phi = 0;
		this.theta = 0;

		this.moveForward = false;
		this.moveBackward = false;
		this.moveLeft = false;
		this.moveRight = false;

		this.mouseDragOn = false;

		this.viewHalfX = 0;
		this.viewHalfY = 0;

		if ( this.domElement !== document ) {
			this.domElement.setAttribute( 'tabindex', - 1 );
		}

		this._contextMenu = this.contextMenu.bind(this);
		this._onMouseMove = this.onMouseMove.bind(this);
		this._onMouseDown = this.onMouseDown.bind(this);
		this._onMouseUp = this.onMouseUp.bind(this);
		this._onKeyDown = this.onKeyDown.bind(this);
		this._onKeyUp = this.onKeyUp.bind(this);

		this.handleResize();
		this.bindEvents();
	}

	/**
	 * HandleResize function
	 */
	handleResize() {
		if ( this.domElement === document ) {
			this.viewHalfX = window.innerWidth / 2;
			this.viewHalfY = window.innerHeight / 2;
		} else {
			this.viewHalfX = this.domElement.offsetWidth / 2;
			this.viewHalfY = this.domElement.offsetHeight / 2;
		}
	}

	/**
	 * BindEvents function
	 */
	bindEvents() {
		this.domElement.addEventListener( 'contextmenu', this._contextmenu, false );
		this.domElement.addEventListener( 'mousemove', this._onMouseMove, false );
		this.domElement.addEventListener( 'mousedown', this._onMouseDown, false );
		this.domElement.addEventListener( 'mouseup', this._onMouseUp, false );

		window.addEventListener( 'keydown', this._onKeyDown, false );
		window.addEventListener( 'keyup', this._onKeyUp, false );
	}

	/**
	 * OnMouseDown function
	 * 
	 * @param {object}
	 *            event Event
	 */
	onMouseDown( event ) {
		if(!this.enabled)
		{
			return;
		}
		if ( this.domElement !== document ) {
			this.domElement.focus();
		}

		event.preventDefault();
		event.stopPropagation();

		if ( this.activeLook ) {
			switch ( event.button ) {
			case 0:
				this.moveForward = true;
				break;
			case 2:
				this.moveBackward = true;
				break;
			}
		}

		this.mouseDragOn = true;
	}

	/**
	 * OnMouseUp function
	 * 
	 * @param {object}
	 *            event Event
	 */
	onMouseUp( event ) {
		if(!this.enabled)
		{
			return;
		}
		event.preventDefault();
		event.stopPropagation();

		if ( this.activeLook ) {
			switch ( event.button ) {
			case 0:
				this.moveForward = false;
				break;
			case 2:
				this.moveBackward = false;
				break;
			}
		}

		this.mouseDragOn = false;
	}

	/**
	 * OnMouseMove function
	 * 
	 * @param {object}
	 *            event Event
	 */
	onMouseMove ( event ) {
		if(!this.enabled)
		{
			return;
		}
		if ( this.domElement === document ) {
			this.mouseX = event.pageX - this.viewHalfX;
			this.mouseY = event.pageY - this.viewHalfY;
		} else {
			this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX;
			this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY;
		}
	}

	/**
	 * OnKeyDown function
	 * 
	 * @param {object}
	 *            event Event
	 */
	onKeyDown( event ) {
		if(!this.enabled)
		{
			return;
		}
		switch ( event.keyCode ) {

		case 38: /* up */
		case 87: /* W */
			this.moveForward = true;
			break;

		case 37: /* left */
		case 65: /* A */
			this.moveLeft = true;
			break;

		case 40: /* down */
		case 83: /* S */
			this.moveBackward = true;
			break;

		case 39: /* right */
		case 68: /* D */
			this.moveRight = true;
			break;

		case 82: /* R */
			this.moveUp = true;
			break;
		case 70: /* F */
			this.moveDown = true;
			break;
		}
	}

	/**
	 * OnKeyUp function
	 * 
	 * @param {object}
	 *            event Event
	 */
	onKeyUp( event ) {
		if(!this.enabled)
		{
			return;
		}
		switch ( event.keyCode ) {

		case 38: /* up */
		case 87: /* W */
			this.moveForward = false;
			break;

		case 37: /* left */
		case 65: /* A */
			this.moveLeft = false;
			break;
		case 40: /* down */
		case 83: /* S */
			this.moveBackward = false;
			break;

		case 39: /* right */
		case 68: /* D */
			this.moveRight = false;
			break;

		case 82: /* R */
			this.moveUp = false;
			break;

		case 70: /* F */
			this.moveDown = false;
			break;
		}
	}

	/**
	 * Update function
	 * 
	 * @param {object}
	 *            delta Delta
	 */
	update( delta ) {
		if ( this.enabled === false ) {
			return;
		}

		if ( this.heightSpeed ) {
			let y = THREE.Math.clamp( this.object.position.y, this.heightMin, this.heightMax );
			let heightDelta = y - this.heightMin;

			this.autoSpeedFactor = delta * ( heightDelta * this.heightCoef );

		} else {
			this.autoSpeedFactor = 0.0;
		}

		let actualMoveSpeed = delta * this.movementSpeed;

		if ( this.moveForward || ( this.autoForward && ! this.moveBackward ) ) {
			this.object.translateZ( - ( actualMoveSpeed + this.autoSpeedFactor ) );
		}
		if ( this.moveBackward ) {
			this.object.translateZ( actualMoveSpeed );
		}

		if ( this.moveLeft ) {
			this.object.translateX( - actualMoveSpeed );
		}
		if ( this.moveRight ) {
			this.object.translateX( actualMoveSpeed );
		}

		if ( this.moveUp ) {
			this.object.translateY( actualMoveSpeed );
		}
		if ( this.moveDown ) {
			this.object.translateY( - actualMoveSpeed );
		}

		let actualLookSpeed = delta * this.lookSpeed;

		if ( ! this.activeLook ) {
			actualLookSpeed = 0;
		}

		let verticalLookRatio = 1;

		if ( this.constrainVertical ) {
			verticalLookRatio = Math.PI / ( this.verticalMax - this.verticalMin );
		}

		this.lon += this.mouseX * actualLookSpeed;
		if ( this.lookVertical ) {
			this.lat -= this.mouseY * actualLookSpeed * verticalLookRatio;
		}

		this.lat = Math.max( - 85, Math.min( 85, this.lat ) );
		this.phi = THREE.Math.degToRad( 90 - this.lat );

		this.theta = THREE.Math.degToRad( this.lon );

		if ( this.constrainVertical ) {
			this.phi = THREE.Math.mapLinear( this.phi, 0, Math.PI, this.verticalMin, this.verticalMax );
		}

		let targetPosition = this.target;
		let position = this.object.position;

		targetPosition.x = position.x + 100 * Math.sin( this.phi ) * Math.cos( this.theta );
		targetPosition.y = position.y + 100 * Math.cos( this.phi );
		targetPosition.z = position.z + 100 * Math.sin( this.phi ) * Math.sin( this.theta );

		this.object.lookAt( targetPosition );
	}

	/**
	 * ContextMenu function
	 * 
	 * @param {object}
	 *            event Event
	 */
	contextMenu( event ) {
		if(!this.enabled)
		{
			return;
		}
		event.preventDefault();
	}

	/**
	 * Dispose function
	 */
	dispose() {
		this.domElement.removeEventListener( 'contextmenu', this._contextmenu, false );
		this.domElement.removeEventListener( 'mousedown', this._onMouseDown, false );
		this.domElement.removeEventListener( 'mousemove', this._onMouseMove, false );
		this.domElement.removeEventListener( 'mouseup', this._onMouseUp, false );

		window.removeEventListener( 'keydown', this._onKeyDown, false );
		window.removeEventListener( 'keyup', this._onKeyUp, false );
	}
}

export {FirstPersonControls};