├── .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 | [](https://npmjs.org/package/aframe-camera-transform-controls-component)
4 | [](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 |
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 |
53 |
54 |
55 | Basic
56 | This is a basic example.
57 |
58 |
59 |
60 |
61 |
62 | GLTF Scene
63 | This is a bigger scene.
64 |
65 |
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 |
--------------------------------------------------------------------------------