├── LICENSE.md
├── README.md
├── dist
└── gestures.js
├── gesture-detector.js
├── gesture-handler.js
├── image-tracking.html
├── index.html
└── styles.css
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
AR.js & A-Frame Gestures
2 |
3 |
4 |
5 | Example of using gesture events on AR.js with A-Frame. This work is based on [this example](https://github.com/8thwall/web/blob/master/examples/aframe/manipulate/README.md) from 8th Wall.
6 |
7 | Scale and rotate 3D elements from your AR.js scene using `gesture-detector` and `gesture-handler` components.
8 |
9 |
10 | ## Try now!
11 |
12 | #### Image Tracking
13 |
14 | 🚀[Open this sample](https://fcor.github.io/arjs-gestures/image-tracking.html) on your phone and [scan this picture](https://raw.githubusercontent.com/AR-js-org/AR.js/master/aframe/examples/image-tracking/nft/trex-image-big.jpeg)
15 |
16 | #### Marker Tracking
17 |
18 | 🚀[Open this sample](https://fcor.github.io/arjs-gestures/index.html) on your phone and [scan this marker](https://killcloud.nyc3.digitaloceanspaces.com/assets/Hiro_marker_ARjs.png)
19 |
20 | ## Installation
21 |
22 | Import this file if you want default touch events. Keep reading to learn how to extend it.
23 |
24 | ```html
25 |
26 | ```
27 |
28 | ## How it works?
29 |
30 | `gesture-detector` listens to regular touch events directly on `a-scene` and emits a custom event indicating how many fingers were involved ("one", "two", "three" or "many") and passing some details of the event, like the position, spread and coordinates where user touched the screen. This component was developed by 8th Wall for their A-Frame based demos and can be found [here](https://github.com/8thwall/web/blob/master/examples/aframe/manipulate/gesture-detector.js).
31 |
32 | `gesture-handler` adds listeners for custom gesture events, emitted by `gesture-detector`. This component should be placed on the 3D element we want to control and it automaticaly detects if the marker or image is found or lost to ensure the element could only be manipulated if it's actually visible. This component could be customized via properties. Currently supports pinch to zoom and finger spin for rotating the element.
33 |
34 | ### Properties
35 |
36 | | Property | Description | Default Value |
37 | | -------------- | --------------------------------------- | ------------- |
38 | | enabled | Whether gesture controls are enabled. | true |
39 | | rotationFactor | Factor for controlling rotation | 5 |
40 | | minScale | Minimum scale applied to the 3D element | 0.3 |
41 | | maxScale | Minimum scale applied to the 3D element | 8 |
42 |
43 | ## Examples
44 |
45 | #### Image Tracking
46 |
47 | ```html
48 |
56 |
67 |
74 |
75 |
76 |
77 |
78 | ```
79 |
80 | #### Marker Tracking
81 |
82 | ```html
83 |
91 |
92 |
96 |
97 |
98 |
99 |
106 |
114 |
115 |
116 |
117 |
118 | ```
119 |
120 | ## Credits
121 | Kudos to 8th wall for sharing their A-Frame Manipulate example!
122 |
123 | Bowser 3D model was made by [santiago3052008](https://sketchfab.com/santiago3052008) and can be found [here](https://sketchfab.com/3d-models/bowser-fa17f94ae350416f86c35db7c0e129c3)
--------------------------------------------------------------------------------
/dist/gestures.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME, THREE */
2 |
3 | AFRAME.registerComponent("gesture-handler", {
4 | schema: {
5 | enabled: { default: true },
6 | rotationFactor: { default: 5 },
7 | minScale: { default: 0.3 },
8 | maxScale: { default: 8 },
9 | },
10 |
11 | init: function () {
12 | this.handleScale = this.handleScale.bind(this);
13 | this.handleRotation = this.handleRotation.bind(this);
14 |
15 | this.isVisible = false;
16 | this.initialScale = this.el.object3D.scale.clone();
17 | this.scaleFactor = 1;
18 |
19 | this.el.sceneEl.addEventListener("markerFound", (e) => {
20 | this.isVisible = true;
21 | });
22 |
23 | this.el.sceneEl.addEventListener("markerLost", (e) => {
24 | this.isVisible = false;
25 | });
26 | },
27 |
28 | update: function () {
29 | if (this.data.enabled) {
30 | this.el.sceneEl.addEventListener("onefingermove", this.handleRotation);
31 | this.el.sceneEl.addEventListener("twofingermove", this.handleScale);
32 | } else {
33 | this.el.sceneEl.removeEventListener("onefingermove", this.handleRotation);
34 | this.el.sceneEl.removeEventListener("twofingermove", this.handleScale);
35 | }
36 | },
37 |
38 | remove: function () {
39 | this.el.sceneEl.removeEventListener("onefingermove", this.handleRotation);
40 | this.el.sceneEl.removeEventListener("twofingermove", this.handleScale);
41 | },
42 |
43 | handleRotation: function (event) {
44 | if (this.isVisible) {
45 | this.el.object3D.rotation.y +=
46 | event.detail.positionChange.x * this.data.rotationFactor;
47 | this.el.object3D.rotation.x +=
48 | event.detail.positionChange.y * this.data.rotationFactor;
49 | }
50 | },
51 |
52 | handleScale: function (event) {
53 | if (this.isVisible) {
54 | this.scaleFactor *=
55 | 1 + event.detail.spreadChange / event.detail.startSpread;
56 |
57 | this.scaleFactor = Math.min(
58 | Math.max(this.scaleFactor, this.data.minScale),
59 | this.data.maxScale
60 | );
61 |
62 | this.el.object3D.scale.x = this.scaleFactor * this.initialScale.x;
63 | this.el.object3D.scale.y = this.scaleFactor * this.initialScale.y;
64 | this.el.object3D.scale.z = this.scaleFactor * this.initialScale.z;
65 | }
66 | },
67 | });
68 |
69 | // Component that detects and emits events for touch gestures
70 |
71 | AFRAME.registerComponent("gesture-detector", {
72 | schema: {
73 | element: { default: "" }
74 | },
75 |
76 | init: function() {
77 | this.targetElement =
78 | this.data.element && document.querySelector(this.data.element);
79 |
80 | if (!this.targetElement) {
81 | this.targetElement = this.el;
82 | }
83 |
84 | this.internalState = {
85 | previousState: null
86 | };
87 |
88 | this.emitGestureEvent = this.emitGestureEvent.bind(this);
89 |
90 | this.targetElement.addEventListener("touchstart", this.emitGestureEvent);
91 |
92 | this.targetElement.addEventListener("touchend", this.emitGestureEvent);
93 |
94 | this.targetElement.addEventListener("touchmove", this.emitGestureEvent);
95 | },
96 |
97 | remove: function() {
98 | this.targetElement.removeEventListener("touchstart", this.emitGestureEvent);
99 |
100 | this.targetElement.removeEventListener("touchend", this.emitGestureEvent);
101 |
102 | this.targetElement.removeEventListener("touchmove", this.emitGestureEvent);
103 | },
104 |
105 | emitGestureEvent(event) {
106 | const currentState = this.getTouchState(event);
107 |
108 | const previousState = this.internalState.previousState;
109 |
110 | const gestureContinues =
111 | previousState &&
112 | currentState &&
113 | currentState.touchCount == previousState.touchCount;
114 |
115 | const gestureEnded = previousState && !gestureContinues;
116 |
117 | const gestureStarted = currentState && !gestureContinues;
118 |
119 | if (gestureEnded) {
120 | const eventName =
121 | this.getEventPrefix(previousState.touchCount) + "fingerend";
122 |
123 | this.el.emit(eventName, previousState);
124 |
125 | this.internalState.previousState = null;
126 | }
127 |
128 | if (gestureStarted) {
129 | currentState.startTime = performance.now();
130 |
131 | currentState.startPosition = currentState.position;
132 |
133 | currentState.startSpread = currentState.spread;
134 |
135 | const eventName =
136 | this.getEventPrefix(currentState.touchCount) + "fingerstart";
137 |
138 | this.el.emit(eventName, currentState);
139 |
140 | this.internalState.previousState = currentState;
141 | }
142 |
143 | if (gestureContinues) {
144 | const eventDetail = {
145 | positionChange: {
146 | x: currentState.position.x - previousState.position.x,
147 |
148 | y: currentState.position.y - previousState.position.y
149 | }
150 | };
151 |
152 | if (currentState.spread) {
153 | eventDetail.spreadChange = currentState.spread - previousState.spread;
154 | }
155 |
156 | // Update state with new data
157 |
158 | Object.assign(previousState, currentState);
159 |
160 | // Add state data to event detail
161 |
162 | Object.assign(eventDetail, previousState);
163 |
164 | const eventName =
165 | this.getEventPrefix(currentState.touchCount) + "fingermove";
166 |
167 | this.el.emit(eventName, eventDetail);
168 | }
169 | },
170 |
171 | getTouchState: function(event) {
172 | if (event.touches.length === 0) {
173 | return null;
174 | }
175 |
176 | // Convert event.touches to an array so we can use reduce
177 |
178 | const touchList = [];
179 |
180 | for (let i = 0; i < event.touches.length; i++) {
181 | touchList.push(event.touches[i]);
182 | }
183 |
184 | const touchState = {
185 | touchCount: touchList.length
186 | };
187 |
188 | // Calculate center of all current touches
189 |
190 | const centerPositionRawX =
191 | touchList.reduce((sum, touch) => sum + touch.clientX, 0) /
192 | touchList.length;
193 |
194 | const centerPositionRawY =
195 | touchList.reduce((sum, touch) => sum + touch.clientY, 0) /
196 | touchList.length;
197 |
198 | touchState.positionRaw = { x: centerPositionRawX, y: centerPositionRawY };
199 |
200 | // Scale touch position and spread by average of window dimensions
201 |
202 | const screenScale = 2 / (window.innerWidth + window.innerHeight);
203 |
204 | touchState.position = {
205 | x: centerPositionRawX * screenScale,
206 | y: centerPositionRawY * screenScale
207 | };
208 |
209 | // Calculate average spread of touches from the center point
210 |
211 | if (touchList.length >= 2) {
212 | const spread =
213 | touchList.reduce((sum, touch) => {
214 | return (
215 | sum +
216 | Math.sqrt(
217 | Math.pow(centerPositionRawX - touch.clientX, 2) +
218 | Math.pow(centerPositionRawY - touch.clientY, 2)
219 | )
220 | );
221 | }, 0) / touchList.length;
222 |
223 | touchState.spread = spread * screenScale;
224 | }
225 |
226 | return touchState;
227 | },
228 |
229 | getEventPrefix(touchCount) {
230 | const numberNames = ["one", "two", "three", "many"];
231 |
232 | return numberNames[Math.min(touchCount, 4) - 1];
233 | }
234 | });
235 |
--------------------------------------------------------------------------------
/gesture-detector.js:
--------------------------------------------------------------------------------
1 | // Component that detects and emits events for touch gestures
2 |
3 | AFRAME.registerComponent("gesture-detector", {
4 | schema: {
5 | element: { default: "" }
6 | },
7 |
8 | init: function() {
9 | this.targetElement =
10 | this.data.element && document.querySelector(this.data.element);
11 |
12 | if (!this.targetElement) {
13 | this.targetElement = this.el;
14 | }
15 |
16 | this.internalState = {
17 | previousState: null
18 | };
19 |
20 | this.emitGestureEvent = this.emitGestureEvent.bind(this);
21 |
22 | this.targetElement.addEventListener("touchstart", this.emitGestureEvent);
23 |
24 | this.targetElement.addEventListener("touchend", this.emitGestureEvent);
25 |
26 | this.targetElement.addEventListener("touchmove", this.emitGestureEvent);
27 | },
28 |
29 | remove: function() {
30 | this.targetElement.removeEventListener("touchstart", this.emitGestureEvent);
31 |
32 | this.targetElement.removeEventListener("touchend", this.emitGestureEvent);
33 |
34 | this.targetElement.removeEventListener("touchmove", this.emitGestureEvent);
35 | },
36 |
37 | emitGestureEvent(event) {
38 | const currentState = this.getTouchState(event);
39 |
40 | const previousState = this.internalState.previousState;
41 |
42 | const gestureContinues =
43 | previousState &&
44 | currentState &&
45 | currentState.touchCount == previousState.touchCount;
46 |
47 | const gestureEnded = previousState && !gestureContinues;
48 |
49 | const gestureStarted = currentState && !gestureContinues;
50 |
51 | if (gestureEnded) {
52 | const eventName =
53 | this.getEventPrefix(previousState.touchCount) + "fingerend";
54 |
55 | this.el.emit(eventName, previousState);
56 |
57 | this.internalState.previousState = null;
58 | }
59 |
60 | if (gestureStarted) {
61 | currentState.startTime = performance.now();
62 |
63 | currentState.startPosition = currentState.position;
64 |
65 | currentState.startSpread = currentState.spread;
66 |
67 | const eventName =
68 | this.getEventPrefix(currentState.touchCount) + "fingerstart";
69 |
70 | this.el.emit(eventName, currentState);
71 |
72 | this.internalState.previousState = currentState;
73 | }
74 |
75 | if (gestureContinues) {
76 | const eventDetail = {
77 | positionChange: {
78 | x: currentState.position.x - previousState.position.x,
79 |
80 | y: currentState.position.y - previousState.position.y
81 | }
82 | };
83 |
84 | if (currentState.spread) {
85 | eventDetail.spreadChange = currentState.spread - previousState.spread;
86 | }
87 |
88 | // Update state with new data
89 |
90 | Object.assign(previousState, currentState);
91 |
92 | // Add state data to event detail
93 |
94 | Object.assign(eventDetail, previousState);
95 |
96 | const eventName =
97 | this.getEventPrefix(currentState.touchCount) + "fingermove";
98 |
99 | this.el.emit(eventName, eventDetail);
100 | }
101 | },
102 |
103 | getTouchState: function(event) {
104 | if (event.touches.length === 0) {
105 | return null;
106 | }
107 |
108 | // Convert event.touches to an array so we can use reduce
109 |
110 | const touchList = [];
111 |
112 | for (let i = 0; i < event.touches.length; i++) {
113 | touchList.push(event.touches[i]);
114 | }
115 |
116 | const touchState = {
117 | touchCount: touchList.length
118 | };
119 |
120 | // Calculate center of all current touches
121 |
122 | const centerPositionRawX =
123 | touchList.reduce((sum, touch) => sum + touch.clientX, 0) /
124 | touchList.length;
125 |
126 | const centerPositionRawY =
127 | touchList.reduce((sum, touch) => sum + touch.clientY, 0) /
128 | touchList.length;
129 |
130 | touchState.positionRaw = { x: centerPositionRawX, y: centerPositionRawY };
131 |
132 | // Scale touch position and spread by average of window dimensions
133 |
134 | const screenScale = 2 / (window.innerWidth + window.innerHeight);
135 |
136 | touchState.position = {
137 | x: centerPositionRawX * screenScale,
138 | y: centerPositionRawY * screenScale
139 | };
140 |
141 | // Calculate average spread of touches from the center point
142 |
143 | if (touchList.length >= 2) {
144 | const spread =
145 | touchList.reduce((sum, touch) => {
146 | return (
147 | sum +
148 | Math.sqrt(
149 | Math.pow(centerPositionRawX - touch.clientX, 2) +
150 | Math.pow(centerPositionRawY - touch.clientY, 2)
151 | )
152 | );
153 | }, 0) / touchList.length;
154 |
155 | touchState.spread = spread * screenScale;
156 | }
157 |
158 | return touchState;
159 | },
160 |
161 | getEventPrefix(touchCount) {
162 | const numberNames = ["one", "two", "three", "many"];
163 |
164 | return numberNames[Math.min(touchCount, 4) - 1];
165 | }
166 | });
167 |
--------------------------------------------------------------------------------
/gesture-handler.js:
--------------------------------------------------------------------------------
1 | /* global AFRAME, THREE */
2 |
3 | AFRAME.registerComponent("gesture-handler", {
4 | schema: {
5 | enabled: { default: true },
6 | rotationFactor: { default: 5 },
7 | minScale: { default: 0.3 },
8 | maxScale: { default: 8 },
9 | },
10 |
11 | init: function () {
12 | this.handleScale = this.handleScale.bind(this);
13 | this.handleRotation = this.handleRotation.bind(this);
14 |
15 | this.isVisible = false;
16 | this.initialScale = this.el.object3D.scale.clone();
17 | this.scaleFactor = 1;
18 |
19 | this.el.sceneEl.addEventListener("markerFound", (e) => {
20 | this.isVisible = true;
21 | });
22 |
23 | this.el.sceneEl.addEventListener("markerLost", (e) => {
24 | this.isVisible = false;
25 | });
26 | },
27 |
28 | update: function () {
29 | if (this.data.enabled) {
30 | this.el.sceneEl.addEventListener("onefingermove", this.handleRotation);
31 | this.el.sceneEl.addEventListener("twofingermove", this.handleScale);
32 | } else {
33 | this.el.sceneEl.removeEventListener("onefingermove", this.handleRotation);
34 | this.el.sceneEl.removeEventListener("twofingermove", this.handleScale);
35 | }
36 | },
37 |
38 | remove: function () {
39 | this.el.sceneEl.removeEventListener("onefingermove", this.handleRotation);
40 | this.el.sceneEl.removeEventListener("twofingermove", this.handleScale);
41 | },
42 |
43 | handleRotation: function (event) {
44 | if (this.isVisible) {
45 | this.el.object3D.rotation.y +=
46 | event.detail.positionChange.x * this.data.rotationFactor;
47 | this.el.object3D.rotation.x +=
48 | event.detail.positionChange.y * this.data.rotationFactor;
49 | }
50 | },
51 |
52 | handleScale: function (event) {
53 | if (this.isVisible) {
54 | this.scaleFactor *=
55 | 1 + event.detail.spreadChange / event.detail.startSpread;
56 |
57 | this.scaleFactor = Math.min(
58 | Math.max(this.scaleFactor, this.data.minScale),
59 | this.data.maxScale
60 | );
61 |
62 | this.el.object3D.scale.x = this.scaleFactor * this.initialScale.x;
63 | this.el.object3D.scale.y = this.scaleFactor * this.initialScale.y;
64 | this.el.object3D.scale.z = this.scaleFactor * this.initialScale.z;
65 | }
66 | },
67 | });
68 |
--------------------------------------------------------------------------------
/image-tracking.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Gesture Interactions - A-Frame & AR.js
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
25 |
36 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Gesture Interactions - A-Frame & AR.js
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
25 |
26 |
30 |
31 |
32 |
33 |
40 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0px;
3 | overflow: hidden;
4 | }
--------------------------------------------------------------------------------