Skip to content

Instantly share code, notes, and snippets.

@mathdoodle
Last active November 1, 2016 12:59
Show Gist options
  • Save mathdoodle/afbea9f332972bbb48e5821fa257d131 to your computer and use it in GitHub Desktop.
Save mathdoodle/afbea9f332972bbb48e5821fa257d131 to your computer and use it in GitHub Desktop.
three.js sphere projection

three.js sphere projection

Overview

This example illustrates some general techniques for working with three.js.

Credits

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"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment