├── .github └── FUNDING.yml ├── .gitignore ├── .jshintrc ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dist ├── aframe-extras.controls.js ├── aframe-extras.controls.js.map ├── aframe-extras.controls.min.js ├── aframe-extras.controls.min.js.map ├── aframe-extras.js ├── aframe-extras.js.map ├── aframe-extras.loaders.js ├── aframe-extras.loaders.js.map ├── aframe-extras.loaders.min.js ├── aframe-extras.loaders.min.js.map ├── aframe-extras.min.js ├── aframe-extras.min.js.map ├── aframe-extras.misc.js ├── aframe-extras.misc.js.map ├── aframe-extras.misc.min.js ├── aframe-extras.misc.min.js.map ├── aframe-extras.pathfinding.js ├── aframe-extras.pathfinding.js.map ├── aframe-extras.pathfinding.min.js ├── aframe-extras.pathfinding.min.js.map ├── aframe-extras.primitives.js ├── aframe-extras.primitives.js.map ├── aframe-extras.primitives.min.js ├── aframe-extras.primitives.min.js.map └── components │ ├── grab.js │ ├── grab.js.map │ ├── grab.min.js │ ├── grab.min.js.map │ ├── sphere-collider.js │ ├── sphere-collider.js.map │ ├── sphere-collider.min.js │ └── sphere-collider.min.js.map ├── examples ├── animation-controls │ ├── animation-controls.js │ ├── index.html │ └── styles.css ├── animation │ └── index.html ├── assets │ ├── 3-dreams-of-black │ │ ├── flamingo.js │ │ └── wolf.json │ ├── castle │ │ ├── Castle-navmesh.glb │ │ └── Castle.glb │ ├── environment │ │ └── Park2 │ │ │ ├── README.md │ │ │ ├── negx.jpg │ │ │ ├── negy.jpg │ │ │ ├── negz.jpg │ │ │ ├── posx.jpg │ │ │ ├── posy.jpg │ │ │ └── posz.jpg │ ├── grid.png │ ├── island-hut │ │ ├── island-hut.bake.ply │ │ ├── island-hut.mtl │ │ ├── island-hut.obj │ │ └── island-hut.png │ ├── monu9 │ │ ├── README.md │ │ ├── monu9.mtl │ │ ├── monu9.obj │ │ └── monu9.png │ ├── rupee │ │ ├── README.md │ │ └── rupee.glb │ ├── scythian │ │ ├── README.md │ │ ├── scythian.bake.ply │ │ ├── scythian.fbm │ │ │ └── scythian.png │ │ ├── scythian.fbx │ │ ├── scythian.json │ │ ├── scythian.mtl │ │ ├── scythian.obj │ │ └── scythian.png │ ├── threejs-examples │ │ ├── README.md │ │ └── scene-animation.json │ └── trex │ │ └── TRex.glb ├── castle │ └── index.html ├── checkpoints │ └── index.html ├── env-map │ └── index.html ├── fbx-model │ └── index.html ├── index.html ├── island │ └── index.html ├── normalize.css ├── skeleton.css ├── tubes │ └── index.html └── vive │ └── index.html ├── index.js ├── lib ├── GamepadButton.js ├── GamepadButtonEvent.js ├── fetch-script.js └── keyboard.polyfill.js ├── package-lock.json ├── package.json ├── src ├── controls │ ├── README.md │ ├── checkpoint-controls.js │ ├── gamepad-controls.js │ ├── index.js │ ├── keyboard-controls.js │ ├── movement-controls.js │ ├── nipple-controls.js │ ├── touch-controls.js │ └── trackpad-controls.js ├── loaders │ ├── README.md │ ├── animation-mixer.js │ ├── collada-model-legacy.js │ ├── fbx-model.js │ ├── gltf-model-legacy.js │ ├── index.js │ └── object-model.js ├── misc │ ├── README.md │ ├── checkpoint.js │ ├── cube-env-map.js │ ├── grab.js │ ├── index.js │ ├── normal-material.js │ └── sphere-collider.js ├── pathfinding │ ├── README.md │ ├── index.js │ ├── nav-agent.js │ ├── nav-mesh.js │ └── system.js └── primitives │ ├── README.md │ ├── a-grid.js │ ├── a-ocean.js │ ├── a-tube.js │ └── index.js ├── tests ├── .jshintrc ├── __init.test.js ├── controls │ └── keyboard-controls.test.js ├── helpers.js ├── karma.conf.js └── misc │ └── sphere-collider.test.js └── webpack.config.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: vincentfretin 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | build 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "THREE": false, 4 | "AFRAME": false, 5 | "window": false, 6 | "document": false, 7 | "require": false, 8 | "console": false, 9 | "module": false 10 | }, 11 | "bitwise": false, 12 | "browser": true, 13 | "eqeqeq": true, 14 | "esnext": true, 15 | "expr": true, 16 | "forin": true, 17 | "immed": true, 18 | "latedef": "nofunc", 19 | "laxbreak": true, 20 | "maxlen": 100, 21 | "newcap": true, 22 | "noarg": true, 23 | "noempty": true, 24 | "noyield": true, 25 | "quotmark": "single", 26 | "smarttabs": false, 27 | "trailing": true, 28 | "undef": true, 29 | "unused": true, 30 | "white": false 31 | } 32 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build 2 | examples 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v4.2.0 4 | 5 | * **movement-controls** Added support for setting rig rotation. Fixed bug with `speed` implementation. 6 | * **nav** Improved nav-mesh load handling, renamed `nav-start` and `nav-end` events to `navigation-start` and `navigation-end`. 7 | * **animation-mixer** Allows property updates while playing, added `timeScale` property. 8 | * **touch-controls** Support two-finger touch to move backwards. 9 | * **fbx-model** Updated to FBXLoader r96. 10 | 11 | ## v4.1.0 12 | 13 | * **movement-controls** — Replace acceleration/easing with `speed` property. 14 | 15 | ## v4.0.0 16 | 17 | * Added CHANGELOG.md 18 | * Removed `universal-controls`, replacing with `movement-controls`. In contrast to previous releases, `movement-controls` is intended to be used _with_ the default `look-controls` component. It adds several locomotion methods, and can be extended to include more, replacing `wasd-controls`. See [documentation](https://github.com/c-frame/aframe-extras/tree/v4.0.0/src/controls). 19 | * Added navmesh support to `movement-controls`. 20 | * Removed physics. Instead, include it separately via [aframe-physics-system](https://github.com/c-frame/aframe-physics-system). 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Don McCurdy 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 | # A-Frame Extras 2 | 3 | [![Latest NPM release](https://img.shields.io/npm/v/aframe-extras.svg)](https://www.npmjs.com/package/aframe-extras) 4 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/c-frame/aframe-extras/master/LICENSE) 5 | 6 | Add-ons and helpers for A-Frame VR. 7 | 8 | Includes components for controls, model loaders, pathfinding, and more: 9 | 10 | 11 |
12 | src
13 | ├── controls/ (Documentation)
14 | │   ├── movement-controls.js
15 | │   ├── checkpoint-controls.js
16 | │   ├── gamepad-controls.js
17 | │   ├── keyboard-controls.js
18 | │   ├── touch-controls.js
19 | │   └── trackpad-controls.js
20 | ├── loaders/ (Documentation)
21 | │   ├── animation-mixer.js
22 | │   ├── collada-model-legacy.js
23 | │   ├── fbx-model.js
24 | │   ├── gltf-model-legacy.js
25 | │   └── object-model.js
26 | ├── misc/ (Documentation)
27 | │   ├── checkpoint.js
28 | │   ├── cube-env-map.js
29 | │   ├── grab.js
30 | │   ├── mesh-smooth.js
31 | │   ├── normal-material.js
32 | │   └── sphere-collider.js
33 | ├── pathfinding/ (Documentation)
34 | │   ├── nav-mesh.js
35 | │   └── nav-agent.js
36 | └── primitives/ (Documentation)
37 |     ├── a-grid.js
38 |     ├── a-ocean.js
39 |     └── a-tube.js
40 | 
41 | 42 | ## Usage (Scripts) 43 | 44 | In the [dist/](https://github.com/c-frame/aframe-extras/tree/master/dist) folder, download any package(s) you need. Include the scripts on your page, and all components are automatically registered for you: 45 | 46 | ```html 47 | 48 | ``` 49 | 50 | replace `7.5.4` by another tag or a commit hash (for example `3e0ab50`) if you want to use a build from master branch. 51 | You can [look at the commits](https://github.com/c-frame/aframe-extras/commits/master) and use the latest commit hash. 52 | 53 | For partial builds, use a subpackage like `aframe-extras.controls.min.js`. Full list of packages above. 54 | 55 | **A-Frame Version Compatibility** 56 | 57 | | A-Frame | Extras | 58 | |----------|--------| 59 | | v1.4.0 | v7.0.0 | 60 | | v1.3.0 | v7.0.0 | 61 | | v1.2.0 | v7.0.0 | 62 | | v1.1.0 | v6.1.1 | 63 | 64 | > **NOTE:** Several components and examples also rely on [aframe-physics-system](https://github.com/c-frame/aframe-physics-system). 65 | 66 | ## Usage (NPM) 67 | 68 | ``` 69 | npm install --save aframe-extras 70 | ``` 71 | 72 | ```javascript 73 | // index.js 74 | import 'aframe-extras'; 75 | // or specific packages 76 | import "aframe-extras/controls/index.js"; 77 | import "aframe-extras/pathfinding/index.js"; 78 | ``` 79 | 80 | Once installed, you'll need to compile your JavaScript using something like [webpack](https://webpack.js.org) with three defined as external, see webpack.config.js in this repo for an example. 81 | 82 | ## Examples 83 | 84 | A live set of usage examples can be found here: 85 | 86 | https://c-frame.github.io/aframe-extras/examples/ 87 | 88 | ## Deprecated Components 89 | 90 | The following components existed in previous versions of A-Frame Extras, but have been removed as of the latest release 91 | 92 | | Component | Removed in | Reasons | 93 | | ---------------- | ---------- | ------------------------------------------------------------ | 94 | | `kinematic-body` | 7.0.0 | Using physics for movement is unstable and performs poorly. When preventing players from passing through obstacles, use a navigation mesh instead whenever possible.

The `kinematic-body` component constrainted player movement using physics, and depended on [aframe-physics-system](http://github.com/c-frame/aframe-physics-system). Using physics for locomotion is not VR-friendly, and often glitchy even for traditional 3D experiences. [Use a navigation mesh](https://github.com/c-frame/aframe-extras/tree/master/src/controls#usage) instead, whenever possible. | 95 | | `jump-ability` | 7.0.0 | Dependent on `kinematic-body` | 96 | | `a-hexgrid` | 7.0.0 | Was based on [this repo](https://github.com/vonWolfehaus/von-grid), which is no longer maintained, and does not work with recent versions of THREE.js. | 97 | | `mesh-smooth` | 7.0.0 | Intended for JSON models, but the JSON Loader is [no longer part of this repo](https://github.com/c-frame/aframe-extras/commit/d079064e6ac55a4cd6bbf64bd46a576e26dd214e). More background [here](https://github.com/c-frame/aframe-extras/issues/411). | 98 | 99 | -------------------------------------------------------------------------------- /dist/aframe-extras.misc.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){if("object"==typeof exports&&"object"==typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var i=e();for(var s in i)("object"==typeof exports?exports:t)[s]=i[s]}}(self,(()=>(()=>{var t={95:t=>{t.exports=AFRAME.registerComponent("checkpoint",{schema:{offset:{default:{x:0,y:0,z:0},type:"vec3"}},init:function(){this.active=!1,this.targetEl=null,this.fire=this.fire.bind(this),this.offset=new THREE.Vector3},update:function(){this.offset.copy(this.data.offset)},play:function(){this.el.addEventListener("click",this.fire)},pause:function(){this.el.removeEventListener("click",this.fire)},remove:function(){this.pause()},fire:function(){const t=this.el.sceneEl.querySelector("[checkpoint-controls]");if(!t)throw new Error("No `checkpoint-controls` component found.");t.components["checkpoint-controls"].setCheckpoint(this.el)},getOffset:function(){return this.offset.copy(this.data.offset)}})},889:t=>{function e(t,e,i,s){t&&(e=e||[],t.traverse((t=>{var n;t.isMesh&&((n=t.material)?Array.isArray(n)?n:n.materials?n.materials:[n]:[]).forEach((t=>{t&&!("envMap"in t)||e.length&&-1===e.indexOf(t.name)||(t.envMap=i,t.reflectivity=s,t.needsUpdate=!0)}))})))}t.exports=AFRAME.registerComponent("cube-env-map",{multiple:!0,schema:{path:{default:""},extension:{default:"jpg",oneOf:["jpg","png"]},enableBackground:{default:!1},reflectivity:{default:1,min:0,max:1},materials:{default:[]}},init:function(){const t=this.data;this.texture=(new THREE.CubeTextureLoader).load([t.path+"posx."+t.extension,t.path+"negx."+t.extension,t.path+"posy."+t.extension,t.path+"negy."+t.extension,t.path+"posz."+t.extension,t.path+"negz."+t.extension]),this.texture.format=THREE.RGBAFormat,this.object3dsetHandler=()=>{const t=this.el.getObject3D("mesh"),i=this.data;e(t,i.materials,this.texture,i.reflectivity)},this.object3dsetHandler(),this.el.addEventListener("object3dset",this.object3dsetHandler)},update:function(t){const i=this.data,s=this.el.getObject3D("mesh");let n=[],r=[];if(i.materials.length&&(t.materials?(n=i.materials.filter((e=>!t.materials.includes(e))),r=t.materials.filter((t=>!i.materials.includes(t)))):n=i.materials),n.length&&e(s,n,this.texture,i.reflectivity),r.length&&e(s,r,null,1),t.materials&&i.reflectivity!==t.reflectivity){const n=i.materials.filter((e=>t.materials.includes(e)));n.length&&e(s,n,this.texture,i.reflectivity)}this.data.enableBackground&&!t.enableBackground?this.setBackground(this.texture):!this.data.enableBackground&&t.enableBackground&&this.setBackground(null)},remove:function(){this.el.removeEventListener("object3dset",this.object3dsetHandler);const t=this.el.getObject3D("mesh"),i=this.data;e(t,i.materials,null,1),i.enableBackground&&this.setBackground(null)},setBackground:function(t){this.el.sceneEl.object3D.background=t}})},771:t=>{t.exports=AFRAME.registerComponent("grab",{init:function(){this.system=this.el.sceneEl.systems.physics,this.GRABBED_STATE="grabbed",this.grabbing=!1,this.hitEl=null,this.physics=this.el.sceneEl.systems.physics,this.constraint=null,this.onHit=this.onHit.bind(this),this.onGripOpen=this.onGripOpen.bind(this),this.onGripClose=this.onGripClose.bind(this)},play:function(){const t=this.el;t.addEventListener("hit",this.onHit),t.addEventListener("gripdown",this.onGripClose),t.addEventListener("gripup",this.onGripOpen),t.addEventListener("trackpaddown",this.onGripClose),t.addEventListener("trackpadup",this.onGripOpen),t.addEventListener("triggerdown",this.onGripClose),t.addEventListener("triggerup",this.onGripOpen)},pause:function(){const t=this.el;t.removeEventListener("hit",this.onHit),t.removeEventListener("gripdown",this.onGripClose),t.removeEventListener("gripup",this.onGripOpen),t.removeEventListener("trackpaddown",this.onGripClose),t.removeEventListener("trackpadup",this.onGripOpen),t.removeEventListener("triggerdown",this.onGripClose),t.removeEventListener("triggerup",this.onGripOpen)},onGripClose:function(){this.grabbing=!0},onGripOpen:function(){const t=this.hitEl;this.grabbing=!1,t&&(t.removeState(this.GRABBED_STATE),this.hitEl=void 0,this.system.removeConstraint(this.constraint),this.constraint=null)},onHit:function(t){const e=t.detail.el;e.is(this.GRABBED_STATE)||!this.grabbing||this.hitEl||(e.addState(this.GRABBED_STATE),this.hitEl=e,this.constraint=new CANNON.LockConstraint(this.el.body,e.body),this.system.addConstraint(this.constraint))}})},778:t=>{t.exports=AFRAME.registerComponent("normal-material",{init:function(){this.material=new THREE.MeshNormalMaterial({flatShading:!0}),this.applyMaterial=this.applyMaterial.bind(this),this.el.addEventListener("object3dset",this.applyMaterial),this.applyMaterial()},remove:function(){this.el.removeEventListener("object3dset",this.applyMaterial)},applyMaterial:function(){this.el.object3D.traverse((t=>{t.isMesh&&(t.material=this.material)}))}})},109:t=>{t.exports=AFRAME.registerComponent("sphere-collider",{schema:{enabled:{default:!0},interval:{default:80},objects:{default:""},state:{default:"collided"},radius:{default:.05},watch:{default:!0}},init:function(){this.observer=null,this.els=[],this.collisions=[],this.prevCheckTime=void 0,this.eventDetail={},this.handleHit=this.handleHit.bind(this),this.handleHitEnd=this.handleHitEnd.bind(this)},play:function(){const t=this.el.sceneEl;this.data.watch&&(this.observer=new MutationObserver(this.update.bind(this,null)),this.observer.observe(t,{childList:!0,subtree:!0}))},pause:function(){this.observer&&(this.observer.disconnect(),this.observer=null)},update:function(){const t=this.data;let e;e=t.objects?this.el.sceneEl.querySelectorAll(t.objects):this.el.sceneEl.children,this.els=Array.prototype.slice.call(e)},tick:function(){const t=new THREE.Vector3,e=new THREE.Vector3,i=new THREE.Vector3,s=new THREE.Vector3,n=new THREE.Box3,r=[],o=new Map;return function(a){if(!this.data.enabled)return;const h=this.prevCheckTime;if(h&&a-ho.get(t)>o.get(e)?1:-1)).forEach(this.handleHit),this.collisions.filter((t=>!o.has(t))).forEach(this.handleHitEnd),function(t,e){t.length=0;for(let i=0;i { 11 | return /******/ (() => { // webpackBootstrap 12 | /******/ var __webpack_modules__ = ({ 13 | 14 | /***/ "./src/primitives/a-grid.js": 15 | /*!**********************************!*\ 16 | !*** ./src/primitives/a-grid.js ***! 17 | \**********************************/ 18 | /***/ ((module) => { 19 | 20 | /** 21 | * Flat grid. 22 | * 23 | * Defaults to 75x75. 24 | */ 25 | module.exports = AFRAME.registerPrimitive('a-grid', { 26 | defaultComponents: { 27 | geometry: { 28 | primitive: 'plane', 29 | width: 75, 30 | height: 75 31 | }, 32 | rotation: {x: -90, y: 0, z: 0}, 33 | material: { 34 | src: 'url(https://cdn.jsdelivr.net/gh/donmccurdy/aframe-extras@v1.16.3/assets/grid.png)', 35 | repeat: '75 75' 36 | } 37 | }, 38 | mappings: { 39 | width: 'geometry.width', 40 | height: 'geometry.height', 41 | src: 'material.src' 42 | } 43 | }); 44 | 45 | 46 | /***/ }), 47 | 48 | /***/ "./src/primitives/a-ocean.js": 49 | /*!***********************************!*\ 50 | !*** ./src/primitives/a-ocean.js ***! 51 | \***********************************/ 52 | /***/ ((module) => { 53 | 54 | /** 55 | * Flat-shaded ocean primitive. 56 | * 57 | * Based on a Codrops tutorial: 58 | * http://tympanus.net/codrops/2016/04/26/the-aviator-animating-basic-3d-scene-threejs/ 59 | */ 60 | module.exports.Primitive = AFRAME.registerPrimitive('a-ocean', { 61 | defaultComponents: { 62 | ocean: {}, 63 | rotation: {x: -90, y: 0, z: 0} 64 | }, 65 | mappings: { 66 | width: 'ocean.width', 67 | depth: 'ocean.depth', 68 | density: 'ocean.density', 69 | amplitude: 'ocean.amplitude', 70 | amplitudeVariance: 'ocean.amplitudeVariance', 71 | speed: 'ocean.speed', 72 | speedVariance: 'ocean.speedVariance', 73 | color: 'ocean.color', 74 | opacity: 'ocean.opacity' 75 | } 76 | }); 77 | 78 | module.exports.Component = AFRAME.registerComponent('ocean', { 79 | schema: { 80 | // Dimensions of the ocean area. 81 | width: {default: 10, min: 0}, 82 | depth: {default: 10, min: 0}, 83 | 84 | // Density of waves. 85 | density: {default: 10}, 86 | 87 | // Wave amplitude and variance. 88 | amplitude: {default: 0.1}, 89 | amplitudeVariance: {default: 0.3}, 90 | 91 | // Wave speed and variance. 92 | speed: {default: 1}, 93 | speedVariance: {default: 2}, 94 | 95 | // Material. 96 | color: {default: '#7AD2F7', type: 'color'}, 97 | opacity: {default: 0.8} 98 | }, 99 | 100 | /** 101 | * Use play() instead of init(), because component mappings – unavailable as dependencies – are 102 | * not guaranteed to have parsed when this component is initialized. 103 | */ 104 | play: function () { 105 | const el = this.el; 106 | const data = this.data; 107 | let material = el.components.material; 108 | 109 | const geometry = new THREE.PlaneGeometry(data.width, data.depth, data.density, data.density); 110 | this.waves = []; 111 | const posAttribute = geometry.getAttribute('position'); 112 | for (let i = 0; i < posAttribute.count; i++) { 113 | this.waves.push({ 114 | z: posAttribute.getZ(i), 115 | ang: Math.random() * Math.PI * 2, 116 | amp: data.amplitude + Math.random() * data.amplitudeVariance, 117 | speed: (data.speed + Math.random() * data.speedVariance) / 1000 // radians / frame 118 | }); 119 | } 120 | 121 | if (!material) { 122 | material = {}; 123 | material.material = new THREE.MeshPhongMaterial({ 124 | color: data.color, 125 | transparent: data.opacity < 1, 126 | opacity: data.opacity, 127 | flatShading: true, 128 | }); 129 | } 130 | 131 | this.mesh = new THREE.Mesh(geometry, material.material); 132 | el.setObject3D('mesh', this.mesh); 133 | }, 134 | 135 | remove: function () { 136 | this.el.removeObject3D('mesh'); 137 | }, 138 | 139 | tick: function (t, dt) { 140 | if (!dt) return; 141 | 142 | const posAttribute = this.mesh.geometry.getAttribute('position'); 143 | for (let i = 0; i < posAttribute.count; i++){ 144 | const vprops = this.waves[i]; 145 | const value = vprops.z + Math.sin(vprops.ang) * vprops.amp; 146 | posAttribute.setZ(i, value); 147 | vprops.ang += vprops.speed * dt; 148 | } 149 | posAttribute.needsUpdate = true; 150 | } 151 | }); 152 | 153 | 154 | /***/ }), 155 | 156 | /***/ "./src/primitives/a-tube.js": 157 | /*!**********************************!*\ 158 | !*** ./src/primitives/a-tube.js ***! 159 | \**********************************/ 160 | /***/ ((module) => { 161 | 162 | /** 163 | * Tube following a custom path. 164 | * 165 | * Usage: 166 | * 167 | * ```html 168 | * 169 | * ``` 170 | */ 171 | module.exports.Primitive = AFRAME.registerPrimitive('a-tube', { 172 | defaultComponents: { 173 | tube: {}, 174 | }, 175 | mappings: { 176 | path: 'tube.path', 177 | segments: 'tube.segments', 178 | radius: 'tube.radius', 179 | 'radial-segments': 'tube.radialSegments', 180 | closed: 'tube.closed' 181 | } 182 | }); 183 | 184 | module.exports.Component = AFRAME.registerComponent('tube', { 185 | schema: { 186 | path: {default: []}, 187 | segments: {default: 64}, 188 | radius: {default: 1}, 189 | radialSegments: {default: 8}, 190 | closed: {default: false} 191 | }, 192 | 193 | init: function () { 194 | const el = this.el, 195 | data = this.data; 196 | let material = el.components.material; 197 | 198 | if (!data.path.length) { 199 | console.error('[a-tube] `path` property expected but not found.'); 200 | return; 201 | } 202 | 203 | const curve = new THREE.CatmullRomCurve3(data.path.map(function (point) { 204 | point = point.split(' '); 205 | return new THREE.Vector3(Number(point[0]), Number(point[1]), Number(point[2])); 206 | })); 207 | const geometry = new THREE.TubeGeometry( 208 | curve, data.segments, data.radius, data.radialSegments, data.closed 209 | ); 210 | 211 | if (!material) { 212 | material = {}; 213 | material.material = new THREE.MeshPhongMaterial(); 214 | } 215 | 216 | this.mesh = new THREE.Mesh(geometry, material.material); 217 | this.el.setObject3D('mesh', this.mesh); 218 | }, 219 | 220 | update: function (prevData) { 221 | if (!Object.keys(prevData).length) return; 222 | 223 | this.remove(); 224 | this.init(); 225 | }, 226 | 227 | remove: function () { 228 | if (this.mesh) this.el.removeObject3D('mesh'); 229 | } 230 | }); 231 | 232 | 233 | /***/ }) 234 | 235 | /******/ }); 236 | /************************************************************************/ 237 | /******/ // The module cache 238 | /******/ var __webpack_module_cache__ = {}; 239 | /******/ 240 | /******/ // The require function 241 | /******/ function __webpack_require__(moduleId) { 242 | /******/ // Check if module is in cache 243 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 244 | /******/ if (cachedModule !== undefined) { 245 | /******/ return cachedModule.exports; 246 | /******/ } 247 | /******/ // Create a new module (and put it into the cache) 248 | /******/ var module = __webpack_module_cache__[moduleId] = { 249 | /******/ // no module.id needed 250 | /******/ // no module.loaded needed 251 | /******/ exports: {} 252 | /******/ }; 253 | /******/ 254 | /******/ // Execute the module function 255 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 256 | /******/ 257 | /******/ // Return the exports of the module 258 | /******/ return module.exports; 259 | /******/ } 260 | /******/ 261 | /************************************************************************/ 262 | var __webpack_exports__ = {}; 263 | // This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. 264 | (() => { 265 | /*!*********************************!*\ 266 | !*** ./src/primitives/index.js ***! 267 | \*********************************/ 268 | __webpack_require__(/*! ./a-grid */ "./src/primitives/a-grid.js"); 269 | __webpack_require__(/*! ./a-ocean */ "./src/primitives/a-ocean.js"); 270 | __webpack_require__(/*! ./a-tube */ "./src/primitives/a-tube.js"); 271 | 272 | })(); 273 | 274 | /******/ return __webpack_exports__; 275 | /******/ })() 276 | ; 277 | }); 278 | //# sourceMappingURL=aframe-extras.primitives.js.map -------------------------------------------------------------------------------- /dist/aframe-extras.primitives.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"aframe-extras.primitives.js","mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;;;;;;;;ACVA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK;AACL,eAAe,mBAAmB;AAClC;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;AACA;AACA,CAAC;;;;;;;;;;;ACvBD;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB;AACxB;AACA,aAAa;AACb,eAAe;AACf,GAAG;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;;AAED,wBAAwB;AACxB;AACA;AACA,YAAY,oBAAoB;AAChC,YAAY,oBAAoB;;AAEhC;AACA,cAAc,YAAY;;AAE1B;AACA,gBAAgB,aAAa;AAC7B,wBAAwB,aAAa;;AAErC;AACA,YAAY,WAAW;AACvB,oBAAoB,WAAW;;AAE/B;AACA,YAAY,kCAAkC;AAC9C,cAAc;AACd,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,oBAAoB,wBAAwB;AAC5C;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO;AACP;;AAEA;AACA;AACA,GAAG;;AAEH;AACA;AACA,GAAG;;AAEH;AACA;;AAEA;AACA,oBAAoB,wBAAwB;AAC5C;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;;;;;;;;;;;ACjGD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,wBAAwB;AACxB;AACA,sBAAsB;AACtB,GAAG;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;;AAED,wBAAwB;AACxB;AACA,qBAAqB,YAAY;AACjC,qBAAqB,YAAY;AACjC,qBAAqB,WAAW;AAChC,qBAAqB,WAAW;AAChC,qBAAqB;AACrB,GAAG;;AAEH;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,KAAK;AACL;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA,GAAG;;AAEH;AACA;;AAEA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA,CAAC;;;;;;;UCpED;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;;;;;;ACtBA,mBAAO,CAAC,4CAAU;AAClB,mBAAO,CAAC,8CAAW;AACnB,mBAAO,CAAC,4CAAU","sources":["webpack://aframe-extras/webpack/universalModuleDefinition","webpack://aframe-extras/./src/primitives/a-grid.js","webpack://aframe-extras/./src/primitives/a-ocean.js","webpack://aframe-extras/./src/primitives/a-tube.js","webpack://aframe-extras/webpack/bootstrap","webpack://aframe-extras/./src/primitives/index.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse {\n\t\tvar a = factory();\n\t\tfor(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];\n\t}\n})(self, () => {\nreturn ","/**\n * Flat grid.\n *\n * Defaults to 75x75.\n */\nmodule.exports = AFRAME.registerPrimitive('a-grid', {\n defaultComponents: {\n geometry: {\n primitive: 'plane',\n width: 75,\n height: 75\n },\n rotation: {x: -90, y: 0, z: 0},\n material: {\n src: 'url(https://cdn.jsdelivr.net/gh/donmccurdy/aframe-extras@v1.16.3/assets/grid.png)',\n repeat: '75 75'\n }\n },\n mappings: {\n width: 'geometry.width',\n height: 'geometry.height',\n src: 'material.src'\n }\n});\n","/**\n * Flat-shaded ocean primitive.\n *\n * Based on a Codrops tutorial:\n * http://tympanus.net/codrops/2016/04/26/the-aviator-animating-basic-3d-scene-threejs/\n */\nmodule.exports.Primitive = AFRAME.registerPrimitive('a-ocean', {\n defaultComponents: {\n ocean: {},\n rotation: {x: -90, y: 0, z: 0}\n },\n mappings: {\n width: 'ocean.width',\n depth: 'ocean.depth',\n density: 'ocean.density',\n amplitude: 'ocean.amplitude',\n amplitudeVariance: 'ocean.amplitudeVariance',\n speed: 'ocean.speed',\n speedVariance: 'ocean.speedVariance',\n color: 'ocean.color',\n opacity: 'ocean.opacity'\n }\n});\n\nmodule.exports.Component = AFRAME.registerComponent('ocean', {\n schema: {\n // Dimensions of the ocean area.\n width: {default: 10, min: 0},\n depth: {default: 10, min: 0},\n\n // Density of waves.\n density: {default: 10},\n\n // Wave amplitude and variance.\n amplitude: {default: 0.1},\n amplitudeVariance: {default: 0.3},\n\n // Wave speed and variance.\n speed: {default: 1},\n speedVariance: {default: 2},\n\n // Material.\n color: {default: '#7AD2F7', type: 'color'},\n opacity: {default: 0.8}\n },\n\n /**\n * Use play() instead of init(), because component mappings – unavailable as dependencies – are\n * not guaranteed to have parsed when this component is initialized.\n */\n play: function () {\n const el = this.el;\n const data = this.data;\n let material = el.components.material;\n\n const geometry = new THREE.PlaneGeometry(data.width, data.depth, data.density, data.density);\n this.waves = [];\n const posAttribute = geometry.getAttribute('position');\n for (let i = 0; i < posAttribute.count; i++) {\n this.waves.push({\n z: posAttribute.getZ(i),\n ang: Math.random() * Math.PI * 2,\n amp: data.amplitude + Math.random() * data.amplitudeVariance,\n speed: (data.speed + Math.random() * data.speedVariance) / 1000 // radians / frame\n });\n }\n\n if (!material) {\n material = {};\n material.material = new THREE.MeshPhongMaterial({\n color: data.color,\n transparent: data.opacity < 1,\n opacity: data.opacity,\n flatShading: true,\n });\n }\n\n this.mesh = new THREE.Mesh(geometry, material.material);\n el.setObject3D('mesh', this.mesh);\n },\n\n remove: function () {\n this.el.removeObject3D('mesh');\n },\n\n tick: function (t, dt) {\n if (!dt) return;\n\n const posAttribute = this.mesh.geometry.getAttribute('position');\n for (let i = 0; i < posAttribute.count; i++){\n const vprops = this.waves[i];\n const value = vprops.z + Math.sin(vprops.ang) * vprops.amp;\n posAttribute.setZ(i, value);\n vprops.ang += vprops.speed * dt;\n }\n posAttribute.needsUpdate = true;\n }\n});\n","/**\n * Tube following a custom path.\n *\n * Usage:\n *\n * ```html\n * \n * ```\n */\nmodule.exports.Primitive = AFRAME.registerPrimitive('a-tube', {\n defaultComponents: {\n tube: {},\n },\n mappings: {\n path: 'tube.path',\n segments: 'tube.segments',\n radius: 'tube.radius',\n 'radial-segments': 'tube.radialSegments',\n closed: 'tube.closed'\n }\n});\n\nmodule.exports.Component = AFRAME.registerComponent('tube', {\n schema: {\n path: {default: []},\n segments: {default: 64},\n radius: {default: 1},\n radialSegments: {default: 8},\n closed: {default: false}\n },\n\n init: function () {\n const el = this.el,\n data = this.data;\n let material = el.components.material;\n\n if (!data.path.length) {\n console.error('[a-tube] `path` property expected but not found.');\n return;\n }\n\n const curve = new THREE.CatmullRomCurve3(data.path.map(function (point) {\n point = point.split(' ');\n return new THREE.Vector3(Number(point[0]), Number(point[1]), Number(point[2]));\n }));\n const geometry = new THREE.TubeGeometry(\n curve, data.segments, data.radius, data.radialSegments, data.closed\n );\n\n if (!material) {\n material = {};\n material.material = new THREE.MeshPhongMaterial();\n }\n\n this.mesh = new THREE.Mesh(geometry, material.material);\n this.el.setObject3D('mesh', this.mesh);\n },\n\n update: function (prevData) {\n if (!Object.keys(prevData).length) return;\n\n this.remove();\n this.init();\n },\n\n remove: function () {\n if (this.mesh) this.el.removeObject3D('mesh');\n }\n});\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","require('./a-grid');\nrequire('./a-ocean');\nrequire('./a-tube');\n"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/aframe-extras.primitives.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var a=t();for(var i in a)("object"==typeof exports?exports:e)[i]=a[i]}}(self,(()=>(()=>{var e={785:e=>{e.exports=AFRAME.registerPrimitive("a-grid",{defaultComponents:{geometry:{primitive:"plane",width:75,height:75},rotation:{x:-90,y:0,z:0},material:{src:"url(https://cdn.jsdelivr.net/gh/donmccurdy/aframe-extras@v1.16.3/assets/grid.png)",repeat:"75 75"}},mappings:{width:"geometry.width",height:"geometry.height",src:"material.src"}})},683:e=>{e.exports.Primitive=AFRAME.registerPrimitive("a-ocean",{defaultComponents:{ocean:{},rotation:{x:-90,y:0,z:0}},mappings:{width:"ocean.width",depth:"ocean.depth",density:"ocean.density",amplitude:"ocean.amplitude",amplitudeVariance:"ocean.amplitudeVariance",speed:"ocean.speed",speedVariance:"ocean.speedVariance",color:"ocean.color",opacity:"ocean.opacity"}}),e.exports.Component=AFRAME.registerComponent("ocean",{schema:{width:{default:10,min:0},depth:{default:10,min:0},density:{default:10},amplitude:{default:.1},amplitudeVariance:{default:.3},speed:{default:1},speedVariance:{default:2},color:{default:"#7AD2F7",type:"color"},opacity:{default:.8}},play:function(){const e=this.el,t=this.data;let a=e.components.material;const i=new THREE.PlaneGeometry(t.width,t.depth,t.density,t.density);this.waves=[];const n=i.getAttribute("position");for(let e=0;e{e.exports.Primitive=AFRAME.registerPrimitive("a-tube",{defaultComponents:{tube:{}},mappings:{path:"tube.path",segments:"tube.segments",radius:"tube.radius","radial-segments":"tube.radialSegments",closed:"tube.closed"}}),e.exports.Component=AFRAME.registerComponent("tube",{schema:{path:{default:[]},segments:{default:64},radius:{default:1},radialSegments:{default:8},closed:{default:!1}},init:function(){const e=this.el,t=this.data;let a=e.components.material;if(!t.path.length)return void console.error("[a-tube] `path` property expected but not found.");const i=new THREE.CatmullRomCurve3(t.path.map((function(e){return e=e.split(" "),new THREE.Vector3(Number(e[0]),Number(e[1]),Number(e[2]))}))),n=new THREE.TubeGeometry(i,t.segments,t.radius,t.radialSegments,t.closed);a||(a={},a.material=new THREE.MeshPhongMaterial),this.mesh=new THREE.Mesh(n,a.material),this.el.setObject3D("mesh",this.mesh)},update:function(e){Object.keys(e).length&&(this.remove(),this.init())},remove:function(){this.mesh&&this.el.removeObject3D("mesh")}})}},t={};function a(i){var n=t[i];if(void 0!==n)return n.exports;var o=t[i]={exports:{}};return e[i](o,o.exports,a),o.exports}return a(785),a(683),a(539),{}})())); 2 | //# sourceMappingURL=aframe-extras.primitives.min.js.map -------------------------------------------------------------------------------- /dist/components/grab.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define([], factory); 6 | else { 7 | var a = factory(); 8 | for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; 9 | } 10 | })(self, () => { 11 | return /******/ (() => { // webpackBootstrap 12 | /******/ var __webpack_modules__ = ({ 13 | 14 | /***/ "./src/misc/grab.js": 15 | /*!**************************!*\ 16 | !*** ./src/misc/grab.js ***! 17 | \**************************/ 18 | /***/ ((module) => { 19 | 20 | /* global CANNON */ 21 | 22 | /** 23 | * Based on aframe/examples/showcase/tracked-controls. 24 | * 25 | * Handles events coming from the hand-controls. 26 | * Determines if the entity is grabbed or released. 27 | * Updates its position to move along the controller. 28 | */ 29 | module.exports = AFRAME.registerComponent('grab', { 30 | init: function () { 31 | this.system = this.el.sceneEl.systems.physics; 32 | 33 | this.GRABBED_STATE = 'grabbed'; 34 | 35 | this.grabbing = false; 36 | this.hitEl = /** @type {AFRAME.Element} */ null; 37 | this.physics = /** @type {AFRAME.System} */ this.el.sceneEl.systems.physics; 38 | this.constraint = /** @type {CANNON.Constraint} */ null; 39 | 40 | // Bind event handlers 41 | this.onHit = this.onHit.bind(this); 42 | this.onGripOpen = this.onGripOpen.bind(this); 43 | this.onGripClose = this.onGripClose.bind(this); 44 | }, 45 | 46 | play: function () { 47 | const el = this.el; 48 | el.addEventListener('hit', this.onHit); 49 | el.addEventListener('gripdown', this.onGripClose); 50 | el.addEventListener('gripup', this.onGripOpen); 51 | el.addEventListener('trackpaddown', this.onGripClose); 52 | el.addEventListener('trackpadup', this.onGripOpen); 53 | el.addEventListener('triggerdown', this.onGripClose); 54 | el.addEventListener('triggerup', this.onGripOpen); 55 | }, 56 | 57 | pause: function () { 58 | const el = this.el; 59 | el.removeEventListener('hit', this.onHit); 60 | el.removeEventListener('gripdown', this.onGripClose); 61 | el.removeEventListener('gripup', this.onGripOpen); 62 | el.removeEventListener('trackpaddown', this.onGripClose); 63 | el.removeEventListener('trackpadup', this.onGripOpen); 64 | el.removeEventListener('triggerdown', this.onGripClose); 65 | el.removeEventListener('triggerup', this.onGripOpen); 66 | }, 67 | 68 | onGripClose: function () { 69 | this.grabbing = true; 70 | }, 71 | 72 | onGripOpen: function () { 73 | const hitEl = this.hitEl; 74 | this.grabbing = false; 75 | if (!hitEl) { return; } 76 | hitEl.removeState(this.GRABBED_STATE); 77 | this.hitEl = undefined; 78 | this.system.removeConstraint(this.constraint); 79 | this.constraint = null; 80 | }, 81 | 82 | onHit: function (evt) { 83 | const hitEl = evt.detail.el; 84 | // If the element is already grabbed (it could be grabbed by another controller). 85 | // If the hand is not grabbing the element does not stick. 86 | // If we're already grabbing something you can't grab again. 87 | if (hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; } 88 | hitEl.addState(this.GRABBED_STATE); 89 | this.hitEl = hitEl; 90 | this.constraint = new CANNON.LockConstraint(this.el.body, hitEl.body); 91 | this.system.addConstraint(this.constraint); 92 | } 93 | }); 94 | 95 | 96 | /***/ }) 97 | 98 | /******/ }); 99 | /************************************************************************/ 100 | /******/ // The module cache 101 | /******/ var __webpack_module_cache__ = {}; 102 | /******/ 103 | /******/ // The require function 104 | /******/ function __webpack_require__(moduleId) { 105 | /******/ // Check if module is in cache 106 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 107 | /******/ if (cachedModule !== undefined) { 108 | /******/ return cachedModule.exports; 109 | /******/ } 110 | /******/ // Create a new module (and put it into the cache) 111 | /******/ var module = __webpack_module_cache__[moduleId] = { 112 | /******/ // no module.id needed 113 | /******/ // no module.loaded needed 114 | /******/ exports: {} 115 | /******/ }; 116 | /******/ 117 | /******/ // Execute the module function 118 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 119 | /******/ 120 | /******/ // Return the exports of the module 121 | /******/ return module.exports; 122 | /******/ } 123 | /******/ 124 | /************************************************************************/ 125 | /******/ 126 | /******/ // startup 127 | /******/ // Load entry module and return exports 128 | /******/ // This entry module is referenced by other modules so it can't be inlined 129 | /******/ var __webpack_exports__ = __webpack_require__("./src/misc/grab.js"); 130 | /******/ 131 | /******/ return __webpack_exports__; 132 | /******/ })() 133 | ; 134 | }); 135 | //# sourceMappingURL=grab.js.map -------------------------------------------------------------------------------- /dist/components/grab.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"components/grab.js","mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;;;;;;;;ACVA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;;AAEA;AACA,iCAAiC,mBAAmB;AACpD,iCAAiC,mBAAmB;AACpD,iCAAiC,mBAAmB;;AAEpD;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA,kBAAkB;AAClB;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA,wEAAwE;AACxE;AACA;AACA;AACA;AACA;AACA,CAAC;;;;;;;UCzED;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;UEtBA;UACA;UACA;UACA","sources":["webpack://aframe-extras/webpack/universalModuleDefinition","webpack://aframe-extras/./src/misc/grab.js","webpack://aframe-extras/webpack/bootstrap","webpack://aframe-extras/webpack/before-startup","webpack://aframe-extras/webpack/startup","webpack://aframe-extras/webpack/after-startup"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse {\n\t\tvar a = factory();\n\t\tfor(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];\n\t}\n})(self, () => {\nreturn ","/* global CANNON */\n\n/**\n * Based on aframe/examples/showcase/tracked-controls.\n *\n * Handles events coming from the hand-controls.\n * Determines if the entity is grabbed or released.\n * Updates its position to move along the controller.\n */\nmodule.exports = AFRAME.registerComponent('grab', {\n init: function () {\n this.system = this.el.sceneEl.systems.physics;\n\n this.GRABBED_STATE = 'grabbed';\n\n this.grabbing = false;\n this.hitEl = /** @type {AFRAME.Element} */ null;\n this.physics = /** @type {AFRAME.System} */ this.el.sceneEl.systems.physics;\n this.constraint = /** @type {CANNON.Constraint} */ null;\n\n // Bind event handlers\n this.onHit = this.onHit.bind(this);\n this.onGripOpen = this.onGripOpen.bind(this);\n this.onGripClose = this.onGripClose.bind(this);\n },\n\n play: function () {\n const el = this.el;\n el.addEventListener('hit', this.onHit);\n el.addEventListener('gripdown', this.onGripClose);\n el.addEventListener('gripup', this.onGripOpen);\n el.addEventListener('trackpaddown', this.onGripClose);\n el.addEventListener('trackpadup', this.onGripOpen);\n el.addEventListener('triggerdown', this.onGripClose);\n el.addEventListener('triggerup', this.onGripOpen);\n },\n\n pause: function () {\n const el = this.el;\n el.removeEventListener('hit', this.onHit);\n el.removeEventListener('gripdown', this.onGripClose);\n el.removeEventListener('gripup', this.onGripOpen);\n el.removeEventListener('trackpaddown', this.onGripClose);\n el.removeEventListener('trackpadup', this.onGripOpen);\n el.removeEventListener('triggerdown', this.onGripClose);\n el.removeEventListener('triggerup', this.onGripOpen);\n },\n\n onGripClose: function () {\n this.grabbing = true;\n },\n\n onGripOpen: function () {\n const hitEl = this.hitEl;\n this.grabbing = false;\n if (!hitEl) { return; }\n hitEl.removeState(this.GRABBED_STATE);\n this.hitEl = undefined;\n this.system.removeConstraint(this.constraint);\n this.constraint = null;\n },\n\n onHit: function (evt) {\n const hitEl = evt.detail.el;\n // If the element is already grabbed (it could be grabbed by another controller).\n // If the hand is not grabbing the element does not stick.\n // If we're already grabbing something you can't grab again.\n if (hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; }\n hitEl.addState(this.GRABBED_STATE);\n this.hitEl = hitEl;\n this.constraint = new CANNON.LockConstraint(this.el.body, hitEl.body);\n this.system.addConstraint(this.constraint);\n }\n});\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","","// startup\n// Load entry module and return exports\n// This entry module is referenced by other modules so it can't be inlined\nvar __webpack_exports__ = __webpack_require__(\"./src/misc/grab.js\");\n",""],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/components/grab.min.js: -------------------------------------------------------------------------------- 1 | !function(t,i){if("object"==typeof exports&&"object"==typeof module)module.exports=i();else if("function"==typeof define&&define.amd)define([],i);else{var e=i();for(var n in e)("object"==typeof exports?exports:t)[n]=e[n]}}(self,(()=>{return t={771:t=>{t.exports=AFRAME.registerComponent("grab",{init:function(){this.system=this.el.sceneEl.systems.physics,this.GRABBED_STATE="grabbed",this.grabbing=!1,this.hitEl=null,this.physics=this.el.sceneEl.systems.physics,this.constraint=null,this.onHit=this.onHit.bind(this),this.onGripOpen=this.onGripOpen.bind(this),this.onGripClose=this.onGripClose.bind(this)},play:function(){const t=this.el;t.addEventListener("hit",this.onHit),t.addEventListener("gripdown",this.onGripClose),t.addEventListener("gripup",this.onGripOpen),t.addEventListener("trackpaddown",this.onGripClose),t.addEventListener("trackpadup",this.onGripOpen),t.addEventListener("triggerdown",this.onGripClose),t.addEventListener("triggerup",this.onGripOpen)},pause:function(){const t=this.el;t.removeEventListener("hit",this.onHit),t.removeEventListener("gripdown",this.onGripClose),t.removeEventListener("gripup",this.onGripOpen),t.removeEventListener("trackpaddown",this.onGripClose),t.removeEventListener("trackpadup",this.onGripOpen),t.removeEventListener("triggerdown",this.onGripClose),t.removeEventListener("triggerup",this.onGripOpen)},onGripClose:function(){this.grabbing=!0},onGripOpen:function(){const t=this.hitEl;this.grabbing=!1,t&&(t.removeState(this.GRABBED_STATE),this.hitEl=void 0,this.system.removeConstraint(this.constraint),this.constraint=null)},onHit:function(t){const i=t.detail.el;i.is(this.GRABBED_STATE)||!this.grabbing||this.hitEl||(i.addState(this.GRABBED_STATE),this.hitEl=i,this.constraint=new CANNON.LockConstraint(this.el.body,i.body),this.system.addConstraint(this.constraint))}})}},i={},function e(n){var s=i[n];if(void 0!==s)return s.exports;var o=i[n]={exports:{}};return t[n](o,o.exports,e),o.exports}(771);var t,i})); 2 | //# sourceMappingURL=grab.min.js.map -------------------------------------------------------------------------------- /dist/components/grab.min.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"components/grab.min.js","mappings":"CAAA,SAA2CA,EAAMC,GAChD,GAAsB,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,SACb,GAAqB,mBAAXG,QAAyBA,OAAOC,IAC9CD,OAAO,GAAIH,OACP,CACJ,IAAIK,EAAIL,IACR,IAAI,IAAIM,KAAKD,GAAuB,iBAAZJ,QAAuBA,QAAUF,GAAMO,GAAKD,EAAEC,EACvE,CACA,CATD,CASGC,MAAM,KACT,O,WCDAL,EAAOD,QAAUO,OAAOC,kBAAkB,OAAQ,CAChDC,KAAM,WACJC,KAAKC,OAASD,KAAKE,GAAGC,QAAQC,QAAQC,QAEtCL,KAAKM,cAAgB,UAErBN,KAAKO,UAAW,EAChBP,KAAKQ,MAA8C,KACnDR,KAAKK,QAA8CL,KAAKE,GAAGC,QAAQC,QAAQC,QAC3EL,KAAKS,WAA8C,KAGnDT,KAAKU,MAAQV,KAAKU,MAAMC,KAAKX,MAC7BA,KAAKY,WAAaZ,KAAKY,WAAWD,KAAKX,MACvCA,KAAKa,YAAcb,KAAKa,YAAYF,KAAKX,KAC3C,EAEAc,KAAM,WACJ,MAAMZ,EAAKF,KAAKE,GAChBA,EAAGa,iBAAiB,MAAOf,KAAKU,OAChCR,EAAGa,iBAAiB,WAAYf,KAAKa,aACrCX,EAAGa,iBAAiB,SAAUf,KAAKY,YACnCV,EAAGa,iBAAiB,eAAgBf,KAAKa,aACzCX,EAAGa,iBAAiB,aAAcf,KAAKY,YACvCV,EAAGa,iBAAiB,cAAef,KAAKa,aACxCX,EAAGa,iBAAiB,YAAaf,KAAKY,WACxC,EAEAI,MAAO,WACL,MAAMd,EAAKF,KAAKE,GAChBA,EAAGe,oBAAoB,MAAOjB,KAAKU,OACnCR,EAAGe,oBAAoB,WAAYjB,KAAKa,aACxCX,EAAGe,oBAAoB,SAAUjB,KAAKY,YACtCV,EAAGe,oBAAoB,eAAgBjB,KAAKa,aAC5CX,EAAGe,oBAAoB,aAAcjB,KAAKY,YAC1CV,EAAGe,oBAAoB,cAAejB,KAAKa,aAC3CX,EAAGe,oBAAoB,YAAajB,KAAKY,WAC3C,EAEAC,YAAa,WACXb,KAAKO,UAAW,CAClB,EAEAK,WAAY,WACV,MAAMJ,EAAQR,KAAKQ,MACnBR,KAAKO,UAAW,EACXC,IACLA,EAAMU,YAAYlB,KAAKM,eACvBN,KAAKQ,WAAQW,EACbnB,KAAKC,OAAOmB,iBAAiBpB,KAAKS,YAClCT,KAAKS,WAAa,KACpB,EAEAC,MAAO,SAAUW,GACf,MAAMb,EAAQa,EAAIC,OAAOpB,GAIrBM,EAAMe,GAAGvB,KAAKM,iBAAmBN,KAAKO,UAAYP,KAAKQ,QAC3DA,EAAMgB,SAASxB,KAAKM,eACpBN,KAAKQ,MAAQA,EACbR,KAAKS,WAAa,IAAIgB,OAAOC,eAAe1B,KAAKE,GAAGyB,KAAMnB,EAAMmB,MAChE3B,KAAKC,OAAO2B,cAAc5B,KAAKS,YACjC,G,GCvEEoB,EAA2B,CAAC,EAGhC,SAASC,EAAoBC,GAE5B,IAAIC,EAAeH,EAAyBE,GAC5C,QAAqBZ,IAAjBa,EACH,OAAOA,EAAa1C,QAGrB,IAAIC,EAASsC,EAAyBE,GAAY,CAGjDzC,QAAS,CAAC,GAOX,OAHA2C,EAAoBF,GAAUxC,EAAQA,EAAOD,QAASwC,GAG/CvC,EAAOD,OACf,CCnB0BwC,CAAoB,K,MDF1CD,C","sources":["webpack://aframe-extras/webpack/universalModuleDefinition","webpack://aframe-extras/./src/misc/grab.js","webpack://aframe-extras/webpack/bootstrap","webpack://aframe-extras/webpack/startup"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse {\n\t\tvar a = factory();\n\t\tfor(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];\n\t}\n})(self, () => {\nreturn ","/* global CANNON */\n\n/**\n * Based on aframe/examples/showcase/tracked-controls.\n *\n * Handles events coming from the hand-controls.\n * Determines if the entity is grabbed or released.\n * Updates its position to move along the controller.\n */\nmodule.exports = AFRAME.registerComponent('grab', {\n init: function () {\n this.system = this.el.sceneEl.systems.physics;\n\n this.GRABBED_STATE = 'grabbed';\n\n this.grabbing = false;\n this.hitEl = /** @type {AFRAME.Element} */ null;\n this.physics = /** @type {AFRAME.System} */ this.el.sceneEl.systems.physics;\n this.constraint = /** @type {CANNON.Constraint} */ null;\n\n // Bind event handlers\n this.onHit = this.onHit.bind(this);\n this.onGripOpen = this.onGripOpen.bind(this);\n this.onGripClose = this.onGripClose.bind(this);\n },\n\n play: function () {\n const el = this.el;\n el.addEventListener('hit', this.onHit);\n el.addEventListener('gripdown', this.onGripClose);\n el.addEventListener('gripup', this.onGripOpen);\n el.addEventListener('trackpaddown', this.onGripClose);\n el.addEventListener('trackpadup', this.onGripOpen);\n el.addEventListener('triggerdown', this.onGripClose);\n el.addEventListener('triggerup', this.onGripOpen);\n },\n\n pause: function () {\n const el = this.el;\n el.removeEventListener('hit', this.onHit);\n el.removeEventListener('gripdown', this.onGripClose);\n el.removeEventListener('gripup', this.onGripOpen);\n el.removeEventListener('trackpaddown', this.onGripClose);\n el.removeEventListener('trackpadup', this.onGripOpen);\n el.removeEventListener('triggerdown', this.onGripClose);\n el.removeEventListener('triggerup', this.onGripOpen);\n },\n\n onGripClose: function () {\n this.grabbing = true;\n },\n\n onGripOpen: function () {\n const hitEl = this.hitEl;\n this.grabbing = false;\n if (!hitEl) { return; }\n hitEl.removeState(this.GRABBED_STATE);\n this.hitEl = undefined;\n this.system.removeConstraint(this.constraint);\n this.constraint = null;\n },\n\n onHit: function (evt) {\n const hitEl = evt.detail.el;\n // If the element is already grabbed (it could be grabbed by another controller).\n // If the hand is not grabbing the element does not stick.\n // If we're already grabbing something you can't grab again.\n if (hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; }\n hitEl.addState(this.GRABBED_STATE);\n this.hitEl = hitEl;\n this.constraint = new CANNON.LockConstraint(this.el.body, hitEl.body);\n this.system.addConstraint(this.constraint);\n }\n});\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// startup\n// Load entry module and return exports\n// This entry module is referenced by other modules so it can't be inlined\nvar __webpack_exports__ = __webpack_require__(771);\n"],"names":["root","factory","exports","module","define","amd","a","i","self","AFRAME","registerComponent","init","this","system","el","sceneEl","systems","physics","GRABBED_STATE","grabbing","hitEl","constraint","onHit","bind","onGripOpen","onGripClose","play","addEventListener","pause","removeEventListener","removeState","undefined","removeConstraint","evt","detail","is","addState","CANNON","LockConstraint","body","addConstraint","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","__webpack_modules__"],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/components/sphere-collider.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(); 4 | else if(typeof define === 'function' && define.amd) 5 | define([], factory); 6 | else { 7 | var a = factory(); 8 | for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i]; 9 | } 10 | })(self, () => { 11 | return /******/ (() => { // webpackBootstrap 12 | /******/ var __webpack_modules__ = ({ 13 | 14 | /***/ "./src/misc/sphere-collider.js": 15 | /*!*************************************!*\ 16 | !*** ./src/misc/sphere-collider.js ***! 17 | \*************************************/ 18 | /***/ ((module) => { 19 | 20 | /** 21 | * Based on aframe/examples/showcase/tracked-controls. 22 | * 23 | * Implement bounding sphere collision detection for entities with a mesh. 24 | * Sets the specified state on the intersected entities. 25 | * 26 | * @property {string} objects - Selector of the entities to test for collision. 27 | * @property {string} state - State to set on collided entities. 28 | * 29 | */ 30 | module.exports = AFRAME.registerComponent('sphere-collider', { 31 | schema: { 32 | enabled: {default: true}, 33 | interval: {default: 80}, 34 | objects: {default: ''}, 35 | state: {default: 'collided'}, 36 | radius: {default: 0.05}, 37 | watch: {default: true} 38 | }, 39 | 40 | init: function () { 41 | /** @type {MutationObserver} */ 42 | this.observer = null; 43 | /** @type {Array} Elements to watch for collisions. */ 44 | this.els = []; 45 | /** @type {Array} Elements currently in collision state. */ 46 | this.collisions = []; 47 | this.prevCheckTime = undefined; 48 | 49 | this.eventDetail = {}; 50 | this.handleHit = this.handleHit.bind(this); 51 | this.handleHitEnd = this.handleHitEnd.bind(this); 52 | }, 53 | 54 | play: function () { 55 | const sceneEl = this.el.sceneEl; 56 | 57 | if (this.data.watch) { 58 | this.observer = new MutationObserver(this.update.bind(this, null)); 59 | this.observer.observe(sceneEl, {childList: true, subtree: true}); 60 | } 61 | }, 62 | 63 | pause: function () { 64 | if (this.observer) { 65 | this.observer.disconnect(); 66 | this.observer = null; 67 | } 68 | }, 69 | 70 | /** 71 | * Update list of entities to test for collision. 72 | */ 73 | update: function () { 74 | const data = this.data; 75 | let objectEls; 76 | 77 | // Push entities into list of els to intersect. 78 | if (data.objects) { 79 | objectEls = this.el.sceneEl.querySelectorAll(data.objects); 80 | } else { 81 | // If objects not defined, intersect with everything. 82 | objectEls = this.el.sceneEl.children; 83 | } 84 | // Convert from NodeList to Array 85 | this.els = Array.prototype.slice.call(objectEls); 86 | }, 87 | 88 | tick: (function () { 89 | const position = new THREE.Vector3(), 90 | meshPosition = new THREE.Vector3(), 91 | colliderScale = new THREE.Vector3(), 92 | size = new THREE.Vector3(), 93 | box = new THREE.Box3(), 94 | collisions = [], 95 | distanceMap = new Map(); 96 | return function (time) { 97 | if (!this.data.enabled) { return; } 98 | 99 | // Only check for intersection if interval time has passed. 100 | const prevCheckTime = this.prevCheckTime; 101 | if (prevCheckTime && (time - prevCheckTime < this.data.interval)) { return; } 102 | // Update check time. 103 | this.prevCheckTime = time; 104 | 105 | const el = this.el, 106 | data = this.data, 107 | mesh = el.getObject3D('mesh'); 108 | let colliderRadius; 109 | 110 | if (!mesh) { return; } 111 | 112 | collisions.length = 0; 113 | distanceMap.clear(); 114 | el.object3D.getWorldPosition(position); 115 | el.object3D.getWorldScale(colliderScale); 116 | colliderRadius = data.radius * scaleFactor(colliderScale); 117 | // Update collision list. 118 | this.els.forEach(intersect); 119 | 120 | // Emit events and add collision states, in order of distance. 121 | collisions 122 | .sort((a, b) => distanceMap.get(a) > distanceMap.get(b) ? 1 : -1) 123 | .forEach(this.handleHit); 124 | 125 | // Remove collision state from other elements. 126 | this.collisions 127 | .filter((el) => !distanceMap.has(el)) 128 | .forEach(this.handleHitEnd); 129 | 130 | // Store new collisions 131 | copyArray(this.collisions, collisions); 132 | 133 | // Bounding sphere collision detection 134 | function intersect (el) { 135 | let radius, mesh, distance, extent; 136 | 137 | if (!el.isEntity) { return; } 138 | 139 | mesh = el.getObject3D('mesh'); 140 | 141 | if (!mesh) { return; } 142 | 143 | box.setFromObject(mesh).getSize(size); 144 | extent = Math.max(size.x, size.y, size.z) / 2; 145 | radius = Math.sqrt(2 * extent * extent); 146 | box.getCenter(meshPosition); 147 | 148 | if (!radius) { return; } 149 | 150 | distance = position.distanceTo(meshPosition); 151 | if (distance < radius + colliderRadius) { 152 | collisions.push(el); 153 | distanceMap.set(el, distance); 154 | } 155 | } 156 | // use max of scale factors to maintain bounding sphere collision 157 | function scaleFactor (scaleVec) { 158 | return Math.max(scaleVec.x, scaleVec.y, scaleVec.z); 159 | } 160 | }; 161 | })(), 162 | 163 | handleHit: function (targetEl) { 164 | targetEl.emit('hit'); 165 | targetEl.addState(this.data.state); 166 | this.eventDetail.el = targetEl; 167 | this.el.emit('hit', this.eventDetail); 168 | }, 169 | handleHitEnd: function (targetEl) { 170 | targetEl.emit('hitend'); 171 | targetEl.removeState(this.data.state); 172 | this.eventDetail.el = targetEl; 173 | this.el.emit('hitend', this.eventDetail); 174 | } 175 | }); 176 | 177 | function copyArray (dest, source) { 178 | dest.length = 0; 179 | for (let i = 0; i < source.length; i++) { dest[i] = source[i]; } 180 | } 181 | 182 | 183 | /***/ }) 184 | 185 | /******/ }); 186 | /************************************************************************/ 187 | /******/ // The module cache 188 | /******/ var __webpack_module_cache__ = {}; 189 | /******/ 190 | /******/ // The require function 191 | /******/ function __webpack_require__(moduleId) { 192 | /******/ // Check if module is in cache 193 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 194 | /******/ if (cachedModule !== undefined) { 195 | /******/ return cachedModule.exports; 196 | /******/ } 197 | /******/ // Create a new module (and put it into the cache) 198 | /******/ var module = __webpack_module_cache__[moduleId] = { 199 | /******/ // no module.id needed 200 | /******/ // no module.loaded needed 201 | /******/ exports: {} 202 | /******/ }; 203 | /******/ 204 | /******/ // Execute the module function 205 | /******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 206 | /******/ 207 | /******/ // Return the exports of the module 208 | /******/ return module.exports; 209 | /******/ } 210 | /******/ 211 | /************************************************************************/ 212 | /******/ 213 | /******/ // startup 214 | /******/ // Load entry module and return exports 215 | /******/ // This entry module is referenced by other modules so it can't be inlined 216 | /******/ var __webpack_exports__ = __webpack_require__("./src/misc/sphere-collider.js"); 217 | /******/ 218 | /******/ return __webpack_exports__; 219 | /******/ })() 220 | ; 221 | }); 222 | //# sourceMappingURL=sphere-collider.js.map -------------------------------------------------------------------------------- /dist/components/sphere-collider.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"components/sphere-collider.js","mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;AACD,O;;;;;;;;;ACVA;AACA;AACA;AACA;AACA;AACA;AACA,cAAc,QAAQ;AACtB,cAAc,QAAQ;AACtB;AACA;AACA;AACA;AACA,cAAc,cAAc;AAC5B,eAAe,YAAY;AAC3B,cAAc,YAAY;AAC1B,YAAY,oBAAoB;AAChC,aAAa,cAAc;AAC3B,YAAY;AACZ,GAAG;;AAEH;AACA,eAAe,kBAAkB;AACjC;AACA,eAAe,gBAAgB;AAC/B;AACA,eAAe,gBAAgB;AAC/B;AACA;;AAEA;AACA;AACA;AACA,GAAG;;AAEH;AACA;;AAEA;AACA;AACA,sCAAsC,+BAA+B;AACrE;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA,MAAM;AACN;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gCAAgC;;AAEhC;AACA;AACA,0EAA0E;AAC1E;AACA;;AAEA;AACA;AACA;AACA;;AAEA,mBAAmB;;AAEnB;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA,4BAA4B;;AAE5B;;AAEA,qBAAqB;;AAErB;AACA;AACA;AACA;;AAEA,uBAAuB;;AAEvB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,GAAG;;AAEH;AACA;AACA;AACA;AACA;AACA,GAAG;AACH;AACA;AACA;AACA;AACA;AACA;AACA,CAAC;;AAED;AACA;AACA,kBAAkB,mBAAmB,OAAO;AAC5C;;;;;;;UChKA;UACA;;UAEA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA;;UAEA;UACA;;UAEA;UACA;UACA;;;;UEtBA;UACA;UACA;UACA","sources":["webpack://aframe-extras/webpack/universalModuleDefinition","webpack://aframe-extras/./src/misc/sphere-collider.js","webpack://aframe-extras/webpack/bootstrap","webpack://aframe-extras/webpack/before-startup","webpack://aframe-extras/webpack/startup","webpack://aframe-extras/webpack/after-startup"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse {\n\t\tvar a = factory();\n\t\tfor(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];\n\t}\n})(self, () => {\nreturn ","/**\n * Based on aframe/examples/showcase/tracked-controls.\n *\n * Implement bounding sphere collision detection for entities with a mesh.\n * Sets the specified state on the intersected entities.\n *\n * @property {string} objects - Selector of the entities to test for collision.\n * @property {string} state - State to set on collided entities.\n *\n */\nmodule.exports = AFRAME.registerComponent('sphere-collider', {\n schema: {\n enabled: {default: true},\n interval: {default: 80},\n objects: {default: ''},\n state: {default: 'collided'},\n radius: {default: 0.05},\n watch: {default: true}\n },\n\n init: function () {\n /** @type {MutationObserver} */\n this.observer = null;\n /** @type {Array} Elements to watch for collisions. */\n this.els = [];\n /** @type {Array} Elements currently in collision state. */\n this.collisions = [];\n this.prevCheckTime = undefined;\n\n this.eventDetail = {};\n this.handleHit = this.handleHit.bind(this);\n this.handleHitEnd = this.handleHitEnd.bind(this);\n },\n\n play: function () {\n const sceneEl = this.el.sceneEl;\n\n if (this.data.watch) {\n this.observer = new MutationObserver(this.update.bind(this, null));\n this.observer.observe(sceneEl, {childList: true, subtree: true});\n }\n },\n\n pause: function () {\n if (this.observer) {\n this.observer.disconnect();\n this.observer = null;\n }\n },\n\n /**\n * Update list of entities to test for collision.\n */\n update: function () {\n const data = this.data;\n let objectEls;\n\n // Push entities into list of els to intersect.\n if (data.objects) {\n objectEls = this.el.sceneEl.querySelectorAll(data.objects);\n } else {\n // If objects not defined, intersect with everything.\n objectEls = this.el.sceneEl.children;\n }\n // Convert from NodeList to Array\n this.els = Array.prototype.slice.call(objectEls);\n },\n\n tick: (function () {\n const position = new THREE.Vector3(),\n meshPosition = new THREE.Vector3(),\n colliderScale = new THREE.Vector3(),\n size = new THREE.Vector3(),\n box = new THREE.Box3(),\n collisions = [],\n distanceMap = new Map();\n return function (time) {\n if (!this.data.enabled) { return; }\n\n // Only check for intersection if interval time has passed.\n const prevCheckTime = this.prevCheckTime;\n if (prevCheckTime && (time - prevCheckTime < this.data.interval)) { return; }\n // Update check time.\n this.prevCheckTime = time;\n\n const el = this.el,\n data = this.data,\n mesh = el.getObject3D('mesh');\n let colliderRadius;\n\n if (!mesh) { return; }\n\n collisions.length = 0;\n distanceMap.clear();\n el.object3D.getWorldPosition(position);\n el.object3D.getWorldScale(colliderScale);\n colliderRadius = data.radius * scaleFactor(colliderScale);\n // Update collision list.\n this.els.forEach(intersect);\n\n // Emit events and add collision states, in order of distance.\n collisions\n .sort((a, b) => distanceMap.get(a) > distanceMap.get(b) ? 1 : -1)\n .forEach(this.handleHit);\n\n // Remove collision state from other elements.\n this.collisions\n .filter((el) => !distanceMap.has(el))\n .forEach(this.handleHitEnd);\n\n // Store new collisions\n copyArray(this.collisions, collisions);\n\n // Bounding sphere collision detection\n function intersect (el) {\n let radius, mesh, distance, extent;\n\n if (!el.isEntity) { return; }\n\n mesh = el.getObject3D('mesh');\n\n if (!mesh) { return; }\n\n box.setFromObject(mesh).getSize(size);\n extent = Math.max(size.x, size.y, size.z) / 2;\n radius = Math.sqrt(2 * extent * extent);\n box.getCenter(meshPosition);\n\n if (!radius) { return; }\n\n distance = position.distanceTo(meshPosition);\n if (distance < radius + colliderRadius) {\n collisions.push(el);\n distanceMap.set(el, distance);\n }\n }\n // use max of scale factors to maintain bounding sphere collision\n function scaleFactor (scaleVec) {\n return Math.max(scaleVec.x, scaleVec.y, scaleVec.z);\n }\n };\n })(),\n\n handleHit: function (targetEl) {\n targetEl.emit('hit');\n targetEl.addState(this.data.state);\n this.eventDetail.el = targetEl;\n this.el.emit('hit', this.eventDetail);\n },\n handleHitEnd: function (targetEl) {\n targetEl.emit('hitend');\n targetEl.removeState(this.data.state);\n this.eventDetail.el = targetEl;\n this.el.emit('hitend', this.eventDetail);\n }\n});\n\nfunction copyArray (dest, source) {\n dest.length = 0;\n for (let i = 0; i < source.length; i++) { dest[i] = source[i]; }\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","","// startup\n// Load entry module and return exports\n// This entry module is referenced by other modules so it can't be inlined\nvar __webpack_exports__ = __webpack_require__(\"./src/misc/sphere-collider.js\");\n",""],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /dist/components/sphere-collider.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var i=t();for(var s in i)("object"==typeof exports?exports:e)[s]=i[s]}}(self,(()=>{return e={109:e=>{e.exports=AFRAME.registerComponent("sphere-collider",{schema:{enabled:{default:!0},interval:{default:80},objects:{default:""},state:{default:"collided"},radius:{default:.05},watch:{default:!0}},init:function(){this.observer=null,this.els=[],this.collisions=[],this.prevCheckTime=void 0,this.eventDetail={},this.handleHit=this.handleHit.bind(this),this.handleHitEnd=this.handleHitEnd.bind(this)},play:function(){const e=this.el.sceneEl;this.data.watch&&(this.observer=new MutationObserver(this.update.bind(this,null)),this.observer.observe(e,{childList:!0,subtree:!0}))},pause:function(){this.observer&&(this.observer.disconnect(),this.observer=null)},update:function(){const e=this.data;let t;t=e.objects?this.el.sceneEl.querySelectorAll(e.objects):this.el.sceneEl.children,this.els=Array.prototype.slice.call(t)},tick:function(){const e=new THREE.Vector3,t=new THREE.Vector3,i=new THREE.Vector3,s=new THREE.Vector3,n=new THREE.Box3,o=[],l=new Map;return function(r){if(!this.data.enabled)return;const h=this.prevCheckTime;if(h&&r-hl.get(e)>l.get(t)?1:-1)).forEach(this.handleHit),this.collisions.filter((e=>!l.has(e))).forEach(this.handleHitEnd),function(e,t){e.length=0;for(let i=0;i {\nreturn ","/**\n * Based on aframe/examples/showcase/tracked-controls.\n *\n * Implement bounding sphere collision detection for entities with a mesh.\n * Sets the specified state on the intersected entities.\n *\n * @property {string} objects - Selector of the entities to test for collision.\n * @property {string} state - State to set on collided entities.\n *\n */\nmodule.exports = AFRAME.registerComponent('sphere-collider', {\n schema: {\n enabled: {default: true},\n interval: {default: 80},\n objects: {default: ''},\n state: {default: 'collided'},\n radius: {default: 0.05},\n watch: {default: true}\n },\n\n init: function () {\n /** @type {MutationObserver} */\n this.observer = null;\n /** @type {Array} Elements to watch for collisions. */\n this.els = [];\n /** @type {Array} Elements currently in collision state. */\n this.collisions = [];\n this.prevCheckTime = undefined;\n\n this.eventDetail = {};\n this.handleHit = this.handleHit.bind(this);\n this.handleHitEnd = this.handleHitEnd.bind(this);\n },\n\n play: function () {\n const sceneEl = this.el.sceneEl;\n\n if (this.data.watch) {\n this.observer = new MutationObserver(this.update.bind(this, null));\n this.observer.observe(sceneEl, {childList: true, subtree: true});\n }\n },\n\n pause: function () {\n if (this.observer) {\n this.observer.disconnect();\n this.observer = null;\n }\n },\n\n /**\n * Update list of entities to test for collision.\n */\n update: function () {\n const data = this.data;\n let objectEls;\n\n // Push entities into list of els to intersect.\n if (data.objects) {\n objectEls = this.el.sceneEl.querySelectorAll(data.objects);\n } else {\n // If objects not defined, intersect with everything.\n objectEls = this.el.sceneEl.children;\n }\n // Convert from NodeList to Array\n this.els = Array.prototype.slice.call(objectEls);\n },\n\n tick: (function () {\n const position = new THREE.Vector3(),\n meshPosition = new THREE.Vector3(),\n colliderScale = new THREE.Vector3(),\n size = new THREE.Vector3(),\n box = new THREE.Box3(),\n collisions = [],\n distanceMap = new Map();\n return function (time) {\n if (!this.data.enabled) { return; }\n\n // Only check for intersection if interval time has passed.\n const prevCheckTime = this.prevCheckTime;\n if (prevCheckTime && (time - prevCheckTime < this.data.interval)) { return; }\n // Update check time.\n this.prevCheckTime = time;\n\n const el = this.el,\n data = this.data,\n mesh = el.getObject3D('mesh');\n let colliderRadius;\n\n if (!mesh) { return; }\n\n collisions.length = 0;\n distanceMap.clear();\n el.object3D.getWorldPosition(position);\n el.object3D.getWorldScale(colliderScale);\n colliderRadius = data.radius * scaleFactor(colliderScale);\n // Update collision list.\n this.els.forEach(intersect);\n\n // Emit events and add collision states, in order of distance.\n collisions\n .sort((a, b) => distanceMap.get(a) > distanceMap.get(b) ? 1 : -1)\n .forEach(this.handleHit);\n\n // Remove collision state from other elements.\n this.collisions\n .filter((el) => !distanceMap.has(el))\n .forEach(this.handleHitEnd);\n\n // Store new collisions\n copyArray(this.collisions, collisions);\n\n // Bounding sphere collision detection\n function intersect (el) {\n let radius, mesh, distance, extent;\n\n if (!el.isEntity) { return; }\n\n mesh = el.getObject3D('mesh');\n\n if (!mesh) { return; }\n\n box.setFromObject(mesh).getSize(size);\n extent = Math.max(size.x, size.y, size.z) / 2;\n radius = Math.sqrt(2 * extent * extent);\n box.getCenter(meshPosition);\n\n if (!radius) { return; }\n\n distance = position.distanceTo(meshPosition);\n if (distance < radius + colliderRadius) {\n collisions.push(el);\n distanceMap.set(el, distance);\n }\n }\n // use max of scale factors to maintain bounding sphere collision\n function scaleFactor (scaleVec) {\n return Math.max(scaleVec.x, scaleVec.y, scaleVec.z);\n }\n };\n })(),\n\n handleHit: function (targetEl) {\n targetEl.emit('hit');\n targetEl.addState(this.data.state);\n this.eventDetail.el = targetEl;\n this.el.emit('hit', this.eventDetail);\n },\n handleHitEnd: function (targetEl) {\n targetEl.emit('hitend');\n targetEl.removeState(this.data.state);\n this.eventDetail.el = targetEl;\n this.el.emit('hitend', this.eventDetail);\n }\n});\n\nfunction copyArray (dest, source) {\n dest.length = 0;\n for (let i = 0; i < source.length; i++) { dest[i] = source[i]; }\n}\n","// The module cache\nvar __webpack_module_cache__ = {};\n\n// The require function\nfunction __webpack_require__(moduleId) {\n\t// Check if module is in cache\n\tvar cachedModule = __webpack_module_cache__[moduleId];\n\tif (cachedModule !== undefined) {\n\t\treturn cachedModule.exports;\n\t}\n\t// Create a new module (and put it into the cache)\n\tvar module = __webpack_module_cache__[moduleId] = {\n\t\t// no module.id needed\n\t\t// no module.loaded needed\n\t\texports: {}\n\t};\n\n\t// Execute the module function\n\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n\n\t// Return the exports of the module\n\treturn module.exports;\n}\n\n","// startup\n// Load entry module and return exports\n// This entry module is referenced by other modules so it can't be inlined\nvar __webpack_exports__ = __webpack_require__(109);\n"],"names":["root","factory","exports","module","define","amd","a","i","self","AFRAME","registerComponent","schema","enabled","default","interval","objects","state","radius","watch","init","this","observer","els","collisions","prevCheckTime","undefined","eventDetail","handleHit","bind","handleHitEnd","play","sceneEl","el","data","MutationObserver","update","observe","childList","subtree","pause","disconnect","objectEls","querySelectorAll","children","Array","prototype","slice","call","tick","position","THREE","Vector3","meshPosition","colliderScale","size","box","Box3","distanceMap","Map","time","colliderRadius","scaleVec","getObject3D","length","clear","object3D","getWorldPosition","getWorldScale","Math","max","x","y","z","forEach","mesh","distance","extent","isEntity","setFromObject","getSize","sqrt","getCenter","distanceTo","push","set","sort","b","get","filter","has","dest","source","copyArray","targetEl","emit","addState","removeState","__webpack_module_cache__","__webpack_require__","moduleId","cachedModule","__webpack_modules__"],"sourceRoot":""} -------------------------------------------------------------------------------- /examples/animation-controls/animation-controls.js: -------------------------------------------------------------------------------- 1 | 2 | const animationNames = { 3 | attack: 'Armature|TRex_Attack', 4 | death: 'Armature|TRex_Death', 5 | idle: 'Armature|TRex_Idle', 6 | jump: 'Armature|TRex_Jump', 7 | run: 'Armature|TRex_Run', 8 | walk: 'Armature|TRex_Walk', 9 | }; 10 | 11 | updateAnimationMixer = () => { 12 | 13 | const data = {useRegExp: true} 14 | data.clip = 'none' 15 | Object.entries(animationNames).forEach((name) => { 16 | 17 | const regExpEscape = (s) => { 18 | return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); 19 | } 20 | 21 | el = document.getElementById(name[0]) 22 | if (el.checked) { 23 | if (data.clip) data.clip += "|" 24 | data.clip += regExpEscape(name[1]) 25 | } 26 | }) 27 | 28 | const keys = ['duration', 29 | 'clampWhenFinished', 30 | 'crossFadeDuration', 31 | 'loop', 32 | 'repetitions', 33 | 'timeScale', 34 | 'startAt'] 35 | keys.forEach((key) => { 36 | const el = document.getElementById(key) 37 | let value = el.value 38 | 39 | const type = AFRAME.components['animation-mixer'].schema[key].type 40 | 41 | if (type === 'number' && isNaN(value)) { 42 | return; 43 | } 44 | 45 | if (type === 'boolean') { 46 | value = el.checked 47 | } 48 | 49 | data[key] = value 50 | }) 51 | 52 | const target = document.getElementById('trex1') 53 | target.setAttribute('animation-mixer', data) 54 | } 55 | 56 | document.addEventListener('DOMContentLoaded', () => { 57 | 58 | const inputs = document.querySelectorAll('input, select') 59 | 60 | inputs.forEach((input) => { 61 | input.addEventListener('change', updateAnimationMixer) 62 | input.addEventListener('click', updateAnimationMixer) 63 | }) 64 | 65 | updateAnimationMixer() 66 | }) 67 | -------------------------------------------------------------------------------- /examples/animation-controls/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Animation Controls 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
Controls
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 59 | 60 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /examples/animation-controls/styles.css: -------------------------------------------------------------------------------- 1 | 2 | .topRight { 3 | position: absolute; 4 | right: 10px; 5 | top: 10px; 6 | z-index: 10; 7 | background: white; 8 | padding: 10px; 9 | } 10 | -------------------------------------------------------------------------------- /examples/animation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Animation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 24 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/assets/castle/Castle-navmesh.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/castle/Castle-navmesh.glb -------------------------------------------------------------------------------- /examples/assets/castle/Castle.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/castle/Castle.glb -------------------------------------------------------------------------------- /examples/assets/environment/Park2/README.md: -------------------------------------------------------------------------------- 1 | Author 2 | ====== 3 | 4 | This is the work of Emil Persson, aka Humus. 5 | http://www.humus.name 6 | humus@comhem.se 7 | 8 | 9 | 10 | Legal stuff 11 | =========== 12 | 13 | This work is free and may be used by anyone for any purpose 14 | and may be distributed freely to anyone using any distribution 15 | media or distribution method as long as this file is included. 16 | Distribution without this file is allowed if it's distributed 17 | with free non-commercial software; however, fair credit of the 18 | original author is expected. 19 | Any commercial distribution of this software requires the written 20 | approval of Emil Persson. 21 | -------------------------------------------------------------------------------- /examples/assets/environment/Park2/negx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/environment/Park2/negx.jpg -------------------------------------------------------------------------------- /examples/assets/environment/Park2/negy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/environment/Park2/negy.jpg -------------------------------------------------------------------------------- /examples/assets/environment/Park2/negz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/environment/Park2/negz.jpg -------------------------------------------------------------------------------- /examples/assets/environment/Park2/posx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/environment/Park2/posx.jpg -------------------------------------------------------------------------------- /examples/assets/environment/Park2/posy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/environment/Park2/posy.jpg -------------------------------------------------------------------------------- /examples/assets/environment/Park2/posz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/environment/Park2/posz.jpg -------------------------------------------------------------------------------- /examples/assets/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/grid.png -------------------------------------------------------------------------------- /examples/assets/island-hut/island-hut.mtl: -------------------------------------------------------------------------------- 1 | # MagicaVoxel @ ephtracy 2 | 3 | newmtl palette 4 | illum 1 5 | Ka 0.000 0.000 0.000 6 | Kd 1.000 1.000 1.000 7 | Ks 0.000 0.000 0.000 8 | map_Kd island-hut.png 9 | -------------------------------------------------------------------------------- /examples/assets/island-hut/island-hut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/island-hut/island-hut.png -------------------------------------------------------------------------------- /examples/assets/monu9/README.md: -------------------------------------------------------------------------------- 1 | # monu9 2 | 3 | Made by [@ephtracy](https://twitter.com/ephtracy), with [MagicaVoxel](https://ephtracy.github.io/). 4 | 5 | -------------------------------------------------------------------------------- /examples/assets/monu9/monu9.mtl: -------------------------------------------------------------------------------- 1 | # MagicaVoxel @ ephtracy 2 | 3 | newmtl palette 4 | illum 1 5 | Ka 0.000 0.000 0.000 6 | Kd 1.000 1.000 1.000 7 | Ks 0.000 0.000 0.000 8 | map_Kd monu9.png 9 | -------------------------------------------------------------------------------- /examples/assets/monu9/monu9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/monu9/monu9.png -------------------------------------------------------------------------------- /examples/assets/rupee/README.md: -------------------------------------------------------------------------------- 1 | # rupee 2 | 3 | https://sketchfab.com/models/0c8d66cf39e84c29b67a62fa11c40eba 4 | 5 | By user [soiber](https://sketchfab.com/soiber) on Sketchfab. 6 | -------------------------------------------------------------------------------- /examples/assets/rupee/rupee.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/rupee/rupee.glb -------------------------------------------------------------------------------- /examples/assets/scythian/README.md: -------------------------------------------------------------------------------- 1 | # scythian 2 | 3 | Made by @donmccurdy, with [MagicaVoxel](https://ephtracy.github.io/). 4 | -------------------------------------------------------------------------------- /examples/assets/scythian/scythian.fbm/scythian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/scythian/scythian.fbm/scythian.png -------------------------------------------------------------------------------- /examples/assets/scythian/scythian.mtl: -------------------------------------------------------------------------------- 1 | # MagicaVoxel @ ephtracy 2 | 3 | newmtl palette 4 | illum 1 5 | Ka 0.000 0.000 0.000 6 | Kd 1.000 1.000 1.000 7 | Ks 0.000 0.000 0.000 8 | map_Kd scythian.png 9 | -------------------------------------------------------------------------------- /examples/assets/scythian/scythian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/scythian/scythian.png -------------------------------------------------------------------------------- /examples/assets/threejs-examples/README.md: -------------------------------------------------------------------------------- 1 | # THREE.js Examples 2 | 3 | See: http://threejs.org/examples/#webgl_animation_scene 4 | -------------------------------------------------------------------------------- /examples/assets/trex/TRex.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c-frame/aframe-extras/f0432405833d2f6072331c32339aba78cf503de9/examples/assets/trex/TRex.glb -------------------------------------------------------------------------------- /examples/castle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Castle 7 | 8 | 9 | 10 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 73 | 76 | 77 | 78 | 79 | 80 | 81 | 88 | 89 | 90 | 97 | 98 | 99 | 100 | 101 | 106 | 107 | 108 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /examples/checkpoints/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Checkpoints 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /examples/env-map/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Environment Map 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/fbx-model/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • fbx-model 7 | 8 | 9 | 10 | 11 | 12 | 18 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Sandbox • VR Examples 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

WebVR Resources

19 |

by Don McCurdy (@donrmccurdy)

20 | 21 |

Projects

22 |
    23 |
  • 24 | A-Frame Extras 25 | — Add-ons and helpers for A-Frame VR. 26 |
  • 27 |
  • 28 | A-Frame Physics System 29 | — Physics system for A-Frame VR, built on CANNON.js. 30 |
  • 31 |
  • 32 | A-Frame Leap Hands 33 | — A-Frame VR component for Leap Motion. 34 |
  • 35 |
  • 36 | Proxy Controls 37 | — Connect input devices from your desktop to your mobile phone with WebRTC. 38 |
  • 39 |
  • 40 | glTF Viewer 41 | — Preview glTF models using three.js and a drag-and-drop interface. 42 |
  • 43 |
44 | 45 |

Examples

46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 |
SceneTagsVR-friendly?
Animationanimation, models
Animation Controlsanimation, models
Checkpointscursor, locomotion
Castlenavmesh, locomotion, hidden virtual joysticks on touch devices
Environment Mapcube-env-map
Tracked controlsphysics, tracked controls
Islandmodels
Tubesa-tube
fbx-modelfbx-model
102 | 103 |

Acknowledgements

104 | 105 |

T-Rex model in animation-controls example from 106 | Quaternius under CC0 license, 107 | converted to GLB using fbx-gltf-pipeline

108 |
109 | 110 | 111 | -------------------------------------------------------------------------------- /examples/island/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Island 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /examples/tubes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Tubes 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /examples/vive/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples • Tracked controls 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('./src/controls'); 2 | require('./src/loaders'); 3 | require('./src/misc'); 4 | require('./src/pathfinding'); 5 | require('./src/primitives'); 6 | -------------------------------------------------------------------------------- /lib/GamepadButton.js: -------------------------------------------------------------------------------- 1 | module.exports = Object.assign(function GamepadButton () {}, { 2 | FACE_1: 0, 3 | FACE_2: 1, 4 | FACE_3: 2, 5 | FACE_4: 3, 6 | 7 | L_SHOULDER_1: 4, 8 | R_SHOULDER_1: 5, 9 | L_SHOULDER_2: 6, 10 | R_SHOULDER_2: 7, 11 | 12 | SELECT: 8, 13 | START: 9, 14 | 15 | DPAD_UP: 12, 16 | DPAD_DOWN: 13, 17 | DPAD_LEFT: 14, 18 | DPAD_RIGHT: 15, 19 | 20 | VENDOR: 16, 21 | }); 22 | -------------------------------------------------------------------------------- /lib/GamepadButtonEvent.js: -------------------------------------------------------------------------------- 1 | function GamepadButtonEvent (type, index, details) { 2 | this.type = type; 3 | this.index = index; 4 | this.pressed = details.pressed; 5 | this.value = details.value; 6 | } 7 | 8 | module.exports = GamepadButtonEvent; 9 | -------------------------------------------------------------------------------- /lib/fetch-script.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Source: https://github.com/Adobe-Marketing-Cloud/fetch-script 3 | */ 4 | 5 | function getScriptId() { 6 | return 'script_' + Date.now() + '_' + Math.ceil(Math.random() * 100000); 7 | } 8 | 9 | function createScript(url, id) { 10 | var script = document.createElement('script'); 11 | script.type = 'text/javascript'; 12 | script.async = true; 13 | script.id = id; 14 | script.src = url; 15 | 16 | return script; 17 | } 18 | 19 | function removeScript(id) { 20 | const script = document.getElementById(id); 21 | const parent = script.parentNode; 22 | 23 | try { 24 | parent && parent.removeChild(script); 25 | } catch (e) { 26 | // ignore 27 | } 28 | } 29 | 30 | function appendScript(script) { 31 | const firstScript = document.getElementsByTagName('script')[0]; 32 | firstScript.parentNode.insertBefore(script, firstScript); 33 | } 34 | 35 | function fetchScriptInternal(url, options, Promise) { 36 | return new Promise(function(resolve, reject) { 37 | const timeout = options.timeout || 5000; 38 | const scriptId = getScriptId(); 39 | const script = createScript(url, scriptId); 40 | 41 | const timeoutId = setTimeout(function() { 42 | reject(new Error('Script request to ' + url + ' timed out')); 43 | 44 | removeScript(scriptId); 45 | }, timeout); 46 | 47 | const disableTimeout = function(timeoutId) { clearTimeout(timeoutId); }; 48 | 49 | script.addEventListener('load', function(e) { 50 | resolve({ok: true}); 51 | 52 | disableTimeout(timeoutId); 53 | removeScript(scriptId); 54 | }); 55 | 56 | script.addEventListener('error', function(e) { 57 | reject(new Error('Script request to ' + url + ' failed ' + e)); 58 | 59 | disableTimeout(timeoutId); 60 | removeScript(scriptId); 61 | }); 62 | 63 | appendScript(script); 64 | }); 65 | } 66 | 67 | function fetchScript(settings) { 68 | settings = settings || {}; 69 | return function (url, options) { 70 | options = options || {}; 71 | return fetchScriptInternal(url, options, settings.Promise || Promise); 72 | }; 73 | } 74 | 75 | module.exports = fetchScript; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aframe-extras", 3 | "version": "7.5.4", 4 | "description": "Add-ons and examples for A-Frame VR.", 5 | "author": "Don McCurdy ", 6 | "license": "MIT", 7 | "main": "./index.js", 8 | "exports": { 9 | ".": "./index.js", 10 | "./controls/*": "./src/controls/*", 11 | "./loaders/*": "./src/loaders/*", 12 | "./misc/*": "./src/misc/*", 13 | "./pathfinding/*": "./src/pathfinding/*", 14 | "./primitives/*": "./src/primitives/*" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/c-frame/aframe-extras.git" 19 | }, 20 | "scripts": { 21 | "dev": "webpack serve", 22 | "dist": "npm run dist:min && npm run dist:max", 23 | "dist:max": "webpack", 24 | "dist:min": "cross-env NODE_ENV=production webpack" 25 | }, 26 | "dependencies": { 27 | "nipplejs": "^0.10.2", 28 | "three": "^0.164.0", 29 | "three-pathfinding": "^1.3.0" 30 | }, 31 | "devDependencies": { 32 | "cross-env": "^7.0.3", 33 | "webpack": "^5.91.0", 34 | "webpack-cli": "^5.1.4", 35 | "webpack-dev-server": "^5.0.4" 36 | }, 37 | "keywords": [ 38 | "aframe", 39 | "a-frame", 40 | "aframe-component", 41 | "vr", 42 | "webgl", 43 | "webvr" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /src/controls/README.md: -------------------------------------------------------------------------------- 1 | # Controls 2 | 3 | Extensible movement/rotation/hotkey controls, with support for a variety of input devices. 4 | 5 | - **movement-controls**: Collection of locomotion controls, which can switch between input devices as they become active. Automatically includes the following components: 6 | + **keyboard-controls**: WASD + arrow controls for movement, and more. 7 | + **touch-controls**: Touch screen (or Cardboard button) to move forward. 8 | + **gamepad-controls**: Gamepad-based rotation and movement. 9 | + **trackpad-controls**: Trackpad-based movement. 10 | - **checkpoint-controls**: Move to checkpoints created with the `checkpoint` component. *Not included by default with `movement-controls`, but may be added as shown in examples.* 11 | - **nipple-controls**: Virtual joysticks for rotation and movement on touch devices. This is using the [nipplejs](https://github.com/yoannmoinet/nipplejs) library. 12 | 13 | For the Cardboard button, this was tested and working on both Chrome Android and Safari iPhone with aframe 1.4.2. 14 | On iPhone you need `≤a-scene vr-mode-ui="cardboardModeEnabled:true">` for the VR button to show up. 15 | On Chrome Android you also need the `cardboardModeEnabled:true` option but this is to register the selectstart/selectend event listener to properly use the Cardboard button. 16 | Be aware that the selectstart/selectend event listener has the side effect to move forward when you press the trigger button with VR controllers. 17 | 18 | ## Usage 19 | 20 | The `movement-controls` component requires the use of a camera "rig" wrapping the camera element. The rig may be assigned any position within your scene, and should be placed at ground level. The camera should only have height offset (used for devices without positional tracking) such as `0 1.6 0`. 21 | 22 | Basic movement: 23 | 24 | ```html 25 | 28 | 31 | 32 | ``` 33 | 34 | With checkpoints, and other input methods disabled: 35 | 36 | ```html 37 | 40 | 43 | 44 | 45 | ``` 46 | 47 | With gamepad, keyboard and nipple controls with virtual joysticks: 48 | 49 | ```html 50 | 53 | 56 | 57 | 58 | ``` 59 | 60 | The default for `nipple-controls` is two joysticks and `dynamic` mode (joysticks hidden, not on fixed position). 61 | The `static` (joysticks visible, fixed position) and `semi` modes are also supported. 62 | If you only want the joystick for movement on the right, you can use: 63 | 64 | ```html 65 | nipple-controls="mode: static; lookJoystickEnabled: false; moveJoystickPosition: right" 66 | ``` 67 | 68 | With navigation mesh: 69 | 70 | ```html 71 | 72 | 75 | 76 | 77 | ``` 78 | 79 | With physics-based movement. 80 | 81 | > **WARNING** *Using physics for movement is unstable and performs poorly. When preventing players from passing through obstacles, use a navigation mesh instead whenever possible.* 82 | 83 | ```html 84 | 85 | 88 | 89 | ``` 90 | 91 | ## Options 92 | 93 | | Property | Default | Description | 94 | |--------------------|---------|-------------| 95 | | enabled | true | Enables/disables movement controls. | 96 | | controls | gamepad, keyboard, touch | Ordered list of controls to be injected. | 97 | | speed | 0.3 | Movement speed. | 98 | | fly | false | Whether vertical movement is enabled. | 99 | | constrainToNavMesh | false | Whether to use navigation system to clamp movement. | 100 | | camera | [camera] | Camera element used for heading of the camera rig. | 101 | 102 | 103 | ## Events 104 | 105 | - The `movement-controls` component emits a `moved` event on the entity with evt.detail.velocity (vec3). 106 | 107 | ## Customizing movement-controls 108 | 109 | To implement your custom controls, define a component and override one or more methods: 110 | 111 | | Method | Type | Required | 112 | |----------------------------------------------------|----------|----------| 113 | | isVelocityActive() : boolean | Movement | Yes | 114 | | getVelocityDelta(deltaMS : number) : THREE.Vector3 | Movement | No | 115 | | getPositionDelta(deltaMS : number) : THREE.Vector3 | Movement | No | 116 | 117 | Example: 118 | 119 | ```js 120 | AFRAME.registerComponent('custom-controls', { 121 | isVelocityActive: function () { 122 | return Math.random() < 0.25; 123 | }, 124 | getPositionDelta: function () { 125 | return new THREE.Vector3(1, 0, 0); 126 | } 127 | }); 128 | ``` 129 | 130 | ## Other Controls 131 | 132 | I've written standalone components for several other control components. These do not work with `movement-controls`, and are older and less well maintained. 133 | 134 | - [gamepad-controls](https://github.com/donmccurdy/aframe-gamepad-controls): A more advanced standalone gamepad controller than the version in this package. 135 | - [keyboard-controls](https://github.com/donmccurdy/aframe-keyboard-controls): A more advanced standalone keyboard controller than the version in this package. 136 | 137 | ## Mobile + Desktop Input Devices 138 | 139 | Connect input devices from your desktop to your mobile phone with WebRTC, using [ProxyControls.js](https://proxy-controls.donmccurdy.com). 140 | 141 | ## Mobile Gamepad Support 142 | 143 | See my [separate overview of gamepad support](https://gist.github.com/donmccurdy/cf336a8b88ba0f10991d4aab936cc28b). 144 | -------------------------------------------------------------------------------- /src/controls/checkpoint-controls.js: -------------------------------------------------------------------------------- 1 | const EPS = 0.1; 2 | 3 | module.exports = AFRAME.registerComponent('checkpoint-controls', { 4 | schema: { 5 | enabled: {default: true}, 6 | mode: {default: 'teleport', oneOf: ['teleport', 'animate']}, 7 | animateSpeed: {default: 3.0} 8 | }, 9 | 10 | init: function () { 11 | this.active = true; 12 | this.checkpoint = null; 13 | 14 | this.isNavMeshConstrained = false; 15 | 16 | this.offset = new THREE.Vector3(); 17 | this.position = new THREE.Vector3(); 18 | this.targetPosition = new THREE.Vector3(); 19 | }, 20 | 21 | play: function () { this.active = true; }, 22 | pause: function () { this.active = false; }, 23 | 24 | setCheckpoint: function (checkpoint) { 25 | const el = this.el; 26 | 27 | if (!this.active) return; 28 | if (this.checkpoint === checkpoint) return; 29 | 30 | if (this.checkpoint) { 31 | el.emit('navigation-end', {checkpoint: this.checkpoint}); 32 | } 33 | 34 | this.checkpoint = checkpoint; 35 | this.sync(); 36 | 37 | // Ignore new checkpoint if we're already there. 38 | if (this.position.distanceTo(this.targetPosition) < EPS) { 39 | this.checkpoint = null; 40 | return; 41 | } 42 | 43 | el.emit('navigation-start', {checkpoint: checkpoint}); 44 | 45 | if (this.data.mode === 'teleport') { 46 | this.el.setAttribute('position', this.targetPosition); 47 | this.checkpoint = null; 48 | el.emit('navigation-end', {checkpoint: checkpoint}); 49 | el.components['movement-controls'].updateNavLocation(); 50 | } 51 | }, 52 | 53 | isVelocityActive: function () { 54 | return !!(this.active && this.checkpoint); 55 | }, 56 | 57 | getVelocity: function () { 58 | if (!this.active) return; 59 | 60 | const data = this.data; 61 | const offset = this.offset; 62 | const position = this.position; 63 | const targetPosition = this.targetPosition; 64 | const checkpoint = this.checkpoint; 65 | 66 | this.sync(); 67 | if (position.distanceTo(targetPosition) < EPS) { 68 | this.checkpoint = null; 69 | this.el.emit('navigation-end', {checkpoint: checkpoint}); 70 | return offset.set(0, 0, 0); 71 | } 72 | offset.setLength(data.animateSpeed); 73 | return offset; 74 | }, 75 | 76 | sync: function () { 77 | const offset = this.offset; 78 | const position = this.position; 79 | const targetPosition = this.targetPosition; 80 | 81 | position.copy(this.el.getAttribute('position')); 82 | this.checkpoint.object3D.getWorldPosition(targetPosition); 83 | targetPosition.add(this.checkpoint.components.checkpoint.getOffset()); 84 | offset.copy(targetPosition).sub(position); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /src/controls/index.js: -------------------------------------------------------------------------------- 1 | require('./checkpoint-controls'); 2 | require('./gamepad-controls'); 3 | require('./keyboard-controls'); 4 | require('./touch-controls'); 5 | require('./movement-controls'); 6 | require('./trackpad-controls'); 7 | require('./nipple-controls'); 8 | -------------------------------------------------------------------------------- /src/controls/keyboard-controls.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | /* eslint-disable no-prototype-builtins */ 3 | require('../../lib/keyboard.polyfill'); 4 | 5 | const PROXY_FLAG = '__keyboard-controls-proxy'; 6 | 7 | const KeyboardEvent = window.KeyboardEvent; 8 | 9 | /** 10 | * Keyboard Controls component. 11 | * 12 | * Stripped-down version of: https://github.com/donmccurdy/aframe-keyboard-controls 13 | * 14 | * Bind keyboard events to components, or control your entities with the WASD keys. 15 | * 16 | * Why use KeyboardEvent.code? "This is set to a string representing the key that was pressed to 17 | * generate the KeyboardEvent, without taking the current keyboard layout (e.g., QWERTY vs. 18 | * Dvorak), locale (e.g., English vs. French), or any modifier keys into account. This is useful 19 | * when you care about which physical key was pressed, rather thanwhich character it corresponds 20 | * to. For example, if you’re a writing a game, you might want a certain set of keys to move the 21 | * player in different directions, and that mapping should ideally be independent of keyboard 22 | * layout. See: https://developers.google.com/web/updates/2016/04/keyboardevent-keys-codes 23 | * 24 | * @namespace wasd-controls 25 | * keys the entity moves and if you release it will stop. Easing simulates friction. 26 | * to the entity when pressing the keys. 27 | * @param {bool} [enabled=true] - To completely enable or disable the controls 28 | */ 29 | module.exports = AFRAME.registerComponent('keyboard-controls', { 30 | schema: { 31 | enabled: { default: true }, 32 | debug: { default: false } 33 | }, 34 | 35 | init: function () { 36 | this.dVelocity = new THREE.Vector3(); 37 | this.localKeys = {}; 38 | this.listeners = { 39 | keydown: this.onKeyDown.bind(this), 40 | keyup: this.onKeyUp.bind(this), 41 | blur: this.onBlur.bind(this), 42 | onContextMenu: this.onContextMenu.bind(this), 43 | }; 44 | }, 45 | 46 | /******************************************************************* 47 | * Movement 48 | */ 49 | 50 | isVelocityActive: function () { 51 | return this.data.enabled && !!Object.keys(this.getKeys()).length; 52 | }, 53 | 54 | getVelocityDelta: function () { 55 | const data = this.data; 56 | const keys = this.getKeys(); 57 | 58 | this.dVelocity.set(0, 0, 0); 59 | if (data.enabled) { 60 | if (keys.KeyW || keys.ArrowUp) { this.dVelocity.z -= 1; } 61 | if (keys.KeyA || keys.ArrowLeft) { this.dVelocity.x -= 1; } 62 | if (keys.KeyS || keys.ArrowDown) { this.dVelocity.z += 1; } 63 | if (keys.KeyD || keys.ArrowRight) { this.dVelocity.x += 1; } 64 | 65 | // Move faster when the shift key is down. 66 | if (keys.ShiftLeft) { this.dVelocity = this.dVelocity.multiplyScalar(2); } 67 | } 68 | 69 | return this.dVelocity.clone(); 70 | }, 71 | 72 | /******************************************************************* 73 | * Events 74 | */ 75 | 76 | play: function () { 77 | this.attachEventListeners(); 78 | }, 79 | 80 | pause: function () { 81 | this.removeEventListeners(); 82 | }, 83 | 84 | attachEventListeners: function () { 85 | window.addEventListener("contextmenu", this.listeners.onContextMenu, false); 86 | window.addEventListener("keydown", this.listeners.keydown, false); 87 | window.addEventListener("keyup", this.listeners.keyup, false); 88 | window.addEventListener("blur", this.listeners.blur, false); 89 | }, 90 | 91 | onContextMenu: function () { 92 | for (const code in this.localKeys) { 93 | if (this.localKeys.hasOwnProperty(code)) { 94 | delete this.localKeys[code]; 95 | } 96 | } 97 | }, 98 | 99 | removeEventListeners: function () { 100 | window.removeEventListener('keydown', this.listeners.keydown); 101 | window.removeEventListener('keyup', this.listeners.keyup); 102 | window.removeEventListener('blur', this.listeners.blur); 103 | }, 104 | 105 | onKeyDown: function (event) { 106 | if (AFRAME.utils.shouldCaptureKeyEvent(event)) { 107 | this.localKeys[event.code] = true; 108 | this.emit(event); 109 | } 110 | }, 111 | 112 | onKeyUp: function (event) { 113 | if (AFRAME.utils.shouldCaptureKeyEvent(event)) { 114 | delete this.localKeys[event.code]; 115 | this.emit(event); 116 | } 117 | }, 118 | 119 | onBlur: function () { 120 | for (const code in this.localKeys) { 121 | if (this.localKeys.hasOwnProperty(code)) { 122 | delete this.localKeys[code]; 123 | } 124 | } 125 | }, 126 | 127 | emit: function (event) { 128 | // TODO - keydown only initially? 129 | // TODO - where the f is the spacebar 130 | 131 | // Emit original event. 132 | if (PROXY_FLAG in event) { 133 | // TODO - Method never triggered. 134 | this.el.emit(event.type, event); 135 | } 136 | 137 | // Emit convenience event, identifying key. 138 | this.el.emit(event.type + ':' + event.code, new KeyboardEvent(event.type, event)); 139 | if (this.data.debug) console.log(event.type + ':' + event.code); 140 | }, 141 | 142 | /******************************************************************* 143 | * Accessors 144 | */ 145 | 146 | isPressed: function (code) { 147 | return code in this.getKeys(); 148 | }, 149 | 150 | getKeys: function () { 151 | if (this.isProxied()) { 152 | return this.el.sceneEl.components['proxy-controls'].getKeyboard(); 153 | } 154 | return this.localKeys; 155 | }, 156 | 157 | isProxied: function () { 158 | const proxyControls = this.el.sceneEl.components['proxy-controls']; 159 | return proxyControls && proxyControls.isConnected(); 160 | } 161 | 162 | }); 163 | -------------------------------------------------------------------------------- /src/controls/movement-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Movement Controls 3 | * 4 | * @author Don McCurdy 5 | */ 6 | 7 | const COMPONENT_SUFFIX = '-controls'; 8 | const MAX_DELTA = 0.2; // ms 9 | const EPS = 10e-6; 10 | const MOVED = 'moved'; 11 | 12 | module.exports = AFRAME.registerComponent('movement-controls', { 13 | 14 | /******************************************************************* 15 | * Schema 16 | */ 17 | 18 | dependencies: ['rotation'], 19 | 20 | schema: { 21 | enabled: { default: true }, 22 | controls: { default: ['gamepad', 'trackpad', 'keyboard', 'touch'] }, 23 | speed: { default: 0.3, min: 0 }, 24 | fly: { default: false }, 25 | constrainToNavMesh: { default: false }, 26 | camera: { default: '[movement-controls] [camera]', type: 'selector' } 27 | }, 28 | 29 | /******************************************************************* 30 | * Lifecycle 31 | */ 32 | 33 | init: function () { 34 | const el = this.el; 35 | if (!this.data.camera) { 36 | this.data.camera = el.querySelector('[camera]'); 37 | } 38 | this.velocityCtrl = null; 39 | 40 | this.velocity = new THREE.Vector3(); 41 | this.heading = new THREE.Quaternion(); 42 | this.eventDetail = {}; 43 | 44 | // Navigation 45 | this.navGroup = null; 46 | this.navNode = null; 47 | 48 | if (el.sceneEl.hasLoaded) { 49 | this.injectControls(); 50 | } else { 51 | el.sceneEl.addEventListener('loaded', this.injectControls.bind(this)); 52 | } 53 | }, 54 | 55 | update: function (prevData) { 56 | const el = this.el; 57 | const data = this.data; 58 | const nav = el.sceneEl.systems.nav; 59 | if (el.sceneEl.hasLoaded) { 60 | this.injectControls(); 61 | } 62 | if (nav && data.constrainToNavMesh !== prevData.constrainToNavMesh) { 63 | data.constrainToNavMesh 64 | ? nav.addAgent(this) 65 | : nav.removeAgent(this); 66 | } 67 | if (data.enabled !== prevData.enabled) { 68 | // Propagate the enabled change to all controls 69 | for (let i = 0; i < data.controls.length; i++) { 70 | const name = data.controls[i] + COMPONENT_SUFFIX; 71 | this.el.setAttribute(name, { enabled: this.data.enabled }); 72 | } 73 | } 74 | }, 75 | 76 | injectControls: function () { 77 | const data = this.data; 78 | 79 | for (let i = 0; i < data.controls.length; i++) { 80 | const name = data.controls[i] + COMPONENT_SUFFIX; 81 | this.el.setAttribute(name, { enabled: this.data.enabled }); 82 | } 83 | }, 84 | 85 | updateNavLocation: function () { 86 | this.navGroup = null; 87 | this.navNode = null; 88 | }, 89 | 90 | /******************************************************************* 91 | * Tick 92 | */ 93 | 94 | tick: (function () { 95 | const start = new THREE.Vector3(); 96 | const end = new THREE.Vector3(); 97 | const clampedEnd = new THREE.Vector3(); 98 | 99 | return function (t, dt) { 100 | if (!dt) return; 101 | 102 | const el = this.el; 103 | const data = this.data; 104 | 105 | if (!data.enabled) return; 106 | 107 | this.updateVelocityCtrl(); 108 | const velocityCtrl = this.velocityCtrl; 109 | const velocity = this.velocity; 110 | 111 | if (!velocityCtrl) return; 112 | 113 | // Update velocity. If FPS is too low, reset. 114 | if (dt / 1000 > MAX_DELTA) { 115 | velocity.set(0, 0, 0); 116 | } else { 117 | this.updateVelocity(dt); 118 | } 119 | 120 | if (data.constrainToNavMesh 121 | && velocityCtrl.isNavMeshConstrained !== false) { 122 | 123 | if (velocity.lengthSq() < EPS) return; 124 | 125 | start.copy(el.object3D.position); 126 | end 127 | .copy(velocity) 128 | .multiplyScalar(dt / 1000) 129 | .add(start); 130 | 131 | const nav = el.sceneEl.systems.nav; 132 | this.navGroup = this.navGroup === null ? nav.getGroup(start) : this.navGroup; 133 | this.navNode = this.navNode || nav.getNode(start, this.navGroup); 134 | this.navNode = nav.clampStep(start, end, this.navGroup, this.navNode, clampedEnd); 135 | el.object3D.position.copy(clampedEnd); 136 | } else if (el.hasAttribute('velocity')) { 137 | el.setAttribute('velocity', velocity); 138 | } else { 139 | el.object3D.position.x += velocity.x * dt / 1000; 140 | el.object3D.position.y += velocity.y * dt / 1000; 141 | el.object3D.position.z += velocity.z * dt / 1000; 142 | } 143 | 144 | }; 145 | }()), 146 | 147 | /******************************************************************* 148 | * Movement 149 | */ 150 | 151 | updateVelocityCtrl: function () { 152 | const data = this.data; 153 | if (data.enabled) { 154 | for (let i = 0, l = data.controls.length; i < l; i++) { 155 | const control = this.el.components[data.controls[i] + COMPONENT_SUFFIX]; 156 | if (control && control.isVelocityActive()) { 157 | this.velocityCtrl = control; 158 | return; 159 | } 160 | } 161 | this.velocityCtrl = null; 162 | } 163 | }, 164 | 165 | updateVelocity: (function () { 166 | const vector2 = new THREE.Vector2(); 167 | const quaternion = new THREE.Quaternion(); 168 | 169 | return function (dt) { 170 | let dVelocity; 171 | const el = this.el; 172 | const control = this.velocityCtrl; 173 | const velocity = this.velocity; 174 | const data = this.data; 175 | 176 | if (control) { 177 | if (control.getVelocityDelta) { 178 | dVelocity = control.getVelocityDelta(dt); 179 | } else if (control.getVelocity) { 180 | velocity.copy(control.getVelocity()); 181 | return; 182 | } else if (control.getPositionDelta) { 183 | velocity.copy(control.getPositionDelta(dt).multiplyScalar(1000 / dt)); 184 | return; 185 | } else { 186 | throw new Error('Incompatible movement controls: ', control); 187 | } 188 | } 189 | 190 | if (el.hasAttribute('velocity') && !data.constrainToNavMesh) { 191 | velocity.copy(this.el.getAttribute('velocity')); 192 | } 193 | 194 | if (dVelocity && data.enabled) { 195 | const cameraEl = data.camera; 196 | 197 | // Rotate to heading 198 | quaternion.copy(cameraEl.object3D.quaternion); 199 | quaternion.premultiply(el.object3D.quaternion); 200 | dVelocity.applyQuaternion(quaternion); 201 | 202 | const factor = dVelocity.length(); 203 | if (data.fly) { 204 | velocity.copy(dVelocity); 205 | velocity.multiplyScalar(this.data.speed * 16.66667); 206 | } else { 207 | vector2.set(dVelocity.x, dVelocity.z); 208 | vector2.setLength(factor * this.data.speed * 16.66667); 209 | velocity.x = vector2.x; 210 | velocity.y = 0; 211 | velocity.z = vector2.y; 212 | } 213 | if (velocity.x !== 0 || velocity.y !== 0 || velocity.z !== 0) { 214 | this.eventDetail.velocity = velocity; 215 | this.el.emit(MOVED, this.eventDetail); 216 | } 217 | } 218 | }; 219 | 220 | }()) 221 | }); 222 | -------------------------------------------------------------------------------- /src/controls/nipple-controls.js: -------------------------------------------------------------------------------- 1 | /* global AFRAME, THREE */ 2 | import nipplejs from "nipplejs"; 3 | 4 | AFRAME.registerComponent("nipple-controls", { 5 | schema: { 6 | enabled: { default: true }, 7 | mode: { default: "dynamic", oneOf: ["static", "semi", "dynamic"] }, 8 | rotationSensitivity: { default: 1.0 }, 9 | moveJoystickEnabled: { default: true }, 10 | lookJoystickEnabled: { default: true }, 11 | sideMargin: { default: "30px" }, 12 | bottomMargin: { default: "70px" }, 13 | moveJoystickPosition: { default: "left", oneOf: ["left", "right"] }, 14 | lookJoystickPosition: { default: "right", oneOf: ["left", "right"] }, 15 | }, 16 | 17 | init() { 18 | this.dVelocity = new THREE.Vector3(); 19 | this.lookVector = new THREE.Vector2(); 20 | const lookControls = this.el.querySelector("[look-controls]").components["look-controls"]; 21 | this.pitchObject = lookControls.pitchObject; 22 | this.yawObject = lookControls.yawObject; 23 | this.rigRotation = this.el.object3D.rotation; 24 | this.moveData = undefined; 25 | this.lookData = undefined; 26 | this.moving = false; 27 | this.rotating = false; 28 | }, 29 | 30 | update(oldData) { 31 | if ( 32 | this.data.moveJoystickPosition !== oldData.moveJoystickPosition || 33 | this.data.sideMargin !== oldData.sideMargin || 34 | this.data.bottomMargin !== oldData.bottomMargin || 35 | this.data.mode !== oldData.mode 36 | ) { 37 | this.removeMoveJoystick(); 38 | } 39 | if ( 40 | this.data.lookJoystickPosition !== oldData.lookJoystickPosition || 41 | this.data.sideMargin !== oldData.sideMargin || 42 | this.data.bottomMargin !== oldData.bottomMargin || 43 | this.data.mode !== oldData.mode 44 | ) { 45 | this.removeLookJoystick(); 46 | } 47 | if (this.data.enabled && this.data.moveJoystickEnabled) { 48 | this.createMoveJoystick(); 49 | } else { 50 | this.removeMoveJoystick(); 51 | } 52 | if (this.data.enabled && this.data.lookJoystickEnabled) { 53 | this.createLookJoystick(); 54 | } else { 55 | this.removeLookJoystick(); 56 | } 57 | }, 58 | 59 | pause() { 60 | this.moving = false; 61 | this.rotating = false; 62 | }, 63 | 64 | remove() { 65 | this.removeMoveJoystick(); 66 | this.removeLookJoystick(); 67 | }, 68 | 69 | isVelocityActive() { 70 | return this.data.enabled && this.moving; 71 | }, 72 | 73 | getVelocityDelta() { 74 | this.dVelocity.set(0, 0, 0); 75 | if (this.isVelocityActive()) { 76 | const force = this.moveData.force < 1 ? this.moveData.force : 1; 77 | const angle = this.moveData.angle.radian; 78 | const x = Math.cos(angle) * force; 79 | const z = -Math.sin(angle) * force; 80 | this.dVelocity.set(x, 0, z); 81 | } 82 | return this.dVelocity; // We don't do a clone() here, the Vector3 will be modified by the calling code but that's fine. 83 | }, 84 | 85 | isRotationActive() { 86 | return this.data.enabled && this.rotating; 87 | }, 88 | 89 | updateRotation(dt) { 90 | if (!this.isRotationActive()) return; 91 | 92 | const force = this.lookData.force < 1 ? this.lookData.force : 1; 93 | const angle = this.lookData.angle.radian; 94 | const lookVector = this.lookVector; 95 | lookVector.x = Math.cos(angle) * force; 96 | lookVector.y = Math.sin(angle) * force; 97 | lookVector.multiplyScalar((this.data.rotationSensitivity * dt) / 1000); 98 | 99 | this.yawObject.rotation.y -= lookVector.x; 100 | let x = this.pitchObject.rotation.x + lookVector.y; 101 | x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, x)); 102 | this.pitchObject.rotation.x = x; 103 | }, 104 | 105 | tick: function (t, dt) { 106 | this.updateRotation(dt); 107 | }, 108 | 109 | initLeftZone() { 110 | const leftZone = document.createElement("div"); 111 | leftZone.setAttribute("id", "joystickLeftZone"); 112 | leftZone.setAttribute( 113 | "style", 114 | `position:absolute;${this.data.moveJoystickPosition}:${this.data.sideMargin};bottom:${this.data.bottomMargin};z-index:1` 115 | ); 116 | document.body.appendChild(leftZone); 117 | this.leftZone = leftZone; 118 | }, 119 | 120 | initRightZone() { 121 | const rightZone = document.createElement("div"); 122 | rightZone.setAttribute("id", "joystickRightZone"); 123 | rightZone.setAttribute( 124 | "style", 125 | `position:absolute;${this.data.lookJoystickPosition}:${this.data.sideMargin};bottom:${this.data.bottomMargin};z-index:1` 126 | ); 127 | document.body.appendChild(rightZone); 128 | this.rightZone = rightZone; 129 | }, 130 | 131 | createMoveJoystick() { 132 | if (this.moveJoystick) return; 133 | this.initLeftZone(); 134 | const options = { 135 | mode: this.data.mode, 136 | zone: this.leftZone, 137 | color: "white", 138 | fadeTime: 0, 139 | }; 140 | this.leftZone.style.width = "100px"; 141 | if (this.data.mode === "static") { 142 | this.leftZone.style.height = "100px"; 143 | options.position = { left: "50%", bottom: "50%" }; 144 | } else { 145 | this.leftZone.style.height = "400px"; 146 | } 147 | 148 | this.moveJoystick = nipplejs.create(options); 149 | this.moveJoystick.on("move", (evt, data) => { 150 | this.moveData = data; 151 | this.moving = true; 152 | }); 153 | this.moveJoystick.on("end", (evt, data) => { 154 | this.moving = false; 155 | }); 156 | }, 157 | 158 | createLookJoystick() { 159 | if (this.lookJoystick) return; 160 | this.initRightZone(); 161 | const options = { 162 | mode: this.data.mode, 163 | zone: this.rightZone, 164 | color: "white", 165 | fadeTime: 0, 166 | }; 167 | this.rightZone.style.width = "100px"; 168 | if (this.data.mode === "static") { 169 | this.rightZone.style.height = "100px"; 170 | options.position = { left: "50%", bottom: "50%" }; 171 | } else { 172 | this.rightZone.style.height = "400px"; 173 | } 174 | 175 | this.lookJoystick = nipplejs.create(options); 176 | this.lookJoystick.on("move", (evt, data) => { 177 | this.lookData = data; 178 | this.rotating = true; 179 | }); 180 | this.lookJoystick.on("end", (evt, data) => { 181 | this.rotating = false; 182 | }); 183 | }, 184 | 185 | removeMoveJoystick() { 186 | if (this.moveJoystick) { 187 | this.moveJoystick.destroy(); 188 | this.moveJoystick = undefined; 189 | } 190 | 191 | this.moveData = undefined; 192 | 193 | if (this.leftZone && this.leftZone.parentNode) { 194 | this.leftZone.remove(); 195 | this.leftZone = undefined; 196 | } 197 | }, 198 | 199 | removeLookJoystick() { 200 | if (this.lookJoystick) { 201 | this.lookJoystick.destroy(); 202 | this.lookJoystick = undefined; 203 | } 204 | 205 | this.lookData = undefined; 206 | 207 | if (this.rightZone && this.rightZone.parentNode) { 208 | this.rightZone.remove(); 209 | this.rightZone = undefined; 210 | } 211 | }, 212 | }); 213 | -------------------------------------------------------------------------------- /src/controls/touch-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Touch-to-move-forward controls for mobile. 3 | */ 4 | module.exports = AFRAME.registerComponent('touch-controls', { 5 | schema: { 6 | enabled: { default: true }, 7 | reverseEnabled: { default: true } 8 | }, 9 | 10 | init: function () { 11 | this.dVelocity = new THREE.Vector3(); 12 | this.bindMethods(); 13 | this.direction = 0; 14 | }, 15 | 16 | play: function () { 17 | this.addEventListeners(); 18 | }, 19 | 20 | pause: function () { 21 | this.removeEventListeners(); 22 | this.dVelocity.set(0, 0, 0); 23 | }, 24 | 25 | remove: function () { 26 | this.pause(); 27 | }, 28 | 29 | addEventListeners: function () { 30 | const sceneEl = this.el.sceneEl; 31 | const canvasEl = sceneEl.canvas; 32 | 33 | if (!canvasEl) { 34 | sceneEl.addEventListener('render-target-loaded', this.addEventListeners.bind(this)); 35 | return; 36 | } 37 | 38 | canvasEl.addEventListener('touchstart', this.onTouchStart, {passive: true}); 39 | canvasEl.addEventListener('touchend', this.onTouchEnd, {passive: true}); 40 | const vrModeUI = sceneEl.getAttribute('vr-mode-ui'); 41 | if (vrModeUI && vrModeUI.cardboardModeEnabled) { 42 | sceneEl.addEventListener('enter-vr', this.onEnterVR); 43 | } 44 | }, 45 | 46 | removeEventListeners: function () { 47 | const canvasEl = this.el.sceneEl && this.el.sceneEl.canvas; 48 | if (!canvasEl) { return; } 49 | 50 | canvasEl.removeEventListener('touchstart', this.onTouchStart); 51 | canvasEl.removeEventListener('touchend', this.onTouchEnd); 52 | this.el.sceneEl.removeEventListener('enter-vr', this.onEnterVR) 53 | }, 54 | 55 | isVelocityActive: function () { 56 | return this.data.enabled && !!this.direction; 57 | }, 58 | 59 | getVelocityDelta: function () { 60 | this.dVelocity.z = this.direction; 61 | return this.dVelocity.clone(); 62 | }, 63 | 64 | bindMethods: function () { 65 | this.onTouchStart = this.onTouchStart.bind(this); 66 | this.onTouchEnd = this.onTouchEnd.bind(this); 67 | this.onEnterVR = this.onEnterVR.bind(this); 68 | }, 69 | 70 | onTouchStart: function (e) { 71 | this.direction = -1; 72 | if (this.data.reverseEnabled && e.touches && e.touches.length === 2) { 73 | this.direction = 1; 74 | } 75 | e.preventDefault(); 76 | }, 77 | 78 | onTouchEnd: function (e) { 79 | this.direction = 0; 80 | e.preventDefault(); 81 | }, 82 | 83 | onEnterVR: function () { 84 | // This is to make the Cardboard button on Chrome Android working 85 | const xrSession = this.el.sceneEl.xrSession; 86 | if (!xrSession) { return; } 87 | xrSession.addEventListener('selectstart', this.onTouchStart); 88 | xrSession.addEventListener('selectend', this.onTouchEnd); 89 | } 90 | }); 91 | -------------------------------------------------------------------------------- /src/controls/trackpad-controls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3dof (Gear VR, Daydream) controls for mobile. 3 | */ 4 | module.exports = AFRAME.registerComponent('trackpad-controls', { 5 | schema: { 6 | enabled: { default: true }, 7 | enableNegX: { default: true }, 8 | enablePosX: { default: true }, 9 | enableNegZ: { default: true }, 10 | enablePosZ: { default: true }, 11 | mode: { default: 'touch', oneOf: ['swipe', 'touch', 'press'] } 12 | 13 | }, 14 | 15 | init: function () { 16 | this.dVelocity = new THREE.Vector3(); 17 | this.zVel = 0; 18 | this.xVel = 0; 19 | this.bindMethods(); 20 | }, 21 | 22 | play: function () { 23 | this.addEventListeners(); 24 | }, 25 | 26 | pause: function () { 27 | this.removeEventListeners(); 28 | this.dVelocity.set(0, 0, 0); 29 | }, 30 | 31 | remove: function () { 32 | this.pause(); 33 | }, 34 | 35 | addEventListeners: function () { 36 | const data = this.data; 37 | const sceneEl = this.el.sceneEl; 38 | 39 | sceneEl.addEventListener('axismove', this.onAxisMove); 40 | 41 | switch (data.mode) { 42 | case 'swipe': 43 | case 'touch': 44 | sceneEl.addEventListener('trackpadtouchstart', this.onTouchStart); 45 | sceneEl.addEventListener('trackpadtouchend', this.onTouchEnd); 46 | break; 47 | 48 | case 'press': 49 | sceneEl.addEventListener('trackpaddown', this.onTouchStart); 50 | sceneEl.addEventListener('trackpadup', this.onTouchEnd); 51 | break; 52 | } 53 | 54 | }, 55 | 56 | removeEventListeners: function () { 57 | const sceneEl = this.el.sceneEl; 58 | 59 | sceneEl.removeEventListener('axismove', this.onAxisMove); 60 | sceneEl.removeEventListener('trackpadtouchstart', this.onTouchStart); 61 | sceneEl.removeEventListener('trackpadtouchend', this.onTouchEnd); 62 | sceneEl.removeEventListener('trackpaddown', this.onTouchStart); 63 | sceneEl.removeEventListener('trackpadup', this.onTouchEnd); 64 | }, 65 | 66 | isVelocityActive: function () { 67 | return this.data.enabled && this.isMoving; 68 | }, 69 | 70 | getVelocityDelta: function () { 71 | this.dVelocity.z = this.isMoving ? -this.zVel : 1; 72 | this.dVelocity.x = this.isMoving ? this.xVel : 1; 73 | return this.dVelocity.clone(); 74 | }, 75 | 76 | bindMethods: function () { 77 | this.onTouchStart = this.onTouchStart.bind(this); 78 | this.onTouchEnd = this.onTouchEnd.bind(this); 79 | this.onAxisMove = this.onAxisMove.bind(this); 80 | }, 81 | 82 | onTouchStart: function (e) { 83 | switch(this.data.mode){ 84 | case 'swipe': 85 | this.canRecordAxis = true; 86 | this.startingAxisData = []; 87 | break; 88 | case 'touch': 89 | this.isMoving = true; 90 | break; 91 | case 'press': 92 | this.isMoving = true; 93 | break; 94 | } 95 | 96 | e.preventDefault(); 97 | }, 98 | 99 | onTouchEnd: function (e) { 100 | if(this.data.mode == 'swipe') { 101 | this.startingAxisData = []; 102 | } 103 | 104 | this.isMoving = false; 105 | e.preventDefault(); 106 | }, 107 | 108 | onAxisMove: function(e){ 109 | switch (this.data.mode) { 110 | case 'swipe': 111 | return this.handleSwipeAxis(e); 112 | case 'touch': 113 | case 'press': 114 | return this.handleTouchAxis(e); 115 | } 116 | }, 117 | 118 | handleSwipeAxis: function(e) { 119 | const data = this.data; 120 | const axisData = e.detail.axis; 121 | 122 | if(this.startingAxisData.length === 0 && this.canRecordAxis){ 123 | this.canRecordAxis = false; 124 | this.startingAxisData[0] = axisData[0]; 125 | this.startingAxisData[1] = axisData[1]; 126 | } 127 | 128 | if(this.startingAxisData.length > 0){ 129 | let velX = 0; 130 | let velZ = 0; 131 | 132 | if (data.enableNegX && axisData[0] < this.startingAxisData[0]) { 133 | velX = -1; 134 | } 135 | 136 | if (data.enablePosX && axisData[0] > this.startingAxisData[0]) { 137 | velX = 1; 138 | } 139 | 140 | if (data.enablePosZ && axisData[1] > this.startingAxisData[1]) { 141 | velZ = -1; 142 | } 143 | 144 | if (data.enableNegZ && axisData[1] < this.startingAxisData[1]) { 145 | velZ = 1; 146 | } 147 | 148 | const absChangeZ = Math.abs(this.startingAxisData[1] - axisData[1]); 149 | const absChangeX = Math.abs(this.startingAxisData[0] - axisData[0]); 150 | 151 | if (absChangeX > absChangeZ) { 152 | this.zVel = 0; 153 | this.xVel = velX; 154 | this.isMoving = true; 155 | } else { 156 | this.xVel = 0; 157 | this.zVel = velZ; 158 | this.isMoving = true; 159 | } 160 | 161 | } 162 | }, 163 | 164 | handleTouchAxis: function(e) { 165 | const data = this.data; 166 | const axisData = e.detail.axis; 167 | 168 | let velX = 0; 169 | let velZ = 0; 170 | 171 | if (data.enableNegX && axisData[0] < 0) { 172 | velX = -1; 173 | } 174 | 175 | if (data.enablePosX && axisData[0] > 0) { 176 | velX = 1; 177 | } 178 | 179 | if (data.enablePosZ && axisData[1] > 0) { 180 | velZ = -1; 181 | } 182 | 183 | if (data.enableNegZ && axisData[1] < 0) { 184 | velZ = 1; 185 | } 186 | 187 | if (Math.abs(axisData[0]) > Math.abs(axisData[1])) { 188 | this.zVel = 0; 189 | this.xVel = velX; 190 | } else { 191 | this.xVel = 0; 192 | this.zVel = velZ; 193 | } 194 | 195 | } 196 | 197 | }); 198 | 199 | -------------------------------------------------------------------------------- /src/loaders/README.md: -------------------------------------------------------------------------------- 1 | # Loaders 2 | 3 | Loaders for various 3D model types. All are trivial wrappers around one of the [many THREE.js loader classes](https://github.com/mrdoob/three.js/tree/master/examples/js/loaders). 4 | 5 | - **collada-model-legacy**: Loader for COLLADA (`.dae`) format, removed from A-Frame core with v0.9.0 release. Where possible, use the `gltf-model` component that ships with A-Frame instead. 6 | - **gltf-model-legacy**: Loader for glTF 1.0 format, removed from A-Frame core with v0.7.0 release. For glTF 2.0, use the `gltf-model` component that ships with A-Frame instead. 7 | - **object-model**: Loader for THREE.js .JSON format, generally containing multiple meshes or an entire scene. Where possible, use the `gltf-model` component that ships with A-Frame instead. 8 | - **fbx-model**: Loader for FBX format. 9 | - **animation-mixer**: Controls animations embedded in a glTF model. 10 | 11 | ## Usage 12 | 13 | ```html 14 | 15 | 16 | ``` 17 | 18 | THREE.js models often need to be scaled down. Example: 19 | 20 | ```html 21 | 22 | 23 | ``` 24 | 25 | ## Animation 26 | 27 | ![9ae34fd9-9ea5-44c5-9b95-2873484a1603-6702-0003a29fed9e49a0](https://cloud.githubusercontent.com/assets/1848368/25648601/845485de-2f82-11e7-8ae8-8e58c9dab9ff.gif) 28 | > Example by [Joe Campbell](https://github.com/rexraptor08) ([source](https://github.com/rexraptor08/animation-controls)). 29 | 30 | glTF and three.js models also support animation, through the `animation-mixer` component. All animations will play by default, or you can specify 31 | an animation and its duration: 32 | 33 | | Property | Default | Description | 34 | |-------------------|----------|-----------------------------------------------------------| 35 | | clip | * | Name of the animation clip(s) to play. Accepts wildcards. | 36 | | useRegExp | false | If true, interpret the `clip` string as a regular expression. If false, it is treated as a literal string, except for the * character, which is treated as a variable-length wildcard. | 37 | | duration | 0 | Duration of one cycle of the animation clip, in seconds. This provides the same functionality as timeScale (apart from pausing), with duration = clipLength/timeScale. This property only has an effect if timeScale is set to 1, otherwise the value of timeScale is used to determine animation playback speed. | 38 | | crossFadeDuration | 0 | Duration of cross-fades between clips, in seconds. | 39 | | loop | repeat | `once`, `repeat`, or `pingpong`. In `repeat` and `pingpong` modes, the clip plays once plus the specified number of repetitions. For `pingpong`, every second clip plays in reverse. | 40 | | repetitions | Infinity | Number of times to play the clip, in addition to the first play. Repetitions are ignored for `loop: once`. | 41 | | timeScale | 1 | Scaling factor for playback speed. A value of 0 causes the animation to pause. Negative values cause the animation to play backwards. | 42 | | clampWhenFinished | false | If true, halts the animation at the last frame. | 43 | | startAt | 0 | Configures the animation clip to begin at a specific start time (in milliseconds). This is useful when you need to jump to an exact time in an animation. The input parameter will be scaled by the mixer's timeScale. Negative values will result in a pause before the animation begins. | 44 | 45 | A list of available animations can usually be found by inspecting the model file or its documentation. All animations will play by default. To play only a specific set of animations, use wildcards: `animation-mixer="clip: run_*"`, or use the useRegExp flag to enable full regular expression matching, e.g. `animation-mixer="useRegExp: true; clip: run|walk"`. 46 | 47 | ### Animation Events 48 | 49 | The `animation-mixer` component emits events at certain points in the animation cycle. 50 | 51 | | Event | Details | Description | 52 | |--------------------|-----------------------|----------------------------------------------------------------| 53 | | animation-loop | `action`, `loopDelta` | Emitted when a single loop of the animation clip has finished. | 54 | | animation-finished | `action`, `direction` | Emitted when all loops of an animation clip have finished. | 55 | -------------------------------------------------------------------------------- /src/loaders/animation-mixer.js: -------------------------------------------------------------------------------- 1 | const LoopMode = { 2 | once: THREE.LoopOnce, 3 | repeat: THREE.LoopRepeat, 4 | pingpong: THREE.LoopPingPong 5 | }; 6 | 7 | /** 8 | * animation-mixer 9 | * 10 | * Player for animation clips. Intended to be compatible with any model format that supports 11 | * skeletal or morph animations through THREE.AnimationMixer. 12 | * See: https://threejs.org/docs/?q=animation#Reference/Animation/AnimationMixer 13 | */ 14 | module.exports = AFRAME.registerComponent('animation-mixer', { 15 | schema: { 16 | clip: { default: '*' }, 17 | useRegExp: {default: false}, 18 | duration: { default: 0 }, 19 | clampWhenFinished: { default: false, type: 'boolean' }, 20 | crossFadeDuration: { default: 0 }, 21 | loop: { default: 'repeat', oneOf: Object.keys(LoopMode) }, 22 | repetitions: { default: Infinity, min: 0 }, 23 | timeScale: { default: 1 }, 24 | startAt: { default: 0 } 25 | }, 26 | 27 | init: function () { 28 | /** @type {THREE.Mesh} */ 29 | this.model = null; 30 | /** @type {THREE.AnimationMixer} */ 31 | this.mixer = null; 32 | /** @type {Array} */ 33 | this.activeActions = []; 34 | 35 | const model = this.el.getObject3D('mesh'); 36 | 37 | if (model) { 38 | this.load(model); 39 | } else { 40 | this.el.addEventListener('model-loaded', (e) => { 41 | this.load(e.detail.model); 42 | }); 43 | } 44 | }, 45 | 46 | load: function (model) { 47 | const el = this.el; 48 | this.model = model; 49 | this.mixer = new THREE.AnimationMixer(model); 50 | this.mixer.addEventListener('loop', (e) => { 51 | el.emit('animation-loop', { action: e.action, loopDelta: e.loopDelta }); 52 | }); 53 | this.mixer.addEventListener('finished', (e) => { 54 | el.emit('animation-finished', { action: e.action, direction: e.direction }); 55 | }); 56 | if (this.data.clip) this.update({}); 57 | }, 58 | 59 | remove: function () { 60 | if (this.mixer) this.mixer.stopAllAction(); 61 | }, 62 | 63 | update: function (prevData) { 64 | if (!prevData) return; 65 | 66 | const data = this.data; 67 | const changes = AFRAME.utils.diff(data, prevData); 68 | 69 | // If selected clips have changed, restart animation. 70 | if ('clip' in changes) { 71 | this.stopAction(); 72 | if (data.clip) this.playAction(); 73 | return; 74 | } 75 | 76 | // Otherwise, modify running actions. 77 | this.activeActions.forEach((action) => { 78 | if ('duration' in changes && data.duration) { 79 | action.setDuration(data.duration); 80 | } 81 | if ('clampWhenFinished' in changes) { 82 | action.clampWhenFinished = data.clampWhenFinished; 83 | } 84 | if ('loop' in changes || 'repetitions' in changes) { 85 | action.setLoop(LoopMode[data.loop], data.repetitions); 86 | } 87 | if ('timeScale' in changes) { 88 | action.setEffectiveTimeScale(data.timeScale); 89 | } 90 | }); 91 | }, 92 | 93 | stopAction: function () { 94 | const data = this.data; 95 | for (let i = 0; i < this.activeActions.length; i++) { 96 | data.crossFadeDuration 97 | ? this.activeActions[i].fadeOut(data.crossFadeDuration) 98 | : this.activeActions[i].stop(); 99 | } 100 | this.activeActions.length = 0; 101 | }, 102 | 103 | playAction: function () { 104 | if (!this.mixer) return; 105 | 106 | const model = this.model, 107 | data = this.data, 108 | clips = model.animations || (model.geometry || {}).animations || []; 109 | 110 | if (!clips.length) return; 111 | 112 | const re = data.useRegExp ? data.clip : wildcardToRegExp(data.clip); 113 | 114 | for (let clip, i = 0; (clip = clips[i]); i++) { 115 | if (clip.name.match(re)) { 116 | const action = this.mixer.clipAction(clip, model); 117 | 118 | action.enabled = true; 119 | action.clampWhenFinished = data.clampWhenFinished; 120 | if (data.duration) action.setDuration(data.duration); 121 | if (data.timeScale !== 1) action.setEffectiveTimeScale(data.timeScale); 122 | // animation-mixer.startAt and AnimationAction.startAt have very different meanings. 123 | // animation-mixer.startAt indicates which frame in the animation to start at, in msecs. 124 | // AnimationAction.startAt indicates when to start the animation (from the 1st frame), 125 | // measured in global mixer time, in seconds. 126 | action.startAt(this.mixer.time - data.startAt / 1000); 127 | action 128 | .setLoop(LoopMode[data.loop], data.repetitions) 129 | .fadeIn(data.crossFadeDuration) 130 | .play(); 131 | this.activeActions.push(action); 132 | } 133 | } 134 | }, 135 | 136 | tick: function (t, dt) { 137 | if (this.mixer && !isNaN(dt)) this.mixer.update(dt / 1000); 138 | } 139 | }); 140 | 141 | /** 142 | * Creates a RegExp from the given string, converting asterisks to .* expressions, 143 | * and escaping all other characters. 144 | */ 145 | function wildcardToRegExp(s) { 146 | return new RegExp('^' + s.split(/\*+/).map(regExpEscape).join('.*') + '$'); 147 | } 148 | 149 | /** 150 | * RegExp-escapes all characters in the given string. 151 | */ 152 | function regExpEscape(s) { 153 | return s.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&'); 154 | } 155 | -------------------------------------------------------------------------------- /src/loaders/collada-model-legacy.js: -------------------------------------------------------------------------------- 1 | import { ColladaLoader } from 'three/addons/loaders/ColladaLoader.js'; 2 | THREE.ColladaLoader = ColladaLoader; 3 | 4 | /** 5 | * collada-model-legacy 6 | * 7 | * Loader for COLLADA (.dae) format. 8 | */ 9 | AFRAME.registerComponent('collada-model-legacy', { 10 | schema: {type: 'asset'}, 11 | 12 | init: function () { 13 | this.model = null; 14 | this.loader = new THREE.ColladaLoader(); 15 | }, 16 | 17 | update: function () { 18 | var self = this; 19 | var el = this.el; 20 | var src = this.data; 21 | var rendererSystem = this.el.sceneEl.systems.renderer; 22 | 23 | if (!src) { return; } 24 | 25 | this.remove(); 26 | 27 | this.loader.load(src, function (colladaModel) { 28 | self.model = colladaModel.scene; 29 | self.model.traverse(function (object) { 30 | if (object.isMesh) { 31 | var material = object.material; 32 | if (material.color) rendererSystem.applyColorCorrection(material.color); 33 | if (material.map) rendererSystem.applyColorCorrection(material.map); 34 | if (material.emissive) rendererSystem.applyColorCorrection(material.emissive); 35 | if (material.emissiveMap) rendererSystem.applyColorCorrection(material.emissiveMap); 36 | } 37 | }); 38 | el.setObject3D('mesh', self.model); 39 | el.emit('model-loaded', {format: 'collada', model: self.model}); 40 | }); 41 | }, 42 | 43 | remove: function () { 44 | if (!this.model) { return; } 45 | this.el.removeObject3D('mesh'); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /src/loaders/fbx-model.js: -------------------------------------------------------------------------------- 1 | import { FBXLoader } from 'three/addons/loaders/FBXLoader.js'; 2 | THREE.FBXLoader = FBXLoader; 3 | 4 | /** 5 | * fbx-model 6 | * 7 | * Loader for FBX format. 8 | */ 9 | AFRAME.registerComponent('fbx-model', { 10 | schema: { 11 | src: { type: 'asset' }, 12 | crossorigin: { default: '' } 13 | }, 14 | 15 | init: function () { 16 | this.model = null; 17 | }, 18 | 19 | update: function () { 20 | const data = this.data; 21 | if (!data.src) return; 22 | 23 | this.remove(); 24 | const loader = new THREE.FBXLoader(); 25 | if (data.crossorigin) loader.setCrossOrigin(data.crossorigin); 26 | loader.load(data.src, this.load.bind(this)); 27 | }, 28 | 29 | load: function (model) { 30 | this.model = model; 31 | this.el.setObject3D('mesh', model); 32 | this.el.emit('model-loaded', {format: 'fbx', model: model}); 33 | }, 34 | 35 | remove: function () { 36 | if (this.model) this.el.removeObject3D('mesh'); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /src/loaders/gltf-model-legacy.js: -------------------------------------------------------------------------------- 1 | const fetchScript = require('../../lib/fetch-script')(); 2 | 3 | const LOADER_SRC = 'https://cdn.jsdelivr.net/gh/mrdoob/three.js@r86/examples/js/loaders/GLTFLoader.js'; 4 | 5 | const loadLoader = (function () { 6 | let promise; 7 | return function () { 8 | promise = promise || fetchScript(LOADER_SRC); 9 | return promise; 10 | }; 11 | }()); 12 | 13 | /** 14 | * Legacy loader for glTF 1.0 models. 15 | * Asynchronously loads THREE.GLTFLoader from jsdelivr. 16 | */ 17 | module.exports = AFRAME.registerComponent('gltf-model-legacy', { 18 | schema: {type: 'model'}, 19 | 20 | init: function () { 21 | this.model = null; 22 | this.loader = null; 23 | this.loaderPromise = loadLoader().then(() => { 24 | this.loader = new THREE.GLTFLoader(); 25 | this.loader.setCrossOrigin('Anonymous'); 26 | }); 27 | }, 28 | 29 | update: function () { 30 | const self = this; 31 | const el = this.el; 32 | const src = this.data; 33 | 34 | if (!src) { return; } 35 | 36 | this.remove(); 37 | 38 | this.loaderPromise.then(() => { 39 | this.loader.load(src, function gltfLoaded (gltfModel) { 40 | self.model = gltfModel.scene; 41 | self.model.animations = gltfModel.animations; 42 | el.setObject3D('mesh', self.model); 43 | el.emit('model-loaded', {format: 'gltf', model: self.model}); 44 | }); 45 | }); 46 | }, 47 | 48 | remove: function () { 49 | if (!this.model) { return; } 50 | this.el.removeObject3D('mesh'); 51 | } 52 | }); 53 | -------------------------------------------------------------------------------- /src/loaders/index.js: -------------------------------------------------------------------------------- 1 | require('./animation-mixer'); 2 | require('./collada-model-legacy'); 3 | require('./fbx-model'); 4 | require('./gltf-model-legacy'); 5 | require('./object-model'); 6 | -------------------------------------------------------------------------------- /src/loaders/object-model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * object-model 3 | * 4 | * Loader for THREE.js JSON format. Somewhat confusingly, there are two different THREE.js formats, 5 | * both having the .json extension. This loader supports only THREE.ObjectLoader, which typically 6 | * includes multiple meshes or an entire scene. 7 | * 8 | * Check the console for errors, if in doubt. You may need to use `json-model` or 9 | * `blend-character-model` for some .js and .json files. 10 | * 11 | * See: https://clara.io/learn/user-guide/data_exchange/threejs_export 12 | */ 13 | module.exports = AFRAME.registerComponent('object-model', { 14 | schema: { 15 | src: { type: 'asset' }, 16 | crossorigin: { default: '' } 17 | }, 18 | 19 | init: function () { 20 | this.model = null; 21 | }, 22 | 23 | update: function () { 24 | let loader; 25 | const data = this.data; 26 | if (!data.src) return; 27 | 28 | this.remove(); 29 | loader = new THREE.ObjectLoader(); 30 | if (data.crossorigin) loader.setCrossOrigin(data.crossorigin); 31 | loader.load(data.src, (object) => { 32 | 33 | // Enable skinning, if applicable. 34 | object.traverse((o) => { 35 | if (o instanceof THREE.SkinnedMesh && o.material) { 36 | o.material.skinning = !!((o.geometry && o.geometry.bones) || []).length; 37 | } 38 | }); 39 | 40 | this.load(object); 41 | }); 42 | }, 43 | 44 | load: function (model) { 45 | this.model = model; 46 | this.el.setObject3D('mesh', model); 47 | this.el.emit('model-loaded', {format: 'json', model: model}); 48 | }, 49 | 50 | remove: function () { 51 | if (this.model) this.el.removeObject3D('mesh'); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /src/misc/README.md: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | 3 | Various other components. 4 | 5 | - **checkpoint**: Target for [checkpoint-controls](/src/controls/checkpoint-controls.js). 6 | - **grab**: When used on one or both hands, lets the player pick up objects with `hand-controls`. Requires `sphere-collider` and using CANNON physics. 7 | - **normal-material**: Applies a MeshNormalMaterial to the entity, such that face colors are determined by their orientation. Helpful for debugging geometry. 8 | - **sphere-collider**: Detects collisions with specified objects. Required for `grab`. 9 | - **cube-env-map**: Applies a CubeTexture as the envMap of an entity, without otherwise modifying the preset materials. 10 | 11 | ## `cube-env-map` 12 | 13 | Usage: 14 | 15 | ``` 16 | 21 | 22 | ``` 23 | 24 | | Option | Description | 25 | |--------|-------------| 26 | | path | Folder containing cubemap images. Path should end in a trailing `/`. Assumes naming scheme `negx.`, `posx.`, ... | 27 | | extension | File extension for each cubemap image. | 28 | | reflectivity | Amount [0,1] of the cubemap that should be reflected. | 29 | | materials | Names of materials to be modified. Defaults to all materials. | 30 | -------------------------------------------------------------------------------- /src/misc/checkpoint.js: -------------------------------------------------------------------------------- 1 | module.exports = AFRAME.registerComponent('checkpoint', { 2 | schema: { 3 | offset: {default: {x: 0, y: 0, z: 0}, type: 'vec3'} 4 | }, 5 | 6 | init: function () { 7 | this.active = false; 8 | this.targetEl = null; 9 | this.fire = this.fire.bind(this); 10 | this.offset = new THREE.Vector3(); 11 | }, 12 | 13 | update: function () { 14 | this.offset.copy(this.data.offset); 15 | }, 16 | 17 | play: function () { this.el.addEventListener('click', this.fire); }, 18 | pause: function () { this.el.removeEventListener('click', this.fire); }, 19 | remove: function () { this.pause(); }, 20 | 21 | fire: function () { 22 | const targetEl = this.el.sceneEl.querySelector('[checkpoint-controls]'); 23 | if (!targetEl) { 24 | throw new Error('No `checkpoint-controls` component found.'); 25 | } 26 | targetEl.components['checkpoint-controls'].setCheckpoint(this.el); 27 | }, 28 | 29 | getOffset: function () { 30 | return this.offset.copy(this.data.offset); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /src/misc/cube-env-map.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Array|THREE.Material} material 3 | * @return {Array} 4 | */ 5 | function ensureMaterialArray (material) { 6 | if (!material) { 7 | return []; 8 | } else if (Array.isArray(material)) { 9 | return material; 10 | } else if (material.materials) { 11 | return material.materials; 12 | } else { 13 | return [material]; 14 | } 15 | } 16 | 17 | /** 18 | * @param {THREE.Object3D} mesh 19 | * @param {Array} materialNames 20 | * @param {THREE.Texture} envMap 21 | * @param {number} reflectivity [description] 22 | */ 23 | function applyEnvMap (mesh, materialNames, envMap, reflectivity) { 24 | if (!mesh) return; 25 | 26 | materialNames = materialNames || []; 27 | 28 | mesh.traverse((node) => { 29 | 30 | if (!node.isMesh) return; 31 | 32 | const meshMaterials = ensureMaterialArray(node.material); 33 | 34 | meshMaterials.forEach((material) => { 35 | 36 | if (material && !('envMap' in material)) return; 37 | if (materialNames.length && materialNames.indexOf(material.name) === -1) return; 38 | 39 | material.envMap = envMap; 40 | material.reflectivity = reflectivity; 41 | material.needsUpdate = true; 42 | 43 | }); 44 | 45 | }); 46 | } 47 | 48 | /** 49 | * Specifies an envMap on an entity, without replacing any existing material 50 | * properties. 51 | */ 52 | module.exports = AFRAME.registerComponent('cube-env-map', { 53 | multiple: true, 54 | 55 | schema: { 56 | path: {default: ''}, 57 | extension: {default: 'jpg', oneOf: ['jpg', 'png']}, 58 | enableBackground: {default: false}, 59 | reflectivity: {default: 1, min: 0, max: 1}, 60 | materials: {default: []} 61 | }, 62 | 63 | init: function () { 64 | const data = this.data; 65 | 66 | this.texture = new THREE.CubeTextureLoader().load([ 67 | data.path + 'posx.' + data.extension, data.path + 'negx.' + data.extension, 68 | data.path + 'posy.' + data.extension, data.path + 'negy.' + data.extension, 69 | data.path + 'posz.' + data.extension, data.path + 'negz.' + data.extension 70 | ]); 71 | this.texture.format = THREE.RGBAFormat; 72 | 73 | this.object3dsetHandler = () => { 74 | const mesh = this.el.getObject3D('mesh'); 75 | const data = this.data; 76 | applyEnvMap(mesh, data.materials, this.texture, data.reflectivity); 77 | }; 78 | 79 | this.object3dsetHandler(); 80 | this.el.addEventListener('object3dset', this.object3dsetHandler); 81 | 82 | }, 83 | 84 | update: function (oldData) { 85 | const data = this.data; 86 | const mesh = this.el.getObject3D('mesh'); 87 | 88 | let addedMaterialNames = []; 89 | let removedMaterialNames = []; 90 | 91 | if (data.materials.length) { 92 | if (oldData.materials) { 93 | addedMaterialNames = data.materials.filter((name) => !oldData.materials.includes(name)); 94 | removedMaterialNames = oldData.materials.filter((name) => !data.materials.includes(name)); 95 | } else { 96 | addedMaterialNames = data.materials; 97 | } 98 | } 99 | if (addedMaterialNames.length) { 100 | applyEnvMap(mesh, addedMaterialNames, this.texture, data.reflectivity); 101 | } 102 | if (removedMaterialNames.length) { 103 | applyEnvMap(mesh, removedMaterialNames, null, 1); 104 | } 105 | 106 | if (oldData.materials && data.reflectivity !== oldData.reflectivity) { 107 | const maintainedMaterialNames = data.materials 108 | .filter((name) => oldData.materials.includes(name)); 109 | if (maintainedMaterialNames.length) { 110 | applyEnvMap(mesh, maintainedMaterialNames, this.texture, data.reflectivity); 111 | } 112 | } 113 | 114 | if (this.data.enableBackground && !oldData.enableBackground) { 115 | this.setBackground(this.texture); 116 | } else if (!this.data.enableBackground && oldData.enableBackground) { 117 | this.setBackground(null); 118 | } 119 | }, 120 | 121 | remove: function () { 122 | this.el.removeEventListener('object3dset', this.object3dsetHandler); 123 | const mesh = this.el.getObject3D('mesh'); 124 | const data = this.data; 125 | 126 | applyEnvMap(mesh, data.materials, null, 1); 127 | if (data.enableBackground) this.setBackground(null); 128 | }, 129 | 130 | setBackground: function (texture) { 131 | this.el.sceneEl.object3D.background = texture; 132 | } 133 | }); -------------------------------------------------------------------------------- /src/misc/grab.js: -------------------------------------------------------------------------------- 1 | /* global CANNON */ 2 | 3 | /** 4 | * Based on aframe/examples/showcase/tracked-controls. 5 | * 6 | * Handles events coming from the hand-controls. 7 | * Determines if the entity is grabbed or released. 8 | * Updates its position to move along the controller. 9 | */ 10 | module.exports = AFRAME.registerComponent('grab', { 11 | init: function () { 12 | this.system = this.el.sceneEl.systems.physics; 13 | 14 | this.GRABBED_STATE = 'grabbed'; 15 | 16 | this.grabbing = false; 17 | this.hitEl = /** @type {AFRAME.Element} */ null; 18 | this.physics = /** @type {AFRAME.System} */ this.el.sceneEl.systems.physics; 19 | this.constraint = /** @type {CANNON.Constraint} */ null; 20 | 21 | // Bind event handlers 22 | this.onHit = this.onHit.bind(this); 23 | this.onGripOpen = this.onGripOpen.bind(this); 24 | this.onGripClose = this.onGripClose.bind(this); 25 | }, 26 | 27 | play: function () { 28 | const el = this.el; 29 | el.addEventListener('hit', this.onHit); 30 | el.addEventListener('gripdown', this.onGripClose); 31 | el.addEventListener('gripup', this.onGripOpen); 32 | el.addEventListener('trackpaddown', this.onGripClose); 33 | el.addEventListener('trackpadup', this.onGripOpen); 34 | el.addEventListener('triggerdown', this.onGripClose); 35 | el.addEventListener('triggerup', this.onGripOpen); 36 | }, 37 | 38 | pause: function () { 39 | const el = this.el; 40 | el.removeEventListener('hit', this.onHit); 41 | el.removeEventListener('gripdown', this.onGripClose); 42 | el.removeEventListener('gripup', this.onGripOpen); 43 | el.removeEventListener('trackpaddown', this.onGripClose); 44 | el.removeEventListener('trackpadup', this.onGripOpen); 45 | el.removeEventListener('triggerdown', this.onGripClose); 46 | el.removeEventListener('triggerup', this.onGripOpen); 47 | }, 48 | 49 | onGripClose: function () { 50 | this.grabbing = true; 51 | }, 52 | 53 | onGripOpen: function () { 54 | const hitEl = this.hitEl; 55 | this.grabbing = false; 56 | if (!hitEl) { return; } 57 | hitEl.removeState(this.GRABBED_STATE); 58 | this.hitEl = undefined; 59 | this.system.removeConstraint(this.constraint); 60 | this.constraint = null; 61 | }, 62 | 63 | onHit: function (evt) { 64 | const hitEl = evt.detail.el; 65 | // If the element is already grabbed (it could be grabbed by another controller). 66 | // If the hand is not grabbing the element does not stick. 67 | // If we're already grabbing something you can't grab again. 68 | if (hitEl.is(this.GRABBED_STATE) || !this.grabbing || this.hitEl) { return; } 69 | hitEl.addState(this.GRABBED_STATE); 70 | this.hitEl = hitEl; 71 | this.constraint = new CANNON.LockConstraint(this.el.body, hitEl.body); 72 | this.system.addConstraint(this.constraint); 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /src/misc/index.js: -------------------------------------------------------------------------------- 1 | require('./checkpoint'); 2 | require('./cube-env-map'); 3 | require('./grab'); 4 | require('./normal-material'); 5 | require('./sphere-collider'); 6 | -------------------------------------------------------------------------------- /src/misc/normal-material.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Recursively applies a MeshNormalMaterial to the entity, such that 3 | * face colors are determined by their orientation. Helpful for 4 | * debugging geometry 5 | */ 6 | module.exports = AFRAME.registerComponent('normal-material', { 7 | init: function () { 8 | this.material = new THREE.MeshNormalMaterial({flatShading: true}); 9 | this.applyMaterial = this.applyMaterial.bind(this); 10 | this.el.addEventListener('object3dset', this.applyMaterial); 11 | this.applyMaterial(); 12 | }, 13 | 14 | remove: function () { 15 | this.el.removeEventListener('object3dset', this.applyMaterial); 16 | }, 17 | 18 | applyMaterial: function () { 19 | this.el.object3D.traverse((node) => { 20 | if (node.isMesh) node.material = this.material; 21 | }); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /src/misc/sphere-collider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on aframe/examples/showcase/tracked-controls. 3 | * 4 | * Implement bounding sphere collision detection for entities with a mesh. 5 | * Sets the specified state on the intersected entities. 6 | * 7 | * @property {string} objects - Selector of the entities to test for collision. 8 | * @property {string} state - State to set on collided entities. 9 | * 10 | */ 11 | module.exports = AFRAME.registerComponent('sphere-collider', { 12 | schema: { 13 | enabled: {default: true}, 14 | interval: {default: 80}, 15 | objects: {default: ''}, 16 | state: {default: 'collided'}, 17 | radius: {default: 0.05}, 18 | watch: {default: true} 19 | }, 20 | 21 | init: function () { 22 | /** @type {MutationObserver} */ 23 | this.observer = null; 24 | /** @type {Array} Elements to watch for collisions. */ 25 | this.els = []; 26 | /** @type {Array} Elements currently in collision state. */ 27 | this.collisions = []; 28 | this.prevCheckTime = undefined; 29 | 30 | this.eventDetail = {}; 31 | this.handleHit = this.handleHit.bind(this); 32 | this.handleHitEnd = this.handleHitEnd.bind(this); 33 | }, 34 | 35 | play: function () { 36 | const sceneEl = this.el.sceneEl; 37 | 38 | if (this.data.watch) { 39 | this.observer = new MutationObserver(this.update.bind(this, null)); 40 | this.observer.observe(sceneEl, {childList: true, subtree: true}); 41 | } 42 | }, 43 | 44 | pause: function () { 45 | if (this.observer) { 46 | this.observer.disconnect(); 47 | this.observer = null; 48 | } 49 | }, 50 | 51 | /** 52 | * Update list of entities to test for collision. 53 | */ 54 | update: function () { 55 | const data = this.data; 56 | let objectEls; 57 | 58 | // Push entities into list of els to intersect. 59 | if (data.objects) { 60 | objectEls = this.el.sceneEl.querySelectorAll(data.objects); 61 | } else { 62 | // If objects not defined, intersect with everything. 63 | objectEls = this.el.sceneEl.children; 64 | } 65 | // Convert from NodeList to Array 66 | this.els = Array.prototype.slice.call(objectEls); 67 | }, 68 | 69 | tick: (function () { 70 | const position = new THREE.Vector3(), 71 | meshPosition = new THREE.Vector3(), 72 | colliderScale = new THREE.Vector3(), 73 | size = new THREE.Vector3(), 74 | box = new THREE.Box3(), 75 | collisions = [], 76 | distanceMap = new Map(); 77 | return function (time) { 78 | if (!this.data.enabled) { return; } 79 | 80 | // Only check for intersection if interval time has passed. 81 | const prevCheckTime = this.prevCheckTime; 82 | if (prevCheckTime && (time - prevCheckTime < this.data.interval)) { return; } 83 | // Update check time. 84 | this.prevCheckTime = time; 85 | 86 | const el = this.el, 87 | data = this.data, 88 | mesh = el.getObject3D('mesh'); 89 | let colliderRadius; 90 | 91 | if (!mesh) { return; } 92 | 93 | collisions.length = 0; 94 | distanceMap.clear(); 95 | el.object3D.getWorldPosition(position); 96 | el.object3D.getWorldScale(colliderScale); 97 | colliderRadius = data.radius * scaleFactor(colliderScale); 98 | // Update collision list. 99 | this.els.forEach(intersect); 100 | 101 | // Emit events and add collision states, in order of distance. 102 | collisions 103 | .sort((a, b) => distanceMap.get(a) > distanceMap.get(b) ? 1 : -1) 104 | .forEach(this.handleHit); 105 | 106 | // Remove collision state from other elements. 107 | this.collisions 108 | .filter((el) => !distanceMap.has(el)) 109 | .forEach(this.handleHitEnd); 110 | 111 | // Store new collisions 112 | copyArray(this.collisions, collisions); 113 | 114 | // Bounding sphere collision detection 115 | function intersect (el) { 116 | let radius, mesh, distance, extent; 117 | 118 | if (!el.isEntity) { return; } 119 | 120 | mesh = el.getObject3D('mesh'); 121 | 122 | if (!mesh) { return; } 123 | 124 | box.setFromObject(mesh).getSize(size); 125 | extent = Math.max(size.x, size.y, size.z) / 2; 126 | radius = Math.sqrt(2 * extent * extent); 127 | box.getCenter(meshPosition); 128 | 129 | if (!radius) { return; } 130 | 131 | distance = position.distanceTo(meshPosition); 132 | if (distance < radius + colliderRadius) { 133 | collisions.push(el); 134 | distanceMap.set(el, distance); 135 | } 136 | } 137 | // use max of scale factors to maintain bounding sphere collision 138 | function scaleFactor (scaleVec) { 139 | return Math.max(scaleVec.x, scaleVec.y, scaleVec.z); 140 | } 141 | }; 142 | })(), 143 | 144 | handleHit: function (targetEl) { 145 | targetEl.emit('hit'); 146 | targetEl.addState(this.data.state); 147 | this.eventDetail.el = targetEl; 148 | this.el.emit('hit', this.eventDetail); 149 | }, 150 | handleHitEnd: function (targetEl) { 151 | targetEl.emit('hitend'); 152 | targetEl.removeState(this.data.state); 153 | this.eventDetail.el = targetEl; 154 | this.el.emit('hitend', this.eventDetail); 155 | } 156 | }); 157 | 158 | function copyArray (dest, source) { 159 | dest.length = 0; 160 | for (let i = 0; i < source.length; i++) { dest[i] = source[i]; } 161 | } 162 | -------------------------------------------------------------------------------- /src/pathfinding/README.md: -------------------------------------------------------------------------------- 1 | # Pathfinding 2 | 3 | Set of components for pathfinding along a nav mesh, using [PatrolJS](https://github.com/nickjanssen/PatrolJS/). 4 | 5 | - **nav-mesh**: Assigns model from the current entity as a [navigation mesh](https://en.wikipedia.org/wiki/Navigation_mesh) for the pathfinding system. A navigation mesh is not the same as visible terrain geometry. See below. 6 | - **nav-agent**: Adds behaviors to an entity allowing it to navigate to any reachable destination along the nav mesh. 7 | 8 | ## Creating a nav mesh 9 | 10 | [Blog post](https://medium.com/@donmccurdy/creating-a-nav-mesh-for-a-webvr-scene-b3fdb6bed918). 11 | 12 | ## Setting a destination 13 | 14 | Controllers can be activated to begin moving their entity toward a destination. Example: 15 | 16 | ```html 17 | 18 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | ``` 29 | 30 | ```js 31 | var npcEl = document.querySelector('#npc'); 32 | npcEl.setAttribute('nav-agent', { 33 | active: true, 34 | destination: e.detail.intersection.point 35 | }); 36 | ``` 37 | 38 | ## Events 39 | 40 | The `nav-agent` component will emit two events: 41 | 42 | - `navigation-start`: Entity beginning travel to a destination. 43 | - `navigation-end`: Entity has reached destination. 44 | 45 | ## Important notes 46 | 47 | This implementation is meant as a proof-of-concept, and doesn't have all the features and polish of game engine navigation. Currently missing: 48 | 49 | - [ ] Smooth rotation when navigating around corners. 50 | - [ ] Dynamic obstacles, like mobile props and NPCs. 51 | - [ ] Multiple nav meshes and/or levels. 52 | -------------------------------------------------------------------------------- /src/pathfinding/index.js: -------------------------------------------------------------------------------- 1 | require('./nav-mesh'); 2 | require('./nav-agent'); 3 | require('./system'); 4 | -------------------------------------------------------------------------------- /src/pathfinding/nav-agent.js: -------------------------------------------------------------------------------- 1 | module.exports = AFRAME.registerComponent('nav-agent', { 2 | schema: { 3 | destination: {type: 'vec3'}, 4 | active: {default: false}, 5 | speed: {default: 2} 6 | }, 7 | init: function () { 8 | this.system = this.el.sceneEl.systems.nav; 9 | this.system.addAgent(this); 10 | this.group = null; 11 | this.path = []; 12 | this.raycaster = new THREE.Raycaster(); 13 | }, 14 | remove: function () { 15 | this.system.removeAgent(this); 16 | }, 17 | update: function () { 18 | this.path.length = 0; 19 | }, 20 | updateNavLocation: function () { 21 | this.group = null; 22 | this.path = []; 23 | }, 24 | tick: (function () { 25 | const vDest = new THREE.Vector3(); 26 | const vDelta = new THREE.Vector3(); 27 | const vNext = new THREE.Vector3(); 28 | 29 | return function (t, dt) { 30 | const el = this.el; 31 | const data = this.data; 32 | const raycaster = this.raycaster; 33 | const speed = data.speed * dt / 1000; 34 | 35 | if (!data.active) return; 36 | 37 | // Use PatrolJS pathfinding system to get shortest path to target. 38 | if (!this.path.length) { 39 | const position = this.el.object3D.position; 40 | this.group = this.group || this.system.getGroup(position); 41 | this.path = this.system.getPath(position, vDest.copy(data.destination), this.group) || []; 42 | el.emit('navigation-start'); 43 | } 44 | 45 | // If no path is found, exit. 46 | if (!this.path.length) { 47 | console.warn('[nav] Unable to find path to %o.', data.destination); 48 | this.el.setAttribute('nav-agent', {active: false}); 49 | el.emit('navigation-end'); 50 | return; 51 | } 52 | 53 | // Current segment is a vector from current position to next waypoint. 54 | const vCurrent = el.object3D.position; 55 | const vWaypoint = this.path[0]; 56 | vDelta.subVectors(vWaypoint, vCurrent); 57 | 58 | const distance = vDelta.length(); 59 | let gazeTarget; 60 | 61 | if (distance < speed) { 62 | // If <1 step from current waypoint, discard it and move toward next. 63 | this.path.shift(); 64 | 65 | // After discarding the last waypoint, exit pathfinding. 66 | if (!this.path.length) { 67 | this.el.setAttribute('nav-agent', {active: false}); 68 | el.emit('navigation-end'); 69 | return; 70 | } 71 | 72 | vNext.copy(vCurrent); 73 | gazeTarget = this.path[0]; 74 | } else { 75 | // If still far away from next waypoint, find next position for 76 | // the current frame. 77 | vNext.copy(vDelta.setLength(speed)).add(vCurrent); 78 | gazeTarget = vWaypoint; 79 | } 80 | 81 | // Look at the next waypoint. 82 | gazeTarget.y = vCurrent.y; 83 | el.object3D.lookAt(gazeTarget); 84 | 85 | // Raycast against the nav mesh, to keep the agent moving along the 86 | // ground, not traveling in a straight line from higher to lower waypoints. 87 | raycaster.ray.origin.copy(vNext); 88 | raycaster.ray.origin.y += 1.5; 89 | raycaster.ray.direction = {x:0, y:-1, z:0}; 90 | const intersections = raycaster.intersectObject(this.system.getNavMesh()); 91 | 92 | if (!intersections.length) { 93 | // Raycasting failed. Step toward the waypoint and hope for the best. 94 | vCurrent.copy(vNext); 95 | } else { 96 | // Re-project next position onto nav mesh. 97 | vDelta.subVectors(intersections[0].point, vCurrent); 98 | vCurrent.add(vDelta.setLength(speed)); 99 | } 100 | 101 | }; 102 | }()) 103 | }); 104 | -------------------------------------------------------------------------------- /src/pathfinding/nav-mesh.js: -------------------------------------------------------------------------------- 1 | /** 2 | * nav-mesh 3 | * 4 | * Waits for a mesh to be loaded on the current entity, then sets it as the 5 | * nav mesh in the pathfinding system. 6 | */ 7 | module.exports = AFRAME.registerComponent('nav-mesh', { 8 | schema: { 9 | nodeName: {type: 'string'} 10 | }, 11 | 12 | init: function () { 13 | this.system = this.el.sceneEl.systems.nav; 14 | this.hasLoadedNavMesh = false; 15 | this.nodeName = this.data.nodeName; 16 | this.el.addEventListener('object3dset', this.loadNavMesh.bind(this)); 17 | }, 18 | 19 | play: function () { 20 | if (!this.hasLoadedNavMesh) this.loadNavMesh(); 21 | }, 22 | 23 | loadNavMesh: function () { 24 | var self = this; 25 | const object = this.el.getObject3D('mesh'); 26 | const scene = this.el.sceneEl.object3D; 27 | 28 | if (!object) return; 29 | 30 | let navMesh; 31 | object.traverse((node) => { 32 | if (node.isMesh && 33 | (!self.nodeName || node.name === self.nodeName)) navMesh = node; 34 | }); 35 | 36 | if (!navMesh) return; 37 | 38 | const navMeshGeometry = navMesh.geometry.clone(); 39 | navMesh.updateWorldMatrix(true, false); 40 | navMeshGeometry.applyMatrix4(navMesh.matrixWorld); 41 | this.system.setNavMeshGeometry(navMeshGeometry); 42 | this.hasLoadedNavMesh = true; 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /src/pathfinding/system.js: -------------------------------------------------------------------------------- 1 | const { Pathfinding } = require('three-pathfinding'); 2 | 3 | const pathfinder = new Pathfinding(); 4 | const ZONE = 'level'; 5 | 6 | /** 7 | * nav 8 | * 9 | * Pathfinding system, using PatrolJS. 10 | */ 11 | module.exports = AFRAME.registerSystem('nav', { 12 | init: function () { 13 | this.navMesh = null; 14 | this.agents = new Set(); 15 | }, 16 | 17 | /** 18 | * @param {THREE.Geometry} geometry 19 | */ 20 | setNavMeshGeometry: function (geometry) { 21 | this.navMesh = new THREE.Mesh(geometry); 22 | pathfinder.setZoneData(ZONE, Pathfinding.createZone(geometry)); 23 | Array.from(this.agents).forEach((agent) => agent.updateNavLocation()); 24 | }, 25 | 26 | /** 27 | * @return {THREE.Mesh} 28 | */ 29 | getNavMesh: function () { 30 | return this.navMesh; 31 | }, 32 | 33 | /** 34 | * @param {NavAgent} ctrl 35 | */ 36 | addAgent: function (ctrl) { 37 | this.agents.add(ctrl); 38 | }, 39 | 40 | /** 41 | * @param {NavAgent} ctrl 42 | */ 43 | removeAgent: function (ctrl) { 44 | this.agents.delete(ctrl); 45 | }, 46 | 47 | /** 48 | * @param {THREE.Vector3} start 49 | * @param {THREE.Vector3} end 50 | * @param {number} groupID 51 | * @return {Array} 52 | */ 53 | getPath: function (start, end, groupID) { 54 | return this.navMesh 55 | ? pathfinder.findPath(start, end, ZONE, groupID) 56 | : null; 57 | }, 58 | 59 | /** 60 | * @param {THREE.Vector3} position 61 | * @return {number} 62 | */ 63 | getGroup: function (position) { 64 | return this.navMesh 65 | ? pathfinder.getGroup(ZONE, position) 66 | : null; 67 | }, 68 | 69 | /** 70 | * @param {THREE.Vector3} position 71 | * @param {number} groupID 72 | * @return {Node} 73 | */ 74 | getNode: function (position, groupID) { 75 | return this.navMesh 76 | ? pathfinder.getClosestNode(position, ZONE, groupID, true) 77 | : null; 78 | }, 79 | 80 | /** 81 | * @param {THREE.Vector3} start Starting position. 82 | * @param {THREE.Vector3} end Desired ending position. 83 | * @param {number} groupID 84 | * @param {Node} node 85 | * @param {THREE.Vector3} endTarget (Output) Adjusted step end position. 86 | * @return {Node} Current node, after step is taken. 87 | */ 88 | clampStep: function (start, end, groupID, node, endTarget) { 89 | if (!this.navMesh) { 90 | endTarget.copy(end); 91 | return null; 92 | } else if (!node) { 93 | endTarget.copy(end); 94 | return this.getNode(end, groupID); 95 | } 96 | return pathfinder.clampStep(start, end, node, ZONE, groupID, endTarget); 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /src/primitives/README.md: -------------------------------------------------------------------------------- 1 | # Primitives 2 | 3 | Reusable entities / primitives. 4 | 5 | - ``: Flat grid, with subdivisions at regular intervals. 6 | - ``: Ocean with animated waves. 7 | - ``: Tube following a custom path. 8 | 9 | ## Usage 10 | 11 | Basic: 12 | 13 | ```html 14 | 15 | 16 | 17 | 18 | 19 | 20 | ``` 21 | 22 | Custom: 23 | 24 | ```html 25 | 26 | ``` 27 | 28 | ```html 29 | 30 | ``` 31 | -------------------------------------------------------------------------------- /src/primitives/a-grid.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flat grid. 3 | * 4 | * Defaults to 75x75. 5 | */ 6 | module.exports = AFRAME.registerPrimitive('a-grid', { 7 | defaultComponents: { 8 | geometry: { 9 | primitive: 'plane', 10 | width: 75, 11 | height: 75 12 | }, 13 | rotation: {x: -90, y: 0, z: 0}, 14 | material: { 15 | src: 'url(https://cdn.jsdelivr.net/gh/donmccurdy/aframe-extras@v1.16.3/assets/grid.png)', 16 | repeat: '75 75' 17 | } 18 | }, 19 | mappings: { 20 | width: 'geometry.width', 21 | height: 'geometry.height', 22 | src: 'material.src' 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/primitives/a-ocean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flat-shaded ocean primitive. 3 | * 4 | * Based on a Codrops tutorial: 5 | * http://tympanus.net/codrops/2016/04/26/the-aviator-animating-basic-3d-scene-threejs/ 6 | */ 7 | module.exports.Primitive = AFRAME.registerPrimitive('a-ocean', { 8 | defaultComponents: { 9 | ocean: {}, 10 | rotation: {x: -90, y: 0, z: 0} 11 | }, 12 | mappings: { 13 | width: 'ocean.width', 14 | depth: 'ocean.depth', 15 | density: 'ocean.density', 16 | amplitude: 'ocean.amplitude', 17 | amplitudeVariance: 'ocean.amplitudeVariance', 18 | speed: 'ocean.speed', 19 | speedVariance: 'ocean.speedVariance', 20 | color: 'ocean.color', 21 | opacity: 'ocean.opacity' 22 | } 23 | }); 24 | 25 | module.exports.Component = AFRAME.registerComponent('ocean', { 26 | schema: { 27 | // Dimensions of the ocean area. 28 | width: {default: 10, min: 0}, 29 | depth: {default: 10, min: 0}, 30 | 31 | // Density of waves. 32 | density: {default: 10}, 33 | 34 | // Wave amplitude and variance. 35 | amplitude: {default: 0.1}, 36 | amplitudeVariance: {default: 0.3}, 37 | 38 | // Wave speed and variance. 39 | speed: {default: 1}, 40 | speedVariance: {default: 2}, 41 | 42 | // Material. 43 | color: {default: '#7AD2F7', type: 'color'}, 44 | opacity: {default: 0.8} 45 | }, 46 | 47 | /** 48 | * Use play() instead of init(), because component mappings – unavailable as dependencies – are 49 | * not guaranteed to have parsed when this component is initialized. 50 | */ 51 | play: function () { 52 | const el = this.el; 53 | const data = this.data; 54 | let material = el.components.material; 55 | 56 | const geometry = new THREE.PlaneGeometry(data.width, data.depth, data.density, data.density); 57 | this.waves = []; 58 | const posAttribute = geometry.getAttribute('position'); 59 | for (let i = 0; i < posAttribute.count; i++) { 60 | this.waves.push({ 61 | z: posAttribute.getZ(i), 62 | ang: Math.random() * Math.PI * 2, 63 | amp: data.amplitude + Math.random() * data.amplitudeVariance, 64 | speed: (data.speed + Math.random() * data.speedVariance) / 1000 // radians / frame 65 | }); 66 | } 67 | 68 | if (!material) { 69 | material = {}; 70 | material.material = new THREE.MeshPhongMaterial({ 71 | color: data.color, 72 | transparent: data.opacity < 1, 73 | opacity: data.opacity, 74 | flatShading: true, 75 | }); 76 | } 77 | 78 | this.mesh = new THREE.Mesh(geometry, material.material); 79 | el.setObject3D('mesh', this.mesh); 80 | }, 81 | 82 | remove: function () { 83 | this.el.removeObject3D('mesh'); 84 | }, 85 | 86 | tick: function (t, dt) { 87 | if (!dt) return; 88 | 89 | const posAttribute = this.mesh.geometry.getAttribute('position'); 90 | for (let i = 0; i < posAttribute.count; i++){ 91 | const vprops = this.waves[i]; 92 | const value = vprops.z + Math.sin(vprops.ang) * vprops.amp; 93 | posAttribute.setZ(i, value); 94 | vprops.ang += vprops.speed * dt; 95 | } 96 | posAttribute.needsUpdate = true; 97 | } 98 | }); 99 | -------------------------------------------------------------------------------- /src/primitives/a-tube.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tube following a custom path. 3 | * 4 | * Usage: 5 | * 6 | * ```html 7 | * 8 | * ``` 9 | */ 10 | module.exports.Primitive = AFRAME.registerPrimitive('a-tube', { 11 | defaultComponents: { 12 | tube: {}, 13 | }, 14 | mappings: { 15 | path: 'tube.path', 16 | segments: 'tube.segments', 17 | radius: 'tube.radius', 18 | 'radial-segments': 'tube.radialSegments', 19 | closed: 'tube.closed' 20 | } 21 | }); 22 | 23 | module.exports.Component = AFRAME.registerComponent('tube', { 24 | schema: { 25 | path: {default: []}, 26 | segments: {default: 64}, 27 | radius: {default: 1}, 28 | radialSegments: {default: 8}, 29 | closed: {default: false} 30 | }, 31 | 32 | init: function () { 33 | const el = this.el, 34 | data = this.data; 35 | let material = el.components.material; 36 | 37 | if (!data.path.length) { 38 | console.error('[a-tube] `path` property expected but not found.'); 39 | return; 40 | } 41 | 42 | const curve = new THREE.CatmullRomCurve3(data.path.map(function (point) { 43 | point = point.split(' '); 44 | return new THREE.Vector3(Number(point[0]), Number(point[1]), Number(point[2])); 45 | })); 46 | const geometry = new THREE.TubeGeometry( 47 | curve, data.segments, data.radius, data.radialSegments, data.closed 48 | ); 49 | 50 | if (!material) { 51 | material = {}; 52 | material.material = new THREE.MeshPhongMaterial(); 53 | } 54 | 55 | this.mesh = new THREE.Mesh(geometry, material.material); 56 | this.el.setObject3D('mesh', this.mesh); 57 | }, 58 | 59 | update: function (prevData) { 60 | if (!Object.keys(prevData).length) return; 61 | 62 | this.remove(); 63 | this.init(); 64 | }, 65 | 66 | remove: function () { 67 | if (this.mesh) this.el.removeObject3D('mesh'); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /src/primitives/index.js: -------------------------------------------------------------------------------- 1 | require('./a-grid'); 2 | require('./a-ocean'); 3 | require('./a-tube'); 4 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../.jshintrc", 3 | "globals" : { 4 | "afterEach" : false, 5 | "beforeEach" : false, 6 | "describe" : false, 7 | "expect" : false, 8 | "it" : false, 9 | "setup" : false, 10 | "sinon" : false, 11 | "spyOn" : false, 12 | "suite" : false, 13 | "test" : false, 14 | "teardown" : false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/__init.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * __init.test.js is run before every test case. 3 | */ 4 | window.debug = true; 5 | 6 | var AScene = require('aframe').AScene; 7 | require('../'); 8 | 9 | setup(function () { 10 | this.sinon = sinon.sandbox.create(); 11 | // Stubs to not create a WebGL context since Travis CI runs headless. 12 | this.sinon.stub(AScene.prototype, 'render'); 13 | this.sinon.stub(AScene.prototype, 'resize'); 14 | this.sinon.stub(AScene.prototype, 'setupRenderer'); 15 | }); 16 | 17 | teardown(function () { 18 | // Clean up any attached elements. 19 | ['canvas', 'a-assets', 'a-scene'].forEach((tagName) => { 20 | const els = document.querySelectorAll(tagName); 21 | for (let i = 0; i < els.length; i++) { 22 | els[i].parentNode.removeChild(els[i]); 23 | } 24 | }); 25 | this.sinon.restore(); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/controls/keyboard-controls.test.js: -------------------------------------------------------------------------------- 1 | const entityFactory = require('../helpers').entityFactory; 2 | const KeyboardEvent = window.KeyboardEvent; 3 | 4 | suite('keyboard-controls', () => { 5 | let el, keyboardControls; 6 | 7 | setup((done) => { 8 | el = entityFactory(); 9 | el.setAttribute('keyboard-controls', ''); 10 | el.addEventListener('loaded', () => { 11 | keyboardControls = el.components['keyboard-controls']; 12 | done(); 13 | }); 14 | }); 15 | 16 | suite('isVelocityActive', () => { 17 | test('not active by default', () => { 18 | expect(keyboardControls.isVelocityActive()).to.be.false; 19 | }); 20 | 21 | test('active when target key is pressed', () => { 22 | window.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyW'})); 23 | expect(keyboardControls.isVelocityActive()).to.be.true; 24 | window.dispatchEvent(new KeyboardEvent('keyup', {code: 'KeyW'})); 25 | expect(keyboardControls.isVelocityActive()).to.be.false; 26 | }); 27 | 28 | test('inactive when disabled', () => { 29 | el.setAttribute('keyboard-controls', {enabled: false}); 30 | window.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyW'})); 31 | expect(keyboardControls.isVelocityActive()).to.be.false; 32 | }); 33 | }); 34 | 35 | suite('getVelocityDelta', () => { 36 | test('updates position with WASD keys', () => { 37 | window.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyW'})); 38 | expect(keyboardControls.getVelocityDelta()).to.shallowDeepEqual({x: 0, y: 0, z: -1}); 39 | window.dispatchEvent(new KeyboardEvent('keydown', {code: 'KeyA'})); 40 | expect(keyboardControls.getVelocityDelta()).to.shallowDeepEqual({x: -1, y: 0, z: -1}); 41 | window.dispatchEvent(new KeyboardEvent('keyup', {code: 'KeyW'})); 42 | window.dispatchEvent(new KeyboardEvent('keyup', {code: 'KeyA'})); 43 | expect(keyboardControls.getVelocityDelta()).to.shallowDeepEqual({x: 0, y: 0, z: 0}); 44 | }); 45 | 46 | test('updates position with arrow keys', () => { 47 | window.dispatchEvent(new KeyboardEvent('keydown', {code: 'ArrowUp'})); 48 | expect(keyboardControls.getVelocityDelta()).to.shallowDeepEqual({x: 0, y: 0, z: -1}); 49 | window.dispatchEvent(new KeyboardEvent('keydown', {code: 'ArrowLeft'})); 50 | expect(keyboardControls.getVelocityDelta()).to.shallowDeepEqual({x: -1, y: 0, z: -1}); 51 | window.dispatchEvent(new KeyboardEvent('keyup', {code: 'ArrowUp'})); 52 | window.dispatchEvent(new KeyboardEvent('keyup', {code: 'ArrowLeft'})); 53 | expect(keyboardControls.getVelocityDelta()).to.shallowDeepEqual({x: 0, y: 0, z: 0}); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helper method to create a scene, create an entity, add entity to scene, 3 | * add scene to document. 4 | * 5 | * @returns {object} An `` element. 6 | */ 7 | module.exports.entityFactory = function (opts) { 8 | const scene = document.createElement('a-scene'); 9 | const assets = document.createElement('a-assets'); 10 | const entity = document.createElement('a-entity'); 11 | scene.appendChild(assets); 12 | scene.appendChild(entity); 13 | entity.sceneEl = scene; 14 | 15 | opts = opts || {}; 16 | 17 | if (opts.assets) { 18 | opts.assets.forEach((asset) => { 19 | assets.appendChild(asset); 20 | }); 21 | } 22 | 23 | document.body.appendChild(scene); 24 | return entity; 25 | }; 26 | 27 | /** 28 | * Creates and attaches a mixin element (and an `` element if necessary). 29 | * 30 | * @param {string} id - ID of mixin. 31 | * @param {object} obj - Map of component names to attribute values. 32 | * @param {Element} scene - Indicate which scene to apply mixin to if necessary. 33 | * @returns {object} An attached `` element. 34 | */ 35 | module.exports.mixinFactory = function (id, obj, scene) { 36 | const mixinEl = document.createElement('a-mixin'); 37 | mixinEl.setAttribute('id', id); 38 | Object.keys(obj).forEach((componentName) => { 39 | mixinEl.setAttribute(componentName, obj[componentName]); 40 | }); 41 | 42 | const assetsEl = scene ? scene.querySelector('a-assets') : document.querySelector('a-assets'); 43 | assetsEl.appendChild(mixinEl); 44 | 45 | return mixinEl; 46 | }; 47 | 48 | /** 49 | * Test that is only run locally and is skipped on CI. 50 | */ 51 | module.exports.getSkipCISuite = function () { 52 | if (window.__env__.TEST_ENV === 'ci') { 53 | return suite.skip; 54 | } else { 55 | return suite; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /tests/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '../', 4 | browserify: { 5 | debug: true, 6 | paths: ['src'] 7 | }, 8 | browsers: ['Firefox', 'Chrome'], 9 | client: { 10 | captureConsole: true, 11 | mocha: {'ui': 'tdd'} 12 | }, 13 | envPreprocessor: ['TEST_ENV'], 14 | files: [ 15 | {pattern: 'tests/**/*.test.js'} 16 | ], 17 | frameworks: ['mocha', 'sinon-chai', 'chai-shallow-deep-equal', 'browserify'], 18 | preprocessors: {'tests/**/*.js': ['browserify', 'env']}, 19 | reporters: ['mocha'] 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /tests/misc/sphere-collider.test.js: -------------------------------------------------------------------------------- 1 | /* global suite, setup, test, expect */ 2 | const entityFactory = require('../helpers').entityFactory; 3 | 4 | suite('sphere-collider', () => { 5 | let el, collider, collidee; 6 | 7 | setup((done) => { 8 | el = entityFactory(); 9 | el.setAttribute('sphere-collider', 'objects: #collidee'); 10 | el.setAttribute('geometry', 'primitive: sphere'); 11 | collidee = document.createElement('a-entity'); 12 | collidee.setAttribute('id', 'collidee'); 13 | el.parentNode.appendChild(collidee); 14 | collidee.setAttribute('position', '5 5 5'); 15 | collidee.setAttribute('geometry', 'primitive: sphere'); 16 | el.parentNode.addEventListener('loaded', () => { 17 | collider = el.components['sphere-collider']; 18 | done(); 19 | }); 20 | }); 21 | 22 | suite('lifecycle', () => { 23 | test('attaches', () => { 24 | expect(collider).to.be.ok; 25 | }); 26 | test('detaches', (done) => { 27 | el.removeAttribute('sphere-collider'); 28 | process.nextTick(() => { 29 | expect(collider.el.components['sphere-collider']).to.not.be.ok; 30 | done(); 31 | }); 32 | }); 33 | }); 34 | 35 | suite('collisions', () => { 36 | test('collided state remains until collision ends', () => { 37 | expect(collidee.is(collider.data.state)).to.be.false; 38 | collidee.setAttribute('position', collider.el.getAttribute('position')); 39 | collider.tick(); 40 | expect(collidee.is(collider.data.state)).to.be.true; 41 | collider.tick(); 42 | expect(collidee.is(collider.data.state)).to.be.true; 43 | collider.el.setAttribute('position', '5 5 5'); 44 | collider.tick(); 45 | expect(collidee.is(collider.data.state)).to.be.false; 46 | }); 47 | test('collision radius accounts for collidee scale', () => { 48 | // Obj3d needs forced update to pickup A-Frame attrs in test context 49 | collidee.object3D.updateMatrixWorld(true); 50 | collider.tick(); 51 | expect(collidee.is(collider.data.state)).to.be.false; 52 | collidee.setAttribute('scale', '10 10 10'); 53 | collidee.object3D.updateMatrixWorld(true); 54 | collider.tick(); 55 | expect(collidee.is(collider.data.state)).to.be.true; 56 | }); 57 | test('collision radius accounts for collider scale', () => { 58 | // Obj3d needs forced update to pickup A-Frame attrs in test context 59 | collidee.object3D.updateMatrixWorld(true); 60 | collider.tick(); 61 | expect(collidee.is(collider.data.state)).to.be.false; 62 | collider.el.setAttribute('scale', '160 1 1'); 63 | collider.el.object3D.updateMatrixWorld(true); 64 | collider.tick(); 65 | expect(collidee.is(collider.data.state)).to.be.true; 66 | }); 67 | test('hit and hitend event emission', function () { 68 | const hitSpy = sinon.spy(), 69 | hitEndSpy = sinon.spy(), 70 | targetHitSpy = sinon.spy(), 71 | targetHitEndSpy = sinon.spy(); 72 | collider.el.addEventListener('hit', hitSpy); 73 | collider.el.addEventListener('hitend', hitEndSpy); 74 | collidee.addEventListener('hit', targetHitSpy); 75 | collidee.addEventListener('hitend', targetHitEndSpy); 76 | collidee.setAttribute('position', collider.el.getAttribute('position')); 77 | collider.tick(); 78 | expect(hitSpy.calledWithMatch({detail: {el: collidee}})).to.be.true; 79 | expect(targetHitSpy.called).to.be.true; 80 | collider.el.setAttribute('position', '5 5 5'); 81 | collider.tick(); 82 | expect(hitEndSpy.calledWithMatch({detail: {el: collidee}})).to.be.true; 83 | expect(targetHitEndSpy.called).to.be.true; 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: { 5 | 'aframe-extras': './index.js', 6 | 'aframe-extras.controls': './src/controls/index.js', 7 | 'aframe-extras.loaders': './src/loaders/index.js', 8 | 'aframe-extras.misc': './src/misc/index.js', 9 | 'aframe-extras.pathfinding': './src/pathfinding/index.js', 10 | 'aframe-extras.primitives': './src/primitives/index.js', 11 | 'components/sphere-collider': './src/misc/sphere-collider.js', 12 | 'components/grab': './src/misc/grab.js', 13 | }, 14 | output: { 15 | libraryTarget: 'umd', 16 | path: path.resolve(__dirname, 'dist'), 17 | publicPath: '/dist/', 18 | filename: 19 | process.env.NODE_ENV === 'production' 20 | ? '[name].min.js' 21 | : '[name].js' 22 | }, 23 | externals: { 24 | // Stubs out `import ... from 'three'` so it returns `import ... from window.THREE` effectively using THREE global variable that is defined by AFRAME. 25 | three: 'THREE', 26 | }, 27 | devtool: 'source-map', 28 | mode: process.env.NODE_ENV === 'production' ? 'production' : 'development', 29 | devServer: { 30 | port: process.env.PORT || 8080, 31 | hot: false, 32 | liveReload: true, 33 | watchFiles: ['src/**', 'examples/**'], 34 | server: { 35 | type: 'https' 36 | }, 37 | static: { 38 | directory: path.resolve(__dirname) 39 | } 40 | }, 41 | }; 42 | --------------------------------------------------------------------------------