diff --git a/com/data2event.js b/com/data2event.js new file mode 100644 index 0000000..348cf09 --- /dev/null +++ b/com/data2event.js @@ -0,0 +1,44 @@ +/* + * ## data_events + * + * allows components to react to data changes + * + * ```html + * + * + * + * ``` + * + */ + +AFRAME.registerComponent('data2event',{ + + init: function(){ + setTimeout( () => { + for( let i in this.el.components ){ + let com = this.el.components[i] + com.data = this.reactify( this.el, com.data) + } + },50) + }, + + reactify: function(el,data){ + return new Proxy(data, { + get(me,k,v) { + return me[k] + }, + set(me,k,v){ + me[k] = v + el.emit(k,{el,k,v}) + } + }) + }, + +}) diff --git a/com/dom.js b/com/dom.js index 2b6cd53..7a6d8c3 100644 --- a/com/dom.js +++ b/com/dom.js @@ -42,7 +42,11 @@ AFRAME.registerComponent('dom',{ this.com = c } }) - if( !this.dom ) return console.warn('dom.js did not find a .dom object inside components') + if( !this.dom || !this.com){ + return console.warn('dom.js did not find a .dom object inside component') + } + + console.dir(this.dom) this .ensureOverlay() @@ -56,14 +60,14 @@ AFRAME.registerComponent('dom',{ this.el.emit('DOMready',{el: this.el.dom}) }, - ensureOverlay(){ + ensureOverlay: function(){ // ensure overlay let overlay = document.querySelector('#overlay') if( !overlay ){ overlay = document.createElement('div') overlay.id = "overlay" document.body.appendChild(overlay) - document.querySelector("a-scene").setAttribute("webxr","overlayElement:#overlay") + // sceneEl.setAttribute("webxr","overlayElement:#overlay") } return this }, @@ -85,6 +89,7 @@ AFRAME.registerComponent('dom',{ this.el.dom = document.createElement('div') this.el.dom.innerHTML = this.dom.html(this) this.el.dom.className = this.dom.attrName + console.dir(this.com.data) this.com.data = this.reactify( this.el, this.com.data ) if( this.dom.events ) this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) ) this.el.dom = this.el.dom.children[0] diff --git a/com/example/helloworld.js b/com/example/helloworld.js index 7c0f930..c253280 100644 --- a/com/example/helloworld.js +++ b/com/example/helloworld.js @@ -1,19 +1,22 @@ AFRAME.registerComponent('helloworld', { schema: { wireframe: { type:"boolean", "default":false}, - foo: {type:"string","default":"foo"} + text: {type:"string","default":"hello world"} }, - dependencies:['dom'], + dependencies: ['data2event'], - init: function () { + init: async function() { this.el.object3D.visible = false + await AFRAME.utils.require(this.dependencies) + this.el.setAttribute("data2event","") + this.el.innerHTML = ` - + ` }, @@ -21,6 +24,7 @@ AFRAME.registerComponent('helloworld', { launcher: function(e){ this.el.object3D.visible = !this.el.object3D.visible + clearInterval(this.interval) this.interval = setInterval( () => { this.data.wireframe = !this.data.wireframe }, 500 ) diff --git a/com/launcher.js b/com/launcher.js index 5da96f0..7d2b7b2 100644 --- a/com/launcher.js +++ b/com/launcher.js @@ -18,23 +18,54 @@ AFRAME.registerComponent('launcher', { schema: { - attach: { type:"selector"} + attach: { type:"selector"}, + padding: { type:"number","default":0.15}, + fingerTip: {type:"selector"}, + fingerDistance: {type:"number", "default":0.25}, + rescale: {type:"number","default":0.4}, + open: { type:"boolean", "default":true}, + colors: { type:"array", "default": [ + '#4C73FE', + '#554CFE', + '#864CFE', + '#B44CFE', + '#E24CFE', + '#FE4CD3', + '#333333', + ]}, + paused: { type:"boolean","default":false}, + cols: { type:"number", "default": 5 } }, dependencies:['dom'], init: async function () { - this.data.apps = [] + this.worldPosition = new THREE.Vector3() await AFRAME.utils.require({ - html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME - dom: "./com/dom.js", - svgfile: "https://7dir.github.io/aframe-svgfile-component/aframe-svgfile-component.min.js", + html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME + dom: "./com/dom.js", + data_events: "./com/data2event.js" }) this.el.setAttribute("dom","") this.render() - this.el.sceneEl.addEventListener('enter-vr', (e) => this.render() ) + + if( this.data.attach ){ + this.el.object3D.visible = false + if( this.isHand(this.data.attach) ){ + this.data.attach.addEventListener('model-loaded', () => this.attachMenu() ) + // add button + this.menubutton = this.createMenuButton() + this.menubutton.object3D.visible = false + this.data.attach.appendChild( this.menubutton ) + }else this.data.attach.appendChild(this.el) + } + + }, + + isHand: (el) => { + return el.getAttributeNames().filter( (n) => n.match(/^hand-tracking/) ? n : null ).length ? true : false }, dom: { @@ -100,26 +131,40 @@ AFRAME.registerComponent('launcher', { }, events:{ + open: function(){ + this.preventAccidentalButtonPresses() + if( this.data.open ){ + this.el.setAttribute("animation",`dur: 200; property: scale; from: 0 0 1; to: ${this.data.rescale} ${this.data.rescale} ${this.data.rescale}`) + this.menubutton.object3D.visible = false + }else{ + this.el.setAttribute("animation",`dur: 200; property: scale; from: ${this.data.rescale} ${this.data.rescale} ${this.data.rescale}; to: 0 0 1`) + this.menubutton.object3D.visible = true + } + } + }, + preventAccidentalButtonPresses: function(){ + this.data.paused = true + setTimeout( () => this.data.paused = false, 500 ) // prevent menubutton press collide with animated buttons + }, + + createMenuButton: function(colo){ + let aentity = document.createElement('a-entity') + aentity.setAttribute("mixin","menubutton") + aentity.addEventListener('obbcollisionstarted', this.onpress ) + aentity.addEventListener('obbcollisionended', this.onreleased ) + return aentity }, render: async function(){ if( !this.el.dom ) return // too early (dom.js component not ready) - let inVR = this.sceneEl && this.sceneEl.renderer.xr.isPresenting let items = [...this.el.children] let requires = [] - let i = 0 - let colors = [ - '#4C73FE', - '#554CFE', - '#864CFE', - '#B44CFE', - '#E24CFE', - '#FE4CD3' - ] - - const add2D = (launchCom,el,manifest,aentity) => { + let i = 0 + let j = 0 + let colors = this.data.colors + const add2D = (launchCom,el,manifest) => { let btn = document.createElement('button') btn.innerHTML = `${ manifest?.icons?.length > 0 ? `` @@ -132,36 +177,105 @@ AFRAME.registerComponent('launcher', { const add3D = (launchCom,el,manifest) => { let aentity = document.createElement('a-entity') let atext = document.createElement('a-entity') + let padding = this.data.padding + if( (i % this.data.cols) == 0 ) j++ aentity.setAttribute("mixin","menuitem") - aentity.setAttribute("position",`${i++ * 0.2} 0 0`) + aentity.setAttribute("position",`${padding+(i++ % this.data.cols) * padding} ${j*padding} 0`) if( !aentity.getAttribute("material")){ aentity.setAttribute('material',`side: double; color: ${colors[ i % colors.length]}`) } + aentity.addEventListener('obbcollisionstarted', this.onpress ) + aentity.addEventListener('obbcollisionended', this.onreleased ) atext.setAttribute("text",`value: ${manifest.short_name}; align: baseline; anchor: align; align:center; wrapCount:7`) atext.setAttribute("scale","0.1 0.1 0.1") - atext.setAttribute("position","0 0 0.0") aentity.appendChild(atext) this.el.appendChild(aentity) + aentity.launchCom = launchCom return aentity } // finally render them! this.el.dom.innerHTML = '' // clear this.system.components.map( (c) => { - const launchComponentKey = c.getAttributeNames().pop() + const launchComponentKey = c.getAttributeNames().shift() const launchCom = c.components[ launchComponentKey ] if( !launchCom ) return console.warn(`could not find component '${launchComponentKey}' (forgot to include script-tag?)`) const manifest = launchCom.manifest if( manifest ){ - add2D(launchCom,c,manifest, add3D(launchCom,c,manifest) ) + add2D(launchCom,c,manifest) + add3D(launchCom,c,manifest) } }) - if( this.data.attach ){ - this.el.object3D.visible = inVR ? true : false - // if( inVR ) this.data.attach.appendChild(this.el) - } + }, + onpress: function(e){ + const launcher = document.querySelector('[launcher]').components.launcher + if( launcher.data.paused ) return // prevent accidental pressed due to animation + if( e.detail.withEl.computedMixinStr == 'menuitem' ) return // dont react to menuitems touching eachother + + // if user press menu button toggle menu + if( launcher && e.srcElement.computedMixinStr == 'menubutton' ){ + return launcher.data.open = !launcher.data.open + } + if( launcher && !launcher.data.open ) return // dont process menuitems when menu is closed + let el = e.srcElement + if(!el) return + el.object3D.traverse( (o) => { + if( o.material && o.material.color ){ + if( !o.material.colorOriginal ) o.material.colorOriginal = o.material.color.clone() + o.material.color.r *= 0.3 + o.material.color.g *= 0.3 + o.material.color.b *= 0.3 + } + }) + if( el.launchCom ){ + console.log("launcher.js: launching "+el.launchCom.el.getAttributeNames().shift()) + launcher.preventAccidentalButtonPresses() + el.launchCom.el.emit('launcher') // launch component! + } + }, + + onreleased: function(e){ + if( e.detail.withEl.computedMixinStr == 'menuitem' ) return // dont react to menuitems touching eachother + let el = e.srcElement + el.object3D.traverse( (o) => { + if( o.material && o.material.color ){ + if( o.material.colorOriginal ) o.material.color = o.material.colorOriginal.clone() + } + }) + }, + + attachMenu: function(){ + if( this.el.parentNode != this.data.attach ){ + this.el.object3D.visible = true + let armature = this.data.attach.object3D.getObjectByName('Armature') + if( !armature ) return console.warn('cannot find armature') + this.data.attach.object3D.children[0].add(this.el.object3D) + this.el.object3D.scale.x = this.data.rescale + this.el.object3D.scale.y = this.data.rescale + this.el.object3D.scale.z = this.data.rescale + + // add obb-collider to index finger-tip + let aentity = document.createElement('a-entity') + trackedObject3DVariable = 'parentNode.components.hand-tracking-controls.bones.9'; + this.data.fingerTip.appendChild(aentity) + aentity.setAttribute('obb-collider', {trackedObject3D: trackedObject3DVariable, size: 0.015}); + + if( this.isHand(this.data.attach) ){ + // shortly show and hide menu into palm (hint user) + setTimeout( () => { this.data.open = false }, 1500 ) + } + } + }, + + tick: function(){ + if( this.data.open ){ + let indexTipPosition = document.querySelector('#right-hand[hand-tracking-controls]').components['hand-tracking-controls'].indexTipPosition + this.el.object3D.getWorldPosition(this.worldPosition) + const lookingAtPalm = this.data.attach.components['hand-tracking-controls'].wristObject3D.rotation.z > 2.0 + if( !lookingAtPalm ){ this.data.open = false } + } }, manifest: { // HTML5 manifest to identify app to xrsh @@ -223,7 +337,7 @@ AFRAME.registerSystem('launcher',{ init: function(){ this.components = [] - // observer HTML changes in + // observe HTML changes in observer = new MutationObserver( (a,b) => this.getLaunchables(a,b) ) observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false}); },