├── .gitignore ├── LICENSE ├── NPM-INSTRUCTIONS.md ├── README.md ├── dist ├── .gitkeep ├── aframe-orbit-controls-component.js └── aframe-orbit-controls-component.min.js ├── examples ├── assets │ ├── index.html │ └── sky.jpg ├── basic │ └── index.html ├── build.js ├── camera-children │ └── index.html ├── index.html ├── main.js └── rotate-to │ └── index.html ├── index.js ├── package-lock.json ├── package.json └── scripts └── unboil.js /.gitignore: -------------------------------------------------------------------------------- 1 | .sw[ponm] 2 | examples/node_modules/ 3 | gh-pages 4 | node_modules/ 5 | npm-debug.log 6 | NPM-INSTRUCTIONS.md 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Kevin Ngo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /NPM-INSTRUCTIONS.md: -------------------------------------------------------------------------------- 1 | ### Update on npm 2 | 3 | Do changes. 4 | Make sure to update README.md to next NPM version. 5 | 6 | run `npm build` 7 | run `npm dist` 8 | 9 | Push changes on git locally. 10 | 11 | bump NPM version, e.g. 12 | `npm version patch -m "Upgrade for reasons"` 13 | 14 | Push git to remote. 15 | Make sure to push tags to remote as well. 16 | 17 | Publish to NPM. 18 | `npm publish` 19 | 20 | --- 21 | 22 | ### Merging a PR 23 | 24 | Review and accept PR on GitHub. 25 | 26 | Pull locally from remote repository. 27 | 28 | Do NPM steps as outline above. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## aframe-orbit-controls-component 2 | 3 | *** 4 | Please use this more performant component instead: https://github.com/ngokevin/kframe/tree/master/components/orbit-controls 5 | *** 6 | 7 | 8 | A (almost) direct port of the ThreeJS Orbit Controls for [A-Frame](https://aframe.io). 9 | It allows users to rotate the camera around an object. Might be useful as a fallback to VR mode. Automatically switches to look-controls in VR mode. 10 | 11 | Have a look at the [examples](https://tizzle.github.io/aframe-orbit-controls-component/) 12 | 13 | ### API 14 | 15 | | Property | Description | Default Value | 16 | | ---------- | ----------- | ------------- | 17 | | enabled | Boolean – defines if the Orbit Controls are used | false 18 | | target | String – the object the camera is looking at and orbits around | '' | 19 | | distance | Number – the distance of the camera to the target | 1 | 20 | | enableRotate | Boolean – defines if the camera can be rotated | true | 21 | | rotateSpeed | Number – rotation speed | 1 | 22 | | enableZoom | Boolean – defines if the camera can be zoomed in or out | true | 23 | | invertZoom | Boolean – defines if zooming is inverted | false | 24 | | zoomSpeed | Number – zoom speed | 1 | 25 | | enablePan | Boolean – defines if the camera can be panned (using the arrow keys) | true | 26 | | keyPanSpeed | Number – panning speed | 7 | 27 | | enableDamping | Boolean – defines if the rotational movement of the camera is damped / eased | false | 28 | | dampingFactor | Number – damping factor | 0.25 | 29 | | autoRotate | Boolean – defines if the camera automatically rotates around the target | false | 30 | | autoRotateSpeed | Number – speed of the automatic rotation | 2 | 31 | | enableKeys | Boolean – defines if the keyboard can be used | true | 32 | | minAzimuthAngle | Number – minimum azimuth angle – Defines how far you can orbit horizontally, lower limit | -Infinity | 33 | | maxAzimuthAngle | Number – maximum azimuth angle – Defines how far you can orbit horizontally, upper limit | Infinity | 34 | | minPolarAngle | Number – minimum polar angle – Defines how far you can orbit vertically, lower limit | 0 | 35 | | maxPolarAngle | Number – maximum polar angle – Defines how far you can orbit vertically, upper limit | Math.PI | 36 | | minZoom | Number – minimum zoom value – Defines how far you can zoom out for Orthographic Cameras | 0 | 37 | | maxZoom | Number – maximum zoom value – Defines how far you can zoom in for Orthographic Cameras | Infinity | 38 | | minDistance | Number – minimum distance – Defines how far you can zoom in for Perspective Cameras | 0 | 39 | | maxDistance | Number – maximum distance – Defines how far you can zoom out for Perspective Cameras | Infinity | 40 | | rotateTo | Vector3 – position to rotate automatically to | {x:0,y:0,z:0} | 41 | | rotateToSpeed | Number – rotateTo speed | 0.05 | 42 | | logPosition | Boolean – prints out camera position to console.log() when rotating | true | 43 | | autoVRLookCam | Boolean - automatically switch over to look-controls in VR mode? | true | 44 | 45 | ### Installation 46 | 47 | #### Browser 48 | 49 | Install and use by directly including the [browser files](dist): 50 | 51 | ```html 52 | 53 | A-Frame using a Camera with Orbit Controls 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | ``` 90 | 91 | 92 | #### npm 93 | 94 | Install via npm: 95 | 96 | ```bash 97 | npm install aframe-orbit-controls-component-2 98 | ``` 99 | 100 | Then register and use. 101 | 102 | ```js 103 | require('aframe'); 104 | require('aframe-orbit-controls-component-2'); 105 | ``` 106 | 107 | Alternatively, include as a ` 110 | ``` 111 | When the user enters VR mode, `orbit-controls` will pause itself and switch to the `look-controls` attached to the same camera. If no `look-controls` is specified on the current camera, one will be created with the default settings (this usually works fine). If you do not want this behaviour (probably becuase you want to control the camera juggling behaviour yourself) just specify `autoVRLookCam:false`. 112 | -------------------------------------------------------------------------------- /dist/.gitkeep: -------------------------------------------------------------------------------- 1 | `npm run dist` to generate browser files. 2 | -------------------------------------------------------------------------------- /dist/aframe-orbit-controls-component.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) 10 | /******/ return installedModules[moduleId].exports; 11 | 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ exports: {}, 15 | /******/ id: moduleId, 16 | /******/ loaded: false 17 | /******/ }; 18 | 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | 22 | /******/ // Flag the module as loaded 23 | /******/ module.loaded = true; 24 | 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | 29 | 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | 36 | /******/ // __webpack_public_path__ 37 | /******/ __webpack_require__.p = ""; 38 | 39 | /******/ // Load entry module and return exports 40 | /******/ return __webpack_require__(0); 41 | /******/ }) 42 | /************************************************************************/ 43 | /******/ ([ 44 | /* 0 */ 45 | /***/ (function(module, exports) { 46 | 47 | /* global AFRAME THREE */ 48 | 49 | if (typeof AFRAME === 'undefined') { 50 | throw new Error('Component attempted to register before AFRAME was available.'); 51 | } 52 | 53 | var radToDeg = THREE.Math.radToDeg; 54 | 55 | /** 56 | * Example component for A-Frame. 57 | */ 58 | AFRAME.registerComponent('orbit-controls', { 59 | dependencies: ['position', 'rotation'], 60 | schema: { 61 | enabled: { 62 | default: true 63 | }, 64 | target: { 65 | default: '' 66 | }, 67 | distance: { 68 | default: 1 69 | }, 70 | enableRotate: { 71 | default: true 72 | }, 73 | rotateSpeed: { 74 | default: 1.0 75 | }, 76 | enableZoom: { 77 | default: true 78 | }, 79 | zoomSpeed: { 80 | default: 1.0 81 | }, 82 | enablePan: { 83 | default: true 84 | }, 85 | keyPanSpeed: { 86 | default: 7.0 87 | }, 88 | enableDamping: { 89 | default: false 90 | }, 91 | dampingFactor: { 92 | default: 0.25 93 | }, 94 | autoRotate: { 95 | default: false 96 | }, 97 | autoRotateSpeed: { 98 | default: 2.0 99 | }, 100 | enableKeys: { 101 | default: true 102 | }, 103 | minAzimuthAngle: { 104 | default: -Infinity 105 | }, 106 | maxAzimuthAngle: { 107 | default: Infinity 108 | }, 109 | minPolarAngle: { 110 | default: 0 111 | }, 112 | maxPolarAngle: { 113 | default: Math.PI 114 | }, 115 | minZoom: { 116 | default: 0 117 | }, 118 | maxZoom: { 119 | default: Infinity 120 | }, 121 | invertZoom: { 122 | default: false 123 | }, 124 | minDistance: { 125 | default: 0 126 | }, 127 | maxDistance: { 128 | default: Infinity 129 | }, 130 | rotateTo: { 131 | type: 'vec3', 132 | default: {x: 0, y: 0, z: 0} 133 | }, 134 | rotateToSpeed: { 135 | type: 'number', 136 | default: 0.05 137 | }, 138 | logPosition: { 139 | type: 'boolean', 140 | default: false 141 | }, 142 | autoVRLookCam: { 143 | type: 'boolean', 144 | default: true 145 | } 146 | }, 147 | 148 | /** 149 | * Set if component needs multiple instancing. 150 | */ 151 | multiple: false, 152 | 153 | /** 154 | * Called once when component is attached. Generally for initial setup. 155 | */ 156 | init: function () { 157 | this.sceneEl = this.el.sceneEl; 158 | this.object = this.el.object3D; 159 | this.target = this.sceneEl.querySelector(this.data.target).object3D.position; 160 | 161 | console.log('enabled: ', this.data.enabled); 162 | 163 | // Find the look-controls component on this camera, or create if it doesn't exist. 164 | this.isRunning = false; 165 | this.lookControls = null; 166 | 167 | if (this.data.autoVRLookCam) { 168 | if (this.el.components['look-controls']) { 169 | this.lookControls = this.el.components['look-controls']; 170 | } else { 171 | this.el.setAttribute('look-controls', ''); 172 | this.lookControls = this.el.components['look-controls']; 173 | } 174 | this.lookControls.pause(); 175 | this.el.sceneEl.addEventListener('enter-vr', this.onEnterVR.bind(this), false); 176 | this.el.sceneEl.addEventListener('exit-vr', this.onExitVR.bind(this), false); 177 | } 178 | 179 | this.dolly = new THREE.Object3D(); 180 | this.dolly.position.copy(this.object.position); 181 | 182 | this.savedPose = null; 183 | 184 | this.STATE = { 185 | NONE: -1, 186 | ROTATE: 0, 187 | DOLLY: 1, 188 | PAN: 2, 189 | TOUCH_ROTATE: 3, 190 | TOUCH_DOLLY: 4, 191 | TOUCH_PAN: 5, 192 | ROTATE_TO: 6 193 | }; 194 | 195 | this.state = this.STATE.NONE; 196 | 197 | this.EPS = 0.000001; 198 | this.lastPosition = new THREE.Vector3(); 199 | this.lastQuaternion = new THREE.Quaternion(); 200 | 201 | this.spherical = new THREE.Spherical(); 202 | this.sphericalDelta = new THREE.Spherical(); 203 | 204 | this.scale = 1.0; 205 | this.zoomChanged = false; 206 | 207 | this.rotateStart = new THREE.Vector2(); 208 | this.rotateEnd = new THREE.Vector2(); 209 | this.rotateDelta = new THREE.Vector2(); 210 | 211 | this.panStart = new THREE.Vector2(); 212 | this.panEnd = new THREE.Vector2(); 213 | this.panDelta = new THREE.Vector2(); 214 | this.panOffset = new THREE.Vector3(); 215 | 216 | this.dollyStart = new THREE.Vector2(); 217 | this.dollyEnd = new THREE.Vector2(); 218 | this.dollyDelta = new THREE.Vector2(); 219 | 220 | this.vector = new THREE.Vector3(); 221 | this.desiredPosition = new THREE.Vector3(); 222 | 223 | this.mouseButtons = { 224 | ORBIT: THREE.MOUSE.LEFT, 225 | ZOOM: THREE.MOUSE.MIDDLE, 226 | PAN: THREE.MOUSE.RIGHT 227 | }; 228 | 229 | this.keys = { 230 | LEFT: 37, 231 | UP: 38, 232 | RIGHT: 39, 233 | BOTTOM: 40 234 | }; 235 | 236 | this.bindMethods(); 237 | }, 238 | 239 | /** 240 | * Called when component is attached and when component data changes. 241 | * Generally modifies the entity based on the data. 242 | */ 243 | update: function (oldData) { 244 | console.log('component update'); 245 | 246 | if (this.data.rotateTo) { 247 | var rotateToVec3 = new THREE.Vector3(this.data.rotateTo.x, this.data.rotateTo.y, this.data.rotateTo.z); 248 | // Check if rotateToVec3 is already desiredPosition 249 | if (!this.desiredPosition.equals(rotateToVec3)) { 250 | this.desiredPosition.copy(rotateToVec3); 251 | this.rotateTo(this.desiredPosition); 252 | } 253 | } 254 | 255 | this.dolly.position.copy(this.object.position); 256 | this.updateView(true); 257 | }, 258 | 259 | /** 260 | * Called when a component is removed (e.g., via removeAttribute). 261 | * Generally undoes all modifications to the entity. 262 | */ 263 | remove: function () { 264 | // console.log("component remove"); 265 | this.isRunning = false; 266 | this.removeEventListeners(); 267 | this.el.sceneEl.removeEventListener('enter-vr', this.onEnterVR, false); 268 | this.el.sceneEl.removeEventListener('exit-vr', this.onExitVR, false); 269 | }, 270 | 271 | /** 272 | * Called on each scene tick. 273 | */ 274 | tick: function (t) { 275 | var render = this.data.enabled && this.isRunning ? this.updateView() : false; 276 | if (render === true && this.data.logPosition === true) { 277 | console.log(this.el.object3D.position); 278 | } 279 | }, 280 | 281 | /* 282 | * Called when entering VR mode 283 | */ 284 | onEnterVR: function (event) { 285 | // console.log('enter vr', this); 286 | 287 | this.saveCameraPose(); 288 | 289 | this.el.setAttribute('position', {x: 0, y: 2, z: 5}); 290 | this.el.setAttribute('rotation', {x: 0, y: 0, z: 0}); 291 | 292 | this.pause(); 293 | this.lookControls.play(); 294 | if (this.data.autoRotate) console.warn('orbit-controls: Sorry, autoRotate is not implemented in VR mode'); 295 | }, 296 | 297 | /* 298 | * Called when exiting VR mode 299 | */ 300 | onExitVR: function (event) { 301 | // console.log('exit vr'); 302 | 303 | this.lookControls.pause(); 304 | this.play(); 305 | 306 | this.restoreCameraPose(); 307 | this.updateView(true); 308 | }, 309 | 310 | /** 311 | * Called when entity pauses. 312 | * Use to stop or remove any dynamic or background behavior such as events. 313 | */ 314 | pause: function () { 315 | // console.log("component pause"); 316 | this.isRunning = false; 317 | this.removeEventListeners(); 318 | }, 319 | 320 | /** 321 | * Called when entity resumes. 322 | * Use to continue or add any dynamic or background behavior such as events. 323 | */ 324 | play: function () { 325 | // console.log("component play"); 326 | this.isRunning = true; 327 | 328 | var camera, cameraType; 329 | this.object.traverse(function (child) { 330 | if (child instanceof THREE.PerspectiveCamera) { 331 | camera = child; 332 | cameraType = 'PerspectiveCamera'; 333 | } else if (child instanceof THREE.OrthographicCamera) { 334 | camera = child; 335 | cameraType = 'OrthographicCamera'; 336 | } 337 | }); 338 | 339 | this.camera = camera; 340 | this.cameraType = cameraType; 341 | 342 | this.sceneEl.addEventListener('renderstart', this.onRenderTargetLoaded, false); 343 | 344 | if (this.lookControls) this.lookControls.pause(); 345 | if (this.canvasEl) this.addEventListeners(); 346 | }, 347 | 348 | /* 349 | * Called when Render Target is completely loaded 350 | * Then set canvasEl and add event listeners 351 | */ 352 | onRenderTargetLoaded: function () { 353 | this.sceneEl.removeEventListener('renderstart', this.onRenderTargetLoaded, false); 354 | this.canvasEl = this.sceneEl.canvas; 355 | this.addEventListeners(); 356 | }, 357 | 358 | /* 359 | * Bind this to all event handlera 360 | */ 361 | bindMethods: function () { 362 | this.onRenderTargetLoaded = this.onRenderTargetLoaded.bind(this); 363 | 364 | this.onContextMenu = this.onContextMenu.bind(this); 365 | this.onMouseDown = this.onMouseDown.bind(this); 366 | this.onMouseWheel = this.onMouseWheel.bind(this); 367 | this.onMouseMove = this.onMouseMove.bind(this); 368 | this.onMouseUp = this.onMouseUp.bind(this); 369 | this.onTouchStart = this.onTouchStart.bind(this); 370 | this.onTouchMove = this.onTouchMove.bind(this); 371 | this.onTouchEnd = this.onTouchEnd.bind(this); 372 | this.onKeyDown = this.onKeyDown.bind(this); 373 | }, 374 | 375 | /* 376 | * Add event listeners 377 | */ 378 | addEventListeners: function () { 379 | this.canvasEl.addEventListener('contextmenu', this.onContextMenu, false); 380 | 381 | this.canvasEl.addEventListener('mousedown', this.onMouseDown, false); 382 | this.canvasEl.addEventListener('mousewheel', this.onMouseWheel, false); 383 | this.canvasEl.addEventListener('MozMousePixelScroll', this.onMouseWheel, false); // firefox 384 | 385 | this.canvasEl.addEventListener('touchstart', this.onTouchStart, false); 386 | this.canvasEl.addEventListener('touchend', this.onTouchEnd, false); 387 | this.canvasEl.addEventListener('touchmove', this.onTouchMove, false); 388 | 389 | window.addEventListener('keydown', this.onKeyDown, false); 390 | }, 391 | 392 | /* 393 | * Remove event listeners 394 | */ 395 | removeEventListeners: function () { 396 | 397 | if(this.canvasEl){ 398 | this.canvasEl.removeEventListener('contextmenu', this.onContextMenu, false); 399 | this.canvasEl.removeEventListener('mousedown', this.onMouseDown, false); 400 | this.canvasEl.removeEventListener('mousewheel', this.onMouseWheel, false); 401 | this.canvasEl.removeEventListener('MozMousePixelScroll', this.onMouseWheel, false); // firefox 402 | 403 | this.canvasEl.removeEventListener('touchstart', this.onTouchStart, false); 404 | this.canvasEl.removeEventListener('touchend', this.onTouchEnd, false); 405 | this.canvasEl.removeEventListener('touchmove', this.onTouchMove, false); 406 | 407 | this.canvasEl.removeEventListener('mousemove', this.onMouseMove, false); 408 | this.canvasEl.removeEventListener('mouseup', this.onMouseUp, false); 409 | this.canvasEl.removeEventListener('mouseout', this.onMouseUp, false); 410 | } 411 | 412 | window.removeEventListener('keydown', this.onKeyDown, false); 413 | }, 414 | 415 | /* 416 | * EVENT LISTENERS 417 | */ 418 | 419 | /* 420 | * Called when right clicking the A-Frame component 421 | */ 422 | 423 | onContextMenu: function (event) { 424 | event.preventDefault(); 425 | }, 426 | 427 | /* 428 | * MOUSE CLICK EVENT LISTENERS 429 | */ 430 | 431 | onMouseDown: function (event) { 432 | // console.log('onMouseDown'); 433 | 434 | if (!this.data.enabled || !this.isRunning) return; 435 | 436 | if (event.button === this.mouseButtons.ORBIT && (event.shiftKey || event.ctrlKey)) { 437 | if (this.data.enablePan === false) return; 438 | this.handleMouseDownPan(event); 439 | this.state = this.STATE.PAN; 440 | } else if (event.button === this.mouseButtons.ORBIT) { 441 | this.panOffset.set(0, 0, 0); 442 | if (this.data.enableRotate === false) return; 443 | this.handleMouseDownRotate(event); 444 | this.state = this.STATE.ROTATE; 445 | } else if (event.button === this.mouseButtons.ZOOM) { 446 | this.panOffset.set(0, 0, 0); 447 | if (this.data.enableZoom === false) return; 448 | this.handleMouseDownDolly(event); 449 | this.state = this.STATE.DOLLY; 450 | } else if (event.button === this.mouseButtons.PAN) { 451 | if (this.data.enablePan === false) return; 452 | this.handleMouseDownPan(event); 453 | this.state = this.STATE.PAN; 454 | } 455 | 456 | if (this.state !== this.STATE.NONE) { 457 | this.canvasEl.addEventListener('mousemove', this.onMouseMove, false); 458 | this.canvasEl.addEventListener('mouseup', this.onMouseUp, false); 459 | this.canvasEl.addEventListener('mouseout', this.onMouseUp, false); 460 | 461 | this.el.emit('start-drag-orbit-controls', null, false); 462 | } 463 | }, 464 | 465 | onMouseMove: function (event) { 466 | // console.log('onMouseMove'); 467 | 468 | if (!this.data.enabled || !this.isRunning) return; 469 | 470 | event.preventDefault(); 471 | 472 | if (this.state === this.STATE.ROTATE) { 473 | if (this.data.enableRotate === false) return; 474 | this.handleMouseMoveRotate(event); 475 | } else if (this.state === this.STATE.DOLLY) { 476 | if (this.data.enableZoom === false) return; 477 | this.handleMouseMoveDolly(event); 478 | } else if (this.state === this.STATE.PAN) { 479 | if (this.data.enablePan === false) return; 480 | this.handleMouseMovePan(event); 481 | } 482 | }, 483 | 484 | onMouseUp: function (event) { 485 | // console.log('onMouseUp'); 486 | 487 | if (!this.data.enabled || !this.isRunning) return; 488 | 489 | if (this.state === this.STATE.ROTATE_TO) return; 490 | 491 | event.preventDefault(); 492 | event.stopPropagation(); 493 | 494 | this.handleMouseUp(event); 495 | 496 | this.canvasEl.removeEventListener('mousemove', this.onMouseMove, false); 497 | this.canvasEl.removeEventListener('mouseup', this.onMouseUp, false); 498 | this.canvasEl.removeEventListener('mouseout', this.onMouseUp, false); 499 | 500 | this.state = this.STATE.NONE; 501 | 502 | this.el.emit('end-drag-orbit-controls', null, false); 503 | }, 504 | 505 | /* 506 | * MOUSE WHEEL EVENT LISTENERS 507 | */ 508 | 509 | onMouseWheel: function (event) { 510 | // console.log('onMouseWheel'); 511 | 512 | if (!this.data.enabled || !this.isRunning || this.data.enableZoom === false || (this.state !== this.STATE.NONE && this.state !== this.STATE.ROTATE)) return; 513 | 514 | event.preventDefault(); 515 | event.stopPropagation(); 516 | this.handleMouseWheel(event); 517 | }, 518 | 519 | /* 520 | * TOUCH EVENT LISTENERS 521 | */ 522 | 523 | onTouchStart: function (event) { 524 | // console.log('onTouchStart'); 525 | 526 | if (!this.data.enabled || !this.isRunning) return; 527 | 528 | switch (event.touches.length) { 529 | case 1: // one-fingered touch: rotate 530 | if (this.data.enableRotate === false) return; 531 | this.handleTouchStartRotate(event); 532 | this.state = this.STATE.TOUCH_ROTATE; 533 | break; 534 | case 2: // two-fingered touch: dolly 535 | if (this.data.enableZoom === false) return; 536 | this.handleTouchStartDolly(event); 537 | this.state = this.STATE.TOUCH_DOLLY; 538 | break; 539 | case 3: // three-fingered touch: pan 540 | if (this.data.enablePan === false) return; 541 | this.handleTouchStartPan(event); 542 | this.state = this.STATE.TOUCH_PAN; 543 | break; 544 | default: 545 | this.state = this.STATE.NONE; 546 | } 547 | 548 | if (this.state !== this.STATE.NONE) { 549 | this.el.emit('start-drag-orbit-controls', null, false); 550 | } 551 | }, 552 | 553 | onTouchMove: function (event) { 554 | // console.log('onTouchMove'); 555 | 556 | if (!this.data.enabled || !this.isRunning) return; 557 | 558 | event.preventDefault(); 559 | event.stopPropagation(); 560 | 561 | switch (event.touches.length) { 562 | case 1: // one-fingered touch: rotate 563 | if (this.enableRotate === false) return; 564 | if (this.state !== this.STATE.TOUCH_ROTATE) return; // is this needed?... 565 | this.handleTouchMoveRotate(event); 566 | break; 567 | 568 | case 2: // two-fingered touch: dolly 569 | if (this.data.enableZoom === false) return; 570 | if (this.state !== this.STATE.TOUCH_DOLLY) return; // is this needed?... 571 | this.handleTouchMoveDolly(event); 572 | break; 573 | 574 | case 3: // three-fingered touch: pan 575 | if (this.data.enablePan === false) return; 576 | if (this.state !== this.STATE.TOUCH_PAN) return; // is this needed?... 577 | this.handleTouchMovePan(event); 578 | break; 579 | 580 | default: 581 | this.state = this.STATE.NONE; 582 | } 583 | }, 584 | 585 | onTouchEnd: function (event) { 586 | // console.log('onTouchEnd'); 587 | 588 | if (!this.data.enabled || !this.isRunning) return; 589 | 590 | this.handleTouchEnd(event); 591 | 592 | this.el.emit('end-drag-orbit-controls', null, false); 593 | 594 | this.state = this.STATE.NONE; 595 | }, 596 | 597 | /* 598 | * KEYBOARD EVENT LISTENERS 599 | */ 600 | 601 | onKeyDown: function (event) { 602 | // console.log('onKeyDown'); 603 | 604 | if (!this.data.enabled || !this.isRunning || this.data.enableKeys === false || this.data.enablePan === false) return; 605 | 606 | this.handleKeyDown(event); 607 | }, 608 | 609 | /* 610 | * EVENT HANDLERS 611 | */ 612 | 613 | /* 614 | * MOUSE CLICK EVENT HANDLERS 615 | */ 616 | 617 | handleMouseDownRotate: function (event) { 618 | // console.log( 'handleMouseDownRotate' ); 619 | this.rotateStart.set(event.clientX, event.clientY); 620 | }, 621 | 622 | handleMouseDownDolly: function (event) { 623 | // console.log( 'handleMouseDownDolly' ); 624 | this.dollyStart.set(event.clientX, event.clientY); 625 | }, 626 | 627 | handleMouseDownPan: function (event) { 628 | // console.log( 'handleMouseDownPan' ); 629 | this.panStart.set(event.clientX, event.clientY); 630 | }, 631 | 632 | handleMouseMoveRotate: function (event) { 633 | // console.log( 'handleMouseMoveRotate' ); 634 | 635 | this.rotateEnd.set(event.clientX, event.clientY); 636 | this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart); 637 | 638 | var canvas = this.canvasEl === document ? this.canvasEl.body : this.canvasEl; 639 | 640 | // rotating across whole screen goes 360 degrees around 641 | this.rotateLeft(2 * Math.PI * this.rotateDelta.x / canvas.clientWidth * this.data.rotateSpeed); 642 | 643 | // rotating up and down along whole screen attempts to go 360, but limited to 180 644 | this.rotateUp(2 * Math.PI * this.rotateDelta.y / canvas.clientHeight * this.data.rotateSpeed); 645 | 646 | this.rotateStart.copy(this.rotateEnd); 647 | 648 | this.updateView(); 649 | }, 650 | 651 | handleMouseMoveDolly: function (event) { 652 | // console.log( 'handleMouseMoveDolly' ); 653 | 654 | this.dollyEnd.set(event.clientX, event.clientY); 655 | this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart); 656 | 657 | if (this.dollyDelta.y > 0) { 658 | !this.data.invertZoom ? this.dollyIn(this.getZoomScale()) : this.dollyOut(this.getZoomScale()); 659 | } else if (this.dollyDelta.y < 0) { 660 | !this.data.invertZoom ? this.dollyOut(this.getZoomScale()) : this.dollyIn(this.getZoomScale()); 661 | } 662 | 663 | this.dollyStart.copy(this.dollyEnd); 664 | 665 | this.updateView(); 666 | }, 667 | 668 | handleMouseMovePan: function (event) { 669 | // console.log( 'handleMouseMovePan' ); 670 | 671 | this.panEnd.set(event.clientX, event.clientY); 672 | this.panDelta.subVectors(this.panEnd, this.panStart); 673 | this.pan(this.panDelta.x, this.panDelta.y); 674 | this.panStart.copy(this.panEnd); 675 | 676 | this.updateView(); 677 | }, 678 | 679 | handleMouseUp: function (event) { 680 | // console.log( 'handleMouseUp' ); 681 | }, 682 | 683 | /* 684 | * MOUSE WHEEL EVENT HANDLERS 685 | */ 686 | 687 | handleMouseWheel: function (event) { 688 | // console.log( 'handleMouseWheel' ); 689 | 690 | var delta = 0; 691 | if (event.wheelDelta !== undefined) { 692 | // WebKit / Opera / Explorer 9 693 | delta = event.wheelDelta; 694 | } else if (event.detail !== undefined) { 695 | // Firefox 696 | delta = -event.detail; 697 | } 698 | 699 | if (delta > 0) { 700 | !this.data.invertZoom ? this.dollyOut(this.getZoomScale()) : this.dollyIn(this.getZoomScale()); 701 | } else if (delta < 0) { 702 | !this.data.invertZoom ? this.dollyIn(this.getZoomScale()) : this.dollyOut(this.getZoomScale()); 703 | } 704 | 705 | this.updateView(); 706 | }, 707 | 708 | /* 709 | * TOUCH EVENT HANDLERS 710 | */ 711 | 712 | handleTouchStartRotate: function (event) { 713 | // console.log( 'handleTouchStartRotate' ); 714 | 715 | this.rotateStart.set(event.touches[0].pageX, event.touches[0].pageY); 716 | }, 717 | 718 | handleTouchStartDolly: function (event) { 719 | // console.log( 'handleTouchStartDolly' ); 720 | 721 | var dx = event.touches[0].pageX - event.touches[1].pageX; 722 | var dy = event.touches[0].pageY - event.touches[1].pageY; 723 | var distance = Math.sqrt(dx * dx + dy * dy); 724 | this.dollyStart.set(0, distance); 725 | }, 726 | 727 | handleTouchStartPan: function (event) { 728 | // console.log( 'handleTouchStartPan' ); 729 | 730 | this.panStart.set(event.touches[0].pageX, event.touches[0].pageY); 731 | }, 732 | 733 | handleTouchMoveRotate: function (event) { 734 | // console.log( 'handleTouchMoveRotate' ); 735 | 736 | this.rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY); 737 | this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart); 738 | 739 | var canvas = this.canvasEl === document ? this.canvasEl.body : this.canvasEl; 740 | // rotating across whole screen goes 360 degrees around 741 | this.rotateLeft(2 * Math.PI * this.rotateDelta.x / canvas.clientWidth * this.data.rotateSpeed); 742 | // rotating up and down along whole screen attempts to go 360, but limited to 180 743 | this.rotateUp(2 * Math.PI * this.rotateDelta.y / canvas.clientHeight * this.data.rotateSpeed); 744 | this.rotateStart.copy(this.rotateEnd); 745 | this.updateView(); 746 | }, 747 | 748 | handleTouchMoveDolly: function (event) { 749 | // console.log( 'handleTouchMoveDolly' ); 750 | 751 | var dx = event.touches[0].pageX - event.touches[1].pageX; 752 | var dy = event.touches[0].pageY - event.touches[1].pageY; 753 | 754 | var distance = Math.sqrt(dx * dx + dy * dy); 755 | 756 | this.dollyEnd.set(0, distance); 757 | this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart); 758 | if (this.dollyDelta.y > 0) { 759 | this.dollyIn(this.getZoomScale()); 760 | } else if (this.dollyDelta.y < 0) { 761 | this.dollyOut(this.getZoomScale()); 762 | } 763 | 764 | this.dollyStart.copy(this.dollyEnd); 765 | this.updateView(); 766 | }, 767 | 768 | handleTouchMovePan: function (event) { 769 | // console.log( 'handleTouchMovePan' ); 770 | 771 | this.panEnd.set(event.touches[0].pageX, event.touches[0].pageY); 772 | this.panDelta.subVectors(this.panEnd, this.panStart); 773 | this.pan(this.panDelta.x, this.panDelta.y); 774 | this.panStart.copy(this.panEnd); 775 | this.updateView(); 776 | }, 777 | 778 | handleTouchEnd: function (event) { 779 | // console.log( 'handleTouchEnd' ); 780 | }, 781 | 782 | /* 783 | * KEYBOARD EVENT HANDLERS 784 | */ 785 | 786 | handleKeyDown: function (event) { 787 | // console.log( 'handleKeyDown' ); 788 | 789 | switch (event.keyCode) { 790 | case this.keys.UP: 791 | this.pan(0, this.data.keyPanSpeed); 792 | this.updateView(); 793 | break; 794 | case this.keys.BOTTOM: 795 | this.pan(0, -this.data.keyPanSpeed); 796 | this.updateView(); 797 | break; 798 | case this.keys.LEFT: 799 | this.pan(this.data.keyPanSpeed, 0); 800 | this.updateView(); 801 | break; 802 | case this.keys.RIGHT: 803 | this.pan(-this.data.keyPanSpeed, 0); 804 | this.updateView(); 805 | break; 806 | } 807 | }, 808 | 809 | /* 810 | * HELPER FUNCTIONS 811 | */ 812 | 813 | getAutoRotationAngle: function () { 814 | return 2 * Math.PI / 60 / 60 * this.data.autoRotateSpeed; 815 | }, 816 | 817 | getZoomScale: function () { 818 | return Math.pow(0.95, this.data.zoomSpeed); 819 | }, 820 | 821 | rotateLeft: function (angle) { 822 | this.sphericalDelta.theta -= angle; 823 | }, 824 | 825 | rotateUp: function (angle) { 826 | this.sphericalDelta.phi -= angle; 827 | }, 828 | 829 | rotateTo: function (vec3) { 830 | this.state = this.STATE.ROTATE_TO; 831 | this.desiredPosition.copy(vec3); 832 | }, 833 | 834 | panHorizontally: function (distance, objectMatrix) { 835 | // console.log('pan horizontally', distance, objectMatrix); 836 | var v = new THREE.Vector3(); 837 | v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix 838 | v.multiplyScalar(-distance); 839 | this.panOffset.add(v); 840 | }, 841 | 842 | panVertically: function (distance, objectMatrix) { 843 | // console.log('pan vertically', distance, objectMatrix); 844 | var v = new THREE.Vector3(); 845 | v.setFromMatrixColumn(objectMatrix, 1); // get Y column of objectMatrix 846 | v.multiplyScalar(distance); 847 | this.panOffset.add(v); 848 | }, 849 | 850 | pan: function (deltaX, deltaY) { // deltaX and deltaY are in pixels; right and down are positive 851 | // console.log('panning', deltaX, deltaY ); 852 | var offset = new THREE.Vector3(); 853 | var canvas = this.canvasEl === document ? this.canvasEl.body : this.canvasEl; 854 | 855 | if (this.cameraType === 'PerspectiveCamera') { 856 | // perspective 857 | var position = this.dolly.position; 858 | offset.copy(position).sub(this.target); 859 | var targetDistance = offset.length(); 860 | targetDistance *= Math.tan((this.camera.fov / 2) * Math.PI / 180.0); // half of the fov is center to top of screen 861 | this.panHorizontally(2 * deltaX * targetDistance / canvas.clientHeight, this.object.matrix); // we actually don't use screenWidth, since perspective camera is fixed to screen height 862 | this.panVertically(2 * deltaY * targetDistance / canvas.clientHeight, this.object.matrix); 863 | } else if (this.cameraType === 'OrthographicCamera') { 864 | // orthographic 865 | this.panHorizontally(deltaX * (this.dolly.right - this.dolly.left) / this.camera.zoom / canvas.clientWidth, this.object.matrix); 866 | this.panVertically(deltaY * (this.dolly.top - this.dolly.bottom) / this.camera.zoom / canvas.clientHeight, this.object.matrix); 867 | } else { 868 | // camera neither orthographic nor perspective 869 | console.warn('Trying to pan: WARNING: Orbit Controls encountered an unknown camera type - pan disabled.'); 870 | this.data.enablePan = false; 871 | } 872 | }, 873 | 874 | dollyIn: function (dollyScale) { 875 | // console.log( "dollyIn camera" ); 876 | if (this.cameraType === 'PerspectiveCamera') { 877 | this.scale *= dollyScale; 878 | } else if (this.cameraType === 'OrthographicCamera') { 879 | this.camera.zoom = Math.max(this.data.minZoom, Math.min(this.data.maxZoom, this.camera.zoom * dollyScale)); 880 | this.camera.updateProjectionMatrix(); 881 | this.zoomChanged = true; 882 | } else { 883 | console.warn('Trying to dolly in: WARNING: Orbit Controls encountered an unknown camera type - dolly/zoom disabled.'); 884 | this.data.enableZoom = false; 885 | } 886 | }, 887 | 888 | dollyOut: function (dollyScale) { 889 | // console.log( "dollyOut camera" ); 890 | if (this.cameraType === 'PerspectiveCamera') { 891 | this.scale /= dollyScale; 892 | } else if (this.cameraType === 'OrthographicCamera') { 893 | this.camera.zoom = Math.max(this.data.minZoom, Math.min(this.data.maxZoom, this.camera.zoom / dollyScale)); 894 | this.camera.updateProjectionMatrix(); 895 | this.zoomChanged = true; 896 | } else { 897 | console.warn('Trying to dolly out: WARNING: Orbit Controls encountered an unknown camera type - dolly/zoom disabled.'); 898 | this.data.enableZoom = false; 899 | } 900 | }, 901 | 902 | lookAtTarget: function (object, target) { 903 | var v = new THREE.Vector3(); 904 | v.subVectors(object.position, target).add(object.position); 905 | object.lookAt(v); 906 | }, 907 | 908 | /* 909 | * SAVES CAMERA POSE (WHEN ENTERING VR) 910 | */ 911 | 912 | saveCameraPose: function () { 913 | if (this.savedPose) { return; } 914 | this.savedPose = { 915 | position: this.dolly.position, 916 | rotation: this.dolly.rotation 917 | }; 918 | }, 919 | 920 | /* 921 | * RESTORE CAMERA POSE (WHEN EXITING VR) 922 | */ 923 | 924 | restoreCameraPose: function () { 925 | if (!this.savedPose) { return; } 926 | this.dolly.position.copy(this.savedPose.position); 927 | this.dolly.rotation.copy(this.savedPose.rotation); 928 | this.savedPose = null; 929 | }, 930 | 931 | /* 932 | * VIEW UPDATE 933 | */ 934 | 935 | updateView: function (forceUpdate) { 936 | if (this.desiredPosition && this.state === this.STATE.ROTATE_TO) { 937 | var desiredSpherical = new THREE.Spherical(); 938 | desiredSpherical.setFromVector3(this.desiredPosition); 939 | var phiDiff = desiredSpherical.phi - this.spherical.phi; 940 | var thetaDiff = desiredSpherical.theta - this.spherical.theta; 941 | this.sphericalDelta.set(this.spherical.radius, phiDiff * this.data.rotateToSpeed, thetaDiff * this.data.rotateToSpeed); 942 | } 943 | 944 | var offset = new THREE.Vector3(); 945 | 946 | var quat = new THREE.Quaternion().setFromUnitVectors(this.dolly.up, new THREE.Vector3(0, 1, 0)); // so camera.up is the orbit axis 947 | var quatInverse = quat.clone().inverse(); 948 | 949 | offset.copy(this.dolly.position).sub(this.target); 950 | offset.applyQuaternion(quat); // rotate offset to "y-axis-is-up" space 951 | this.spherical.setFromVector3(offset); // angle from z-axis around y-axis 952 | 953 | if (this.data.autoRotate && this.state === this.STATE.NONE) this.rotateLeft(this.getAutoRotationAngle()); 954 | 955 | this.spherical.theta += this.sphericalDelta.theta; 956 | this.spherical.phi += this.sphericalDelta.phi; 957 | this.spherical.theta = Math.max(this.data.minAzimuthAngle, Math.min(this.data.maxAzimuthAngle, this.spherical.theta)); // restrict theta to be inside desired limits 958 | this.spherical.phi = Math.max(this.data.minPolarAngle, Math.min(this.data.maxPolarAngle, this.spherical.phi)); // restrict phi to be inside desired limits 959 | this.spherical.makeSafe(); 960 | this.spherical.radius *= this.scale; 961 | this.spherical.radius = Math.max(this.data.minDistance, Math.min(this.data.maxDistance, this.spherical.radius)); // restrict radius to be inside desired limits 962 | 963 | this.target.add(this.panOffset); // move target to panned location 964 | 965 | offset.setFromSpherical(this.spherical); 966 | offset.applyQuaternion(quatInverse); // rotate offset back to "camera-up-vector-is-up" space 967 | 968 | this.dolly.position.copy(this.target).add(offset); 969 | 970 | if (this.target) { 971 | this.lookAtTarget(this.dolly, this.target); 972 | } 973 | 974 | if (this.data.enableDamping === true) { 975 | this.sphericalDelta.theta *= (1 - this.data.dampingFactor); 976 | this.sphericalDelta.phi *= (1 - this.data.dampingFactor); 977 | } else { 978 | this.sphericalDelta.set(0, 0, 0); 979 | } 980 | 981 | this.scale = 1; 982 | this.panOffset.set(0, 0, 0); 983 | 984 | // update condition is: 985 | // min(camera displacement, camera rotation in radians)^2 > EPS 986 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 987 | 988 | if (forceUpdate === true || 989 | this.zoomChanged || 990 | this.lastPosition.distanceToSquared(this.dolly.position) > this.EPS || 991 | 8 * (1 - this.lastQuaternion.dot(this.dolly.quaternion)) > this.EPS) { 992 | // this.el.emit('change-drag-orbit-controls', null, false); 993 | 994 | var hmdQuaternion = this.calculateHMDQuaternion(); 995 | var hmdEuler = new THREE.Euler(); 996 | hmdEuler.setFromQuaternion(hmdQuaternion, 'YXZ'); 997 | 998 | this.el.setAttribute('position', { 999 | x: this.dolly.position.x, 1000 | y: this.dolly.position.y, 1001 | z: this.dolly.position.z 1002 | }); 1003 | 1004 | this.el.setAttribute('rotation', { 1005 | x: radToDeg(hmdEuler.x), 1006 | y: radToDeg(hmdEuler.y), 1007 | z: radToDeg(hmdEuler.z) 1008 | }); 1009 | 1010 | this.lastPosition.copy(this.dolly.position); 1011 | this.lastQuaternion.copy(this.dolly.quaternion); 1012 | 1013 | this.zoomChanged = false; 1014 | 1015 | return true; 1016 | } 1017 | 1018 | return false; 1019 | }, 1020 | 1021 | calculateHMDQuaternion: (function () { 1022 | var hmdQuaternion = new THREE.Quaternion(); 1023 | return function () { 1024 | hmdQuaternion.copy(this.dolly.quaternion); 1025 | return hmdQuaternion; 1026 | }; 1027 | })() 1028 | 1029 | }); 1030 | 1031 | 1032 | /***/ }) 1033 | /******/ ]); -------------------------------------------------------------------------------- /dist/aframe-orbit-controls-component.min.js: -------------------------------------------------------------------------------- 1 | !function(t){function e(i){if(s[i])return s[i].exports;var a=s[i]={exports:{},id:i,loaded:!1};return t[i].call(a.exports,a,a.exports,e),a.loaded=!0,a.exports}var s={};return e.m=t,e.c=s,e.p="",e(0)}([function(t,e){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");var s=THREE.Math.radToDeg;AFRAME.registerComponent("orbit-controls",{dependencies:["position","rotation"],schema:{enabled:{default:!0},target:{default:""},distance:{default:1},enableRotate:{default:!0},rotateSpeed:{default:1},enableZoom:{default:!0},zoomSpeed:{default:1},enablePan:{default:!0},keyPanSpeed:{default:7},enableDamping:{default:!1},dampingFactor:{default:.25},autoRotate:{default:!1},autoRotateSpeed:{default:2},enableKeys:{default:!0},minAzimuthAngle:{default:-(1/0)},maxAzimuthAngle:{default:1/0},minPolarAngle:{default:0},maxPolarAngle:{default:Math.PI},minZoom:{default:0},maxZoom:{default:1/0},invertZoom:{default:!1},minDistance:{default:0},maxDistance:{default:1/0},rotateTo:{type:"vec3",default:{x:0,y:0,z:0}},rotateToSpeed:{type:"number",default:.05},logPosition:{type:"boolean",default:!1},autoVRLookCam:{type:"boolean",default:!0}},multiple:!1,init:function(){this.sceneEl=this.el.sceneEl,this.object=this.el.object3D,this.target=this.sceneEl.querySelector(this.data.target).object3D.position,console.log("enabled: ",this.data.enabled),this.isRunning=!1,this.lookControls=null,this.data.autoVRLookCam&&(this.el.components["look-controls"]?this.lookControls=this.el.components["look-controls"]:(this.el.setAttribute("look-controls",""),this.lookControls=this.el.components["look-controls"]),this.lookControls.pause(),this.el.sceneEl.addEventListener("enter-vr",this.onEnterVR.bind(this),!1),this.el.sceneEl.addEventListener("exit-vr",this.onExitVR.bind(this),!1)),this.dolly=new THREE.Object3D,this.dolly.position.copy(this.object.position),this.savedPose=null,this.STATE={NONE:-1,ROTATE:0,DOLLY:1,PAN:2,TOUCH_ROTATE:3,TOUCH_DOLLY:4,TOUCH_PAN:5,ROTATE_TO:6},this.state=this.STATE.NONE,this.EPS=1e-6,this.lastPosition=new THREE.Vector3,this.lastQuaternion=new THREE.Quaternion,this.spherical=new THREE.Spherical,this.sphericalDelta=new THREE.Spherical,this.scale=1,this.zoomChanged=!1,this.rotateStart=new THREE.Vector2,this.rotateEnd=new THREE.Vector2,this.rotateDelta=new THREE.Vector2,this.panStart=new THREE.Vector2,this.panEnd=new THREE.Vector2,this.panDelta=new THREE.Vector2,this.panOffset=new THREE.Vector3,this.dollyStart=new THREE.Vector2,this.dollyEnd=new THREE.Vector2,this.dollyDelta=new THREE.Vector2,this.vector=new THREE.Vector3,this.desiredPosition=new THREE.Vector3,this.mouseButtons={ORBIT:THREE.MOUSE.LEFT,ZOOM:THREE.MOUSE.MIDDLE,PAN:THREE.MOUSE.RIGHT},this.keys={LEFT:37,UP:38,RIGHT:39,BOTTOM:40},this.bindMethods()},update:function(t){if(console.log("component update"),this.data.rotateTo){var e=new THREE.Vector3(this.data.rotateTo.x,this.data.rotateTo.y,this.data.rotateTo.z);this.desiredPosition.equals(e)||(this.desiredPosition.copy(e),this.rotateTo(this.desiredPosition))}this.dolly.position.copy(this.object.position),this.updateView(!0)},remove:function(){this.isRunning=!1,this.removeEventListeners(),this.el.sceneEl.removeEventListener("enter-vr",this.onEnterVR,!1),this.el.sceneEl.removeEventListener("exit-vr",this.onExitVR,!1)},tick:function(t){var e=!(!this.data.enabled||!this.isRunning)&&this.updateView();e===!0&&this.data.logPosition===!0&&console.log(this.el.object3D.position)},onEnterVR:function(t){this.saveCameraPose(),this.el.setAttribute("position",{x:0,y:2,z:5}),this.el.setAttribute("rotation",{x:0,y:0,z:0}),this.pause(),this.lookControls.play(),this.data.autoRotate&&console.warn("orbit-controls: Sorry, autoRotate is not implemented in VR mode")},onExitVR:function(t){this.lookControls.pause(),this.play(),this.restoreCameraPose(),this.updateView(!0)},pause:function(){this.isRunning=!1,this.removeEventListeners()},play:function(){this.isRunning=!0;var t,e;this.object.traverse(function(s){s instanceof THREE.PerspectiveCamera?(t=s,e="PerspectiveCamera"):s instanceof THREE.OrthographicCamera&&(t=s,e="OrthographicCamera")}),this.camera=t,this.cameraType=e,this.sceneEl.addEventListener("renderstart",this.onRenderTargetLoaded,!1),this.lookControls&&this.lookControls.pause(),this.canvasEl&&this.addEventListeners()},onRenderTargetLoaded:function(){this.sceneEl.removeEventListener("renderstart",this.onRenderTargetLoaded,!1),this.canvasEl=this.sceneEl.canvas,this.addEventListeners()},bindMethods:function(){this.onRenderTargetLoaded=this.onRenderTargetLoaded.bind(this),this.onContextMenu=this.onContextMenu.bind(this),this.onMouseDown=this.onMouseDown.bind(this),this.onMouseWheel=this.onMouseWheel.bind(this),this.onMouseMove=this.onMouseMove.bind(this),this.onMouseUp=this.onMouseUp.bind(this),this.onTouchStart=this.onTouchStart.bind(this),this.onTouchMove=this.onTouchMove.bind(this),this.onTouchEnd=this.onTouchEnd.bind(this),this.onKeyDown=this.onKeyDown.bind(this)},addEventListeners:function(){this.canvasEl.addEventListener("contextmenu",this.onContextMenu,!1),this.canvasEl.addEventListener("mousedown",this.onMouseDown,!1),this.canvasEl.addEventListener("mousewheel",this.onMouseWheel,!1),this.canvasEl.addEventListener("MozMousePixelScroll",this.onMouseWheel,!1),this.canvasEl.addEventListener("touchstart",this.onTouchStart,!1),this.canvasEl.addEventListener("touchend",this.onTouchEnd,!1),this.canvasEl.addEventListener("touchmove",this.onTouchMove,!1),window.addEventListener("keydown",this.onKeyDown,!1)},removeEventListeners:function(){this.canvasEl&&(this.canvasEl.removeEventListener("contextmenu",this.onContextMenu,!1),this.canvasEl.removeEventListener("mousedown",this.onMouseDown,!1),this.canvasEl.removeEventListener("mousewheel",this.onMouseWheel,!1),this.canvasEl.removeEventListener("MozMousePixelScroll",this.onMouseWheel,!1),this.canvasEl.removeEventListener("touchstart",this.onTouchStart,!1),this.canvasEl.removeEventListener("touchend",this.onTouchEnd,!1),this.canvasEl.removeEventListener("touchmove",this.onTouchMove,!1),this.canvasEl.removeEventListener("mousemove",this.onMouseMove,!1),this.canvasEl.removeEventListener("mouseup",this.onMouseUp,!1),this.canvasEl.removeEventListener("mouseout",this.onMouseUp,!1)),window.removeEventListener("keydown",this.onKeyDown,!1)},onContextMenu:function(t){t.preventDefault()},onMouseDown:function(t){if(this.data.enabled&&this.isRunning){if(t.button===this.mouseButtons.ORBIT&&(t.shiftKey||t.ctrlKey)){if(this.data.enablePan===!1)return;this.handleMouseDownPan(t),this.state=this.STATE.PAN}else if(t.button===this.mouseButtons.ORBIT){if(this.panOffset.set(0,0,0),this.data.enableRotate===!1)return;this.handleMouseDownRotate(t),this.state=this.STATE.ROTATE}else if(t.button===this.mouseButtons.ZOOM){if(this.panOffset.set(0,0,0),this.data.enableZoom===!1)return;this.handleMouseDownDolly(t),this.state=this.STATE.DOLLY}else if(t.button===this.mouseButtons.PAN){if(this.data.enablePan===!1)return;this.handleMouseDownPan(t),this.state=this.STATE.PAN}this.state!==this.STATE.NONE&&(this.canvasEl.addEventListener("mousemove",this.onMouseMove,!1),this.canvasEl.addEventListener("mouseup",this.onMouseUp,!1),this.canvasEl.addEventListener("mouseout",this.onMouseUp,!1),this.el.emit("start-drag-orbit-controls",null,!1))}},onMouseMove:function(t){if(this.data.enabled&&this.isRunning)if(t.preventDefault(),this.state===this.STATE.ROTATE){if(this.data.enableRotate===!1)return;this.handleMouseMoveRotate(t)}else if(this.state===this.STATE.DOLLY){if(this.data.enableZoom===!1)return;this.handleMouseMoveDolly(t)}else if(this.state===this.STATE.PAN){if(this.data.enablePan===!1)return;this.handleMouseMovePan(t)}},onMouseUp:function(t){this.data.enabled&&this.isRunning&&this.state!==this.STATE.ROTATE_TO&&(t.preventDefault(),t.stopPropagation(),this.handleMouseUp(t),this.canvasEl.removeEventListener("mousemove",this.onMouseMove,!1),this.canvasEl.removeEventListener("mouseup",this.onMouseUp,!1),this.canvasEl.removeEventListener("mouseout",this.onMouseUp,!1),this.state=this.STATE.NONE,this.el.emit("end-drag-orbit-controls",null,!1))},onMouseWheel:function(t){this.data.enabled&&this.isRunning&&this.data.enableZoom!==!1&&(this.state===this.STATE.NONE||this.state===this.STATE.ROTATE)&&(t.preventDefault(),t.stopPropagation(),this.handleMouseWheel(t))},onTouchStart:function(t){if(this.data.enabled&&this.isRunning){switch(t.touches.length){case 1:if(this.data.enableRotate===!1)return;this.handleTouchStartRotate(t),this.state=this.STATE.TOUCH_ROTATE;break;case 2:if(this.data.enableZoom===!1)return;this.handleTouchStartDolly(t),this.state=this.STATE.TOUCH_DOLLY;break;case 3:if(this.data.enablePan===!1)return;this.handleTouchStartPan(t),this.state=this.STATE.TOUCH_PAN;break;default:this.state=this.STATE.NONE}this.state!==this.STATE.NONE&&this.el.emit("start-drag-orbit-controls",null,!1)}},onTouchMove:function(t){if(this.data.enabled&&this.isRunning)switch(t.preventDefault(),t.stopPropagation(),t.touches.length){case 1:if(this.enableRotate===!1)return;if(this.state!==this.STATE.TOUCH_ROTATE)return;this.handleTouchMoveRotate(t);break;case 2:if(this.data.enableZoom===!1)return;if(this.state!==this.STATE.TOUCH_DOLLY)return;this.handleTouchMoveDolly(t);break;case 3:if(this.data.enablePan===!1)return;if(this.state!==this.STATE.TOUCH_PAN)return;this.handleTouchMovePan(t);break;default:this.state=this.STATE.NONE}},onTouchEnd:function(t){this.data.enabled&&this.isRunning&&(this.handleTouchEnd(t),this.el.emit("end-drag-orbit-controls",null,!1),this.state=this.STATE.NONE)},onKeyDown:function(t){this.data.enabled&&this.isRunning&&this.data.enableKeys!==!1&&this.data.enablePan!==!1&&this.handleKeyDown(t)},handleMouseDownRotate:function(t){this.rotateStart.set(t.clientX,t.clientY)},handleMouseDownDolly:function(t){this.dollyStart.set(t.clientX,t.clientY)},handleMouseDownPan:function(t){this.panStart.set(t.clientX,t.clientY)},handleMouseMoveRotate:function(t){this.rotateEnd.set(t.clientX,t.clientY),this.rotateDelta.subVectors(this.rotateEnd,this.rotateStart);var e=this.canvasEl===document?this.canvasEl.body:this.canvasEl;this.rotateLeft(2*Math.PI*this.rotateDelta.x/e.clientWidth*this.data.rotateSpeed),this.rotateUp(2*Math.PI*this.rotateDelta.y/e.clientHeight*this.data.rotateSpeed),this.rotateStart.copy(this.rotateEnd),this.updateView()},handleMouseMoveDolly:function(t){this.dollyEnd.set(t.clientX,t.clientY),this.dollyDelta.subVectors(this.dollyEnd,this.dollyStart),this.dollyDelta.y>0?this.data.invertZoom?this.dollyOut(this.getZoomScale()):this.dollyIn(this.getZoomScale()):this.dollyDelta.y<0&&(this.data.invertZoom?this.dollyIn(this.getZoomScale()):this.dollyOut(this.getZoomScale())),this.dollyStart.copy(this.dollyEnd),this.updateView()},handleMouseMovePan:function(t){this.panEnd.set(t.clientX,t.clientY),this.panDelta.subVectors(this.panEnd,this.panStart),this.pan(this.panDelta.x,this.panDelta.y),this.panStart.copy(this.panEnd),this.updateView()},handleMouseUp:function(t){},handleMouseWheel:function(t){var e=0;void 0!==t.wheelDelta?e=t.wheelDelta:void 0!==t.detail&&(e=-t.detail),e>0?this.data.invertZoom?this.dollyIn(this.getZoomScale()):this.dollyOut(this.getZoomScale()):e<0&&(this.data.invertZoom?this.dollyOut(this.getZoomScale()):this.dollyIn(this.getZoomScale())),this.updateView()},handleTouchStartRotate:function(t){this.rotateStart.set(t.touches[0].pageX,t.touches[0].pageY)},handleTouchStartDolly:function(t){var e=t.touches[0].pageX-t.touches[1].pageX,s=t.touches[0].pageY-t.touches[1].pageY,i=Math.sqrt(e*e+s*s);this.dollyStart.set(0,i)},handleTouchStartPan:function(t){this.panStart.set(t.touches[0].pageX,t.touches[0].pageY)},handleTouchMoveRotate:function(t){this.rotateEnd.set(t.touches[0].pageX,t.touches[0].pageY),this.rotateDelta.subVectors(this.rotateEnd,this.rotateStart);var e=this.canvasEl===document?this.canvasEl.body:this.canvasEl;this.rotateLeft(2*Math.PI*this.rotateDelta.x/e.clientWidth*this.data.rotateSpeed),this.rotateUp(2*Math.PI*this.rotateDelta.y/e.clientHeight*this.data.rotateSpeed),this.rotateStart.copy(this.rotateEnd),this.updateView()},handleTouchMoveDolly:function(t){var e=t.touches[0].pageX-t.touches[1].pageX,s=t.touches[0].pageY-t.touches[1].pageY,i=Math.sqrt(e*e+s*s);this.dollyEnd.set(0,i),this.dollyDelta.subVectors(this.dollyEnd,this.dollyStart),this.dollyDelta.y>0?this.dollyIn(this.getZoomScale()):this.dollyDelta.y<0&&this.dollyOut(this.getZoomScale()),this.dollyStart.copy(this.dollyEnd),this.updateView()},handleTouchMovePan:function(t){this.panEnd.set(t.touches[0].pageX,t.touches[0].pageY),this.panDelta.subVectors(this.panEnd,this.panStart),this.pan(this.panDelta.x,this.panDelta.y),this.panStart.copy(this.panEnd),this.updateView()},handleTouchEnd:function(t){},handleKeyDown:function(t){switch(t.keyCode){case this.keys.UP:this.pan(0,this.data.keyPanSpeed),this.updateView();break;case this.keys.BOTTOM:this.pan(0,-this.data.keyPanSpeed),this.updateView();break;case this.keys.LEFT:this.pan(this.data.keyPanSpeed,0),this.updateView();break;case this.keys.RIGHT:this.pan(-this.data.keyPanSpeed,0),this.updateView()}},getAutoRotationAngle:function(){return 2*Math.PI/60/60*this.data.autoRotateSpeed},getZoomScale:function(){return Math.pow(.95,this.data.zoomSpeed)},rotateLeft:function(t){this.sphericalDelta.theta-=t},rotateUp:function(t){this.sphericalDelta.phi-=t},rotateTo:function(t){this.state=this.STATE.ROTATE_TO,this.desiredPosition.copy(t)},panHorizontally:function(t,e){var s=new THREE.Vector3;s.setFromMatrixColumn(e,0),s.multiplyScalar(-t),this.panOffset.add(s)},panVertically:function(t,e){var s=new THREE.Vector3;s.setFromMatrixColumn(e,1),s.multiplyScalar(t),this.panOffset.add(s)},pan:function(t,e){var s=new THREE.Vector3,i=this.canvasEl===document?this.canvasEl.body:this.canvasEl;if("PerspectiveCamera"===this.cameraType){var a=this.dolly.position;s.copy(a).sub(this.target);var o=s.length();o*=Math.tan(this.camera.fov/2*Math.PI/180),this.panHorizontally(2*t*o/i.clientHeight,this.object.matrix),this.panVertically(2*e*o/i.clientHeight,this.object.matrix)}else"OrthographicCamera"===this.cameraType?(this.panHorizontally(t*(this.dolly.right-this.dolly.left)/this.camera.zoom/i.clientWidth,this.object.matrix),this.panVertically(e*(this.dolly.top-this.dolly.bottom)/this.camera.zoom/i.clientHeight,this.object.matrix)):(console.warn("Trying to pan: WARNING: Orbit Controls encountered an unknown camera type - pan disabled."),this.data.enablePan=!1)},dollyIn:function(t){"PerspectiveCamera"===this.cameraType?this.scale*=t:"OrthographicCamera"===this.cameraType?(this.camera.zoom=Math.max(this.data.minZoom,Math.min(this.data.maxZoom,this.camera.zoom*t)),this.camera.updateProjectionMatrix(),this.zoomChanged=!0):(console.warn("Trying to dolly in: WARNING: Orbit Controls encountered an unknown camera type - dolly/zoom disabled."),this.data.enableZoom=!1)},dollyOut:function(t){"PerspectiveCamera"===this.cameraType?this.scale/=t:"OrthographicCamera"===this.cameraType?(this.camera.zoom=Math.max(this.data.minZoom,Math.min(this.data.maxZoom,this.camera.zoom/t)),this.camera.updateProjectionMatrix(),this.zoomChanged=!0):(console.warn("Trying to dolly out: WARNING: Orbit Controls encountered an unknown camera type - dolly/zoom disabled."),this.data.enableZoom=!1)},lookAtTarget:function(t,e){var s=new THREE.Vector3;s.subVectors(t.position,e).add(t.position),t.lookAt(s)},saveCameraPose:function(){this.savedPose||(this.savedPose={position:this.dolly.position,rotation:this.dolly.rotation})},restoreCameraPose:function(){this.savedPose&&(this.dolly.position.copy(this.savedPose.position),this.dolly.rotation.copy(this.savedPose.rotation),this.savedPose=null)},updateView:function(t){if(this.desiredPosition&&this.state===this.STATE.ROTATE_TO){var e=new THREE.Spherical;e.setFromVector3(this.desiredPosition);var i=e.phi-this.spherical.phi,a=e.theta-this.spherical.theta;this.sphericalDelta.set(this.spherical.radius,i*this.data.rotateToSpeed,a*this.data.rotateToSpeed)}var o=new THREE.Vector3,n=(new THREE.Quaternion).setFromUnitVectors(this.dolly.up,new THREE.Vector3(0,1,0)),h=n.clone().inverse();if(o.copy(this.dolly.position).sub(this.target),o.applyQuaternion(n),this.spherical.setFromVector3(o),this.data.autoRotate&&this.state===this.STATE.NONE&&this.rotateLeft(this.getAutoRotationAngle()),this.spherical.theta+=this.sphericalDelta.theta,this.spherical.phi+=this.sphericalDelta.phi,this.spherical.theta=Math.max(this.data.minAzimuthAngle,Math.min(this.data.maxAzimuthAngle,this.spherical.theta)),this.spherical.phi=Math.max(this.data.minPolarAngle,Math.min(this.data.maxPolarAngle,this.spherical.phi)),this.spherical.makeSafe(),this.spherical.radius*=this.scale,this.spherical.radius=Math.max(this.data.minDistance,Math.min(this.data.maxDistance,this.spherical.radius)),this.target.add(this.panOffset),o.setFromSpherical(this.spherical),o.applyQuaternion(h),this.dolly.position.copy(this.target).add(o),this.target&&this.lookAtTarget(this.dolly,this.target),this.data.enableDamping===!0?(this.sphericalDelta.theta*=1-this.data.dampingFactor,this.sphericalDelta.phi*=1-this.data.dampingFactor):this.sphericalDelta.set(0,0,0),this.scale=1,this.panOffset.set(0,0,0),t===!0||this.zoomChanged||this.lastPosition.distanceToSquared(this.dolly.position)>this.EPS||8*(1-this.lastQuaternion.dot(this.dolly.quaternion))>this.EPS){var l=this.calculateHMDQuaternion(),r=new THREE.Euler;return r.setFromQuaternion(l,"YXZ"),this.el.setAttribute("position",{x:this.dolly.position.x,y:this.dolly.position.y,z:this.dolly.position.z}),this.el.setAttribute("rotation",{x:s(r.x),y:s(r.y),z:s(r.z)}),this.lastPosition.copy(this.dolly.position),this.lastQuaternion.copy(this.dolly.quaternion),this.zoomChanged=!1,!0}return!1},calculateHMDQuaternion:function(){var t=new THREE.Quaternion;return function(){return t.copy(this.dolly.quaternion),t}}()})}]); -------------------------------------------------------------------------------- /examples/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A-Frame Example Component - Basic 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /examples/assets/sky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizzle/aframe-orbit-controls-component/870d7357ebc0100ab7234875d93ccb334cdc12bf/examples/assets/sky.jpg -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A-Frame Example Component - Basic 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 | 47 |
48 | 49 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /examples/camera-children/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A-Frame Example Component - Basic 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 28 | 29 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A-Frame Example Component 5 | 6 | 25 | 26 | 27 | 28 |

A-Frame Example Component

29 | Basic 30 |

This is an example showing the basic functions of orbit controls.

31 | 32 | Rotate-To 33 |

This is an example showing a rotation to a manual position.

34 | 35 | Camera-Children 36 |

This is an example showing a camera with a light as a child entity that always follows the camera.

37 | 38 | Assets 39 |

This is an example showing the use of an a-asset as a sky.

40 | 41 |
42 |
43 | Fork me on GitHub 44 |
45 |
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/main.js: -------------------------------------------------------------------------------- 1 | require('aframe'); 2 | require('../index.js'); 3 | -------------------------------------------------------------------------------- /examples/rotate-to/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A-Frame Example Component - Rotate-To 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME THREE */ 2 | 3 | if (typeof AFRAME === 'undefined') { 4 | throw new Error('Component attempted to register before AFRAME was available.'); 5 | } 6 | 7 | var radToDeg = THREE.Math.radToDeg; 8 | 9 | /** 10 | * Example component for A-Frame. 11 | */ 12 | AFRAME.registerComponent('orbit-controls', { 13 | dependencies: ['position', 'rotation'], 14 | schema: { 15 | enabled: { 16 | default: true 17 | }, 18 | target: { 19 | default: '' 20 | }, 21 | distance: { 22 | default: 1 23 | }, 24 | enableRotate: { 25 | default: true 26 | }, 27 | rotateSpeed: { 28 | default: 1.0 29 | }, 30 | enableZoom: { 31 | default: true 32 | }, 33 | zoomSpeed: { 34 | default: 1.0 35 | }, 36 | enablePan: { 37 | default: true 38 | }, 39 | keyPanSpeed: { 40 | default: 7.0 41 | }, 42 | enableDamping: { 43 | default: false 44 | }, 45 | dampingFactor: { 46 | default: 0.25 47 | }, 48 | autoRotate: { 49 | default: false 50 | }, 51 | autoRotateSpeed: { 52 | default: 2.0 53 | }, 54 | enableKeys: { 55 | default: true 56 | }, 57 | minAzimuthAngle: { 58 | default: -Infinity 59 | }, 60 | maxAzimuthAngle: { 61 | default: Infinity 62 | }, 63 | minPolarAngle: { 64 | default: 0 65 | }, 66 | maxPolarAngle: { 67 | default: Math.PI 68 | }, 69 | minZoom: { 70 | default: 0 71 | }, 72 | maxZoom: { 73 | default: Infinity 74 | }, 75 | invertZoom: { 76 | default: false 77 | }, 78 | minDistance: { 79 | default: 0 80 | }, 81 | maxDistance: { 82 | default: Infinity 83 | }, 84 | rotateTo: { 85 | type: 'vec3', 86 | default: {x: 0, y: 0, z: 0} 87 | }, 88 | rotateToSpeed: { 89 | type: 'number', 90 | default: 0.05 91 | }, 92 | logPosition: { 93 | type: 'boolean', 94 | default: false 95 | }, 96 | autoVRLookCam: { 97 | type: 'boolean', 98 | default: true 99 | } 100 | }, 101 | 102 | /** 103 | * Set if component needs multiple instancing. 104 | */ 105 | multiple: false, 106 | 107 | /** 108 | * Called once when component is attached. Generally for initial setup. 109 | */ 110 | init: function () { 111 | this.sceneEl = this.el.sceneEl; 112 | this.object = this.el.object3D; 113 | this.target = this.sceneEl.querySelector(this.data.target).object3D.position; 114 | 115 | console.log('enabled: ', this.data.enabled); 116 | 117 | // Find the look-controls component on this camera, or create if it doesn't exist. 118 | this.isRunning = false; 119 | this.lookControls = null; 120 | 121 | if (this.data.autoVRLookCam) { 122 | if (this.el.components['look-controls']) { 123 | this.lookControls = this.el.components['look-controls']; 124 | } else { 125 | this.el.setAttribute('look-controls', ''); 126 | this.lookControls = this.el.components['look-controls']; 127 | } 128 | this.lookControls.pause(); 129 | this.el.sceneEl.addEventListener('enter-vr', this.onEnterVR.bind(this), false); 130 | this.el.sceneEl.addEventListener('exit-vr', this.onExitVR.bind(this), false); 131 | } 132 | 133 | this.dolly = new THREE.Object3D(); 134 | this.dolly.position.copy(this.object.position); 135 | 136 | this.savedPose = null; 137 | 138 | this.STATE = { 139 | NONE: -1, 140 | ROTATE: 0, 141 | DOLLY: 1, 142 | PAN: 2, 143 | TOUCH_ROTATE: 3, 144 | TOUCH_DOLLY: 4, 145 | TOUCH_PAN: 5, 146 | ROTATE_TO: 6 147 | }; 148 | 149 | this.state = this.STATE.NONE; 150 | 151 | this.EPS = 0.000001; 152 | this.lastPosition = new THREE.Vector3(); 153 | this.lastQuaternion = new THREE.Quaternion(); 154 | 155 | this.spherical = new THREE.Spherical(); 156 | this.sphericalDelta = new THREE.Spherical(); 157 | 158 | this.scale = 1.0; 159 | this.zoomChanged = false; 160 | 161 | this.rotateStart = new THREE.Vector2(); 162 | this.rotateEnd = new THREE.Vector2(); 163 | this.rotateDelta = new THREE.Vector2(); 164 | 165 | this.panStart = new THREE.Vector2(); 166 | this.panEnd = new THREE.Vector2(); 167 | this.panDelta = new THREE.Vector2(); 168 | this.panOffset = new THREE.Vector3(); 169 | 170 | this.dollyStart = new THREE.Vector2(); 171 | this.dollyEnd = new THREE.Vector2(); 172 | this.dollyDelta = new THREE.Vector2(); 173 | 174 | this.vector = new THREE.Vector3(); 175 | this.desiredPosition = new THREE.Vector3(); 176 | 177 | this.mouseButtons = { 178 | ORBIT: THREE.MOUSE.LEFT, 179 | ZOOM: THREE.MOUSE.MIDDLE, 180 | PAN: THREE.MOUSE.RIGHT 181 | }; 182 | 183 | this.keys = { 184 | LEFT: 37, 185 | UP: 38, 186 | RIGHT: 39, 187 | BOTTOM: 40 188 | }; 189 | 190 | this.bindMethods(); 191 | }, 192 | 193 | /** 194 | * Called when component is attached and when component data changes. 195 | * Generally modifies the entity based on the data. 196 | */ 197 | update: function (oldData) { 198 | console.log('component update'); 199 | 200 | if (this.data.rotateTo) { 201 | var rotateToVec3 = new THREE.Vector3(this.data.rotateTo.x, this.data.rotateTo.y, this.data.rotateTo.z); 202 | // Check if rotateToVec3 is already desiredPosition 203 | if (!this.desiredPosition.equals(rotateToVec3)) { 204 | this.desiredPosition.copy(rotateToVec3); 205 | this.rotateTo(this.desiredPosition); 206 | } 207 | } 208 | 209 | this.dolly.position.copy(this.object.position); 210 | this.updateView(true); 211 | }, 212 | 213 | /** 214 | * Called when a component is removed (e.g., via removeAttribute). 215 | * Generally undoes all modifications to the entity. 216 | */ 217 | remove: function () { 218 | // console.log("component remove"); 219 | this.isRunning = false; 220 | this.removeEventListeners(); 221 | this.el.sceneEl.removeEventListener('enter-vr', this.onEnterVR, false); 222 | this.el.sceneEl.removeEventListener('exit-vr', this.onExitVR, false); 223 | }, 224 | 225 | /** 226 | * Called on each scene tick. 227 | */ 228 | tick: function (t) { 229 | var render = this.data.enabled && this.isRunning ? this.updateView() : false; 230 | if (render === true && this.data.logPosition === true) { 231 | console.log(this.el.object3D.position); 232 | } 233 | }, 234 | 235 | /* 236 | * Called when entering VR mode 237 | */ 238 | onEnterVR: function (event) { 239 | // console.log('enter vr', this); 240 | 241 | this.saveCameraPose(); 242 | 243 | this.el.setAttribute('position', {x: 0, y: 2, z: 5}); 244 | this.el.setAttribute('rotation', {x: 0, y: 0, z: 0}); 245 | 246 | this.pause(); 247 | this.lookControls.play(); 248 | if (this.data.autoRotate) console.warn('orbit-controls: Sorry, autoRotate is not implemented in VR mode'); 249 | }, 250 | 251 | /* 252 | * Called when exiting VR mode 253 | */ 254 | onExitVR: function (event) { 255 | // console.log('exit vr'); 256 | 257 | this.lookControls.pause(); 258 | this.play(); 259 | 260 | this.restoreCameraPose(); 261 | this.updateView(true); 262 | }, 263 | 264 | /** 265 | * Called when entity pauses. 266 | * Use to stop or remove any dynamic or background behavior such as events. 267 | */ 268 | pause: function () { 269 | // console.log("component pause"); 270 | this.isRunning = false; 271 | this.removeEventListeners(); 272 | }, 273 | 274 | /** 275 | * Called when entity resumes. 276 | * Use to continue or add any dynamic or background behavior such as events. 277 | */ 278 | play: function () { 279 | // console.log("component play"); 280 | this.isRunning = true; 281 | 282 | var camera, cameraType; 283 | this.object.traverse(function (child) { 284 | if (child instanceof THREE.PerspectiveCamera) { 285 | camera = child; 286 | cameraType = 'PerspectiveCamera'; 287 | } else if (child instanceof THREE.OrthographicCamera) { 288 | camera = child; 289 | cameraType = 'OrthographicCamera'; 290 | } 291 | }); 292 | 293 | this.camera = camera; 294 | this.cameraType = cameraType; 295 | 296 | this.sceneEl.addEventListener('renderstart', this.onRenderTargetLoaded, false); 297 | 298 | if (this.lookControls) this.lookControls.pause(); 299 | if (this.canvasEl) this.addEventListeners(); 300 | }, 301 | 302 | /* 303 | * Called when Render Target is completely loaded 304 | * Then set canvasEl and add event listeners 305 | */ 306 | onRenderTargetLoaded: function () { 307 | this.sceneEl.removeEventListener('renderstart', this.onRenderTargetLoaded, false); 308 | this.canvasEl = this.sceneEl.canvas; 309 | this.addEventListeners(); 310 | }, 311 | 312 | /* 313 | * Bind this to all event handlera 314 | */ 315 | bindMethods: function () { 316 | this.onRenderTargetLoaded = this.onRenderTargetLoaded.bind(this); 317 | 318 | this.onContextMenu = this.onContextMenu.bind(this); 319 | this.onMouseDown = this.onMouseDown.bind(this); 320 | this.onMouseWheel = this.onMouseWheel.bind(this); 321 | this.onMouseMove = this.onMouseMove.bind(this); 322 | this.onMouseUp = this.onMouseUp.bind(this); 323 | this.onTouchStart = this.onTouchStart.bind(this); 324 | this.onTouchMove = this.onTouchMove.bind(this); 325 | this.onTouchEnd = this.onTouchEnd.bind(this); 326 | this.onKeyDown = this.onKeyDown.bind(this); 327 | }, 328 | 329 | /* 330 | * Add event listeners 331 | */ 332 | addEventListeners: function () { 333 | this.canvasEl.addEventListener('contextmenu', this.onContextMenu, false); 334 | 335 | this.canvasEl.addEventListener('mousedown', this.onMouseDown, false); 336 | this.canvasEl.addEventListener('mousewheel', this.onMouseWheel, false); 337 | this.canvasEl.addEventListener('MozMousePixelScroll', this.onMouseWheel, false); // firefox 338 | 339 | this.canvasEl.addEventListener('touchstart', this.onTouchStart, false); 340 | this.canvasEl.addEventListener('touchend', this.onTouchEnd, false); 341 | this.canvasEl.addEventListener('touchmove', this.onTouchMove, false); 342 | 343 | window.addEventListener('keydown', this.onKeyDown, false); 344 | }, 345 | 346 | /* 347 | * Remove event listeners 348 | */ 349 | removeEventListeners: function () { 350 | 351 | if(this.canvasEl){ 352 | this.canvasEl.removeEventListener('contextmenu', this.onContextMenu, false); 353 | this.canvasEl.removeEventListener('mousedown', this.onMouseDown, false); 354 | this.canvasEl.removeEventListener('mousewheel', this.onMouseWheel, false); 355 | this.canvasEl.removeEventListener('MozMousePixelScroll', this.onMouseWheel, false); // firefox 356 | 357 | this.canvasEl.removeEventListener('touchstart', this.onTouchStart, false); 358 | this.canvasEl.removeEventListener('touchend', this.onTouchEnd, false); 359 | this.canvasEl.removeEventListener('touchmove', this.onTouchMove, false); 360 | 361 | this.canvasEl.removeEventListener('mousemove', this.onMouseMove, false); 362 | this.canvasEl.removeEventListener('mouseup', this.onMouseUp, false); 363 | this.canvasEl.removeEventListener('mouseout', this.onMouseUp, false); 364 | } 365 | 366 | window.removeEventListener('keydown', this.onKeyDown, false); 367 | }, 368 | 369 | /* 370 | * EVENT LISTENERS 371 | */ 372 | 373 | /* 374 | * Called when right clicking the A-Frame component 375 | */ 376 | 377 | onContextMenu: function (event) { 378 | event.preventDefault(); 379 | }, 380 | 381 | /* 382 | * MOUSE CLICK EVENT LISTENERS 383 | */ 384 | 385 | onMouseDown: function (event) { 386 | // console.log('onMouseDown'); 387 | 388 | if (!this.data.enabled || !this.isRunning) return; 389 | 390 | if (event.button === this.mouseButtons.ORBIT && (event.shiftKey || event.ctrlKey)) { 391 | if (this.data.enablePan === false) return; 392 | this.handleMouseDownPan(event); 393 | this.state = this.STATE.PAN; 394 | } else if (event.button === this.mouseButtons.ORBIT) { 395 | this.panOffset.set(0, 0, 0); 396 | if (this.data.enableRotate === false) return; 397 | this.handleMouseDownRotate(event); 398 | this.state = this.STATE.ROTATE; 399 | } else if (event.button === this.mouseButtons.ZOOM) { 400 | this.panOffset.set(0, 0, 0); 401 | if (this.data.enableZoom === false) return; 402 | this.handleMouseDownDolly(event); 403 | this.state = this.STATE.DOLLY; 404 | } else if (event.button === this.mouseButtons.PAN) { 405 | if (this.data.enablePan === false) return; 406 | this.handleMouseDownPan(event); 407 | this.state = this.STATE.PAN; 408 | } 409 | 410 | if (this.state !== this.STATE.NONE) { 411 | this.canvasEl.addEventListener('mousemove', this.onMouseMove, false); 412 | this.canvasEl.addEventListener('mouseup', this.onMouseUp, false); 413 | this.canvasEl.addEventListener('mouseout', this.onMouseUp, false); 414 | 415 | this.el.emit('start-drag-orbit-controls', null, false); 416 | } 417 | }, 418 | 419 | onMouseMove: function (event) { 420 | // console.log('onMouseMove'); 421 | 422 | if (!this.data.enabled || !this.isRunning) return; 423 | 424 | event.preventDefault(); 425 | 426 | if (this.state === this.STATE.ROTATE) { 427 | if (this.data.enableRotate === false) return; 428 | this.handleMouseMoveRotate(event); 429 | } else if (this.state === this.STATE.DOLLY) { 430 | if (this.data.enableZoom === false) return; 431 | this.handleMouseMoveDolly(event); 432 | } else if (this.state === this.STATE.PAN) { 433 | if (this.data.enablePan === false) return; 434 | this.handleMouseMovePan(event); 435 | } 436 | }, 437 | 438 | onMouseUp: function (event) { 439 | // console.log('onMouseUp'); 440 | 441 | if (!this.data.enabled || !this.isRunning) return; 442 | 443 | if (this.state === this.STATE.ROTATE_TO) return; 444 | 445 | event.preventDefault(); 446 | event.stopPropagation(); 447 | 448 | this.handleMouseUp(event); 449 | 450 | this.canvasEl.removeEventListener('mousemove', this.onMouseMove, false); 451 | this.canvasEl.removeEventListener('mouseup', this.onMouseUp, false); 452 | this.canvasEl.removeEventListener('mouseout', this.onMouseUp, false); 453 | 454 | this.state = this.STATE.NONE; 455 | 456 | this.el.emit('end-drag-orbit-controls', null, false); 457 | }, 458 | 459 | /* 460 | * MOUSE WHEEL EVENT LISTENERS 461 | */ 462 | 463 | onMouseWheel: function (event) { 464 | // console.log('onMouseWheel'); 465 | 466 | if (!this.data.enabled || !this.isRunning || this.data.enableZoom === false || (this.state !== this.STATE.NONE && this.state !== this.STATE.ROTATE)) return; 467 | 468 | event.preventDefault(); 469 | event.stopPropagation(); 470 | this.handleMouseWheel(event); 471 | }, 472 | 473 | /* 474 | * TOUCH EVENT LISTENERS 475 | */ 476 | 477 | onTouchStart: function (event) { 478 | // console.log('onTouchStart'); 479 | 480 | if (!this.data.enabled || !this.isRunning) return; 481 | 482 | switch (event.touches.length) { 483 | case 1: // one-fingered touch: rotate 484 | if (this.data.enableRotate === false) return; 485 | this.handleTouchStartRotate(event); 486 | this.state = this.STATE.TOUCH_ROTATE; 487 | break; 488 | case 2: // two-fingered touch: dolly 489 | if (this.data.enableZoom === false) return; 490 | this.handleTouchStartDolly(event); 491 | this.state = this.STATE.TOUCH_DOLLY; 492 | break; 493 | case 3: // three-fingered touch: pan 494 | if (this.data.enablePan === false) return; 495 | this.handleTouchStartPan(event); 496 | this.state = this.STATE.TOUCH_PAN; 497 | break; 498 | default: 499 | this.state = this.STATE.NONE; 500 | } 501 | 502 | if (this.state !== this.STATE.NONE) { 503 | this.el.emit('start-drag-orbit-controls', null, false); 504 | } 505 | }, 506 | 507 | onTouchMove: function (event) { 508 | // console.log('onTouchMove'); 509 | 510 | if (!this.data.enabled || !this.isRunning) return; 511 | 512 | event.preventDefault(); 513 | event.stopPropagation(); 514 | 515 | switch (event.touches.length) { 516 | case 1: // one-fingered touch: rotate 517 | if (this.enableRotate === false) return; 518 | if (this.state !== this.STATE.TOUCH_ROTATE) return; // is this needed?... 519 | this.handleTouchMoveRotate(event); 520 | break; 521 | 522 | case 2: // two-fingered touch: dolly 523 | if (this.data.enableZoom === false) return; 524 | if (this.state !== this.STATE.TOUCH_DOLLY) return; // is this needed?... 525 | this.handleTouchMoveDolly(event); 526 | break; 527 | 528 | case 3: // three-fingered touch: pan 529 | if (this.data.enablePan === false) return; 530 | if (this.state !== this.STATE.TOUCH_PAN) return; // is this needed?... 531 | this.handleTouchMovePan(event); 532 | break; 533 | 534 | default: 535 | this.state = this.STATE.NONE; 536 | } 537 | }, 538 | 539 | onTouchEnd: function (event) { 540 | // console.log('onTouchEnd'); 541 | 542 | if (!this.data.enabled || !this.isRunning) return; 543 | 544 | this.handleTouchEnd(event); 545 | 546 | this.el.emit('end-drag-orbit-controls', null, false); 547 | 548 | this.state = this.STATE.NONE; 549 | }, 550 | 551 | /* 552 | * KEYBOARD EVENT LISTENERS 553 | */ 554 | 555 | onKeyDown: function (event) { 556 | // console.log('onKeyDown'); 557 | 558 | if (!this.data.enabled || !this.isRunning || this.data.enableKeys === false || this.data.enablePan === false) return; 559 | 560 | this.handleKeyDown(event); 561 | }, 562 | 563 | /* 564 | * EVENT HANDLERS 565 | */ 566 | 567 | /* 568 | * MOUSE CLICK EVENT HANDLERS 569 | */ 570 | 571 | handleMouseDownRotate: function (event) { 572 | // console.log( 'handleMouseDownRotate' ); 573 | this.rotateStart.set(event.clientX, event.clientY); 574 | }, 575 | 576 | handleMouseDownDolly: function (event) { 577 | // console.log( 'handleMouseDownDolly' ); 578 | this.dollyStart.set(event.clientX, event.clientY); 579 | }, 580 | 581 | handleMouseDownPan: function (event) { 582 | // console.log( 'handleMouseDownPan' ); 583 | this.panStart.set(event.clientX, event.clientY); 584 | }, 585 | 586 | handleMouseMoveRotate: function (event) { 587 | // console.log( 'handleMouseMoveRotate' ); 588 | 589 | this.rotateEnd.set(event.clientX, event.clientY); 590 | this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart); 591 | 592 | var canvas = this.canvasEl === document ? this.canvasEl.body : this.canvasEl; 593 | 594 | // rotating across whole screen goes 360 degrees around 595 | this.rotateLeft(2 * Math.PI * this.rotateDelta.x / canvas.clientWidth * this.data.rotateSpeed); 596 | 597 | // rotating up and down along whole screen attempts to go 360, but limited to 180 598 | this.rotateUp(2 * Math.PI * this.rotateDelta.y / canvas.clientHeight * this.data.rotateSpeed); 599 | 600 | this.rotateStart.copy(this.rotateEnd); 601 | 602 | this.updateView(); 603 | }, 604 | 605 | handleMouseMoveDolly: function (event) { 606 | // console.log( 'handleMouseMoveDolly' ); 607 | 608 | this.dollyEnd.set(event.clientX, event.clientY); 609 | this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart); 610 | 611 | if (this.dollyDelta.y > 0) { 612 | !this.data.invertZoom ? this.dollyIn(this.getZoomScale()) : this.dollyOut(this.getZoomScale()); 613 | } else if (this.dollyDelta.y < 0) { 614 | !this.data.invertZoom ? this.dollyOut(this.getZoomScale()) : this.dollyIn(this.getZoomScale()); 615 | } 616 | 617 | this.dollyStart.copy(this.dollyEnd); 618 | 619 | this.updateView(); 620 | }, 621 | 622 | handleMouseMovePan: function (event) { 623 | // console.log( 'handleMouseMovePan' ); 624 | 625 | this.panEnd.set(event.clientX, event.clientY); 626 | this.panDelta.subVectors(this.panEnd, this.panStart); 627 | this.pan(this.panDelta.x, this.panDelta.y); 628 | this.panStart.copy(this.panEnd); 629 | 630 | this.updateView(); 631 | }, 632 | 633 | handleMouseUp: function (event) { 634 | // console.log( 'handleMouseUp' ); 635 | }, 636 | 637 | /* 638 | * MOUSE WHEEL EVENT HANDLERS 639 | */ 640 | 641 | handleMouseWheel: function (event) { 642 | // console.log( 'handleMouseWheel' ); 643 | 644 | var delta = 0; 645 | if (event.wheelDelta !== undefined) { 646 | // WebKit / Opera / Explorer 9 647 | delta = event.wheelDelta; 648 | } else if (event.detail !== undefined) { 649 | // Firefox 650 | delta = -event.detail; 651 | } 652 | 653 | if (delta > 0) { 654 | !this.data.invertZoom ? this.dollyOut(this.getZoomScale()) : this.dollyIn(this.getZoomScale()); 655 | } else if (delta < 0) { 656 | !this.data.invertZoom ? this.dollyIn(this.getZoomScale()) : this.dollyOut(this.getZoomScale()); 657 | } 658 | 659 | this.updateView(); 660 | }, 661 | 662 | /* 663 | * TOUCH EVENT HANDLERS 664 | */ 665 | 666 | handleTouchStartRotate: function (event) { 667 | // console.log( 'handleTouchStartRotate' ); 668 | 669 | this.rotateStart.set(event.touches[0].pageX, event.touches[0].pageY); 670 | }, 671 | 672 | handleTouchStartDolly: function (event) { 673 | // console.log( 'handleTouchStartDolly' ); 674 | 675 | var dx = event.touches[0].pageX - event.touches[1].pageX; 676 | var dy = event.touches[0].pageY - event.touches[1].pageY; 677 | var distance = Math.sqrt(dx * dx + dy * dy); 678 | this.dollyStart.set(0, distance); 679 | }, 680 | 681 | handleTouchStartPan: function (event) { 682 | // console.log( 'handleTouchStartPan' ); 683 | 684 | this.panStart.set(event.touches[0].pageX, event.touches[0].pageY); 685 | }, 686 | 687 | handleTouchMoveRotate: function (event) { 688 | // console.log( 'handleTouchMoveRotate' ); 689 | 690 | this.rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY); 691 | this.rotateDelta.subVectors(this.rotateEnd, this.rotateStart); 692 | 693 | var canvas = this.canvasEl === document ? this.canvasEl.body : this.canvasEl; 694 | // rotating across whole screen goes 360 degrees around 695 | this.rotateLeft(2 * Math.PI * this.rotateDelta.x / canvas.clientWidth * this.data.rotateSpeed); 696 | // rotating up and down along whole screen attempts to go 360, but limited to 180 697 | this.rotateUp(2 * Math.PI * this.rotateDelta.y / canvas.clientHeight * this.data.rotateSpeed); 698 | this.rotateStart.copy(this.rotateEnd); 699 | this.updateView(); 700 | }, 701 | 702 | handleTouchMoveDolly: function (event) { 703 | // console.log( 'handleTouchMoveDolly' ); 704 | 705 | var dx = event.touches[0].pageX - event.touches[1].pageX; 706 | var dy = event.touches[0].pageY - event.touches[1].pageY; 707 | 708 | var distance = Math.sqrt(dx * dx + dy * dy); 709 | 710 | this.dollyEnd.set(0, distance); 711 | this.dollyDelta.subVectors(this.dollyEnd, this.dollyStart); 712 | if (this.dollyDelta.y > 0) { 713 | this.dollyIn(this.getZoomScale()); 714 | } else if (this.dollyDelta.y < 0) { 715 | this.dollyOut(this.getZoomScale()); 716 | } 717 | 718 | this.dollyStart.copy(this.dollyEnd); 719 | this.updateView(); 720 | }, 721 | 722 | handleTouchMovePan: function (event) { 723 | // console.log( 'handleTouchMovePan' ); 724 | 725 | this.panEnd.set(event.touches[0].pageX, event.touches[0].pageY); 726 | this.panDelta.subVectors(this.panEnd, this.panStart); 727 | this.pan(this.panDelta.x, this.panDelta.y); 728 | this.panStart.copy(this.panEnd); 729 | this.updateView(); 730 | }, 731 | 732 | handleTouchEnd: function (event) { 733 | // console.log( 'handleTouchEnd' ); 734 | }, 735 | 736 | /* 737 | * KEYBOARD EVENT HANDLERS 738 | */ 739 | 740 | handleKeyDown: function (event) { 741 | // console.log( 'handleKeyDown' ); 742 | 743 | switch (event.keyCode) { 744 | case this.keys.UP: 745 | this.pan(0, this.data.keyPanSpeed); 746 | this.updateView(); 747 | break; 748 | case this.keys.BOTTOM: 749 | this.pan(0, -this.data.keyPanSpeed); 750 | this.updateView(); 751 | break; 752 | case this.keys.LEFT: 753 | this.pan(this.data.keyPanSpeed, 0); 754 | this.updateView(); 755 | break; 756 | case this.keys.RIGHT: 757 | this.pan(-this.data.keyPanSpeed, 0); 758 | this.updateView(); 759 | break; 760 | } 761 | }, 762 | 763 | /* 764 | * HELPER FUNCTIONS 765 | */ 766 | 767 | getAutoRotationAngle: function () { 768 | return 2 * Math.PI / 60 / 60 * this.data.autoRotateSpeed; 769 | }, 770 | 771 | getZoomScale: function () { 772 | return Math.pow(0.95, this.data.zoomSpeed); 773 | }, 774 | 775 | rotateLeft: function (angle) { 776 | this.sphericalDelta.theta -= angle; 777 | }, 778 | 779 | rotateUp: function (angle) { 780 | this.sphericalDelta.phi -= angle; 781 | }, 782 | 783 | rotateTo: function (vec3) { 784 | this.state = this.STATE.ROTATE_TO; 785 | this.desiredPosition.copy(vec3); 786 | }, 787 | 788 | panHorizontally: function (distance, objectMatrix) { 789 | // console.log('pan horizontally', distance, objectMatrix); 790 | var v = new THREE.Vector3(); 791 | v.setFromMatrixColumn(objectMatrix, 0); // get X column of objectMatrix 792 | v.multiplyScalar(-distance); 793 | this.panOffset.add(v); 794 | }, 795 | 796 | panVertically: function (distance, objectMatrix) { 797 | // console.log('pan vertically', distance, objectMatrix); 798 | var v = new THREE.Vector3(); 799 | v.setFromMatrixColumn(objectMatrix, 1); // get Y column of objectMatrix 800 | v.multiplyScalar(distance); 801 | this.panOffset.add(v); 802 | }, 803 | 804 | pan: function (deltaX, deltaY) { // deltaX and deltaY are in pixels; right and down are positive 805 | // console.log('panning', deltaX, deltaY ); 806 | var offset = new THREE.Vector3(); 807 | var canvas = this.canvasEl === document ? this.canvasEl.body : this.canvasEl; 808 | 809 | if (this.cameraType === 'PerspectiveCamera') { 810 | // perspective 811 | var position = this.dolly.position; 812 | offset.copy(position).sub(this.target); 813 | var targetDistance = offset.length(); 814 | targetDistance *= Math.tan((this.camera.fov / 2) * Math.PI / 180.0); // half of the fov is center to top of screen 815 | this.panHorizontally(2 * deltaX * targetDistance / canvas.clientHeight, this.object.matrix); // we actually don't use screenWidth, since perspective camera is fixed to screen height 816 | this.panVertically(2 * deltaY * targetDistance / canvas.clientHeight, this.object.matrix); 817 | } else if (this.cameraType === 'OrthographicCamera') { 818 | // orthographic 819 | this.panHorizontally(deltaX * (this.dolly.right - this.dolly.left) / this.camera.zoom / canvas.clientWidth, this.object.matrix); 820 | this.panVertically(deltaY * (this.dolly.top - this.dolly.bottom) / this.camera.zoom / canvas.clientHeight, this.object.matrix); 821 | } else { 822 | // camera neither orthographic nor perspective 823 | console.warn('Trying to pan: WARNING: Orbit Controls encountered an unknown camera type - pan disabled.'); 824 | this.data.enablePan = false; 825 | } 826 | }, 827 | 828 | dollyIn: function (dollyScale) { 829 | // console.log( "dollyIn camera" ); 830 | if (this.cameraType === 'PerspectiveCamera') { 831 | this.scale *= dollyScale; 832 | } else if (this.cameraType === 'OrthographicCamera') { 833 | this.camera.zoom = Math.max(this.data.minZoom, Math.min(this.data.maxZoom, this.camera.zoom * dollyScale)); 834 | this.camera.updateProjectionMatrix(); 835 | this.zoomChanged = true; 836 | } else { 837 | console.warn('Trying to dolly in: WARNING: Orbit Controls encountered an unknown camera type - dolly/zoom disabled.'); 838 | this.data.enableZoom = false; 839 | } 840 | }, 841 | 842 | dollyOut: function (dollyScale) { 843 | // console.log( "dollyOut camera" ); 844 | if (this.cameraType === 'PerspectiveCamera') { 845 | this.scale /= dollyScale; 846 | } else if (this.cameraType === 'OrthographicCamera') { 847 | this.camera.zoom = Math.max(this.data.minZoom, Math.min(this.data.maxZoom, this.camera.zoom / dollyScale)); 848 | this.camera.updateProjectionMatrix(); 849 | this.zoomChanged = true; 850 | } else { 851 | console.warn('Trying to dolly out: WARNING: Orbit Controls encountered an unknown camera type - dolly/zoom disabled.'); 852 | this.data.enableZoom = false; 853 | } 854 | }, 855 | 856 | lookAtTarget: function (object, target) { 857 | var v = new THREE.Vector3(); 858 | v.subVectors(object.position, target).add(object.position); 859 | object.lookAt(v); 860 | }, 861 | 862 | /* 863 | * SAVES CAMERA POSE (WHEN ENTERING VR) 864 | */ 865 | 866 | saveCameraPose: function () { 867 | if (this.savedPose) { return; } 868 | this.savedPose = { 869 | position: this.dolly.position, 870 | rotation: this.dolly.rotation 871 | }; 872 | }, 873 | 874 | /* 875 | * RESTORE CAMERA POSE (WHEN EXITING VR) 876 | */ 877 | 878 | restoreCameraPose: function () { 879 | if (!this.savedPose) { return; } 880 | this.dolly.position.copy(this.savedPose.position); 881 | this.dolly.rotation.copy(this.savedPose.rotation); 882 | this.savedPose = null; 883 | }, 884 | 885 | /* 886 | * VIEW UPDATE 887 | */ 888 | 889 | updateView: function (forceUpdate) { 890 | if (this.desiredPosition && this.state === this.STATE.ROTATE_TO) { 891 | var desiredSpherical = new THREE.Spherical(); 892 | desiredSpherical.setFromVector3(this.desiredPosition); 893 | var phiDiff = desiredSpherical.phi - this.spherical.phi; 894 | var thetaDiff = desiredSpherical.theta - this.spherical.theta; 895 | this.sphericalDelta.set(this.spherical.radius, phiDiff * this.data.rotateToSpeed, thetaDiff * this.data.rotateToSpeed); 896 | } 897 | 898 | var offset = new THREE.Vector3(); 899 | 900 | var quat = new THREE.Quaternion().setFromUnitVectors(this.dolly.up, new THREE.Vector3(0, 1, 0)); // so camera.up is the orbit axis 901 | var quatInverse = quat.clone().inverse(); 902 | 903 | offset.copy(this.dolly.position).sub(this.target); 904 | offset.applyQuaternion(quat); // rotate offset to "y-axis-is-up" space 905 | this.spherical.setFromVector3(offset); // angle from z-axis around y-axis 906 | 907 | if (this.data.autoRotate && this.state === this.STATE.NONE) this.rotateLeft(this.getAutoRotationAngle()); 908 | 909 | this.spherical.theta += this.sphericalDelta.theta; 910 | this.spherical.phi += this.sphericalDelta.phi; 911 | this.spherical.theta = Math.max(this.data.minAzimuthAngle, Math.min(this.data.maxAzimuthAngle, this.spherical.theta)); // restrict theta to be inside desired limits 912 | this.spherical.phi = Math.max(this.data.minPolarAngle, Math.min(this.data.maxPolarAngle, this.spherical.phi)); // restrict phi to be inside desired limits 913 | this.spherical.makeSafe(); 914 | this.spherical.radius *= this.scale; 915 | this.spherical.radius = Math.max(this.data.minDistance, Math.min(this.data.maxDistance, this.spherical.radius)); // restrict radius to be inside desired limits 916 | 917 | this.target.add(this.panOffset); // move target to panned location 918 | 919 | offset.setFromSpherical(this.spherical); 920 | offset.applyQuaternion(quatInverse); // rotate offset back to "camera-up-vector-is-up" space 921 | 922 | this.dolly.position.copy(this.target).add(offset); 923 | 924 | if (this.target) { 925 | this.lookAtTarget(this.dolly, this.target); 926 | } 927 | 928 | if (this.data.enableDamping === true) { 929 | this.sphericalDelta.theta *= (1 - this.data.dampingFactor); 930 | this.sphericalDelta.phi *= (1 - this.data.dampingFactor); 931 | } else { 932 | this.sphericalDelta.set(0, 0, 0); 933 | } 934 | 935 | this.scale = 1; 936 | this.panOffset.set(0, 0, 0); 937 | 938 | // update condition is: 939 | // min(camera displacement, camera rotation in radians)^2 > EPS 940 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 941 | 942 | if (forceUpdate === true || 943 | this.zoomChanged || 944 | this.lastPosition.distanceToSquared(this.dolly.position) > this.EPS || 945 | 8 * (1 - this.lastQuaternion.dot(this.dolly.quaternion)) > this.EPS) { 946 | // this.el.emit('change-drag-orbit-controls', null, false); 947 | 948 | var hmdQuaternion = this.calculateHMDQuaternion(); 949 | var hmdEuler = new THREE.Euler(); 950 | hmdEuler.setFromQuaternion(hmdQuaternion, 'YXZ'); 951 | 952 | this.el.setAttribute('position', { 953 | x: this.dolly.position.x, 954 | y: this.dolly.position.y, 955 | z: this.dolly.position.z 956 | }); 957 | 958 | this.el.setAttribute('rotation', { 959 | x: radToDeg(hmdEuler.x), 960 | y: radToDeg(hmdEuler.y), 961 | z: radToDeg(hmdEuler.z) 962 | }); 963 | 964 | this.lastPosition.copy(this.dolly.position); 965 | this.lastQuaternion.copy(this.dolly.quaternion); 966 | 967 | this.zoomChanged = false; 968 | 969 | return true; 970 | } 971 | 972 | return false; 973 | }, 974 | 975 | calculateHMDQuaternion: (function () { 976 | var hmdQuaternion = new THREE.Quaternion(); 977 | return function () { 978 | hmdQuaternion.copy(this.dolly.quaternion); 979 | return hmdQuaternion; 980 | }; 981 | })() 982 | 983 | }); 984 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-orbit-controls-component-2", 3 | "version": "0.1.14", 4 | "description": "A (almost) direct port of the ThreeJS OrbitControls as a component for A-Frame.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "browserify examples/main.js -o examples/build.js", 8 | "dev": "budo examples/main.js:build.js --dir examples --port 8000 --live --open", 9 | "dist": "webpack index.js dist/aframe-orbit-controls-component.js && webpack -p index.js dist/aframe-orbit-controls-component.min.js", 10 | "lint": "semistandard -v | snazzy", 11 | "postpublish": "npm run dist", 12 | "preghpages": "npm run build && shx rm -rf gh-pages && shx mkdir gh-pages && shx cp -r examples/* gh-pages", 13 | "ghpages": "npm run preghpages && ghpages -p gh-pages", 14 | "unboil": "node scripts/unboil.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/tizzle/aframe-orbit-controls-component.git" 19 | }, 20 | "keywords": [ 21 | "aframe", 22 | "aframe-component", 23 | "aframe-vr", 24 | "vr", 25 | "mozvr", 26 | "webvr" 27 | ], 28 | "author": "Till Hinrichs ", 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/tizzle/aframe-orbit-controls-component/issues" 32 | }, 33 | "homepage": "https://github.com/tizzle/aframe-orbit-controls-component#readme", 34 | "devDependencies": { 35 | "aframe": "^0.7.0", 36 | "browserify": "^13.0.0", 37 | "browserify-css": "^0.9.1", 38 | "budo": "^8.2.2", 39 | "ghpages": "^0.0.8", 40 | "inquirer": "^1.0.2", 41 | "randomcolor": "^0.4.4", 42 | "semistandard": "^8.0.0", 43 | "shelljs": "^0.7.0", 44 | "shx": "^0.1.1", 45 | "snazzy": "^4.0.0", 46 | "webpack": "^1.13.0" 47 | }, 48 | "semistandard": { 49 | "ignore": [ 50 | "examples/build.js", 51 | "dist/**" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/unboil.js: -------------------------------------------------------------------------------- 1 | /* global find, ls, sed */ 2 | require('shelljs/global'); 3 | var exec = require('child_process').exec; 4 | var inquirer = require('inquirer'); 5 | 6 | // You can manually add configuration options here if you don't want to go through the 7 | // interactive script or if the interactive script is not working. 8 | var CONFIG = { 9 | // What is your component's short-name? (e.g., `rick-roll` for aframe-rick-roll-component). 10 | shortname: '', 11 | // What is your component's long-name? (e.g., `Rick Roll` for A-Frame Rick Roll Component). 12 | longname: '', 13 | // Where is your component on GitHub (e.g., yourusername/aframe-rick-roll-component). 14 | repo: '', 15 | // Who are you? (e.g., Jane John ). 16 | author: '' 17 | }; 18 | 19 | // --- 20 | 21 | exec("sed '1,/--trim--/d' README.md | tee README.md"); 22 | 23 | if (CONFIG.shortname && CONFIG.longname && CONFIG.repo) { 24 | run(CONFIG); 25 | process.exit(0); 26 | } 27 | 28 | var q1 = { 29 | name: 'shortname', 30 | message: 'What is your component\'s short-name? (e.g., `rick-roll` for aframe-rick-roll-component, ``)', 31 | type: 'input' 32 | }; 33 | 34 | var q2 = { 35 | name: 'longname', 36 | message: 'What is your component\'s long-name? (e.g., `Rick Roll` for A-Frame Rick Roll Component)', 37 | type: 'input' 38 | }; 39 | 40 | var q3 = { 41 | name: 'repo', 42 | message: 'Where is your component on Github? (e.g., yourusername/aframe-rick-roll-component)', 43 | type: 'input' 44 | }; 45 | 46 | var q4 = { 47 | name: 'author', 48 | message: 'Who are you? (e.g., Jane John )', 49 | type: 'input' 50 | }; 51 | 52 | inquirer.prompt([q1, q2, q3, q4], run); 53 | 54 | function run (ans) { 55 | ls(['index.js', 'package.json', 'README.md']).forEach(function (file) { 56 | sed('-i', 'aframe-example-component', 'aframe-' + ans.shortname + '-component', file); 57 | sed('-i', 'Example Component', ans.longname + ' Component', file); 58 | sed('-i', 'Example component', ans.longname + ' component', file); 59 | sed('-i', "'example'", "'" + ans.shortname + "'", file); 60 | }); 61 | 62 | ls('README.md').forEach(function (file) { 63 | sed('-i', 'example component', ans.longname + ' component', file); 64 | sed('-i', 'example=', ans.shortname + '=', file); 65 | }); 66 | 67 | find('examples').filter(function (file) { return file.match(/\.html/); }).forEach(function (file) { 68 | sed('-i', 'Example Component', ans.longname + ' Component', file); 69 | sed('-i', 'ngokevin/aframe-component-boilerplate', ans.repo, file); 70 | }); 71 | 72 | ls(['package.json', 'README.md']).forEach(function (file) { 73 | sed('-i', 'aframe-example-component', 'aframe-' + ans.shortname + '-component', file); 74 | sed('-i', 'ngokevin/aframe-component-boilerplate', ans.repo, file); 75 | sed('-i', 'Kevin Ngo ', ans.author, file); 76 | }); 77 | } 78 | --------------------------------------------------------------------------------