Compare commits

..

No commits in common. "0c173ce21cf20e855b23a3e3b36c3e69928492ce" and "35e511812855e1dbc7adab4a60d9b703d7169b49" have entirely different histories.

4 changed files with 285 additions and 431 deletions

View File

@ -1,289 +0,0 @@
//
// this is just an AFRAME wrapper for golden-layout v2 (docs: https://golden-layout.github.io/golden-layout/)
//
//
AFRAME.registerComponent('apptiler', {
schema: {
},
requires:{
"goldenlayout_css1": "https://unpkg.com/golden-layout@2.6.0/dist/css/goldenlayout-base.css",
"goldenlayout_css2": "https://unpkg.com/golden-layout@2.6.0/dist/css/themes/goldenlayout-dark-theme.css"
},
dom: {
events: [],
// for stylesheet see bottom of file
html: (me) => `
<div id="windowmanager">
<div class="modals"></div>
</div>`,
},
events:{
DOMready: async function(){
await this.initLayout(this)
AFRAME.app.foreach( (opts) => {
this.add( opts.component, opts.app.el.dom)
if( opts.component != 'apptiler' ) opts.app.el.dom.querySelector('.modal').classList.add(['tile'])
})
setTimeout( () => document.querySelector('#overlay').classList.add(['apptiler']), 100 )
},
},
init: function () {
this.require( this.requires )
},
initLayout: async function(){
if( this.goldenLayout !== undefined || !this.el.dom.querySelector(".modals")) return console.warn("TODO: fix duplicate ready-events")
let { GoldenLayout } = await import("https://cdn.skypack.dev/pin/golden-layout@v2.5.0-dAz3xMzxIRpbnbfEAik0/mode=imports/optimized/golden-layout.js");
class Modal {
constructor(container) {
this.container = container;
this.rootElement = container.element;
this.rootElement.innerHTML = ''
this.resizeWithContainerAutomatically = true;
}
}
const myLayout = {
root: {
type: 'row',
content: []
}
};
this.goldenLayout = new GoldenLayout( this.el.dom.querySelector('.modals'));
this.goldenLayout.registerComponent('Modal', Modal);
this.goldenLayout.loadLayout(myLayout);
},
add: function(title,el){
if( title == 'apptiler' ) return // dont add yourself to yourself please
let item = this.goldenLayout.addComponent('Modal', undefined, title )
try{
item.parentItem.contentItems[ item.parentItem.contentItems.length-1 ].element.querySelector('.lm_content').appendChild(el)
}catch(e){} // ignore elements which are already appended
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "windowmanager",
"name": "Window Manager",
"icons": [
{
"src": "/images/icons-vector.svg",
"type": "image/svg+xml",
"sizes": "512x512"
}
],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [],
"description": "2D/3D management of windows",
"screenshots": [
{
"src": "/images/screenshot1.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
}
],
"help":`
Window Manager
The window manager manages all the windows in 2D/XR.
This is a core XRSH system application
`
}
});
document.head.innerHTML += `
<style type="text/css">
:root {
--xrsh-primary: #3aacff;
--xrsh-primary-fg: #FFF;
--xrsh-light-primary: #00a3Ff;
--xrsh-secondary: #872eff;
--xrsh-third: #ce7df2;
--xrsh-box-shadow: #0005;
--xrsh-dark-gray: #343334;
--xrsh-gray: #424280;
--xrsh-white: #fdfdfd;
--xrsh-light-gray: #efefef;
--xrsh-lighter-gray: #e4e2fb96;
--xrsh-modal: #000B;
--xrsh-font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
--xrsh-font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
--xrsh-font-size-0: 12px;
--xrsh-font-size-1: 14px;
--xrsh-font-size-2: 17px;
--xrsh-font-size-3: 21px;
--xrsh-modal-button-bg: #CCC;
--xrsh-modal-button-fg: #FFF;
}
body,
html.a-fullscreen body{
color: var(--xrsh-dark-gray);
font-size: var(--xrsh-font-size-1);
font-family: var(--xrsh-font-sans-serif);
accent-color: var(--xrsh-light-primary);
}
#overlay{
opacity:0;
}
#overlay.apptiler{
transition:1s;
opacity:1;
}
h1,h2,h3,h4,h5{
color: var(--xrsh-gray);
}
h1 { font-size: var(--xrsh-font-size-3); }
h2,h3,h4{ font-size: var(--xrsh-font-size-2); }
button,.btn,input[type=submit]{
border-radius:7px;
background: var(--xrsh-primary);
color: var(--xrsh-primary-fg);
transition:0.3s;
padding: 10px;
font-weight: bold;
border: none;
cursor:pointer;
}
button:hover,.btn:hover,input[type=submit]:hover{
filter: brightness(1.5);
}
.modal{
background: var(--xrsh-modal);
padding: 20px 20px 10px 20px;
border-radius: 15px;
display:inline-block;
position:relative;
z-index:50;
height:unset;
overflow:hidden;
margin-left:0px;
margin-top:-41px;
color: var(--xrsh-white);
}
.modal.tile{
}
.modal .top{
background: var(--xrsh-light-primary);
border-radius:15px;
position: absolute;
z-index:1;
left: 0;
right: 0;
top: 0;
height: 25px;
padding: 13px 10px 1px 14px;
color: #FFF;
}
/* remove this to see why this is a workaround for border-radius bug */
.modal .top .title{
position: absolute;
background: var(--xrsh-light-primary);
display: block;
left: 0;
right: 0;
height: 27px;
padding: 0px 0px 0px 20px;
font-weight: bold;
}
.modal .top button{
padding: 5px 7px;
background: var(--xrsh-modal-button-bg);
color: var(--xrsh-modal-button-fg);
font-weight: bold;
float:right;
}
.modal .top button:hover{
filter: brightness(1.1);
}
.modal .top button.close{
background: transparent;
display: block;
font-size: 20px;
padding: 0;
transform: translate(-4px,-3px);
margin-left: 6px;
}
legend{
font-size: var(--xrsh-font-size-0);
margin-bottom: 15px;
border-bottom: 1px solid var(--xrsh-light-primary);
}
fieldset{
border: none;
padding: 0;
margin: 0;
margin-bottom: 0px;
margin-bottom: 5px;
}
label{
margin-left:10px;
}
button,input,.btn{
margin-bottom:10px;
}
[type="checkbox"], [type="radio"]{
transform: scale(1.4);
margin-left:3px;
}
#windowmanager{
display: flex;
height: 100%;
width: 100%;
position:fixed;
top:0;
left:0;
bottom:0;
right:0;
}
.modals{
flex: 0 0 auto;
height:100%;
width:100%;
margin-right: 3px;
}
#windowmanager .lm_content{
box-sizing:content-box;
border:1px solid #444;
background:transparent;
}
#windowmanager .lm_goldenlayout{
background: var(--xrsh-modal);
}
#windowmanager .lm_title{
font-family: var(--xrsh-font-sans-serif);
font-size: var(--xrsh-font-size-1);
font-weight:bold;
}
</style>
`

View File

@ -3,18 +3,20 @@ AFRAME.registerComponent('helloworld', {
foo: { type:"string"}
},
requires:{
dependencies:{
"html": "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
"stylis": "https://unpkg.com/stylis@4.3.1/dist/umd/stylis.js" // modern CSS (https://stylis.js.org)
},
dom: {
scale: 3,
scale: 3.5,
events: ['click','input'],
html: (me) => `
<div id="${me.el.uid}" class="modal hello">
<div class="top">
<div class="title">Hello world</div>
<button class="close"></button>
<button class="fold">_</button>
</div>
<br><br>
<fieldset>
@ -33,44 +35,29 @@ AFRAME.registerComponent('helloworld', {
<button>hello</button>
</div>`,
css: `.modal.hello {
position:relative;
top:0;
width:200px;
.title { font-weight:bold; } /* modern nested buildless css thanks to stylis */
position:relative;top:0;width:200px
foo { /* modern css supported via stylis */ }
}`
},
events:{
html: function( ){ console.log("html-mesh requirement mounted") },
stylis: function( ){ console.log("stylis requirement mounted") },
DOMready: function(e){
// our reactive dom element has been added to the dom (DOMElement = this.el.dom)
},
html: function( ){ console.log("htmlmesh component mounted!") }, // html-component was added to this AFRAME entity
click: function(e){ // a click was detected on this.el.dom or AFRAME entity
let el = e.detail.target || e.detail.srcElement
if( !el ) return
if( el.className.match("fold") ) this.el.toggleFold()
if( el.className.match("close") ) this.el.close()
},
input: function(e){
if( !e.detail.target ) return
if( e.detail.target.id == 'myRange' ) this.data.value = e.detail.target.value // reactive demonstration
},
value: function(e){ this.el.dom.querySelector("#value").innerHTML = e.detail.v }, // auto-emitted when this.data.value gets updated
value: function(e){ this.el.dom.querySelector("#value").innerHTML = e.detail.v }, // this.data.title was changed
},
init: function () {
this.require( this.requires )
this.scene.addEventListener('apps:2D', () => this.el.setAttribute('visible', false) )
this.scene.addEventListener('apps:XR', () => {
this.el.setAttribute('visible', true)
this.require( this.dependencies )
.then( () => {
this.el.setAttribute("html",`html:#${this.el.uid}; cursor:#cursor`)
})
},

102
app/windowmanager.js Normal file
View File

@ -0,0 +1,102 @@
//
// this is just an AFRAME wrapper for https://golden-layout.com
//
//
AFRAME.registerComponent('windowmanager', {
schema: {
},
dependencies:{
"cash": "https://cdn.jsdelivr.net/npm/cash-dom/dist/cash.min.js", // tiny jquery replacement
"goldenlayout": "https://golden-layout.com/files/latest/js/goldenlayout.min.js",
"goldenlayout_css1": "https://golden-layout.com/files/latest/css/goldenlayout-base.css",
"goldenlayout_css2": "https://golden-layout.com/files/latest/css/goldenlayout-dark-theme.css"
},
events:{
ready: function(){
//this.initLayout()
}
},
init: function () {
this.require( this.dependencies )
},
initLayout: async function(){
// const goldenlayout_module = "https://cdn.skypack.dev/pin/golden-layout@v2.5.0-dAz3xMzxIRpbnbfEAik0/mode=imports/optimized/golden-layout.js";
// const { ComponentContainer, ComponentItemConfig, GoldenLayout, ItemType } = await import(goldenlayout_module)
this.el.layout = document.createElement('div')
document.querySelector("#overlay").appendChild(this.el.layout)
class MyComponent {
constructor(container) {
this.container = container;
this.rootElement = container.element;
this.rootElement.innerHTML = '<h2>' + 'Component Type: MyComponent' + '</h2>';
this.resizeWithContainerAutomatically = true;
}
}
const myLayout = {
content: [
{
title: 'Terminal 1',
type: 'component',
componentType: 'MyComponent',
width: 50,
},
{
title: 'My Component 2',
type: 'component',
componentType: 'MyComponent',
// componentState: { text: 'Component 2' }
}
]
};
const goldenLayout = new GoldenLayout(this.el.layout);
goldenLayout.registerComponent( 'MyComponent', MyComponent);
goldenLayout.loadLayout(myLayout);
goldenLayout.addComponent('MyComponent', undefined, 'Added Component');
},
manifest: { // HTML5 manifest to identify app to xrsh
"short_name": "windowmanager",
"name": "Window Manager",
"icons": [
{
"src": "/images/icons-vector.svg",
"type": "image/svg+xml",
"sizes": "512x512"
}
],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"scope": "/",
"theme_color": "#3367D6",
"shortcuts": [],
"description": "2D/3D management of windows",
"screenshots": [
{
"src": "/images/screenshot1.png",
"type": "image/png",
"sizes": "540x720",
"form_factor": "narrow"
}
],
"help":`
Window Manager
The window manager manages all the windows in 2D/XR.
This is a core XRSH system application
`
}
});

View File

@ -1,87 +1,41 @@
// 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')
let id = this.data.uri.split("/").pop() // 'a/b/c/mycom.js' => 'mycom.js'
let component = id.split(".js").shift() // 'mycom.js' => 'mycom'
let mount = () => {
let entity = document.createElement("a-entity")
this.el.setAttribute(component,this.data)
}
if( AFRAME.components[component] || document.head.querySelector(`script#${id}`) ) mount()
else this.require([ this.data.uri ]).then( mount ).catch(console.error)
},
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){
require: AFRAME.AComponent.prototype.require = function(packages){
let deps = []
if( !packages.map ) packages = Object.values(packages)
packages.map( (package) => {
let id = package.split("/").pop()
// prevent duplicate requests
if( AFRAME.required[id] ) return
AFRAME.required[id] = true
if( !document.head.querySelector(`script#${id}`) ){
let {id,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;
}
let script = document.createElement("script")
script.id = id
script.src = package
script.onload = () => resolve()
script.onerror = (e) => reject(e)
document.head.appendChild(script)
})
deps.push(p)
}
})
Promise.all(deps).then( () => this.el.emit( readyEvent||'ready', packages) )
return Promise.all(deps)
}
})
@ -97,21 +51,29 @@ AFRAME.registerComponent('app', {
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)
// monkeypatching updateProperties will detect a component config like:
// dom: {
// html: `<h1>hello</h1>`,
// css: `#hello {color:red}`,
// events: ['click']
// }
// and use it to create a reactive DOM-component (using native javascript Proxy)
// which delegates all related DOM-events AND data-changes back to the AFRAME component
AFRAME.AComponent.prototype.updateProperties = function(updateProperties){
return function(){
updateProperties.apply(this,arguments)
if( this.dom && this.data && this.data.uri ){
if( this.dom && this.data){
tasks = {
generateUniqueId: () => {
this.el.uid = String(Math.random()).substr(10)
this.el.uid = String(Math.random()).substr(2)
return tasks
},
@ -119,7 +81,7 @@ AFRAME.AComponent.prototype.updateProperties = function(updateProperties){
let overlay = document.querySelector('#overlay')
if( !overlay ){
overlay = document.createElement('div')
overlay.id = "overlay"
overlay.id = "#overlay"
document.body.appendChild(overlay)
document.querySelector("a-scene").setAttribute("webxr","overlayElement:#overlay")
}
@ -136,7 +98,6 @@ AFRAME.AComponent.prototype.updateProperties = function(updateProperties){
}
})
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) ) )
@ -160,8 +121,12 @@ AFRAME.AComponent.prototype.updateProperties = function(updateProperties){
this.el.dom.remove()
this.el.removeAttribute("html")
}
this.el.toggleFold = () => {
this.el.dom.querySelector(".modal").classList.toggle('fold')
this.el.dom.querySelector('.top .fold').innerText = this.el.dom.querySelector('.modal').className.match(/fold/) ? '▢' : '_'
}
return tasks
}
},
}
tasks
@ -171,12 +136,12 @@ AFRAME.AComponent.prototype.updateProperties = function(updateProperties){
.createReactiveDOMElement()
.scaleDOMvsXR()
.addModalFunctions()
// finally lets add the bad boy to the DOM
tasks.overlay.appendChild(this.el.dom)
this.el.emit('DOMready',{el: this.el.dom})
}
}
}( AFRAME.AComponent.prototype.updateProperties)
//
// base CSS for XRSH apps
//
@ -184,12 +149,33 @@ AFRAME.AComponent.prototype.updateProperties = function(updateProperties){
// * no icon libraries (favicon e.g.)
// * 'border-radius: 2px 3px 4px 5px' (applies 2px to all corners)
//
document.head.innerHTML += `
<style type="text/css">
/* CSS reset */
html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:0.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type="button"],[type="reset"],[type="submit"],button{-webkit-appearance:button}[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}
:root {
--xrsh-primary: #3aacff;
--xrsh-primary-fg: #FFF;
--xrsh-light-primary: #00a3Ff;
--xrsh-secondary: #872eff;
--xrsh-third: #ce7df2;
--xrsh-box-shadow: #0005;
--xrsh-dark-gray: #343334;
--xrsh-gray: #424280;
--xrsh-white: #fdfdfd;
--xrsh-light-gray: #efefef;
--xrsh-lighter-gray: #e4e2fb96;
--xrsh-font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
--xrsh-font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
--xrsh-font-size-0: 12px;
--xrsh-font-size-1: 14px;
--xrsh-font-size-2: 17px;
--xrsh-font-size-3: 21px;
--xrsh-modal-button-bg: #CCC;
--xrsh-modal-button-fg: #FFF;
}
a-scene{
position:fixed;
top:0;
@ -201,65 +187,133 @@ document.head.innerHTML += `
z-index:10;
}
body,
html.a-fullscreen body{
color: var(--xrsh-dark-gray);
font-size: var(--xrsh-font-size-1);
font-family: var(--xrsh-font-sans-serif);
padding:15px;
accent-color: var(--xrsh-light-primary);
}
#overlay{
display: flex; /* tile modals */
z-index:10;
}
#overlay.hide{
z-index:-10;
}
#toggle_overlay{
position: fixed;
right: 20px;
bottom: 73px;
width: 58px;
text-align: center;
height: 40px;
padding: 0;
z-index: 100;
border: 3px solid #3aacff;
border-radius:11px;
h1,h2,h3,h4,h5{
color: var(--xrsh-gray);
}
h1 { font-size: var(--xrsh-font-size-3); }
h2,h3,h4{ font-size: var(--xrsh-font-size-2); }
button,.btn,input[type=submit]{
border-radius:7px;
background: var(--xrsh-primary);
color: var(--xrsh-primary-fg);
transition:0.3s;
padding: 0px;
padding: 10px;
font-weight: bold;
border: none;
cursor:pointer;
font-family:sans-serif;
font-size:15px;
color: #FFF;
background: #3aacff;
transition:0.5s;
}
.XR #toggle_overlay{
background: transparent;
color: #3aacff;
button:hover,.btn:hover,input[type=submit]:hover{
filter: brightness(1.5);
}
.XR #overlay{
visibility: hidden;
.modal{
background: #f0f0f0;
padding: 20px 20px 10px 20px;
border-radius: 15px;
display: inline-block;
position:relative;
z-index:50;
height:unset;
overflow:hidden;
margin-left:10px;
}
.modal.fold {
height:22px;
overflow:hidden;
}
.modal .top{
background: var(--xrsh-light-primary);
border-radius:15px;
position: absolute;
z-index:1;
left: 0;
right: 0;
top: 0;
height: 25px;
padding: 13px 10px 1px 14px;
color: #FFF;
}
/* remove this to see why this is a workaround for border-radius bug */
.modal .top .title{
position: absolute;
background: var(--xrsh-light-primary);
display: block;
left: 0;
right: 0;
height: 27px;
padding: 0px 0px 0px 20px;
font-weight: bold;
}
.modal .top button{
padding: 5px 7px;
background: var(--xrsh-modal-button-bg);
color: var(--xrsh-modal-button-fg);
font-weight: bold;
float:right;
}
.modal .top button:hover{
filter: brightness(1.1);
}
.modal .top button.close{
background: transparent;
display: block;
font-size: 20px;
padding: 0;
transform: translate(-4px,-3px) scale(1.5,1);
margin-left: 6px;
}
.modal .top button.fold{
background: transparent;
border: none;
margin: 0;
margin-left: 30px;
transform: scale(1.2) translate(0px,-6px);
}
legend{
font-size: var(--xrsh-font-size-0);
margin-bottom: 15px;
border-bottom: 1px solid var(--xrsh-light-primary);
}
fieldset{
border: none;
padding: 0;
margin: 0;
margin-bottom: 0px;
margin-bottom: 5px;
}
label{
margin-left:10px;
}
button,input,.btn{
margin-bottom:10px;
}
[type="checkbox"], [type="radio"]{
transform: scale(1.4);
margin-left:3px;
}
}
</style>
`
// 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.innerText = 'XRSH'
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
})
})