1/** 2 * @author Eberhard Graether / http://egraether.com/ 3 * @author Mark Lundin / http://mark-lundin.com 4 * @author Simone Manini / http://daron1337.github.io 5 * @author Luca Antiga / http://lantiga.github.io 6 */ 7 8THREE.TrackballControls = function ( object, domElement ) { 9 10 var _this = this; 11 var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 12 13 this.object = object; 14 this.domElement = ( domElement !== undefined ) ? domElement : document; 15 16 // API 17 18 this.enabled = true; 19 20 this.screen = { left: 0, top: 0, width: 0, height: 0 }; 21 22 this.rotateSpeed = 1.0; 23 this.zoomSpeed = 1.2; 24 this.panSpeed = 0.3; 25 26 this.noRotate = false; 27 this.noZoom = false; 28 this.noPan = false; 29 30 this.staticMoving = false; 31 this.dynamicDampingFactor = 0.2; 32 33 this.minDistance = 0; 34 this.maxDistance = Infinity; 35 36 this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ]; 37 38 // internals 39 40 this.target = new THREE.Vector3(); 41 42 var EPS = 0.000001; 43 44 var lastPosition = new THREE.Vector3(); 45 46 var _state = STATE.NONE, 47 _prevState = STATE.NONE, 48 49 _eye = new THREE.Vector3(), 50 51 _movePrev = new THREE.Vector2(), 52 _moveCurr = new THREE.Vector2(), 53 54 _lastAxis = new THREE.Vector3(), 55 _lastAngle = 0, 56 57 _zoomStart = new THREE.Vector2(), 58 _zoomEnd = new THREE.Vector2(), 59 60 _touchZoomDistanceStart = 0, 61 _touchZoomDistanceEnd = 0, 62 63 _panStart = new THREE.Vector2(), 64 _panEnd = new THREE.Vector2(); 65 66 // for reset 67 68 this.target0 = this.target.clone(); 69 this.position0 = this.object.position.clone(); 70 this.up0 = this.object.up.clone(); 71 72 // events 73 74 var changeEvent = { type: 'change' }; 75 var startEvent = { type: 'start' }; 76 var endEvent = { type: 'end' }; 77 78 79 // methods 80 81 this.handleResize = function () { 82 83 if ( this.domElement === document ) { 84 85 this.screen.left = 0; 86 this.screen.top = 0; 87 this.screen.width = window.innerWidth; 88 this.screen.height = window.innerHeight; 89 90 } else { 91 92 var box = this.domElement.getBoundingClientRect(); 93 // adjustments come from similar code in the jquery offset() function 94 var d = this.domElement.ownerDocument.documentElement; 95 this.screen.left = box.left + window.pageXOffset - d.clientLeft; 96 this.screen.top = box.top + window.pageYOffset - d.clientTop; 97 this.screen.width = box.width; 98 this.screen.height = box.height; 99 100 } 101 102 }; 103 104 var getMouseOnScreen = ( function () { 105 106 var vector = new THREE.Vector2(); 107 108 return function getMouseOnScreen( pageX, pageY ) { 109 110 vector.set( 111 ( pageX - _this.screen.left ) / _this.screen.width, 112 ( pageY - _this.screen.top ) / _this.screen.height 113 ); 114 115 return vector; 116 117 }; 118 119 }() ); 120 121 var getMouseOnCircle = ( function () { 122 123 var vector = new THREE.Vector2(); 124 125 return function getMouseOnCircle( pageX, pageY ) { 126 127 vector.set( 128 ( ( pageX - _this.screen.width * 0.5 - _this.screen.left ) / ( _this.screen.width * 0.5 ) ), 129 ( ( _this.screen.height + 2 * ( _this.screen.top - pageY ) ) / _this.screen.width ) // screen.width intentional 130 ); 131 132 return vector; 133 134 }; 135 136 }() ); 137 138 this.rotateCamera = ( function () { 139 140 var axis = new THREE.Vector3(), 141 quaternion = new THREE.Quaternion(), 142 eyeDirection = new THREE.Vector3(), 143 objectUpDirection = new THREE.Vector3(), 144 objectSidewaysDirection = new THREE.Vector3(), 145 moveDirection = new THREE.Vector3(), 146 angle; 147 148 return function rotateCamera() { 149 150 moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); 151 angle = moveDirection.length(); 152 153 if ( angle ) { 154 155 _eye.copy( _this.object.position ).sub( _this.target ); 156 157 eyeDirection.copy( _eye ).normalize(); 158 objectUpDirection.copy( _this.object.up ).normalize(); 159 objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); 160 161 objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); 162 objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); 163 164 moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); 165 166 axis.crossVectors( moveDirection, _eye ).normalize(); 167 168 angle *= _this.rotateSpeed; 169 quaternion.setFromAxisAngle( axis, angle ); 170 171 _eye.applyQuaternion( quaternion ); 172 _this.object.up.applyQuaternion( quaternion ); 173 174 _lastAxis.copy( axis ); 175 _lastAngle = angle; 176 177 } else if ( ! _this.staticMoving && _lastAngle ) { 178 179 _lastAngle *= Math.sqrt( 1.0 - _this.dynamicDampingFactor ); 180 _eye.copy( _this.object.position ).sub( _this.target ); 181 quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); 182 _eye.applyQuaternion( quaternion ); 183 _this.object.up.applyQuaternion( quaternion ); 184 185 } 186 187 _movePrev.copy( _moveCurr ); 188 189 }; 190 191 }() ); 192 193 194 this.zoomCamera = function () { 195 196 var factor; 197 198 if ( _state === STATE.TOUCH_ZOOM_PAN ) { 199 200 factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 201 _touchZoomDistanceStart = _touchZoomDistanceEnd; 202 _eye.multiplyScalar( factor ); 203 204 } else { 205 206 factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * _this.zoomSpeed; 207 208 if ( factor !== 1.0 && factor > 0.0 ) { 209 210 _eye.multiplyScalar( factor ); 211 212 } 213 214 if ( _this.staticMoving ) { 215 216 _zoomStart.copy( _zoomEnd ); 217 218 } else { 219 220 _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 221 222 } 223 224 } 225 226 }; 227 228 this.panCamera = ( function () { 229 230 var mouseChange = new THREE.Vector2(), 231 objectUp = new THREE.Vector3(), 232 pan = new THREE.Vector3(); 233 234 return function panCamera() { 235 236 mouseChange.copy( _panEnd ).sub( _panStart ); 237 238 if ( mouseChange.lengthSq() ) { 239 240 mouseChange.multiplyScalar( _eye.length() * _this.panSpeed ); 241 242 pan.copy( _eye ).cross( _this.object.up ).setLength( mouseChange.x ); 243 pan.add( objectUp.copy( _this.object.up ).setLength( mouseChange.y ) ); 244 245 _this.object.position.add( pan ); 246 _this.target.add( pan ); 247 248 if ( _this.staticMoving ) { 249 250 _panStart.copy( _panEnd ); 251 252 } else { 253 254 _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( _this.dynamicDampingFactor ) ); 255 256 } 257 258 } 259 260 }; 261 262 }() ); 263 264 this.checkDistances = function () { 265 266 if ( ! _this.noZoom || ! _this.noPan ) { 267 268 if ( _eye.lengthSq() > _this.maxDistance * _this.maxDistance ) { 269 270 _this.object.position.addVectors( _this.target, _eye.setLength( _this.maxDistance ) ); 271 _zoomStart.copy( _zoomEnd ); 272 273 } 274 275 if ( _eye.lengthSq() < _this.minDistance * _this.minDistance ) { 276 277 _this.object.position.addVectors( _this.target, _eye.setLength( _this.minDistance ) ); 278 _zoomStart.copy( _zoomEnd ); 279 280 } 281 282 } 283 284 }; 285 286 this.update = function () { 287 288 _eye.subVectors( _this.object.position, _this.target ); 289 290 if ( ! _this.noRotate ) { 291 292 _this.rotateCamera(); 293 294 } 295 296 if ( ! _this.noZoom ) { 297 298 _this.zoomCamera(); 299 300 } 301 302 if ( ! _this.noPan ) { 303 304 _this.panCamera(); 305 306 } 307 308 _this.object.position.addVectors( _this.target, _eye ); 309 310 _this.checkDistances(); 311 312 _this.object.lookAt( _this.target ); 313 314 if ( lastPosition.distanceToSquared( _this.object.position ) > EPS ) { 315 316 _this.dispatchEvent( changeEvent ); 317 318 lastPosition.copy( _this.object.position ); 319 320 } 321 322 }; 323 324 this.reset = function () { 325 326 _state = STATE.NONE; 327 _prevState = STATE.NONE; 328 329 _this.target.copy( _this.target0 ); 330 _this.object.position.copy( _this.position0 ); 331 _this.object.up.copy( _this.up0 ); 332 333 _eye.subVectors( _this.object.position, _this.target ); 334 335 _this.object.lookAt( _this.target ); 336 337 _this.dispatchEvent( changeEvent ); 338 339 lastPosition.copy( _this.object.position ); 340 341 }; 342 343 // listeners 344 345 function keydown( event ) { 346 347 if ( _this.enabled === false ) return; 348 349 window.removeEventListener( 'keydown', keydown ); 350 351 _prevState = _state; 352 353 if ( _state !== STATE.NONE ) { 354 355 return; 356 357 } else if ( event.keyCode === _this.keys[ STATE.ROTATE ] && ! _this.noRotate ) { 358 359 _state = STATE.ROTATE; 360 361 } else if ( event.keyCode === _this.keys[ STATE.ZOOM ] && ! _this.noZoom ) { 362 363 _state = STATE.ZOOM; 364 365 } else if ( event.keyCode === _this.keys[ STATE.PAN ] && ! _this.noPan ) { 366 367 _state = STATE.PAN; 368 369 } 370 371 } 372 373 function keyup( event ) { 374 375 if ( _this.enabled === false ) return; 376 377 _state = _prevState; 378 379 window.addEventListener( 'keydown', keydown, false ); 380 381 } 382 383 function mousedown( event ) { 384 385 if ( _this.enabled === false ) return; 386 387 event.preventDefault(); 388 event.stopPropagation(); 389 390 if ( _state === STATE.NONE ) { 391 392 _state = event.button; 393 394 } 395 396 if ( _state === STATE.ROTATE && ! _this.noRotate ) { 397 398 _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 399 _movePrev.copy( _moveCurr ); 400 401 } else if ( _state === STATE.ZOOM && ! _this.noZoom ) { 402 403 _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 404 _zoomEnd.copy( _zoomStart ); 405 406 } else if ( _state === STATE.PAN && ! _this.noPan ) { 407 408 _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 409 _panEnd.copy( _panStart ); 410 411 } 412 413 document.addEventListener( 'mousemove', mousemove, false ); 414 document.addEventListener( 'mouseup', mouseup, false ); 415 416 _this.dispatchEvent( startEvent ); 417 418 } 419 420 function mousemove( event ) { 421 422 if ( _this.enabled === false ) return; 423 424 event.preventDefault(); 425 event.stopPropagation(); 426 427 if ( _state === STATE.ROTATE && ! _this.noRotate ) { 428 429 _movePrev.copy( _moveCurr ); 430 _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 431 432 } else if ( _state === STATE.ZOOM && ! _this.noZoom ) { 433 434 _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 435 436 } else if ( _state === STATE.PAN && ! _this.noPan ) { 437 438 _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 439 440 } 441 442 } 443 444 function mouseup( event ) { 445 446 if ( _this.enabled === false ) return; 447 448 event.preventDefault(); 449 event.stopPropagation(); 450 451 _state = STATE.NONE; 452 453 document.removeEventListener( 'mousemove', mousemove ); 454 document.removeEventListener( 'mouseup', mouseup ); 455 _this.dispatchEvent( endEvent ); 456 457 } 458 459 function mousewheel( event ) { 460 461 if ( _this.enabled === false ) return; 462 463 if ( _this.noZoom === true ) return; 464 465 event.preventDefault(); 466 event.stopPropagation(); 467 468 switch ( event.deltaMode ) { 469 470 case 2: 471 // Zoom in pages 472 _zoomStart.y -= event.deltaY * 0.025; 473 break; 474 475 case 1: 476 // Zoom in lines 477 _zoomStart.y -= event.deltaY * 0.01; 478 break; 479 480 default: 481 // undefined, 0, assume pixels 482 _zoomStart.y -= event.deltaY * 0.00025; 483 break; 484 485 } 486 487 _this.dispatchEvent( startEvent ); 488 _this.dispatchEvent( endEvent ); 489 490 } 491 492 function touchstart( event ) { 493 494 if ( _this.enabled === false ) return; 495 496 event.preventDefault(); 497 498 switch ( event.touches.length ) { 499 500 case 1: 501 _state = STATE.TOUCH_ROTATE; 502 _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 503 _movePrev.copy( _moveCurr ); 504 break; 505 506 default: // 2 or more 507 _state = STATE.TOUCH_ZOOM_PAN; 508 var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 509 var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 510 _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 511 512 var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 513 var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 514 _panStart.copy( getMouseOnScreen( x, y ) ); 515 _panEnd.copy( _panStart ); 516 break; 517 518 } 519 520 _this.dispatchEvent( startEvent ); 521 522 } 523 524 function touchmove( event ) { 525 526 if ( _this.enabled === false ) return; 527 528 event.preventDefault(); 529 event.stopPropagation(); 530 531 switch ( event.touches.length ) { 532 533 case 1: 534 _movePrev.copy( _moveCurr ); 535 _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 536 break; 537 538 default: // 2 or more 539 var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 540 var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 541 _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); 542 543 var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 544 var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 545 _panEnd.copy( getMouseOnScreen( x, y ) ); 546 break; 547 548 } 549 550 } 551 552 function touchend( event ) { 553 554 if ( _this.enabled === false ) return; 555 556 switch ( event.touches.length ) { 557 558 case 0: 559 _state = STATE.NONE; 560 break; 561 562 case 1: 563 _state = STATE.TOUCH_ROTATE; 564 _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 565 _movePrev.copy( _moveCurr ); 566 break; 567 568 } 569 570 _this.dispatchEvent( endEvent ); 571 572 } 573 574 function contextmenu( event ) { 575 576 if ( _this.enabled === false ) return; 577 578 event.preventDefault(); 579 580 } 581 582 this.dispose = function () { 583 584 this.domElement.removeEventListener( 'contextmenu', contextmenu, false ); 585 this.domElement.removeEventListener( 'mousedown', mousedown, false ); 586 this.domElement.removeEventListener( 'wheel', mousewheel, false ); 587 588 this.domElement.removeEventListener( 'touchstart', touchstart, false ); 589 this.domElement.removeEventListener( 'touchend', touchend, false ); 590 this.domElement.removeEventListener( 'touchmove', touchmove, false ); 591 592 document.removeEventListener( 'mousemove', mousemove, false ); 593 document.removeEventListener( 'mouseup', mouseup, false ); 594 595 window.removeEventListener( 'keydown', keydown, false ); 596 window.removeEventListener( 'keyup', keyup, false ); 597 598 }; 599 600 this.domElement.addEventListener( 'contextmenu', contextmenu, false ); 601 this.domElement.addEventListener( 'mousedown', mousedown, false ); 602 this.domElement.addEventListener( 'wheel', mousewheel, false ); 603 604 this.domElement.addEventListener( 'touchstart', touchstart, false ); 605 this.domElement.addEventListener( 'touchend', touchend, false ); 606 this.domElement.addEventListener( 'touchmove', touchmove, false ); 607 608 window.addEventListener( 'keydown', keydown, false ); 609 window.addEventListener( 'keyup', keyup, false ); 610 611 this.handleResize(); 612 613 // force an update at start 614 this.update(); 615 616}; 617 618THREE.TrackballControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 619THREE.TrackballControls.prototype.constructor = THREE.TrackballControls;