├── .gitignore ├── LICENSE ├── README.md ├── dist ├── aframe-motion-capture-components.js └── aframe-motion-capture-components.min.js ├── examples ├── animationTool.html ├── assets │ ├── dev-recording.json │ ├── ghost.mtl │ ├── ghost.obj │ ├── pacman.mtl │ ├── pacman.obj │ └── tracked-recording.json ├── development.html ├── js │ ├── build.js │ ├── components │ │ ├── aabb-collider.js │ │ ├── grab.js │ │ ├── ground.js │ │ ├── line.js │ │ └── ui-raycaster.js │ └── shaders │ │ └── skyGradient.js ├── queryParams.html ├── record.html ├── recordings │ ├── grabLeft.json │ └── grabRight.json ├── replay.html └── style.css ├── index.html ├── package.json ├── src ├── components │ ├── avatar-recorder.js │ ├── avatar-replayer.js │ ├── motion-capture-recorder.js │ ├── motion-capture-replayer.js │ └── stroke.js ├── constants.js ├── index.js └── systems │ ├── motion-capture-replayer.js │ └── recordingdb.js ├── tests ├── __init.test.js ├── helpers.js ├── index.test.js └── karma.conf.js └── webpack.dev.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sw[ponm] 3 | gh-pages 4 | node_modules/ 5 | npm-debug.log -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Diego Marcos 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## aframe-motion-capture-components 2 | 3 | [A-Frame](https://aframe.io) motion capture components record pose and events 4 | from entities (e.g., camera and tracked controllers) that can be stored in JSON 5 | or localStorage and then later replayed. 6 | 7 | The motion capture components allow us to emulate the presence of a VR headset 8 | and controllers. We can build test automation tools for VR experiences. We can 9 | replay the recorded user behavior and assert the state of the entities at the 10 | end. This can happen with no user intervention at all. 11 | 12 | We can also record user interactions and develop on the go where there's no VR 13 | hardware available. We can iterate over the visual aspect or behavior of the 14 | experience using the recorded user input. [Read more about the motion capture 15 | components](https://blog.mozvr.com/a-saturday-night/) and [its use cases as 16 | development tools](https://aframe.io/blog/motion-capture/). 17 | 18 | The A-Frame Inspector uses these components to power the Motion Capture 19 | Development Tools UI. 20 | 21 | [TRY THE DEMOS](http://swimminglessonsformodernlife.com/aframe-motion-capture-components/) 22 | 23 | ![](https://cloud.githubusercontent.com/assets/674727/24481580/0ac87ace-14a0-11e7-8281-c032c90f0529.gif) 24 | 25 | ## Usage 26 | 27 | The motion capture components is most easily used by opening the A-Frame 28 | Inspector (` + + i`), and hitting `m` to open the Motion Capture 29 | Development Tools UI. 30 | 31 | ### Avatar Recording 32 | 33 | An avatar is the representation of a user. Use the `avatar-recorder` to record 34 | headset and tracked controller poses as well as controller events (i.e., button 35 | presses and touches). 36 | 37 | 1. Set the `avatar-recorder` component on the `` element. 38 | 2. Make sure your controllers have `id`s. 39 | 3. Hit `` to start recording. 40 | 4. Record movements and controller events. 41 | 5. Hit `` again to stop recording. 42 | 6. You'll have an option to save the JSON file or upload it by pressing `u` on the keyboard. 43 | 7. The recording will play from `localStorage`. 44 | 45 | ```html 46 | 47 | 48 | 49 | 50 | ``` 51 | 52 | Hit `c` on the keyboard to clear all recordings from `localStorage`. 53 | 54 | ### Avatar Replaying 55 | 56 | The `avatar-recorder` will automatically set the `avatar-replayer` component. 57 | Though we can specify the `avatar-replayer` explicitly if we want to configure 58 | it or if we don't need recording (i.e., production). 59 | 60 | `avatar-replayer` can be manually disabled from the URL query parameter 61 | `avatar-replayer-disabled` (e.g., 62 | `http://localhost:8000/?avatar-replayer-disabled`). `spectator-mode` can be 63 | enabled using the URL query parameter `specatatorMode`. 64 | 65 | ##### From localStorage 66 | 67 | By default, the `avatar-recorder` will save the recording into `localStorage` 68 | which the `avatar-replayer` will replay from by default. Recordings are stored 69 | in `localStorage.getItem('avatarRecordings')` and are keyed `recordingName` 70 | (defaults to `default`). 71 | 72 | Hit `p` to toggle playback. 73 | 74 | ##### From File 75 | 76 | We can specify the path to a recording file via the `avatar-recording` **query 77 | parameter** in the URL: 78 | 79 | ```html 80 | https://foo.bar?avatar-recording=path/to/recording.json 81 | https://foo.bar?avatar-recording=path/to/anotherRecording.json 82 | ``` 83 | 84 | Or we can specify the path to a recording file in the HTML via the `src` property: 85 | 86 | ```html 87 | 88 | 89 | 90 | 91 | ``` 92 | 93 | ## API 94 | 95 | ### avatar-recorder 96 | 97 | | Property | Description | Default Value | 98 | | ----------------- | ------------------------------------------------------- | ------------- | 99 | | autoPlay | Whether to play recording on page load. | true | 100 | | autoRecord | Whether to start recording on page load. | false | 101 | | autoSaveFile | Whether to prompt to save a JSON of the recording to file system. | true | 102 | | localStorage | Whether to persist recordings in localStorage keyed as `avatarRecordings`. | false | 103 | | loop | Whether to replay recording in a loop. | false | 104 | | recordingName | Name of recording to store in `localStorage.getItem('avatarRecordings')`. | default | 105 | | spectatorMode | Whether to replay recording in third person mode. | false | 106 | | spectatorPosition | Initial position of the spectator camera. | 0 0 0 | 107 | 108 | #### Methods 109 | 110 | | Method | Description | 111 | | ----------------- | ------------------------------------------------------- | 112 | | saveRecordingFile (recording) | Save recording to file. `recording` can either be raw data or recording name stored in localStorage. | 113 | | startRecording () | Start recording | 114 | | stopRecording () | Stop recording. | 115 | 116 | #### Keyboard Shortcuts 117 | 118 | | Key | Description | 119 | | ------- | ---------------------------------------------- | 120 | | space | Toggle recording. | 121 | | q | Toggle spectator mode camera. | 122 | | c | Clear recording from localStorage and memory. | 123 | | u | Upload recording to file host and get short URL. | 124 | 125 | ### avatar-replayer 126 | 127 | For spectator mode, `avatar-replayer` will create a head geometry to make the 128 | camera visible, represented as a pink box with eyes. This set as 129 | `cameraEl.getObject3D('replayerMesh')` but is not visible by default. 130 | 131 | | Property | Description | Default Value | 132 | | ----------------- | ------------------------------------------ | ------------- | 133 | | autoPlay | Whether to play recording on page load. | true | 134 | | loop | Whether to replay recording in a loop. | false | 135 | | recordingName | Specify to replay recording from localStorage. | default | 136 | | spectatorMode | Whether to replay recording in third person mode. | false | 137 | | spectatorPosition | Initial position of the spectator camera. | 0 0 0 | 138 | | src | Path or URL to recording data. | '' | 139 | 140 | #### Methods 141 | 142 | | Method | Description | 143 | | ----------------- | ------------------------------------------------------- | 144 | | replayRecordingFromSource () | Replay recording from either `recordingName` for localStorage or `src` for external file. | 145 | | startReplaying (recordingData) | Start replaying given passed recording data (object). | 146 | | stopReplaying () | Stop replaying. | 147 | 148 | ### motion-capture-replayer 149 | 150 | | Property | Description | Default Value | 151 | | -------- | ---------------------------------------------------- | ------------- | 152 | | enabled | | true | 153 | | loop | The animation replays in a loop. | false | 154 | | recorderEl | An entity that it's the source of the recording. | null | 155 | | src | The recording data can be hosted in a URL. | '' | 156 | 157 | ### motion-capture-recorder 158 | 159 | | Property | Description | Default Value | 160 | | -------- | ----------------------------------------------------- | ------------- | 161 | | autoRecord | The component start recording at page load. | false | 162 | | enabled | | true | 163 | | hand | The controller that will trigger recording. | 'right' | 164 | | recordingControls | Recording is activated by the controller trigger | false | 165 | | persistStroke | The recorded stroke is persisted as reference. | false | 166 | | visibleStroke | The recorded stroke is renderered for visual feedback.| true | 167 | 168 | ## Installation 169 | 170 | ### Browser 171 | 172 | Install and use by directly including the [browser files](dist): 173 | 174 | ```html 175 | 176 | Motion Capture 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | ``` 187 | 188 | Or with [angle](https://npmjs.com/package/angle/), you can install the proper 189 | version of the component straight into your HTML file, respective to your 190 | version of A-Frame: 191 | 192 | ```sh 193 | npm install -g angle && angle install aframe-motion-capture-components 194 | ``` 195 | 196 | ### npm 197 | 198 | Install via npm: 199 | 200 | ```bash 201 | npm install aframe-motion-capture-components 202 | ``` 203 | 204 | Then require and use. 205 | 206 | ```js 207 | require('aframe'); 208 | require('aframe-motion-capture-components'); 209 | ``` 210 | -------------------------------------------------------------------------------- /dist/aframe-motion-capture-components.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, __webpack_require__) { 46 | 47 | if (typeof AFRAME === 'undefined') { 48 | throw new Error('Component attempted to register before AFRAME was available.'); 49 | } 50 | 51 | // Components. 52 | __webpack_require__(1); 53 | __webpack_require__(2); 54 | __webpack_require__(3); 55 | __webpack_require__(5); 56 | __webpack_require__(6); 57 | 58 | // Systems. 59 | __webpack_require__(7); 60 | __webpack_require__(8); 61 | 62 | 63 | /***/ }), 64 | /* 1 */ 65 | /***/ (function(module, exports) { 66 | 67 | /* global AFRAME, THREE */ 68 | 69 | var EVENTS = { 70 | axismove: {id: 0, props: ['id', 'axis', 'changed']}, 71 | buttonchanged: {id: 1, props: ['id', 'state']}, 72 | buttondown: {id: 2, props: ['id', 'state']}, 73 | buttonup: {id: 3, props: ['id', 'state']}, 74 | touchstart: {id: 4, props: ['id', 'state']}, 75 | touchend: {id: 5, props: ['id', 'state']} 76 | }; 77 | 78 | var EVENTS_DECODE = { 79 | 0: 'axismove', 80 | 1: 'buttonchanged', 81 | 2: 'buttondown', 82 | 3: 'buttonup', 83 | 4: 'touchstart', 84 | 5: 'touchend' 85 | }; 86 | 87 | AFRAME.registerComponent('motion-capture-recorder', { 88 | schema: { 89 | autoRecord: {default: false}, 90 | enabled: {default: true}, 91 | hand: {default: 'right'}, 92 | recordingControls: {default: false}, 93 | persistStroke: {default: false}, 94 | visibleStroke: {default: true} 95 | }, 96 | 97 | init: function () { 98 | this.drawing = false; 99 | this.recordedEvents = []; 100 | this.recordedPoses = []; 101 | this.addEventListeners(); 102 | }, 103 | 104 | addEventListeners: function () { 105 | var el = this.el; 106 | this.recordEvent = this.recordEvent.bind(this); 107 | el.addEventListener('axismove', this.recordEvent); 108 | el.addEventListener('buttonchanged', this.onTriggerChanged.bind(this)); 109 | el.addEventListener('buttonchanged', this.recordEvent); 110 | el.addEventListener('buttonup', this.recordEvent); 111 | el.addEventListener('buttondown', this.recordEvent); 112 | el.addEventListener('touchstart', this.recordEvent); 113 | el.addEventListener('touchend', this.recordEvent); 114 | }, 115 | 116 | recordEvent: function (evt) { 117 | var detail; 118 | if (!this.isRecording) { return; } 119 | 120 | // Filter out `target`, not serializable. 121 | if ('detail' in evt && 'state' in evt.detail && typeof evt.detail.state === 'object' && 122 | 'target' in evt.detail.state) { 123 | delete evt.detail.state.target; 124 | } 125 | 126 | detail = {}; 127 | EVENTS[evt.type].props.forEach(function buildDetail (propName) { 128 | // Convert GamepadButton to normal JS object. 129 | if (propName === 'state') { 130 | var stateProp; 131 | detail.state = {}; 132 | for (stateProp in evt.detail.state) { 133 | detail.state[stateProp] = evt.detail.state[stateProp]; 134 | } 135 | return; 136 | } 137 | detail[propName] = evt.detail[propName]; 138 | }); 139 | 140 | this.recordedEvents.push({ 141 | name: evt.type, 142 | detail: detail, 143 | timestamp: this.lastTimestamp 144 | }); 145 | }, 146 | 147 | onTriggerChanged: function (evt) { 148 | var data = this.data; 149 | var value; 150 | if (!data.enabled || data.autoRecord) { return; } 151 | // Not Trigger 152 | if (evt.detail.id !== 1 || !this.data.recordingControls) { return; } 153 | value = evt.detail.state.value; 154 | if (value <= 0.1) { 155 | if (this.isRecording) { this.stopRecording(); } 156 | return; 157 | } 158 | if (!this.isRecording) { this.startRecording(); } 159 | }, 160 | 161 | getJSONData: function () { 162 | var data; 163 | var trackedControlsComponent = this.el.components['tracked-controls']; 164 | var controller = trackedControlsComponent && trackedControlsComponent.controller; 165 | if (!this.recordedPoses) { return; } 166 | data = { 167 | poses: this.getStrokeJSON(this.recordedPoses), 168 | events: this.recordedEvents 169 | }; 170 | if (controller) { 171 | data.gamepad = { 172 | id: controller.id, 173 | hand: controller.hand, 174 | index: controller.index 175 | }; 176 | } 177 | return data; 178 | }, 179 | 180 | getStrokeJSON: function (stroke) { 181 | var point; 182 | var points = []; 183 | for (var i = 0; i < stroke.length; i++) { 184 | point = stroke[i]; 185 | points.push({ 186 | position: point.position, 187 | rotation: point.rotation, 188 | timestamp: point.timestamp 189 | }); 190 | } 191 | return points; 192 | }, 193 | 194 | saveCapture: function (binary) { 195 | var jsonData = JSON.stringify(this.getJSONData()); 196 | var type = binary ? 'application/octet-binary' : 'application/json'; 197 | var blob = new Blob([jsonData], {type: type}); 198 | var url = URL.createObjectURL(blob); 199 | var fileName = 'motion-capture-' + document.title + '-' + Date.now() + '.json'; 200 | var aEl = document.createElement('a'); 201 | aEl.setAttribute('class', 'motion-capture-download'); 202 | aEl.href = url; 203 | aEl.setAttribute('download', fileName); 204 | aEl.innerHTML = 'downloading...'; 205 | aEl.style.display = 'none'; 206 | document.body.appendChild(aEl); 207 | setTimeout(function () { 208 | aEl.click(); 209 | document.body.removeChild(aEl); 210 | }, 1); 211 | }, 212 | 213 | update: function () { 214 | var el = this.el; 215 | var data = this.data; 216 | if (this.data.autoRecord) { 217 | this.startRecording(); 218 | } else { 219 | // Don't try to record camera with controllers. 220 | if (el.components.camera) { return; } 221 | 222 | if (data.recordingControls) { 223 | el.setAttribute('vive-controls', {hand: data.hand}); 224 | el.setAttribute('oculus-touch-controls', {hand: data.hand}); 225 | } 226 | el.setAttribute('stroke', ''); 227 | } 228 | }, 229 | 230 | tick: (function () { 231 | var position = new THREE.Vector3(); 232 | var rotation = new THREE.Quaternion(); 233 | var scale = new THREE.Vector3(); 234 | 235 | return function (time, delta) { 236 | var newPoint; 237 | var pointerPosition; 238 | this.lastTimestamp = time; 239 | if (!this.data.enabled || !this.isRecording) { return; } 240 | newPoint = { 241 | position: AFRAME.utils.clone(this.el.getAttribute('position')), 242 | rotation: AFRAME.utils.clone(this.el.getAttribute('rotation')), 243 | timestamp: time 244 | }; 245 | this.recordedPoses.push(newPoint); 246 | if (!this.data.visibleStroke) { return; } 247 | this.el.object3D.updateMatrixWorld(); 248 | this.el.object3D.matrixWorld.decompose(position, rotation, scale); 249 | pointerPosition = this.getPointerPosition(position, rotation); 250 | this.el.components.stroke.drawPoint(position, rotation, time, pointerPosition); 251 | }; 252 | })(), 253 | 254 | getPointerPosition: (function () { 255 | var pointerPosition = new THREE.Vector3(); 256 | var offset = new THREE.Vector3(0, 0.7, 1); 257 | return function getPointerPosition (position, orientation) { 258 | var pointer = offset 259 | .clone() 260 | .applyQuaternion(orientation) 261 | .normalize() 262 | .multiplyScalar(-0.03); 263 | pointerPosition.copy(position).add(pointer); 264 | return pointerPosition; 265 | }; 266 | })(), 267 | 268 | startRecording: function () { 269 | var el = this.el; 270 | if (this.isRecording) { return; } 271 | if (el.components.stroke) { el.components.stroke.reset(); } 272 | this.isRecording = true; 273 | this.recordedPoses = []; 274 | this.recordedEvents = []; 275 | el.emit('strokestarted', {entity: el, poses: this.recordedPoses}); 276 | }, 277 | 278 | stopRecording: function () { 279 | var el = this.el; 280 | if (!this.isRecording) { return; } 281 | el.emit('strokeended', {poses: this.recordedPoses}); 282 | this.isRecording = false; 283 | if (!this.data.visibleStroke || this.data.persistStroke) { return; } 284 | el.components.stroke.reset(); 285 | } 286 | }); 287 | 288 | 289 | /***/ }), 290 | /* 2 */ 291 | /***/ (function(module, exports) { 292 | 293 | /* global THREE, AFRAME */ 294 | AFRAME.registerComponent('motion-capture-replayer', { 295 | schema: { 296 | enabled: {default: true}, 297 | recorderEl: {type: 'selector'}, 298 | loop: {default: false}, 299 | src: {default: ''}, 300 | spectatorCamera: {default: false} 301 | }, 302 | 303 | init: function () { 304 | this.currentPoseTime = 0; 305 | this.currentEventTime = 0; 306 | this.currentPoseIndex = 0; 307 | this.currentEventIndex = 0; 308 | this.onStrokeStarted = this.onStrokeStarted.bind(this); 309 | this.onStrokeEnded = this.onStrokeEnded.bind(this); 310 | this.playComponent = this.playComponent.bind(this); 311 | this.el.addEventListener('pause', this.playComponent); 312 | this.discardedFrames = 0; 313 | this.playingEvents = []; 314 | this.playingPoses = []; 315 | this.gamepadData = null; 316 | }, 317 | 318 | remove: function () { 319 | var el = this.el; 320 | var gamepadData = this.gamepadData; 321 | var gamepads; 322 | var found = -1; 323 | 324 | el.removeEventListener('pause', this.playComponent); 325 | this.stopReplaying(); 326 | el.pause(); 327 | el.play(); 328 | 329 | // Remove gamepad from system. 330 | if (this.gamepadData) { 331 | gamepads = el.sceneEl.systems['motion-capture-replayer'].gamepads; 332 | gamepads.forEach(function (gamepad, i) { 333 | if (gamepad === gamepadData) { found = i; } 334 | }); 335 | if (found !== -1) { 336 | gamepads.splice(found, 1); 337 | } 338 | } 339 | }, 340 | 341 | update: function (oldData) { 342 | var data = this.data; 343 | this.updateRecorder(data.recorderEl, oldData.recorderEl); 344 | if (!this.el.isPlaying) { this.playComponent(); } 345 | if (oldData.src === data.src) { return; } 346 | if (data.src) { this.updateSrc(data.src); } 347 | }, 348 | 349 | updateRecorder: function (newRecorderEl, oldRecorderEl) { 350 | if (oldRecorderEl && oldRecorderEl !== newRecorderEl) { 351 | oldRecorderEl.removeEventListener('strokestarted', this.onStrokeStarted); 352 | oldRecorderEl.removeEventListener('strokeended', this.onStrokeEnded); 353 | } 354 | if (!newRecorderEl || oldRecorderEl === newRecorderEl) { return; } 355 | newRecorderEl.addEventListener('strokestarted', this.onStrokeStarted); 356 | newRecorderEl.addEventListener('strokeended', this.onStrokeEnded); 357 | }, 358 | 359 | updateSrc: function (src) { 360 | this.el.sceneEl.systems['motion-capture-recorder'].loadRecordingFromUrl( 361 | src, false, this.startReplaying.bind(this)); 362 | }, 363 | 364 | onStrokeStarted: function(evt) { 365 | this.reset(); 366 | }, 367 | 368 | onStrokeEnded: function(evt) { 369 | this.startReplayingPoses(evt.detail.poses); 370 | }, 371 | 372 | play: function () { 373 | if (this.playingStroke) { this.playStroke(this.playingStroke); } 374 | }, 375 | 376 | playComponent: function () { 377 | this.el.isPlaying = true; 378 | this.play(); 379 | }, 380 | 381 | /** 382 | * @param {object} data - Recording data. 383 | */ 384 | startReplaying: function (data) { 385 | var el = this.el; 386 | 387 | this.ignoredFrames = 0; 388 | this.storeInitialPose(); 389 | this.isReplaying = true; 390 | this.startReplayingPoses(data.poses); 391 | this.startReplayingEvents(data.events); 392 | 393 | // Add gamepad metadata to system. 394 | if (data.gamepad) { 395 | this.gamepadData = data.gamepad; 396 | el.sceneEl.systems['motion-capture-replayer'].gamepads.push(data.gamepad); 397 | el.emit('gamepadconnected'); 398 | } 399 | 400 | el.emit('replayingstarted'); 401 | }, 402 | 403 | stopReplaying: function () { 404 | this.isReplaying = false; 405 | this.restoreInitialPose(); 406 | this.el.emit('replayingstopped'); 407 | }, 408 | 409 | storeInitialPose: function () { 410 | var el = this.el; 411 | this.initialPose = { 412 | position: AFRAME.utils.clone(el.getAttribute('position')), 413 | rotation: AFRAME.utils.clone(el.getAttribute('rotation')) 414 | }; 415 | }, 416 | 417 | restoreInitialPose: function () { 418 | var el = this.el; 419 | if (!this.initialPose) { return; } 420 | el.setAttribute('position', this.initialPose.position); 421 | el.setAttribute('rotation', this.initialPose.rotation); 422 | }, 423 | 424 | startReplayingPoses: function (poses) { 425 | this.isReplaying = true; 426 | this.currentPoseIndex = 0; 427 | if (poses.length === 0) { return; } 428 | this.playingPoses = poses; 429 | this.currentPoseTime = poses[0].timestamp; 430 | }, 431 | 432 | /** 433 | * @param events {Array} - Array of events with timestamp, name, and detail. 434 | */ 435 | startReplayingEvents: function (events) { 436 | var firstEvent; 437 | this.isReplaying = true; 438 | this.currentEventIndex = 0; 439 | if (events.length === 0) { return; } 440 | firstEvent = events[0]; 441 | this.playingEvents = events; 442 | this.currentEventTime = firstEvent.timestamp; 443 | this.el.emit(firstEvent.name, firstEvent.detail); 444 | }, 445 | 446 | // Reset player 447 | reset: function () { 448 | this.playingPoses = null; 449 | this.currentTime = undefined; 450 | this.currentPoseIndex = undefined; 451 | }, 452 | 453 | /** 454 | * Called on tick. 455 | */ 456 | playRecording: function (delta) { 457 | var currentPose; 458 | var currentEvent 459 | var playingPoses = this.playingPoses; 460 | var playingEvents = this.playingEvents; 461 | currentPose = playingPoses && playingPoses[this.currentPoseIndex] 462 | currentEvent = playingEvents && playingEvents[this.currentEventIndex]; 463 | this.currentPoseTime += delta; 464 | this.currentEventTime += delta; 465 | // Determine next pose. 466 | // Comparing currentPoseTime to currentEvent.timestamp is not a typo. 467 | while ((currentPose && this.currentPoseTime >= currentPose.timestamp) || 468 | (currentEvent && this.currentPoseTime >= currentEvent.timestamp)) { 469 | // Pose. 470 | if (currentPose && this.currentPoseTime >= currentPose.timestamp) { 471 | if (this.currentPoseIndex === playingPoses.length - 1) { 472 | if (this.data.loop) { 473 | this.currentPoseIndex = 0; 474 | this.currentPoseTime = playingPoses[0].timestamp; 475 | } else { 476 | this.stopReplaying(); 477 | } 478 | } 479 | applyPose(this.el, currentPose); 480 | this.currentPoseIndex += 1; 481 | currentPose = playingPoses[this.currentPoseIndex]; 482 | } 483 | // Event. 484 | if (currentEvent && this.currentPoseTime >= currentEvent.timestamp) { 485 | if (this.currentEventIndex === playingEvents.length && this.data.loop) { 486 | this.currentEventIndex = 0; 487 | this.currentEventTime = playingEvents[0].timestamp; 488 | } 489 | this.el.emit(currentEvent.name, currentEvent.detail); 490 | this.currentEventIndex += 1; 491 | currentEvent = this.playingEvents[this.currentEventIndex]; 492 | } 493 | } 494 | }, 495 | 496 | tick: function (time, delta) { 497 | // Ignore the first couple of frames that come from window.RAF on Firefox. 498 | if (this.ignoredFrames !== 2 && !window.debug) { 499 | this.ignoredFrames++; 500 | return; 501 | } 502 | 503 | if (!this.isReplaying) { return; } 504 | this.playRecording(delta); 505 | } 506 | }); 507 | 508 | function applyPose (el, pose) { 509 | el.setAttribute('position', pose.position); 510 | el.setAttribute('rotation', pose.rotation); 511 | }; 512 | 513 | 514 | /***/ }), 515 | /* 3 */ 516 | /***/ (function(module, exports, __webpack_require__) { 517 | 518 | /* global THREE, AFRAME */ 519 | var constants = __webpack_require__(4); 520 | var log = AFRAME.utils.debug('aframe-motion-capture:avatar-recorder:info'); 521 | var warn = AFRAME.utils.debug('aframe-motion-capture:avatar-recorder:warn'); 522 | 523 | /** 524 | * Wrapper around individual motion-capture-recorder components for recording camera and 525 | * controllers together. 526 | */ 527 | AFRAME.registerComponent('avatar-recorder', { 528 | schema: { 529 | autoPlay: {default: false}, 530 | autoRecord: {default: false}, 531 | cameraOverride: {type: 'selector'}, 532 | localStorage: {default: true}, 533 | recordingName: {default: constants.DEFAULT_RECORDING_NAME}, 534 | loop: {default: true} 535 | }, 536 | 537 | init: function () { 538 | this.cameraEl = null; 539 | this.isRecording = false; 540 | this.trackedControllerEls = {}; 541 | this.recordingData = null; 542 | 543 | this.onKeyDown = AFRAME.utils.bind(this.onKeyDown, this); 544 | this.tick = AFRAME.utils.throttle(this.throttledTick, 100, this); 545 | }, 546 | 547 | /** 548 | * Poll for tracked controllers. 549 | */ 550 | throttledTick: function () { 551 | var self = this; 552 | var trackedControllerEls = this.el.querySelectorAll('[tracked-controls]'); 553 | this.trackedControllerEls = {}; 554 | trackedControllerEls.forEach(function setupController (trackedControllerEl) { 555 | if (!trackedControllerEl.id) { 556 | warn('Found a tracked controller entity without an ID. ' + 557 | 'Provide an ID or this controller will not be recorded'); 558 | return; 559 | } 560 | trackedControllerEl.setAttribute('motion-capture-recorder', { 561 | autoRecord: false, 562 | visibleStroke: false 563 | }); 564 | self.trackedControllerEls[trackedControllerEl.id] = trackedControllerEl; 565 | if (self.isRecording) { 566 | trackedControllerEl.components['motion-capture-recorder'].startRecording(); 567 | } 568 | }); 569 | }, 570 | 571 | play: function () { 572 | window.addEventListener('keydown', this.onKeyDown); 573 | }, 574 | 575 | pause: function () { 576 | window.removeEventListener('keydown', this.onKeyDown); 577 | }, 578 | 579 | /** 580 | * Keyboard shortcuts. 581 | */ 582 | onKeyDown: function (evt) { 583 | var key = evt.keyCode; 584 | var KEYS = {space: 32}; 585 | switch (key) { 586 | // : Toggle recording. 587 | case KEYS.space: { 588 | this.toggleRecording(); 589 | break; 590 | } 591 | } 592 | }, 593 | 594 | /** 595 | * Start or stop recording. 596 | */ 597 | toggleRecording: function () { 598 | if (this.isRecording) { 599 | this.stopRecording(); 600 | } else { 601 | this.startRecording(); 602 | } 603 | }, 604 | 605 | /** 606 | * Set motion capture recorder on the camera once the camera is ready. 607 | */ 608 | setupCamera: function (doneCb) { 609 | var el = this.el; 610 | var self = this; 611 | 612 | if (this.data.cameraOverride) { 613 | prepareCamera(this.data.cameraOverride); 614 | return; 615 | } 616 | 617 | // Grab camera. 618 | if (el.camera && el.camera.el) { 619 | prepareCamera(el.camera.el); 620 | return; 621 | } 622 | 623 | el.addEventListener('camera-set-active', function setup (evt) { 624 | prepareCamera(evt.detail.cameraEl); 625 | el.removeEventListener('camera-set-active', setup); 626 | }); 627 | 628 | function prepareCamera (cameraEl) { 629 | if (self.cameraEl) { 630 | self.cameraEl.removeAttribute('motion-capture-recorder'); 631 | } 632 | self.cameraEl = cameraEl; 633 | cameraEl.setAttribute('motion-capture-recorder', { 634 | autoRecord: false, 635 | visibleStroke: false 636 | }); 637 | doneCb(cameraEl) 638 | } 639 | }, 640 | 641 | /** 642 | * Start recording camera and tracked controls. 643 | */ 644 | startRecording: function () { 645 | var trackedControllerEls = this.trackedControllerEls; 646 | var self = this; 647 | 648 | if (this.isRecording) { return; } 649 | 650 | log('Starting recording!'); 651 | 652 | if (this.el.components['avatar-replayer']) { 653 | this.el.components['avatar-replayer'].stopReplaying(); 654 | } 655 | 656 | // Get camera. 657 | this.setupCamera(function cameraSetUp () { 658 | self.isRecording = true; 659 | // Record camera. 660 | self.cameraEl.components['motion-capture-recorder'].startRecording(); 661 | // Record tracked controls. 662 | Object.keys(trackedControllerEls).forEach(function startRecordingController (id) { 663 | trackedControllerEls[id].components['motion-capture-recorder'].startRecording(); 664 | }); 665 | }); 666 | }, 667 | 668 | /** 669 | * Tell camera and tracked controls motion-capture-recorder components to stop recording. 670 | * Store recording and replay if autoPlay is on. 671 | */ 672 | stopRecording: function () { 673 | var trackedControllerEls = this.trackedControllerEls; 674 | 675 | if (!this.isRecording) { return; } 676 | 677 | log('Stopped recording.'); 678 | this.isRecording = false; 679 | this.cameraEl.components['motion-capture-recorder'].stopRecording(); 680 | Object.keys(trackedControllerEls).forEach(function (id) { 681 | trackedControllerEls[id].components['motion-capture-recorder'].stopRecording(); 682 | }); 683 | this.recordingData = this.getJSONData(); 684 | this.storeRecording(this.recordingData); 685 | 686 | if (this.data.autoPlay) { 687 | this.replayRecording(); 688 | } 689 | }, 690 | 691 | /** 692 | * Gather the JSON data from the camera and tracked controls motion-capture-recorder 693 | * components. Combine them together, keyed by the (active) `camera` and by the 694 | * tracked controller IDs. 695 | */ 696 | getJSONData: function () { 697 | var data = {}; 698 | var trackedControllerEls = this.trackedControllerEls; 699 | 700 | if (this.isRecording) { return; } 701 | 702 | // Camera. 703 | data.camera = this.cameraEl.components['motion-capture-recorder'].getJSONData(); 704 | 705 | // Tracked controls. 706 | Object.keys(trackedControllerEls).forEach(function getControllerData (id) { 707 | data[id] = trackedControllerEls[id].components['motion-capture-recorder'].getJSONData(); 708 | }); 709 | 710 | return data; 711 | }, 712 | 713 | /** 714 | * Store recording in IndexedDB using recordingdb system. 715 | */ 716 | storeRecording: function (recordingData) { 717 | var data = this.data; 718 | if (!data.localStorage) { return; } 719 | log('Recording stored in localStorage.'); 720 | this.el.systems.recordingdb.addRecording(data.recordingName, recordingData); 721 | } 722 | }); 723 | 724 | 725 | /***/ }), 726 | /* 4 */ 727 | /***/ (function(module, exports) { 728 | 729 | module.exports.LOCALSTORAGE_RECORDINGS = 'avatarRecordings'; 730 | module.exports.DEFAULT_RECORDING_NAME = 'default'; 731 | 732 | 733 | /***/ }), 734 | /* 5 */ 735 | /***/ (function(module, exports, __webpack_require__) { 736 | 737 | /* global THREE, AFRAME */ 738 | var constants = __webpack_require__(4); 739 | 740 | var bind = AFRAME.utils.bind; 741 | var error = AFRAME.utils.debug('aframe-motion-capture:avatar-replayer:error'); 742 | var log = AFRAME.utils.debug('aframe-motion-capture:avatar-replayer:info'); 743 | var warn = AFRAME.utils.debug('aframe-motion-capture:avatar-replayer:warn'); 744 | 745 | var fileLoader = new THREE.FileLoader(); 746 | 747 | AFRAME.registerComponent('avatar-replayer', { 748 | schema: { 749 | autoPlay: {default: true}, 750 | cameraOverride: {type: 'selector'}, 751 | loop: {default: false}, 752 | recordingName: {default: constants.DEFAULT_RECORDING_NAME}, 753 | spectatorMode: {default: false}, 754 | spectatorPosition: {default: {x: 0, y: 1.6, z: 2}, type: 'vec3'}, 755 | src: {default: ''} 756 | }, 757 | 758 | init: function () { 759 | var sceneEl = this.el; 760 | 761 | // Bind methods. 762 | this.onKeyDown = bind(this.onKeyDown, this); 763 | 764 | // Prepare camera. 765 | this.setupCamera = bind(this.setupCamera, this); 766 | if (sceneEl.camera) { 767 | this.setupCamera(); 768 | } else { 769 | sceneEl.addEventListener('camera-set-active', this.setupCamera); 770 | } 771 | 772 | if (this.data.autoPlay) { 773 | this.replayRecordingFromSource(); 774 | } 775 | }, 776 | 777 | update: function (oldData) { 778 | var data = this.data; 779 | var spectatorModeUrlParam; 780 | 781 | spectatorModeUrlParam = 782 | window.location.search.indexOf('spectatormode') !== -1 || 783 | window.location.search.indexOf('spectatorMode') !== -1; 784 | 785 | // Handle toggling spectator mode. Don't run on initialization. Want to activate after 786 | // the player camera is initialized. 787 | if (oldData.spectatorMode !== data.spectatorMode || 788 | spectatorModeUrlParam) { 789 | if (data.spectatorMode || spectatorModeUrlParam) { 790 | this.activateSpectatorCamera(); 791 | } else if (oldData.spectatorMode === true) { 792 | this.deactivateSpectatorCamera(); 793 | } 794 | } 795 | 796 | // Handle `src` changing. 797 | if (data.src && oldData.src !== data.src && data.autoPlay) { 798 | this.replayRecordingFromSource(); 799 | } 800 | }, 801 | 802 | play: function () { 803 | window.addEventListener('keydown', this.onKeyDown); 804 | }, 805 | 806 | pause: function () { 807 | window.removeEventListener('keydown', this.onKeyDown); 808 | }, 809 | 810 | remove: function () { 811 | this.stopReplaying(); 812 | this.cameraEl.removeObject3D('replayerMesh'); 813 | }, 814 | 815 | /** 816 | * Grab a handle to the "original" camera. 817 | * Initialize spectator camera and dummy geometry for original camera. 818 | */ 819 | setupCamera: function () { 820 | var data = this.data; 821 | var sceneEl = this.el; 822 | 823 | if (data.cameraOverride) { 824 | // Specify which camera is the original camera (e.g., used by Inspector). 825 | this.cameraEl = data.cameraOverride; 826 | } else { 827 | // Default camera. 828 | this.cameraEl = sceneEl.camera.el; 829 | // Make sure A-Frame doesn't automatically remove this camera. 830 | this.cameraEl.removeAttribute('data-aframe-default-camera'); 831 | } 832 | this.cameraEl.setAttribute('data-aframe-avatar-replayer-camera', ''); 833 | 834 | sceneEl.removeEventListener('camera-set-active', this.setupCamera); 835 | 836 | this.configureHeadGeometry(); 837 | 838 | // Create spectator camera for either if we are in spectator mode or toggling to it. 839 | this.initSpectatorCamera(); 840 | }, 841 | 842 | /** 843 | * q: Toggle spectator camera. 844 | */ 845 | onKeyDown: function (evt) { 846 | switch (evt.keyCode) { 847 | // q. 848 | case 81: { 849 | this.el.setAttribute('avatar-replayer', 'spectatorMode', !this.data.spectatorMode); 850 | break; 851 | } 852 | } 853 | }, 854 | 855 | /** 856 | * Activate spectator camera, show replayer mesh. 857 | */ 858 | activateSpectatorCamera: function () { 859 | var spectatorCameraEl = this.spectatorCameraEl; 860 | 861 | if (!spectatorCameraEl) { 862 | this.el.addEventListener('spectatorcameracreated', 863 | bind(this.activateSpectatorCamera, this)); 864 | return; 865 | } 866 | 867 | if (!spectatorCameraEl.hasLoaded) { 868 | spectatorCameraEl.addEventListener('loaded', bind(this.activateSpectatorCamera, this)); 869 | return; 870 | } 871 | 872 | log('Activating spectator camera'); 873 | spectatorCameraEl.setAttribute('camera', 'active', true); 874 | this.cameraEl.getObject3D('replayerMesh').visible = true; 875 | }, 876 | 877 | /** 878 | * Deactivate spectator camera (by setting original camera active), hide replayer mesh. 879 | */ 880 | deactivateSpectatorCamera: function () { 881 | log('Deactivating spectator camera'); 882 | this.cameraEl.setAttribute('camera', 'active', true); 883 | this.cameraEl.getObject3D('replayerMesh').visible = false; 884 | }, 885 | 886 | /** 887 | * Create and activate spectator camera if in spectator mode. 888 | */ 889 | initSpectatorCamera: function () { 890 | var data = this.data; 891 | var sceneEl = this.el; 892 | var spectatorCameraEl; 893 | var spectatorCameraRigEl; 894 | 895 | // Developer-defined spectator rig. 896 | if (this.el.querySelector('#spectatorCameraRig')) { 897 | this.spectatorCameraEl = sceneEl.querySelector('#spectatorCameraRig'); 898 | return; 899 | } 900 | 901 | // Create spectator camera rig. 902 | spectatorCameraRigEl = sceneEl.querySelector('#spectatorCameraRig') || 903 | document.createElement('a-entity'); 904 | spectatorCameraRigEl.id = 'spectatorCameraRig'; 905 | spectatorCameraRigEl.setAttribute('position', data.spectatorPosition); 906 | this.spectatorCameraRigEl = spectatorCameraRigEl; 907 | 908 | // Create spectator camera. 909 | spectatorCameraEl = sceneEl.querySelector('#spectatorCamera') || 910 | document.createElement('a-entity'); 911 | spectatorCameraEl.id = 'spectatorCamera'; 912 | spectatorCameraEl.setAttribute('camera', {active: data.spectatorMode, userHeight: 0}); 913 | spectatorCameraEl.setAttribute('look-controls', ''); 914 | spectatorCameraEl.setAttribute('wasd-controls', {fly: true}); 915 | this.spectatorCameraEl = spectatorCameraEl; 916 | 917 | // Append rig. 918 | spectatorCameraRigEl.appendChild(spectatorCameraEl); 919 | sceneEl.appendChild(spectatorCameraRigEl); 920 | sceneEl.emit('spectatorcameracreated'); 921 | }, 922 | 923 | /** 924 | * Check for recording sources and play. 925 | */ 926 | replayRecordingFromSource: function () { 927 | var data = this.data; 928 | var recordingdb = this.el.systems.recordingdb;; 929 | var recordingNames; 930 | var src; 931 | var self = this; 932 | 933 | // Allow override to display replayer from query param. 934 | if (new URLSearchParams(window.location.search).get('avatar-replayer-disabled') !== null) { 935 | return; 936 | } 937 | 938 | recordingdb.getRecordingNames().then(function (recordingNames) { 939 | // See if recording defined in query parameter. 940 | var queryParamSrc = self.getSrcFromSearchParam(); 941 | 942 | // 1. Try `avatar-recorder` query parameter as recording name from IndexedDB. 943 | if (recordingNames.indexOf(queryParamSrc) !== -1) { 944 | log('Replaying `' + queryParamSrc + '` from IndexedDB.'); 945 | recordingdb.getRecording(queryParamSrc).then(bind(self.startReplaying, self)); 946 | return; 947 | } 948 | 949 | // 2. Use `avatar-recorder` query parameter or `data.src` as URL. 950 | src = queryParamSrc || self.data.src; 951 | if (src) { 952 | if (self.data.src) { 953 | log('Replaying from component `src`', src); 954 | } else if (queryParamSrc) { 955 | log('Replaying from query parameter `recording`', src); 956 | } 957 | self.loadRecordingFromUrl(src, false, bind(self.startReplaying, self)); 958 | return; 959 | } 960 | 961 | // 3. Use `data.recordingName` as recording name from IndexedDB. 962 | if (recordingNames.indexOf(self.data.recordingName) !== -1) { 963 | log('Replaying `' + self.data.recordingName + '` from IndexedDB.'); 964 | recordingdb.getRecording(self.data.recordingName).then(bind(self.startReplaying, self)); 965 | } 966 | }); 967 | }, 968 | 969 | /** 970 | * Defined for test stubbing. 971 | */ 972 | getSrcFromSearchParam: function () { 973 | var search = new URLSearchParams(window.location.search); 974 | return search.get('recording') || search.get('avatar-recording'); 975 | }, 976 | 977 | /** 978 | * Set player on camera and controllers (marked by ID). 979 | * 980 | * @params {object} replayData - { 981 | * camera: {poses: [], events: []}, 982 | * [c1ID]: {poses: [], events: []}, 983 | * [c2ID]: {poses: [], events: []} 984 | * } 985 | */ 986 | startReplaying: function (replayData) { 987 | var data = this.data; 988 | var self = this; 989 | var sceneEl = this.el; 990 | 991 | if (this.isReplaying) { return; } 992 | 993 | // Wait for camera. 994 | if (!this.el.camera) { 995 | this.el.addEventListener('camera-set-active', function waitForCamera () { 996 | self.startReplaying(replayData); 997 | self.el.removeEventListener('camera-set-active', waitForCamera); 998 | }); 999 | return; 1000 | } 1001 | 1002 | this.replayData = replayData; 1003 | this.isReplaying = true; 1004 | 1005 | this.cameraEl.removeAttribute('motion-capture-replayer'); 1006 | 1007 | Object.keys(replayData).forEach(function setReplayer (key) { 1008 | var replayingEl; 1009 | 1010 | if (key === 'camera') { 1011 | // Grab camera. 1012 | replayingEl = self.cameraEl; 1013 | } else { 1014 | // Grab other entities. 1015 | replayingEl = sceneEl.querySelector('#' + key); 1016 | if (!replayingEl) { 1017 | error('No element found with ID ' + key + '.'); 1018 | return; 1019 | } 1020 | } 1021 | 1022 | log('Setting motion-capture-replayer on ' + key + '.'); 1023 | replayingEl.setAttribute('motion-capture-replayer', {loop: data.loop}); 1024 | replayingEl.components['motion-capture-replayer'].startReplaying(replayData[key]); 1025 | }); 1026 | }, 1027 | 1028 | /** 1029 | * Create head geometry for spectator mode. 1030 | * Always created in case we want to toggle, but only visible during spectator mode. 1031 | */ 1032 | configureHeadGeometry: function () { 1033 | var cameraEl = this.cameraEl; 1034 | var headMesh; 1035 | var leftEyeMesh; 1036 | var rightEyeMesh; 1037 | var leftEyeBallMesh; 1038 | var rightEyeBallMesh; 1039 | 1040 | if (cameraEl.getObject3D('mesh') || cameraEl.getObject3D('replayerMesh')) { return; } 1041 | 1042 | // Head. 1043 | headMesh = new THREE.Mesh(); 1044 | headMesh.geometry = new THREE.BoxBufferGeometry(0.3, 0.3, 0.2); 1045 | headMesh.material = new THREE.MeshStandardMaterial({color: 'pink'}); 1046 | headMesh.visible = this.data.spectatorMode; 1047 | 1048 | // Left eye. 1049 | leftEyeMesh = new THREE.Mesh(); 1050 | leftEyeMesh.geometry = new THREE.SphereBufferGeometry(0.05); 1051 | leftEyeMesh.material = new THREE.MeshBasicMaterial({color: 'white'}); 1052 | leftEyeMesh.position.x -= 0.1; 1053 | leftEyeMesh.position.y += 0.1; 1054 | leftEyeMesh.position.z -= 0.1; 1055 | leftEyeBallMesh = new THREE.Mesh(); 1056 | leftEyeBallMesh.geometry = new THREE.SphereBufferGeometry(0.025); 1057 | leftEyeBallMesh.material = new THREE.MeshBasicMaterial({color: 'black'}); 1058 | leftEyeBallMesh.position.z -= 0.04; 1059 | leftEyeMesh.add(leftEyeBallMesh); 1060 | headMesh.add(leftEyeMesh); 1061 | 1062 | // Right eye. 1063 | rightEyeMesh = new THREE.Mesh(); 1064 | rightEyeMesh.geometry = new THREE.SphereBufferGeometry(0.05); 1065 | rightEyeMesh.material = new THREE.MeshBasicMaterial({color: 'white'}); 1066 | rightEyeMesh.position.x += 0.1; 1067 | rightEyeMesh.position.y += 0.1; 1068 | rightEyeMesh.position.z -= 0.1; 1069 | rightEyeBallMesh = new THREE.Mesh(); 1070 | rightEyeBallMesh.geometry = new THREE.SphereBufferGeometry(0.025); 1071 | rightEyeBallMesh.material = new THREE.MeshBasicMaterial({color: 'black'}); 1072 | rightEyeBallMesh.position.z -= 0.04; 1073 | rightEyeMesh.add(rightEyeBallMesh); 1074 | headMesh.add(rightEyeMesh); 1075 | 1076 | cameraEl.setObject3D('replayerMesh', headMesh); 1077 | }, 1078 | 1079 | /** 1080 | * Remove motion-capture-replayer components. 1081 | */ 1082 | stopReplaying: function () { 1083 | var self = this; 1084 | 1085 | if (!this.isReplaying || !this.replayData) { return; } 1086 | 1087 | this.isReplaying = false; 1088 | Object.keys(this.replayData).forEach(function removeReplayer (key) { 1089 | if (key === 'camera') { 1090 | self.cameraEl.removeComponent('motion-capture-replayer'); 1091 | } else { 1092 | el = document.querySelector('#' + key); 1093 | if (!el) { 1094 | warn('No element with id ' + key); 1095 | return; 1096 | } 1097 | el.removeComponent('motion-capture-replayer'); 1098 | } 1099 | }); 1100 | }, 1101 | 1102 | /** 1103 | * XHR for data. 1104 | */ 1105 | loadRecordingFromUrl: function (url, binary, callback) { 1106 | var data; 1107 | var self = this; 1108 | fileLoader.crossOrigin = 'anonymous'; 1109 | if (binary === true) { 1110 | fileLoader.setResponseType('arraybuffer'); 1111 | } 1112 | fileLoader.load(url, function (buffer) { 1113 | if (binary === true) { 1114 | data = self.loadStrokeBinary(buffer); 1115 | } else { 1116 | data = JSON.parse(buffer); 1117 | } 1118 | if (callback) { callback(data); } 1119 | }); 1120 | } 1121 | }); 1122 | 1123 | 1124 | /***/ }), 1125 | /* 6 */ 1126 | /***/ (function(module, exports) { 1127 | 1128 | /* global THREE AFRAME */ 1129 | AFRAME.registerComponent('stroke', { 1130 | schema: { 1131 | enabled: {default: true}, 1132 | color: {default: '#ef2d5e', type: 'color'} 1133 | }, 1134 | 1135 | init: function () { 1136 | var maxPoints = this.maxPoints = 3000; 1137 | var strokeEl; 1138 | this.idx = 0; 1139 | this.numPoints = 0; 1140 | 1141 | // Buffers 1142 | this.vertices = new Float32Array(maxPoints*3*3); 1143 | this.normals = new Float32Array(maxPoints*3*3); 1144 | this.uvs = new Float32Array(maxPoints*2*2); 1145 | 1146 | // Geometries 1147 | this.geometry = new THREE.BufferGeometry(); 1148 | this.geometry.setDrawRange(0, 0); 1149 | this.geometry.addAttribute('position', new THREE.BufferAttribute(this.vertices, 3).setDynamic(true)); 1150 | this.geometry.addAttribute('uv', new THREE.BufferAttribute(this.uvs, 2).setDynamic(true)); 1151 | this.geometry.addAttribute('normal', new THREE.BufferAttribute(this.normals, 3).setDynamic(true)); 1152 | 1153 | this.material = new THREE.MeshStandardMaterial({ 1154 | color: this.data.color, 1155 | roughness: 0.75, 1156 | metalness: 0.25, 1157 | side: THREE.DoubleSide 1158 | }); 1159 | 1160 | var mesh = new THREE.Mesh(this.geometry, this.material); 1161 | mesh.drawMode = THREE.TriangleStripDrawMode; 1162 | mesh.frustumCulled = false; 1163 | 1164 | // Injects stroke entity 1165 | strokeEl = document.createElement('a-entity'); 1166 | strokeEl.setObject3D('stroke', mesh); 1167 | this.el.sceneEl.appendChild(strokeEl); 1168 | }, 1169 | 1170 | update: function() { 1171 | this.material.color.set(this.data.color); 1172 | }, 1173 | 1174 | drawPoint: (function () { 1175 | var direction = new THREE.Vector3(); 1176 | var positionA = new THREE.Vector3(); 1177 | var positionB = new THREE.Vector3(); 1178 | return function (position, orientation, timestamp, pointerPosition) { 1179 | var uv = 0; 1180 | var numPoints = this.numPoints; 1181 | var brushSize = 0.01; 1182 | if (numPoints === this.maxPoints) { return; } 1183 | for (i = 0; i < numPoints; i++) { 1184 | this.uvs[uv++] = i / (numPoints - 1); 1185 | this.uvs[uv++] = 0; 1186 | 1187 | this.uvs[uv++] = i / (numPoints - 1); 1188 | this.uvs[uv++] = 1; 1189 | } 1190 | 1191 | direction.set(1, 0, 0); 1192 | direction.applyQuaternion(orientation); 1193 | direction.normalize(); 1194 | 1195 | positionA.copy(pointerPosition); 1196 | positionB.copy(pointerPosition); 1197 | positionA.add(direction.clone().multiplyScalar(brushSize / 2)); 1198 | positionB.add(direction.clone().multiplyScalar(-brushSize / 2)); 1199 | 1200 | this.vertices[this.idx++] = positionA.x; 1201 | this.vertices[this.idx++] = positionA.y; 1202 | this.vertices[this.idx++] = positionA.z; 1203 | 1204 | this.vertices[this.idx++] = positionB.x; 1205 | this.vertices[this.idx++] = positionB.y; 1206 | this.vertices[this.idx++] = positionB.z; 1207 | 1208 | this.computeVertexNormals(); 1209 | this.geometry.attributes.normal.needsUpdate = true; 1210 | this.geometry.attributes.position.needsUpdate = true; 1211 | this.geometry.attributes.uv.needsUpdate = true; 1212 | 1213 | this.geometry.setDrawRange(0, numPoints * 2); 1214 | this.numPoints += 1; 1215 | return true; 1216 | } 1217 | })(), 1218 | 1219 | reset: function () { 1220 | var idx = 0; 1221 | var vertices = this.vertices; 1222 | for (i = 0; i < this.numPoints; i++) { 1223 | vertices[idx++] = 0; 1224 | vertices[idx++] = 0; 1225 | vertices[idx++] = 0; 1226 | 1227 | vertices[idx++] = 0; 1228 | vertices[idx++] = 0; 1229 | vertices[idx++] = 0; 1230 | } 1231 | this.geometry.setDrawRange(0, 0); 1232 | this.idx = 0; 1233 | this.numPoints = 0; 1234 | }, 1235 | 1236 | computeVertexNormals: function () { 1237 | var pA = new THREE.Vector3(); 1238 | var pB = new THREE.Vector3(); 1239 | var pC = new THREE.Vector3(); 1240 | var cb = new THREE.Vector3(); 1241 | var ab = new THREE.Vector3(); 1242 | 1243 | for (var i = 0, il = this.idx; i < il; i++) { 1244 | this.normals[ i ] = 0; 1245 | } 1246 | 1247 | var pair = true; 1248 | for (i = 0, il = this.idx; i < il; i += 3) { 1249 | if (pair) { 1250 | pA.fromArray(this.vertices, i); 1251 | pB.fromArray(this.vertices, i + 3); 1252 | pC.fromArray(this.vertices, i + 6); 1253 | } else { 1254 | pA.fromArray(this.vertices, i + 3); 1255 | pB.fromArray(this.vertices, i); 1256 | pC.fromArray(this.vertices, i + 6); 1257 | } 1258 | pair = !pair; 1259 | 1260 | cb.subVectors(pC, pB); 1261 | ab.subVectors(pA, pB); 1262 | cb.cross(ab); 1263 | cb.normalize(); 1264 | 1265 | this.normals[i] += cb.x; 1266 | this.normals[i + 1] += cb.y; 1267 | this.normals[i + 2] += cb.z; 1268 | 1269 | this.normals[i + 3] += cb.x; 1270 | this.normals[i + 4] += cb.y; 1271 | this.normals[i + 5] += cb.z; 1272 | 1273 | this.normals[i + 6] += cb.x; 1274 | this.normals[i + 7] += cb.y; 1275 | this.normals[i + 8] += cb.z; 1276 | } 1277 | 1278 | /* 1279 | first and last vertice (0 and 8) belongs just to one triangle 1280 | second and penultimate (1 and 7) belongs to two triangles 1281 | the rest of the vertices belongs to three triangles 1282 | 1283 | 1_____3_____5_____7 1284 | /\ /\ /\ /\ 1285 | / \ / \ / \ / \ 1286 | /____\/____\/____\/____\ 1287 | 0 2 4 6 8 1288 | */ 1289 | 1290 | // Vertices that are shared across three triangles 1291 | for (i = 2 * 3, il = this.idx - 2 * 3; i < il; i++) { 1292 | this.normals[ i ] = this.normals[ i ] / 3; 1293 | } 1294 | 1295 | // Second and penultimate triangle, that shares just two triangles 1296 | this.normals[ 3 ] = this.normals[ 3 ] / 2; 1297 | this.normals[ 3 + 1 ] = this.normals[ 3 + 1 ] / 2; 1298 | this.normals[ 3 + 2 ] = this.normals[ 3 * 1 + 2 ] / 2; 1299 | 1300 | this.normals[ this.idx - 2 * 3 ] = this.normals[ this.idx - 2 * 3 ] / 2; 1301 | this.normals[ this.idx - 2 * 3 + 1 ] = this.normals[ this.idx - 2 * 3 + 1 ] / 2; 1302 | this.normals[ this.idx - 2 * 3 + 2 ] = this.normals[ this.idx - 2 * 3 + 2 ] / 2; 1303 | 1304 | this.geometry.normalizeNormals(); 1305 | } 1306 | }); 1307 | 1308 | 1309 | /***/ }), 1310 | /* 7 */ 1311 | /***/ (function(module, exports) { 1312 | 1313 | AFRAME.registerSystem('motion-capture-replayer', { 1314 | init: function () { 1315 | var sceneEl = this.sceneEl; 1316 | var trackedControlsComponent; 1317 | var trackedControlsSystem; 1318 | var trackedControlsTick; 1319 | 1320 | trackedControlsSystem = sceneEl.systems['tracked-controls']; 1321 | trackedControlsTick = AFRAME.components['tracked-controls'].Component.prototype.tick; 1322 | 1323 | // Gamepad data stored in recording and added here by `motion-capture-replayer` component. 1324 | this.gamepads = []; 1325 | 1326 | // Wrap `updateControllerList`. 1327 | this.updateControllerListOriginal = trackedControlsSystem.updateControllerList.bind( 1328 | trackedControlsSystem); 1329 | trackedControlsSystem.updateControllerList = this.updateControllerList.bind(this); 1330 | 1331 | // Wrap `tracked-controls` tick. 1332 | trackedControlsComponent = AFRAME.components['tracked-controls'].Component.prototype; 1333 | trackedControlsComponent.tick = this.trackedControlsTickWrapper; 1334 | trackedControlsComponent.trackedControlsTick = trackedControlsTick; 1335 | }, 1336 | 1337 | trackedControlsTickWrapper: function (time, delta) { 1338 | if (this.el.components['motion-capture-replayer']) { return; } 1339 | this.trackedControlsTick(time, delta); 1340 | }, 1341 | 1342 | /** 1343 | * Wrap `updateControllerList` to stub in the gamepads and emit `controllersupdated`. 1344 | */ 1345 | updateControllerList: function () { 1346 | var i; 1347 | var sceneEl = this.sceneEl; 1348 | var trackedControlsSystem = sceneEl.systems['tracked-controls']; 1349 | 1350 | this.updateControllerListOriginal(); 1351 | 1352 | this.gamepads.forEach(function (gamepad) { 1353 | if (trackedControlsSystem.controllers[gamepad.index]) { return; } 1354 | trackedControlsSystem.controllers[gamepad.index] = gamepad; 1355 | }); 1356 | 1357 | for (i = 0; i < trackedControlsSystem.controllers.length; i++) { 1358 | if (trackedControlsSystem.controllers[i]) { continue; } 1359 | trackedControlsSystem.controllers[i] = {id: '___', index: -1, hand: 'finger'}; 1360 | } 1361 | 1362 | sceneEl.emit('controllersupdated', undefined, false); 1363 | } 1364 | }); 1365 | 1366 | 1367 | /***/ }), 1368 | /* 8 */ 1369 | /***/ (function(module, exports, __webpack_require__) { 1370 | 1371 | /* global indexedDB */ 1372 | var constants = __webpack_require__(4); 1373 | 1374 | var DB_NAME = 'motionCaptureRecordings'; 1375 | var OBJECT_STORE_NAME = 'recordings'; 1376 | var VERSION = 1; 1377 | 1378 | /** 1379 | * Interface for storing and accessing recordings from Indexed DB. 1380 | */ 1381 | AFRAME.registerSystem('recordingdb', { 1382 | init: function () { 1383 | var request; 1384 | var self = this; 1385 | 1386 | this.db = null; 1387 | this.hasLoaded = false; 1388 | 1389 | request = indexedDB.open(DB_NAME, VERSION); 1390 | 1391 | request.onerror = function () { 1392 | console.error('Error opening IndexedDB for motion capture.', request.error); 1393 | }; 1394 | 1395 | // Initialize database. 1396 | request.onupgradeneeded = function (evt) { 1397 | var db = self.db = evt.target.result; 1398 | var objectStore; 1399 | 1400 | // Create object store. 1401 | objectStore = db.createObjectStore('recordings', { 1402 | autoIncrement: false 1403 | }); 1404 | objectStore.createIndex('recordingName', 'recordingName', {unique: true}); 1405 | self.objectStore = objectStore; 1406 | }; 1407 | 1408 | // Got database. 1409 | request.onsuccess = function (evt) { 1410 | self.db = evt.target.result; 1411 | self.hasLoaded = true; 1412 | self.sceneEl.emit('recordingdbinitialized'); 1413 | }; 1414 | }, 1415 | 1416 | /** 1417 | * Need a new transaction for everything. 1418 | */ 1419 | getTransaction: function () { 1420 | var transaction = this.db.transaction([OBJECT_STORE_NAME], 'readwrite'); 1421 | return transaction.objectStore(OBJECT_STORE_NAME); 1422 | }, 1423 | 1424 | getRecordingNames: function () { 1425 | var self = this; 1426 | return new Promise(function (resolve) { 1427 | var recordingNames = []; 1428 | 1429 | self.waitForDb(function () { 1430 | self.getTransaction().openCursor().onsuccess = function (evt) { 1431 | var cursor = evt.target.result; 1432 | 1433 | // No recordings. 1434 | if (!cursor) { 1435 | resolve(recordingNames.sort()); 1436 | return; 1437 | } 1438 | 1439 | recordingNames.push(cursor.key); 1440 | cursor.continue(); 1441 | }; 1442 | }); 1443 | }); 1444 | }, 1445 | 1446 | getRecordings: function (cb) { 1447 | var self = this; 1448 | return new Promise(function getRecordings (resolve) { 1449 | self.waitForDb(function () { 1450 | self.getTransaction().openCursor().onsuccess = function (evt) { 1451 | var cursor = evt.target.result; 1452 | var recordings = [cursor.value]; 1453 | while (cursor.ontinue()) { 1454 | recordings.push(cursor.value); 1455 | } 1456 | resolve(recordings); 1457 | }; 1458 | }); 1459 | }); 1460 | }, 1461 | 1462 | getRecording: function (name) { 1463 | var self = this; 1464 | return new Promise(function getRecording (resolve) { 1465 | self.waitForDb(function () { 1466 | self.getTransaction().get(name).onsuccess = function (evt) { 1467 | resolve(evt.target.result); 1468 | }; 1469 | }); 1470 | }); 1471 | }, 1472 | 1473 | addRecording: function (name, data) { 1474 | this.getTransaction().add(data, name); 1475 | }, 1476 | 1477 | deleteRecording: function (name) { 1478 | this.getTransaction().delete(name); 1479 | }, 1480 | 1481 | /** 1482 | * Helper to wait for store to be initialized before using it. 1483 | */ 1484 | waitForDb: function (cb) { 1485 | if (this.hasLoaded) { 1486 | cb(); 1487 | return; 1488 | } 1489 | this.sceneEl.addEventListener('recordingdbinitialized', cb); 1490 | } 1491 | }); 1492 | 1493 | 1494 | /***/ }) 1495 | /******/ ]); -------------------------------------------------------------------------------- /dist/aframe-motion-capture-components.min.js: -------------------------------------------------------------------------------- 1 | !function(t){function e(i){if(r[i])return r[i].exports;var o=r[i]={exports:{},id:i,loaded:!1};return t[i].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");r(4),r(5),r(2),r(3),r(6),r(7),r(8)},function(t,e){t.exports.LOCALSTORAGE_RECORDINGS="avatarRecordings",t.exports.DEFAULT_RECORDING_NAME="default"},function(t,e,r){var i=r(1),o=AFRAME.utils.debug("aframe-motion-capture:avatar-recorder:info"),a=AFRAME.utils.debug("aframe-motion-capture:avatar-recorder:warn");AFRAME.registerComponent("avatar-recorder",{schema:{autoPlay:{default:!1},autoRecord:{default:!1},cameraOverride:{type:"selector"},localStorage:{default:!0},recordingName:{default:i.DEFAULT_RECORDING_NAME},loop:{default:!0}},init:function(){this.cameraEl=null,this.isRecording=!1,this.trackedControllerEls={},this.recordingData=null,this.onKeyDown=AFRAME.utils.bind(this.onKeyDown,this),this.tick=AFRAME.utils.throttle(this.throttledTick,100,this)},throttledTick:function(){var t=this,e=this.el.querySelectorAll("[tracked-controls]");this.trackedControllerEls={},e.forEach(function(e){return e.id?(e.setAttribute("motion-capture-recorder",{autoRecord:!1,visibleStroke:!1}),t.trackedControllerEls[e.id]=e,void(t.isRecording&&e.components["motion-capture-recorder"].startRecording())):void a("Found a tracked controller entity without an ID. Provide an ID or this controller will not be recorded")})},play:function(){window.addEventListener("keydown",this.onKeyDown)},pause:function(){window.removeEventListener("keydown",this.onKeyDown)},onKeyDown:function(t){var e=t.keyCode,r={space:32};switch(e){case r.space:this.toggleRecording()}},toggleRecording:function(){this.isRecording?this.stopRecording():this.startRecording()},setupCamera:function(t){function e(e){i.cameraEl&&i.cameraEl.removeAttribute("motion-capture-recorder"),i.cameraEl=e,e.setAttribute("motion-capture-recorder",{autoRecord:!1,visibleStroke:!1}),t(e)}var r=this.el,i=this;return this.data.cameraOverride?void e(this.data.cameraOverride):r.camera&&r.camera.el?void e(r.camera.el):void r.addEventListener("camera-set-active",function t(i){e(i.detail.cameraEl),r.removeEventListener("camera-set-active",t)})},startRecording:function(){var t=this.trackedControllerEls,e=this;this.isRecording||(o("Starting recording!"),this.el.components["avatar-replayer"]&&this.el.components["avatar-replayer"].stopReplaying(),this.setupCamera(function(){e.isRecording=!0,e.cameraEl.components["motion-capture-recorder"].startRecording(),Object.keys(t).forEach(function(e){t[e].components["motion-capture-recorder"].startRecording()})}))},stopRecording:function(){var t=this.trackedControllerEls;this.isRecording&&(o("Stopped recording."),this.isRecording=!1,this.cameraEl.components["motion-capture-recorder"].stopRecording(),Object.keys(t).forEach(function(e){t[e].components["motion-capture-recorder"].stopRecording()}),this.recordingData=this.getJSONData(),this.storeRecording(this.recordingData),this.data.autoPlay&&this.replayRecording())},getJSONData:function(){var t={},e=this.trackedControllerEls;if(!this.isRecording)return t.camera=this.cameraEl.components["motion-capture-recorder"].getJSONData(),Object.keys(e).forEach(function(r){t[r]=e[r].components["motion-capture-recorder"].getJSONData()}),t},storeRecording:function(t){var e=this.data;e.localStorage&&(o("Recording stored in localStorage."),this.el.systems.recordingdb.addRecording(e.recordingName,t))}})},function(t,e,r){var i=r(1),o=AFRAME.utils.bind,a=AFRAME.utils.debug("aframe-motion-capture:avatar-replayer:error"),n=AFRAME.utils.debug("aframe-motion-capture:avatar-replayer:info"),s=AFRAME.utils.debug("aframe-motion-capture:avatar-replayer:warn"),c=new THREE.FileLoader;AFRAME.registerComponent("avatar-replayer",{schema:{autoPlay:{default:!0},cameraOverride:{type:"selector"},loop:{default:!1},recordingName:{default:i.DEFAULT_RECORDING_NAME},spectatorMode:{default:!1},spectatorPosition:{default:{x:0,y:1.6,z:2},type:"vec3"},src:{default:""}},init:function(){var t=this.el;this.onKeyDown=o(this.onKeyDown,this),this.setupCamera=o(this.setupCamera,this),t.camera?this.setupCamera():t.addEventListener("camera-set-active",this.setupCamera),this.data.autoPlay&&this.replayRecordingFromSource()},update:function(t){var e,r=this.data;e=window.location.search.indexOf("spectatormode")!==-1||window.location.search.indexOf("spectatorMode")!==-1,(t.spectatorMode!==r.spectatorMode||e)&&(r.spectatorMode||e?this.activateSpectatorCamera():t.spectatorMode===!0&&this.deactivateSpectatorCamera()),r.src&&t.src!==r.src&&r.autoPlay&&this.replayRecordingFromSource()},play:function(){window.addEventListener("keydown",this.onKeyDown)},pause:function(){window.removeEventListener("keydown",this.onKeyDown)},remove:function(){this.stopReplaying(),this.cameraEl.removeObject3D("replayerMesh")},setupCamera:function(){var t=this.data,e=this.el;t.cameraOverride?this.cameraEl=t.cameraOverride:(this.cameraEl=e.camera.el,this.cameraEl.removeAttribute("data-aframe-default-camera")),this.cameraEl.setAttribute("data-aframe-avatar-replayer-camera",""),e.removeEventListener("camera-set-active",this.setupCamera),this.configureHeadGeometry(),this.initSpectatorCamera()},onKeyDown:function(t){switch(t.keyCode){case 81:this.el.setAttribute("avatar-replayer","spectatorMode",!this.data.spectatorMode)}},activateSpectatorCamera:function(){var t=this.spectatorCameraEl;return t?t.hasLoaded?(n("Activating spectator camera"),t.setAttribute("camera","active",!0),void(this.cameraEl.getObject3D("replayerMesh").visible=!0)):void t.addEventListener("loaded",o(this.activateSpectatorCamera,this)):void this.el.addEventListener("spectatorcameracreated",o(this.activateSpectatorCamera,this))},deactivateSpectatorCamera:function(){n("Deactivating spectator camera"),this.cameraEl.setAttribute("camera","active",!0),this.cameraEl.getObject3D("replayerMesh").visible=!1},initSpectatorCamera:function(){var t,e,r=this.data,i=this.el;return this.el.querySelector("#spectatorCameraRig")?void(this.spectatorCameraEl=i.querySelector("#spectatorCameraRig")):(e=i.querySelector("#spectatorCameraRig")||document.createElement("a-entity"),e.id="spectatorCameraRig",e.setAttribute("position",r.spectatorPosition),this.spectatorCameraRigEl=e,t=i.querySelector("#spectatorCamera")||document.createElement("a-entity"),t.id="spectatorCamera",t.setAttribute("camera",{active:r.spectatorMode,userHeight:0}),t.setAttribute("look-controls",""),t.setAttribute("wasd-controls",{fly:!0}),this.spectatorCameraEl=t,e.appendChild(t),i.appendChild(e),void i.emit("spectatorcameracreated"))},replayRecordingFromSource:function(){var t,e=(this.data,this.el.systems.recordingdb),r=this;null===new URLSearchParams(window.location.search).get("avatar-replayer-disabled")&&e.getRecordingNames().then(function(i){var a=r.getSrcFromSearchParam();return i.indexOf(a)!==-1?(n("Replaying `"+a+"` from IndexedDB."),void e.getRecording(a).then(o(r.startReplaying,r))):(t=a||r.data.src)?(r.data.src?n("Replaying from component `src`",t):a&&n("Replaying from query parameter `recording`",t),void r.loadRecordingFromUrl(t,!1,o(r.startReplaying,r))):void(i.indexOf(r.data.recordingName)!==-1&&(n("Replaying `"+r.data.recordingName+"` from IndexedDB."),e.getRecording(r.data.recordingName).then(o(r.startReplaying,r))))})},getSrcFromSearchParam:function(){var t=new URLSearchParams(window.location.search);return t.get("recording")||t.get("avatar-recording")},startReplaying:function(t){var e=this.data,r=this,i=this.el;if(!this.isReplaying){if(!this.el.camera)return void this.el.addEventListener("camera-set-active",function e(){r.startReplaying(t),r.el.removeEventListener("camera-set-active",e)});this.replayData=t,this.isReplaying=!0,this.cameraEl.removeAttribute("motion-capture-replayer"),Object.keys(t).forEach(function(o){var s;if("camera"===o)s=r.cameraEl;else if(s=i.querySelector("#"+o),!s)return void a("No element found with ID "+o+".");n("Setting motion-capture-replayer on "+o+"."),s.setAttribute("motion-capture-replayer",{loop:e.loop}),s.components["motion-capture-replayer"].startReplaying(t[o])})}},configureHeadGeometry:function(){var t,e,r,i,o,a=this.cameraEl;a.getObject3D("mesh")||a.getObject3D("replayerMesh")||(t=new THREE.Mesh,t.geometry=new THREE.BoxBufferGeometry(.3,.3,.2),t.material=new THREE.MeshStandardMaterial({color:"pink"}),t.visible=this.data.spectatorMode,e=new THREE.Mesh,e.geometry=new THREE.SphereBufferGeometry(.05),e.material=new THREE.MeshBasicMaterial({color:"white"}),e.position.x-=.1,e.position.y+=.1,e.position.z-=.1,i=new THREE.Mesh,i.geometry=new THREE.SphereBufferGeometry(.025),i.material=new THREE.MeshBasicMaterial({color:"black"}),i.position.z-=.04,e.add(i),t.add(e),r=new THREE.Mesh,r.geometry=new THREE.SphereBufferGeometry(.05),r.material=new THREE.MeshBasicMaterial({color:"white"}),r.position.x+=.1,r.position.y+=.1,r.position.z-=.1,o=new THREE.Mesh,o.geometry=new THREE.SphereBufferGeometry(.025),o.material=new THREE.MeshBasicMaterial({color:"black"}),o.position.z-=.04,r.add(o),t.add(r),a.setObject3D("replayerMesh",t))},stopReplaying:function(){var t=this;this.isReplaying&&this.replayData&&(this.isReplaying=!1,Object.keys(this.replayData).forEach(function(e){if("camera"===e)t.cameraEl.removeComponent("motion-capture-replayer");else{if(el=document.querySelector("#"+e),!el)return void s("No element with id "+e);el.removeComponent("motion-capture-replayer")}}))},loadRecordingFromUrl:function(t,e,r){var i,o=this;c.crossOrigin="anonymous",e===!0&&c.setResponseType("arraybuffer"),c.load(t,function(t){i=e===!0?o.loadStrokeBinary(t):JSON.parse(t),r&&r(i)})}})},function(t,e){var r={axismove:{id:0,props:["id","axis","changed"]},buttonchanged:{id:1,props:["id","state"]},buttondown:{id:2,props:["id","state"]},buttonup:{id:3,props:["id","state"]},touchstart:{id:4,props:["id","state"]},touchend:{id:5,props:["id","state"]}};AFRAME.registerComponent("motion-capture-recorder",{schema:{autoRecord:{default:!1},enabled:{default:!0},hand:{default:"right"},recordingControls:{default:!1},persistStroke:{default:!1},visibleStroke:{default:!0}},init:function(){this.drawing=!1,this.recordedEvents=[],this.recordedPoses=[],this.addEventListeners()},addEventListeners:function(){var t=this.el;this.recordEvent=this.recordEvent.bind(this),t.addEventListener("axismove",this.recordEvent),t.addEventListener("buttonchanged",this.onTriggerChanged.bind(this)),t.addEventListener("buttonchanged",this.recordEvent),t.addEventListener("buttonup",this.recordEvent),t.addEventListener("buttondown",this.recordEvent),t.addEventListener("touchstart",this.recordEvent),t.addEventListener("touchend",this.recordEvent)},recordEvent:function(t){var e;this.isRecording&&("detail"in t&&"state"in t.detail&&"object"==typeof t.detail.state&&"target"in t.detail.state&&delete t.detail.state.target,e={},r[t.type].props.forEach(function(r){if("state"!==r)e[r]=t.detail[r];else{var i;e.state={};for(i in t.detail.state)e.state[i]=t.detail.state[i]}}),this.recordedEvents.push({name:t.type,detail:e,timestamp:this.lastTimestamp}))},onTriggerChanged:function(t){var e,r=this.data;if(r.enabled&&!r.autoRecord&&1===t.detail.id&&this.data.recordingControls)return e=t.detail.state.value,e<=.1?void(this.isRecording&&this.stopRecording()):void(this.isRecording||this.startRecording())},getJSONData:function(){var t,e=this.el.components["tracked-controls"],r=e&&e.controller;if(this.recordedPoses)return t={poses:this.getStrokeJSON(this.recordedPoses),events:this.recordedEvents},r&&(t.gamepad={id:r.id,hand:r.hand,index:r.index}),t},getStrokeJSON:function(t){for(var e,r=[],i=0;i=e.timestamp||i&&this.currentPoseTime>=i.timestamp;)e&&this.currentPoseTime>=e.timestamp&&(this.currentPoseIndex===o.length-1&&(this.data.loop?(this.currentPoseIndex=0,this.currentPoseTime=o[0].timestamp):this.stopReplaying()),r(this.el,e),this.currentPoseIndex+=1,e=o[this.currentPoseIndex]),i&&this.currentPoseTime>=i.timestamp&&(this.currentEventIndex===a.length&&this.data.loop&&(this.currentEventIndex=0,this.currentEventTime=a[0].timestamp),this.el.emit(i.name,i.detail),this.currentEventIndex+=1,i=this.playingEvents[this.currentEventIndex])},tick:function(t,e){return 2===this.ignoredFrames||window.debug?void(this.isReplaying&&this.playRecording(e)):void this.ignoredFrames++}})},function(t,e){AFRAME.registerComponent("stroke",{schema:{enabled:{default:!0},color:{default:"#ef2d5e",type:"color"}},init:function(){var t,e=this.maxPoints=3e3;this.idx=0,this.numPoints=0,this.vertices=new Float32Array(3*e*3),this.normals=new Float32Array(3*e*3),this.uvs=new Float32Array(2*e*2),this.geometry=new THREE.BufferGeometry,this.geometry.setDrawRange(0,0),this.geometry.addAttribute("position",new THREE.BufferAttribute(this.vertices,3).setDynamic(!0)),this.geometry.addAttribute("uv",new THREE.BufferAttribute(this.uvs,2).setDynamic(!0)),this.geometry.addAttribute("normal",new THREE.BufferAttribute(this.normals,3).setDynamic(!0)),this.material=new THREE.MeshStandardMaterial({color:this.data.color,roughness:.75,metalness:.25,side:THREE.DoubleSide});var r=new THREE.Mesh(this.geometry,this.material);r.drawMode=THREE.TriangleStripDrawMode,r.frustumCulled=!1,t=document.createElement("a-entity"),t.setObject3D("stroke",r),this.el.sceneEl.appendChild(t)},update:function(){this.material.color.set(this.data.color)},drawPoint:function(){var t=new THREE.Vector3,e=new THREE.Vector3,r=new THREE.Vector3;return function(o,a,n,s){var c=0,d=this.numPoints,l=.01;if(d!==this.maxPoints){for(i=0;i 2 | 3 | A-Frame Motion Capture Components Animation 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/assets/ghost.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'ghost.blend' 2 | # Material Count: 3 3 | 4 | newmtl eyesmat 5 | Ns 121.568627 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.006370 0.006370 0.006370 8 | Ks 1.000000 1.000000 1.000000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | 14 | newmtl ghostmat 15 | Ns 96.078431 16 | Ka 1.000000 1.000000 1.000000 17 | Kd 1.000000 0.034340 0.138432 18 | Ks 0.500000 0.500000 0.500000 19 | Ke 0.000000 0.000000 0.000000 20 | Ni 1.000000 21 | d 1.000000 22 | illum 2 23 | 24 | newmtl whiteeyesmat 25 | Ns 96.078431 26 | Ka 1.000000 1.000000 1.000000 27 | Kd 0.800000 0.800000 0.800000 28 | Ks 0.000000 0.000000 0.000000 29 | Ke 0.000000 0.000000 0.000000 30 | Ni 1.000000 31 | d 1.000000 32 | illum 1 33 | -------------------------------------------------------------------------------- /examples/assets/pacman.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'pacman.blend' 2 | # Material Count: 2 3 | 4 | newmtl eyesmat 5 | Ns 121.568627 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.006370 0.006370 0.006370 8 | Ks 1.000000 1.000000 1.000000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.000000 11 | d 1.000000 12 | illum 2 13 | 14 | newmtl pacmanmat 15 | Ns 19.607843 16 | Ka 1.000000 1.000000 1.000000 17 | Kd 0.800000 0.551855 0.000000 18 | Ks 0.281250 0.281250 0.281250 19 | Ke 0.000000 0.000000 0.000000 20 | Ni 1.000000 21 | d 1.000000 22 | illum 2 23 | -------------------------------------------------------------------------------- /examples/development.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Motion Capture Components Development 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 24 | 25 | 26 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/js/build.js: -------------------------------------------------------------------------------- 1 | !function(t){function e(i){if(r[i])return r[i].exports;var o=r[i]={exports:{},id:i,loaded:!1};return t[i].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var r={};return e.m=t,e.c=r,e.p="",e(0)}([function(t,e,r){if("undefined"==typeof AFRAME)throw new Error("Component attempted to register before AFRAME was available.");r(1),r(2),r(3),r(4),r(5),r(6)},function(t,e){var r={axismove:{id:0,props:["id","axis"]},buttonchanged:{id:1,props:["id","state"]},buttondown:{id:2,props:["id","state"]},buttonup:{id:3,props:["id","state"]},touchstart:{id:4,props:["id","state"]},touchend:{id:5,props:["id","state"]}};AFRAME.registerComponent("motion-capture-recorder",{schema:{autoRecord:{default:!1},enabled:{default:!0},hand:{default:"right"},persistStroke:{default:!1},visibleStroke:{default:!0}},init:function(){this.drawing=!1,this.recordedEvents=[],this.recordedPoses=[],this.addEventListeners()},addEventListeners:function(){var t=this.el;this.recordEvent=this.recordEvent.bind(this),t.addEventListener("axismove",this.recordEvent),t.addEventListener("buttonchanged",this.onTriggerChanged.bind(this)),t.addEventListener("buttonchanged",this.recordEvent),t.addEventListener("buttonup",this.recordEvent),t.addEventListener("buttondown",this.recordEvent),t.addEventListener("touchstart",this.recordEvent),t.addEventListener("touchend",this.recordEvent)},recordEvent:function(t){var e;this.isRecording&&(e={},r[t.type].props.forEach(function(r){e[r]=t.detail[r]}),this.recordedEvents.push({name:t.type,detail:e,timestamp:this.lastTimestamp}))},onTriggerChanged:function(t){var e,r=this.data;if(r.enabled&&!r.autoRecord&&1===t.detail.id)return e=t.detail.state.value,e<=.1?void(this.isRecording&&this.stopRecording()):void(this.isRecording||this.startRecording())},getJSONData:function(){if(this.recordedPoses)return{poses:this.system.getStrokeJSON(this.recordedPoses),events:this.recordedEvents}},saveCapture:function(t){var e=JSON.stringify(this.getJSONData()),r=t?"application/octet-binary":"application/json",i=new Blob([e],{type:r}),o=URL.createObjectURL(i),n="motion-capture-"+document.title+"-"+Date.now()+".json",a=document.createElement("a");a.setAttribute("class","motion-capture-download"),a.href=o,a.setAttribute("download",n),a.innerHTML="downloading...",a.style.display="none",document.body.appendChild(a),setTimeout(function(){a.click(),document.body.removeChild(a)},1)},update:function(){var t=this.el,e=this.data;if(this.data.autoRecord)this.startRecording();else{if(t.components.camera)return;t.setAttribute("vive-controls",{hand:e.hand}),t.setAttribute("oculus-touch-controls",{hand:e.hand}),t.setAttribute("stroke",{hand:e.hand})}},tick:function(){var t=new THREE.Vector3,e=new THREE.Quaternion,r=new THREE.Vector3;return function(i,o){var n,a;this.lastTimestamp=i,this.data.enabled&&this.isRecording&&(n={position:this.el.getAttribute("position"),rotation:this.el.getAttribute("rotation"),timestamp:i},this.recordedPoses.push(n),this.data.visibleStroke&&(this.el.object3D.updateMatrixWorld(),this.el.object3D.matrixWorld.decompose(t,e,r),a=this.getPointerPosition(t,e),this.el.components.stroke.drawPoint(t,e,i,a)))}}(),getPointerPosition:function(){var t=new THREE.Vector3,e=new THREE.Vector3(0,.7,1);return function(r,i){var o=e.clone().applyQuaternion(i).normalize().multiplyScalar(-.03);return t.copy(r).add(o),t}}(),startRecording:function(){var t=this.el;this.isRecording||(t.components.stroke&&t.components.stroke.reset(),this.isRecording=!0,this.recordedPoses=[],this.recordedEvents=[],t.emit("strokestarted",{entity:t,poses:this.recordedPoses}))},stopRecording:function(){var t=this.el;this.isRecording&&(t.emit("strokeended",{poses:this.recordedPoses}),this.isRecording=!1,this.data.visibleStroke&&!this.data.persistStroke&&t.components.stroke.reset())}})},function(t,e){function r(t,e){t.setAttribute("position",e.position),t.setAttribute("rotation",e.rotation)}AFRAME.registerComponent("motion-capture-replayer",{schema:{enabled:{default:!0},recorderEl:{type:"selector"},loop:{default:!1},src:{default:""},spectatorCamera:{default:!1}},init:function(){this.currentPoseTime=0,this.currentEventTime=0,this.currentPoseIndex=0,this.currentEventIndex=0,this.onStrokeStarted=this.onStrokeStarted.bind(this),this.onStrokeEnded=this.onStrokeEnded.bind(this),this.el.addEventListener("pause",this.playComponent.bind(this)),this.discardedFrames=0,this.playingEvents=[],this.playingPoses=[]},update:function(t){var e=this.data;this.updateRecorder(e.recorderEl,t.recorderEl),this.el.isPlaying||this.playComponent(),t.src!==e.src&&e.src&&this.updateSrc(e.src)},updateRecorder:function(t,e){e&&e!==t&&(e.removeEventListener("strokestarted",this.onStrokeStarted),e.removeEventListener("strokeended",this.onStrokeEnded)),t&&e!==t&&(t.addEventListener("strokestarted",this.onStrokeStarted),t.addEventListener("strokeended",this.onStrokeEnded))},updateSrc:function(t){this.el.sceneEl.systems["motion-capture-recorder"].loadRecordingFromUrl(t,!1,this.startReplaying.bind(this))},onStrokeStarted:function(t){this.reset()},onStrokeEnded:function(t){this.startReplayingPoses(t.detail.poses)},play:function(){this.playingStroke&&this.playStroke(this.playingStroke)},playComponent:function(){this.el.isPlaying=!0,this.play()},startReplaying:function(t){this.ignoredFrames=0,this.storeInitialPose(),this.isReplaying=!0,this.startReplayingPoses(t.poses),this.startReplayingEvents(t.events),this.el.emit("replayingstarted")},stopReplaying:function(){this.isReplaying=!1,this.restoreInitialPose(),this.el.emit("replayingstopped")},storeInitialPose:function(){var t=this.el;this.initialPose={position:t.getAttribute("position"),rotation:t.getAttribute("rotation")}},restoreInitialPose:function(){var t=this.el;this.initialPose&&(t.setAttribute("position",this.initialPose.position),t.setAttribute("rotation",this.initialPose.rotation))},startReplayingPoses:function(t){this.isReplaying=!0,this.currentPoseIndex=0,0!==t.length&&(this.playingPoses=t,this.currentPoseTime=t[0].timestamp)},startReplayingEvents:function(t){var e;this.isReplaying=!0,this.currentEventIndex=0,0!==t.length&&(e=t[0],this.playingEvents=t,this.currentEventTime=e.timestamp,this.el.emit(e.name,e))},reset:function(){this.playingPoses=null,this.currentTime=void 0,this.currentPoseIndex=void 0},playRecording:function(t){var e,i,o=this.playingPoses,n=this.playingEvents;for(e=o&&o[this.currentPoseIndex],i=n&&n[this.currentEventIndex],this.currentPoseTime+=t,this.currentEventTime+=t;e&&this.currentPoseTime>=e.timestamp||i&&this.currentPoseTime>=i.timestamp;)e&&this.currentPoseTime>=e.timestamp&&(this.currentPoseIndex===o.length-1&&(this.data.loop?(this.currentPoseIndex=0,this.currentPoseTime=o[0].timestamp):this.stopReplaying()),r(this.el,e),this.currentPoseIndex+=1,e=o[this.currentPoseIndex]),i&&this.currentPoseTime>=i.timestamp&&(this.currentEventIndex===n.length&&this.data.loop&&(this.currentEventIndex=0,this.currentEventTime=n[0].timestamp),this.el.emit(i.name,i.detail),this.currentEventIndex+=1,i=this.playingEvents[this.currentEventIndex])},tick:function(t,e){return 2===this.ignoredFrames||window.debug?void(this.isReplaying&&this.playRecording(e)):void this.ignoredFrames++}})},function(t,e){var r=AFRAME.utils.debug("aframe-motion-capture:avatar-recorder:info"),i=AFRAME.utils.debug("aframe-motion-capture:avatar-recorder:warn"),o="avatar-recording";AFRAME.registerComponent("avatar-recorder",{schema:{autoRecord:{default:!1},autoPlay:{default:!0},spectatorPlay:{default:!1},spectatorPosition:{default:"0 1.6 0",type:"vec3"},localStorage:{default:!0},saveFile:{default:!0},loop:{default:!0}},init:function(){function t(t){e.cameraEl&&e.cameraEl.removeAttribute("motion-capture-recorder"),e.cameraEl=t,e.cameraEl.setAttribute("motion-capture-recorder",{autoRecord:!1,visibleStroke:!1})}var e=this,r=this.el;this.trackedControllerEls={},this.onKeyDown=this.onKeyDown.bind(this),this.tick=AFRAME.utils.throttle(this.throttledTick,100,this),r.camera&&r.camera.el&&t(r.camera.el),r.addEventListener("camera-set-active",function(e){t(e.detail.cameraEl)})},replayRecording:function(){var t=this.data,e=this.el,i=JSON.parse(localStorage.getItem(o))||this.recordingData;i&&(r("Replaying recording."),e.setAttribute("avatar-replayer",{loop:t.loop,spectatorMode:t.spectatorPlay,spectatorPosition:t.spectatorPosition}),e.components["avatar-replayer"].startReplaying(i))},stopReplaying:function(){var t=this.el.components["avatar-replayer"];t&&(r("Stopped replaying."),t.stopReplaying(),this.el.setAttribute("avatar-replayer","spectatorMode",!1))},throttledTick:function(){var t=this,e=this.el.querySelectorAll("[tracked-controls]");e.forEach(function(e){return e.id?void(t.trackedControllerEls[e.id]||(e.setAttribute("motion-capture-recorder",{autoRecord:!1,visibleStroke:!1}),t.trackedControllerEls[e.id]=e,this.isRecording&&e.components["motion-capture-recorder"].startRecording())):void i("Found tracked controllers with no id. It will not be recorded")})},play:function(){var t=this;this.data.autoPlay&&setTimeout(function(){t.replayRecording()},500),window.addEventListener("keydown",this.onKeyDown)},pause:function(){window.removeEventListener("keydown",this.onKeyDown)},onKeyDown:function(t){var e=t.keyCode;if(32===e||80===e||67===e)switch(e){case 32:this.toggleRecording();break;case 80:this.toggleReplaying();break;case 67:r("Recording cleared from localStorage."),this.recordingData=null,localStorage.removeItem(o)}},toggleReplaying:function(){var t=this.el.components["avatar-replayer"];t||(this.el.setAttribute("avatar-replayer",""),t=this.el.components["avatar-replayer"]),t.isReplaying?this.stopReplaying():this.replayRecording()},toggleRecording:function(){this.isRecording?this.stopRecording():this.startRecording()},startRecording:function(){var t=this.trackedControllerEls,e=Object.keys(t);this.isRecording||(r("Starting recording!"),this.stopReplaying(),this.isRecording=!0,this.cameraEl.components["motion-capture-recorder"].startRecording(),e.forEach(function(e){t[e].components["motion-capture-recorder"].startRecording()}))},stopRecording:function(){var t=this.trackedControllerEls,e=Object.keys(t);this.isRecording&&(r("Stopped recording."),this.isRecording=!1,this.cameraEl.components["motion-capture-recorder"].stopRecording(),e.forEach(function(e){t[e].components["motion-capture-recorder"].stopRecording()}),this.saveRecording(),this.data.autoPlay&&this.replayRecording())},getJSONData:function(){var t={},e=this.trackedControllerEls,r=Object.keys(e);if(!this.isRecording)return this.isRecording=!1,t.camera=this.cameraEl.components["motion-capture-recorder"].getJSONData(),r.forEach(function(r){t[r]=e[r].components["motion-capture-recorder"].getJSONData()}),this.recordingData=t,t},saveRecording:function(){var t=this.getJSONData();this.data.localStorage&&(r("Recording saved to localStorage."),this.saveToLocalStorage(t)),this.data.saveFile&&(r("Recording saved to file."),this.saveRecordingFile(t))},saveToLocalStorage:function(t){localStorage.setItem(o,JSON.stringify(t))},saveRecordingFile:function(t){var e=JSON.stringify(t),r=this.data.binaryFormat?"application/octet-binary":"application/json",i=new Blob([e],{type:r}),o=URL.createObjectURL(i),n="player-recording-"+document.title+"-"+Date.now()+".json",a=document.createElement("a");a.href=o,a.setAttribute("download",n),a.innerHTML="downloading...",a.style.display="none",document.body.appendChild(a),setTimeout(function(){a.click(),document.body.removeChild(a)},1)}})},function(t,e){var r=AFRAME.utils.debug("aframe-motion-capture:avatar-replayer:error"),i=AFRAME.utils.debug("aframe-motion-capture:avatar-replayer:info"),o=AFRAME.utils.debug("aframe-motion-capture:avatar-replayer:warn");AFRAME.registerComponent("avatar-replayer",{schema:{src:{default:""},loop:{default:!1},spectatorMode:{default:!1},spectatorPosition:{default:"0 1.6 2",type:"vec3"}},init:function(){var t=this.el;this.storeInitialCamera=this.storeInitialCamera.bind(this),this.initSpectatorCamera(),t.camera?this.storeInitialCamera():this.el.addEventListener("camera-set-active",this.storeInitialCamera),this.el.addEventListener("replayingstopped",this.restoreCamera.bind(this)),this.onKeyDown=this.onKeyDown.bind(this)},restoreCamera:function(){this.currentCameraEl.play(),this.currentCameraEl.setAttribute("camera","active",!0)},storeInitialCamera:function(){this.currentCameraEl=this.el.camera.el,this.currentCameraEl.removeAttribute("data-aframe-default-camera"),this.el.appendChild(this.spectatorCameraEl),this.el.removeEventListener("camera-set-active",this.storeInitialCamera)},play:function(){window.addEventListener("keydown",this.onKeyDown)},pause:function(){window.removeEventListener("keydown",this.onKeyDown)},onKeyDown:function(t){var e=t.keyCode;if(9===e)switch(e){case 9:this.toggleSpectatorCamera()}},toggleSpectatorCamera:function(){this.el.setAttribute("avatar-replayer","spectatorMode",!this.data.spectatorMode)},update:function(t){var e=this.data;e.src&&t.src!==e.src&&this.updateSrc(e.src)},initSpectatorCamera:function(){var t=this.spectatorCameraEl=document.createElement("a-entity");t.id="spectatorCamera",t.setAttribute("camera",""),t.setAttribute("look-controls",""),t.setAttribute("wasd-controls","")},updateSrc:function(t){this.loadRecordingFromUrl(t,!1,this.startReplaying.bind(this))},startReplaying:function(t){var e=this.data,o=this,n=this.puppetEl,a=this.el;return this.recordingReplayData=t,this.isReplaying=!0,this.el.camera?(n&&n.removeAttribute("motion-capture-replayer"),Object.keys(t).forEach(function(n){var s;if("camera"===n)i("Setting motion-capture-replayer on camera."),s=o.data.spectatorMode?o.currentCameraEl:a.camera.el;else if(s=a.querySelector("#"+n),!s)return void r("No element found with ID "+n+".");i("Setting motion-capture-replayer on "+n+"."),s.setAttribute("motion-capture-replayer",{loop:e.loop}),s.components["motion-capture-replayer"].startReplaying(t[n]),this.puppetEl=s}),void this.configureCamera()):void this.el.addEventListener("camera-set-active",function(){o.startReplaying(t)})},configureCamera:function(){var t=this.data,e=this.currentCameraEl,r=this.spectatorCameraEl;return r.hasLoaded?(t.spectatorMode?(r.setAttribute("position",t.spectatorPosition),r.setAttribute("camera","active",!0)):e.setAttribute("camera","active",!0),void this.configureHeadGeometry()):void r.addEventListener("loaded",this.configureCamera.bind(this))},configureHeadGeometry:function(){var t=this.currentCameraEl;t.getObject3D("mesh")||this.data.spectatorMode&&(t.setAttribute("geometry",{primitive:"box",height:.3,width:.3,depth:.2}),t.setAttribute("material",{color:"pink"}))},stopReplaying:function(){var t,e=this;this.isReplaying&&this.recordingReplayData&&(this.isReplaying=!1,t=Object.keys(this.recordingReplayData),t.forEach(function(t){"camera"===t?e.el.camera.el.components["motion-capture-replayer"].stopReplaying():(el=document.querySelector("#"+t),el||o("No element with id "+t),el.components["motion-capture-replayer"].stopReplaying())}))},loadRecordingFromUrl:function(t,e,r){var i,o=new THREE.FileLoader(this.manager),n=this;o.crossOrigin="anonymous",e===!0&&o.setResponseType("arraybuffer"),o.load(t,function(t){i=e===!0?n.loadStrokeBinary(t):JSON.parse(t),r&&r(i)})}})},function(t,e){AFRAME.registerComponent("stroke",{schema:{enabled:{default:!0},color:{default:"#ef2d5e",type:"color"}},init:function(){var t,e=this.maxPoints=3e3;this.idx=0,this.numPoints=0,this.vertices=new Float32Array(3*e*3),this.normals=new Float32Array(3*e*3),this.uvs=new Float32Array(2*e*2),this.geometry=new THREE.BufferGeometry,this.geometry.setDrawRange(0,0),this.geometry.addAttribute("position",new THREE.BufferAttribute(this.vertices,3).setDynamic(!0)),this.geometry.addAttribute("uv",new THREE.BufferAttribute(this.uvs,2).setDynamic(!0)),this.geometry.addAttribute("normal",new THREE.BufferAttribute(this.normals,3).setDynamic(!0)),this.material=new THREE.MeshStandardMaterial({color:this.data.color,roughness:.75,metalness:.25,side:THREE.DoubleSide});var r=new THREE.Mesh(this.geometry,this.material);r.drawMode=THREE.TriangleStripDrawMode,r.frustumCulled=!1,t=document.createElement("a-entity"),t.setObject3D("stroke",r),this.el.sceneEl.appendChild(t)},update:function(){this.material.color.set(this.data.color)},drawPoint:function(){var t=new THREE.Vector3,e=new THREE.Vector3,r=new THREE.Vector3;return function(o,n,a,s){var c=0,d=this.numPoints,l=.01;if(d!==this.maxPoints){for(i=0;i= elMin.x) && 85 | (self.elMin.y <= elMax.y && self.elMax.y >= elMin.y) && 86 | (self.elMin.z <= elMax.z && self.elMax.z >= elMin.z); 87 | if (!intersected) { return; } 88 | collisions.push(el); 89 | } 90 | 91 | function handleHit (hitEl) { 92 | hitEl.emit('hit'); 93 | hitEl.addState(self.data.state); 94 | self.el.emit('hit', {el: hitEl}); 95 | } 96 | 97 | function updateBoundingBox () { 98 | boundingBox.setFromObject(mesh); 99 | self.elMin.copy(boundingBox.min); 100 | self.elMax.copy(boundingBox.max); 101 | } 102 | }; 103 | })() 104 | }); 105 | -------------------------------------------------------------------------------- /examples/js/components/grab.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME */ 2 | 3 | /** 4 | * Handles events coming from the hand-controls. 5 | * Determines if the entity is grabbed or released. 6 | * Updates its position to move along the controller. 7 | */ 8 | AFRAME.registerComponent('grab', { 9 | init: function () { 10 | this.GRABBED_STATE = 'grabbed'; 11 | // Bind event handlers 12 | this.onHit = this.onHit.bind(this); 13 | this.onGripOpen = this.onGripOpen.bind(this); 14 | this.onGripClose = this.onGripClose.bind(this); 15 | }, 16 | 17 | play: function () { 18 | var el = this.el; 19 | el.addEventListener('hit', this.onHit); 20 | el.addEventListener('gripclose', this.onGripClose); 21 | el.addEventListener('gripopen', this.onGripOpen); 22 | el.addEventListener('thumbup', this.onGripClose); 23 | el.addEventListener('thumbdown', this.onGripOpen); 24 | el.addEventListener('pointup', this.onGripClose); 25 | el.addEventListener('pointdown', this.onGripOpen); 26 | }, 27 | 28 | pause: function () { 29 | var el = this.el; 30 | el.removeEventListener('hit', this.onHit); 31 | el.removeEventListener('gripclose', this.onGripClose); 32 | el.removeEventListener('gripopen', this.onGripOpen); 33 | el.removeEventListener('thumbup', this.onGripClose); 34 | el.removeEventListener('thumbdown', this.onGripOpen); 35 | el.removeEventListener('pointup', this.onGripClose); 36 | el.removeEventListener('pointdown', this.onGripOpen); 37 | }, 38 | 39 | onGripClose: function (evt) { 40 | this.grabbing = true; 41 | delete this.previousPosition; 42 | }, 43 | 44 | onGripOpen: function (evt) { 45 | var hitEl = this.hitEl; 46 | this.grabbing = false; 47 | if (!hitEl) { return; } 48 | hitEl.removeState(this.GRABBED_STATE); 49 | this.hitEl = undefined; 50 | }, 51 | 52 | onHit: function (evt) { 53 | var hitEl = evt.detail.el; 54 | // If the element is already grabbed (it could be grabbed by another controller). 55 | // If the hand is not grabbing the element does not stick. 56 | // If we're already grabbing something you can't grab again. 57 | if (!hitEl || hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; } 58 | hitEl.addState(this.GRABBED_STATE); 59 | this.hitEl = hitEl; 60 | }, 61 | 62 | tick: function () { 63 | var hitEl = this.hitEl; 64 | var position; 65 | if (!hitEl) { return; } 66 | this.updateDelta(); 67 | position = hitEl.getAttribute('position'); 68 | hitEl.setAttribute('position', { 69 | x: position.x + this.deltaPosition.x, 70 | y: position.y + this.deltaPosition.y, 71 | z: position.z + this.deltaPosition.z 72 | }); 73 | }, 74 | 75 | updateDelta: function () { 76 | var currentPosition = this.el.getAttribute('position'); 77 | var previousPosition = this.previousPosition || currentPosition; 78 | var deltaPosition = { 79 | x: currentPosition.x - previousPosition.x, 80 | y: currentPosition.y - previousPosition.y, 81 | z: currentPosition.z - previousPosition.z 82 | }; 83 | this.previousPosition = currentPosition; 84 | this.deltaPosition = deltaPosition; 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /examples/js/components/ground.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | /** 4 | * Loads and setup ground model. 5 | */ 6 | AFRAME.registerComponent('ground', { 7 | init: function () { 8 | var objectLoader; 9 | var object3D = this.el.object3D; 10 | var MODEL_URL = 'https://cdn.aframe.io/link-traversal/models/ground.json'; 11 | if (this.objectLoader) { return; } 12 | objectLoader = this.objectLoader = new THREE.ObjectLoader(); 13 | objectLoader.crossOrigin = ''; 14 | objectLoader.load(MODEL_URL, function (obj) { 15 | obj.children.forEach(function (value) { 16 | value.receiveShadow = true; 17 | value.material.shading = THREE.FlatShading; 18 | }); 19 | object3D.add(obj); 20 | }); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /examples/js/components/line.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE */ 2 | AFRAME.registerComponent('line', { 3 | schema: { 4 | start: {type: 'vec3', default: '0 0 0'}, 5 | end: {type: 'vec3', default: '0 0 0'} 6 | }, 7 | 8 | init: function () { 9 | var material = this.material = new THREE.LineBasicMaterial({color: 0xfffdb3}); 10 | var geometry = this.geometry = new THREE.Geometry(); 11 | this.line = new THREE.Line(geometry, material); 12 | this.el.setObject3D('line', this.line); 13 | }, 14 | 15 | update: function () { 16 | var vertices = []; 17 | var start = this.data.start; 18 | var end = this.data.end; 19 | var halfX = (start.x + end.x) / 2; 20 | var halfY = (start.y + end.y) / 2; 21 | var halfZ = (start.z + end.z) / 2; 22 | var half = new THREE.Vector3(halfX, halfY, halfZ); 23 | vertices.push(start); 24 | vertices.push(half); 25 | vertices.push(end); 26 | this.geometry.vertices = vertices; 27 | this.geometry.verticesNeedUpdate = true; 28 | } 29 | }); -------------------------------------------------------------------------------- /examples/js/components/ui-raycaster.js: -------------------------------------------------------------------------------- 1 | /* globals AFRAME THREE */ 2 | /** 3 | * Raycaster component. 4 | * 5 | * Pass options to three.js Raycaster including which objects to test. 6 | * Poll for intersections. 7 | * Emit event on origin entity and on target entity on intersect. 8 | * 9 | * @member {array} intersectedEls - List of currently intersected entities. 10 | * @member {array} objects - Cached list of meshes to intersect. 11 | * @member {number} prevCheckTime - Previous time intersection was checked. To help interval. 12 | * @member {object} raycaster - three.js Raycaster. 13 | */ 14 | AFRAME.registerComponent('ui-raycaster', { 15 | schema: { 16 | far: {default: Infinity}, // Infinity. 17 | interval: {default: 100}, 18 | near: {default: 0}, 19 | objects: {default: ''}, 20 | recursive: {default: true}, 21 | rotation: {default: 0} 22 | }, 23 | 24 | init: function () { 25 | this.direction = new THREE.Vector3(); 26 | this.intersectedEls = []; 27 | this.objects = null; 28 | this.prevCheckTime = undefined; 29 | this.raycaster = new THREE.Raycaster(); 30 | this.updateOriginDirection(); 31 | this.refreshObjects = this.refreshObjects.bind(this); 32 | }, 33 | 34 | play: function () { 35 | this.el.sceneEl.addEventListener('child-attached', this.refreshObjects); 36 | this.el.sceneEl.addEventListener('child-detached', this.refreshObjects); 37 | }, 38 | 39 | pause: function () { 40 | this.el.sceneEl.removeEventListener('child-attached', this.refreshObjects); 41 | this.el.sceneEl.removeEventListener('child-detached', this.refreshObjects); 42 | }, 43 | 44 | /** 45 | * Create or update raycaster object. 46 | */ 47 | update: function () { 48 | var data = this.data; 49 | var raycaster = this.raycaster; 50 | 51 | // Set raycaster properties. 52 | raycaster.far = data.far; 53 | raycaster.near = data.near; 54 | 55 | this.refreshObjects(); 56 | }, 57 | 58 | /** 59 | * Update list of objects to test for intersection. 60 | */ 61 | refreshObjects: function () { 62 | var data = this.data; 63 | var i; 64 | var objectEls; 65 | 66 | // Push meshes onto list of objects to intersect. 67 | if (data.objects) { 68 | objectEls = this.el.sceneEl.querySelectorAll(data.objects); 69 | this.objects = []; 70 | for (i = 0; i < objectEls.length; i++) { 71 | this.objects.push(objectEls[i].object3D); 72 | } 73 | return; 74 | } 75 | 76 | // If objects not defined, intersect with everything. 77 | this.objects = this.el.sceneEl.object3D.children; 78 | }, 79 | 80 | /** 81 | * Check for intersections and cleared intersections on an interval. 82 | */ 83 | tick: function (time) { 84 | var el = this.el; 85 | var data = this.data; 86 | var intersectedEls; 87 | var intersections; 88 | var prevCheckTime = this.prevCheckTime; 89 | var prevIntersectedEls; 90 | 91 | // Only check for intersection if interval time has passed. 92 | if (prevCheckTime && (time - prevCheckTime < data.interval)) { return; } 93 | 94 | // Store old previously intersected entities. 95 | prevIntersectedEls = this.intersectedEls.slice(); 96 | 97 | // Raycast. 98 | this.updateOriginDirection(); 99 | intersections = this.raycaster.intersectObjects(this.objects, data.recursive); 100 | 101 | // Only keep intersections against objects that have a reference to an entity. 102 | intersections = intersections.filter(function hasEl (intersection) { 103 | return !!intersection.object.el; 104 | }); 105 | 106 | // Update intersectedEls. 107 | intersectedEls = this.intersectedEls = intersections.map(function getEl (intersection) { 108 | return intersection.object.el; 109 | }); 110 | 111 | // Emit intersected on intersected entity per intersected entity. 112 | intersections.forEach(function emitEvents (intersection) { 113 | var intersectedEl = intersection.object.el; 114 | intersectedEl.addState('hovered'); 115 | intersectedEl.emit('raycaster-intersected', {el: el, intersection: intersection}); 116 | }); 117 | 118 | // Emit all intersections at once on raycasting entity. 119 | if (intersections.length) { 120 | el.emit('raycaster-intersection', { 121 | els: intersectedEls, 122 | intersections: intersections 123 | }); 124 | } 125 | 126 | // Emit intersection cleared on both entities per formerly intersected entity. 127 | prevIntersectedEls.forEach(function checkStillIntersected (intersectedEl) { 128 | if (intersectedEls.indexOf(intersectedEl) !== -1) { return; } 129 | intersectedEl.removeState('hovered'); 130 | el.emit('raycaster-intersection-cleared', {el: intersectedEl}); 131 | intersectedEl.emit('raycaster-intersected-cleared', {el: el}); 132 | }); 133 | }, 134 | 135 | /** 136 | * Set origin and direction of raycaster using entity position and rotation. 137 | */ 138 | updateOriginDirection: (function () { 139 | var directionHelper = new THREE.Quaternion(); 140 | var originVec3 = new THREE.Vector3(); 141 | var scaleDummy = new THREE.Vector3(); 142 | 143 | // Closure to make quaternion/vector3 objects private. 144 | return function updateOriginDirection () { 145 | var el = this.el; 146 | var direction = this.direction; 147 | var object3D = el.object3D; 148 | 149 | // Update matrix world. 150 | object3D.updateMatrixWorld(); 151 | // Grab the position and rotation. 152 | object3D.matrixWorld.decompose(originVec3, directionHelper, scaleDummy); 153 | // Apply rotation to a 0, 0, -1 vector. 154 | direction.set(0, 0, -1); 155 | direction.applyAxisAngle(new THREE.Vector3(1, 0, 0), (this.data.rotation / 360) * 2 * Math.PI); 156 | direction.applyQuaternion(directionHelper); 157 | 158 | this.raycaster.set(originVec3, direction); 159 | }; 160 | })() 161 | }); 162 | -------------------------------------------------------------------------------- /examples/js/shaders/skyGradient.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME */ 2 | AFRAME.registerShader('skyGradient', { 3 | schema: { 4 | colorTop: { type: 'color', default: 'black', is: 'uniform' }, 5 | colorBottom: { type: 'color', default: 'red', is: 'uniform' } 6 | }, 7 | 8 | vertexShader: [ 9 | 'varying vec3 vWorldPosition;', 10 | 11 | 'void main() {', 12 | 13 | 'vec4 worldPosition = modelMatrix * vec4( position, 1.0 );', 14 | 'vWorldPosition = worldPosition.xyz;', 15 | 16 | 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', 17 | 18 | '}' 19 | 20 | ].join('\n'), 21 | 22 | fragmentShader: [ 23 | 'uniform vec3 colorTop;', 24 | 'uniform vec3 colorBottom;', 25 | 26 | 'varying vec3 vWorldPosition;', 27 | 28 | 'void main()', 29 | 30 | '{', 31 | 'vec3 pointOnSphere = normalize(vWorldPosition.xyz);', 32 | 'float f = 1.0;', 33 | 'if(pointOnSphere.y > - 0.2){', 34 | 35 | 'f = sin(pointOnSphere.y * 2.0);', 36 | 37 | '}', 38 | 'gl_FragColor = vec4(mix(colorBottom,colorTop, f ), 1.0);', 39 | 40 | '}' 41 | ].join('\n') 42 | }); 43 | -------------------------------------------------------------------------------- /examples/queryParams.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Motion Capture Components - Query Parameters 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 26 | 27 | 28 | 31 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /examples/record.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Motion Capture Components Record 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 21 | 23 | 24 | 27 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /examples/replay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | A-Frame Motion Capture Components Replay 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 21 | 22 | 23 | 24 | 27 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 | 71 | -------------------------------------------------------------------------------- /examples/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | bottom: 0; 9 | left: 0; 10 | position: fixed; 11 | right: 0; 12 | top: 0; 13 | font-family: sans-serif; 14 | font-size: 22px; 15 | } 16 | 17 | /* Applied to the body element */ 18 | body { 19 | height: 100%; 20 | margin: 0; 21 | overflow: hidden; 22 | padding: 0; 23 | width: 100%; 24 | } 25 | 26 | .container { 27 | width: 100%; 28 | height: 100%; 29 | } 30 | 31 | .sidebar { 32 | color: white; 33 | font-size: 16px; 34 | float: left; 35 | height: 100%; 36 | width: 25%; 37 | background-color: #33425B; 38 | padding: 30px; 39 | line-height: 1.4; 40 | overflow: auto; 41 | } 42 | 43 | .sidebar h1{ 44 | margin-bottom: 20px; 45 | } 46 | 47 | a-scene { 48 | margin-left: 25%; 49 | height: 100%; 50 | width: 75%; 51 | } 52 | 53 | @media screen and (max-width: 1024px) { 54 | html { 55 | font-size: 14px; 56 | } 57 | .sidebar { 58 | padding: 10px; 59 | } 60 | } 61 | 62 | @media screen and (max-width: 600px) { 63 | .sidebar { 64 | position: absolute; 65 | bottom: 0; 66 | float: none; 67 | height: 30%; 68 | width: 100%; 69 | padding: 10px; 70 | } 71 | 72 | a-scene { 73 | margin-left: 0; 74 | height: 70%; 75 | width: 100%; 76 | } 77 | } 78 | 79 | @media screen and (max-width: 320px) { 80 | html { 81 | font-size: 12px; 82 | } 83 | .sidebar { 84 | padding: 10px; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | A-Frame Motion Capture 4 | 5 | 22 | 23 | 24 |

A-Frame Motion Capture

25 | Replay 26 | Development 27 | Record 28 | Animation Tool 29 | Query Parameters 30 | Query Parameters (Grab Left Recording) 31 | Query Parameters (Grab Right Recording) 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-motion-capture-components", 3 | "version": "0.2.7", 4 | "description": "A-Frame motion capture components", 5 | "author": "Diego Marcos ", 6 | "license": "MIT", 7 | "main": "src/index.js", 8 | "scripts": { 9 | "build": "cross-env NODE_ENV=production webpack --config webpack.dev.js", 10 | "start": "webpack-dev-server --host 0.0.0.0 --config webpack.dev.js --progress --colors --hot -d --open --inline", 11 | "dist": "webpack src/index.js dist/aframe-motion-capture-components.js && webpack -p src/index.js dist/aframe-motion-capture-components.min.js", "lint": "semistandard -v | snazzy", 12 | "prepublish": "npm run dist", 13 | "preghpages": "npm run build && shx rm -rf gh-pages && shx mkdir gh-pages && shx cp -r examples/* gh-pages", 14 | "ghpages": "npm run preghpages && gh-pages -d gh-pages", 15 | "test": "karma start ./tests/karma.conf.js", 16 | "test:firefox": "karma start ./tests/karma.conf.js --browsers Firefox", 17 | "test:chrome": "karma start ./tests/karma.conf.js --browsers Chrome" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/dmarcos/aframe-motion-capture-components.git" 22 | }, 23 | "keywords": [ 24 | "aframe", 25 | "a-frame", 26 | "aframe-component", 27 | "aframe-vr", 28 | "vr", 29 | "webgl", 30 | "webvr", 31 | "mozvr" 32 | ], 33 | "bugs": { 34 | "url": "https://github.com/dmarcos/aframe-motion-capture-components/issues" 35 | }, 36 | "homepage": "https://github.com/dmarcos/aframe-motion-capture-components#readme", 37 | "devDependencies": { 38 | "aframe": "^0.8.2", 39 | "chai": "^3.4.1", 40 | "chai-shallow-deep-equal": "^1.3.0", 41 | "cross-env": "^3.1.3", 42 | "gh-pages": "^0.11.0", 43 | "karma": "^0.13.15", 44 | "karma-browserify": "^4.4.2", 45 | "karma-chai-shallow-deep-equal": "0.0.4", 46 | "karma-chrome-launcher": "2.0.0", 47 | "karma-env-preprocessor": "^0.1.1", 48 | "karma-firefox-launcher": "^0.1.7", 49 | "karma-mocha": "^0.2.1", 50 | "karma-mocha-reporter": "^1.1.3", 51 | "karma-sinon-chai": "^1.1.0", 52 | "mocha": "^2.3.4", 53 | "semistandard": "^8.0.0", 54 | "shx": "^0.1.1", 55 | "sinon": "^1.17.5", 56 | "sinon-chai": "^2.8.0", 57 | "snazzy": "^4.0.0", 58 | "webpack": "^1.13.0", 59 | "webpack-dev-server": "^1.16.2" 60 | }, 61 | "semistandard": { 62 | "ignore": [ 63 | "examples/js/build.js", 64 | "dist/**" 65 | ] 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/components/avatar-recorder.js: -------------------------------------------------------------------------------- 1 | /* global THREE, AFRAME */ 2 | var constants = require('../constants'); 3 | var log = AFRAME.utils.debug('aframe-motion-capture:avatar-recorder:info'); 4 | var warn = AFRAME.utils.debug('aframe-motion-capture:avatar-recorder:warn'); 5 | 6 | /** 7 | * Wrapper around individual motion-capture-recorder components for recording camera and 8 | * controllers together. 9 | */ 10 | AFRAME.registerComponent('avatar-recorder', { 11 | schema: { 12 | autoPlay: {default: false}, 13 | autoRecord: {default: false}, 14 | cameraOverride: {type: 'selector'}, 15 | localStorage: {default: true}, 16 | recordingName: {default: constants.DEFAULT_RECORDING_NAME}, 17 | loop: {default: true} 18 | }, 19 | 20 | init: function () { 21 | this.cameraEl = null; 22 | this.isRecording = false; 23 | this.trackedControllerEls = {}; 24 | this.recordingData = null; 25 | 26 | this.onKeyDown = AFRAME.utils.bind(this.onKeyDown, this); 27 | this.tick = AFRAME.utils.throttle(this.throttledTick, 100, this); 28 | }, 29 | 30 | /** 31 | * Poll for tracked controllers. 32 | */ 33 | throttledTick: function () { 34 | var self = this; 35 | var trackedControllerEls = this.el.querySelectorAll('[tracked-controls]'); 36 | this.trackedControllerEls = {}; 37 | trackedControllerEls.forEach(function setupController (trackedControllerEl) { 38 | if (!trackedControllerEl.id) { 39 | warn('Found a tracked controller entity without an ID. ' + 40 | 'Provide an ID or this controller will not be recorded'); 41 | return; 42 | } 43 | trackedControllerEl.setAttribute('motion-capture-recorder', { 44 | autoRecord: false, 45 | visibleStroke: false 46 | }); 47 | self.trackedControllerEls[trackedControllerEl.id] = trackedControllerEl; 48 | if (self.isRecording) { 49 | trackedControllerEl.components['motion-capture-recorder'].startRecording(); 50 | } 51 | }); 52 | }, 53 | 54 | play: function () { 55 | window.addEventListener('keydown', this.onKeyDown); 56 | }, 57 | 58 | pause: function () { 59 | window.removeEventListener('keydown', this.onKeyDown); 60 | }, 61 | 62 | /** 63 | * Keyboard shortcuts. 64 | */ 65 | onKeyDown: function (evt) { 66 | var key = evt.keyCode; 67 | var KEYS = {space: 32}; 68 | switch (key) { 69 | // : Toggle recording. 70 | case KEYS.space: { 71 | this.toggleRecording(); 72 | break; 73 | } 74 | } 75 | }, 76 | 77 | /** 78 | * Start or stop recording. 79 | */ 80 | toggleRecording: function () { 81 | if (this.isRecording) { 82 | this.stopRecording(); 83 | } else { 84 | this.startRecording(); 85 | } 86 | }, 87 | 88 | /** 89 | * Set motion capture recorder on the camera once the camera is ready. 90 | */ 91 | setupCamera: function (doneCb) { 92 | var el = this.el; 93 | var self = this; 94 | 95 | if (this.data.cameraOverride) { 96 | prepareCamera(this.data.cameraOverride); 97 | return; 98 | } 99 | 100 | // Grab camera. 101 | if (el.camera && el.camera.el) { 102 | prepareCamera(el.camera.el); 103 | return; 104 | } 105 | 106 | el.addEventListener('camera-set-active', function setup (evt) { 107 | prepareCamera(evt.detail.cameraEl); 108 | el.removeEventListener('camera-set-active', setup); 109 | }); 110 | 111 | function prepareCamera (cameraEl) { 112 | if (self.cameraEl) { 113 | self.cameraEl.removeAttribute('motion-capture-recorder'); 114 | } 115 | self.cameraEl = cameraEl; 116 | cameraEl.setAttribute('motion-capture-recorder', { 117 | autoRecord: false, 118 | visibleStroke: false 119 | }); 120 | doneCb(cameraEl) 121 | } 122 | }, 123 | 124 | /** 125 | * Start recording camera and tracked controls. 126 | */ 127 | startRecording: function () { 128 | var trackedControllerEls = this.trackedControllerEls; 129 | var self = this; 130 | 131 | if (this.isRecording) { return; } 132 | 133 | log('Starting recording!'); 134 | 135 | if (this.el.components['avatar-replayer']) { 136 | this.el.components['avatar-replayer'].stopReplaying(); 137 | } 138 | 139 | // Get camera. 140 | this.setupCamera(function cameraSetUp () { 141 | self.isRecording = true; 142 | // Record camera. 143 | self.cameraEl.components['motion-capture-recorder'].startRecording(); 144 | // Record tracked controls. 145 | Object.keys(trackedControllerEls).forEach(function startRecordingController (id) { 146 | trackedControllerEls[id].components['motion-capture-recorder'].startRecording(); 147 | }); 148 | }); 149 | }, 150 | 151 | /** 152 | * Tell camera and tracked controls motion-capture-recorder components to stop recording. 153 | * Store recording and replay if autoPlay is on. 154 | */ 155 | stopRecording: function () { 156 | var trackedControllerEls = this.trackedControllerEls; 157 | 158 | if (!this.isRecording) { return; } 159 | 160 | log('Stopped recording.'); 161 | this.isRecording = false; 162 | this.cameraEl.components['motion-capture-recorder'].stopRecording(); 163 | Object.keys(trackedControllerEls).forEach(function (id) { 164 | trackedControllerEls[id].components['motion-capture-recorder'].stopRecording(); 165 | }); 166 | this.recordingData = this.getJSONData(); 167 | this.storeRecording(this.recordingData); 168 | 169 | if (this.data.autoPlay) { 170 | this.replayRecording(); 171 | } 172 | }, 173 | 174 | /** 175 | * Gather the JSON data from the camera and tracked controls motion-capture-recorder 176 | * components. Combine them together, keyed by the (active) `camera` and by the 177 | * tracked controller IDs. 178 | */ 179 | getJSONData: function () { 180 | var data = {}; 181 | var trackedControllerEls = this.trackedControllerEls; 182 | 183 | if (this.isRecording) { return; } 184 | 185 | // Camera. 186 | data.camera = this.cameraEl.components['motion-capture-recorder'].getJSONData(); 187 | 188 | // Tracked controls. 189 | Object.keys(trackedControllerEls).forEach(function getControllerData (id) { 190 | data[id] = trackedControllerEls[id].components['motion-capture-recorder'].getJSONData(); 191 | }); 192 | 193 | return data; 194 | }, 195 | 196 | /** 197 | * Store recording in IndexedDB using recordingdb system. 198 | */ 199 | storeRecording: function (recordingData) { 200 | var data = this.data; 201 | if (!data.localStorage) { return; } 202 | log('Recording stored in localStorage.'); 203 | this.el.systems.recordingdb.addRecording(data.recordingName, recordingData); 204 | } 205 | }); 206 | -------------------------------------------------------------------------------- /src/components/avatar-replayer.js: -------------------------------------------------------------------------------- 1 | /* global THREE, AFRAME */ 2 | var constants = require('../constants'); 3 | 4 | var bind = AFRAME.utils.bind; 5 | var error = AFRAME.utils.debug('aframe-motion-capture:avatar-replayer:error'); 6 | var log = AFRAME.utils.debug('aframe-motion-capture:avatar-replayer:info'); 7 | var warn = AFRAME.utils.debug('aframe-motion-capture:avatar-replayer:warn'); 8 | 9 | var fileLoader = new THREE.FileLoader(); 10 | 11 | AFRAME.registerComponent('avatar-replayer', { 12 | schema: { 13 | autoPlay: {default: true}, 14 | cameraOverride: {type: 'selector'}, 15 | loop: {default: false}, 16 | recordingName: {default: constants.DEFAULT_RECORDING_NAME}, 17 | spectatorMode: {default: false}, 18 | spectatorPosition: {default: {x: 0, y: 1.6, z: 2}, type: 'vec3'}, 19 | src: {default: ''} 20 | }, 21 | 22 | init: function () { 23 | var sceneEl = this.el; 24 | 25 | // Bind methods. 26 | this.onKeyDown = bind(this.onKeyDown, this); 27 | 28 | // Prepare camera. 29 | this.setupCamera = bind(this.setupCamera, this); 30 | if (sceneEl.camera) { 31 | this.setupCamera(); 32 | } else { 33 | sceneEl.addEventListener('camera-set-active', this.setupCamera); 34 | } 35 | 36 | if (this.data.autoPlay) { 37 | this.replayRecordingFromSource(); 38 | } 39 | }, 40 | 41 | update: function (oldData) { 42 | var data = this.data; 43 | var spectatorModeUrlParam; 44 | 45 | spectatorModeUrlParam = 46 | window.location.search.indexOf('spectatormode') !== -1 || 47 | window.location.search.indexOf('spectatorMode') !== -1; 48 | 49 | // Handle toggling spectator mode. Don't run on initialization. Want to activate after 50 | // the player camera is initialized. 51 | if (oldData.spectatorMode !== data.spectatorMode || 52 | spectatorModeUrlParam) { 53 | if (data.spectatorMode || spectatorModeUrlParam) { 54 | this.activateSpectatorCamera(); 55 | } else if (oldData.spectatorMode === true) { 56 | this.deactivateSpectatorCamera(); 57 | } 58 | } 59 | 60 | // Handle `src` changing. 61 | if (data.src && oldData.src !== data.src && data.autoPlay) { 62 | this.replayRecordingFromSource(); 63 | } 64 | }, 65 | 66 | play: function () { 67 | window.addEventListener('keydown', this.onKeyDown); 68 | }, 69 | 70 | pause: function () { 71 | window.removeEventListener('keydown', this.onKeyDown); 72 | }, 73 | 74 | remove: function () { 75 | this.stopReplaying(); 76 | this.cameraEl.removeObject3D('replayerMesh'); 77 | }, 78 | 79 | /** 80 | * Grab a handle to the "original" camera. 81 | * Initialize spectator camera and dummy geometry for original camera. 82 | */ 83 | setupCamera: function () { 84 | var data = this.data; 85 | var sceneEl = this.el; 86 | 87 | if (data.cameraOverride) { 88 | // Specify which camera is the original camera (e.g., used by Inspector). 89 | this.cameraEl = data.cameraOverride; 90 | } else { 91 | // Default camera. 92 | this.cameraEl = sceneEl.camera.el; 93 | // Make sure A-Frame doesn't automatically remove this camera. 94 | this.cameraEl.removeAttribute('data-aframe-default-camera'); 95 | } 96 | this.cameraEl.setAttribute('data-aframe-avatar-replayer-camera', ''); 97 | 98 | sceneEl.removeEventListener('camera-set-active', this.setupCamera); 99 | 100 | this.configureHeadGeometry(); 101 | 102 | // Create spectator camera for either if we are in spectator mode or toggling to it. 103 | this.initSpectatorCamera(); 104 | }, 105 | 106 | /** 107 | * q: Toggle spectator camera. 108 | */ 109 | onKeyDown: function (evt) { 110 | switch (evt.keyCode) { 111 | // q. 112 | case 81: { 113 | this.el.setAttribute('avatar-replayer', 'spectatorMode', !this.data.spectatorMode); 114 | break; 115 | } 116 | } 117 | }, 118 | 119 | /** 120 | * Activate spectator camera, show replayer mesh. 121 | */ 122 | activateSpectatorCamera: function () { 123 | var spectatorCameraEl = this.spectatorCameraEl; 124 | 125 | if (!spectatorCameraEl) { 126 | this.el.addEventListener('spectatorcameracreated', 127 | bind(this.activateSpectatorCamera, this)); 128 | return; 129 | } 130 | 131 | if (!spectatorCameraEl.hasLoaded) { 132 | spectatorCameraEl.addEventListener('loaded', bind(this.activateSpectatorCamera, this)); 133 | return; 134 | } 135 | 136 | log('Activating spectator camera'); 137 | spectatorCameraEl.setAttribute('camera', 'active', true); 138 | this.cameraEl.getObject3D('replayerMesh').visible = true; 139 | }, 140 | 141 | /** 142 | * Deactivate spectator camera (by setting original camera active), hide replayer mesh. 143 | */ 144 | deactivateSpectatorCamera: function () { 145 | log('Deactivating spectator camera'); 146 | this.cameraEl.setAttribute('camera', 'active', true); 147 | this.cameraEl.getObject3D('replayerMesh').visible = false; 148 | }, 149 | 150 | /** 151 | * Create and activate spectator camera if in spectator mode. 152 | */ 153 | initSpectatorCamera: function () { 154 | var data = this.data; 155 | var sceneEl = this.el; 156 | var spectatorCameraEl; 157 | var spectatorCameraRigEl; 158 | 159 | // Developer-defined spectator rig. 160 | if (this.el.querySelector('#spectatorCameraRig')) { 161 | this.spectatorCameraEl = sceneEl.querySelector('#spectatorCameraRig'); 162 | return; 163 | } 164 | 165 | // Create spectator camera rig. 166 | spectatorCameraRigEl = sceneEl.querySelector('#spectatorCameraRig') || 167 | document.createElement('a-entity'); 168 | spectatorCameraRigEl.id = 'spectatorCameraRig'; 169 | spectatorCameraRigEl.setAttribute('position', data.spectatorPosition); 170 | this.spectatorCameraRigEl = spectatorCameraRigEl; 171 | 172 | // Create spectator camera. 173 | spectatorCameraEl = sceneEl.querySelector('#spectatorCamera') || 174 | document.createElement('a-entity'); 175 | spectatorCameraEl.id = 'spectatorCamera'; 176 | spectatorCameraEl.setAttribute('camera', {active: data.spectatorMode, userHeight: 0}); 177 | spectatorCameraEl.setAttribute('look-controls', ''); 178 | spectatorCameraEl.setAttribute('wasd-controls', {fly: true}); 179 | this.spectatorCameraEl = spectatorCameraEl; 180 | 181 | // Append rig. 182 | spectatorCameraRigEl.appendChild(spectatorCameraEl); 183 | sceneEl.appendChild(spectatorCameraRigEl); 184 | sceneEl.emit('spectatorcameracreated'); 185 | }, 186 | 187 | /** 188 | * Check for recording sources and play. 189 | */ 190 | replayRecordingFromSource: function () { 191 | var data = this.data; 192 | var recordingdb = this.el.systems.recordingdb;; 193 | var recordingNames; 194 | var src; 195 | var self = this; 196 | 197 | // Allow override to display replayer from query param. 198 | if (new URLSearchParams(window.location.search).get('avatar-replayer-disabled') !== null) { 199 | return; 200 | } 201 | 202 | recordingdb.getRecordingNames().then(function (recordingNames) { 203 | // See if recording defined in query parameter. 204 | var queryParamSrc = self.getSrcFromSearchParam(); 205 | 206 | // 1. Try `avatar-recorder` query parameter as recording name from IndexedDB. 207 | if (recordingNames.indexOf(queryParamSrc) !== -1) { 208 | log('Replaying `' + queryParamSrc + '` from IndexedDB.'); 209 | recordingdb.getRecording(queryParamSrc).then(bind(self.startReplaying, self)); 210 | return; 211 | } 212 | 213 | // 2. Use `avatar-recorder` query parameter or `data.src` as URL. 214 | src = queryParamSrc || self.data.src; 215 | if (src) { 216 | if (self.data.src) { 217 | log('Replaying from component `src`', src); 218 | } else if (queryParamSrc) { 219 | log('Replaying from query parameter `recording`', src); 220 | } 221 | self.loadRecordingFromUrl(src, false, bind(self.startReplaying, self)); 222 | return; 223 | } 224 | 225 | // 3. Use `data.recordingName` as recording name from IndexedDB. 226 | if (recordingNames.indexOf(self.data.recordingName) !== -1) { 227 | log('Replaying `' + self.data.recordingName + '` from IndexedDB.'); 228 | recordingdb.getRecording(self.data.recordingName).then(bind(self.startReplaying, self)); 229 | } 230 | }); 231 | }, 232 | 233 | /** 234 | * Defined for test stubbing. 235 | */ 236 | getSrcFromSearchParam: function () { 237 | var search = new URLSearchParams(window.location.search); 238 | return search.get('recording') || search.get('avatar-recording'); 239 | }, 240 | 241 | /** 242 | * Set player on camera and controllers (marked by ID). 243 | * 244 | * @params {object} replayData - { 245 | * camera: {poses: [], events: []}, 246 | * [c1ID]: {poses: [], events: []}, 247 | * [c2ID]: {poses: [], events: []} 248 | * } 249 | */ 250 | startReplaying: function (replayData) { 251 | var data = this.data; 252 | var self = this; 253 | var sceneEl = this.el; 254 | 255 | if (this.isReplaying) { return; } 256 | 257 | // Wait for camera. 258 | if (!this.el.camera) { 259 | this.el.addEventListener('camera-set-active', function waitForCamera () { 260 | self.startReplaying(replayData); 261 | self.el.removeEventListener('camera-set-active', waitForCamera); 262 | }); 263 | return; 264 | } 265 | 266 | this.replayData = replayData; 267 | this.isReplaying = true; 268 | 269 | this.cameraEl.removeAttribute('motion-capture-replayer'); 270 | 271 | Object.keys(replayData).forEach(function setReplayer (key) { 272 | var replayingEl; 273 | 274 | if (key === 'camera') { 275 | // Grab camera. 276 | replayingEl = self.cameraEl; 277 | } else { 278 | // Grab other entities. 279 | replayingEl = sceneEl.querySelector('#' + key); 280 | if (!replayingEl) { 281 | error('No element found with ID ' + key + '.'); 282 | return; 283 | } 284 | } 285 | 286 | log('Setting motion-capture-replayer on ' + key + '.'); 287 | replayingEl.setAttribute('motion-capture-replayer', {loop: data.loop}); 288 | replayingEl.components['motion-capture-replayer'].startReplaying(replayData[key]); 289 | }); 290 | }, 291 | 292 | /** 293 | * Create head geometry for spectator mode. 294 | * Always created in case we want to toggle, but only visible during spectator mode. 295 | */ 296 | configureHeadGeometry: function () { 297 | var cameraEl = this.cameraEl; 298 | var headMesh; 299 | var leftEyeMesh; 300 | var rightEyeMesh; 301 | var leftEyeBallMesh; 302 | var rightEyeBallMesh; 303 | 304 | if (cameraEl.getObject3D('mesh') || cameraEl.getObject3D('replayerMesh')) { return; } 305 | 306 | // Head. 307 | headMesh = new THREE.Mesh(); 308 | headMesh.geometry = new THREE.BoxBufferGeometry(0.3, 0.3, 0.2); 309 | headMesh.material = new THREE.MeshStandardMaterial({color: 'pink'}); 310 | headMesh.visible = this.data.spectatorMode; 311 | 312 | // Left eye. 313 | leftEyeMesh = new THREE.Mesh(); 314 | leftEyeMesh.geometry = new THREE.SphereBufferGeometry(0.05); 315 | leftEyeMesh.material = new THREE.MeshBasicMaterial({color: 'white'}); 316 | leftEyeMesh.position.x -= 0.1; 317 | leftEyeMesh.position.y += 0.1; 318 | leftEyeMesh.position.z -= 0.1; 319 | leftEyeBallMesh = new THREE.Mesh(); 320 | leftEyeBallMesh.geometry = new THREE.SphereBufferGeometry(0.025); 321 | leftEyeBallMesh.material = new THREE.MeshBasicMaterial({color: 'black'}); 322 | leftEyeBallMesh.position.z -= 0.04; 323 | leftEyeMesh.add(leftEyeBallMesh); 324 | headMesh.add(leftEyeMesh); 325 | 326 | // Right eye. 327 | rightEyeMesh = new THREE.Mesh(); 328 | rightEyeMesh.geometry = new THREE.SphereBufferGeometry(0.05); 329 | rightEyeMesh.material = new THREE.MeshBasicMaterial({color: 'white'}); 330 | rightEyeMesh.position.x += 0.1; 331 | rightEyeMesh.position.y += 0.1; 332 | rightEyeMesh.position.z -= 0.1; 333 | rightEyeBallMesh = new THREE.Mesh(); 334 | rightEyeBallMesh.geometry = new THREE.SphereBufferGeometry(0.025); 335 | rightEyeBallMesh.material = new THREE.MeshBasicMaterial({color: 'black'}); 336 | rightEyeBallMesh.position.z -= 0.04; 337 | rightEyeMesh.add(rightEyeBallMesh); 338 | headMesh.add(rightEyeMesh); 339 | 340 | cameraEl.setObject3D('replayerMesh', headMesh); 341 | }, 342 | 343 | /** 344 | * Remove motion-capture-replayer components. 345 | */ 346 | stopReplaying: function () { 347 | var self = this; 348 | 349 | if (!this.isReplaying || !this.replayData) { return; } 350 | 351 | this.isReplaying = false; 352 | Object.keys(this.replayData).forEach(function removeReplayer (key) { 353 | if (key === 'camera') { 354 | self.cameraEl.removeComponent('motion-capture-replayer'); 355 | } else { 356 | el = document.querySelector('#' + key); 357 | if (!el) { 358 | warn('No element with id ' + key); 359 | return; 360 | } 361 | el.removeComponent('motion-capture-replayer'); 362 | } 363 | }); 364 | }, 365 | 366 | /** 367 | * XHR for data. 368 | */ 369 | loadRecordingFromUrl: function (url, binary, callback) { 370 | var data; 371 | var self = this; 372 | fileLoader.crossOrigin = 'anonymous'; 373 | if (binary === true) { 374 | fileLoader.setResponseType('arraybuffer'); 375 | } 376 | fileLoader.load(url, function (buffer) { 377 | if (binary === true) { 378 | data = self.loadStrokeBinary(buffer); 379 | } else { 380 | data = JSON.parse(buffer); 381 | } 382 | if (callback) { callback(data); } 383 | }); 384 | } 385 | }); 386 | -------------------------------------------------------------------------------- /src/components/motion-capture-recorder.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | 3 | var EVENTS = { 4 | axismove: {id: 0, props: ['id', 'axis', 'changed']}, 5 | buttonchanged: {id: 1, props: ['id', 'state']}, 6 | buttondown: {id: 2, props: ['id', 'state']}, 7 | buttonup: {id: 3, props: ['id', 'state']}, 8 | touchstart: {id: 4, props: ['id', 'state']}, 9 | touchend: {id: 5, props: ['id', 'state']} 10 | }; 11 | 12 | var EVENTS_DECODE = { 13 | 0: 'axismove', 14 | 1: 'buttonchanged', 15 | 2: 'buttondown', 16 | 3: 'buttonup', 17 | 4: 'touchstart', 18 | 5: 'touchend' 19 | }; 20 | 21 | AFRAME.registerComponent('motion-capture-recorder', { 22 | schema: { 23 | autoRecord: {default: false}, 24 | enabled: {default: true}, 25 | hand: {default: 'right'}, 26 | recordingControls: {default: false}, 27 | persistStroke: {default: false}, 28 | visibleStroke: {default: true} 29 | }, 30 | 31 | init: function () { 32 | this.drawing = false; 33 | this.recordedEvents = []; 34 | this.recordedPoses = []; 35 | this.addEventListeners(); 36 | }, 37 | 38 | addEventListeners: function () { 39 | var el = this.el; 40 | this.recordEvent = this.recordEvent.bind(this); 41 | el.addEventListener('axismove', this.recordEvent); 42 | el.addEventListener('buttonchanged', this.onTriggerChanged.bind(this)); 43 | el.addEventListener('buttonchanged', this.recordEvent); 44 | el.addEventListener('buttonup', this.recordEvent); 45 | el.addEventListener('buttondown', this.recordEvent); 46 | el.addEventListener('touchstart', this.recordEvent); 47 | el.addEventListener('touchend', this.recordEvent); 48 | }, 49 | 50 | recordEvent: function (evt) { 51 | var detail; 52 | if (!this.isRecording) { return; } 53 | 54 | // Filter out `target`, not serializable. 55 | if ('detail' in evt && 'state' in evt.detail && typeof evt.detail.state === 'object' && 56 | 'target' in evt.detail.state) { 57 | delete evt.detail.state.target; 58 | } 59 | 60 | detail = {}; 61 | EVENTS[evt.type].props.forEach(function buildDetail (propName) { 62 | // Convert GamepadButton to normal JS object. 63 | if (propName === 'state') { 64 | var stateProp; 65 | detail.state = {}; 66 | for (stateProp in evt.detail.state) { 67 | detail.state[stateProp] = evt.detail.state[stateProp]; 68 | } 69 | return; 70 | } 71 | detail[propName] = evt.detail[propName]; 72 | }); 73 | 74 | this.recordedEvents.push({ 75 | name: evt.type, 76 | detail: detail, 77 | timestamp: this.lastTimestamp 78 | }); 79 | }, 80 | 81 | onTriggerChanged: function (evt) { 82 | var data = this.data; 83 | var value; 84 | if (!data.enabled || data.autoRecord) { return; } 85 | // Not Trigger 86 | if (evt.detail.id !== 1 || !this.data.recordingControls) { return; } 87 | value = evt.detail.state.value; 88 | if (value <= 0.1) { 89 | if (this.isRecording) { this.stopRecording(); } 90 | return; 91 | } 92 | if (!this.isRecording) { this.startRecording(); } 93 | }, 94 | 95 | getJSONData: function () { 96 | var data; 97 | var trackedControlsComponent = this.el.components['tracked-controls']; 98 | var controller = trackedControlsComponent && trackedControlsComponent.controller; 99 | if (!this.recordedPoses) { return; } 100 | data = { 101 | poses: this.getStrokeJSON(this.recordedPoses), 102 | events: this.recordedEvents 103 | }; 104 | if (controller) { 105 | data.gamepad = { 106 | id: controller.id, 107 | hand: controller.hand, 108 | index: controller.index 109 | }; 110 | } 111 | return data; 112 | }, 113 | 114 | getStrokeJSON: function (stroke) { 115 | var point; 116 | var points = []; 117 | for (var i = 0; i < stroke.length; i++) { 118 | point = stroke[i]; 119 | points.push({ 120 | position: point.position, 121 | rotation: point.rotation, 122 | timestamp: point.timestamp 123 | }); 124 | } 125 | return points; 126 | }, 127 | 128 | saveCapture: function (binary) { 129 | var jsonData = JSON.stringify(this.getJSONData()); 130 | var type = binary ? 'application/octet-binary' : 'application/json'; 131 | var blob = new Blob([jsonData], {type: type}); 132 | var url = URL.createObjectURL(blob); 133 | var fileName = 'motion-capture-' + document.title + '-' + Date.now() + '.json'; 134 | var aEl = document.createElement('a'); 135 | aEl.setAttribute('class', 'motion-capture-download'); 136 | aEl.href = url; 137 | aEl.setAttribute('download', fileName); 138 | aEl.innerHTML = 'downloading...'; 139 | aEl.style.display = 'none'; 140 | document.body.appendChild(aEl); 141 | setTimeout(function () { 142 | aEl.click(); 143 | document.body.removeChild(aEl); 144 | }, 1); 145 | }, 146 | 147 | update: function () { 148 | var el = this.el; 149 | var data = this.data; 150 | if (this.data.autoRecord) { 151 | this.startRecording(); 152 | } else { 153 | // Don't try to record camera with controllers. 154 | if (el.components.camera) { return; } 155 | 156 | if (data.recordingControls) { 157 | el.setAttribute('vive-controls', {hand: data.hand}); 158 | el.setAttribute('oculus-touch-controls', {hand: data.hand}); 159 | } 160 | el.setAttribute('stroke', ''); 161 | } 162 | }, 163 | 164 | tick: (function () { 165 | var position = new THREE.Vector3(); 166 | var rotation = new THREE.Quaternion(); 167 | var scale = new THREE.Vector3(); 168 | 169 | return function (time, delta) { 170 | var newPoint; 171 | var pointerPosition; 172 | this.lastTimestamp = time; 173 | if (!this.data.enabled || !this.isRecording) { return; } 174 | newPoint = { 175 | position: AFRAME.utils.clone(this.el.getAttribute('position')), 176 | rotation: AFRAME.utils.clone(this.el.getAttribute('rotation')), 177 | timestamp: time 178 | }; 179 | this.recordedPoses.push(newPoint); 180 | if (!this.data.visibleStroke) { return; } 181 | this.el.object3D.updateMatrixWorld(); 182 | this.el.object3D.matrixWorld.decompose(position, rotation, scale); 183 | pointerPosition = this.getPointerPosition(position, rotation); 184 | this.el.components.stroke.drawPoint(position, rotation, time, pointerPosition); 185 | }; 186 | })(), 187 | 188 | getPointerPosition: (function () { 189 | var pointerPosition = new THREE.Vector3(); 190 | var offset = new THREE.Vector3(0, 0.7, 1); 191 | return function getPointerPosition (position, orientation) { 192 | var pointer = offset 193 | .clone() 194 | .applyQuaternion(orientation) 195 | .normalize() 196 | .multiplyScalar(-0.03); 197 | pointerPosition.copy(position).add(pointer); 198 | return pointerPosition; 199 | }; 200 | })(), 201 | 202 | startRecording: function () { 203 | var el = this.el; 204 | if (this.isRecording) { return; } 205 | if (el.components.stroke) { el.components.stroke.reset(); } 206 | this.isRecording = true; 207 | this.recordedPoses = []; 208 | this.recordedEvents = []; 209 | el.emit('strokestarted', {entity: el, poses: this.recordedPoses}); 210 | }, 211 | 212 | stopRecording: function () { 213 | var el = this.el; 214 | if (!this.isRecording) { return; } 215 | el.emit('strokeended', {poses: this.recordedPoses}); 216 | this.isRecording = false; 217 | if (!this.data.visibleStroke || this.data.persistStroke) { return; } 218 | el.components.stroke.reset(); 219 | } 220 | }); 221 | -------------------------------------------------------------------------------- /src/components/motion-capture-replayer.js: -------------------------------------------------------------------------------- 1 | /* global THREE, AFRAME */ 2 | AFRAME.registerComponent('motion-capture-replayer', { 3 | schema: { 4 | enabled: {default: true}, 5 | recorderEl: {type: 'selector'}, 6 | loop: {default: false}, 7 | src: {default: ''}, 8 | spectatorCamera: {default: false} 9 | }, 10 | 11 | init: function () { 12 | this.currentPoseTime = 0; 13 | this.currentEventTime = 0; 14 | this.currentPoseIndex = 0; 15 | this.currentEventIndex = 0; 16 | this.onStrokeStarted = this.onStrokeStarted.bind(this); 17 | this.onStrokeEnded = this.onStrokeEnded.bind(this); 18 | this.playComponent = this.playComponent.bind(this); 19 | this.el.addEventListener('pause', this.playComponent); 20 | this.discardedFrames = 0; 21 | this.playingEvents = []; 22 | this.playingPoses = []; 23 | this.gamepadData = null; 24 | }, 25 | 26 | remove: function () { 27 | var el = this.el; 28 | var gamepadData = this.gamepadData; 29 | var gamepads; 30 | var found = -1; 31 | 32 | el.removeEventListener('pause', this.playComponent); 33 | this.stopReplaying(); 34 | el.pause(); 35 | el.play(); 36 | 37 | // Remove gamepad from system. 38 | if (this.gamepadData) { 39 | gamepads = el.sceneEl.systems['motion-capture-replayer'].gamepads; 40 | gamepads.forEach(function (gamepad, i) { 41 | if (gamepad === gamepadData) { found = i; } 42 | }); 43 | if (found !== -1) { 44 | gamepads.splice(found, 1); 45 | } 46 | } 47 | }, 48 | 49 | update: function (oldData) { 50 | var data = this.data; 51 | this.updateRecorder(data.recorderEl, oldData.recorderEl); 52 | if (!this.el.isPlaying) { this.playComponent(); } 53 | if (oldData.src === data.src) { return; } 54 | if (data.src) { this.updateSrc(data.src); } 55 | }, 56 | 57 | updateRecorder: function (newRecorderEl, oldRecorderEl) { 58 | if (oldRecorderEl && oldRecorderEl !== newRecorderEl) { 59 | oldRecorderEl.removeEventListener('strokestarted', this.onStrokeStarted); 60 | oldRecorderEl.removeEventListener('strokeended', this.onStrokeEnded); 61 | } 62 | if (!newRecorderEl || oldRecorderEl === newRecorderEl) { return; } 63 | newRecorderEl.addEventListener('strokestarted', this.onStrokeStarted); 64 | newRecorderEl.addEventListener('strokeended', this.onStrokeEnded); 65 | }, 66 | 67 | updateSrc: function (src) { 68 | this.el.sceneEl.systems['motion-capture-recorder'].loadRecordingFromUrl( 69 | src, false, this.startReplaying.bind(this)); 70 | }, 71 | 72 | onStrokeStarted: function(evt) { 73 | this.reset(); 74 | }, 75 | 76 | onStrokeEnded: function(evt) { 77 | this.startReplayingPoses(evt.detail.poses); 78 | }, 79 | 80 | play: function () { 81 | if (this.playingStroke) { this.playStroke(this.playingStroke); } 82 | }, 83 | 84 | playComponent: function () { 85 | this.el.isPlaying = true; 86 | this.play(); 87 | }, 88 | 89 | /** 90 | * @param {object} data - Recording data. 91 | */ 92 | startReplaying: function (data) { 93 | var el = this.el; 94 | 95 | this.ignoredFrames = 0; 96 | this.storeInitialPose(); 97 | this.isReplaying = true; 98 | this.startReplayingPoses(data.poses); 99 | this.startReplayingEvents(data.events); 100 | 101 | // Add gamepad metadata to system. 102 | if (data.gamepad) { 103 | this.gamepadData = data.gamepad; 104 | el.sceneEl.systems['motion-capture-replayer'].gamepads.push(data.gamepad); 105 | el.sceneEl.systems['motion-capture-replayer'].updateControllerList(); 106 | } 107 | 108 | el.emit('replayingstarted'); 109 | }, 110 | 111 | stopReplaying: function () { 112 | this.isReplaying = false; 113 | this.restoreInitialPose(); 114 | this.el.emit('replayingstopped'); 115 | }, 116 | 117 | storeInitialPose: function () { 118 | var el = this.el; 119 | this.initialPose = { 120 | position: AFRAME.utils.clone(el.getAttribute('position')), 121 | rotation: AFRAME.utils.clone(el.getAttribute('rotation')) 122 | }; 123 | }, 124 | 125 | restoreInitialPose: function () { 126 | var el = this.el; 127 | if (!this.initialPose) { return; } 128 | el.setAttribute('position', this.initialPose.position); 129 | el.setAttribute('rotation', this.initialPose.rotation); 130 | }, 131 | 132 | startReplayingPoses: function (poses) { 133 | this.isReplaying = true; 134 | this.currentPoseIndex = 0; 135 | if (poses.length === 0) { return; } 136 | this.playingPoses = poses; 137 | this.currentPoseTime = poses[0].timestamp; 138 | }, 139 | 140 | /** 141 | * @param events {Array} - Array of events with timestamp, name, and detail. 142 | */ 143 | startReplayingEvents: function (events) { 144 | var firstEvent; 145 | this.isReplaying = true; 146 | this.currentEventIndex = 0; 147 | if (events.length === 0) { return; } 148 | firstEvent = events[0]; 149 | this.playingEvents = events; 150 | this.currentEventTime = firstEvent.timestamp; 151 | this.el.emit(firstEvent.name, firstEvent.detail); 152 | }, 153 | 154 | // Reset player 155 | reset: function () { 156 | this.playingPoses = null; 157 | this.currentTime = undefined; 158 | this.currentPoseIndex = undefined; 159 | }, 160 | 161 | /** 162 | * Called on tick. 163 | */ 164 | playRecording: function (delta) { 165 | var currentPose; 166 | var currentEvent 167 | var playingPoses = this.playingPoses; 168 | var playingEvents = this.playingEvents; 169 | currentPose = playingPoses && playingPoses[this.currentPoseIndex] 170 | currentEvent = playingEvents && playingEvents[this.currentEventIndex]; 171 | this.currentPoseTime += delta; 172 | this.currentEventTime += delta; 173 | // Determine next pose. 174 | // Comparing currentPoseTime to currentEvent.timestamp is not a typo. 175 | while ((currentPose && this.currentPoseTime >= currentPose.timestamp) || 176 | (currentEvent && this.currentPoseTime >= currentEvent.timestamp)) { 177 | // Pose. 178 | if (currentPose && this.currentPoseTime >= currentPose.timestamp) { 179 | if (this.currentPoseIndex === playingPoses.length - 1) { 180 | if (this.data.loop) { 181 | this.currentPoseIndex = 0; 182 | this.currentPoseTime = playingPoses[0].timestamp; 183 | } else { 184 | this.stopReplaying(); 185 | } 186 | } 187 | applyPose(this.el, currentPose); 188 | this.currentPoseIndex += 1; 189 | currentPose = playingPoses[this.currentPoseIndex]; 190 | } 191 | // Event. 192 | if (currentEvent && this.currentPoseTime >= currentEvent.timestamp) { 193 | if (this.currentEventIndex === playingEvents.length && this.data.loop) { 194 | this.currentEventIndex = 0; 195 | this.currentEventTime = playingEvents[0].timestamp; 196 | } 197 | this.el.emit(currentEvent.name, currentEvent.detail); 198 | this.currentEventIndex += 1; 199 | currentEvent = this.playingEvents[this.currentEventIndex]; 200 | } 201 | } 202 | }, 203 | 204 | tick: function (time, delta) { 205 | // Ignore the first couple of frames that come from window.RAF on Firefox. 206 | if (this.ignoredFrames !== 2 && !window.debug) { 207 | this.ignoredFrames++; 208 | return; 209 | } 210 | 211 | if (!this.isReplaying) { return; } 212 | this.playRecording(delta); 213 | } 214 | }); 215 | 216 | function applyPose (el, pose) { 217 | el.setAttribute('position', pose.position); 218 | el.setAttribute('rotation', pose.rotation); 219 | el.object3D.updateMatrix() 220 | }; 221 | -------------------------------------------------------------------------------- /src/components/stroke.js: -------------------------------------------------------------------------------- 1 | /* global THREE AFRAME */ 2 | AFRAME.registerComponent('stroke', { 3 | schema: { 4 | enabled: {default: true}, 5 | color: {default: '#ef2d5e', type: 'color'} 6 | }, 7 | 8 | init: function () { 9 | var maxPoints = this.maxPoints = 3000; 10 | var strokeEl; 11 | this.idx = 0; 12 | this.numPoints = 0; 13 | 14 | // Buffers 15 | this.vertices = new Float32Array(maxPoints*3*3); 16 | this.normals = new Float32Array(maxPoints*3*3); 17 | this.uvs = new Float32Array(maxPoints*2*2); 18 | 19 | // Geometries 20 | this.geometry = new THREE.BufferGeometry(); 21 | this.geometry.setDrawRange(0, 0); 22 | this.geometry.addAttribute('position', new THREE.BufferAttribute(this.vertices, 3).setDynamic(true)); 23 | this.geometry.addAttribute('uv', new THREE.BufferAttribute(this.uvs, 2).setDynamic(true)); 24 | this.geometry.addAttribute('normal', new THREE.BufferAttribute(this.normals, 3).setDynamic(true)); 25 | 26 | this.material = new THREE.MeshStandardMaterial({ 27 | color: this.data.color, 28 | roughness: 0.75, 29 | metalness: 0.25, 30 | side: THREE.DoubleSide 31 | }); 32 | 33 | var mesh = new THREE.Mesh(this.geometry, this.material); 34 | mesh.drawMode = THREE.TriangleStripDrawMode; 35 | mesh.frustumCulled = false; 36 | 37 | // Injects stroke entity 38 | strokeEl = document.createElement('a-entity'); 39 | strokeEl.setObject3D('stroke', mesh); 40 | this.el.sceneEl.appendChild(strokeEl); 41 | }, 42 | 43 | update: function() { 44 | this.material.color.set(this.data.color); 45 | }, 46 | 47 | drawPoint: (function () { 48 | var direction = new THREE.Vector3(); 49 | var positionA = new THREE.Vector3(); 50 | var positionB = new THREE.Vector3(); 51 | return function (position, orientation, timestamp, pointerPosition) { 52 | var uv = 0; 53 | var numPoints = this.numPoints; 54 | var brushSize = 0.01; 55 | if (numPoints === this.maxPoints) { return; } 56 | for (i = 0; i < numPoints; i++) { 57 | this.uvs[uv++] = i / (numPoints - 1); 58 | this.uvs[uv++] = 0; 59 | 60 | this.uvs[uv++] = i / (numPoints - 1); 61 | this.uvs[uv++] = 1; 62 | } 63 | 64 | direction.set(1, 0, 0); 65 | direction.applyQuaternion(orientation); 66 | direction.normalize(); 67 | 68 | positionA.copy(pointerPosition); 69 | positionB.copy(pointerPosition); 70 | positionA.add(direction.clone().multiplyScalar(brushSize / 2)); 71 | positionB.add(direction.clone().multiplyScalar(-brushSize / 2)); 72 | 73 | this.vertices[this.idx++] = positionA.x; 74 | this.vertices[this.idx++] = positionA.y; 75 | this.vertices[this.idx++] = positionA.z; 76 | 77 | this.vertices[this.idx++] = positionB.x; 78 | this.vertices[this.idx++] = positionB.y; 79 | this.vertices[this.idx++] = positionB.z; 80 | 81 | this.computeVertexNormals(); 82 | this.geometry.attributes.normal.needsUpdate = true; 83 | this.geometry.attributes.position.needsUpdate = true; 84 | this.geometry.attributes.uv.needsUpdate = true; 85 | 86 | this.geometry.setDrawRange(0, numPoints * 2); 87 | this.numPoints += 1; 88 | return true; 89 | } 90 | })(), 91 | 92 | reset: function () { 93 | var idx = 0; 94 | var vertices = this.vertices; 95 | for (i = 0; i < this.numPoints; i++) { 96 | vertices[idx++] = 0; 97 | vertices[idx++] = 0; 98 | vertices[idx++] = 0; 99 | 100 | vertices[idx++] = 0; 101 | vertices[idx++] = 0; 102 | vertices[idx++] = 0; 103 | } 104 | this.geometry.setDrawRange(0, 0); 105 | this.idx = 0; 106 | this.numPoints = 0; 107 | }, 108 | 109 | computeVertexNormals: function () { 110 | var pA = new THREE.Vector3(); 111 | var pB = new THREE.Vector3(); 112 | var pC = new THREE.Vector3(); 113 | var cb = new THREE.Vector3(); 114 | var ab = new THREE.Vector3(); 115 | 116 | for (var i = 0, il = this.idx; i < il; i++) { 117 | this.normals[ i ] = 0; 118 | } 119 | 120 | var pair = true; 121 | for (i = 0, il = this.idx; i < il; i += 3) { 122 | if (pair) { 123 | pA.fromArray(this.vertices, i); 124 | pB.fromArray(this.vertices, i + 3); 125 | pC.fromArray(this.vertices, i + 6); 126 | } else { 127 | pA.fromArray(this.vertices, i + 3); 128 | pB.fromArray(this.vertices, i); 129 | pC.fromArray(this.vertices, i + 6); 130 | } 131 | pair = !pair; 132 | 133 | cb.subVectors(pC, pB); 134 | ab.subVectors(pA, pB); 135 | cb.cross(ab); 136 | cb.normalize(); 137 | 138 | this.normals[i] += cb.x; 139 | this.normals[i + 1] += cb.y; 140 | this.normals[i + 2] += cb.z; 141 | 142 | this.normals[i + 3] += cb.x; 143 | this.normals[i + 4] += cb.y; 144 | this.normals[i + 5] += cb.z; 145 | 146 | this.normals[i + 6] += cb.x; 147 | this.normals[i + 7] += cb.y; 148 | this.normals[i + 8] += cb.z; 149 | } 150 | 151 | /* 152 | first and last vertice (0 and 8) belongs just to one triangle 153 | second and penultimate (1 and 7) belongs to two triangles 154 | the rest of the vertices belongs to three triangles 155 | 156 | 1_____3_____5_____7 157 | /\ /\ /\ /\ 158 | / \ / \ / \ / \ 159 | /____\/____\/____\/____\ 160 | 0 2 4 6 8 161 | */ 162 | 163 | // Vertices that are shared across three triangles 164 | for (i = 2 * 3, il = this.idx - 2 * 3; i < il; i++) { 165 | this.normals[ i ] = this.normals[ i ] / 3; 166 | } 167 | 168 | // Second and penultimate triangle, that shares just two triangles 169 | this.normals[ 3 ] = this.normals[ 3 ] / 2; 170 | this.normals[ 3 + 1 ] = this.normals[ 3 + 1 ] / 2; 171 | this.normals[ 3 + 2 ] = this.normals[ 3 * 1 + 2 ] / 2; 172 | 173 | this.normals[ this.idx - 2 * 3 ] = this.normals[ this.idx - 2 * 3 ] / 2; 174 | this.normals[ this.idx - 2 * 3 + 1 ] = this.normals[ this.idx - 2 * 3 + 1 ] / 2; 175 | this.normals[ this.idx - 2 * 3 + 2 ] = this.normals[ this.idx - 2 * 3 + 2 ] / 2; 176 | 177 | this.geometry.normalizeNormals(); 178 | } 179 | }); 180 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | module.exports.LOCALSTORAGE_RECORDINGS = 'avatarRecordings'; 2 | module.exports.DEFAULT_RECORDING_NAME = 'default'; 3 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | if (typeof AFRAME === 'undefined') { 2 | throw new Error('Component attempted to register before AFRAME was available.'); 3 | } 4 | 5 | // Components. 6 | require('./components/motion-capture-recorder.js'); 7 | require('./components/motion-capture-replayer.js'); 8 | require('./components/avatar-recorder.js'); 9 | require('./components/avatar-replayer.js'); 10 | require('./components/stroke.js'); 11 | 12 | // Systems. 13 | require('./systems/motion-capture-replayer.js'); 14 | require('./systems/recordingdb.js'); 15 | -------------------------------------------------------------------------------- /src/systems/motion-capture-replayer.js: -------------------------------------------------------------------------------- 1 | AFRAME.registerSystem('motion-capture-replayer', { 2 | init: function () { 3 | var sceneEl = this.sceneEl; 4 | var trackedControlsComponent; 5 | var trackedControlsSystem; 6 | var trackedControlsTick; 7 | 8 | trackedControlsSystem = sceneEl.systems['tracked-controls']; 9 | trackedControlsTick = AFRAME.components['tracked-controls'].Component.prototype.tick; 10 | 11 | // Gamepad data stored in recording and added here by `motion-capture-replayer` component. 12 | this.gamepads = []; 13 | 14 | // Wrap `updateControllerList`. 15 | this.updateControllerListOriginal = trackedControlsSystem.updateControllerList.bind( 16 | trackedControlsSystem); 17 | this.throttledUpdateControllerListOriginal = trackedControlsSystem.throttledUpdateControllerList 18 | trackedControlsSystem.throttledUpdateControllerList = this.updateControllerList.bind(this); 19 | 20 | // Wrap `tracked-controls` tick. 21 | trackedControlsComponent = AFRAME.components['tracked-controls'].Component.prototype; 22 | trackedControlsComponent.tick = this.trackedControlsTickWrapper; 23 | trackedControlsComponent.trackedControlsTick = trackedControlsTick; 24 | }, 25 | 26 | remove: function () { 27 | // restore modified objects 28 | var trackedControlsComponent = AFRAME.components['tracked-controls'].Component.prototype; 29 | var trackedControlsSystem = this.sceneEl.systems['tracked-controls']; 30 | trackedControlsComponent.tick = trackedControlsComponent.trackedControlsTick; 31 | delete trackedControlsComponent.trackedControlsTick; 32 | trackedControlsSystem.throttledUpdateControllerList = this.throttledUpdateControllerListOriginal; 33 | }, 34 | 35 | trackedControlsTickWrapper: function (time, delta) { 36 | if (this.el.components['motion-capture-replayer']) { return; } 37 | this.trackedControlsTick(time, delta); 38 | }, 39 | 40 | /** 41 | * Wrap `updateControllerList` to stub in the gamepads and emit `controllersupdated`. 42 | */ 43 | updateControllerList: function (gamepads) { 44 | var i; 45 | var sceneEl = this.sceneEl; 46 | var trackedControlsSystem = sceneEl.systems['tracked-controls']; 47 | gamepads = gamepads || [] 48 | // convert from read-only GamepadList 49 | gamepads = Array.from(gamepads) 50 | 51 | this.gamepads.forEach(function (gamepad) { 52 | if (gamepads[gamepad.index]) { return; } 53 | // to pass check in updateControllerListOriginal 54 | gamepad.pose = true; 55 | gamepads[gamepad.index] = gamepad; 56 | }); 57 | 58 | this.updateControllerListOriginal(gamepads); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /src/systems/recordingdb.js: -------------------------------------------------------------------------------- 1 | /* global indexedDB */ 2 | var constants = require('../constants'); 3 | 4 | var DB_NAME = 'motionCaptureRecordings'; 5 | var OBJECT_STORE_NAME = 'recordings'; 6 | var VERSION = 1; 7 | 8 | /** 9 | * Interface for storing and accessing recordings from Indexed DB. 10 | */ 11 | AFRAME.registerSystem('recordingdb', { 12 | init: function () { 13 | var request; 14 | var self = this; 15 | 16 | this.db = null; 17 | this.hasLoaded = false; 18 | 19 | request = indexedDB.open(DB_NAME, VERSION); 20 | 21 | request.onerror = function () { 22 | console.error('Error opening IndexedDB for motion capture.', request.error); 23 | }; 24 | 25 | // Initialize database. 26 | request.onupgradeneeded = function (evt) { 27 | var db = self.db = evt.target.result; 28 | var objectStore; 29 | 30 | // Create object store. 31 | objectStore = db.createObjectStore('recordings', { 32 | autoIncrement: false 33 | }); 34 | objectStore.createIndex('recordingName', 'recordingName', {unique: true}); 35 | self.objectStore = objectStore; 36 | }; 37 | 38 | // Got database. 39 | request.onsuccess = function (evt) { 40 | self.db = evt.target.result; 41 | self.hasLoaded = true; 42 | self.sceneEl.emit('recordingdbinitialized'); 43 | }; 44 | }, 45 | 46 | /** 47 | * Need a new transaction for everything. 48 | */ 49 | getTransaction: function () { 50 | var transaction = this.db.transaction([OBJECT_STORE_NAME], 'readwrite'); 51 | return transaction.objectStore(OBJECT_STORE_NAME); 52 | }, 53 | 54 | getRecordingNames: function () { 55 | var self = this; 56 | return new Promise(function (resolve) { 57 | var recordingNames = []; 58 | 59 | self.waitForDb(function () { 60 | self.getTransaction().openCursor().onsuccess = function (evt) { 61 | var cursor = evt.target.result; 62 | 63 | // No recordings. 64 | if (!cursor) { 65 | resolve(recordingNames.sort()); 66 | return; 67 | } 68 | 69 | recordingNames.push(cursor.key); 70 | cursor.continue(); 71 | }; 72 | }); 73 | }); 74 | }, 75 | 76 | getRecordings: function (cb) { 77 | var self = this; 78 | return new Promise(function getRecordings (resolve) { 79 | self.waitForDb(function () { 80 | self.getTransaction().openCursor().onsuccess = function (evt) { 81 | var cursor = evt.target.result; 82 | var recordings = [cursor.value]; 83 | while (cursor.ontinue()) { 84 | recordings.push(cursor.value); 85 | } 86 | resolve(recordings); 87 | }; 88 | }); 89 | }); 90 | }, 91 | 92 | getRecording: function (name) { 93 | var self = this; 94 | return new Promise(function getRecording (resolve) { 95 | self.waitForDb(function () { 96 | self.getTransaction().get(name).onsuccess = function (evt) { 97 | resolve(evt.target.result); 98 | }; 99 | }); 100 | }); 101 | }, 102 | 103 | addRecording: function (name, data) { 104 | this.getTransaction().add(data, name); 105 | }, 106 | 107 | deleteRecording: function (name) { 108 | this.getTransaction().delete(name); 109 | }, 110 | 111 | /** 112 | * Helper to wait for store to be initialized before using it. 113 | */ 114 | waitForDb: function (cb) { 115 | if (this.hasLoaded) { 116 | cb(); 117 | return; 118 | } 119 | this.sceneEl.addEventListener('recordingdbinitialized', cb); 120 | } 121 | }); 122 | -------------------------------------------------------------------------------- /tests/__init.test.js: -------------------------------------------------------------------------------- 1 | /* global sinon, setup, teardown */ 2 | /** 3 | * __init.test.js is run before every test case. 4 | */ 5 | window.debug = true; 6 | const AScene = require('aframe').AScene; 7 | 8 | navigator.getVRDisplays = function () { 9 | var resolvePromise = Promise.resolve(); 10 | var mockVRDisplay = { 11 | requestPresent: resolvePromise, 12 | exitPresent: resolvePromise, 13 | getPose: function () { return {orientation: null, position: null}; }, 14 | requestAnimationFrame: function () { return 1; } 15 | }; 16 | return Promise.resolve([mockVRDisplay]); 17 | }; 18 | 19 | setup(function () { 20 | this.sinon = sinon.sandbox.create(); 21 | // Stubs to not create a WebGL context since Travis CI runs headless. 22 | this.sinon.stub(AScene.prototype, 'render'); 23 | this.sinon.stub(AScene.prototype, 'resize'); 24 | this.sinon.stub(AScene.prototype, 'setupRenderer'); 25 | }); 26 | 27 | teardown(function () { 28 | // Clean up any attached elements. 29 | var attachedEls = ['canvas', 'a-assets', 'a-scene']; 30 | var els = document.querySelectorAll(attachedEls.join(',')); 31 | for (var i = 0; i < els.length; i++) { 32 | els[i].parentNode.removeChild(els[i]); 33 | } 34 | this.sinon.restore(); 35 | }); 36 | -------------------------------------------------------------------------------- /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('../src/index.js'); 4 | const helpers = require('./helpers'); 5 | 6 | suite('avatar-recorder', function () { 7 | var component; 8 | var sceneEl; 9 | 10 | setup(function (done) { 11 | sceneEl = document.createElement('a-scene'); 12 | sceneEl.addEventListener('componentinitialized', evt => { 13 | if (evt.detail.name !== 'avatar-recorder') { return; } 14 | if (sceneEl.systems.recordingdb.hasLoaded) { 15 | component = sceneEl.components['avatar-recorder']; 16 | waitForCamera(); 17 | } else { 18 | sceneEl.addEventListener('recordingdbinitialized', function () { 19 | component = sceneEl.components['avatar-recorder']; 20 | waitForCamera(); 21 | }); 22 | } 23 | }); 24 | 25 | function waitForCamera () { 26 | if (sceneEl.camera) { 27 | done(); 28 | return; 29 | } 30 | sceneEl.addEventListener('camera-set-active', function () { 31 | done(); 32 | }); 33 | } 34 | 35 | sceneEl.setAttribute('avatar-recorder', ''); 36 | document.body.appendChild(sceneEl); 37 | }); 38 | 39 | teardown(function () { 40 | // https://github.com/aframevr/aframe/pull/2302 41 | window.removeEventListener('keydown', component.onKeyDown); 42 | }); 43 | 44 | test('gets the camera element', function () { 45 | component.startRecording(); 46 | assert.equal(component.cameraEl, sceneEl.camera.el); 47 | }); 48 | 49 | test('sets motion-capture-recorder on camera', function () { 50 | sceneEl.addEventListener('camera-set-active', () => { 51 | component.startRecording(); 52 | assert.ok(sceneEl.camera.el.getAttribute('motion-capture-recorder')); 53 | done(); 54 | }); 55 | }); 56 | 57 | test('sets motion-capture-recorder on tracked controllers', function (done) { 58 | var controllers; 59 | 60 | // Create controllers. 61 | const c1 = document.createElement('a-entity'); 62 | c1.setAttribute('id', 'c1'); 63 | c1.setAttribute('tracked-controls', ''); 64 | const c2 = document.createElement('a-entity'); 65 | c2.setAttribute('id', 'c2'); 66 | c2.setAttribute('tracked-controls', ''); 67 | 68 | component.throttledTick(); 69 | controllers = component.trackedControllerEls; 70 | assert.notOk('c1' in controllers, 'Controller 1 not appended yet'); 71 | assert.notOk('c2' in controllers, 'Controller 2 not appended yet'); 72 | 73 | sceneEl.appendChild(c1); 74 | sceneEl.appendChild(c2); 75 | setTimeout(() => { 76 | component.throttledTick(); 77 | controllers = component.trackedControllerEls; 78 | assert.ok('c1' in controllers, 'Controller 1 detected'); 79 | assert.ok('c2' in controllers, 'Controller 2 detected'); 80 | done(); 81 | }); 82 | }); 83 | 84 | test('adds recording to IndexedDB', function (done) { 85 | sceneEl.setAttribute('avatar-recorder', 'recordingName', 'foo'); 86 | component.startRecording(); 87 | component.recordingData = {camera: {poses: [{timestamp: 0}], events: []}}; 88 | component.isRecording = true; 89 | component.stopRecording(); 90 | sceneEl.systems.recordingdb.getRecording('foo').then(data => { 91 | assert.shallowDeepEqual(data, component.recordingData); 92 | done(); 93 | }); 94 | }); 95 | }); 96 | 97 | suite('avatar-replayer', function () { 98 | var component; 99 | var sceneEl; 100 | 101 | setup(function (done) { 102 | sceneEl = document.createElement('a-scene'); 103 | sceneEl.addEventListener('componentinitialized', evt => { 104 | if (evt.detail.name !== 'avatar-replayer') { return; } 105 | component = sceneEl.components['avatar-replayer']; 106 | done(); 107 | }); 108 | sceneEl.setAttribute('avatar-replayer', ''); 109 | document.body.appendChild(sceneEl); 110 | }); 111 | 112 | test('sets motion-capture-replayer on camera and controllers', (done) => { 113 | const c1 = document.createElement('a-entity'); 114 | c1.setAttribute('id', 'c1'); 115 | c1.setAttribute('tracked-controls', ''); 116 | sceneEl.appendChild(c1); 117 | const c2 = document.createElement('a-entity'); 118 | c2.setAttribute('id', 'c2'); 119 | c2.setAttribute('tracked-controls', ''); 120 | sceneEl.appendChild(c2); 121 | 122 | sceneEl.addEventListener('camera-set-active', () => { 123 | sceneEl.components['avatar-replayer'].startReplaying({ 124 | camera: {poses: [{timestamp: 0}], events: []}, 125 | c1: {poses: [{timestamp: 0}], events: []}, 126 | c2: {poses: [{timestamp: 0}], events: []} 127 | }); 128 | assert.ok(sceneEl.camera.el.getAttribute('motion-capture-replayer')); 129 | assert.ok(c1.getAttribute('motion-capture-replayer')); 130 | assert.ok(c2.getAttribute('motion-capture-replayer')); 131 | done(); 132 | }); 133 | }); 134 | 135 | test('calls startReplaying on motion-capture-replayer', function (done) { 136 | const c1 = document.createElement('a-entity'); 137 | c1.setAttribute('id', 'c1'); 138 | c1.setAttribute('tracked-controls', ''); 139 | c1.setAttribute('motion-capture-replayer', ''); 140 | const c1StartPlayingSpy = this.sinon.spy(c1.components['motion-capture-replayer'], 141 | 'startReplaying'); 142 | sceneEl.appendChild(c1); 143 | 144 | sceneEl.addEventListener('camera-set-active', () => { 145 | component.startReplaying({ 146 | camera: {poses: [{timestamp: 0}], events: []}, 147 | c1: {poses: [{timestamp: 0}], events: []}, 148 | }); 149 | assert.ok(c1StartPlayingSpy.called); 150 | done(); 151 | }); 152 | }); 153 | }); 154 | 155 | suite('motion-capture-recorder', function () { 156 | var component; 157 | var el; 158 | 159 | setup(function (done) { 160 | el = helpers.entityFactory(); 161 | el.addEventListener('componentinitialized', evt => { 162 | if (evt.detail.name !== 'motion-capture-recorder') { return; } 163 | component = el.components['motion-capture-recorder']; 164 | done(); 165 | }); 166 | el.setAttribute('motion-capture-recorder', ''); 167 | }); 168 | 169 | suite('tick', function () { 170 | test('records poses', function () { 171 | assert.equal(component.recordedPoses.length, 0); 172 | el.setAttribute('position', '1 1 1'); 173 | el.setAttribute('rotation', '90 90 90'); 174 | 175 | component.isRecording = true; 176 | component.tick(100); 177 | 178 | el.setAttribute('position', '2 2 2'); 179 | el.setAttribute('rotation', '0 0 0'); 180 | component.tick(200); 181 | 182 | assert.equal(component.recordedPoses.length, 2); 183 | assert.shallowDeepEqual(component.recordedPoses[0].position, {x: 1, y: 1, z: 1}); 184 | assert.shallowDeepEqual(component.recordedPoses[0].rotation, {x: 90, y: 90, z: 90}); 185 | assert.equal(component.recordedPoses[0].timestamp, 100); 186 | assert.shallowDeepEqual(component.recordedPoses[1].position, {x: 2, y: 2, z: 2}); 187 | assert.shallowDeepEqual(component.recordedPoses[1].rotation, {x: 0, y: 0, z: 0}); 188 | assert.equal(component.recordedPoses[1].timestamp, 200); 189 | }); 190 | 191 | test('does not record pose if not recording', function () { 192 | assert.equal(component.recordedPoses.length, 0); 193 | component.isRecording = false; 194 | component.tick(100); 195 | assert.equal(component.recordedPoses.length, 0); 196 | }); 197 | }); 198 | 199 | suite('recordEvent', function () { 200 | test('records axismove', function (done) { 201 | assert.equal(component.recordedEvents.length, 0); 202 | component.tick(100); 203 | component.isRecording = true; 204 | el.emit('axismove', {id: 'foo', axis: {x: 1, y: 1}, changed: [true, true]}); 205 | setTimeout(() => { 206 | assert.equal(component.recordedEvents.length, 1); 207 | assert.equal(component.recordedEvents[0].name, 'axismove'); 208 | assert.shallowDeepEqual(component.recordedEvents[0].detail, { 209 | id: 'foo', axis: {x: 1, y: 1}, changed: [true, true] 210 | }); 211 | assert.equal(component.recordedEvents[0].timestamp, 100); 212 | done(); 213 | }); 214 | }); 215 | 216 | test('records buttonchanged', function (done) { 217 | assert.equal(component.recordedEvents.length, 0); 218 | component.tick(100); 219 | component.isRecording = true; 220 | el.emit('buttonchanged', {id: 'foo', state: {pressed: true}}); 221 | setTimeout(() => { 222 | assert.equal(component.recordedEvents.length, 1); 223 | assert.equal(component.recordedEvents[0].name, 'buttonchanged'); 224 | assert.shallowDeepEqual(component.recordedEvents[0].detail, 225 | {id: 'foo', state: {pressed: true}}); 226 | assert.equal(component.recordedEvents[0].timestamp, 100); 227 | done(); 228 | }); 229 | }); 230 | 231 | test('records buttonup', function (done) { 232 | assert.equal(component.recordedEvents.length, 0); 233 | component.tick(100); 234 | component.isRecording = true; 235 | el.emit('buttonup', {id: 'foo', state: {pressed: true}}); 236 | setTimeout(() => { 237 | assert.equal(component.recordedEvents.length, 1); 238 | assert.equal(component.recordedEvents[0].name, 'buttonup'); 239 | assert.shallowDeepEqual(component.recordedEvents[0].detail, 240 | {id: 'foo', state: {pressed: true}}); 241 | assert.equal(component.recordedEvents[0].timestamp, 100); 242 | done(); 243 | }); 244 | }); 245 | 246 | test('records buttondown', function (done) { 247 | assert.equal(component.recordedEvents.length, 0); 248 | component.tick(100); 249 | component.isRecording = true; 250 | el.emit('buttondown', {id: 'foo', state: {pressed: true}}); 251 | setTimeout(() => { 252 | assert.equal(component.recordedEvents.length, 1); 253 | assert.equal(component.recordedEvents[0].name, 'buttondown'); 254 | assert.shallowDeepEqual(component.recordedEvents[0].detail, 255 | {id: 'foo', state: {pressed: true}}); 256 | assert.equal(component.recordedEvents[0].timestamp, 100); 257 | done(); 258 | }); 259 | }); 260 | 261 | test('records touchstart', function (done) { 262 | assert.equal(component.recordedEvents.length, 0); 263 | component.tick(100); 264 | component.isRecording = true; 265 | el.emit('touchstart', {id: 'foo', state: {pressed: true}}); 266 | setTimeout(() => { 267 | assert.equal(component.recordedEvents.length, 1); 268 | assert.equal(component.recordedEvents[0].name, 'touchstart'); 269 | assert.shallowDeepEqual(component.recordedEvents[0].detail, 270 | {id: 'foo', state: {pressed: true}}); 271 | assert.equal(component.recordedEvents[0].timestamp, 100); 272 | done(); 273 | }); 274 | }); 275 | 276 | test('records touchend', function (done) { 277 | assert.equal(component.recordedEvents.length, 0); 278 | component.tick(100); 279 | component.isRecording = true; 280 | el.emit('touchend', {id: 'foo', state: {pressed: true}}); 281 | setTimeout(() => { 282 | assert.equal(component.recordedEvents.length, 1); 283 | assert.equal(component.recordedEvents[0].name, 'touchend'); 284 | assert.shallowDeepEqual(component.recordedEvents[0].detail, 285 | {id: 'foo', state: {pressed: true}}); 286 | assert.equal(component.recordedEvents[0].timestamp, 100); 287 | done(); 288 | }); 289 | }); 290 | }); 291 | 292 | suite('startRecording', function () { 293 | test('starts recording', function () { 294 | assert.notOk(component.isRecording); 295 | component.startRecording(); 296 | assert.ok(component.isRecording); 297 | }); 298 | }); 299 | 300 | suite('stopRecording', function () { 301 | test('stops recording', function () { 302 | component.isRecording = true; 303 | component.stopRecording(); 304 | assert.notOk(component.isRecording); 305 | }); 306 | }); 307 | }); 308 | 309 | suite('motion-capture-replayer', function () { 310 | var component; 311 | var el; 312 | 313 | setup(function (done) { 314 | el = helpers.entityFactory(); 315 | el.addEventListener('componentinitialized', evt => { 316 | if (evt.detail.name !== 'motion-capture-replayer') { return; } 317 | component = el.components['motion-capture-replayer']; 318 | done(); 319 | }); 320 | el.setAttribute('motion-capture-replayer', 'loop: false'); 321 | }); 322 | 323 | test('plays poses', function () { 324 | var rotTemp 325 | 326 | assert.shallowDeepEqual(el.getAttribute('position'), {x: 0, y: 0, z: 0}); 327 | assert.shallowDeepEqual(el.getAttribute('rotation'), {x: 0, y: 0, z: 0}); 328 | 329 | component.startReplayingPoses([ 330 | {timestamp: 100, position: '1 1 1', rotation: '90 90 90'}, 331 | {timestamp: 200, position: '2 2 2', rotation: '60 60 60'}, 332 | {timestamp: 250, position: '3 3 3', rotation: '30 30 30'} 333 | ]); 334 | 335 | component.tick(150, 50); 336 | assert.shallowDeepEqual(el.getAttribute('position'), {x: 1, y: 1, z: 1}); 337 | assert.shallowDeepEqual(el.getAttribute('rotation'), {x: 90, y: 90, z: 90}); 338 | 339 | component.tick(200, 50); 340 | assert.shallowDeepEqual(el.getAttribute('position'), {x: 2, y: 2, z: 2}); 341 | rotTemp = el.getAttribute('rotation'); 342 | rotTemp.x = Math.round(rotTemp.x); 343 | rotTemp.y = Math.round(rotTemp.y); 344 | rotTemp.z = Math.round(rotTemp.z); 345 | assert.shallowDeepEqual(rotTemp, {x: 60, y: 60, z: 60}); 346 | 347 | component.tick(300, 100); 348 | assert.shallowDeepEqual(el.getAttribute('position'), {x: 3, y: 3, z: 3}); 349 | rotTemp = el.getAttribute('rotation'); 350 | rotTemp.x = Math.round(rotTemp.x); 351 | rotTemp.y = Math.round(rotTemp.y); 352 | rotTemp.z = Math.round(rotTemp.z); 353 | assert.shallowDeepEqual(rotTemp, {x: 30, y: 30, z: 30}); 354 | }); 355 | 356 | test('plays events', function (done) { 357 | el.addEventListener('buttondown', function (evt) { 358 | assert.equal(evt.detail.id, 'foo'); 359 | assert.ok(evt.detail.state); 360 | setTimeout(() => { 361 | component.tick(200, 100); 362 | }); 363 | }); 364 | 365 | el.addEventListener('axismove', function (evt) { 366 | assert.equal(evt.detail.id, 'bar'); 367 | assert.equal(evt.detail.axis.x, 1); 368 | assert.equal(evt.detail.axis.y, 1); 369 | setTimeout(() => { 370 | component.tick(250, 50); 371 | }); 372 | }); 373 | 374 | el.addEventListener('touchend', function (evt) { 375 | assert.equal(evt.detail.id, 'baz'); 376 | assert.ok(evt.detail.state); 377 | done(); 378 | }); 379 | 380 | component.startReplayingEvents([ 381 | {timestamp: 100, name: 'buttondown', detail: {id: 'foo', state: {pressed: true}}}, 382 | {timestamp: 200, name: 'axismove', detail: {id: 'bar', axis: {x: 1, y: 1}}}, 383 | {timestamp: 250, name: 'touchend', detail: {id: 'baz', state: {pressed: true}}} 384 | ]); 385 | component.tick(150, 50); 386 | }); 387 | }); 388 | 389 | suite('motion-capture-replayer system', function () { 390 | var el; 391 | 392 | setup(function (done) { 393 | el = helpers.entityFactory(); 394 | el.addEventListener('componentinitialized', evt => { 395 | if (evt.detail.name !== 'motion-capture-replayer') { return; } 396 | component = el.components['motion-capture-replayer']; 397 | setTimeout(() => { done(); }, 50); 398 | }); 399 | el.setAttribute('motion-capture-replayer', 'loop: false'); 400 | }); 401 | 402 | test('injects tracked-controls', function (done) { 403 | assert.equal(el.sceneEl.systems['tracked-controls'].controllers.length, 0); 404 | 405 | el.sceneEl.addEventListener('controllersupdated', () => { 406 | assert.equal(el.sceneEl.systems['motion-capture-replayer'].gamepads.length, 1); 407 | assert.equal(el.sceneEl.systems['tracked-controls'].controllers.length, 1); 408 | assert.equal(el.sceneEl.systems['tracked-controls'].controllers[0].id, 409 | 'OpenVR Controller'); 410 | done(); 411 | }); 412 | 413 | el.components['motion-capture-replayer'].startReplaying({ 414 | gamepad: {id: 'OpenVR Controller', index: 1, hand: 'left'}, 415 | poses: [{timestamp: 100, position: '1 1 1', rotation: '90 90 90'}], 416 | events: [] 417 | }); 418 | }); 419 | }); 420 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | var PLUGINS = []; 4 | if (process.env.NODE_ENV === 'production') { 5 | PLUGINS.push(new webpack.optimize.UglifyJsPlugin()); 6 | } 7 | 8 | module.exports = { 9 | devServer: { 10 | disableHostCheck: true 11 | }, 12 | entry: './src/index.js', 13 | output: { 14 | path: __dirname, 15 | filename: './examples/js/build.js' 16 | }, 17 | plugins: PLUGINS 18 | }; 19 | --------------------------------------------------------------------------------