From c1d3afe540aead56c4a5f645aaa22a84a489d199 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Wed, 29 May 2024 16:13:53 +0000 Subject: [PATCH] main: work in progress [might break] --- com/data2event.js | 4 +- com/dom.js | 20 +++----- com/example/helloworld-iframe.js | 73 +++++++++++++++------------ com/example/helloworld-window.js | 87 +++++++++++++++++++------------- com/launcher.js | 8 +-- com/paste.js | 5 +- com/require.js | 9 ++-- app/spatialize.js => com/xd.js | 64 ++++++++++++----------- 8 files changed, 152 insertions(+), 118 deletions(-) rename app/spatialize.js => com/xd.js (56%) diff --git a/com/data2event.js b/com/data2event.js index 348cf09..ff7d628 100644 --- a/com/data2event.js +++ b/com/data2event.js @@ -24,7 +24,9 @@ AFRAME.registerComponent('data2event',{ setTimeout( () => { for( let i in this.el.components ){ let com = this.el.components[i] - com.data = this.reactify( this.el, com.data) + if( typeof com.data == 'object' ){ + com.data = this.reactify( this.el, com.data) + } } },50) }, diff --git a/com/dom.js b/com/dom.js index 7a6d8c3..093888d 100644 --- a/com/dom.js +++ b/com/dom.js @@ -46,15 +46,13 @@ AFRAME.registerComponent('dom',{ return console.warn('dom.js did not find a .dom object inside component') } - console.dir(this.dom) - this .ensureOverlay() .addCSS() .createReactiveDOMElement() + .assignUniqueID() .scaleDOMvsXR() .triggerKeyboardForInputs() - .setupListeners() document.querySelector('#overlay').appendChild(this.el.dom) this.el.emit('DOMready',{el: this.el.dom}) @@ -89,13 +87,18 @@ 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] return this }, + assignUniqueID: function(){ + // assign unique app id so it's easy to reference (by html-mesh component e.g.) + if( !this.el.uid ) this.el.uid = '_'+String(Math.random()).substr(10) + return this + }, + addCSS: function(){ if( this.dom.css && !document.head.querySelector(`style#${this.attrName}`) ){ document.head.innerHTML += `` @@ -108,15 +111,6 @@ AFRAME.registerComponent('dom',{ return this }, - setupListeners: function(){ - this.el.sceneEl.addEventListener('apps:2D', () => this.el.setAttribute('visible', false) ) - this.el.sceneEl.addEventListener('apps:XR', () => { - this.el.setAttribute('visible', true) - this.el.setAttribute("html",`html:#${this.el.uid}; cursor:#cursor`) - }) - return this - }, - triggerKeyboardForInputs: function(){ // https://developer.oculus.com/documentation/web/webxr-keyboard ; [...this.el.dom.querySelectorAll('[type=text]')].map( (input) => { diff --git a/com/example/helloworld-iframe.js b/com/example/helloworld-iframe.js index 4131713..f72c2c5 100644 --- a/com/example/helloworld-iframe.js +++ b/com/example/helloworld-iframe.js @@ -12,7 +12,7 @@ AFRAME.registerComponent('helloworld-iframe', { }, dom: { - scale: 1, + scale: 3, events: ['click','keydown'], html: (me) => `
@@ -26,49 +26,60 @@ AFRAME.registerComponent('helloworld-iframe', { events:{ - // component events - html: function( ){ console.log("html-mesh requirement mounted") }, - // combined AFRAME+DOM reactive events click: function(e){ }, // keydown: function(e){ }, // - // reactive events for this.data updates - myvalue: function(e){ /*this.el.dom.querySelector('b').innerText = this.data.myvalue*/ }, - - init: function(){ - alert("ja") + // reactive updates (data2event.js) + url: function(e){ + this.el.dom.querySelector('iframe').src = this.data.url + console.dir(this.el.dom.querySelector('iframe')) }, - ready: function( ){ - this.el.dom.style.display = 'none' - console.log("this.el.dom has been added to DOM") - }, - - launcher: function(){ - console.log("this.el.dom iframe has been added to DOM") + launcher: async function(){ let URL = this.data.url || prompt('enter URL to display','https://fabien.benetou.fr/Wiki/Wiki') if( !URL ) return - this.el.dom.querySelector('iframe').src = URL - new WinBox("Hello World",{ - width: 250, - height: 150, - class:["iframe"], - x:"center", - y:"center", - id: this.el.uid, // important hint for html-mesh - root: document.querySelector("#overlay"), - mount: this.el.dom, - onclose: () => { this.el.dom.style.display = 'none'; return false; } - }); - this.el.dom.style.display = '' + + let s = await AFRAME.utils.require(this.requires) + + // instance this component + const instance = this.el.cloneNode(false) + this.el.sceneEl.appendChild( instance ) + instance.setAttribute("dom", "") + instance.setAttribute("data2event","") + instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' ) + instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(1.39) ) + instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera + + const setupWindow = () => { + const com = instance.components['helloworld-iframe'] + instance.dom.style.display = 'none' + new WinBox("Hello World",{ + width: 250, + height: 150, + class:["iframe"], + x:"center", + y:"center", + id: instance.uid, // important hint for html-mesh + root: document.querySelector("#overlay"), + mount: instance.dom, + onclose: () => { instance.dom.style.display = 'none'; return false; }, + oncreate: () => { + com.data.url = URL + instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`) + } + }); + instance.dom.style.display = '' + } + + setTimeout( () => setupWindow(), 10 ) // give new components time to init }, }, manifest: { // HTML5 manifest to identify app to xrsh - "short_name": "Hello world", - "name": "Hello world", + "short_name": "Iframe", + "name": "Hello world IFRAME window", "icons": [ { "src": "https://css.gg/browse.svg", diff --git a/com/example/helloworld-window.js b/com/example/helloworld-window.js index 8a2e237..ed48d76 100644 --- a/com/example/helloworld-window.js +++ b/com/example/helloworld-window.js @@ -1,20 +1,23 @@ AFRAME.registerComponent('helloworld-window', { schema: { - foo: { type:"string"} + foo: { type:"string", "default":"foo"} }, - dependencies: ['dom'], - - init: async function(){ - this.el.object3D.visible = false - + requires: { + dom: "./com/dom.js", // interpret .dom object + xd: "./com/dom.js", // allow switching between 2D/3D + html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME + winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox + winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", // }, + init: function(){ }, + dom: { - scale: 1, + scale: 3, events: ['click','keydown'], html: (me) => `
-
${me.data.foo} ${me.data.myvalue} +
${me.data.foo} ${me.data.myvalue}
`, css: (me) => `.helloworld-window div.pad { padding:11px; }` @@ -26,41 +29,53 @@ AFRAME.registerComponent('helloworld-window', { click: function(e){ }, // keydown: function(e){ }, // - // reactive events for this.data updates - myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue }, + // reactive events for this.data updates (data2event.js) + myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue }, + foo: function(e){ this.el.dom.querySelector('span').innerText = this.data.foo }, launcher: async function(){ - let s = await AFRAME.utils.require({ - dom: "./com/dom.js", // interpret .dom object - html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME - winboxjs: "https://unpkg.com/winbox@0.2.82/dist/winbox.bundle.min.js", // deadsimple windows: https://nextapps-de.github.io/winbox - winboxcss: "https://unpkg.com/winbox@0.2.82/dist/css/winbox.min.css", // - }) - console.dir(s) - this.el.setAttribute("dom","") - this.el.setAttribute("data2event","") - this.el.setAttribute("html","") - this.el.dom.style.display = 'none' - this.data.myvalue = 1 - return - setInterval( () => this.data.myvalue++, 100 ) - new WinBox("Hello World",{ - width: 250, - height: 150, - x:"center", - y:"center", - id: this.el.uid, // important hint for html-mesh - root: document.querySelector("#overlay"), - mount: this.el.dom, - onclose: () => { this.el.dom.style.display = 'none'; return false; } - }); - this.el.dom.style.display = '' + let s = await AFRAME.utils.require(this.requires) + + // instance this component + const instance = this.el.cloneNode(false) + this.el.sceneEl.appendChild( instance ) + instance.setAttribute("dom", "") + instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' ) + instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(1.39) ) + instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera + + const setupWindow = () => { + const com = instance.components['helloworld-window'] + instance.dom.style.display = 'none' + + new WinBox("Hello World",{ + width: 250, + height: 150, + x:"center", + y:"center", + id: instance.uid, // important hint for html-mesh + root: document.querySelector("#overlay"), + mount: instance.dom, + onclose: () => { instance.dom.style.display = 'none'; return false; }, + oncreate: () => instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`) + }); + instance.dom.style.display = '' // show + + // data2event demo + instance.setAttribute("data2event","") + com.data.myvalue = 1 + com.data.foo = `instance ${instance.uid}: ` + setInterval( () => com.data.myvalue++, 200 ) + } + + setTimeout( () => setupWindow(), 10 ) // give new components time to init + }, }, manifest: { // HTML5 manifest to identify app to xrsh - "short_name": "Hello world window", + "short_name": "window", "name": "Hello world window", "icons": [ { diff --git a/com/launcher.js b/com/launcher.js index 3db743d..ca012a3 100644 --- a/com/launcher.js +++ b/com/launcher.js @@ -49,6 +49,7 @@ AFRAME.registerComponent('launcher', { }) this.el.setAttribute("dom","") + this.el.setAttribute("noxd","ignore") // hint to XD.js that we manage ourselve concerning 2D/3D switching this.render() if( this.data.attach ){ @@ -81,7 +82,7 @@ AFRAME.registerComponent('launcher', { overflow:hidden; position: fixed; right: 162px; - bottom: 0px; + bottom: 10px; left:20px; background: transparent; padding-bottom: 54px; @@ -345,13 +346,14 @@ AFRAME.registerSystem('launcher',{ getLaunchables: function(mutationsList,observer){ let searchEvent = 'launcher' let els = [...this.sceneEl.getElementsByTagName("*")] + let seen = {} this.components = els.filter( (el) => { let hasEvent = false if( el.components ){ for( let i in el.components ){ - if( el.components[i].events && el.components[i].events[searchEvent] ){ - hasEvent = true + if( el.components[i].events && el.components[i].events[searchEvent] && !seen[i] ){ + hasEvent = seen[i] = true } } } diff --git a/com/paste.js b/com/paste.js index 4e8a053..ddb308b 100644 --- a/com/paste.js +++ b/com/paste.js @@ -108,7 +108,7 @@ AFRAME.registerComponent('paste', { return osbutton }, - getPositionInFrontOfCamera: function(){ + getPositionInFrontOfCamera: function(distance){ const camera = this.el.sceneEl.camera; let pos = new THREE.Vector3() let direction = new THREE.Vector3(); @@ -117,7 +117,8 @@ AFRAME.registerComponent('paste', { camera.getWorldPosition(pos) direction.normalize(); // Scale the direction by 1 meter - direction.multiplyScalar(1.5); + if( !distance ) distance = 1.5 + direction.multiplyScalar(distance); // Add the camera's position to the scaled direction to get the target point pos.add(direction); return pos diff --git a/com/require.js b/com/require.js index fd90741..36ac00d 100644 --- a/com/require.js +++ b/com/require.js @@ -25,8 +25,11 @@ AFRAME.utils.require = function(arr_or_obj,opts){ packagesArr.map( (package) => { try{ package = package.match(/\./) ? package : AFRAME.utils.require.baseURL+package+".js" - let id = Object.keys(arr_or_obj)[i] - if( id == i ) id = parseURI(package).component + let id = Object.keys(arr_or_obj)[i++] + if( id.match(/^[0-9]/) ){ // if AFRAME component.dependency -array was passed + id = parseURI(package).component + } + // prevent duplicate requests if( AFRAME.required[id] ) return // already loaded before AFRAME.required[id] = true @@ -55,9 +58,9 @@ AFRAME.utils.require = function(arr_or_obj,opts){ } }catch(e){ console.error(`package ${package} could not be retrieved..aborting :(`); + console.dir(e) if( opts.halt ) throw e; } - i++ }) return Promise.all(deps) } diff --git a/app/spatialize.js b/com/xd.js similarity index 56% rename from app/spatialize.js rename to com/xd.js index badad40..4f86df0 100644 --- a/app/spatialize.js +++ b/com/xd.js @@ -1,12 +1,16 @@ -AFRAME.registerComponent('spatialize', { +AFRAME.registerComponent('xd', { schema: { foo: { type:"string"} }, init: function () { + if( Object.keys(this.el.components).length > 1 ) return // we collect a-entities which wish to be toggled in this.showElements() + + this.el.sceneEl.addEventListener('enter-vr',() => this.toggle(true) ) + this.el.sceneEl.addEventListener('exit-vr', () => this.toggle(false) ) + this.el.sceneEl.addEventListener('2D', () => this.showElements(false) ) + this.el.sceneEl.addEventListener('3D', () => this.showElements(true) ) - document.querySelector('a-scene').addEventListener('enter-vr',() => this.toggle(true) ) - document.querySelector('a-scene').addEventListener('exit-vr', () => this.toggle(false) ) // toggle immersive with ESCAPE document.body.addEventListener('keydown', (e) => e.key == 'Escape' && this.toggle() ) @@ -21,43 +25,25 @@ AFRAME.registerComponent('spatialize', { } ` - this.el.sceneEl.addEventListener('launched',(e) => { - console.dir(e) - let appEl = e.detail.el.dom - if( appEl && appEl.style && appEl.style.display != 'none' && appEl.innerHTML ){ - this.btn.style.display = '' // show button - } - }) + this.events.launcher = () => this.toggle() }, - requires:{ - // somecomponent: "https://unpkg.com/some-aframe-component/mycom.min.js" - }, - - events:{ - - // component events - ready: function(e){ - //this.btn.style.display = 'none' - this.btn.style.background = 'var(--xrsh-primary)' - this.btn.style.color = '#FFF' - }, - - launcher: function(e){ this.toggle() }, - + showElements: function(state){ + let els = [...document.querySelectorAll('[xd]')] + els = els.filter( (el) => el != this.el ? el : null ) // filter out self + els.map( (el) => el.setAttribute("visible", state ? "true" : false ) ) }, // draw a button so we can toggle apps between 2D / XR toggle: function(state){ state = state || !document.body.className.match(/XR/) document.body.classList[ state ? 'add' : 'remove'](['XR']) - AFRAME.scenes[0].emit( state ? 'apps:XR' : 'apps:2D') - this.btn.innerHTML = state ? "2D" : "2D" + AFRAME.scenes[0].emit( state ? '3D' : '2D') }, manifest: { // HTML5 manifest to identify app to xrsh - "short_name": "2D", - "name": "spatialize", + "short_name": "XD", + "name": "2D/3D switcher", "icons": [], "id": "/?source=pwa", "start_url": "/?source=pwa", @@ -103,3 +89,23 @@ in above's case "\nHelloworld application\n" will qualify as header. }); +AFRAME.utils.XD = function(){ + return document.body.classList.contains('XR') ? '3D' : '2D' +} + + +AFRAME.utils.XD.getPositionInFrontOfCamera = function(distance){ + const camera = AFRAME.scenes[0].camera; + let pos = new THREE.Vector3() + let direction = new THREE.Vector3(); + // Get camera's forward direction (without rotation) + camera.getWorldDirection(direction); + camera.getWorldPosition(pos) + direction.normalize(); + // Scale the direction by 1 meter + if( !distance ) distance = 1.5 + direction.multiplyScalar(distance); + // Add the camera's position to the scaled direction to get the target point + pos.add(direction); + return pos +}