├── .gitignore
├── LICENSE
├── README.md
├── browser.js
├── dist
├── aframe-no-click-look-controls.js
└── aframe-no-click-look-controls.min.js
├── index.js
├── package.json
└── tests
├── __init.test.js
├── helpers.js
├── index.test.js
└── karma.conf.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .sw[ponm]
2 | examples/node_modules/
3 | gh-pages
4 | node_modules/
5 | build.js
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Alex Kass
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 | #BROKEN DUE TO NEW VERSION OF A-FRAME!
2 |
3 | # A-Frame `no-click-look-controls` Component
4 |
5 | ##Overview
6 | Intuitive camera controls for desktop 3D experiences with [A-Frame](aframe.io).
7 |
8 | ##Features
9 | * :no_entry_sign:<--->:no_entry_sign: Dynamically set maximum yaw and pitch (see options) to control sensitivity and max turn angles.
10 | * :computer: Provides intuitive desktop view controls without requiring mousedown+drag.
11 | * :sunglasses::iphone::100: Includes the core touch and HMD view controls for drop-in replacement of core `look-controls` component.
12 |
13 | ##Demos:
14 |
15 | [Panorama with plenty of space to explore the whole scene without anxiety of moving cursor off the canvas](https://alexrkass.github.io/no-click-example/)
16 |
17 | [User interface with restricted view angles to focus user on content.](https://alexrkass.github.io/aframe-thetarestricted-example)
18 |
19 | ##Usage
20 | ####Script
21 | ```html
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | ```
38 | ####NPM
39 |
40 | Install.
41 |
42 | ```
43 | $ npm install aframe-no-click-look-controls
44 | ```
45 |
46 | Register.
47 |
48 | ```javascript
49 | var AFRAME = require('aframe-core');
50 | var NoClickLookControls = require('aframe-no-click-look-controls');
51 | AFRAME.registerComponent('no-click-look-controls', NoClickLookControls);
52 | ```
53 |
54 | Use.
55 |
56 | ##Options
57 |
58 | (units are radians)
59 |
60 | Property | Default | Description
61 | --------------|---------|-------------
62 | maxyaw | 3π | Controls the max y-axis rotation. Actual max viewing angle is twice the parameter, ie 3π is 3π to the right and 3π to the left.
63 | maxpitch | π/2 | Controls the max x-axis rotation. Actual max viewing angle is twice the parameter, ie π/2 is π/2 up and π/2 down.
64 | enabled | true | Enables controls
65 |
66 | ##TODOS (PRs welcome)
67 |
68 | * allow asymmetrical yaw and pitch values rather than forcing symmetrical distances from original camera position
69 |
70 | * add option to slow down camera rotation as the mouse gets closer to the edge of the canvas
71 |
72 | * write tests
73 |
74 | * add a tiny touch of motion smoothing
75 |
--------------------------------------------------------------------------------
/browser.js:
--------------------------------------------------------------------------------
1 | // Browser distribution of the A-Frame component.
2 | (function () {
3 | if (typeof AFRAME === 'undefined') {
4 | console.error('Component attempted to register before AFRAME was available.');
5 | return;
6 | }
7 |
8 | // Register all components here.
9 | var components = {
10 | "no-click-look-controls": require('./index').component
11 | };
12 |
13 | Object.keys(components).forEach(function (name) {
14 | if (AFRAME.aframeCore) {
15 | AFRAME.aframeCore.registerComponent(name, components[name]);
16 | } else {
17 | AFRAME.registerComponent(name, components[name]);
18 | }
19 | });
20 | })();
21 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | var registerComponent = require('../aframe-core/src/core/component').registerComponent;
2 | var THREE = require('./../aframe-core/lib/three');
3 |
4 | // To avoid recalculation at every mouse movement tick
5 | var PI_2 = Math.PI / 2;
6 |
7 | module.exports.component = {
8 | dependencies: ['position', 'rotation'],
9 | schema: {
10 | enabled: { default: true },
11 | maxpitch: {default: PI_2},
12 | maxyaw: {default: PI_2 * 6},
13 | },
14 |
15 | /**
16 | * Called once when component is attached. Generally for initial setup.
17 | */
18 | init: function () {
19 | var scene = this.el.sceneEl;
20 | this.setupMouseControls();
21 | this.setupHMDControls();
22 | this.attachEventListeners();
23 | scene.addBehavior(this);
24 | this.previousPosition = new THREE.Vector3();
25 | this.deltaPosition = new THREE.Vector3();
26 | },
27 |
28 | setupMouseControls: function () {
29 | this.canvasEl = document.querySelector('a-scene').canvas;
30 | // The canvas where the scene is painted
31 | this.hovering = false;
32 | this.pitchObject = new THREE.Object3D();
33 | this.yawObject = new THREE.Object3D();
34 | this.yawObject.position.y = 10;
35 | this.yawObject.add(this.pitchObject);
36 | },
37 |
38 | setupHMDControls: function () {
39 | this.dolly = new THREE.Object3D();
40 | this.euler = new THREE.Euler();
41 | this.controls = new THREE.VRControls(this.dolly);
42 | this.zeroQuaternion = new THREE.Quaternion();
43 | },
44 |
45 | attachEventListeners: function () {
46 | var canvasEl = document.querySelector('a-scene').canvas;
47 |
48 | // Mouse Events
49 | canvasEl.addEventListener('mousemove', this.onMouseMove.bind(this), true);
50 | canvasEl.addEventListener('mouseout', this.onMouseOut.bind(this), true);
51 | canvasEl.addEventListener('mouseover', this.onMouseOver.bind(this), true);
52 | // Touch events
53 | canvasEl.addEventListener('touchstart', this.onTouchStart.bind(this));
54 | canvasEl.addEventListener('touchmove', this.onTouchMove.bind(this));
55 | canvasEl.addEventListener('touchend', this.onTouchEnd.bind(this));
56 | },
57 |
58 | update: function () {
59 | if (!this.data.enabled) { return; }
60 | this.controls.update();
61 | this.updateOrientation();
62 | this.updatePosition();
63 | },
64 |
65 | updateOrientation: (function () {
66 | var hmdEuler = new THREE.Euler();
67 | hmdEuler.order = 'YXZ';
68 | return function () {
69 | var pitchObject = this.pitchObject;
70 | var yawObject = this.yawObject;
71 | var hmdQuaternion = this.calculateHMDQuaternion();
72 | hmdEuler.setFromQuaternion(hmdQuaternion);
73 | this.el.setAttribute('rotation', {
74 | x: THREE.Math.radToDeg(hmdEuler.x) + THREE.Math.radToDeg(pitchObject.rotation.x),
75 | y: THREE.Math.radToDeg(hmdEuler.y) + THREE.Math.radToDeg(yawObject.rotation.y),
76 | z: THREE.Math.radToDeg(hmdEuler.z)
77 | });
78 | };
79 | })(),
80 |
81 | calculateHMDQuaternion: (function () {
82 | var hmdQuaternion = new THREE.Quaternion();
83 | return function () {
84 | var dolly = this.dolly;
85 | if (!this.zeroed && !dolly.quaternion.equals(this.zeroQuaternion)) {
86 | this.zeroOrientation();
87 | this.zeroed = true;
88 | }
89 | hmdQuaternion.copy(this.zeroQuaternion).multiply(dolly.quaternion);
90 | return hmdQuaternion;
91 | };
92 | })(),
93 |
94 | updatePosition: (function () {
95 | var position = new THREE.Vector3();
96 | var quaternion = new THREE.Quaternion();
97 | var scale = new THREE.Vector3();
98 | return function () {
99 | var el = this.el;
100 | var deltaPosition = this.calculateDeltaPosition();
101 | var currentPosition = el.getComputedAttribute('position');
102 | this.el.object3D.matrixWorld.decompose(position, quaternion, scale);
103 | deltaPosition.applyQuaternion(quaternion);
104 | el.setAttribute('position', {
105 | x: currentPosition.x + deltaPosition.x,
106 | y: currentPosition.y + deltaPosition.y,
107 | z: currentPosition.z + deltaPosition.z
108 | });
109 | };
110 | })(),
111 |
112 | calculateDeltaPosition: function () {
113 | var dolly = this.dolly;
114 | var deltaPosition = this.deltaPosition;
115 | var previousPosition = this.previousPosition;
116 | deltaPosition.copy(dolly.position);
117 | deltaPosition.sub(previousPosition);
118 | previousPosition.copy(dolly.position);
119 | return deltaPosition;
120 | },
121 |
122 | updateHMDQuaternion: (function () {
123 | var hmdQuaternion = new THREE.Quaternion();
124 | return function () {
125 | var dolly = this.dolly;
126 | this.controls.update();
127 | if (!this.zeroed && !dolly.quaternion.equals(this.zeroQuaternion)) {
128 | this.zeroOrientation();
129 | this.zeroed = true;
130 | }
131 | hmdQuaternion.copy(this.zeroQuaternion).multiply(dolly.quaternion);
132 | return hmdQuaternion;
133 | };
134 | })(),
135 |
136 | zeroOrientation: function () {
137 | var euler = new THREE.Euler();
138 | euler.setFromQuaternion(this.dolly.quaternion.clone().inverse());
139 | // Cancel out roll and pitch. We want to only reset yaw
140 | euler.z = 0;
141 | euler.x = 0;
142 | this.zeroQuaternion.setFromEuler(euler);
143 | },
144 |
145 | getMousePosition: function(event, canvasEl) {
146 |
147 | var rect = canvasEl.getBoundingClientRect();
148 |
149 | // Returns a value from -1 to 1 for X and Y representing the percentage of the max-yaw and max-pitch from the center of the canvas
150 | // -1 is far left or top, 1 is far right or bottom
151 | return {x: -2*(.5 - (event.clientX - rect.left)/rect.width), y: -2*(.5 - (event.clientY - rect.top)/rect.height)};
152 | },
153 |
154 | onMouseMove: function (event) {
155 | var pos = this.getMousePosition(event, this.canvasEl);
156 | var x = pos.x;
157 | var y = pos.y;
158 |
159 | var pitchObject = this.pitchObject;
160 | var yawObject = this.yawObject;
161 |
162 | if (!this.hovering || !this.data.enabled) { return; }
163 | yawObject.rotation.y = this.data.maxyaw * -x;
164 | pitchObject.rotation.x = this.data.maxpitch * -y;
165 | },
166 |
167 | onMouseOver: function (event) {
168 | this.hovering = true;
169 | },
170 |
171 | onMouseOut: function (event) {
172 | this.hovering = false;
173 | },
174 |
175 | onTouchStart: function (e) {
176 | if (e.touches.length !== 1) { return; }
177 | this.touchStart = {
178 | x: e.touches[0].pageX,
179 | y: e.touches[0].pageY
180 | };
181 | this.touchStarted = true;
182 | },
183 |
184 | onTouchMove: function (e) {
185 | var deltaY;
186 | var yawObject = this.yawObject;
187 | if (!this.touchStarted) { return; }
188 | deltaY = 2 * Math.PI * (e.touches[0].pageX - this.touchStart.x) / this.canvasEl.clientWidth;
189 | // Limits touch orientaion to to yaw (y axis)
190 | yawObject.rotation.y -= deltaY * 0.5;
191 | this.touchStart = {
192 | x: e.touches[0].pageX,
193 | y: e.touches[0].pageY
194 | };
195 | },
196 |
197 | onTouchEnd: function () {
198 | this.touchStarted = false;
199 | },
200 | /**
201 | * Called when a component is removed (e.g., via removeAttribute).
202 | * Generally undoes all modifications to the entity.
203 | */
204 | remove: function () { }
205 | };
206 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aframe-no-click-look-controls",
3 | "version": "1.0.4",
4 | "description": "Intuitive controls for desktop A-frame experiences",
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": "browserify browser.js -o dist/aframe-no-click-look-controls.js && browserify browser.js | uglifyjs > dist/aframe-no-click-look-controls.min.js",
10 | "postpublish": "npm run dist",
11 | "preghpages": "npm run build && rm -rf gh-pages && cp -r examples gh-pages",
12 | "ghpages": "npm run preghpages && ghpages -p gh-pages",
13 | "test": "karma start ./tests/karma.conf.js"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/alexrkass/aframe-no-click-look-controls.git"
18 | },
19 | "keywords": [
20 | "aframe",
21 | "aframe-component",
22 | "camera",
23 | "no-click",
24 | "aframe-vr",
25 | "vr",
26 | "aframe-layout",
27 | "mozvr",
28 | "webvr"
29 | ],
30 | "author": "Alex Kass ",
31 | "license": "MIT",
32 | "bugs": {
33 | "url": "https://github.com/alexrkass/aframe-no-click-look-controls/issues"
34 | },
35 | "homepage": "https://github.com/alexrkass/aframe-no-click-look-controls#readme",
36 | "devDependencies": {
37 | "aframe-core": "^0.1.0",
38 | "browserify": "^12.0.1",
39 | "browserify-css": "^0.8.3",
40 | "budo": "^7.1.0",
41 | "chai": "^3.4.1",
42 | "chai-shallow-deep-equal": "^1.3.0",
43 | "ghpages": "0.0.3",
44 | "karma": "^0.13.15",
45 | "karma-browserify": "^4.4.2",
46 | "karma-chai-shallow-deep-equal": "0.0.4",
47 | "karma-firefox-launcher": "^0.1.7",
48 | "karma-mocha": "^0.2.1",
49 | "karma-mocha-reporter": "^1.1.3",
50 | "karma-sinon-chai": "^1.1.0",
51 | "mocha": "^2.3.4",
52 | "uglify-js": "^2.6.0",
53 | "webpack": "^1.12.9"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/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 |
8 | var AScene = require('aframe-core').AScene;
9 |
10 | beforeEach(function () {
11 | this.sinon = sinon.sandbox.create();
12 | // Stub to not create a WebGL context since Travis CI runs headless.
13 | this.sinon.stub(AScene.prototype, 'attachedCallback');
14 | });
15 |
16 | afterEach(function () {
17 | // Clean up any attached elements.
18 | ['canvas', 'a-assets', 'a-scene'].forEach(function (tagName) {
19 | var els = document.querySelectorAll(tagName);
20 | for (var i = 0; i < els.length; i++) {
21 | els[i].parentNode.removeChild(els[i]);
22 | }
23 | });
24 | AScene.scene = null;
25 |
26 | this.sinon.restore();
27 | });
28 |
--------------------------------------------------------------------------------
/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 () {
10 | var scene = document.createElement('a-scene');
11 | var entity = document.createElement('a-entity');
12 | scene.appendChild(entity);
13 | document.body.appendChild(scene);
14 | return entity;
15 | };
16 |
17 | /**
18 | * Creates and attaches a mixin element (and an `` element if necessary).
19 | *
20 | * @param {string} id - ID of mixin.
21 | * @param {object} obj - Map of component names to attribute values.
22 | * @returns {object} An attached `` element.
23 | */
24 | module.exports.mixinFactory = function (id, obj) {
25 | var mixinEl = document.createElement('a-mixin');
26 | mixinEl.setAttribute('id', id);
27 | Object.keys(obj).forEach(function (componentName) {
28 | mixinEl.setAttribute(componentName, obj[componentName]);
29 | });
30 |
31 | var assetsEl = document.querySelector('a-assets');
32 | if (!assetsEl) {
33 | assetsEl = document.createElement('a-assets');
34 | document.body.appendChild(assetsEl);
35 | }
36 | assetsEl.appendChild(mixinEl);
37 |
38 | return mixinEl;
39 | };
40 |
41 | /**
42 | * Test that is only run locally and is skipped on CI.
43 | */
44 | module.exports.getSkipCISuite = function () {
45 | if (window.__env__.TEST_ENV === 'ci') {
46 | return suite.skip;
47 | } else {
48 | return suite;
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/tests/index.test.js:
--------------------------------------------------------------------------------
1 | var Aframe = require('aframe-core');
2 | var noClickLookControls = require('../index.js').component;
3 | var entityFactory = require('./helpers').entityFactory;
4 |
5 | Aframe.registerComponent('no-click-look-controls', noClickLookControls);
6 |
7 | describe('no-click-look-controls', function () {
8 | beforeEach(function (done) {
9 | this.el = entityFactory();
10 | this.el.addEventListener('loaded', function () {
11 | done();
12 | });
13 | });
14 |
15 | describe('no-click-look-controls', function () {
16 | it('is good', function () {
17 | assert.equal(1, 1);
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/tests/karma.conf.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | module.exports = function (config) {
3 | config.set({
4 | basePath: '../',
5 | browserify: {
6 | paths: ['./']
7 | },
8 | browsers: ['firefox_latest'],
9 | customLaunchers: {
10 | firefox_latest: {
11 | base: 'FirefoxNightly',
12 | prefs: { /* empty */ }
13 | }
14 | },
15 | client: {
16 | captureConsole: true,
17 | mocha: {ui: 'bdd'}
18 | },
19 | envPreprocessor: [
20 | 'TEST_ENV'
21 | ],
22 | files: [
23 | 'tests/**/*.test.js',
24 | ],
25 | frameworks: ['mocha', 'sinon-chai', 'chai-shallow-deep-equal', 'browserify'],
26 | preprocessors: {
27 | 'tests/**/*.js': ['browserify']
28 | },
29 | reporters: ['mocha']
30 | });
31 | };
32 |
--------------------------------------------------------------------------------