├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── aframe-camera-transform-controls-component.js └── aframe-camera-transform-controls-component.min.js ├── examples ├── basic.png ├── basic │ └── index.html ├── scene.png └── scene │ ├── desert.glb │ └── index.html ├── index.html ├── index.js ├── package-lock.json ├── package.json ├── readme.gif └── tests ├── __init.test.js ├── helpers.js ├── index.test.js └── karma.conf.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.glb binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sw[ponm] 3 | examples/build.js 4 | gh-pages 5 | node_modules/ 6 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | addons: 3 | firefox: 'latest' 4 | node_js: 5 | - '6.9' 6 | 7 | install: 8 | - npm install 9 | - ./node_modules/.bin/mozilla-download ./firefox/ --product firefox --branch mozilla-aurora 10 | - export FIREFOX_NIGHTLY_BIN="./firefox/firefox/firefox-bin" 11 | 12 | before_script: 13 | - export DISPLAY=:99.0 14 | - sh -e /etc/init.d/xvfb start 15 | 16 | script: 17 | - $CI_ACTION 18 | 19 | env: 20 | global: 21 | - TEST_SUITE=unit 22 | matrix: 23 | - CI_ACTION="npm run test" 24 | - CI_ACTION="npm run dist" 25 | # - CI_ACTION="npm run lint" 26 | 27 | branches: 28 | only: 29 | - master 30 | 31 | cache: 32 | directories: 33 | - node_modules 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Fernando Serrano <fernandojsg@gmail.com> 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## aframe-camera-transform-controls-component 2 | 3 | [![Version](http://img.shields.io/npm/v/aframe-camera-transform-controls-component.svg?style=flat-square)](https://npmjs.org/package/aframe-camera-transform-controls-component) 4 | [![License](http://img.shields.io/npm/l/aframe-camera-transform-controls-component.svg?style=flat-square)](https://npmjs.org/package/aframe-camera-transform-controls-component) 5 | 6 | A Camera Transform Controls component for [A-Frame](https://aframe.io). 7 | 8 |

9 | Recording component 10 |

11 | 12 | ### API 13 | 14 | | Property | Description | Default Value | 15 | | -------- | ----------- | ------------- | 16 | | enabled | | true | 17 | | cameraRigId | Camera rig containing the camera and both controllers | cameraRig | 18 | | onStart | Event used to start the panning or scale & rotate | triggerdown | 19 | | onEnd | Event used to stop panning or scale & rotate | triggerup | 20 | | showHint | Show a line between both controllers and the scale factor | true | 21 | 22 | 23 | ### Installation 24 | 25 | #### Browser 26 | 27 | Install and use by directly including the [browser files](dist): 28 | 29 | ```html 30 | 31 | My A-Frame Scene 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ``` 41 | 42 | #### npm 43 | 44 | Install via npm: 45 | 46 | ```bash 47 | npm install aframe-camera-transform-controls-component 48 | ``` 49 | 50 | Then require and use. 51 | 52 | ```js 53 | require('aframe'); 54 | require('aframe-camera-transform-controls-component'); 55 | ``` 56 | -------------------------------------------------------------------------------- /dist/aframe-camera-transform-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 */ 48 | 49 | if (typeof AFRAME === 'undefined') { 50 | throw new Error('Component attempted to register before AFRAME was available.'); 51 | } 52 | 53 | AFRAME.registerComponent('hint-scale', { 54 | schema: { 55 | rightEl: {type: 'selector'}, 56 | leftEl: {type: 'selector'}, 57 | text: {default: '1.0x'} 58 | }, 59 | 60 | init: function () { 61 | var el = this.el; 62 | this.rightEl = this.leftEl = null; 63 | var middle = this.middle = document.createElement('a-sphere'); 64 | var text = this.text = document.createElement('a-entity'); 65 | var line = this.line = document.createElement('a-entity'); 66 | 67 | this.cameraObject = this.el.parentElement.querySelector('[camera]').object3D; 68 | 69 | el.appendChild(middle); 70 | el.appendChild(text); 71 | el.appendChild(line); 72 | 73 | this.rightEl = document.getElementById('righthand'); 74 | this.leftEl = document.getElementById('lefthand'); 75 | 76 | middle.setAttribute('radius', '0.003'); 77 | middle.setAttribute('color', '#000'); 78 | 79 | line.setAttribute('line', {color: 'black'}); 80 | text.setAttribute('text', {color: 'black', align: 'center', value: '1.0x', width: 0.5}); 81 | }, 82 | 83 | update: function (oldData) { 84 | if (oldData.text !== this.data.text) { 85 | this.text.setAttribute('text', {value: this.data.text}); 86 | } 87 | }, 88 | 89 | tick: (function () { 90 | return function () { 91 | 92 | var linePosL = new THREE.Vector3(); 93 | var linePosR = new THREE.Vector3(); 94 | var mid = new THREE.Vector3(); 95 | 96 | if (this.el.getAttribute('visible') === true) { 97 | 98 | var posL = this.leftEl.getAttribute('position'); 99 | var posR = this.rightEl.getAttribute('position'); 100 | 101 | mid.subVectors(posL, posR).multiplyScalar(0.5).add(posR); 102 | this.middle.setAttribute('position', mid); 103 | 104 | mid.y += 0.025; 105 | this.text.setAttribute('position', mid); 106 | this.text.object3D.lookAt(this.cameraObject.position); 107 | 108 | linePosR.copy(posR); 109 | linePosL.copy(posL); 110 | 111 | this.line.setAttribute('line', {start: linePosL, end: linePosR}); 112 | } 113 | } 114 | })() 115 | }); 116 | 117 | /** 118 | * Camera Transform Controls component for A-Frame. 119 | */ 120 | var UP = new THREE.Vector3(0, 1, 0); 121 | AFRAME.registerComponent('camera-transform-controls', { 122 | schema: { 123 | enabled: {default: true}, 124 | cameraRigId: {default: 'cameraRig'}, 125 | onStart: {default: 'gripdown'}, 126 | onEnd: {default: 'gripup'}, 127 | showHint: {default: true} 128 | }, 129 | 130 | init: function () { 131 | this.cameraRigEl = document.getElementById(this.data.cameraRigId); 132 | 133 | var hintEl = this.hintEl = document.createElement('a-entity'); 134 | hintEl.setAttribute('hint-scale', ''); 135 | hintEl.setAttribute('visible', false); 136 | this.cameraRigEl.appendChild(hintEl); 137 | 138 | this.currentDragCenter = new THREE.Vector3(); 139 | this.panningController = null; 140 | 141 | this.controllers = { 142 | left: { 143 | entity: null, 144 | dragging: false, 145 | dragStartPoint: new THREE.Vector3() 146 | }, 147 | right: { 148 | entity: null, 149 | dragging: false, 150 | dragStartPoint: new THREE.Vector3() 151 | } 152 | }; 153 | 154 | this.originalPosition = new THREE.Vector3(); 155 | this.originalScale = new THREE.Vector3(); 156 | this.originalRotation = new THREE.Vector3(); 157 | 158 | this.isLeftButtonDown = false; 159 | this.isRightButtonDown = false; 160 | 161 | this.cameraScaleEventDetail = {cameraScaleFactor: 1}; 162 | }, 163 | 164 | /** 165 | * Reset original camera rig transforms if disabling camera scaler. 166 | */ 167 | update: function (oldData) { 168 | var cameraRigEl = this.cameraRigEl; 169 | 170 | if (!cameraRigEl) {return;} 171 | 172 | // Enabling. Store original transformations. 173 | if (!oldData.enabled && this.data.enabled) { 174 | this.originalPosition.copy(cameraRigEl.object3D.position); 175 | this.originalScale.copy(cameraRigEl.object3D.scale); 176 | this.originalRotation.copy(cameraRigEl.object3D.rotation); 177 | } 178 | 179 | // Disabling, reset to original transformations. 180 | if (oldData.enabled && !this.data.enabled) { 181 | cameraRigEl.setAttribute('position', this.originalPosition); 182 | cameraRigEl.setAttribute('scale', this.originalScale); 183 | cameraRigEl.setAttribute('rotation', this.originalRotation.clone()); 184 | } 185 | }, 186 | 187 | tick: function () { 188 | this.hintEl.setAttribute('visible', false); 189 | 190 | if (!this.data.enabled) { return; } 191 | 192 | if (!this.isLeftButtonDown && !this.isRightButtonDown) { return; } 193 | 194 | if (this.isLeftButtonDown && this.isRightButtonDown) { 195 | this.twoHandInteraction(); 196 | this.hintEl.setAttribute('visible', this.data.showHint); 197 | } else { 198 | this.processPanning(); 199 | } 200 | }, 201 | 202 | onButtonDown: function (evt) { 203 | var left; 204 | var target; 205 | 206 | if (!this.cameraRigEl.object3D) { return; } 207 | 208 | target = evt.target; 209 | left = target === this.leftHandEl; 210 | 211 | if (left) { 212 | this.isLeftButtonDown = true; 213 | this.panningController = this.controllers.left; 214 | } else { 215 | this.isRightButtonDown = true; 216 | this.panningController = this.controllers.right; 217 | } 218 | 219 | this.panningController.entity.object3D.getWorldPosition( 220 | this.panningController.dragStartPoint); 221 | 222 | this.released = this.isLeftButtonDown && this.isRightButtonDown; 223 | }, 224 | 225 | onButtonUp: function (evt) { 226 | var left; 227 | var target; 228 | 229 | target = evt.target; 230 | left = evt.target === this.leftHandEl; 231 | 232 | if (left) { 233 | this.panningController = this.controllers.right; 234 | this.isLeftButtonDown = false; 235 | } else { 236 | this.panningController = this.controllers.left; 237 | this.isRightButtonDown = false; 238 | } 239 | 240 | this.panningController.entity.object3D.getWorldPosition( 241 | this.panningController.dragStartPoint); 242 | 243 | if (!this.isLeftButtonDown && !this.isRightButtonDown) { 244 | this.cameraScaleEventDetail.cameraScaleFactor = this.cameraRigEl.object3D.scale.x; 245 | this.el.emit('camerascale', this.cameraScaleEventDetail); 246 | } 247 | 248 | this.released = true; 249 | }, 250 | 251 | /** 252 | * With two hands, translate/rotate/zoom. 253 | */ 254 | twoHandInteraction: (function () { 255 | var centerVec3 = new THREE.Vector3(); 256 | var currentDistanceVec3 = new THREE.Vector3(); 257 | var currentPositionLeft = new THREE.Vector3(); 258 | var currentPositionRight = new THREE.Vector3(); 259 | var midPoint = new THREE.Vector3(); 260 | var prevDistanceVec3 = new THREE.Vector3(); 261 | 262 | return function () { 263 | var currentAngle; 264 | var currentDistance; 265 | var deltaAngle; 266 | var deltaDistance; 267 | var translation; 268 | 269 | this.leftHandEl.object3D.getWorldPosition(currentPositionLeft); 270 | this.rightHandEl.object3D.getWorldPosition(currentPositionRight); 271 | 272 | if (this.released) { 273 | this.prevAngle = signedAngleTo(currentPositionLeft, currentPositionRight); 274 | this.initAngle = this.prevAngle = Math.atan2( 275 | currentPositionLeft.x - currentPositionRight.x, 276 | currentPositionLeft.z - currentPositionRight.z); 277 | midPoint.copy(currentPositionLeft) 278 | .add(currentPositionRight) 279 | .multiplyScalar(0.5); 280 | this.prevDistance = prevDistanceVec3.copy(currentPositionLeft) 281 | .sub(currentPositionRight) 282 | .length(); 283 | this.released = false; 284 | } 285 | 286 | currentDistance = currentDistanceVec3.copy(currentPositionLeft) 287 | .sub(currentPositionRight) 288 | .length(); 289 | deltaDistance = this.prevDistance - currentDistance; 290 | 291 | //Get center point using local positions. 292 | centerVec3.copy(this.leftHandEl.object3D.position) 293 | .add(this.rightHandEl.object3D.position) 294 | .multiplyScalar(0.5); 295 | 296 | // Set camera rig scale. 297 | this.cameraRigEl.object3D.scale.addScalar(deltaDistance); 298 | this.cameraRigEl.setAttribute('scale', this.cameraRigEl.object3D.scale); 299 | this.hintEl.setAttribute('hint-scale', {text: this.cameraRigEl.object3D.scale.x.toFixed(2) + 'x'}); 300 | 301 | // Set camera rig position. 302 | translation = centerVec3 303 | .applyQuaternion(this.cameraRigEl.object3D.quaternion) 304 | .multiplyScalar(deltaDistance); 305 | this.cameraRigEl.object3D.position.sub(translation); 306 | this.cameraRigEl.setAttribute('position', this.cameraRigEl.object3D.position); 307 | 308 | // Set camera rig rotation. 309 | currentAngle = Math.atan2(currentPositionLeft.x - currentPositionRight.x, 310 | currentPositionLeft.z - currentPositionRight.z); 311 | deltaAngle = currentAngle - this.prevAngle; 312 | this.rotateScene(midPoint, deltaAngle); 313 | 314 | this.prevAngle = currentAngle - deltaAngle; 315 | } 316 | })(), 317 | 318 | rotateScene: (function () { 319 | var dirVec3 = new THREE.Vector3(); 320 | 321 | return function (midPoint, deltaAngle) { 322 | var cameraRigEl = this.cameraRigEl; 323 | var rotation; 324 | 325 | // Rotate the direction. 326 | dirVec3.copy(cameraRigEl.object3D.position) 327 | .sub(midPoint) 328 | .applyAxisAngle(UP, -deltaAngle); 329 | 330 | cameraRigEl.object3D.position.copy(midPoint).add(dirVec3); 331 | cameraRigEl.setAttribute('position', cameraRigEl.object3D.position); 332 | 333 | rotation = cameraRigEl.getAttribute('rotation'); 334 | rotation.y -= deltaAngle * THREE.Math.RAD2DEG; 335 | cameraRigEl.setAttribute('rotation', rotation); 336 | }; 337 | })(), 338 | 339 | /** 340 | * One hand panning. 341 | */ 342 | processPanning: (function () { 343 | var currentPosition = new THREE.Vector3(); 344 | var deltaPosition = new THREE.Vector3(); 345 | 346 | return function () { 347 | var dragStartPoint = this.panningController.dragStartPoint; 348 | this.panningController.entity.object3D.getWorldPosition(currentPosition); 349 | deltaPosition.copy(dragStartPoint).sub(currentPosition); 350 | 351 | // Apply panning. 352 | this.cameraRigEl.object3D.position.add(deltaPosition); 353 | this.cameraRigEl.setAttribute('position', this.cameraRigEl.object3D.position); 354 | }; 355 | })(), 356 | 357 | registerHand: function (entity, hand) { 358 | this.controllers[hand].entity = entity; 359 | entity.addEventListener(this.data.onStart, this.onButtonDown.bind(this)); 360 | entity.addEventListener(this.data.onEnd, this.onButtonUp.bind(this)); 361 | 362 | if (hand === 'left') { 363 | this.leftHandEl = entity; 364 | } else { 365 | this.rightHandEl = entity; 366 | } 367 | } 368 | }); 369 | 370 | AFRAME.registerComponent('camera-transform-controls-hand', { 371 | schema: { 372 | hand: {default: 'right'} 373 | }, 374 | 375 | play: function () { 376 | this.el.sceneEl.components['camera-transform-controls'].registerHand(this.el, this.data.hand); 377 | } 378 | }); 379 | 380 | function signedAngleTo (fromVec3, toVec3) { 381 | var angle; 382 | var cross; 383 | angle = fromVec3.angleTo(toVec3); 384 | cross = fromVec3.clone().cross(toVec3); 385 | if (UP.dot(cross) < 0) { // Or > 0. 386 | angle = -angle; 387 | } 388 | return angle; 389 | } 390 | 391 | /***/ }) 392 | /******/ ]); -------------------------------------------------------------------------------- /dist/aframe-camera-transform-controls-component.min.js: -------------------------------------------------------------------------------- 1 | !function(t){function e(n){if(i[n])return i[n].exports;var o=i[n]={exports:{},id:n,loaded:!1};return t[n].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var i={};return e.m=t,e.c=i,e.p="",e(0)}([function(t,e){function i(t,e){var i,o;return i=t.angleTo(e),o=t.clone().cross(e),n.dot(o)<0&&(i=-i),i}if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");AFRAME.registerComponent("hint-scale",{schema:{rightEl:{type:"selector"},leftEl:{type:"selector"},text:{default:"1.0x"}},init:function(){var t=this.el;this.rightEl=this.leftEl=null;var e=this.middle=document.createElement("a-sphere"),i=this.text=document.createElement("a-entity"),n=this.line=document.createElement("a-entity");this.cameraObject=this.el.parentElement.querySelector("[camera]").object3D,t.appendChild(e),t.appendChild(i),t.appendChild(n),this.rightEl=document.getElementById("righthand"),this.leftEl=document.getElementById("lefthand"),e.setAttribute("radius","0.003"),e.setAttribute("color","#000"),n.setAttribute("line",{color:"black"}),i.setAttribute("text",{color:"black",align:"center",value:"1.0x",width:.5})},update:function(t){t.text!==this.data.text&&this.text.setAttribute("text",{value:this.data.text})},tick:function(){return function(){var t=new THREE.Vector3,e=new THREE.Vector3,i=new THREE.Vector3;if(this.el.getAttribute("visible")===!0){var n=this.leftEl.getAttribute("position"),o=this.rightEl.getAttribute("position");i.subVectors(n,o).multiplyScalar(.5).add(o),this.middle.setAttribute("position",i),i.y+=.025,this.text.setAttribute("position",i),this.text.object3D.lookAt(this.cameraObject.position),e.copy(o),t.copy(n),this.line.setAttribute("line",{start:t,end:e})}}}()});var n=new THREE.Vector3(0,1,0);AFRAME.registerComponent("camera-transform-controls",{schema:{enabled:{default:!0},cameraRigId:{default:"cameraRig"},onStart:{default:"gripdown"},onEnd:{default:"gripup"},showHint:{default:!0}},init:function(){this.cameraRigEl=document.getElementById(this.data.cameraRigId);var t=this.hintEl=document.createElement("a-entity");t.setAttribute("hint-scale",""),t.setAttribute("visible",!1),this.cameraRigEl.appendChild(t),this.currentDragCenter=new THREE.Vector3,this.panningController=null,this.controllers={left:{entity:null,dragging:!1,dragStartPoint:new THREE.Vector3},right:{entity:null,dragging:!1,dragStartPoint:new THREE.Vector3}},this.originalPosition=new THREE.Vector3,this.originalScale=new THREE.Vector3,this.originalRotation=new THREE.Vector3,this.isLeftButtonDown=!1,this.isRightButtonDown=!1,this.cameraScaleEventDetail={cameraScaleFactor:1}},update:function(t){var e=this.cameraRigEl;e&&(!t.enabled&&this.data.enabled&&(this.originalPosition.copy(e.object3D.position),this.originalScale.copy(e.object3D.scale),this.originalRotation.copy(e.object3D.rotation)),t.enabled&&!this.data.enabled&&(e.setAttribute("position",this.originalPosition),e.setAttribute("scale",this.originalScale),e.setAttribute("rotation",this.originalRotation.clone())))},tick:function(){this.hintEl.setAttribute("visible",!1),this.data.enabled&&(this.isLeftButtonDown||this.isRightButtonDown)&&(this.isLeftButtonDown&&this.isRightButtonDown?(this.twoHandInteraction(),this.hintEl.setAttribute("visible",this.data.showHint)):this.processPanning())},onButtonDown:function(t){var e,i;this.cameraRigEl.object3D&&(i=t.target,e=i===this.leftHandEl,e?(this.isLeftButtonDown=!0,this.panningController=this.controllers.left):(this.isRightButtonDown=!0,this.panningController=this.controllers.right),this.panningController.entity.object3D.getWorldPosition(this.panningController.dragStartPoint),this.released=this.isLeftButtonDown&&this.isRightButtonDown)},onButtonUp:function(t){var e,i;i=t.target,e=t.target===this.leftHandEl,e?(this.panningController=this.controllers.right,this.isLeftButtonDown=!1):(this.panningController=this.controllers.left,this.isRightButtonDown=!1),this.panningController.entity.object3D.getWorldPosition(this.panningController.dragStartPoint),this.isLeftButtonDown||this.isRightButtonDown||(this.cameraScaleEventDetail.cameraScaleFactor=this.cameraRigEl.object3D.scale.x,this.el.emit("camerascale",this.cameraScaleEventDetail)),this.released=!0},twoHandInteraction:function(){var t=new THREE.Vector3,e=new THREE.Vector3,n=new THREE.Vector3,o=new THREE.Vector3,a=new THREE.Vector3,r=new THREE.Vector3;return function(){var s,l,c,h,d;this.leftHandEl.object3D.getWorldPosition(n),this.rightHandEl.object3D.getWorldPosition(o),this.released&&(this.prevAngle=i(n,o),this.initAngle=this.prevAngle=Math.atan2(n.x-o.x,n.z-o.z),a.copy(n).add(o).multiplyScalar(.5),this.prevDistance=r.copy(n).sub(o).length(),this.released=!1),l=e.copy(n).sub(o).length(),h=this.prevDistance-l,t.copy(this.leftHandEl.object3D.position).add(this.rightHandEl.object3D.position).multiplyScalar(.5),this.cameraRigEl.object3D.scale.addScalar(h),this.cameraRigEl.setAttribute("scale",this.cameraRigEl.object3D.scale),this.hintEl.setAttribute("hint-scale",{text:this.cameraRigEl.object3D.scale.x.toFixed(2)+"x"}),d=t.applyQuaternion(this.cameraRigEl.object3D.quaternion).multiplyScalar(h),this.cameraRigEl.object3D.position.sub(d),this.cameraRigEl.setAttribute("position",this.cameraRigEl.object3D.position),s=Math.atan2(n.x-o.x,n.z-o.z),c=s-this.prevAngle,this.rotateScene(a,c),this.prevAngle=s-c}}(),rotateScene:function(){var t=new THREE.Vector3;return function(e,i){var o,a=this.cameraRigEl;t.copy(a.object3D.position).sub(e).applyAxisAngle(n,-i),a.object3D.position.copy(e).add(t),a.setAttribute("position",a.object3D.position),o=a.getAttribute("rotation"),o.y-=i*THREE.Math.RAD2DEG,a.setAttribute("rotation",o)}}(),processPanning:function(){var t=new THREE.Vector3,e=new THREE.Vector3;return function(){var i=this.panningController.dragStartPoint;this.panningController.entity.object3D.getWorldPosition(t),e.copy(i).sub(t),this.cameraRigEl.object3D.position.add(e),this.cameraRigEl.setAttribute("position",this.cameraRigEl.object3D.position)}}(),registerHand:function(t,e){this.controllers[e].entity=t,t.addEventListener(this.data.onStart,this.onButtonDown.bind(this)),t.addEventListener(this.data.onEnd,this.onButtonUp.bind(this)),"left"===e?this.leftHandEl=t:this.rightHandEl=t}}),AFRAME.registerComponent("camera-transform-controls-hand",{schema:{hand:{default:"right"}},play:function(){this.el.sceneEl.components["camera-transform-controls"].registerHand(this.el,this.data.hand)}})}]); -------------------------------------------------------------------------------- /examples/basic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fernandojsg/aframe-camera-transform-controls-component/e4c1e1378c234e2ce8b8630c0353cb3d1a8beffb/examples/basic.png -------------------------------------------------------------------------------- /examples/basic/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example Scene 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/scene.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fernandojsg/aframe-camera-transform-controls-component/e4c1e1378c234e2ce8b8630c0353cb3d1a8beffb/examples/scene.png -------------------------------------------------------------------------------- /examples/scene/desert.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fernandojsg/aframe-camera-transform-controls-component/e4c1e1378c234e2ce8b8630c0353cb3d1a8beffb/examples/scene/desert.glb -------------------------------------------------------------------------------- /examples/scene/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Camera Transform Controls Component - Basic 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Camera Transform Controls Component 6 | 7 | 48 | 49 | 50 |

A-Frame Camera Transform Controls Component

51 | 52 | 59 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME */ 2 | 3 | if (typeof AFRAME === 'undefined') { 4 | throw new Error('Component attempted to register before AFRAME was available.'); 5 | } 6 | 7 | AFRAME.registerComponent('hint-scale', { 8 | schema: { 9 | rightEl: {type: 'selector'}, 10 | leftEl: {type: 'selector'}, 11 | text: {default: '1.0x'} 12 | }, 13 | 14 | init: function () { 15 | var el = this.el; 16 | this.rightEl = this.leftEl = null; 17 | var middle = this.middle = document.createElement('a-sphere'); 18 | var text = this.text = document.createElement('a-entity'); 19 | var line = this.line = document.createElement('a-entity'); 20 | 21 | this.cameraObject = this.el.parentElement.querySelector('[camera]').object3D; 22 | 23 | el.appendChild(middle); 24 | el.appendChild(text); 25 | el.appendChild(line); 26 | 27 | this.rightEl = document.getElementById('righthand'); 28 | this.leftEl = document.getElementById('lefthand'); 29 | 30 | middle.setAttribute('radius', '0.003'); 31 | middle.setAttribute('color', '#000'); 32 | 33 | line.setAttribute('line', {color: 'black'}); 34 | text.setAttribute('text', {color: 'black', align: 'center', value: '1.0x', width: 0.5}); 35 | }, 36 | 37 | update: function (oldData) { 38 | if (oldData.text !== this.data.text) { 39 | this.text.setAttribute('text', {value: this.data.text}); 40 | } 41 | }, 42 | 43 | tick: (function () { 44 | return function () { 45 | 46 | var linePosL = new THREE.Vector3(); 47 | var linePosR = new THREE.Vector3(); 48 | var mid = new THREE.Vector3(); 49 | 50 | if (this.el.getAttribute('visible') === true) { 51 | 52 | var posL = this.leftEl.getAttribute('position'); 53 | var posR = this.rightEl.getAttribute('position'); 54 | 55 | mid.subVectors(posL, posR).multiplyScalar(0.5).add(posR); 56 | this.middle.setAttribute('position', mid); 57 | 58 | mid.y += 0.025; 59 | this.text.setAttribute('position', mid); 60 | this.text.object3D.lookAt(this.cameraObject.position); 61 | 62 | linePosR.copy(posR); 63 | linePosL.copy(posL); 64 | 65 | this.line.setAttribute('line', {start: linePosL, end: linePosR}); 66 | } 67 | } 68 | })() 69 | }); 70 | 71 | /** 72 | * Camera Transform Controls component for A-Frame. 73 | */ 74 | var UP = new THREE.Vector3(0, 1, 0); 75 | AFRAME.registerComponent('camera-transform-controls', { 76 | schema: { 77 | enabled: {default: true}, 78 | cameraRigId: {default: 'cameraRig'}, 79 | onStart: {default: 'gripdown'}, 80 | onEnd: {default: 'gripup'}, 81 | showHint: {default: true} 82 | }, 83 | 84 | init: function () { 85 | this.cameraRigEl = document.getElementById(this.data.cameraRigId); 86 | 87 | var hintEl = this.hintEl = document.createElement('a-entity'); 88 | hintEl.setAttribute('hint-scale', ''); 89 | hintEl.setAttribute('visible', false); 90 | this.cameraRigEl.appendChild(hintEl); 91 | 92 | this.currentDragCenter = new THREE.Vector3(); 93 | this.panningController = null; 94 | 95 | this.controllers = { 96 | left: { 97 | entity: null, 98 | dragging: false, 99 | dragStartPoint: new THREE.Vector3() 100 | }, 101 | right: { 102 | entity: null, 103 | dragging: false, 104 | dragStartPoint: new THREE.Vector3() 105 | } 106 | }; 107 | 108 | this.originalPosition = new THREE.Vector3(); 109 | this.originalScale = new THREE.Vector3(); 110 | this.originalRotation = new THREE.Vector3(); 111 | 112 | this.isLeftButtonDown = false; 113 | this.isRightButtonDown = false; 114 | 115 | this.cameraScaleEventDetail = {cameraScaleFactor: 1}; 116 | }, 117 | 118 | /** 119 | * Reset original camera rig transforms if disabling camera scaler. 120 | */ 121 | update: function (oldData) { 122 | var cameraRigEl = this.cameraRigEl; 123 | 124 | if (!cameraRigEl) {return;} 125 | 126 | // Enabling. Store original transformations. 127 | if (!oldData.enabled && this.data.enabled) { 128 | this.originalPosition.copy(cameraRigEl.object3D.position); 129 | this.originalScale.copy(cameraRigEl.object3D.scale); 130 | this.originalRotation.copy(cameraRigEl.object3D.rotation); 131 | } 132 | 133 | // Disabling, reset to original transformations. 134 | if (oldData.enabled && !this.data.enabled) { 135 | cameraRigEl.setAttribute('position', this.originalPosition); 136 | cameraRigEl.setAttribute('scale', this.originalScale); 137 | cameraRigEl.setAttribute('rotation', this.originalRotation.clone()); 138 | } 139 | }, 140 | 141 | tick: function () { 142 | this.hintEl.setAttribute('visible', false); 143 | 144 | if (!this.data.enabled) { return; } 145 | 146 | if (!this.isLeftButtonDown && !this.isRightButtonDown) { return; } 147 | 148 | if (this.isLeftButtonDown && this.isRightButtonDown) { 149 | this.twoHandInteraction(); 150 | this.hintEl.setAttribute('visible', this.data.showHint); 151 | } else { 152 | this.processPanning(); 153 | } 154 | }, 155 | 156 | onButtonDown: function (evt) { 157 | var left; 158 | var target; 159 | 160 | if (!this.cameraRigEl.object3D) { return; } 161 | 162 | target = evt.target; 163 | left = target === this.leftHandEl; 164 | 165 | if (left) { 166 | this.isLeftButtonDown = true; 167 | this.panningController = this.controllers.left; 168 | } else { 169 | this.isRightButtonDown = true; 170 | this.panningController = this.controllers.right; 171 | } 172 | 173 | this.panningController.entity.object3D.getWorldPosition( 174 | this.panningController.dragStartPoint); 175 | 176 | this.released = this.isLeftButtonDown && this.isRightButtonDown; 177 | }, 178 | 179 | onButtonUp: function (evt) { 180 | var left; 181 | var target; 182 | 183 | target = evt.target; 184 | left = evt.target === this.leftHandEl; 185 | 186 | if (left) { 187 | this.panningController = this.controllers.right; 188 | this.isLeftButtonDown = false; 189 | } else { 190 | this.panningController = this.controllers.left; 191 | this.isRightButtonDown = false; 192 | } 193 | 194 | this.panningController.entity.object3D.getWorldPosition( 195 | this.panningController.dragStartPoint); 196 | 197 | if (!this.isLeftButtonDown && !this.isRightButtonDown) { 198 | this.cameraScaleEventDetail.cameraScaleFactor = this.cameraRigEl.object3D.scale.x; 199 | this.el.emit('camerascale', this.cameraScaleEventDetail); 200 | } 201 | 202 | this.released = true; 203 | }, 204 | 205 | /** 206 | * With two hands, translate/rotate/zoom. 207 | */ 208 | twoHandInteraction: (function () { 209 | var centerVec3 = new THREE.Vector3(); 210 | var currentDistanceVec3 = new THREE.Vector3(); 211 | var currentPositionLeft = new THREE.Vector3(); 212 | var currentPositionRight = new THREE.Vector3(); 213 | var midPoint = new THREE.Vector3(); 214 | var prevDistanceVec3 = new THREE.Vector3(); 215 | 216 | return function () { 217 | var currentAngle; 218 | var currentDistance; 219 | var deltaAngle; 220 | var deltaDistance; 221 | var translation; 222 | 223 | this.leftHandEl.object3D.getWorldPosition(currentPositionLeft); 224 | this.rightHandEl.object3D.getWorldPosition(currentPositionRight); 225 | 226 | if (this.released) { 227 | this.prevAngle = signedAngleTo(currentPositionLeft, currentPositionRight); 228 | this.initAngle = this.prevAngle = Math.atan2( 229 | currentPositionLeft.x - currentPositionRight.x, 230 | currentPositionLeft.z - currentPositionRight.z); 231 | midPoint.copy(currentPositionLeft) 232 | .add(currentPositionRight) 233 | .multiplyScalar(0.5); 234 | this.prevDistance = prevDistanceVec3.copy(currentPositionLeft) 235 | .sub(currentPositionRight) 236 | .length(); 237 | this.released = false; 238 | } 239 | 240 | currentDistance = currentDistanceVec3.copy(currentPositionLeft) 241 | .sub(currentPositionRight) 242 | .length(); 243 | deltaDistance = this.prevDistance - currentDistance; 244 | 245 | //Get center point using local positions. 246 | centerVec3.copy(this.leftHandEl.object3D.position) 247 | .add(this.rightHandEl.object3D.position) 248 | .multiplyScalar(0.5); 249 | 250 | // Set camera rig scale. 251 | this.cameraRigEl.object3D.scale.addScalar(deltaDistance); 252 | this.cameraRigEl.setAttribute('scale', this.cameraRigEl.object3D.scale); 253 | this.hintEl.setAttribute('hint-scale', {text: this.cameraRigEl.object3D.scale.x.toFixed(2) + 'x'}); 254 | 255 | // Set camera rig position. 256 | translation = centerVec3 257 | .applyQuaternion(this.cameraRigEl.object3D.quaternion) 258 | .multiplyScalar(deltaDistance); 259 | this.cameraRigEl.object3D.position.sub(translation); 260 | this.cameraRigEl.setAttribute('position', this.cameraRigEl.object3D.position); 261 | 262 | // Set camera rig rotation. 263 | currentAngle = Math.atan2(currentPositionLeft.x - currentPositionRight.x, 264 | currentPositionLeft.z - currentPositionRight.z); 265 | deltaAngle = currentAngle - this.prevAngle; 266 | this.rotateScene(midPoint, deltaAngle); 267 | 268 | this.prevAngle = currentAngle - deltaAngle; 269 | } 270 | })(), 271 | 272 | rotateScene: (function () { 273 | var dirVec3 = new THREE.Vector3(); 274 | 275 | return function (midPoint, deltaAngle) { 276 | var cameraRigEl = this.cameraRigEl; 277 | var rotation; 278 | 279 | // Rotate the direction. 280 | dirVec3.copy(cameraRigEl.object3D.position) 281 | .sub(midPoint) 282 | .applyAxisAngle(UP, -deltaAngle); 283 | 284 | cameraRigEl.object3D.position.copy(midPoint).add(dirVec3); 285 | cameraRigEl.setAttribute('position', cameraRigEl.object3D.position); 286 | 287 | rotation = cameraRigEl.getAttribute('rotation'); 288 | rotation.y -= deltaAngle * THREE.Math.RAD2DEG; 289 | cameraRigEl.setAttribute('rotation', rotation); 290 | }; 291 | })(), 292 | 293 | /** 294 | * One hand panning. 295 | */ 296 | processPanning: (function () { 297 | var currentPosition = new THREE.Vector3(); 298 | var deltaPosition = new THREE.Vector3(); 299 | 300 | return function () { 301 | var dragStartPoint = this.panningController.dragStartPoint; 302 | this.panningController.entity.object3D.getWorldPosition(currentPosition); 303 | deltaPosition.copy(dragStartPoint).sub(currentPosition); 304 | 305 | // Apply panning. 306 | this.cameraRigEl.object3D.position.add(deltaPosition); 307 | this.cameraRigEl.setAttribute('position', this.cameraRigEl.object3D.position); 308 | }; 309 | })(), 310 | 311 | registerHand: function (entity, hand) { 312 | this.controllers[hand].entity = entity; 313 | entity.addEventListener(this.data.onStart, this.onButtonDown.bind(this)); 314 | entity.addEventListener(this.data.onEnd, this.onButtonUp.bind(this)); 315 | 316 | if (hand === 'left') { 317 | this.leftHandEl = entity; 318 | } else { 319 | this.rightHandEl = entity; 320 | } 321 | } 322 | }); 323 | 324 | AFRAME.registerComponent('camera-transform-controls-hand', { 325 | schema: { 326 | hand: {default: 'right'} 327 | }, 328 | 329 | play: function () { 330 | this.el.sceneEl.components['camera-transform-controls'].registerHand(this.el, this.data.hand); 331 | } 332 | }); 333 | 334 | function signedAngleTo (fromVec3, toVec3) { 335 | var angle; 336 | var cross; 337 | angle = fromVec3.angleTo(toVec3); 338 | cross = fromVec3.clone().cross(toVec3); 339 | if (UP.dot(cross) < 0) { // Or > 0. 340 | angle = -angle; 341 | } 342 | return angle; 343 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-camera-transform-controls-component", 3 | "version": "1.0.0", 4 | "description": "A Camera Transform Controls component for A-Frame.", 5 | "main": "index.js", 6 | "unpkg": "dist/aframe-camera-transform-controls-component.min.js", 7 | "scripts": { 8 | "dev": "budo index.js:dist/aframe-camera-transform-controls-component.min.js --port 7000 --live --open", 9 | "dist": "webpack index.js dist/aframe-camera-transform-controls-component.js && webpack -p index.js dist/aframe-camera-transform-controls-component.min.js", 10 | "lint": "semistandard -v | snazzy", 11 | "prepublish": "npm run dist", 12 | "ghpages": "ghpages", 13 | "start": "npm run dev", 14 | "test": "karma start ./tests/karma.conf.js", 15 | "test:firefox": "karma start ./tests/karma.conf.js --browsers Firefox", 16 | "test:chrome": "karma start ./tests/karma.conf.js --browsers Chrome" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/fernandojsg/aframe-camera-transform-controls-component.git" 21 | }, 22 | "keywords": [ 23 | "aframe", 24 | "aframe-component", 25 | "aframe-vr", 26 | "vr", 27 | "mozvr", 28 | "webvr", 29 | "camera-transform-controls" 30 | ], 31 | "author": "Fernando Serrano ", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/fernandojsg/aframe-camera-transform-controls-component/issues" 35 | }, 36 | "homepage": "https://github.com/fernandojsg/aframe-camera-transform-controls-component#readme", 37 | "devDependencies": { 38 | "aframe": "*", 39 | "browserify": "^13.0.0", 40 | "budo": "^8.2.2", 41 | "chai": "^3.4.1", 42 | "chai-shallow-deep-equal": "^1.3.0", 43 | "ghpages": "^0.0.8", 44 | "karma": "^0.13.15", 45 | "karma-browserify": "^4.4.2", 46 | "karma-chai-shallow-deep-equal": "0.0.4", 47 | "karma-chrome-launcher": "2.0.0", 48 | "karma-env-preprocessor": "^0.1.1", 49 | "karma-firefox-launcher": "^0.1.7", 50 | "karma-mocha": "^0.2.1", 51 | "karma-mocha-reporter": "^1.1.3", 52 | "karma-sinon-chai": "^1.1.0", 53 | "mocha": "^2.3.4", 54 | "randomcolor": "^0.4.4", 55 | "semistandard": "^8.0.0", 56 | "shelljs": "^0.7.0", 57 | "sinon": "^1.17.5", 58 | "sinon-chai": "^2.8.0", 59 | "shx": "^0.1.1", 60 | "snazzy": "^4.0.0", 61 | "webpack": "^1.13.0" 62 | }, 63 | "semistandard": { 64 | "globals": [ 65 | "AFRAME", 66 | "THREE" 67 | ], 68 | "ignore": [ 69 | "examples/build.js", 70 | "dist/**" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /readme.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fernandojsg/aframe-camera-transform-controls-component/e4c1e1378c234e2ce8b8630c0353cb3d1a8beffb/readme.gif -------------------------------------------------------------------------------- /tests/__init.test.js: -------------------------------------------------------------------------------- 1 | /* global sinon, setup, teardown */ 2 | 3 | /** 4 | * __init.test.js is run before every test case. 5 | */ 6 | window.debug = true; 7 | var AScene = require('aframe').AScene 8 | 9 | navigator.getVRDisplays = function () { 10 | var resolvePromise = Promise.resolve(); 11 | var mockVRDisplay = { 12 | requestPresent: resolvePromise, 13 | exitPresent: resolvePromise, 14 | getPose: function () { return {orientation: null, position: null}; }, 15 | requestAnimationFrame: function () { return 1; } 16 | }; 17 | return Promise.resolve([mockVRDisplay]); 18 | }; 19 | 20 | setup(function () { 21 | this.sinon = sinon.sandbox.create(); 22 | // Stubs to not create a WebGL context since Travis CI runs headless. 23 | this.sinon.stub(AScene.prototype, 'render'); 24 | this.sinon.stub(AScene.prototype, 'resize'); 25 | this.sinon.stub(AScene.prototype, 'setupRenderer'); 26 | }); 27 | 28 | teardown(function () { 29 | // Clean up any attached elements. 30 | var attachedEls = ['canvas', 'a-assets', 'a-scene']; 31 | var els = document.querySelectorAll(attachedEls.join(',')); 32 | for (var i = 0; i < els.length; i++) { 33 | els[i].parentNode.removeChild(els[i]); 34 | } 35 | this.sinon.restore(); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | /* global suite */ 2 | 3 | /** 4 | * Helper method to create a scene, create an entity, add entity to scene, 5 | * add scene to document. 6 | * 7 | * @returns {object} An `` element. 8 | */ 9 | module.exports.entityFactory = function (opts) { 10 | var scene = document.createElement('a-scene'); 11 | var assets = document.createElement('a-assets'); 12 | var entity = document.createElement('a-entity'); 13 | scene.appendChild(assets); 14 | scene.appendChild(entity); 15 | 16 | opts = opts || {}; 17 | 18 | if (opts.assets) { 19 | opts.assets.forEach(function (asset) { 20 | assets.appendChild(asset); 21 | }); 22 | } 23 | 24 | document.body.appendChild(scene); 25 | return entity; 26 | }; 27 | 28 | /** 29 | * Creates and attaches a mixin element (and an `` element if necessary). 30 | * 31 | * @param {string} id - ID of mixin. 32 | * @param {object} obj - Map of component names to attribute values. 33 | * @param {Element} scene - Indicate which scene to apply mixin to if necessary. 34 | * @returns {object} An attached `` element. 35 | */ 36 | module.exports.mixinFactory = function (id, obj, scene) { 37 | var mixinEl = document.createElement('a-mixin'); 38 | mixinEl.setAttribute('id', id); 39 | Object.keys(obj).forEach(function (componentName) { 40 | mixinEl.setAttribute(componentName, obj[componentName]); 41 | }); 42 | 43 | var assetsEl = scene ? scene.querySelector('a-assets') : document.querySelector('a-assets'); 44 | assetsEl.appendChild(mixinEl); 45 | 46 | return mixinEl; 47 | }; 48 | 49 | /** 50 | * Test that is only run locally and is skipped on CI. 51 | */ 52 | module.exports.getSkipCISuite = function () { 53 | if (window.__env__.TEST_ENV === 'ci') { 54 | return suite.skip; 55 | } else { 56 | return suite; 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | /* global assert, setup, suite, test */ 2 | require('aframe'); 3 | require('../index.js'); 4 | var entityFactory = require('./helpers').entityFactory; 5 | 6 | suite('camera-transform-controls component', function () { 7 | var component; 8 | var el; 9 | 10 | setup(function (done) { 11 | el = entityFactory(); 12 | el.addEventListener('componentinitialized', function (evt) { 13 | if (evt.detail.name !== 'camera-transform-controls') { return; } 14 | component = el.components['camera-transform-controls']; 15 | done(); 16 | }); 17 | el.setAttribute('camera-transform-controls', {}); 18 | }); 19 | 20 | suite('foo property', function () { 21 | test('is good', function () { 22 | assert.equal(1, 1); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration. 2 | module.exports = function (config) { 3 | config.set({ 4 | basePath: '../', 5 | browserify: { 6 | debug: true, 7 | paths: ['./'] 8 | }, 9 | browsers: ['Firefox', 'Chrome'], 10 | client: { 11 | captureConsole: true, 12 | mocha: {ui: 'tdd'} 13 | }, 14 | envPreprocessor: ['TEST_ENV'], 15 | files: [ 16 | // Define test files. 17 | {pattern: 'tests/**/*.test.js'}, 18 | // Serve test assets. 19 | {pattern: 'tests/assets/**/*', included: false, served: true} 20 | ], 21 | frameworks: ['mocha', 'sinon-chai', 'chai-shallow-deep-equal', 'browserify'], 22 | preprocessors: {'tests/**/*.js': ['browserify', 'env']}, 23 | reporters: ['mocha'] 24 | }); 25 | }; 26 | --------------------------------------------------------------------------------