// this is a highlevel way of loading buildless 'apps' (a collection of js components)
AFRAME.required = {}
AFRAME.app = new Proxy({
add(component, entity){
this[component] = this[component] || []
this[component].push(entity)
},
foreach(cb){
for( let i in this ){
if( typeof this[i] != 'function' ) this[i].map( (app) => cb({app,component:i}) )
}
}
},{
get(me,k) { return me[k] },
set(me,k,v){ me[k] = v }
})
AFRAME.registerComponent('app', {
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) ) app.el.setAttribute(component,app.data)
})
}
},
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||'ready', packages) )
}
})
// 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){
return function(){
updateProperties.apply(this,arguments)
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})
}
})
if( !this.data ) return
// reactify components with dom-definition
if( this.data.uri && this.dom && !this.el.dom ){
tasks = {
ensureOverlay: () => {
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")
}
tasks.overlay = overlay
return tasks
},
createReactiveDOMElement: () => {
this.el.dom = document.createElement('div')
this.el.dom.className = this.parseAppURI(this.data.uri).component
this.el.dom.innerHTML = this.dom.html(this)
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
},
requireDependencies: () => {
this.require( this.requires )
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
}
}
tasks
.ensureOverlay()
.addCSS()
.createReactiveDOMElement()
.scaleDOMvsXR()
.requireDependencies()
.setupListeners()
tasks.overlay.appendChild(this.el.dom)
this.el.emit('DOMready',{el: this.el.dom})
}
// assign unique app id
if( !this.el.uid ) this.el.uid = '_'+String(Math.random()).substr(10)
}
}( AFRAME.AComponent.prototype.updateProperties)
document.head.innerHTML += `
`
// draw a button so we can toggle apps between 2D / XR
let toggle = (state) => {
state = state || document.body.className.match(/XR/)
document.body.classList[ state ? 'remove' : 'add'](['XR'])
AFRAME.scenes[0].emit( state ? 'apps:2D' : 'apps:XR')
}
document.addEventListener("DOMContentLoaded", (event) => {
let btn = document.createElement("button")
btn.id = "toggle_overlay"
btn.innerHTML = ""
btn.addEventListener('click', (e) => toggle() )
document.body.appendChild(btn)
document.querySelector('a-scene').addEventListener('enter-vr', () => toggle(true) )
document.querySelector('a-scene').addEventListener('exit-vr', () => toggle(false) )
document.querySelector('a-scene').addEventListener('loaded', () => {
let VRbtn = document.querySelector('a-scene .a-enter-vr')
let ARbtn = document.querySelector('a-scene .a-enter-ar')
if( VRbtn ) document.body.appendChild(VRbtn) // move to body
if( ARbtn ) document.body.appendChild(ARbtn) // so they will always be visible
})
// toggle immersive with ESCAPE
document.body.addEventListener('keydown', (e) => e.key == 'Escape' && toggle() )
})