Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active October 30, 2016 13:38
Show Gist options
  • Save mathdoodle/1315a499cc7fbfefc6dc71328f3a937d to your computer and use it in GitHub Desktop.
Save mathdoodle/1315a499cc7fbfefc6dc71328f3a937d to your computer and use it in GitHub Desktop.
three.js template

OrbitControls with three.js

This example illustrates using OrbitControls to move a three.js Object3D (a PerspectiveCamera in this case).

Credits

Three.js "tutorials by example"

Author: Lee Stemkoski

Date: July 2013

export interface Renderer {
setSize: (width: number, height: number) => any;
}
export interface Camera {
aspect: number;
updateProjectionMatrix: () => any;
}
/**
* Update renderer and camera when the window is resized
*
* renderer -- the renderer to update
* camera -- the camera to update
*/
export default function bindResize(renderer: Renderer, camera: Camera) {
/**
*
*/
const listener = function() {
// Notify the renderer of the size change.
renderer.setSize(window.innerWidth, window.innerHeight);
// Update the camera.
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
const useCapture = false;
window.addEventListener('resize', listener, useCapture);
// return .stop() the function to stop watching window resize
return {
/**
* Stop watching window resize
*/
stop : function(){
window.removeEventListener('resize', listener, useCapture);
}
};
}
/**
* @author alteredq / http://alteredqualia.com/
* @author mr.doob / http://mrdoob.com/
*/
const Detector = {
canvas: !! window['CanvasRenderingContext2D'],
webgl: ( function () { try { return !! window['WebGLRenderingContext'] && !! document.createElement( 'canvas' ).getContext( 'experimental-webgl' ); } catch( e ) { return false; } } )(),
workers: !! window['Worker'],
fileapi: window['File'] && window['FileReader'] && window['FileList'] && window['Blob'],
getWebGLErrorMessage: function () {
var element = document.createElement( 'div' );
element.id = 'webgl-error-message';
element.style.fontFamily = 'monospace';
element.style.fontSize = '13px';
element.style.fontWeight = 'normal';
element.style.textAlign = 'center';
element.style.background = '#fff';
element.style.color = '#000';
element.style.padding = '1.5em';
element.style.width = '400px';
element.style.margin = '5em auto 0';
if ( ! this.webgl ) {
element.innerHTML = window['WebGLRenderingContext'] ? [
'Your graphics card does not seem to support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation" style="color:#000">WebGL</a>.<br />',
'Find out how to get it <a href="http://get.webgl.org/" style="color:#000">here</a>.'
].join( '\n' ) : [
'Your browser does not seem to support <a href="http://khronos.org/webgl/wiki/Getting_a_WebGL_Implementation" style="color:#000">WebGL</a>.<br/>',
'Find out how to get it <a href="http://get.webgl.org/" style="color:#000">here</a>.'
].join( '\n' );
}
return element;
},
addGetWebGLMessage: function ( parameters ) {
var parent, id, element;
parameters = parameters || {};
parent = parameters.parent !== undefined ? parameters.parent : document.body;
id = parameters.id !== undefined ? parameters.id : 'oldie';
element = Detector.getWebGLErrorMessage();
element.id = id;
parent.appendChild( element );
}
};
export default Detector;
// This THREEx helper makes it easy to handle the fullscreen API
// * it hides the prefix for each browser
// * it hides the little discrepencies of the various vendor API
// * at the time of this writing (nov 2011) it is available in
// [firefox nightly](http://blog.pearce.org.nz/2011/11/firefoxs-html-full-screen-api-enabled.html),
// [webkit nightly](http://peter.sh/2011/01/javascript-full-screen-api-navigation-timing-and-repeating-css-gradients/) and
// [chrome stable](http://updates.html5rocks.com/2011/10/Let-Your-Content-Do-the-Talking-Fullscreen-API).
// internal constants to know which fullscreen API implementation is available
function cancelFullScreen(el) {
var requestMethod = el.cancelFullScreen || el.webkitCancelFullScreen || el.mozCancelFullScreen || el.exitFullscreen;
if (requestMethod) { // cancel full screen.
requestMethod.call(el);
}
else if (typeof window['ActiveXObject'] !== "undefined") { // Older IE.
var wscript = new ActiveXObject("WScript.Shell");
if (wscript !== null) {
wscript.SendKeys("{F11}");
}
}
}
function requestFullScreen(el) {
// Supports most browsers and their versions.
var requestMethod = el.requestFullScreen || el.webkitRequestFullScreen(el.ALLOW_KEYBOARD_INPUT) || el.mozRequestFullScreen || el.msRequestFullScreen;
if (requestMethod) { // Native full screen.
requestMethod.call(el);
}
else if (typeof window['ActiveXObject'] !== "undefined") { // Older IE.
var wscript = new ActiveXObject("WScript.Shell");
if (wscript !== null) {
wscript.SendKeys("{F11}");
}
}
return false
}
function toggleFull() {
var elem = document.body; // Make the body go full screen.
var isInFullScreen = (document['fullScreenElement'] && document['fullScreenElement'] !== null) || (document['mozFullScreen'] || document['webkitIsFullScreen']);
if (isInFullScreen) {
cancelFullScreen(document);
} else {
requestFullScreen(elem);
}
return false;
}
const hasWebkitFullScreen = 'webkitCancelFullScreen' in document ? true : false;
// console.log(`hasWebkitFullScreen => ${hasWebkitFullScreen}`);
const hasMozFullScreen = 'mozCancelFullScreen' in document ? true : false;
// console.log(`hasMozFullScreen => ${hasMozFullScreen}`);
const FullScreen = {
/**
* test if it is possible to have fullscreen
*
* @returns {Boolean} true if fullscreen API is available, false otherwise
*/
available: function() {
return hasWebkitFullScreen || hasMozFullScreen;
},
/**
* test if fullscreen is currently activated
*
* @returns {Boolean} true if fullscreen is currently activated, false otherwise
*/
activated: function(): boolean {
if( hasWebkitFullScreen ) {
return document['webkitIsFullScreen'];
}
else if( hasMozFullScreen ) {
return document['mozFullScreen'];
}
else {
console.assert(false);
}
},
/**
* Request fullscreen on a given element
* @param {DomElement} element to make fullscreen. optional. default to document.body
*/
request: function(element: HTMLElement) {
element = element || document.body;
if( hasWebkitFullScreen ) {
element['webkitRequestFullScreen'](Element['ALLOW_KEYBOARD_INPUT']);
}
else if( this._hasMozFullScreen ) {
element['mozRequestFullScreen']();
}
else {
console.assert(false);
}
},
/**
* Cancel fullscreen
*/
cancel: function() {
if( hasWebkitFullScreen ) {
document['webkitCancelFullScreen']();
}
else if( this._hasMozFullScreen ) {
document['mozCancelFullScreen']();
}
else {
console.assert(false);
}
},
/**
* Bind a key to renderer screenshot
* usage: THREEx.FullScreen.bindKey({ charCode : 'a'.charCodeAt(0) });
*/
bindKey: function(opts){
opts = opts || {};
var charCode = opts.charCode || 'f'.charCodeAt(0);
var dblclick = opts.dblclick !== undefined ? opts.dblclick : false;
var element = opts.element
var toggle = function() {
console.log("toggle")
if( FullScreen.activated() ) {
FullScreen.cancel();
}
else {
FullScreen.request(element);
}
}
var onKeyPress = function(event){
if( event.which !== charCode ) return;
toggle();
}.bind(this);
document.addEventListener('keypress', onKeyPress, false);
dblclick && document.addEventListener('dblclick', toggle, false);
return {
unbind : function(){
document.removeEventListener('keypress', onKeyPress, false);
dblclick && document.removeEventListener('dblclick', toggle, false);
}
};
}
};
export default FullScreen;
<!DOCTYPE html>
<html>
<head>
<style>
/* STYLE-MARKER */
</style>
<!-- SCRIPTS-MARKER -->
<script src="https://jspm.io/system.js"></script>
</head>
<body>
<div id="ThreeJS" style="position: absolute; left:0px; top:0px"></div>
<script>
// CODE-MARKER
</script>
<script>System.import('./index.js')</script>
</body>
</html>
import Detector from './Detector';
import FullScreen from './FullScreen';
import KeyboardState from './KeyboardState';
import OrbitControls from './OrbitControls';
import bindResize from './bindResize';
let scene: THREE.Scene;
let camera: THREE.PerspectiveCamera;
let renderer: THREE.WebGLRenderer;
let container: HTMLElement;
let stats: Stats;
let controls: OrbitControls;
const keyboard = new KeyboardState();
DomReady.ready(function() {
init();
animate();
});
function init()
{
scene = new THREE.Scene();
const SCREEN_WIDTH = window.innerWidth, SCREEN_HEIGHT = window.innerHeight;
const VIEW_ANGLE = 45, ASPECT = SCREEN_WIDTH / SCREEN_HEIGHT, NEAR = 0.1, FAR = 20000;
camera = new THREE.PerspectiveCamera( VIEW_ANGLE, ASPECT, NEAR, FAR);
scene.add(camera);
camera.position.set(0,150,400);
camera.lookAt(scene.position);
if (Detector.webgl) {
renderer = new THREE.WebGLRenderer( {antialias:true} );
}
else {
throw new Error("WeBGL is not supported");
}
renderer.setSize(SCREEN_WIDTH, SCREEN_HEIGHT);
container = document.getElementById( 'ThreeJS' );
container.appendChild( renderer.domElement );
bindResize(renderer, camera);
FullScreen.bindKey({ charCode : 'm'.charCodeAt(0) });
controls = new OrbitControls( camera, renderer.domElement );
stats = new Stats();
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
stats.domElement.style.zIndex = '100';
container.appendChild( stats.domElement );
const light = new THREE.PointLight(0xffffff);
light.position.set(100,250,100);
scene.add(light);
const loader = new THREE.TextureLoader();
loader.load('img/textures/checkerboard.jpg',
function (texture: THREE.Texture) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set( 10, 10 );
const floorMaterial = new THREE.MeshBasicMaterial( { map: texture, side: THREE.DoubleSide } );
const floorGeometry = new THREE.PlaneGeometry(1000, 1000, 10, 10);
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.position.y = -0.5;
floor.rotation.x = Math.PI / 2;
scene.add(floor);
},
function(progress) {
// console.log((progress.loaded / progress.total * 100) + '% loaded' );
},
function(e) {
console.log( `An error happened ${JSON.stringify(e)}` );
}
);
var skyBoxGeometry = new THREE.CubeGeometry( 10000, 10000, 10000 );
var skyBoxMaterial = new THREE.MeshBasicMaterial( { color: 0x9999ff, side: THREE.BackSide } );
var skyBox = new THREE.Mesh( skyBoxGeometry, skyBoxMaterial );
scene.add(skyBox);
const geometry = new THREE.SphereGeometry(30, 32, 16);
const material = new THREE.MeshLambertMaterial({ color: 0x000088 });
const mesh = new THREE.Mesh( geometry, material );
mesh.position.set(0, 40, 0);
scene.add(mesh);
}
function animate() {
render();
update();
requestAnimationFrame( animate );
}
function update() {
if ( keyboard.pressed("z") ) {
// do something
}
controls.update();
stats.update();
}
function render() {
renderer.render( scene, camera );
}
/**
* @author Lee Stemkoski
* @author David Geo Holmes (TypeScript migration)
*
* Usage:
* (1) create a global variable:
* var keyboard = new KeyboardState();
* (2) during main loop:
* keyboard.update();
* (3) check state of keys:
* keyboard.down("A") -- true for one update cycle after key is pressed
* keyboard.pressed("A") -- true as long as key is being pressed
* keyboard.up("A") -- true for one update cycle after key is released
*
* See KeyboardState.k object data below for names of keys whose state can be polled
*/
export default class KeyboardState {
constructor() {
// bind keyEvents
document.addEventListener("keydown", KeyboardState.onKeyDown, false);
document.addEventListener("keyup", KeyboardState.onKeyUp, false);
}
update()
{
for (var key in KeyboardState.status)
{
// ensure that every keypress has "down" status exactly once
if (!KeyboardState.status[key].updatedPreviously)
{
KeyboardState.status[key].down = true;
KeyboardState.status[key].pressed = true;
KeyboardState.status[key].updatedPreviously = true;
}
else // updated previously
{
KeyboardState.status[key].down = false;
}
// key has been flagged as "up" since last update
if ( KeyboardState.status[key].up )
{
delete KeyboardState.status[key];
continue; // move on to next key
}
if ( !KeyboardState.status[key].pressed ) // key released
KeyboardState.status[key].up = true;
}
}
down(keyName: string): boolean {
return (KeyboardState.status[keyName] && KeyboardState.status[keyName].down);
}
pressed(keyName: string): boolean {
return (KeyboardState.status[keyName] && KeyboardState.status[keyName].pressed);
}
up(keyName: string): boolean {
return (KeyboardState.status[keyName] && KeyboardState.status[keyName].up);
}
debug() {
var list = "Keys active: ";
for (var arg in KeyboardState.status)
list += " " + arg
console.log(list);
}
static k: {[keyCode: number]: string} = {
8: "backspace", 9: "tab", 13: "enter", 16: "shift",
17: "ctrl", 18: "alt", 27: "esc", 32: "space",
33: "pageup", 34: "pagedown", 35: "end", 36: "home",
37: "left", 38: "up", 39: "right", 40: "down",
45: "insert", 46: "delete", 186: ";", 187: "=",
188: ",", 189: "-", 190: ".", 191: "/",
219: "[", 220: "\\", 221: "]", 222: "'"
};
static status: {[key: string]: {down: boolean; pressed: boolean; up: boolean; updatedPreviously: boolean}} = {};
static keyName(keyCode: number): string {
return ( KeyboardState.k[keyCode] != null ) ? KeyboardState.k[keyCode] : String.fromCharCode(keyCode);
}
static onKeyUp(event: KeyboardEvent) {
const key = KeyboardState.keyName(event.keyCode);
if ( KeyboardState.status[key] )
KeyboardState.status[key].pressed = false;
}
static onKeyDown(event: KeyboardEvent) {
const key = KeyboardState.keyName(event.keyCode);
if (!KeyboardState.status[key]) {
KeyboardState.status[key] = { down: false, pressed: false, up: false, updatedPreviously: false };
}
}
}
/**
* @author qiao / https://github.com/qiao
* @author mrdoob / http://mrdoob.com
* @author alteredq / http://alteredqualia.com/
* @author WestLangley / http://github.com/WestLangley
* @author David Geo Holmes (TypeScript migration)
*/
const EPS = 0.000001;
// 65 /*A*/, 83 /*S*/, 68 /*D*/
const keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40, ROTATE: 65, ZOOM: 83, PAN: 68 };
export default class OrbitControls {
private object: THREE.Object3D;
private domElement;
public enabled = true;
public center = new THREE.Vector3();
public userZoom = true;
public userZoomSpeed = 1.0;
public userRotate = true;
public userRotateSpeed = 1.0;
public userPan = true;
public userPanSpeed = 2.0;
public autoRotate = false;
public autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
public minPolarAngle = 0; // radians
public maxPolarAngle = Math.PI; // radians
public minDistance = 0;
public maxDistance = Infinity;
private phiDelta = 0;
private thetaDelta = 0;
private scale = 1;
private lastPosition = new THREE.Vector3();
constructor( object: THREE.Object3D , domElement: HTMLCanvasElement ) {
this.object = object;
this.domElement = ( domElement !== undefined ) ? domElement : document;
var PIXELS_PER_ROUND = 1800;
var rotateStart = new THREE.Vector2();
var rotateEnd = new THREE.Vector2();
var rotateDelta = new THREE.Vector2();
var zoomStart = new THREE.Vector2();
var zoomEnd = new THREE.Vector2();
var zoomDelta = new THREE.Vector2();
var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 };
var state = STATE.NONE;
// events
var changeEvent = { type: 'change' };
this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
const onMouseDown = (event: MouseEvent) => {
if (this.enabled === false ) return;
if (this.userRotate === false ) return;
event.preventDefault();
if ( state === STATE.NONE )
{
if ( event.button === 0 )
state = STATE.ROTATE;
if ( event.button === 1 )
state = STATE.ZOOM;
if ( event.button === 2 )
state = STATE.PAN;
}
if ( state === STATE.ROTATE ) {
//state = STATE.ROTATE;
rotateStart.set( event.clientX, event.clientY );
}
else if ( state === STATE.ZOOM ) {
//state = STATE.ZOOM;
zoomStart.set( event.clientX, event.clientY );
}
else if ( state === STATE.PAN ) {
//state = STATE.PAN;
}
document.addEventListener( 'mousemove', onMouseMove, false );
document.addEventListener( 'mouseup', onMouseUp, false );
}
this.domElement.addEventListener( 'mousedown', onMouseDown, false );
const onMouseMove = (event: MouseEvent) => {
if (this.enabled === false ) return;
event.preventDefault();
if ( state === STATE.ROTATE ) {
rotateEnd.set( event.clientX, event.clientY );
rotateDelta.subVectors( rotateEnd, rotateStart );
this.rotateLeft( 2 * Math.PI * rotateDelta.x / PIXELS_PER_ROUND * this.userRotateSpeed );
this.rotateUp( 2 * Math.PI * rotateDelta.y / PIXELS_PER_ROUND * this.userRotateSpeed );
rotateStart.copy( rotateEnd );
}
else if ( state === STATE.ZOOM ) {
zoomEnd.set( event.clientX, event.clientY );
zoomDelta.subVectors( zoomEnd, zoomStart );
if ( zoomDelta.y > 0 ) {
this.zoomIn();
}
else {
this.zoomOut();
}
zoomStart.copy( zoomEnd );
} else if ( state === STATE.PAN ) {
const movementX: number = event['movementX'] || event['mozMovementX'] || event['webkitMovementX'] || 0;
const movementY = event['movementY'] || event['mozMovementY'] || event['webkitMovementY'] || 0;
this.pan( new THREE.Vector3( - movementX, movementY, 0 ) );
}
}
const onMouseUp = (event: MouseEvent) => {
if (this.enabled === false) return;
if (this.userRotate === false) return;
document.removeEventListener( 'mousemove', onMouseMove, false );
document.removeEventListener( 'mouseup', onMouseUp, false );
state = STATE.NONE;
}
const onMouseWheel = (event: MouseWheelEvent) => {
if (this.enabled === false ) return;
if (this.userZoom === false ) return;
let delta = 0;
if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9
delta = event.wheelDelta;
}
else if ( event.detail ) { // Firefox
delta = - event.detail;
}
if ( delta > 0 ) {
this.zoomOut();
}
else {
this.zoomIn();
}
}
this.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox
const onKeyDown = (event: KeyboardEvent) => {
if (this.enabled === false) return;
if (this.userPan === false) return;
switch ( event.keyCode ) {
/*case scope.keys.UP:
scope.pan( new THREE.Vector3( 0, 1, 0 ) );
break;
case scope.keys.BOTTOM:
scope.pan( new THREE.Vector3( 0, - 1, 0 ) );
break;
case scope.keys.LEFT:
scope.pan( new THREE.Vector3( - 1, 0, 0 ) );
break;
case scope.keys.RIGHT:
scope.pan( new THREE.Vector3( 1, 0, 0 ) );
break;
*/
case keys.ROTATE:
state = STATE.ROTATE;
break;
case keys.ZOOM:
state = STATE.ZOOM;
break;
case keys.PAN:
state = STATE.PAN;
break;
}
}
const onKeyUp = (event: KeyboardEvent) => {
switch (event.keyCode) {
case keys.ROTATE:
case keys.ZOOM:
case keys.PAN:
state = STATE.NONE;
break;
}
}
window.addEventListener( 'keydown', onKeyDown, false );
window.addEventListener( 'keyup', onKeyUp, false );
}
private getAutoRotationAngle(): number {
return 2 * Math.PI / 60 / 60 * this.autoRotateSpeed;
}
private getZoomScale(): number {
return Math.pow( 0.95, this.userZoomSpeed );
}
rotateLeft(angle?: number): void {
if ( angle === undefined ) {
angle = this.getAutoRotationAngle();
}
this.thetaDelta -= angle;
}
rotateRight(angle?: number): void {
if ( angle === undefined ) {
angle = this.getAutoRotationAngle();
}
this.thetaDelta += angle;
}
rotateUp(angle?: number) {
if ( angle === undefined ) {
angle = this.getAutoRotationAngle();
}
this.phiDelta -= angle;
}
rotateDown(angle?: number) {
if ( angle === undefined ) {
angle = this.getAutoRotationAngle();
}
this.phiDelta += angle;
}
zoomIn(zoomScale?: number): void {
if ( zoomScale === undefined ) {
zoomScale = this.getZoomScale();
}
this.scale /= zoomScale;
}
zoomOut(zoomScale?: number): void {
if (zoomScale === undefined) {
zoomScale = this.getZoomScale();
}
this.scale *= zoomScale;
}
pan( distance: THREE.Vector3 ) {
distance.transformDirection( this.object.matrix );
distance.multiplyScalar( this.userPanSpeed );
this.object.position.add( distance );
this.center.add( distance );
}
update() {
const position = this.object.position;
const offset = position.clone().sub( this.center );
// angle from z-axis around y-axis
let theta = Math.atan2( offset.x, offset.z );
// angle from y-axis
let phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );
if (this.autoRotate) {
this.rotateLeft(this.getAutoRotationAngle());
}
theta += this.thetaDelta;
phi += this.phiDelta;
// 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() * this.scale;
// restrict radius to be between desired limits
radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) );
offset.x = radius * Math.sin( phi ) * Math.sin( theta );
offset.y = radius * Math.cos( phi );
offset.z = radius * Math.sin( phi ) * Math.cos( theta );
position.copy( this.center ).add( offset );
this.object.lookAt( this.center );
this.thetaDelta = 0;
this.phiDelta = 0;
this.scale = 1;
if ( this.lastPosition.distanceTo( this.object.position ) > 0 ) {
// this.dispatchEvent( changeEvent );
this.lastPosition.copy( this.object.position );
}
}
}
{
"description": "three.js template",
"name": "copy-of-",
"version": "0.1.0",
"dependencies": {
"DomReady": "1.0.0",
"three.js": "0.82.0",
"stats.js": "0.16.0"
},
"keywords": [
"THREE",
"three.js"
]
}
// This THREEx helper makes it easy to handle window resize.
// It will update renderer and camera when window is resized.
//
// # Usage
//
// **Step 1**: Start updating renderer and camera
//
// ```var windowResize = THREEx.WindowResize(aRenderer, aCamera)```
//
// **Step 2**: Start updating renderer and camera
//
// ```windowResize.stop()```
// # Code
/**
* Update renderer and camera when the window is resized
*
* @param {Object} renderer the renderer to update
* @param {Object} Camera the camera to update
*/
export default function WindowResize(renderer, camera){
var callback = function(){
// notify the renderer of the size change
renderer.setSize( window.innerWidth, window.innerHeight );
// update the camera
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
}
// bind the resize event
window.addEventListener('resize', callback, false);
// return .stop() the function to stop watching window resize
return {
/**
* Stop watching window resize
*/
stop : function(){
window.removeEventListener('resize', callback);
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment