This example illustrates some general techniques for working with three.js.
Original concept by Lee Stemkoski
function makeGeometry(size = 1): THREE.BufferGeometry { | |
const vertices = new Float32Array( [ | |
0, 0, 0, size, 0, 0, | |
0, 0, 0, 0, size, 0, | |
0, 0, 0, 0, 0, size | |
] ); | |
const colors = new Float32Array( [ | |
1, 0, 0, 1, 0, 0, | |
0, 1, 0, 0, 1, 0, | |
0, 0, 1, 0, 0, 1 | |
] ); | |
const geometry = new THREE.BufferGeometry(); | |
geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3)); | |
geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
return geometry; | |
} | |
function makeMaterial() { | |
return new THREE.LineBasicMaterial({vertexColors: THREE.VertexColors}); | |
} | |
export default class Axis extends THREE.LineSegments { | |
constructor(size = 1) { | |
super(makeGeometry(size), makeMaterial()); | |
} | |
} |
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); | |
} | |
}; | |
} |
/** | |
* | |
*/ | |
export default function createSphereArc(P: THREE.Vector3, Q: THREE.Vector3): THREE.Curve<THREE.Vector3> { | |
const arc = new THREE.Curve<THREE.Vector3>() | |
arc.getPoint = greatCircleFunction(P, Q) | |
return arc; | |
} | |
/** | |
* | |
*/ | |
function greatCircleFunction(P: THREE.Vector3, Q: THREE.Vector3): (t: number) => THREE.Vector3 { | |
const angle = P.angleTo(Q) | |
return function(t: number) { | |
const p = P.clone().multiplyScalar(Math.sin((1 - t) * angle)) | |
const q = Q.clone().multiplyScalar(Math.sin(t * angle)) | |
const X = new THREE.Vector3().addVectors(p, q).divideScalar(Math.sin(angle)) | |
return X | |
} | |
} |
/** | |
* @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 Axis from './Axis'; | |
import createSphereArc from './createSphereArc' | |
import Detector from './Detector'; | |
import FullScreen from './FullScreen'; | |
import KeyboardState from './KeyboardState'; | |
import makeCurvedLine from './makeCurvedLine'; | |
import makeLineBetweenPoints from './makeLineBetweenPoints'; | |
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(); | |
function init() { | |
scene = new THREE.Scene(); | |
const aspect = window.innerWidth / window.innerHeight | |
camera = new THREE.PerspectiveCamera(45, aspect , 0.1, 20000); | |
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(window.innerWidth, window.innerHeight); | |
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 skyBoxGeometry = new THREE.CubeGeometry(10000, 10000, 10000); | |
const side: THREE.Side = THREE.BackSide | |
const skyBoxMaterial: THREE.MeshBasicMaterial = new THREE.MeshBasicMaterial({color: 0x222244, side: THREE.BackSide}); | |
const skyBox = new THREE.Mesh( skyBoxGeometry, skyBoxMaterial ); | |
scene.add(skyBox); | |
const radius = 50 | |
scene.add(new Axis(radius * 1.5)) | |
// The sphere onto which we will project the wireframe cube. | |
const sphereGeometry = new THREE.SphereGeometry(radius, 32, 16) | |
const sphereMaterial = new THREE.MeshLambertMaterial({color: 0x888888}) | |
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial) | |
scene.add(sphere) | |
// A fudge factor to get the lines outside the sphere | |
sphere.scale.set(0.995, 0.995, 0.995) | |
// The wireframe cube. | |
const cubeGeometry = new THREE.CubeGeometry(120, 120, 120) | |
const cubeMaterial = new THREE.MeshBasicMaterial({color: 0xffff00, wireframe: true}) | |
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial) | |
scene.add(cube) | |
// Lines from vertices of the cube to the sphere. | |
// These indicate the projection lines. | |
for (let i = 0; i < cube.geometry.vertices.length; i++) { | |
const from = cube.geometry.vertices[i].clone() | |
const to = projectOntoMesh(from, sphere) | |
const line = makeLineBetweenPoints(from, to, new THREE.Color(0xffff00), true) | |
scene.add(line) | |
} | |
// The projection of the edges of the cube onto the sphere. | |
const edges: THREE.Vector3[][] = [] | |
for (let i = 0; i < cube.geometry.faces.length; i++) { | |
const face = cube.geometry.faces[i] | |
const a = face.a | |
const b = face.b | |
const c = face.c | |
const va = cube.geometry.vertices[a] | |
const vb = cube.geometry.vertices[b] | |
const vc = cube.geometry.vertices[c] | |
addEdgeToArray(edges, [va, vb]) | |
addEdgeToArray(edges, [vb, vc]) | |
addEdgeToArray(edges, [vc, va]) | |
} | |
for (let i = 0; i < edges.length; i++) { | |
const P = projectOntoMesh(edges[i][0], sphere) | |
const Q = projectOntoMesh(edges[i][1], sphere) | |
const arc = createSphereArc(P, Q) | |
const curvedLine = makeCurvedLine(arc, new THREE.Color(0xffff00)) | |
scene.add(curvedLine) | |
} | |
} | |
/** | |
* | |
*/ | |
function update(timestamp: number) { | |
if ( keyboard.pressed("z") ) { | |
// do something | |
} | |
controls.update() | |
} | |
/** | |
* | |
*/ | |
function render() { | |
renderer.render(scene, camera) | |
} | |
function animate(timestamp: number) { | |
stats.begin() | |
update(timestamp) | |
render() | |
stats.end() | |
requestAnimationFrame(animate) | |
} | |
DomReady.ready(function() { | |
init() | |
requestAnimationFrame(animate) | |
}) | |
/** | |
* Computes the intersection point of a ray from the point (position vector) with the Mesh. | |
* This illustrates the approach in three.js of finding an intersection of a ray with a mesh. | |
*/ | |
function projectOntoMesh(point: THREE.Vector3, mesh: THREE.Mesh): THREE.Vector3 { | |
const origin = point.clone() | |
const direction = point.clone().multiplyScalar(-1).normalize() | |
const ray = new THREE.Raycaster(origin, direction) | |
const intersections = ray.intersectObject(mesh) | |
if (intersections.length > 0) { | |
return intersections[0].point | |
} | |
else { | |
return void 0 | |
} | |
} | |
/** | |
* Edges are equal if their endpoints are equal without regard to orientation. | |
*/ | |
function edgeEquals(edge1: THREE.Vector3[], edge2: THREE.Vector3[]): boolean { | |
return edge1[0].equals(edge2[0]) && edge1[1].equals(edge2[1]) && edge1[0].equals(edge2[1]) && edge1[1].equals(edge2[0]) | |
} | |
function containsEdge(edges: THREE.Vector3[][], edge: THREE.Vector3[]): boolean { | |
for (let i = 0; i < edges.length; i++) { | |
if (edgeEquals(edges[i], edge)) { | |
return true | |
} | |
} | |
return false | |
} | |
/** | |
* Adds only unique edges to the array of edges. | |
*/ | |
function addEdgeToArray(edges: THREE.Vector3[][], edge: THREE.Vector3[]) { | |
if (!containsEdge(edges, edge)) { | |
edges.push(edge) | |
} | |
} |
/** | |
* @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 }; | |
} | |
} | |
} |
/** | |
* Creates a THREE.Line from a THREE.Curve. | |
* The curve will usually be a parameterized curve. | |
*/ | |
export default function makeCurvedLine(curve: THREE.Curve<THREE.Vector3>, color: THREE.Color): THREE.Line { | |
const geometry = new THREE.Geometry() | |
geometry.vertices = curve.getPoints(100) | |
// The following line does not appear to be essential in this example. | |
geometry.computeLineDistances() | |
const material = new THREE.LineBasicMaterial() | |
material.color = color | |
return new THREE.Line(geometry, material) | |
} |
/** | |
* FIXME: The definition of THREE.Line doesn't agree with the arguments here. | |
*/ | |
export default function makeLineBetweenPoints(P: THREE.Vector3, Q: THREE.Vector3, color: THREE.Color, dashed = false): THREE.Line { | |
const geometry = new THREE.Geometry() | |
geometry.vertices.push(P, Q) | |
geometry.computeLineDistances() | |
const material: any = dashed ? new THREE.LineDashedMaterial({dashSize: 2, gapSize: 2}) : new THREE.LineBasicMaterial() | |
material.color = color | |
return new THREE.Line(geometry, material) | |
} |
/** | |
* @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 sphere projection", | |
"name": "threejs-sphere-projection", | |
"version": "0.1.0", | |
"dependencies": { | |
"DomReady": "1.0.0", | |
"three.js": "0.82.0", | |
"stats.js": "0.16.0" | |
}, | |
"keywords": [ | |
"THREE", | |
"three.js", | |
"Sphere", | |
"Projection" | |
], | |
"author": "David Geo Holmes" | |
} |
// |