├── js ├── .trees.js.swp ├── trees.js └── OrbitControls.js ├── README.md ├── index.html ├── style.css └── logo.svg /js/.trees.js.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnafish/teamtrees/HEAD/js/.trees.js.swp -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # \#TeamTrees 2 | 3 | ## Usage 4 | Download or clone repo. Go to [wrap API](https://wrapapi.com/api/johnfish/teamtrees/treecount/0.0.1) and generate your key, add it to the request in `trees.js`. Then, open up `index.html` and it should just work! 5 | 6 | ## Example 7 | Check out [the live example](http://johnafish.github.io/teamtrees/index.html). It doesn't live update, but it does everything else! 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #TeamTrees 5 | 6 | 7 | 8 |

0

9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0px; 3 | padding: 0px; 4 | overflow: hidden; 5 | } 6 | 7 | canvas { 8 | width: 100%; 9 | height: 100%; 10 | } 11 | 12 | #logo { 13 | position: fixed; 14 | bottom: 10%; 15 | right: 50%; 16 | transform: translate(50%, 0); 17 | height: 10%; 18 | } 19 | 20 | #cash { 21 | position: fixed; 22 | top: 10%; 23 | right: 50%; 24 | transform: translate(50%,-50%); 25 | text-transform: uppercase; 26 | font-family: verdana; 27 | font-size: 6em; 28 | font-weight: 700; 29 | color: #f5f5f5; 30 | text-shadow: 1px 1px 1px #919191, 31 | 1px 2px 1px #919191, 32 | 1px 3px 1px #919191, 33 | 1px 4px 1px #919191, 34 | 1px 5px 1px #919191, 35 | 1px 6px 1px #919191, 36 | 1px 7px 1px #919191, 37 | 1px 8px 1px #919191, 38 | 1px 9px 1px #919191, 39 | 1px 10px 1px #919191, 40 | 1px 18px 6px rgba(16,16,16,0.4), 41 | 1px 22px 10px rgba(16,16,16,0.2), 42 | 1px 25px 35px rgba(16,16,16,0.2), 43 | 1px 30px 60px rgba(16,16,16,0.4); 44 | } 45 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /js/trees.js: -------------------------------------------------------------------------------- 1 | // 2 | // #TeamTrees Web Version 3 | // John Fish, Nov 2019 4 | // 5 | 6 | // README: generate an API key here: https://wrapapi.com/api/johnfish/teamtrees/treecount/0.0.1 7 | // it will not update without such a key 8 | var wrapAPIKey = "key goes in between the quotes"; 9 | 10 | // Various three.js global variables 11 | var scene, 12 | camera, 13 | renderer, 14 | controls, 15 | group; 16 | 17 | // Tracking tree count 18 | var numTrees = 16000000; 19 | var curTrees = 0; 20 | 21 | // Leaf materials 22 | var leaveDarkMaterial = new THREE.MeshLambertMaterial({ color: 0x91E56E }); 23 | var leaveLightMaterial = new THREE.MeshLambertMaterial({ color: 0xA2FF7A }); 24 | var stemMaterial = new THREE.MeshLambertMaterial({ color: 0x7D5A4F }); 25 | var cubeGeometry = new THREE.BoxGeometry(1, 1, 1); 26 | 27 | var radius = 35; // Planet radius 28 | 29 | // Takes number, formats as currency... stolen from stackoverflow 30 | function formatNumber(num) { 31 | return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,') 32 | } 33 | 34 | // Retrieve trees from wrapapi, update text + grow trees when appropriate 35 | function fetchTrees() { 36 | $.ajax({ 37 | url: "https://wrapapi.com/use/johnfish/teamtrees/treecount/0.0.1", 38 | method: "POST", 39 | data: { 40 | "wrapAPIKey": wrapAPIKey 41 | } 42 | }).done(function(data) { 43 | if (data.success) { 44 | numTrees = data["data"]["#totalTrees"]; 45 | } 46 | $("#cash").text("$"+formatNumber(numTrees)) 47 | var diff = Math.floor(numTrees / 10000) - curTrees 48 | if (diff > 0) { 49 | curTrees += diff 50 | growTrees(diff) 51 | } 52 | }); 53 | } 54 | 55 | // Generate a planet at (0,0,0) with specified radius 56 | function planet(r) { 57 | var groundMaterial = new THREE.MeshLambertMaterial({ color: 0x634b35}); 58 | var planetGeometry = new THREE.SphereGeometry(r, 100, 100); 59 | var planet = new THREE.Mesh(planetGeometry, groundMaterial); 60 | planet.position.set(0,0,0); 61 | scene.add(planet) 62 | } 63 | 64 | // Generate a simple tree, rotated with provided angles 65 | function tree(angles) { 66 | var stem = new THREE.Mesh(cubeGeometry, stemMaterial ); 67 | stem.position.set(0, radius + 0.75, 0 ); 68 | stem.scale.set( 0.3, 1.5, 0.3 ); 69 | 70 | var leaveDark = new THREE.Mesh(cubeGeometry, leaveDarkMaterial ); 71 | leaveDark.position.set( 0, radius + 1.2, 0 ); 72 | leaveDark.scale.set( 1, 2, 1 ); 73 | 74 | var leaveLight = new THREE.Mesh(cubeGeometry, leaveLightMaterial ); 75 | leaveLight.position.set( 0, radius + 1.2, 0 ); 76 | leaveLight.scale.set( 1.4, 0.5, 1.4 ); 77 | 78 | var tree = new THREE.Group(); 79 | tree.add( leaveDark ); 80 | tree.add( leaveLight ); 81 | tree.add( stem ); 82 | 83 | tree.rotation.set(angles[0], angles[1], angles[2]) 84 | 85 | return tree 86 | } 87 | 88 | // Generate a random angle triple from [0, 2PI] 89 | function randomAngleTriple() { 90 | return [ 91 | 2 * Math.PI * Math.random(), 92 | 2 * Math.PI * Math.random(), 93 | 2 * Math.PI * Math.random() 94 | ] 95 | } 96 | 97 | // Add n trees to scene randomly 98 | function growTrees(n) { 99 | for (var i = 0; i < n; i++) { 100 | scene.add(tree(randomAngleTriple())) 101 | } 102 | } 103 | 104 | function init() { 105 | // Update tree count regularly 106 | fetchTrees() 107 | setInterval(fetchTrees, 30000) 108 | 109 | // Set up scene + renderer 110 | scene = new THREE.Scene(); 111 | camera = new THREE.PerspectiveCamera(100, window.innerWidth / window.innerHeight, 0.1, 1000); 112 | camera.position.z = 80 113 | 114 | renderer = new THREE.WebGLRenderer(); 115 | renderer.setSize( window.innerWidth, window.innerHeight ); 116 | document.body.appendChild( renderer.domElement ); 117 | 118 | // Create lights, add lights to scene 119 | var light1 = new THREE.DirectionalLight( 0xDDEED3, 1 ); 120 | var light2 = new THREE.AmbientLight(0x7D7D7D); 121 | light1.position.set( 0, 0, 1 ); 122 | 123 | scene.add(light1); 124 | scene.add(light2); 125 | scene.add(planet(radius)); 126 | 127 | // Orbital controls (rotation) 128 | controls = new THREE.OrbitControls(camera, renderer.domElement); 129 | controls.autoRotate = true; 130 | controls.update(); 131 | } 132 | 133 | function render() { 134 | requestAnimationFrame( render ); 135 | controls.update(); 136 | renderer.render( scene, camera ); 137 | } 138 | 139 | init(); 140 | render(); 141 | -------------------------------------------------------------------------------- /js/OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | * @author ScieCode / http://github.com/sciecode 8 | */ 9 | 10 | // This set of controls performs orbiting, dollying (zooming), and panning. 11 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 12 | // 13 | // Orbit - left mouse / touch: one-finger move 14 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 15 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 16 | 17 | THREE.OrbitControls = function ( object, domElement ) { 18 | 19 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); 20 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 21 | 22 | this.object = object; 23 | this.domElement = domElement; 24 | 25 | // Set to false to disable this control 26 | this.enabled = true; 27 | 28 | // "target" sets the location of focus, where the object orbits around 29 | this.target = new THREE.Vector3(); 30 | 31 | // How far you can dolly in and out ( PerspectiveCamera only ) 32 | this.minDistance = 0; 33 | this.maxDistance = Infinity; 34 | 35 | // How far you can zoom in and out ( OrthographicCamera only ) 36 | this.minZoom = 0; 37 | this.maxZoom = Infinity; 38 | 39 | // How far you can orbit vertically, upper and lower limits. 40 | // Range is 0 to Math.PI radians. 41 | this.minPolarAngle = 0; // radians 42 | this.maxPolarAngle = Math.PI; // radians 43 | 44 | // How far you can orbit horizontally, upper and lower limits. 45 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 46 | this.minAzimuthAngle = - Infinity; // radians 47 | this.maxAzimuthAngle = Infinity; // radians 48 | 49 | // Set to true to enable damping (inertia) 50 | // If damping is enabled, you must call controls.update() in your animation loop 51 | this.enableDamping = false; 52 | this.dampingFactor = 0.05; 53 | 54 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 55 | // Set to false to disable zooming 56 | this.enableZoom = true; 57 | this.zoomSpeed = 1.0; 58 | 59 | // Set to false to disable rotating 60 | this.enableRotate = true; 61 | this.rotateSpeed = 1.0; 62 | 63 | // Set to false to disable panning 64 | this.enablePan = true; 65 | this.panSpeed = 1.0; 66 | this.screenSpacePanning = false; // if true, pan in screen-space 67 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 68 | 69 | // Set to true to automatically rotate around the target 70 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 71 | this.autoRotate = false; 72 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 73 | 74 | // Set to false to disable use of the keys 75 | this.enableKeys = true; 76 | 77 | // The four arrow keys 78 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 79 | 80 | // Mouse buttons 81 | this.mouseButtons = { LEFT: THREE.MOUSE.ROTATE, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.PAN }; 82 | 83 | // Touch fingers 84 | this.touches = { ONE: THREE.TOUCH.ROTATE, TWO: THREE.TOUCH.DOLLY_PAN }; 85 | 86 | // for reset 87 | this.target0 = this.target.clone(); 88 | this.position0 = this.object.position.clone(); 89 | this.zoom0 = this.object.zoom; 90 | 91 | // 92 | // public methods 93 | // 94 | 95 | this.getPolarAngle = function () { 96 | 97 | return spherical.phi; 98 | 99 | }; 100 | 101 | this.getAzimuthalAngle = function () { 102 | 103 | return spherical.theta; 104 | 105 | }; 106 | 107 | this.saveState = function () { 108 | 109 | scope.target0.copy( scope.target ); 110 | scope.position0.copy( scope.object.position ); 111 | scope.zoom0 = scope.object.zoom; 112 | 113 | }; 114 | 115 | this.reset = function () { 116 | 117 | scope.target.copy( scope.target0 ); 118 | scope.object.position.copy( scope.position0 ); 119 | scope.object.zoom = scope.zoom0; 120 | 121 | scope.object.updateProjectionMatrix(); 122 | scope.dispatchEvent( changeEvent ); 123 | 124 | scope.update(); 125 | 126 | state = STATE.NONE; 127 | 128 | }; 129 | 130 | // this method is exposed, but perhaps it would be better if we can make it private... 131 | this.update = function () { 132 | 133 | var offset = new THREE.Vector3(); 134 | 135 | // so camera.up is the orbit axis 136 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 137 | var quatInverse = quat.clone().inverse(); 138 | 139 | var lastPosition = new THREE.Vector3(); 140 | var lastQuaternion = new THREE.Quaternion(); 141 | 142 | return function update() { 143 | 144 | var position = scope.object.position; 145 | 146 | offset.copy( position ).sub( scope.target ); 147 | 148 | // rotate offset to "y-axis-is-up" space 149 | offset.applyQuaternion( quat ); 150 | 151 | // angle from z-axis around y-axis 152 | spherical.setFromVector3( offset ); 153 | 154 | if ( scope.autoRotate && state === STATE.NONE ) { 155 | 156 | rotateLeft( getAutoRotationAngle() ); 157 | 158 | } 159 | 160 | if ( scope.enableDamping ) { 161 | 162 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 163 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 164 | 165 | } else { 166 | 167 | spherical.theta += sphericalDelta.theta; 168 | spherical.phi += sphericalDelta.phi; 169 | 170 | } 171 | 172 | // restrict theta to be between desired limits 173 | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); 174 | 175 | // restrict phi to be between desired limits 176 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 177 | 178 | spherical.makeSafe(); 179 | 180 | 181 | spherical.radius *= scale; 182 | 183 | // restrict radius to be between desired limits 184 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 185 | 186 | // move target to panned location 187 | 188 | if ( scope.enableDamping === true ) { 189 | 190 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 191 | 192 | } else { 193 | 194 | scope.target.add( panOffset ); 195 | 196 | } 197 | 198 | offset.setFromSpherical( spherical ); 199 | 200 | // rotate offset back to "camera-up-vector-is-up" space 201 | offset.applyQuaternion( quatInverse ); 202 | 203 | position.copy( scope.target ).add( offset ); 204 | 205 | scope.object.lookAt( scope.target ); 206 | 207 | if ( scope.enableDamping === true ) { 208 | 209 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 210 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 211 | 212 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 213 | 214 | } else { 215 | 216 | sphericalDelta.set( 0, 0, 0 ); 217 | 218 | panOffset.set( 0, 0, 0 ); 219 | 220 | } 221 | 222 | scale = 1; 223 | 224 | // update condition is: 225 | // min(camera displacement, camera rotation in radians)^2 > EPS 226 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 227 | 228 | if ( zoomChanged || 229 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 230 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 231 | 232 | scope.dispatchEvent( changeEvent ); 233 | 234 | lastPosition.copy( scope.object.position ); 235 | lastQuaternion.copy( scope.object.quaternion ); 236 | zoomChanged = false; 237 | 238 | return true; 239 | 240 | } 241 | 242 | return false; 243 | 244 | }; 245 | 246 | }(); 247 | 248 | this.dispose = function () { 249 | 250 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); 251 | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); 252 | scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); 253 | 254 | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); 255 | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); 256 | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); 257 | 258 | document.removeEventListener( 'mousemove', onMouseMove, false ); 259 | document.removeEventListener( 'mouseup', onMouseUp, false ); 260 | 261 | scope.domElement.removeEventListener( 'keydown', onKeyDown, false ); 262 | 263 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 264 | 265 | }; 266 | 267 | // 268 | // internals 269 | // 270 | 271 | var scope = this; 272 | 273 | var changeEvent = { type: 'change' }; 274 | var startEvent = { type: 'start' }; 275 | var endEvent = { type: 'end' }; 276 | 277 | var STATE = { 278 | NONE: - 1, 279 | ROTATE: 0, 280 | DOLLY: 1, 281 | PAN: 2, 282 | TOUCH_ROTATE: 3, 283 | TOUCH_PAN: 4, 284 | TOUCH_DOLLY_PAN: 5, 285 | TOUCH_DOLLY_ROTATE: 6 286 | }; 287 | 288 | var state = STATE.NONE; 289 | 290 | var EPS = 0.000001; 291 | 292 | // current position in spherical coordinates 293 | var spherical = new THREE.Spherical(); 294 | var sphericalDelta = new THREE.Spherical(); 295 | 296 | var scale = 1; 297 | var panOffset = new THREE.Vector3(); 298 | var zoomChanged = false; 299 | 300 | var rotateStart = new THREE.Vector2(); 301 | var rotateEnd = new THREE.Vector2(); 302 | var rotateDelta = new THREE.Vector2(); 303 | 304 | var panStart = new THREE.Vector2(); 305 | var panEnd = new THREE.Vector2(); 306 | var panDelta = new THREE.Vector2(); 307 | 308 | var dollyStart = new THREE.Vector2(); 309 | var dollyEnd = new THREE.Vector2(); 310 | var dollyDelta = new THREE.Vector2(); 311 | 312 | function getAutoRotationAngle() { 313 | 314 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 315 | 316 | } 317 | 318 | function getZoomScale() { 319 | 320 | return Math.pow( 0.95, scope.zoomSpeed ); 321 | 322 | } 323 | 324 | function rotateLeft( angle ) { 325 | 326 | sphericalDelta.theta -= angle; 327 | 328 | } 329 | 330 | function rotateUp( angle ) { 331 | 332 | sphericalDelta.phi -= angle; 333 | 334 | } 335 | 336 | var panLeft = function () { 337 | 338 | var v = new THREE.Vector3(); 339 | 340 | return function panLeft( distance, objectMatrix ) { 341 | 342 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 343 | v.multiplyScalar( - distance ); 344 | 345 | panOffset.add( v ); 346 | 347 | }; 348 | 349 | }(); 350 | 351 | var panUp = function () { 352 | 353 | var v = new THREE.Vector3(); 354 | 355 | return function panUp( distance, objectMatrix ) { 356 | 357 | if ( scope.screenSpacePanning === true ) { 358 | 359 | v.setFromMatrixColumn( objectMatrix, 1 ); 360 | 361 | } else { 362 | 363 | v.setFromMatrixColumn( objectMatrix, 0 ); 364 | v.crossVectors( scope.object.up, v ); 365 | 366 | } 367 | 368 | v.multiplyScalar( distance ); 369 | 370 | panOffset.add( v ); 371 | 372 | }; 373 | 374 | }(); 375 | 376 | // deltaX and deltaY are in pixels; right and down are positive 377 | var pan = function () { 378 | 379 | var offset = new THREE.Vector3(); 380 | 381 | return function pan( deltaX, deltaY ) { 382 | 383 | var element = scope.domElement; 384 | 385 | if ( scope.object.isPerspectiveCamera ) { 386 | 387 | // perspective 388 | var position = scope.object.position; 389 | offset.copy( position ).sub( scope.target ); 390 | var targetDistance = offset.length(); 391 | 392 | // half of the fov is center to top of screen 393 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 394 | 395 | // we use only clientHeight here so aspect ratio does not distort speed 396 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 397 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 398 | 399 | } else if ( scope.object.isOrthographicCamera ) { 400 | 401 | // orthographic 402 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 403 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 404 | 405 | } else { 406 | 407 | // camera neither orthographic nor perspective 408 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 409 | scope.enablePan = false; 410 | 411 | } 412 | 413 | }; 414 | 415 | }(); 416 | 417 | function dollyIn( dollyScale ) { 418 | 419 | if ( scope.object.isPerspectiveCamera ) { 420 | 421 | scale /= dollyScale; 422 | 423 | } else if ( scope.object.isOrthographicCamera ) { 424 | 425 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 426 | scope.object.updateProjectionMatrix(); 427 | zoomChanged = true; 428 | 429 | } else { 430 | 431 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 432 | scope.enableZoom = false; 433 | 434 | } 435 | 436 | } 437 | 438 | function dollyOut( dollyScale ) { 439 | 440 | if ( scope.object.isPerspectiveCamera ) { 441 | 442 | scale *= dollyScale; 443 | 444 | } else if ( scope.object.isOrthographicCamera ) { 445 | 446 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 447 | scope.object.updateProjectionMatrix(); 448 | zoomChanged = true; 449 | 450 | } else { 451 | 452 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 453 | scope.enableZoom = false; 454 | 455 | } 456 | 457 | } 458 | 459 | // 460 | // event callbacks - update the object state 461 | // 462 | 463 | function handleMouseDownRotate( event ) { 464 | 465 | rotateStart.set( event.clientX, event.clientY ); 466 | 467 | } 468 | 469 | function handleMouseDownDolly( event ) { 470 | 471 | dollyStart.set( event.clientX, event.clientY ); 472 | 473 | } 474 | 475 | function handleMouseDownPan( event ) { 476 | 477 | panStart.set( event.clientX, event.clientY ); 478 | 479 | } 480 | 481 | function handleMouseMoveRotate( event ) { 482 | 483 | rotateEnd.set( event.clientX, event.clientY ); 484 | 485 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 486 | 487 | var element = scope.domElement; 488 | 489 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 490 | 491 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 492 | 493 | rotateStart.copy( rotateEnd ); 494 | 495 | scope.update(); 496 | 497 | } 498 | 499 | function handleMouseMoveDolly( event ) { 500 | 501 | dollyEnd.set( event.clientX, event.clientY ); 502 | 503 | dollyDelta.subVectors( dollyEnd, dollyStart ); 504 | 505 | if ( dollyDelta.y > 0 ) { 506 | 507 | dollyIn( getZoomScale() ); 508 | 509 | } else if ( dollyDelta.y < 0 ) { 510 | 511 | dollyOut( getZoomScale() ); 512 | 513 | } 514 | 515 | dollyStart.copy( dollyEnd ); 516 | 517 | scope.update(); 518 | 519 | } 520 | 521 | function handleMouseMovePan( event ) { 522 | 523 | panEnd.set( event.clientX, event.clientY ); 524 | 525 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 526 | 527 | pan( panDelta.x, panDelta.y ); 528 | 529 | panStart.copy( panEnd ); 530 | 531 | scope.update(); 532 | 533 | } 534 | 535 | function handleMouseUp( /*event*/ ) { 536 | 537 | // no-op 538 | 539 | } 540 | 541 | function handleMouseWheel( event ) { 542 | 543 | if ( event.deltaY < 0 ) { 544 | 545 | dollyOut( getZoomScale() ); 546 | 547 | } else if ( event.deltaY > 0 ) { 548 | 549 | dollyIn( getZoomScale() ); 550 | 551 | } 552 | 553 | scope.update(); 554 | 555 | } 556 | 557 | function handleKeyDown( event ) { 558 | 559 | var needsUpdate = false; 560 | 561 | switch ( event.keyCode ) { 562 | 563 | case scope.keys.UP: 564 | pan( 0, scope.keyPanSpeed ); 565 | needsUpdate = true; 566 | break; 567 | 568 | case scope.keys.BOTTOM: 569 | pan( 0, - scope.keyPanSpeed ); 570 | needsUpdate = true; 571 | break; 572 | 573 | case scope.keys.LEFT: 574 | pan( scope.keyPanSpeed, 0 ); 575 | needsUpdate = true; 576 | break; 577 | 578 | case scope.keys.RIGHT: 579 | pan( - scope.keyPanSpeed, 0 ); 580 | needsUpdate = true; 581 | break; 582 | 583 | } 584 | 585 | if ( needsUpdate ) { 586 | 587 | // prevent the browser from scrolling on cursor keys 588 | event.preventDefault(); 589 | 590 | scope.update(); 591 | 592 | } 593 | 594 | 595 | } 596 | 597 | function handleTouchStartRotate( event ) { 598 | 599 | if ( event.touches.length == 1 ) { 600 | 601 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 602 | 603 | } else { 604 | 605 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 606 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 607 | 608 | rotateStart.set( x, y ); 609 | 610 | } 611 | 612 | } 613 | 614 | function handleTouchStartPan( event ) { 615 | 616 | if ( event.touches.length == 1 ) { 617 | 618 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 619 | 620 | } else { 621 | 622 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 623 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 624 | 625 | panStart.set( x, y ); 626 | 627 | } 628 | 629 | } 630 | 631 | function handleTouchStartDolly( event ) { 632 | 633 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 634 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 635 | 636 | var distance = Math.sqrt( dx * dx + dy * dy ); 637 | 638 | dollyStart.set( 0, distance ); 639 | 640 | } 641 | 642 | function handleTouchStartDollyPan( event ) { 643 | 644 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 645 | 646 | if ( scope.enablePan ) handleTouchStartPan( event ); 647 | 648 | } 649 | 650 | function handleTouchStartDollyRotate( event ) { 651 | 652 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 653 | 654 | if ( scope.enableRotate ) handleTouchStartRotate( event ); 655 | 656 | } 657 | 658 | function handleTouchMoveRotate( event ) { 659 | 660 | if ( event.touches.length == 1 ) { 661 | 662 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 663 | 664 | } else { 665 | 666 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 667 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 668 | 669 | rotateEnd.set( x, y ); 670 | 671 | } 672 | 673 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 674 | 675 | var element = scope.domElement; 676 | 677 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 678 | 679 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 680 | 681 | rotateStart.copy( rotateEnd ); 682 | 683 | } 684 | 685 | function handleTouchMovePan( event ) { 686 | 687 | if ( event.touches.length == 1 ) { 688 | 689 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 690 | 691 | } else { 692 | 693 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 694 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 695 | 696 | panEnd.set( x, y ); 697 | 698 | } 699 | 700 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 701 | 702 | pan( panDelta.x, panDelta.y ); 703 | 704 | panStart.copy( panEnd ); 705 | 706 | } 707 | 708 | function handleTouchMoveDolly( event ) { 709 | 710 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 711 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 712 | 713 | var distance = Math.sqrt( dx * dx + dy * dy ); 714 | 715 | dollyEnd.set( 0, distance ); 716 | 717 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 718 | 719 | dollyIn( dollyDelta.y ); 720 | 721 | dollyStart.copy( dollyEnd ); 722 | 723 | } 724 | 725 | function handleTouchMoveDollyPan( event ) { 726 | 727 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 728 | 729 | if ( scope.enablePan ) handleTouchMovePan( event ); 730 | 731 | } 732 | 733 | function handleTouchMoveDollyRotate( event ) { 734 | 735 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 736 | 737 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 738 | 739 | } 740 | 741 | function handleTouchEnd( /*event*/ ) { 742 | 743 | // no-op 744 | 745 | } 746 | 747 | // 748 | // event handlers - FSM: listen for events and reset state 749 | // 750 | 751 | function onMouseDown( event ) { 752 | 753 | if ( scope.enabled === false ) return; 754 | 755 | // Prevent the browser from scrolling. 756 | 757 | event.preventDefault(); 758 | 759 | // Manually set the focus since calling preventDefault above 760 | // prevents the browser from setting it automatically. 761 | 762 | scope.domElement.focus ? scope.domElement.focus() : window.focus(); 763 | 764 | switch ( event.button ) { 765 | 766 | case 0: 767 | 768 | switch ( scope.mouseButtons.LEFT ) { 769 | 770 | case THREE.MOUSE.ROTATE: 771 | 772 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 773 | 774 | if ( scope.enablePan === false ) return; 775 | 776 | handleMouseDownPan( event ); 777 | 778 | state = STATE.PAN; 779 | 780 | } else { 781 | 782 | if ( scope.enableRotate === false ) return; 783 | 784 | handleMouseDownRotate( event ); 785 | 786 | state = STATE.ROTATE; 787 | 788 | } 789 | 790 | break; 791 | 792 | case THREE.MOUSE.PAN: 793 | 794 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 795 | 796 | if ( scope.enableRotate === false ) return; 797 | 798 | handleMouseDownRotate( event ); 799 | 800 | state = STATE.ROTATE; 801 | 802 | } else { 803 | 804 | if ( scope.enablePan === false ) return; 805 | 806 | handleMouseDownPan( event ); 807 | 808 | state = STATE.PAN; 809 | 810 | } 811 | 812 | break; 813 | 814 | default: 815 | 816 | state = STATE.NONE; 817 | 818 | } 819 | 820 | break; 821 | 822 | 823 | case 1: 824 | 825 | switch ( scope.mouseButtons.MIDDLE ) { 826 | 827 | case THREE.MOUSE.DOLLY: 828 | 829 | if ( scope.enableZoom === false ) return; 830 | 831 | handleMouseDownDolly( event ); 832 | 833 | state = STATE.DOLLY; 834 | 835 | break; 836 | 837 | 838 | default: 839 | 840 | state = STATE.NONE; 841 | 842 | } 843 | 844 | break; 845 | 846 | case 2: 847 | 848 | switch ( scope.mouseButtons.RIGHT ) { 849 | 850 | case THREE.MOUSE.ROTATE: 851 | 852 | if ( scope.enableRotate === false ) return; 853 | 854 | handleMouseDownRotate( event ); 855 | 856 | state = STATE.ROTATE; 857 | 858 | break; 859 | 860 | case THREE.MOUSE.PAN: 861 | 862 | if ( scope.enablePan === false ) return; 863 | 864 | handleMouseDownPan( event ); 865 | 866 | state = STATE.PAN; 867 | 868 | break; 869 | 870 | default: 871 | 872 | state = STATE.NONE; 873 | 874 | } 875 | 876 | break; 877 | 878 | } 879 | 880 | if ( state !== STATE.NONE ) { 881 | 882 | document.addEventListener( 'mousemove', onMouseMove, false ); 883 | document.addEventListener( 'mouseup', onMouseUp, false ); 884 | 885 | scope.dispatchEvent( startEvent ); 886 | 887 | } 888 | 889 | } 890 | 891 | function onMouseMove( event ) { 892 | 893 | if ( scope.enabled === false ) return; 894 | 895 | event.preventDefault(); 896 | 897 | switch ( state ) { 898 | 899 | case STATE.ROTATE: 900 | 901 | if ( scope.enableRotate === false ) return; 902 | 903 | handleMouseMoveRotate( event ); 904 | 905 | break; 906 | 907 | case STATE.DOLLY: 908 | 909 | if ( scope.enableZoom === false ) return; 910 | 911 | handleMouseMoveDolly( event ); 912 | 913 | break; 914 | 915 | case STATE.PAN: 916 | 917 | if ( scope.enablePan === false ) return; 918 | 919 | handleMouseMovePan( event ); 920 | 921 | break; 922 | 923 | } 924 | 925 | } 926 | 927 | function onMouseUp( event ) { 928 | 929 | if ( scope.enabled === false ) return; 930 | 931 | handleMouseUp( event ); 932 | 933 | document.removeEventListener( 'mousemove', onMouseMove, false ); 934 | document.removeEventListener( 'mouseup', onMouseUp, false ); 935 | 936 | scope.dispatchEvent( endEvent ); 937 | 938 | state = STATE.NONE; 939 | 940 | } 941 | 942 | function onMouseWheel( event ) { 943 | 944 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 945 | 946 | event.preventDefault(); 947 | event.stopPropagation(); 948 | 949 | scope.dispatchEvent( startEvent ); 950 | 951 | handleMouseWheel( event ); 952 | 953 | scope.dispatchEvent( endEvent ); 954 | 955 | } 956 | 957 | function onKeyDown( event ) { 958 | 959 | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; 960 | 961 | handleKeyDown( event ); 962 | 963 | } 964 | 965 | function onTouchStart( event ) { 966 | 967 | if ( scope.enabled === false ) return; 968 | 969 | event.preventDefault(); 970 | 971 | switch ( event.touches.length ) { 972 | 973 | case 1: 974 | 975 | switch ( scope.touches.ONE ) { 976 | 977 | case THREE.TOUCH.ROTATE: 978 | 979 | if ( scope.enableRotate === false ) return; 980 | 981 | handleTouchStartRotate( event ); 982 | 983 | state = STATE.TOUCH_ROTATE; 984 | 985 | break; 986 | 987 | case THREE.TOUCH.PAN: 988 | 989 | if ( scope.enablePan === false ) return; 990 | 991 | handleTouchStartPan( event ); 992 | 993 | state = STATE.TOUCH_PAN; 994 | 995 | break; 996 | 997 | default: 998 | 999 | state = STATE.NONE; 1000 | 1001 | } 1002 | 1003 | break; 1004 | 1005 | case 2: 1006 | 1007 | switch ( scope.touches.TWO ) { 1008 | 1009 | case THREE.TOUCH.DOLLY_PAN: 1010 | 1011 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1012 | 1013 | handleTouchStartDollyPan( event ); 1014 | 1015 | state = STATE.TOUCH_DOLLY_PAN; 1016 | 1017 | break; 1018 | 1019 | case THREE.TOUCH.DOLLY_ROTATE: 1020 | 1021 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1022 | 1023 | handleTouchStartDollyRotate( event ); 1024 | 1025 | state = STATE.TOUCH_DOLLY_ROTATE; 1026 | 1027 | break; 1028 | 1029 | default: 1030 | 1031 | state = STATE.NONE; 1032 | 1033 | } 1034 | 1035 | break; 1036 | 1037 | default: 1038 | 1039 | state = STATE.NONE; 1040 | 1041 | } 1042 | 1043 | if ( state !== STATE.NONE ) { 1044 | 1045 | scope.dispatchEvent( startEvent ); 1046 | 1047 | } 1048 | 1049 | } 1050 | 1051 | function onTouchMove( event ) { 1052 | 1053 | if ( scope.enabled === false ) return; 1054 | 1055 | event.preventDefault(); 1056 | event.stopPropagation(); 1057 | 1058 | switch ( state ) { 1059 | 1060 | case STATE.TOUCH_ROTATE: 1061 | 1062 | if ( scope.enableRotate === false ) return; 1063 | 1064 | handleTouchMoveRotate( event ); 1065 | 1066 | scope.update(); 1067 | 1068 | break; 1069 | 1070 | case STATE.TOUCH_PAN: 1071 | 1072 | if ( scope.enablePan === false ) return; 1073 | 1074 | handleTouchMovePan( event ); 1075 | 1076 | scope.update(); 1077 | 1078 | break; 1079 | 1080 | case STATE.TOUCH_DOLLY_PAN: 1081 | 1082 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1083 | 1084 | handleTouchMoveDollyPan( event ); 1085 | 1086 | scope.update(); 1087 | 1088 | break; 1089 | 1090 | case STATE.TOUCH_DOLLY_ROTATE: 1091 | 1092 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1093 | 1094 | handleTouchMoveDollyRotate( event ); 1095 | 1096 | scope.update(); 1097 | 1098 | break; 1099 | 1100 | default: 1101 | 1102 | state = STATE.NONE; 1103 | 1104 | } 1105 | 1106 | } 1107 | 1108 | function onTouchEnd( event ) { 1109 | 1110 | if ( scope.enabled === false ) return; 1111 | 1112 | handleTouchEnd( event ); 1113 | 1114 | scope.dispatchEvent( endEvent ); 1115 | 1116 | state = STATE.NONE; 1117 | 1118 | } 1119 | 1120 | function onContextMenu( event ) { 1121 | 1122 | if ( scope.enabled === false ) return; 1123 | 1124 | event.preventDefault(); 1125 | 1126 | } 1127 | 1128 | // 1129 | 1130 | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); 1131 | 1132 | scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); 1133 | scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); 1134 | 1135 | scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); 1136 | scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); 1137 | scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); 1138 | 1139 | scope.domElement.addEventListener( 'keydown', onKeyDown, false ); 1140 | 1141 | // make sure element can receive keys. 1142 | 1143 | if ( scope.domElement.tabIndex === - 1 ) { 1144 | 1145 | scope.domElement.tabIndex = 0; 1146 | 1147 | } 1148 | 1149 | // force an update at start 1150 | 1151 | this.update(); 1152 | 1153 | }; 1154 | 1155 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 1156 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 1157 | 1158 | 1159 | // This set of controls performs orbiting, dollying (zooming), and panning. 1160 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1161 | // This is very similar to OrbitControls, another set of touch behavior 1162 | // 1163 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1164 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1165 | // Pan - left mouse, or arrow keys / touch: one-finger move 1166 | 1167 | THREE.MapControls = function ( object, domElement ) { 1168 | 1169 | THREE.OrbitControls.call( this, object, domElement ); 1170 | 1171 | this.mouseButtons.LEFT = THREE.MOUSE.PAN; 1172 | this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE; 1173 | 1174 | this.touches.ONE = THREE.TOUCH.PAN; 1175 | this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE; 1176 | 1177 | }; 1178 | 1179 | THREE.MapControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 1180 | THREE.MapControls.prototype.constructor = THREE.MapControls; 1181 | --------------------------------------------------------------------------------