From d719fb7e02a8ce6b8ba973c266a332b53b1f9e19 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Thu, 30 May 2024 14:39:42 +0000 Subject: [PATCH] main: work in progress [might break] --- {app => com.refactor}/aframe-inspector.js | 0 {app => com.refactor}/isoterminal.js | 0 {app => com.refactor}/load.js | 0 {app => com.refactor}/manual.js | 0 {app => com.refactor}/vconsole.js | 0 com/add.js | 95 +++++++ com/app.js | 289 ---------------------- com/cast.js | 166 +++++++++++++ com/dom.js | 4 +- com/example/helloworld-iframe.js | 33 ++- com/example/helloworld-window.js | 20 +- com/example/helloworld.js | 1 + com/launcher.js | 2 + com/xrfragments.js | 2 - 14 files changed, 306 insertions(+), 306 deletions(-) rename {app => com.refactor}/aframe-inspector.js (100%) rename {app => com.refactor}/isoterminal.js (100%) rename {app => com.refactor}/load.js (100%) rename {app => com.refactor}/manual.js (100%) rename {app => com.refactor}/vconsole.js (100%) create mode 100644 com/add.js delete mode 100644 com/app.js create mode 100644 com/cast.js diff --git a/app/aframe-inspector.js b/com.refactor/aframe-inspector.js similarity index 100% rename from app/aframe-inspector.js rename to com.refactor/aframe-inspector.js diff --git a/app/isoterminal.js b/com.refactor/isoterminal.js similarity index 100% rename from app/isoterminal.js rename to com.refactor/isoterminal.js diff --git a/app/load.js b/com.refactor/load.js similarity index 100% rename from app/load.js rename to com.refactor/load.js diff --git a/app/manual.js b/com.refactor/manual.js similarity index 100% rename from app/manual.js rename to com.refactor/manual.js diff --git a/app/vconsole.js b/com.refactor/vconsole.js similarity index 100% rename from app/vconsole.js rename to com.refactor/vconsole.js diff --git a/com/add.js b/com/add.js new file mode 100644 index 0000000..16bd6c2 --- /dev/null +++ b/com/add.js @@ -0,0 +1,95 @@ +AFRAME.registerComponent('add', { + schema:{ + comps: {type:"array"} + }, + + init: function () { }, + + getInstallables: function(){ + const installed = document.querySelector('[launcher]').components.launcher.system.getLaunchables() + return this.data.comps.map( (c) => { + return installed[c] ? null : c + }) + .filter( (c) => c ) // filters out null elements + }, + + events:{ + launcher: function(){ + if( this.el.sceneEl.renderer.xr.isPresenting ){ + this.el.sceneEl.exitVR() // *FIXME* we need a gui + } + + let msg = `Which item to add to the menu?\n\n` + const coms = this.getInstallables() + for( let i = 0; i < coms.length; i++ ){ + msg += `${i+1}. ${coms[i]}\n` + } + let choice = prompt(msg, 1) + if( parseInt(choice) == NaN ) return console.log("choice NaN") + + // add a-entity + selected component + const com = coms[ parseInt(choice)-1 ] + if( !com ) return console.log("choice != component") + AFRAME.utils.require([com]) + .then( () => { + let el = document.createElement('a-entity') + el.setAttribute( com.split("/").pop(), "") + this.el.sceneEl.appendChild(el) + }) + } + }, + + manifest: { // HTML5 manifest to identify app to xrsh + "short_name": "+", + "name": "Add item", + "icons": [ + { + "src": "https://css.gg/add-r.svg", + "type": "image/svg+xml", + "sizes": "512x512" + } + ], + "id": "/?source=pwa", + "start_url": "/?source=pwa", + "background_color": "#3367D6", + "display": "standalone", + "scope": "/", + "theme_color": "#3367D6", + "category":"system", + "shortcuts": [ + { + "name": "What is the latest news?", + "cli":{ + "usage": "helloworld [options]", + "example": "helloworld news", + "args":{ + "--latest": {type:"string"} + } + }, + "short_name": "Today", + "description": "View weather information for today", + "url": "/today?source=pwa", + "icons": [{ "src": "/images/today.png", "sizes": "192x192" }] + } + ], + "description": "adds item to menu", + "screenshots": [ + { + "src": "/images/screenshot1.png", + "type": "image/png", + "sizes": "540x720", + "form_factor": "narrow" + } + ], + "help":` +Helloworld application + +This is a help file which describes the application. +It will be rendered thru troika text, and will contain +headers based on non-punctualized lines separated by linebreaks, +in above's case "\nHelloworld application\n" will qualify as header. + ` + } + +}); + diff --git a/com/app.js b/com/app.js deleted file mode 100644 index 4127e10..0000000 --- a/com/app.js +++ /dev/null @@ -1,289 +0,0 @@ -// this is a highlevel way of loading buildless 'apps' (a collection of js components) - -AFRAME.required = {} -AFRAME.app = new Proxy({ - - order:0, - - components: {}, // component-strings in this array are automatically - // added to each app - add(component, entity){ - // categorize by component to prevent similar apps loading duplicate dependencies simultaniously - this[component] = this[component] || [] - this[component].push(entity) - entity.data.order = entity.data.order || this.count() - }, - count(){ - let n = 0 - this.foreach( () => n++ ) - return n - }, - foreach(cb){ - const isArray = (e) => e.push - let arr = [] - for( let i in this ){ - if( isArray(this[i]) ) this[i].map( (app) => arr.push(app.el.app) ) - } - arr.sort( (a,b) => a.data.order > b.data.order ) - .map( cb ) - } -},{ - get(me,k) { return me[k] }, - set(me,k,v){ me[k] = v } -}) - -/* - * This is the abstract 'app' component - */ - -appComponent = { - schema:{ - "uri":{ type:"string"} - }, - - events:{ - "app:ready": function(){ - let {id,component,type} = this.parseAppURI(this.data.uri) - AFRAME.app[component].map( (app) => { - if( !app.el.getAttribute(component) ){ - if( AFRAME.components[ component ] ){ - app.el.setAttribute(component,app.data) - }else console.warn(`${component} was not fully downloaded yet (${app.data.uri})`) - } - }) - }, - "requires:ready": function(){ - let {id,component,type} = this.parseAppURI(this.data.uri) - AFRAME.app[component].map( (app) => { - if( app.readyFired ) return - setTimeout( () => { - if( this.el.dom ) this.el.dom.style.display = '' // finally show dom elements - app.el.emit('ready') - app.readyFired = true - },400) // big js scripts need some parsing time - }) - }, - }, - - init: function() { - let {id,component,type} = this.parseAppURI(this.data.uri) - let sel = `script#${component}` - if( AFRAME.app[component] || AFRAME.components[component] || document.head.querySelector(sel) ) return AFRAME.app.add(component,this) - AFRAME.app.add(component,this) - this.require([ this.data.uri ], 'app:ready') - }, - - parseAppURI: AFRAME.AComponent.prototype.parseAppURI = function(uri){ - return { - id: String(uri).split("/").pop(), // 'a/b/c/mycom.js' => 'mycom.js' - component: String(uri).split("/").pop().split(".js").shift(), // 'mycom.js' => 'mycom' - type: String(uri).split(".").pop() // 'mycom.js' => 'js' - } - }, - // usage: require(["./app/foo.js"]) - // require({foo: "https://foo.com/foo.js"}) - require: AFRAME.AComponent.prototype.require = function(packages,readyEvent){ - let deps = [] - if( !packages.map ) packages = Object.values(packages) - packages.map( (package) => { - try{ - let id = package.split("/").pop() - // prevent duplicate requests - if( AFRAME.required[id] ) return - AFRAME.required[id] = true - - if( !document.head.querySelector(`script#${id}`) ){ - let {component,type} = this.parseAppURI(package) - let p = new Promise( (resolve,reject) => { - switch(type){ - case "js": let script = document.createElement("script") - script.id = id - script.src = package - script.onload = () => resolve() - script.onerror = (e) => reject(e) - document.head.appendChild(script) - break; - case "css": let link = document.createElement("link") - link.id = id - link.href = package - link.rel = 'stylesheet' - document.head.appendChild(link) - resolve() - break; - } - }) - deps.push(p) - } - }catch(e){ console.error(`package ${package} could not be retrieved..aborting :(`); throw e; } - }) - Promise.all(deps).then( () => { - this.el.emit( readyEvent || 'requireReady', packages) - }) - } - -} - -AFRAME.registerComponent('app', appComponent) -AFRAME.registerComponent('com', appComponent) - -/* - * Here are monkeypatched AFRAME component prototype functions - * - * monkeypatching initComponent will trigger events when components - * are initialized (that way apps can react to attached components) - * basically, in both situations: - * - * - * - * event 'foo' will be triggered as both entities (in)directly require component 'foo' - */ - -AFRAME.AComponent.prototype.initComponent = function(initComponent){ - return function(){ - this.el.emit( this.attrName, this) - this.scene = AFRAME.scenes[0] // mount scene for convenience - return initComponent.apply(this,arguments) - } -}( AFRAME.AComponent.prototype.initComponent) - - -AFRAME.AComponent.prototype.updateProperties = function(updateProperties){ - let setupApp = function(){ - updateProperties.apply(this,arguments) - - if( !this.data || !this.data.uri || this.isApp ) return // only deal with apps (once) - - // 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") - } - - const reactify = (el,aframe) => new Proxy(this.data,{ - get(me,k,v) { return me[k] - }, - set(me,k,v){ - me[k] = v - aframe.emit(k,{el,k,v}) - } - }) - - // reactify components with dom-definition - if( this.data.uri && this.dom && !this.el.dom ){ - - tasks = { - - createReactiveDOMElement: () => { - this.el.dom = document.createElement('div') - this.el.dom.className = this.parseAppURI(this.data.uri).component - this.el.dom.innerHTML = this.com.dom.html(this) - this.el.dom.style.display = 'none' - this.data = reactify( this.dom.el, this.el ) - this.dom.events.map( (e) => this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) ) ) - return tasks - }, - - addCSS: () => { - if( this.dom.css && !document.head.querySelector(`style#${this.attrName}`) ){ - document.head.innerHTML += `` - } - return tasks - }, - - scaleDOMvsXR: () => { - if( this.dom.scale ) this.el.setAttribute('scale',`${this.dom.scale} ${this.dom.scale} ${this.dom.scale}`) - return tasks - }, - - setupListeners: () => { - this.scene.addEventListener('apps:2D', () => this.el.setAttribute('visible', false) ) - this.scene.addEventListener('apps:XR', () => { - this.el.setAttribute('visible', true) - this.el.setAttribute("html",`html:#${this.el.uid}; cursor:#cursor`) - }) - return tasks - }, - - initAutoComponents: () => { - for ( let i in AFRAME.app.components ) { - this.el.setAttribute( i, AFRAME.app.components[i] ) - } - return tasks - }, - - triggerKeyboardForInputs: () => { - // https://developer.oculus.com/documentation/web/webxr-keyboard ; - [...this.el.dom.querySelectorAll('[type=text]')].map( (input) => { - let triggerKeyboard = function(){ - this.focus() - console.log("focus") - } - input.addEventListener('click', triggerKeyboard ) - }) - return tasks - } - - } - - tasks - .addCSS() - .createReactiveDOMElement() - .scaleDOMvsXR() - .triggerKeyboardForInputs() - .setupListeners() - .initAutoComponents() - - document.querySelector('#overlay').appendChild(this.el.dom) - this.el.emit('DOMready',{el: this.el.dom}) - - }else this.data = reactify( this.el, this.el ) - - // assign unique app id - if( !this.el.uid ) this.el.uid = '_'+String(Math.random()).substr(10) - - // require coms - let requires = {} - for ( let i in AFRAME.app.components ) { - if( !AFRAME.components[i] ) requires[i] = AFRAME.app.components[i] - } - if( this.requires ) requires = {...requires, ...this.requires } - if( Object.values(requires).length ) this.require( requires, 'requires:ready' ) - else this.el.emit('requires:ready' ) - - // mark app as being initialized - this.isApp = true - this.el.app = this - } - return function(){ - try{ setupApp.apply(this,arguments) }catch(e){ console.error(e) } - } -}( AFRAME.AComponent.prototype.updateProperties) - -document.head.innerHTML += ` - -` diff --git a/com/cast.js b/com/cast.js new file mode 100644 index 0000000..0b5ec56 --- /dev/null +++ b/com/cast.js @@ -0,0 +1,166 @@ +AFRAME.registerComponent('cast', { + schema:{ + comps: {type:"array"} + }, + + requires: { + dom: "./com/dom.js", // interpret .dom object + xd: "./com/xd.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", // + }, + + dom: { + scale: 0.8, + events: ['click','keydown'], + html: (me) => `
+ +
`, + + css: (me) => `.helloworld-window div.pad { padding:11px; }` + }, + + init: function () { }, + + getInstallables: function(){ + const installed = document.querySelector('[launcher]').components.launcher.system.getLaunchables() + return this.data.comps.map( (c) => { + return installed[c] ? null : c + }) + .filter( (c) => c ) // filters out null elements + }, + + events:{ + + launcher: async function(){ + if( this.el.sceneEl.renderer.xr.isPresenting ){ + this.el.sceneEl.exitVR() // *FIXME* we need a gui + } + const el = document.querySelector('body'); + const cropTarget = await CropTarget.fromElement(el); + const stream = await navigator.mediaDevices.getDisplayMedia(); + const [track] = stream.getVideoTracks(); + this.track = track + this.stream = stream + this.createWindow() + } + }, + + createWindow: async function(){ + 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("xd", "") // allows flipping between DOM/WebGL when toggling XD-button + instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' ) + instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) ) + instance.setAttribute("grabbable","") + instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera + instance.track = this.track + instance.stream = this.stream + + const setupWindow = () => { + instance.dom.style.display = 'none' + + const video = instance.dom.querySelector('video') + video.addEventListener( "loadedmetadata", function () { + let width = Math.round(window.innerWidth*0.4) + let factor = width / this.videoWidth + let height = Math.round(this.videoHeight * factor) + new WinBox("Casting Tab",{ + width, + height, + 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 + + // hint grabbable's obb-collider to track the window-object + instance.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0' + instance.components['obb-collider'].update() + }) + video.srcObject = instance.stream + video.play() + + this.createVideoTexture.apply(instance) + } + setTimeout( () => setupWindow(), 10 ) // give new components time to init + }, + + createVideoTexture: function(){ + console.dir(this) + const texture = new THREE.VideoTexture( video ); + texture.colorSpace = THREE.SRGBColorSpace; + const geometry = new THREE.PlaneGeometry( 16, 9 ); + geometry.scale( 0.2, 0.2, 0.2 ); + const material = new THREE.MeshBasicMaterial( { map: texture } ); + const mesh = new THREE.Mesh( geometry, material ); + mesh.lookAt( this.sceneEl.camera.position ); + this.object3D.add(mesh) + }, + + manifest: { // HTML5 manifest to identify app to xrsh + "short_name": "tab", + "name": "Browser Tab", + "icons": [ + { + "src": "https://css.gg/cast.svg", + "type": "image/svg+xml", + "sizes": "512x512" + } + ], + "id": "/?source=pwa", + "start_url": "/?source=pwa", + "background_color": "#3367D6", + "display": "standalone", + "scope": "/", + "theme_color": "#3367D6", + "category":"system", + "shortcuts": [ + { + "name": "What is the latest news?", + "cli":{ + "usage": "helloworld [options]", + "example": "helloworld news", + "args":{ + "--latest": {type:"string"} + } + }, + "short_name": "Today", + "description": "View weather information for today", + "url": "/today?source=pwa", + "icons": [{ "src": "/images/today.png", "sizes": "192x192" }] + } + ], + "description": "adds item to menu", + "screenshots": [ + { + "src": "/images/screenshot1.png", + "type": "image/png", + "sizes": "540x720", + "form_factor": "narrow" + } + ], + "help":` +Helloworld application + +This is a help file which describes the application. +It will be rendered thru troika text, and will contain +headers based on non-punctualized lines separated by linebreaks, +in above's case "\nHelloworld application\n" will qualify as header. + ` + } + +}); + diff --git a/com/dom.js b/com/dom.js index 093888d..cac678c 100644 --- a/com/dom.js +++ b/com/dom.js @@ -100,8 +100,8 @@ AFRAME.registerComponent('dom',{ }, addCSS: function(){ - if( this.dom.css && !document.head.querySelector(`style#${this.attrName}`) ){ - document.head.innerHTML += `` + if( this.dom.css && !document.head.querySelector(`style#${this.com.attrName}`) ){ + document.head.innerHTML += `` } return this }, diff --git a/com/example/helloworld-iframe.js b/com/example/helloworld-iframe.js index f72c2c5..29fad50 100644 --- a/com/example/helloworld-iframe.js +++ b/com/example/helloworld-iframe.js @@ -14,13 +14,26 @@ AFRAME.registerComponent('helloworld-iframe', { dom: { scale: 3, events: ['click','keydown'], - html: (me) => `
+ html: (me) => `
+ + Unfortunately,

+ The browser does not allow IFRAME rendering
+ in immersive mode (for security reasons).

+
`, css: (me) => ` - .XR #overlay .winbox.iframe{ visibility: visible; } /* don't hide in XR mode */ - .winbox.iframe iframe { background:#FFF; } + .XR #overlay .winbox.iframe{ + visibility: visible; + position:relative; + } /* don't hide in XR mode */ + .winbox.iframe > .wb-body { background:#FFF !important; + overflow-y: hidden; + overflow-x: hidden; + } + .winbox.iframe iframe { z-index:10; } + .winbox.iframe #warning { position:absolute; z-index:9; top:100px; left:20px; width:100%; height:50px; color:black; display:none; } ` }, @@ -40,6 +53,10 @@ AFRAME.registerComponent('helloworld-iframe', { let URL = this.data.url || prompt('enter URL to display','https://fabien.benetou.fr/Wiki/Wiki') if( !URL ) return + this.createWindow() + }, + + createWindow: function(){ let s = await AFRAME.utils.require(this.requires) // instance this component @@ -51,12 +68,16 @@ AFRAME.registerComponent('helloworld-iframe', { instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(1.39) ) instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera + this.el.sceneEl.addEventListener('3D', () => { + instance.dom.querySelector('#warning').style.display = 'block' // show warning + }) + const setupWindow = () => { const com = instance.components['helloworld-iframe'] instance.dom.style.display = 'none' - new WinBox("Hello World",{ - width: 250, - height: 150, + new WinBox( this.manifest.short_name+" "+URL,{ + width: Math.round(window.innerWidth*0.6), + height: Math.round(window.innerHeight*0.6), class:["iframe"], x:"center", y:"center", diff --git a/com/example/helloworld-window.js b/com/example/helloworld-window.js index ed48d76..1cba0fd 100644 --- a/com/example/helloworld-window.js +++ b/com/example/helloworld-window.js @@ -14,7 +14,7 @@ AFRAME.registerComponent('helloworld-window', { init: function(){ }, dom: { - scale: 3, + scale: 0.8, events: ['click','keydown'], html: (me) => `
${me.data.foo} ${me.data.myvalue} @@ -39,9 +39,11 @@ AFRAME.registerComponent('helloworld-window', { // instance this component const instance = this.el.cloneNode(false) this.el.sceneEl.appendChild( instance ) - instance.setAttribute("dom", "") + instance.setAttribute("dom", "") + instance.setAttribute("xd", "") // allows flipping between DOM/WebGL when toggling XD-button instance.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' ) - instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(1.39) ) + instance.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) ) + instance.setAttribute("grabbable","") instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera const setupWindow = () => { @@ -61,11 +63,15 @@ AFRAME.registerComponent('helloworld-window', { }); instance.dom.style.display = '' // show + // hint grabbable's obb-collider to track the window-object + instance.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0' + instance.components['obb-collider'].update() + // data2event demo - instance.setAttribute("data2event","") - com.data.myvalue = 1 - com.data.foo = `instance ${instance.uid}: ` - setInterval( () => com.data.myvalue++, 200 ) + //instance.setAttribute("data2event","") + //com.data.myvalue = 1 + //com.data.foo = `instance ${instance.uid}: ` + //setInterval( () => com.data.myvalue++, 500 ) } setTimeout( () => setupWindow(), 10 ) // give new components time to init diff --git a/com/example/helloworld.js b/com/example/helloworld.js index c253280..cad2f6f 100644 --- a/com/example/helloworld.js +++ b/com/example/helloworld.js @@ -11,6 +11,7 @@ AFRAME.registerComponent('helloworld', { await AFRAME.utils.require(this.dependencies) this.el.setAttribute("data2event","") + this.el.setAttribute("grabbable","") this.el.innerHTML = ` diff --git a/com/launcher.js b/com/launcher.js index ca012a3..9a52c25 100644 --- a/com/launcher.js +++ b/com/launcher.js @@ -234,6 +234,7 @@ AFRAME.registerComponent('launcher', { console.log("launcher.js: launching "+el.launchCom.el.getAttributeNames().shift()) launcher.preventAccidentalButtonPresses() el.launchCom.el.emit('launcher') // launch component! + this.data.open = false // close to prevent infinite loop of clicks when leaving immersive mode } }, @@ -360,6 +361,7 @@ AFRAME.registerSystem('launcher',{ return hasEvent ? el : null }) this.updateLauncher() + return seen }, updateLauncher: function(){ diff --git a/com/xrfragments.js b/com/xrfragments.js index 733a934..50c4ea8 100644 --- a/com/xrfragments.js +++ b/com/xrfragments.js @@ -3,8 +3,6 @@ AFRAME.registerComponent('xrfragments', { url: { type:"string"} }, - dependencies:['dom'], - init: function () { },