launcher/handmenu demo
This commit is contained in:
parent
46bb9afe6d
commit
987828b233
|
@ -19,7 +19,7 @@ BEGIN{
|
|||
print ""
|
||||
}
|
||||
|
||||
/\$\(/ { cmd=$0;
|
||||
/\$(?![\(|"])/ { cmd=$0;
|
||||
gsub(/^.*\$\(/,"",cmd);
|
||||
gsub(/\).*/,"",cmd);
|
||||
cmd | getline stdout; close(cmd);
|
||||
|
|
80
README.md
80
README.md
|
@ -101,7 +101,7 @@ a Linux ISO image (via WASM).
|
|||
| `height` | `number` | 600 ||
|
||||
| `depth` | `number` | 0.03 ||
|
||||
| `lineHeight` | `number` | 18 ||
|
||||
| `prompt` | `boolean` | true | boot straight into ISO or give user choice |
|
||||
| `bootmenu` | `boolean` | true | give user choice [or boot straight into ISO ] |
|
||||
| `padding` | `number`` | 18 | |
|
||||
| `maximized` | `boolean` | false | |
|
||||
| `minimized` | `boolean` | false | |
|
||||
|
@ -147,6 +147,54 @@ NOTE: For convenience reasons, events are forwarded between com/isoterminal.js,
|
|||
```
|
||||
|
||||
|
||||
## [launcher](com/launcher.js)
|
||||
|
||||
displays app (icons) for enduser to launch
|
||||
|
||||
```javascript
|
||||
<a-scene launcher/>
|
||||
```
|
||||
|
||||
| property | type | example |
|
||||
|--------------|--------------------|----------------------------------------------------------------------------------------|
|
||||
| `registries` | `array` of strings | `<a-entity launcher="registers: https://foo.com/index.json, ./index.json"/>` |
|
||||
|
||||
| event | target | info |
|
||||
|--------------|-------------------------------------------------------------------------------------------------------------|
|
||||
| `launcher` | an app | when pressing an app icon, `launcher` event will be send to the respective app |
|
||||
|
||||
There a multiple ways of letting the launcher know that an app can be launched:
|
||||
|
||||
1. any AFRAME component with an `launcher`-event + manifest is automatically added:
|
||||
|
||||
```javascript
|
||||
AFRAME.registerComponent('foo',{
|
||||
events:{
|
||||
launcher: function(){ ...launch something... }
|
||||
},
|
||||
manifest:{ // HTML5 manifesto JSON object
|
||||
// https://www.w3.org/TR/appmanifest/
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. dynamically in javascript
|
||||
|
||||
```javascript
|
||||
window.launcher.register({
|
||||
name:"foo",
|
||||
icon: "https://.../optional_icon.png"
|
||||
description: "lorem ipsum",
|
||||
cb: () => alert("foo")
|
||||
})
|
||||
//window.launcher.unregister('foo')
|
||||
```
|
||||
|
||||
|
||||
|
||||
Restore the pose.
|
||||
|
||||
|
||||
## [pastedrop](com/pastedrop.js)
|
||||
|
||||
detects user copy/paste and file dragdrop action
|
||||
|
@ -161,9 +209,12 @@ and clipboard functions
|
|||
| `pasteFile` | self | always translates input to a File object |
|
||||
|
||||
|
||||
Restore the pose.
|
||||
|
||||
|
||||
## [require](com/require('').js)
|
||||
|
||||
automatically requires dependencies
|
||||
automatically requires dependencies or missing components
|
||||
|
||||
```javascript
|
||||
await AFRAME.utils.require( this.dependencies ) (*) autoload missing components
|
||||
|
@ -173,3 +224,28 @@ await AFRAME.utils.require(["./app/foo.js","foo.css"],this)
|
|||
```
|
||||
|
||||
> (*) = prefixes baseURL AFRAME.utils.require.baseURL ('./com/' e.g.)
|
||||
|
||||
|
||||
## [window](com/window.js)
|
||||
|
||||
wraps a draggable window around a dom id or [dom](com/dom.js) component.
|
||||
|
||||
```html
|
||||
<a-entity window="dom: #mydiv"/>
|
||||
```
|
||||
|
||||
> depends on [AFRAME.utils.require](com/require.js)
|
||||
|
||||
| property | type | default | info |
|
||||
|------------------|-----------|------------------------|------|
|
||||
| `title` |`string` | "" | |
|
||||
| `width` |`string` | | |
|
||||
| `height` |`string` | 260px | |
|
||||
| `uid` |`string` | | |
|
||||
| `attach` |`selector` | | |
|
||||
| `dom` |`selector` | | |
|
||||
| `max` |`boolean` | false | |
|
||||
| `min` |`boolean` | false | |
|
||||
| `x` |`string` | "center" | |
|
||||
| `y` |`string` | "center" | |
|
||||
| `class` |`array` | [] | |
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
window.AFRAME.registerComponent('xrf-wear', {
|
||||
schema:{
|
||||
el: {type:"selector"},
|
||||
position: {type:"vec3"},
|
||||
rotation: {type:"vec3"}
|
||||
},
|
||||
init: function(){
|
||||
$('a-scene').addEventListener('enter-vr', (e) => this.wear(e) )
|
||||
$('a-scene').addEventListener('exit-vr', (e) => this.unwear(e) )
|
||||
},
|
||||
wear: function(){
|
||||
if( !this.wearable ){
|
||||
let d = this.data
|
||||
this.wearable = new THREE.Group()
|
||||
this.el.object3D.children.map( (c) => this.wearable.add(c) )
|
||||
this.wearable.position.set( d.position.x, d.position.y, d.position.z)
|
||||
this.wearable.rotation.set( d.rotation.x, d.rotation.y, d.rotation.z)
|
||||
}
|
||||
this.data.el.object3D.add(this.wearable)
|
||||
},
|
||||
unwear: function(){
|
||||
this.data.el.remove(this.wearable)
|
||||
this.wearable.children.map( (c) => this.el.object3D.add(c) )
|
||||
delete this.wearable
|
||||
}
|
||||
})
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
*Usage:
|
||||
* <a-entity controlattach="el: #leftHand; class: foo"></a-entity>
|
||||
* <a-entity id="leftHand">
|
||||
* <a-entity class="foo" position="0 0 0" rotation="0 0 0" scale="0 0 0"></a-entity>
|
||||
* <a-entity>
|
||||
*
|
||||
* NOTE: you can hint different position/rotation-offsets by adding controller-specific entities to the target:
|
||||
*
|
||||
* <a-entity id="leftHand">
|
||||
* <a-entity class="foo" position="0 0 0" rotation="0 0 0" scale="0 0 0"></a-entity>
|
||||
* <a-entity class="foo hand-tracking-controls" position="0 0 0" rotation="0 0 0" scale="0 0 0"></a-entity>
|
||||
* <a-entity>
|
||||
*
|
||||
* The controllernames are hinted by the 'controllermodelready' event:
|
||||
*
|
||||
* 'hand-tracking-controls',
|
||||
* 'meta-touch-controls',
|
||||
* 'valve-index-controls',
|
||||
* 'logitech-mx-ink-controls',
|
||||
* 'windows-motion-controls',
|
||||
* 'hp-mixed-reality-controls',
|
||||
* 'generic-tracked-controller-controls',
|
||||
* 'pico-controls',
|
||||
* (and more in the future)
|
||||
*/
|
||||
|
||||
AFRAME.registerComponent('controlattach', {
|
||||
schema:{
|
||||
el: {type:"selector"},
|
||||
class: {type:"string"}
|
||||
},
|
||||
init: function(){
|
||||
if( !this.data.el ) return console.warn(`controlattach.js: cannot find ${this.data.el}`)
|
||||
this.controllers = {}
|
||||
this.remember()
|
||||
this.data.el.addEventListener('controllermodelready', this.bindPlaceHolders.bind(this,["controllermodelready"]) )
|
||||
this.data.el.addEventListener('controllerconnected', this.bindPlaceHolders.bind(this,["controllerconnected"] ) )
|
||||
},
|
||||
|
||||
bindPlaceHolders: function(type,e){
|
||||
let controllerName = e.detail.name
|
||||
let selector = `.${this.data.class}.${controllerName}`
|
||||
let placeholder = this.data.el.querySelector(selector)
|
||||
if( !placeholder ){
|
||||
placeholder = this.data.el.querySelector(`.${this.data.class}`)
|
||||
console.warn(`controlattach.js: '${selector}' not found, fallback to default`)
|
||||
}
|
||||
if( !placeholder ) return console.warn("controlattach.js: could not find placeholder to attach to")
|
||||
this.attachPlaceHolder(type,e,placeholder, controllerName)
|
||||
},
|
||||
|
||||
attachPlaceHolder: function(type, el, placeholder, controllerName){
|
||||
this.el.object3DMap = {} // unsync THREE <-> AFRAME entity
|
||||
// these are handled by the placeholder entity
|
||||
this.obj.position.set(0,0,0);
|
||||
this.obj.rotation.set(0,0,0);
|
||||
this.obj.scale.set(1,1,1);
|
||||
|
||||
if( controllerName != 'hand-tracking-controls' ){
|
||||
// re-add for controller-models which don't re-add children ('meta-touch-controls' e.g.)
|
||||
if( this.data.el.getObject3D("mesh") ){
|
||||
this.data.el.getObject3D("mesh").add(placeholder.object3D)
|
||||
}
|
||||
}
|
||||
placeholder.object3D.add( this.obj )
|
||||
},
|
||||
|
||||
detach: function(){
|
||||
this.el.setObject3D( this.obj.uuid, this.obj )
|
||||
this.el.object3D.position.copy( this.position )
|
||||
this.el.object3D.rotation.copy( this.rotation )
|
||||
this.el.object3D.scale.copy( this.scale )
|
||||
},
|
||||
|
||||
remember: function(){
|
||||
this.obj = this.el.object3D
|
||||
this.position = this.el.object3D.position.clone()
|
||||
this.rotation = this.el.object3D.rotation.clone()
|
||||
this.scale = this.el.object3D.scale.clone()
|
||||
this.parent = this.el.object3D.parent
|
||||
}
|
||||
|
||||
})
|
||||
|
|
@ -94,8 +94,12 @@ if( !AFRAME.components.dom ){
|
|||
this.el.dom.innerHTML = this.dom.html(this)
|
||||
this.el.dom.className = this.dom.attrName
|
||||
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]
|
||||
if( this.dom.events ){
|
||||
this.dom.events.map( (e) => {
|
||||
this.el.dom.addEventListener(e, (ev) => this.el.emit(e,ev) )
|
||||
})
|
||||
}
|
||||
return this
|
||||
},
|
||||
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
AFRAME.registerComponent('helloworld-html', {
|
||||
schema: {
|
||||
foo: { type:"string"}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
|
||||
this.el.addEventListener('ready', () => this.el.dom.style.display = 'none' )
|
||||
},
|
||||
|
||||
requires:{
|
||||
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
|
||||
},
|
||||
|
||||
dom: {
|
||||
scale: 1,
|
||||
events: ['click'],
|
||||
html: (me) => `<div>
|
||||
<div class="pad"> helloworld-html: ${me.data.foo} <b>${me.data.myvalue}</b></span>
|
||||
</div>`,
|
||||
|
||||
css: (me) => `.helloworld-html {
|
||||
color: var(--xrsh-light-gray); /* see index.css */
|
||||
}`,
|
||||
},
|
||||
|
||||
events:{
|
||||
|
||||
// combined AFRAME+DOM reactive events
|
||||
keydown: function(e){ }, //
|
||||
click: function(e){
|
||||
console.dir(e)
|
||||
}, //
|
||||
|
||||
// reactive events for this.data updates
|
||||
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
|
||||
|
||||
|
||||
ready: function( ){
|
||||
this.el.dom.style.display = 'none'
|
||||
console.log("this.el.dom has been added to DOM")
|
||||
this.el.dom.children[0].id = this.el.uid // important hint for html-mesh
|
||||
this.data.myvalue = 1
|
||||
setInterval( () => this.data.myvalue++, 100 )
|
||||
},
|
||||
|
||||
launcher: function(){
|
||||
this.el.dom.style.display = ''
|
||||
console.log("launcher")
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
manifest: { // HTML5 manifest to identify app to xrsh
|
||||
"short_name": "Hello world",
|
||||
"name": "Hello world",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://css.gg/browser.svg",
|
||||
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNCA4QzQuNTUyMjggOCA1IDcuNTUyMjggNSA3QzUgNi40NDc3MiA0LjU1MjI4IDYgNCA2QzMuNDQ3NzIgNiAzIDYuNDQ3NzIgMyA3QzMgNy41NTIyOCAzLjQ0NzcyIDggNCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNOCA3QzggNy41NTIyOCA3LjU1MjI4IDggNyA4QzYuNDQ3NzIgOCA2IDcuNTUyMjggNiA3QzYgNi40NDc3MiA2LjQ0NzcyIDYgNyA2QzcuNTUyMjggNiA4IDYuNDQ3NzIgOCA3WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNMTAgOEMxMC41NTIzIDggMTEgNy41NTIyOCAxMSA3QzExIDYuNDQ3NzIgMTAuNTUyMyA2IDEwIDZDOS40NDc3MSA2IDkgNi40NDc3MiA5IDdDOSA3LjU1MjI4IDkuNDQ3NzEgOCAxMCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMyAzQzEuMzQzMTUgMyAwIDQuMzQzMTUgMCA2VjE4QzAgMTkuNjU2OSAxLjM0MzE1IDIxIDMgMjFIMjFDMjIuNjU2OSAyMSAyNCAxOS42NTY5IDI0IDE4VjZDMjQgNC4zNDMxNSAyMi42NTY5IDMgMjEgM0gzWk0yMSA1SDNDMi40NDc3MiA1IDIgNS40NDc3MiAyIDZWOUgyMlY2QzIyIDUuNDQ3NzIgMjEuNTUyMyA1IDIxIDVaTTIgMThWMTFIMjJWMThDMjIgMTguNTUyMyAyMS41NTIzIDE5IDIxIDE5SDNDMi40NDc3MiAxOSAyIDE4LjU1MjMgMiAxOFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"id": "/?source=pwa",
|
||||
"start_url": "/?source=pwa",
|
||||
"background_color": "#3367D6",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#3367D6",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "What is the latest news?",
|
||||
"cli":{
|
||||
"usage": "helloworld <type> [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": "Hello world information",
|
||||
"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.
|
||||
`
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
AFRAME.registerComponent('helloworld-iframe', {
|
||||
schema: {
|
||||
url: { type:"string"}
|
||||
},
|
||||
|
||||
init: function(){},
|
||||
|
||||
requires:{
|
||||
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: 3,
|
||||
events: ['click','keydown'],
|
||||
html: (me) => `<div class="relative">
|
||||
<span id="warning">
|
||||
<b>Unfortunately,</b><br><br>
|
||||
The browser does not allow IFRAME rendering<br>
|
||||
in immersive mode (for security reasons).<br><br>
|
||||
</span>
|
||||
<iframe src=""></iframe>
|
||||
</div>`,
|
||||
|
||||
css: (me) => `
|
||||
.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; }
|
||||
`
|
||||
},
|
||||
|
||||
events:{
|
||||
|
||||
// combined AFRAME+DOM reactive events
|
||||
click: function(e){ }, //
|
||||
keydown: function(e){ }, //
|
||||
|
||||
// reactive updates (data2event.js)
|
||||
url: function(e){
|
||||
this.el.dom.querySelector('iframe').src = this.data.url
|
||||
console.dir(this.el.dom.querySelector('iframe'))
|
||||
},
|
||||
|
||||
launcher: async function(){
|
||||
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
|
||||
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
|
||||
|
||||
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( 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",
|
||||
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("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
|
||||
instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
|
||||
instance.setAttribute("show-texture-in-xr","") // only show aframe-html texture in xr mode
|
||||
}
|
||||
});
|
||||
instance.dom.style.display = ''
|
||||
}
|
||||
|
||||
setTimeout( () => setupWindow(), 10 ) // give new components time to init
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
manifest: { // HTML5 manifest to identify app to xrsh
|
||||
"short_name": "Iframe",
|
||||
"name": "Hello world IFRAME window",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://css.gg/browse.svg",
|
||||
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNCA4QzQuNTUyMjggOCA1IDcuNTUyMjggNSA3QzUgNi40NDc3MiA0LjU1MjI4IDYgNCA2QzMuNDQ3NzIgNiAzIDYuNDQ3NzIgMyA3QzMgNy41NTIyOCAzLjQ0NzcyIDggNCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNOCA3QzggNy41NTIyOCA3LjU1MjI4IDggNyA4QzYuNDQ3NzIgOCA2IDcuNTUyMjggNiA3QzYgNi40NDc3MiA2LjQ0NzcyIDYgNyA2QzcuNTUyMjggNiA4IDYuNDQ3NzIgOCA3WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNMTAgOEMxMC41NTIzIDggMTEgNy41NTIyOCAxMSA3QzExIDYuNDQ3NzIgMTAuNTUyMyA2IDEwIDZDOS40NDc3MSA2IDkgNi40NDc3MiA5IDdDOSA3LjU1MjI4IDkuNDQ3NzEgOCAxMCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMyAzQzEuMzQzMTUgMyAwIDQuMzQzMTUgMCA2VjE4QzAgMTkuNjU2OSAxLjM0MzE1IDIxIDMgMjFIMjFDMjIuNjU2OSAyMSAyNCAxOS42NTY5IDI0IDE4VjZDMjQgNC4zNDMxNSAyMi42NTY5IDMgMjEgM0gzWk0yMSA1SDNDMi40NDc3MiA1IDIgNS40NDc3MiAyIDZWOUgyMlY2QzIyIDUuNDQ3NzIgMjEuNTUyMyA1IDIxIDVaTTIgMThWMTFIMjJWMThDMjIgMTguNTUyMyAyMS41NTIzIDE5IDIxIDE5SDNDMi40NDc3MiAxOSAyIDE4LjU1MjMgMiAxOFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"id": "/?source=pwa",
|
||||
"start_url": "/?source=pwa",
|
||||
"background_color": "#3367D6",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#3367D6",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "What is the latest news?",
|
||||
"cli":{
|
||||
"usage": "helloworld <type> [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": "Hello world information",
|
||||
"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.
|
||||
`
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
AFRAME.registerComponent('helloworld-window', {
|
||||
schema: {
|
||||
foo: { type:"string", "default":"foo"}
|
||||
},
|
||||
|
||||
requires: {
|
||||
dom: "./com/dom.js", // interpret .dom object
|
||||
xrtexture: "./com/html-as-texture-in-xr.js", // allow switching between 3D/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: 0.8,
|
||||
events: ['click','keydown'],
|
||||
html: (me) => `<div>
|
||||
<div class="pad"> <span>${me.data.foo}</span> <b>${me.data.myvalue}</b></span>
|
||||
</div>`,
|
||||
|
||||
css: (me) => `.helloworld-window div.pad { padding:11px; }`
|
||||
},
|
||||
|
||||
events:{
|
||||
|
||||
// combined AFRAME+DOM reactive events
|
||||
click: function(e){ }, //
|
||||
keydown: function(e){ }, //
|
||||
|
||||
// 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(this.requires)
|
||||
|
||||
// instance this component
|
||||
const instance = this.el.cloneNode(false)
|
||||
this.el.sceneEl.appendChild( instance )
|
||||
instance.setAttribute("dom", "")
|
||||
instance.setAttribute("grabbable","")
|
||||
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("show-texture-in-xr",`domid: #${instance.uid}`) // only show aframe-html texture in xr mode
|
||||
}
|
||||
});
|
||||
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++, 500 )
|
||||
}
|
||||
|
||||
setTimeout( () => setupWindow(), 10 ) // give new components time to init
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
manifest: { // HTML5 manifest to identify app to xrsh
|
||||
"short_name": "window",
|
||||
"name": "Hello world window",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://css.gg/browser.svg",
|
||||
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBkPSJNNCA4QzQuNTUyMjggOCA1IDcuNTUyMjggNSA3QzUgNi40NDc3MiA0LjU1MjI4IDYgNCA2QzMuNDQ3NzIgNiAzIDYuNDQ3NzIgMyA3QzMgNy41NTIyOCAzLjQ0NzcyIDggNCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNOCA3QzggNy41NTIyOCA3LjU1MjI4IDggNyA4QzYuNDQ3NzIgOCA2IDcuNTUyMjggNiA3QzYgNi40NDc3MiA2LjQ0NzcyIDYgNyA2QzcuNTUyMjggNiA4IDYuNDQ3NzIgOCA3WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBkPSJNMTAgOEMxMC41NTIzIDggMTEgNy41NTIyOCAxMSA3QzExIDYuNDQ3NzIgMTAuNTUyMyA2IDEwIDZDOS40NDc3MSA2IDkgNi40NDc3MiA5IDdDOSA3LjU1MjI4IDkuNDQ3NzEgOCAxMCA4WiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMyAzQzEuMzQzMTUgMyAwIDQuMzQzMTUgMCA2VjE4QzAgMTkuNjU2OSAxLjM0MzE1IDIxIDMgMjFIMjFDMjIuNjU2OSAyMSAyNCAxOS42NTY5IDI0IDE4VjZDMjQgNC4zNDMxNSAyMi42NTY5IDMgMjEgM0gzWk0yMSA1SDNDMi40NDc3MiA1IDIgNS40NDc3MiAyIDZWOUgyMlY2QzIyIDUuNDQ3NzIgMjEuNTUyMyA1IDIxIDVaTTIgMThWMTFIMjJWMThDMjIgMTguNTUyMyAyMS41NTIzIDE5IDIxIDE5SDNDMi40NDc3MiAxOSAyIDE4LjU1MjMgMiAxOFoiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"id": "/?source=pwa",
|
||||
"start_url": "/?source=pwa",
|
||||
"background_color": "#3367D6",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#3367D6",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "What is the latest news?",
|
||||
"cli":{
|
||||
"usage": "helloworld <type> [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": "Hello world information",
|
||||
"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.
|
||||
`
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
AFRAME.registerComponent('helloworld', {
|
||||
schema: {
|
||||
wireframe: { type:"boolean", "default":false},
|
||||
text: {type:"string","default":"hello world"}
|
||||
},
|
||||
|
||||
dependencies: ['data2event'],
|
||||
|
||||
init: async function() {
|
||||
this.el.object3D.visible = false
|
||||
|
||||
await AFRAME.utils.require(this.dependencies)
|
||||
this.el.setAttribute("data2event","")
|
||||
this.el.setAttribute("grabbable","")
|
||||
|
||||
this.el.innerHTML = `
|
||||
<a-box position="-1 0.5 -3" rotation="0 45 0" color="#4CC3D9"></a-box>
|
||||
<a-sphere position="0 1.25 -5" radius="1.25" color="#EF2D5E"></a-sphere>
|
||||
<a-cylinder position="1 0.75 -3" radius="0.5" height="1.5" color="#FFC65D"></a-cylinder>
|
||||
<a-entity position="0 1.8 -3" scale="10 10 10" text="value: ${this.data.text}; align:center; color:#888"></a-entity>
|
||||
`
|
||||
},
|
||||
|
||||
events:{
|
||||
|
||||
launcher: function(e){
|
||||
this.el.object3D.visible = !this.el.object3D.visible
|
||||
clearInterval(this.interval)
|
||||
this.interval = setInterval( () => {
|
||||
this.data.wireframe = !this.data.wireframe
|
||||
}, 500 )
|
||||
},
|
||||
|
||||
// reactive this.data value demo
|
||||
wireframe:function( ){
|
||||
this.el.object3D.traverse( (obj) => obj.material && (obj.material.wireframe = this.data.wireframe) )
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
manifest: { // HTML5 manifest to identify app to xrsh
|
||||
"short_name": "Hello world",
|
||||
"name": "Hello world",
|
||||
"icons": [
|
||||
{
|
||||
"src": "https://css.gg/shape-hexagon.svg",
|
||||
"src": "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNNiAxNS4yMzQ4TDEyIDE4LjU2ODFMMTggMTUuMjM0OFY4Ljc2NTIxTDEyIDUuNDMxODhMNiA4Ljc2NTIxVjE1LjIzNDhaTTEyIDJMMyA3VjE3TDEyIDIyTDIxIDE3VjdMMTIgMloiCiAgICBmaWxsPSJjdXJyZW50Q29sb3IiCiAgLz4KPC9zdmc+",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"id": "/?source=pwa",
|
||||
"start_url": "/?source=pwa",
|
||||
"background_color": "#3367D6",
|
||||
"display": "standalone",
|
||||
"scope": "/",
|
||||
"theme_color": "#3367D6",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "What is the latest news?",
|
||||
"cli":{
|
||||
"usage": "helloworld <type> [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": "Hello world information",
|
||||
"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.
|
||||
`
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -1,34 +1,44 @@
|
|||
AFRAME.registerComponent('helloworld-htmlform', {
|
||||
AFRAME.registerComponent('helloworld-window', {
|
||||
schema: {
|
||||
foo: { type:"string"}
|
||||
},
|
||||
|
||||
init: function () {},
|
||||
init: function () {
|
||||
|
||||
},
|
||||
|
||||
requires:{
|
||||
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", // deadsimple windows: https://nextapps-de.github.io/winbox
|
||||
window: "com/window.js",
|
||||
reactive: "com/data2event.js"
|
||||
},
|
||||
|
||||
dom: {
|
||||
scale: 1,
|
||||
scale: 0.66,
|
||||
events: ['click','input'],
|
||||
html: (me) => `<div class="htmlform">
|
||||
<fieldset>
|
||||
<legend>Colour</legend>
|
||||
<input type="radio" id="color-red" name="color" value="red" checked><label for="color-red"> Red</label><br>
|
||||
<input type="radio" id="color-blue" name="color" value="blue"><label for="color-blue"> Blue</label><br>
|
||||
<legend>Theme</legend>
|
||||
<input type="radio" id="theme" name="theme" value="0" checked style=""><label for="theme" style="margin-right:15px;">Normal</label>
|
||||
<input type="radio" id="themei" name="theme" value="1"><label for="themei">Invert</label><br>
|
||||
</fieldset>
|
||||
<br>
|
||||
<fieldset>
|
||||
<legend>Material:</legend>
|
||||
<input id="material-wireframe" type="checkbox" name="wireframe"><label for="material-wireframe"> Wireframe</label><br>
|
||||
<legend>Welcome to XR Shell</legend>
|
||||
A free offline-first morphable<br>
|
||||
environment which provides <br>
|
||||
<span id="myvalue"></span> XR-friendly shells.<br>
|
||||
<ol>
|
||||
<li>check the <a href="/" target="_blank">website</a></li>
|
||||
<li>check the <a href="https://forgejo.isvery.ninja/xrsh/xrsh-buildroot/src/branch/main/buildroot-v86/board/v86/rootfs_overlay/root/manual.md" target="_blank">manual</a></li>
|
||||
</ol>
|
||||
</fieldset>
|
||||
<!--
|
||||
<fieldset>
|
||||
<legend>Size</legend>
|
||||
<input type="range" min="0.1" max="2" value="1" step="0.01" id="myRange" style="background-color: transparent;">
|
||||
</fieldset>
|
||||
<button>hello <span id="myvalue"></span></button>
|
||||
-->
|
||||
</div>`,
|
||||
|
||||
css: (me) => `.htmlform { padding:11px; }`
|
||||
|
@ -38,13 +48,16 @@ AFRAME.registerComponent('helloworld-htmlform', {
|
|||
events:{
|
||||
|
||||
// component events
|
||||
html: function( ){ console.log("html-mesh requirement mounted") },
|
||||
window: function( ){ console.log("window component mounted") },
|
||||
|
||||
// combined AFRAME+DOM reactive events
|
||||
click: function(e){ }, //
|
||||
click: function(e){ console.dir(e) }, //
|
||||
input: function(e){
|
||||
if( !e.detail.target ) return
|
||||
if( e.detail.target.id == 'myRange' ) this.data.myvalue = e.detail.target.value // reactive demonstration
|
||||
if( e.detail.target.name == 'theme' ) document.body.style.filter = `invert(${e.detail.target.value})`
|
||||
if( e.detail.target.name == 'cmenu' ) document.querySelector(".iconmenu").style.display = e.detail.target.value == 'on' ? '' : 'none';
|
||||
console.dir(e.detail)
|
||||
},
|
||||
|
||||
// reactive events for this.data updates
|
||||
|
@ -54,51 +67,23 @@ AFRAME.registerComponent('helloworld-htmlform', {
|
|||
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("show-texture-in-xr", "") // only show aframe-html in xr
|
||||
instance.setAttribute("grabbable","")
|
||||
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
||||
|
||||
const setupWindow = () => {
|
||||
const com = instance.components['helloworld-htmlform']
|
||||
instance.dom.style.display = 'none'
|
||||
new WinBox("Hello World",{
|
||||
width: 250,
|
||||
height: 340,
|
||||
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("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) )
|
||||
instance.setAttribute("html",`html:#${instance.uid}; cursor:#cursor`)
|
||||
instance.setAttribute("show-texture-in-xr","") // only show aframe-html texture in xr mode
|
||||
}
|
||||
});
|
||||
instance.dom.style.display = AFRAME.utils.XD() == '3D' ? 'none' : '' // show/hide
|
||||
|
||||
// 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++, 500 )
|
||||
}
|
||||
|
||||
setTimeout( () => setupWindow(), 10 ) // give new components time to init
|
||||
this.el.setAttribute("dom", "")
|
||||
this.el.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
||||
},
|
||||
|
||||
ready: function( ){
|
||||
this.el.dom.style.display = 'none'
|
||||
console.log("this.el.dom has been added to DOM")
|
||||
this.data.myvalue = 1
|
||||
DOMready: function(){
|
||||
this.el.setAttribute("window", `title: Welcome; uid: ${this.el.uid}; attach: #overlay; dom: #${this.el.dom.id}; width:250; height: 360`)
|
||||
|
||||
// data2event demo
|
||||
this.el.setAttribute("data2event","")
|
||||
this.data.myvalue = 1001
|
||||
this.data.foo = `this.el ${this.el.uid}: `
|
||||
setInterval( () => this.data.myvalue++, 500 )
|
||||
|
||||
},
|
||||
|
||||
"window.oncreate": function(){
|
||||
this.el.setAttribute("html-as-texture-in-xr", `domid: .winbox#${this.el.uid}; faceuser: true`)
|
||||
}
|
||||
|
||||
},
|
|
@ -34,69 +34,62 @@ if( !AFRAME.components['html-as-texture-in-xr'] ){
|
|||
|
||||
AFRAME.registerComponent('html-as-texture-in-xr', {
|
||||
schema: {
|
||||
domid: { type:"string"},
|
||||
faceuser: { type: "boolean", default: false}
|
||||
domid: { type:"string"},
|
||||
doublesided: {type: "boolean", default: true},
|
||||
faceuser: { type: "boolean", default: false}
|
||||
},
|
||||
|
||||
dependencies:{
|
||||
html: "https://unpkg.com/aframe-htmlmesh@2.1.0/build/aframe-html.js", // html to AFRAME
|
||||
//html: "https://coderofsalvation.github.io/aframe-htmlmesh/build/aframe-html.js"
|
||||
//html: "com/aframe-html.js"
|
||||
html: "com/lib/aframe-html.js"
|
||||
},
|
||||
|
||||
init: async function () {
|
||||
let el = document.querySelector(this.data.domid)
|
||||
if( ! el ){
|
||||
return console.error("html-as-texture-in-xr: cannot get dom element "+this.data.dom.id)
|
||||
return console.error("html-as-texture-in-xr: cannot get dom element "+this.data.domid)
|
||||
}
|
||||
let s = await AFRAME.utils.require(this.dependencies)
|
||||
this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; xrlayer: true`)
|
||||
|
||||
this.forwardClickToMesh();
|
||||
|
||||
this.el.sceneEl.addEventListener('enter-vr', () => this.enableDoubleSided() )
|
||||
|
||||
this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; `)
|
||||
this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' )
|
||||
if( this.data.faceuser ){
|
||||
this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.4) )
|
||||
}
|
||||
},
|
||||
|
||||
manifest: { // HTML5 manifest to identify app to xrsh
|
||||
"short_name": "show-texture-in-xr",
|
||||
"name": "2D/3D switcher",
|
||||
"icons": [],
|
||||
"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 <type> [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": "use ESC-key to toggle between 2D / 3D",
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/images/screenshot1.png",
|
||||
"type": "image/png",
|
||||
"sizes": "540x720",
|
||||
"form_factor": "narrow"
|
||||
}
|
||||
],
|
||||
"help":`
|
||||
Helloworld application
|
||||
forwardClickToMesh: function(){
|
||||
// monkeypatch: forward click to mesh
|
||||
const handle = AFRAME.components['html'].Component.prototype.handle
|
||||
AFRAME.components['html'].Component.prototype.handle = function(type,evt){
|
||||
|
||||
`
|
||||
if( !this.el.sceneEl.renderer.xr.isPresenting ) return // ignore events in desktop mode
|
||||
if( this.el.sceneEl.renderer.xr.isPresenting && type.match(/^mouse/) ) return // ignore mouse-events in XR
|
||||
|
||||
if( type == 'click' && evt.detail.length && evt.detail[0].uv ){
|
||||
const mesh = this.el.object3D.children[0]
|
||||
const uv = evt.detail[0].uv;
|
||||
const _pointer = new THREE.Vector2();
|
||||
const _event = { type: '', data: _pointer };
|
||||
_event.type = type;
|
||||
_event.data.set( uv.x, 1 - uv.y );
|
||||
mesh.dispatchEvent( _event );
|
||||
}
|
||||
return handle.apply(this,[type,evt])
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
enableDoubleSided: function(){
|
||||
// enable doubleside
|
||||
this.el.object3D.traverse( (o) => {
|
||||
if( o.constructor && String(o.constructor).match(/HTMLMesh/) ){
|
||||
o.material.side = THREE.DoubleSide
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -140,7 +140,7 @@ if( typeof AFRAME != 'undefined '){
|
|||
css: (me) => `
|
||||
|
||||
.isoterminal{
|
||||
padding: ${me.com.data.padding}px;
|
||||
padding: ${me.com.data.padding}px 0px 0px ${me.com.data.padding}px;
|
||||
width:100%;
|
||||
height:99%;
|
||||
resize: both;
|
||||
|
@ -252,6 +252,8 @@ if( typeof AFRAME != 'undefined '){
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
.wb-control { margin-right:10px }
|
||||
|
||||
.XR .isoterminal{
|
||||
background: #000;
|
||||
}
|
||||
|
@ -336,7 +338,7 @@ if( typeof AFRAME != 'undefined '){
|
|||
this.term.emit('term_init', {instance, aEntity:this})
|
||||
//instance.winbox.resize(720,380)
|
||||
let size = `width: ${this.data.width}; height: ${this.data.height}`
|
||||
instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-max, no-resize`)
|
||||
instance.setAttribute("window", `title: xrsh; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}; class: no-full, no-max, no-resize, no-close; `)
|
||||
})
|
||||
|
||||
instance.addEventListener('window.oncreate', (e) => {
|
||||
|
@ -372,7 +374,7 @@ if( typeof AFRAME != 'undefined '){
|
|||
const w = instance.winbox
|
||||
if(!w) return
|
||||
w.titleBak = w.titleBak || w.title
|
||||
w.setTitle( `${w.titleBak} [${msg}]` )
|
||||
w.setTitle( `${w.titleBak} ${msg ? "["+msg+"]" : ""}` )
|
||||
})
|
||||
|
||||
instance.addEventListener('window.onclose', (e) => {
|
||||
|
@ -383,18 +385,13 @@ if( typeof AFRAME != 'undefined '){
|
|||
instance.addEventListener('window.onresize', resize )
|
||||
instance.addEventListener('window.onmaximize', resize )
|
||||
|
||||
const focus = (showdom) => (e) => {
|
||||
const focus = (e) => {
|
||||
this.el.emit('focus',e.detail)
|
||||
if( this.el.components.window && this.data.renderer == 'canvas'){
|
||||
this.el.components.window.show( showdom )
|
||||
}
|
||||
}
|
||||
|
||||
this.el.addEventListener('obbcollisionstarted', focus(false) )
|
||||
this.el.sceneEl.addEventListener('enter-vr', focus(false) )
|
||||
this.el.sceneEl.addEventListener('enter-ar', focus(false) )
|
||||
this.el.sceneEl.addEventListener('exit-vr', focus(true) )
|
||||
this.el.sceneEl.addEventListener('exit-ar', focus(true) )
|
||||
this.el.addEventListener('obbcollisionstarted', focus )
|
||||
this.el.sceneEl.addEventListener('exit-vr', focus )
|
||||
this.el.sceneEl.addEventListener('exit-ar', focus )
|
||||
|
||||
instance.object3D.quaternion.copy( AFRAME.scenes[0].camera.quaternion ) // face towards camera
|
||||
},
|
||||
|
@ -444,7 +441,12 @@ if( typeof AFRAME != 'undefined '){
|
|||
myvalue: function(e){ this.el.dom.querySelector('b').innerText = this.data.myvalue },
|
||||
|
||||
launcher: async function(){
|
||||
this.initTerminal()
|
||||
if( !this.term.instance ){
|
||||
this.initTerminal()
|
||||
}else{
|
||||
// toggle visibility
|
||||
this.el.winbox[ this.el.winbox.min ? 'restore' : 'minimize' ]()
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
|
|
@ -266,13 +266,13 @@ ISOTerminal.prototype.startVM = function(opts){
|
|||
let msglib = this.getLoaderMsg()
|
||||
let msg = msglib.motd
|
||||
|
||||
this.emit('status',msglib.loadmsg)
|
||||
this.emit('serial-output-string', msg)
|
||||
this.emit('bootMenu',{bootMenu: this.opts.bootMenu, bootMenuURL: this.opts.bootMenuURL })
|
||||
}
|
||||
|
||||
ISOTerminal.prototype.bootISO = function(){
|
||||
let msglib = this.getLoaderMsg()
|
||||
this.emit('status',msglib.loadmsg)
|
||||
let msg = "\n\r" + msglib.empowermsg + msglib.text_color + msglib.loadmsg + msglib.text_reset
|
||||
this.emit('serial-output-string', msg)
|
||||
this.emit('runISO',{...this.v86opts, bufferLatency: this.opts.bufferLatency })
|
||||
|
|
|
@ -4,8 +4,8 @@ ISOTerminal.addEventListener('ready', function(e){
|
|||
|
||||
ISOTerminal.prototype.bootMenu = function(e){
|
||||
this.boot.menu.selected = false // reset
|
||||
const autobootURL = e.detail.bootMenuURL && document.location.hash.length > 1
|
||||
const autoboot = e.detail.bootMenu || autobootURL
|
||||
const autobootURL = e && e.detail.bootMenuURL && document.location.hash.length > 1
|
||||
const autoboot = e && e.detail.bootMenu || autobootURL
|
||||
if( !autoboot ){
|
||||
|
||||
let msg = '\n\r'
|
||||
|
|
|
@ -96,10 +96,10 @@ ISOTerminal.addEventListener('init', function(){
|
|||
},
|
||||
keyHandler: function(ch){
|
||||
let erase = false
|
||||
if( ch == '\x7F' ){
|
||||
ch = "\b \b" // why does write() not just support \x7F ?
|
||||
erase = true
|
||||
}
|
||||
// if( ch == '\x7F' ){
|
||||
// ch = "\b \b" // why does write() not just support \x7F ?
|
||||
// erase = true
|
||||
// }
|
||||
this.send(ch)
|
||||
const reset = () => {
|
||||
this.console = ""
|
||||
|
|
|
@ -92,7 +92,11 @@ ISOTerminal.prototype.TermInit = function(){
|
|||
}
|
||||
if( !erase ) this.lastChar = ch
|
||||
})
|
||||
aEntity.el.addEventListener('focus', () => el.querySelector("textarea").focus() )
|
||||
aEntity.el.addEventListener('focus', () => {
|
||||
let textarea = el.querySelector("textarea")
|
||||
textarea.focus()
|
||||
if( document.activeElement != textarea ) textarea.focus()
|
||||
})
|
||||
aEntity.el.addEventListener('serial-output-string', (e) => {
|
||||
let msg = e.detail
|
||||
this.term.write(msg)
|
||||
|
|
313
com/launcher.js
313
com/launcher.js
|
@ -1,20 +1,52 @@
|
|||
/*
|
||||
* ## launcher
|
||||
/**
|
||||
* ## [launcher](com/launcher.js)
|
||||
*
|
||||
* displays app (icons) for enduser to launch
|
||||
* displays app (icons) in 2D and 3D handmenu (enduser can launch desktop-like 'apps')
|
||||
*
|
||||
* ```html
|
||||
* <a-entity launcher>
|
||||
* <a-entity launch="component: helloworld; foo: bar"><a-entity>
|
||||
* </a-entity>
|
||||
*
|
||||
* ```javascript
|
||||
* <a-entity app="app/launcher.js"/>
|
||||
* ```
|
||||
*
|
||||
* | property | type | example |
|
||||
* |--------------|--------------------|----------------------------------------------------------------------------------------|
|
||||
* | `registries` | `array` of strings | <a-entity app="app/launcher.js; registers: https://foo.com/index.json, ./index.json"/> |
|
||||
* | `attach` | `selector` | hand or object to attach menu to |
|
||||
* | `registries` | `array` of strings | `<a-entity launcher="registers: https://foo.com/index.json, ./index.json"/>` |
|
||||
*
|
||||
* | event | target | info |
|
||||
* |--------------|-------------------------------------------------------------------------------------------------------------|
|
||||
* | `launcher` | an app | when pressing an app icon, `launcher` event will be send to the respective app |
|
||||
*/
|
||||
*
|
||||
* There a multiple ways of letting the launcher know that an app can be launched:
|
||||
*
|
||||
* 1. any AFRAME component with an `launcher`-event + manifest is automatically added:
|
||||
*
|
||||
* ```javascript
|
||||
* AFRAME.registerComponent('foo',{
|
||||
* events:{
|
||||
* launcher: function(){ ...launch something... }
|
||||
* },
|
||||
* manifest:{ // HTML5 manifesto JSON object
|
||||
* // https://www.w3.org/TR/appmanifest/
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 2. dynamically in javascript
|
||||
*
|
||||
* ```javascript
|
||||
* window.launcher.register({
|
||||
* name:"foo",
|
||||
* icon: "https://.../optional_icon.png"
|
||||
* description: "lorem ipsum",
|
||||
* cb: () => alert("foo")
|
||||
* })
|
||||
* //window.launcher.unregister('foo')
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
|
||||
AFRAME.registerComponent('launch', { // use this component to auto-launch component
|
||||
init: function(){
|
||||
|
@ -26,12 +58,6 @@ AFRAME.registerComponent('launch', { // use this component to auto-launch compon
|
|||
|
||||
AFRAME.registerComponent('launcher', {
|
||||
schema: {
|
||||
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',
|
||||
|
@ -42,52 +68,32 @@ AFRAME.registerComponent('launcher', {
|
|||
'#333333',
|
||||
]},
|
||||
paused: { type:"boolean","default":false},
|
||||
cols: { type:"number", "default": 5 }
|
||||
},
|
||||
|
||||
dependencies:{
|
||||
dom: "com/dom.js"
|
||||
requires:{
|
||||
dom: "com/dom.js",
|
||||
htmlinxr: "com/html-as-texture-in-xr.js",
|
||||
data2events: "com/data2event.js"
|
||||
},
|
||||
|
||||
init: async function () {
|
||||
await AFRAME.utils.require(this.dependencies)
|
||||
this.el.object3D.visible = false;
|
||||
await AFRAME.utils.require(this.requires)
|
||||
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",
|
||||
data2events: "./com/data2event.js"
|
||||
})
|
||||
|
||||
this.el.setAttribute("dom","")
|
||||
this.el.setAttribute("noxd","ignore") // hint to XD.js that we manage ourselve concerning 2D/3D switching
|
||||
this.el.setAttribute("pressable","")
|
||||
this.el.sceneEl.addEventListener('enter-vr', () => this.centerMenu() )
|
||||
this.el.sceneEl.addEventListener('enter-ar', () => this.centerMenu() )
|
||||
this.render()
|
||||
|
||||
if( this.data.attach ){
|
||||
this.el.object3D.visible = false
|
||||
if( this.isHand(this.data.attach) ){
|
||||
this.data.attach.addEventListener('model-loaded', () => {
|
||||
this.ready = true
|
||||
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: {
|
||||
scale: 3,
|
||||
scale: 0.8,
|
||||
events: ['click'],
|
||||
html: (me) => `<div class="iconmenu">loading components..</div>`,
|
||||
css: (me) => `.iconmenu {
|
||||
css: (me) => `
|
||||
.iconmenu {
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -95,7 +101,6 @@ AFRAME.registerComponent('launcher', {
|
|||
height: 50px;
|
||||
overflow:hidden;
|
||||
position: fixed;
|
||||
right: 162px;
|
||||
bottom: 10px;
|
||||
left:20px;
|
||||
background: transparent;
|
||||
|
@ -116,6 +121,8 @@ AFRAME.registerComponent('launcher', {
|
|||
border-top: 2px solid #BBB;
|
||||
border-bottom: 2px solid #BBB;
|
||||
font-size:18px;
|
||||
color: #777;
|
||||
line-height: 7px;
|
||||
}
|
||||
|
||||
.iconmenu > button:first-child {
|
||||
|
@ -132,6 +139,10 @@ AFRAME.registerComponent('launcher', {
|
|||
padding-top:13px;
|
||||
}
|
||||
|
||||
.iconmenu > button:only-child{
|
||||
border-radius:5px 5px 5px 5px;
|
||||
}
|
||||
|
||||
.iconmenu > button > img {
|
||||
transform: translate(0px,-14px);
|
||||
opacity:0.5;
|
||||
|
@ -146,15 +157,11 @@ 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
|
||||
}
|
||||
|
||||
click: function(e){ },
|
||||
|
||||
DOMready: function(){
|
||||
this.el.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}; faceuser: false`)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -163,136 +170,70 @@ AFRAME.registerComponent('launcher', {
|
|||
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(els){
|
||||
if( !this.el.dom ) return // too early (dom.js component not ready)
|
||||
|
||||
let requires = []
|
||||
let i = 0
|
||||
let j = 0
|
||||
let colors = this.data.colors
|
||||
const add2D = (launchCom,el,manifest) => {
|
||||
const add2D = (launchCom,manifest) => {
|
||||
let btn = document.createElement('button')
|
||||
btn.innerHTML = `${ manifest?.icons?.length > 0
|
||||
? `<img src='${manifest.icons[0].src}' title='${manifest.name}: ${manifest.description}'/>`
|
||||
: `${manifest.short_name}`
|
||||
}`
|
||||
btn.addEventListener('click', () => el.emit('launcher',{}) )
|
||||
this.el.dom.appendChild(btn)
|
||||
}
|
||||
let iconDefault = "data:image/svg+xml;base64,PHN2ZwogIHdpZHRoPSIyNCIKICBoZWlnaHQ9IjI0IgogIHZpZXdCb3g9IjAgMCAyNCAyNCIKICBmaWxsPSJub25lIgogIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKPgogIDxwYXRoCiAgICBmaWxsLXJ1bGU9ImV2ZW5vZGQiCiAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICBkPSJNMjAuMTcwMiAzTDIwLjE2NjMgMy4wMDQ1M0MyMS43NDU4IDMuMDkwODQgMjMgNC4zOTg5NiAyMyA2VjE4QzIzIDE5LjY1NjkgMjEuNjU2OSAyMSAyMCAyMUg0QzIuMzQzMTUgMjEgMSAxOS42NTY5IDEgMThWNkMxIDQuMzQzMTUgMi4zNDMxNSAzIDQgM0gyMC4xNzAyWk0xMC40NzY0IDVIMTYuNDc2NEwxMy4wODkgOUg3LjA4ODk5TDEwLjQ3NjQgNVpNNS4wODg5OSA5TDguNDc2NDQgNUg0QzMuNDQ3NzIgNSAzIDUuNDQ3NzIgMyA2VjlINS4wODg5OVpNMyAxMVYxOEMzIDE4LjU1MjMgMy40NDc3MiAxOSA0IDE5SDIwQzIwLjU1MjMgMTkgMjEgMTguNTUyMyAyMSAxOFYxMUgzWk0yMSA5VjZDMjEgNS40NDc3MSAyMC41NTIzIDUgMjAgNUgxOC40NzY0TDE1LjA4OSA5SDIxWiIKICAgIGZpbGw9ImN1cnJlbnRDb2xvciIKICAvPgo8L3N2Zz4="
|
||||
let html = manifest?.icons?.length > 0 || !manifest.name ? `<img src='${manifest.icons[0].src || iconDefault}' title='${manifest.name}: ${manifest.description}'/>` : ""
|
||||
if( manifest.name && !html ) html = `${manifest.short_name || manifest.name}`
|
||||
btn.innerHTML = html
|
||||
|
||||
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",`${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")
|
||||
aentity.appendChild(atext)
|
||||
this.el.appendChild(aentity)
|
||||
aentity.launchCom = launchCom
|
||||
return aentity
|
||||
btn.addEventListener('click', (e) => {
|
||||
launchCom.launcher()
|
||||
e.stopPropagation()
|
||||
// visual feedback to user
|
||||
btn.style.filter = "brightness(0.5)"
|
||||
this.el.components.html.rerender()
|
||||
setTimeout( () => {
|
||||
btn.style.filter = "brightness(1)"
|
||||
this.el.components.html.rerender()
|
||||
}, 500 )
|
||||
return false
|
||||
})
|
||||
this.el.dom.appendChild(btn)
|
||||
}
|
||||
|
||||
// finally render them!
|
||||
this.el.dom.innerHTML = '' // clear
|
||||
els = els || this.system.components
|
||||
els = els || this.system.launchables
|
||||
els.map( (c) => {
|
||||
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
|
||||
const manifest = c.manifest
|
||||
if( manifest ){
|
||||
add2D(launchCom,c,manifest)
|
||||
add3D(launchCom,c,manifest)
|
||||
add2D(c,manifest)
|
||||
}
|
||||
})
|
||||
|
||||
this.centerMenu();
|
||||
|
||||
},
|
||||
|
||||
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 && !launcher.data.open && e.srcElement.computedMixinStr == 'menubutton' ){
|
||||
return (launcher.data.open = true)
|
||||
}
|
||||
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
|
||||
centerMenu: function(){
|
||||
// center along x-axis
|
||||
this.el.object3D.traverse( (o) => {
|
||||
if( o.constructor && String(o.constructor).match(/HTMLMesh/) ){
|
||||
this.setOriginToMiddle(o, this.el.object3D, {x:true})
|
||||
o.position.z = 0.012 // position a bit before the grab-line
|
||||
o.position.y = -0.01 // position a bit before the grab-line
|
||||
}
|
||||
})
|
||||
if( el.launchCom ){
|
||||
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
|
||||
}
|
||||
this.el.object3D.visible = true // ensure visibility
|
||||
},
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
setOriginToMiddle: function(fromObject, toObject, axis) {
|
||||
var boxFrom = new THREE.Box3().setFromObject(fromObject);
|
||||
var boxTo = new THREE.Box3().setFromObject(toObject);
|
||||
var center = new THREE.Vector3();
|
||||
if( !toObject.positionOriginal ) toObject.positionOriginal = toObject.position.clone()
|
||||
center.x = axis.x ? - (boxFrom.max.x/2) : 0
|
||||
center.y = axis.y ? - (boxFrom.max.y/2) : 0
|
||||
center.z = axis.z ? - (boxFrom.max.z/2) : 0
|
||||
toObject.position.copy( toObject.positionOriginal )
|
||||
toObject.position.sub(center);
|
||||
},
|
||||
|
||||
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.ready && 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
|
||||
"short_name": "launcher",
|
||||
|
@ -352,35 +293,73 @@ in above's case "\nHelloworld application\n" will qualify as header.
|
|||
AFRAME.registerSystem('launcher',{
|
||||
|
||||
init: function(){
|
||||
this.components = []
|
||||
this.launchables = []
|
||||
this.dom = []
|
||||
this.registered = []
|
||||
// observe HTML changes in <a-scene>
|
||||
observer = new MutationObserver( (a,b) => this.getLaunchables(a,b) )
|
||||
observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false});
|
||||
|
||||
window.launcher = this
|
||||
this.getLaunchables()
|
||||
},
|
||||
|
||||
register: function(launchable){
|
||||
try{
|
||||
let {name, description, cb} = launchable
|
||||
this.registered.push({
|
||||
manifest: {name, description, icons: launchable.icon ? [{src:launchable.icon}] : [] },
|
||||
launcher: cb
|
||||
})
|
||||
}catch(e){
|
||||
console.error('AFRAME.systems.launcher.register({ name, description, icon, cb }) got invalid obj')
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
this.getLaunchables()
|
||||
},
|
||||
|
||||
unregister: function(launchableName){
|
||||
this.registered = this.registered.filter( (l) => l.name != launchableName )
|
||||
},
|
||||
|
||||
getLaunchables: function(mutationsList,observer){
|
||||
let searchEvent = 'launcher'
|
||||
let els = [...this.sceneEl.getElementsByTagName("*")]
|
||||
let seen = {}
|
||||
this.launchables = [
|
||||
/*
|
||||
* {
|
||||
* manifest: {...}
|
||||
* launcher: () => ....
|
||||
* }
|
||||
*/
|
||||
];
|
||||
|
||||
this.components = els.filter( (el) => {
|
||||
// collect manually registered launchables
|
||||
this.registered.map( (launchable) => this.launchables.push(launchable) )
|
||||
|
||||
// collect launchables in aframe dom elements
|
||||
this.dom = 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] && !seen[i] ){
|
||||
hasEvent = seen[i] = true
|
||||
let com = hasEvent = seen[i] = el.components[i]
|
||||
com.launcher = () => com.el.emit('launcher',null,false) // important: no bubble
|
||||
this.launchables.push(com)
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasEvent ? el : null
|
||||
})
|
||||
this.updateLauncher()
|
||||
return seen
|
||||
return this.launchables
|
||||
},
|
||||
|
||||
updateLauncher: function(){
|
||||
let launcher = document.querySelector('[launcher]')
|
||||
if( launcher ) launcher.components.launcher.render()
|
||||
if( launcher && launcher.components.launcher) launcher.components.launcher.render()
|
||||
}
|
||||
|
||||
})
|
||||
|
|
|
@ -0,0 +1,679 @@
|
|||
(function (three) {
|
||||
'use strict';
|
||||
|
||||
// This is a copy of https://github.com/mrdoob/three.js/blob/0403020848c26a9605eb91c99a949111ad4a532e/examples/jsm/interactive/HTMLMesh.js
|
||||
|
||||
class HTMLMesh extends three.Mesh {
|
||||
|
||||
constructor( dom ) {
|
||||
|
||||
const texture = new HTMLTexture( dom );
|
||||
|
||||
const geometry = new three.PlaneGeometry( texture.image.width * 0.001, texture.image.height * 0.001 );
|
||||
const material = new three.MeshBasicMaterial( { map: texture, toneMapped: false, transparent: true } );
|
||||
|
||||
super( geometry, material );
|
||||
|
||||
function onEvent( event ) {
|
||||
|
||||
material.map.dispatchDOMEvent( event );
|
||||
|
||||
}
|
||||
|
||||
this.addEventListener( 'mousedown', onEvent );
|
||||
this.addEventListener( 'mousemove', onEvent );
|
||||
this.addEventListener( 'mouseup', onEvent );
|
||||
this.addEventListener( 'click', onEvent );
|
||||
|
||||
this.dispose = function () {
|
||||
|
||||
geometry.dispose();
|
||||
material.dispose();
|
||||
|
||||
material.map.dispose();
|
||||
|
||||
canvases.delete( dom );
|
||||
|
||||
this.removeEventListener( 'mousedown', onEvent );
|
||||
this.removeEventListener( 'mousemove', onEvent );
|
||||
this.removeEventListener( 'mouseup', onEvent );
|
||||
this.removeEventListener( 'click', onEvent );
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class HTMLTexture extends three.CanvasTexture {
|
||||
|
||||
constructor( dom ) {
|
||||
|
||||
super( html2canvas( dom ) );
|
||||
|
||||
this.dom = dom;
|
||||
|
||||
this.anisotropy = 16;
|
||||
this.encoding = three.sRGBEncoding;
|
||||
this.minFilter = three.LinearFilter;
|
||||
this.magFilter = three.LinearFilter;
|
||||
|
||||
// Create an observer on the DOM, and run html2canvas update in the next loop
|
||||
const observer = new MutationObserver( () => {
|
||||
|
||||
if ( ! this.scheduleUpdate ) {
|
||||
|
||||
// ideally should use xr.requestAnimationFrame, here setTimeout to avoid passing the renderer
|
||||
this.scheduleUpdate = setTimeout( () => this.update(), 16 );
|
||||
|
||||
}
|
||||
|
||||
} );
|
||||
|
||||
const config = { attributes: true, childList: true, subtree: true, characterData: true };
|
||||
observer.observe( dom, config );
|
||||
|
||||
this.observer = observer;
|
||||
|
||||
}
|
||||
|
||||
dispatchDOMEvent( event ) {
|
||||
|
||||
if ( event.data ) {
|
||||
|
||||
htmlevent( this.dom, event.type, event.data.x, event.data.y );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
update() {
|
||||
|
||||
this.image = html2canvas( this.dom );
|
||||
this.needsUpdate = true;
|
||||
|
||||
this.scheduleUpdate = null;
|
||||
|
||||
}
|
||||
|
||||
dispose() {
|
||||
|
||||
if ( this.observer ) {
|
||||
|
||||
this.observer.disconnect();
|
||||
|
||||
}
|
||||
|
||||
this.scheduleUpdate = clearTimeout( this.scheduleUpdate );
|
||||
|
||||
super.dispose();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
|
||||
const canvases = new WeakMap();
|
||||
|
||||
function html2canvas( element ) {
|
||||
|
||||
const range = document.createRange();
|
||||
const color = new three.Color();
|
||||
|
||||
function Clipper( context ) {
|
||||
|
||||
const clips = [];
|
||||
let isClipping = false;
|
||||
|
||||
function doClip() {
|
||||
|
||||
if ( isClipping ) {
|
||||
|
||||
isClipping = false;
|
||||
context.restore();
|
||||
|
||||
}
|
||||
|
||||
if ( clips.length === 0 ) return;
|
||||
|
||||
let minX = - Infinity, minY = - Infinity;
|
||||
let maxX = Infinity, maxY = Infinity;
|
||||
|
||||
for ( let i = 0; i < clips.length; i ++ ) {
|
||||
|
||||
const clip = clips[ i ];
|
||||
|
||||
minX = Math.max( minX, clip.x );
|
||||
minY = Math.max( minY, clip.y );
|
||||
maxX = Math.min( maxX, clip.x + clip.width );
|
||||
maxY = Math.min( maxY, clip.y + clip.height );
|
||||
|
||||
}
|
||||
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.rect( minX, minY, maxX - minX, maxY - minY );
|
||||
context.clip();
|
||||
|
||||
isClipping = true;
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
add: function ( clip ) {
|
||||
|
||||
clips.push( clip );
|
||||
doClip();
|
||||
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
|
||||
clips.pop();
|
||||
doClip();
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
function drawText( style, x, y, string ) {
|
||||
|
||||
if ( string !== '' ) {
|
||||
|
||||
if ( style.textTransform === 'uppercase' ) {
|
||||
|
||||
string = string.toUpperCase();
|
||||
|
||||
}
|
||||
|
||||
context.font = style.fontWeight + ' ' + style.fontSize + ' ' + style.fontFamily;
|
||||
context.textBaseline = 'top';
|
||||
context.fillStyle = style.color;
|
||||
context.fillText( string, x, y + parseFloat( style.fontSize ) * 0.1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function buildRectPath( x, y, w, h, r ) {
|
||||
|
||||
if ( w < 2 * r ) r = w / 2;
|
||||
if ( h < 2 * r ) r = h / 2;
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo( x + r, y );
|
||||
context.arcTo( x + w, y, x + w, y + h, r );
|
||||
context.arcTo( x + w, y + h, x, y + h, r );
|
||||
context.arcTo( x, y + h, x, y, r );
|
||||
context.arcTo( x, y, x + w, y, r );
|
||||
context.closePath();
|
||||
|
||||
}
|
||||
|
||||
function drawBorder( style, which, x, y, width, height ) {
|
||||
|
||||
const borderWidth = style[ which + 'Width' ];
|
||||
const borderStyle = style[ which + 'Style' ];
|
||||
const borderColor = style[ which + 'Color' ];
|
||||
|
||||
if ( borderWidth !== '0px' && borderStyle !== 'none' && borderColor !== 'transparent' && borderColor !== 'rgba(0, 0, 0, 0)' ) {
|
||||
|
||||
context.strokeStyle = borderColor;
|
||||
context.lineWidth = parseFloat( borderWidth );
|
||||
context.beginPath();
|
||||
context.moveTo( x, y );
|
||||
context.lineTo( x + width, y + height );
|
||||
context.stroke();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function drawElement( element, style ) {
|
||||
|
||||
let x = 0, y = 0, width = 0, height = 0;
|
||||
|
||||
if ( element.nodeType === Node.TEXT_NODE ) {
|
||||
|
||||
// text
|
||||
|
||||
range.selectNode( element );
|
||||
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
x = rect.left - offset.left - 0.5;
|
||||
y = rect.top - offset.top - 0.5;
|
||||
width = rect.width;
|
||||
height = rect.height;
|
||||
|
||||
drawText( style, x, y, element.nodeValue.trim() );
|
||||
|
||||
} else if ( element.nodeType === Node.COMMENT_NODE ) {
|
||||
|
||||
return;
|
||||
|
||||
} else if ( element instanceof HTMLCanvasElement ) {
|
||||
|
||||
// Canvas element
|
||||
if ( element.style.display === 'none' ) return;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
x = rect.left - offset.left - 0.5;
|
||||
y = rect.top - offset.top - 0.5;
|
||||
|
||||
context.save();
|
||||
const dpr = window.devicePixelRatio;
|
||||
context.scale( 1 / dpr, 1 / dpr );
|
||||
context.drawImage( element, x, y );
|
||||
context.restore();
|
||||
|
||||
} else if ( element instanceof HTMLImageElement ) {
|
||||
|
||||
if ( element.style.display === 'none' ) return;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
x = rect.left - offset.left - 0.5;
|
||||
y = rect.top - offset.top - 0.5;
|
||||
width = rect.width;
|
||||
height = rect.height;
|
||||
|
||||
context.drawImage( element, x, y, width, height );
|
||||
|
||||
} else {
|
||||
|
||||
if ( element.style.display === 'none' ) return;
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
x = rect.left - offset.left - 0.5;
|
||||
y = rect.top - offset.top - 0.5;
|
||||
width = rect.width;
|
||||
height = rect.height;
|
||||
|
||||
style = window.getComputedStyle( element );
|
||||
|
||||
// Get the border of the element used for fill and border
|
||||
|
||||
buildRectPath( x, y, width, height, parseFloat( style.borderRadius ) );
|
||||
|
||||
const backgroundColor = style.backgroundColor;
|
||||
|
||||
if ( backgroundColor !== 'transparent' && backgroundColor !== 'rgba(0, 0, 0, 0)' ) {
|
||||
|
||||
context.fillStyle = backgroundColor;
|
||||
context.fill();
|
||||
|
||||
}
|
||||
|
||||
// If all the borders match then stroke the round rectangle
|
||||
|
||||
const borders = [ 'borderTop', 'borderLeft', 'borderBottom', 'borderRight' ];
|
||||
|
||||
let match = true;
|
||||
let prevBorder = null;
|
||||
|
||||
for ( const border of borders ) {
|
||||
|
||||
if ( prevBorder !== null ) {
|
||||
|
||||
match = ( style[ border + 'Width' ] === style[ prevBorder + 'Width' ] ) &&
|
||||
( style[ border + 'Color' ] === style[ prevBorder + 'Color' ] ) &&
|
||||
( style[ border + 'Style' ] === style[ prevBorder + 'Style' ] );
|
||||
|
||||
}
|
||||
|
||||
if ( match === false ) break;
|
||||
|
||||
prevBorder = border;
|
||||
|
||||
}
|
||||
|
||||
if ( match === true ) {
|
||||
|
||||
// They all match so stroke the rectangle from before allows for border-radius
|
||||
|
||||
const width = parseFloat( style.borderTopWidth );
|
||||
|
||||
if ( style.borderTopWidth !== '0px' && style.borderTopStyle !== 'none' && style.borderTopColor !== 'transparent' && style.borderTopColor !== 'rgba(0, 0, 0, 0)' ) {
|
||||
|
||||
context.strokeStyle = style.borderTopColor;
|
||||
context.lineWidth = width;
|
||||
context.stroke();
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Otherwise draw individual borders
|
||||
|
||||
drawBorder( style, 'borderTop', x, y, width, 0 );
|
||||
drawBorder( style, 'borderLeft', x, y, 0, height );
|
||||
drawBorder( style, 'borderBottom', x, y + height, width, 0 );
|
||||
drawBorder( style, 'borderRight', x + width, y, 0, height );
|
||||
|
||||
}
|
||||
|
||||
if ( element instanceof HTMLInputElement ) {
|
||||
|
||||
let accentColor = style.accentColor;
|
||||
|
||||
if ( accentColor === undefined || accentColor === 'auto' ) accentColor = style.color;
|
||||
|
||||
color.set( accentColor );
|
||||
|
||||
const luminance = Math.sqrt( 0.299 * ( color.r ** 2 ) + 0.587 * ( color.g ** 2 ) + 0.114 * ( color.b ** 2 ) );
|
||||
const accentTextColor = luminance < 0.5 ? 'white' : '#111111';
|
||||
|
||||
if ( element.type === 'radio' ) {
|
||||
|
||||
buildRectPath( x, y, width, height, height );
|
||||
|
||||
context.fillStyle = 'white';
|
||||
context.strokeStyle = accentColor;
|
||||
context.lineWidth = 1;
|
||||
context.fill();
|
||||
context.stroke();
|
||||
|
||||
if ( element.checked ) {
|
||||
|
||||
buildRectPath( x + 2, y + 2, width - 4, height - 4, height );
|
||||
|
||||
context.fillStyle = accentColor;
|
||||
context.strokeStyle = accentTextColor;
|
||||
context.lineWidth = 2;
|
||||
context.fill();
|
||||
context.stroke();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( element.type === 'checkbox' ) {
|
||||
|
||||
buildRectPath( x, y, width, height, 2 );
|
||||
|
||||
context.fillStyle = element.checked ? accentColor : 'white';
|
||||
context.strokeStyle = element.checked ? accentTextColor : accentColor;
|
||||
context.lineWidth = 1;
|
||||
context.stroke();
|
||||
context.fill();
|
||||
|
||||
if ( element.checked ) {
|
||||
|
||||
const currentTextAlign = context.textAlign;
|
||||
|
||||
context.textAlign = 'center';
|
||||
|
||||
const properties = {
|
||||
color: accentTextColor,
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: height + 'px',
|
||||
fontWeight: 'bold'
|
||||
};
|
||||
|
||||
drawText( properties, x + ( width / 2 ), y, '✔' );
|
||||
|
||||
context.textAlign = currentTextAlign;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( element.type === 'range' ) {
|
||||
|
||||
const [ min, max, value ] = [ 'min', 'max', 'value' ].map( property => parseFloat( element[ property ] ) );
|
||||
const position = ( ( value - min ) / ( max - min ) ) * ( width - height );
|
||||
|
||||
buildRectPath( x, y + ( height / 4 ), width, height / 2, height / 4 );
|
||||
context.fillStyle = accentTextColor;
|
||||
context.strokeStyle = accentColor;
|
||||
context.lineWidth = 1;
|
||||
context.fill();
|
||||
context.stroke();
|
||||
|
||||
buildRectPath( x, y + ( height / 4 ), position + ( height / 2 ), height / 2, height / 4 );
|
||||
context.fillStyle = accentColor;
|
||||
context.fill();
|
||||
|
||||
buildRectPath( x + position, y, height, height, height / 2 );
|
||||
context.fillStyle = accentColor;
|
||||
context.fill();
|
||||
|
||||
}
|
||||
|
||||
if ( element.type === 'color' || element.type === 'text' || element.type === 'number' ) {
|
||||
|
||||
clipper.add( { x: x, y: y, width: width, height: height } );
|
||||
|
||||
drawText( style, x + parseInt( style.paddingLeft ), y + parseInt( style.paddingTop ), element.value );
|
||||
|
||||
clipper.remove();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
// debug
|
||||
context.strokeStyle = '#' + Math.random().toString( 16 ).slice( - 3 );
|
||||
context.strokeRect( x - 0.5, y - 0.5, width + 1, height + 1 );
|
||||
*/
|
||||
|
||||
const isClipping = style.overflow === 'auto' || style.overflow === 'hidden';
|
||||
|
||||
if ( isClipping ) clipper.add( { x: x, y: y, width: width, height: height } );
|
||||
|
||||
for ( let i = 0; i < element.childNodes.length; i ++ ) {
|
||||
|
||||
drawElement( element.childNodes[ i ], style );
|
||||
|
||||
}
|
||||
|
||||
if ( isClipping ) clipper.remove();
|
||||
|
||||
}
|
||||
|
||||
const offset = element.getBoundingClientRect();
|
||||
|
||||
let canvas = canvases.get( element );
|
||||
|
||||
if ( canvas === undefined ) {
|
||||
|
||||
canvas = document.createElement( 'canvas' );
|
||||
canvas.width = offset.width;
|
||||
canvas.height = offset.height;
|
||||
canvases.set( element, canvas );
|
||||
|
||||
}
|
||||
|
||||
const context = canvas.getContext( '2d'/*, { alpha: false }*/ );
|
||||
|
||||
const clipper = new Clipper( context );
|
||||
|
||||
// console.time( 'drawElement' );
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
drawElement( element );
|
||||
|
||||
// console.timeEnd( 'drawElement' );
|
||||
|
||||
return canvas;
|
||||
|
||||
}
|
||||
|
||||
function htmlevent( element, event, x, y ) {
|
||||
|
||||
const mouseEventInit = {
|
||||
clientX: ( x * element.offsetWidth ) + element.offsetLeft,
|
||||
clientY: ( y * element.offsetHeight ) + element.offsetTop,
|
||||
view: element.ownerDocument.defaultView
|
||||
};
|
||||
|
||||
// TODO: Find out why this is added. Keep commented out when this file is updated
|
||||
// window.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
x = x * rect.width + rect.left;
|
||||
y = y * rect.height + rect.top;
|
||||
|
||||
function traverse( element ) {
|
||||
|
||||
if ( element.nodeType !== Node.TEXT_NODE && element.nodeType !== Node.COMMENT_NODE ) {
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
if ( x > rect.left && x < rect.right && y > rect.top && y < rect.bottom ) {
|
||||
|
||||
element.dispatchEvent( new MouseEvent( event, mouseEventInit ) );
|
||||
|
||||
if ( element instanceof HTMLInputElement && element.type === 'range' && ( event === 'mousedown' || event === 'click' ) ) {
|
||||
|
||||
const [ min, max ] = [ 'min', 'max' ].map( property => parseFloat( element[ property ] ) );
|
||||
|
||||
const width = rect.width;
|
||||
const offsetX = x - rect.x;
|
||||
const proportion = offsetX / width;
|
||||
element.value = min + ( max - min ) * proportion;
|
||||
element.dispatchEvent( new InputEvent( 'input', { bubbles: true } ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for ( let i = 0; i < element.childNodes.length; i ++ ) {
|
||||
|
||||
traverse( element.childNodes[ i ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
traverse( element );
|
||||
|
||||
}
|
||||
|
||||
/* jshint esversion: 9, -W097 */
|
||||
|
||||
const schemaHTML = {
|
||||
html: {
|
||||
type: 'selector',
|
||||
},
|
||||
cursor: {
|
||||
type: 'selector',
|
||||
}
|
||||
};
|
||||
|
||||
{
|
||||
schemaHTML.html.description = `HTML element to use.`;
|
||||
schemaHTML.cursor.description = `Visual indicator for where the user is currently pointing`;
|
||||
}
|
||||
|
||||
const _pointer = new THREE.Vector2();
|
||||
const _event = { type: '', data: _pointer };
|
||||
AFRAME.registerComponent('html', {
|
||||
schema: schemaHTML,
|
||||
init() {
|
||||
this.rerender = this.rerender.bind(this);
|
||||
this.handle = this.handle.bind(this);
|
||||
this.onClick = e => this.handle('click', e);
|
||||
this.onMouseLeave = e => this.handle('mouseleave', e);
|
||||
this.onMouseEnter = e => this.handle('mouseenter', e);
|
||||
this.onMouseUp = e => this.handle('mouseup', e);
|
||||
this.onMouseDown = e => this.handle('mousedown', e);
|
||||
this.mouseMoveDetail = {
|
||||
detail: {
|
||||
cursorEl: null,
|
||||
intersection: null
|
||||
}
|
||||
};
|
||||
},
|
||||
play() {
|
||||
this.el.addEventListener('click', this.onClick);
|
||||
this.el.addEventListener('mouseleave', this.onMouseLeave);
|
||||
this.el.addEventListener('mouseenter', this.onMouseEnter);
|
||||
this.el.addEventListener('mouseup', this.onMouseUp);
|
||||
this.el.addEventListener('mousedown', this.onMouseDown);
|
||||
},
|
||||
pause() {
|
||||
this.el.removeEventListener('click', this.onClick);
|
||||
this.el.removeEventListener('mouseleave', this.onMouseLeave);
|
||||
this.el.removeEventListener('mouseenter', this.onMouseEnter);
|
||||
this.el.removeEventListener('mouseup', this.onMouseUp);
|
||||
this.el.removeEventListener('mousedown', this.onMouseDown);
|
||||
},
|
||||
update() {
|
||||
this.remove();
|
||||
if (!this.data.html) return;
|
||||
const mesh = new HTMLMesh(this.data.html);
|
||||
this.el.setObject3D('html', mesh);
|
||||
this.data.html.addEventListener('input', this.rerender);
|
||||
this.data.html.addEventListener('change', this.rerender);
|
||||
this.cursor = this.data.cursor ? this.data.cursor.object3D : null;
|
||||
},
|
||||
tick() {
|
||||
if (this.activeRaycaster) {
|
||||
const intersection = this.activeRaycaster.components.raycaster.getIntersection(this.el);
|
||||
this.mouseMoveDetail.detail.cursorEl = this.activeRaycaster;
|
||||
this.mouseMoveDetail.detail.intersection = intersection;
|
||||
this.handle('mousemove', this.mouseMoveDetail);
|
||||
}
|
||||
},
|
||||
handle(type, evt) {
|
||||
const intersection = evt.detail.intersection;
|
||||
const raycaster = evt.detail.cursorEl;
|
||||
if (type === 'mouseenter') {
|
||||
this.activeRaycaster = raycaster;
|
||||
}
|
||||
if (type === 'mouseleave' && this.activeRaycaster === raycaster) {
|
||||
this.activeRaycaster = null;
|
||||
}
|
||||
if (this.cursor) this.cursor.visible = false;
|
||||
if (intersection) {
|
||||
const mesh = this.el.getObject3D('html');
|
||||
const uv = intersection.uv;
|
||||
_event.type = type;
|
||||
_event.data.set( uv.x, 1 - uv.y );
|
||||
mesh.dispatchEvent( _event );
|
||||
|
||||
if (this.cursor) {
|
||||
this.cursor.visible = true;
|
||||
this.cursor.parent.worldToLocal(this.cursor.position.copy(intersection.point));
|
||||
}
|
||||
}
|
||||
},
|
||||
rerender() {
|
||||
const mesh = this.el.getObject3D('html');
|
||||
if (mesh && !mesh.material.map.scheduleUpdate) {
|
||||
mesh.material.map.scheduleUpdate = setTimeout( () => mesh.material.map.update(), 16 );
|
||||
}
|
||||
},
|
||||
remove() {
|
||||
const mesh = this.el.getObject3D('html');
|
||||
if (mesh) {
|
||||
this.el.removeObject3D('html');
|
||||
this.data.html.removeEventListener('input', this.rerender);
|
||||
this.data.html.removeEventListener('change', this.rerender);
|
||||
mesh.dispose();
|
||||
}
|
||||
this.activeRaycaster = null;
|
||||
this.mouseMoveDetail.detail.cursorEl = null;
|
||||
this.mouseMoveDetail.detail.intersection = null;
|
||||
this.cursor = null;
|
||||
},
|
||||
});
|
||||
|
||||
})(THREE);
|
||||
//# sourceMappingURL=aframe-html.js.map
|
|
@ -17,7 +17,9 @@ window.AFRAME.registerComponent('pinch-to-teleport', {
|
|||
pos.x += direction.x
|
||||
pos.z += direction.z
|
||||
// set the new position
|
||||
this.data.rig.setAttribute("position", pos);
|
||||
if( !this.data.rig ){
|
||||
this.data.rig.setAttribute("position", pos);
|
||||
}
|
||||
// !!! NOTE - it would be more efficient to do the
|
||||
// position change on the players THREE.Object:
|
||||
// `player.object3D.position.add(direction)`
|
|
@ -2,19 +2,23 @@
|
|||
|
||||
AFRAME.registerComponent('pressable', {
|
||||
schema: {
|
||||
pressDistance: {
|
||||
default: 0.01
|
||||
}
|
||||
pressDistance: { default: 0.005 },
|
||||
pressDuration: { default: 300 },
|
||||
immersiveOnly: { default: true }
|
||||
},
|
||||
init: function() {
|
||||
this.worldPosition = new THREE.Vector3();
|
||||
this.fingerWorldPosition = new THREE.Vector3();
|
||||
this.raycaster = new THREE.Raycaster()
|
||||
this.handEls = document.querySelectorAll('[hand-tracking-controls]');
|
||||
this.pressed = false;
|
||||
this.distance = -1
|
||||
// we throttle by distance, to support scenes with loads of clickable objects (far away)
|
||||
this.tick = this.throttleByDistance( () => this.detectPress() )
|
||||
this.el.addEventListener("raycaster-intersected", (e) => {
|
||||
if( !this.data || (this.data.immersiveOnly && !this.el.sceneEl.renderer.xr.isPresenting) ) return
|
||||
this.el.emit('click', e.detail )
|
||||
})
|
||||
|
||||
},
|
||||
throttleByDistance: function(f){
|
||||
return function(){
|
||||
|
@ -30,43 +34,48 @@ AFRAME.registerComponent('pressable', {
|
|||
}
|
||||
},
|
||||
detectPress: function(){
|
||||
if( this.handEls.length == 0 ){
|
||||
this.handEls = document.querySelectorAll('[hand-tracking-controls]');
|
||||
}
|
||||
var handEls = this.handEls;
|
||||
var handEl;
|
||||
let minDistance = 5
|
||||
|
||||
// compensate for xrf-get AFRAME component (which references non-reparented buffergeometries from the 3D model)
|
||||
let object3D = this.el.object3D.child || this.el.object3D
|
||||
// compensate for an object inside a group
|
||||
let object3D = this.el.object3D.type == "Group" ? this.el.object3D.children[0] : this.el.object3D
|
||||
if( !object3D ) return
|
||||
|
||||
for (var i = 0; i < handEls.length; i++) {
|
||||
handEl = handEls[i];
|
||||
let indexTipPosition = handEl.components['hand-tracking-controls'].indexTipPosition
|
||||
// Apply the relative position to the parent's world position
|
||||
handEl.object3D.updateMatrixWorld();
|
||||
handEl.object3D.getWorldPosition( this.fingerWorldPosition )
|
||||
this.fingerWorldPosition.add( indexTipPosition )
|
||||
let indexTip = handEl.object3D.getObjectByName('index-finger-tip')
|
||||
if( ! indexTip ) return // nothing to do here
|
||||
|
||||
this.raycaster.far = this.data.pressDistance
|
||||
// Create a direction vector (doesnt matter because it is supershort for 'touch' purposes)
|
||||
const direction = new THREE.Vector3(1.0,0,0);
|
||||
this.raycaster.set(this.fingerWorldPosition, direction)
|
||||
|
||||
// Create a direction vector to negative Z
|
||||
const direction = new THREE.Vector3(0,0,-1.0);
|
||||
direction.normalize()
|
||||
this.raycaster.set(indexTip.position, direction)
|
||||
intersects = this.raycaster.intersectObjects([object3D],true)
|
||||
|
||||
object3D.getWorldPosition(this.worldPosition)
|
||||
|
||||
distance = this.fingerWorldPosition.distanceTo(this.worldPosition)
|
||||
distance = indexTip.position.distanceTo(this.worldPosition)
|
||||
minDistance = distance < minDistance ? distance : minDistance
|
||||
|
||||
if (intersects.length ){
|
||||
this.i = this.i || 0;
|
||||
if( !this.pressed ){
|
||||
this.el.emit('pressedstarted');
|
||||
this.el.emit('click');
|
||||
this.el.emit('pressedstarted', intersects);
|
||||
this.el.emit('click', intersects);
|
||||
this.pressed = setTimeout( () => {
|
||||
this.el.emit('pressedended');
|
||||
this.el.emit('pressedended', intersects);
|
||||
this.pressed = null
|
||||
},300)
|
||||
}, this.data.pressDuration )
|
||||
}
|
||||
}
|
||||
}
|
||||
this.distance = minDistance
|
||||
}
|
||||
},
|
||||
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* ## [require](com/require('').js)
|
||||
*
|
||||
* automatically requires dependencies
|
||||
* automatically requires dependencies or missing components
|
||||
*
|
||||
* ```javascript
|
||||
* await AFRAME.utils.require( this.dependencies ) (*) autoload missing components
|
||||
|
@ -74,3 +74,38 @@ AFRAME.utils.require = function(arr_or_obj,opts){
|
|||
AFRAME.utils.require.required = {}
|
||||
|
||||
AFRAME.utils.require.baseURL = './com/'
|
||||
|
||||
|
||||
//// this component will scan the DOM for missing components and lazy load them
|
||||
//AFRAME.registerSystem('require',{
|
||||
//
|
||||
// init: function(){
|
||||
// this.components = []
|
||||
// // observe HTML changes in <a-scene>
|
||||
// observer = new MutationObserver( (a,b) => this.getMissingComponents(a,b) )
|
||||
// observer.observe( this.sceneEl, {characterData: false, childList: true, attributes: false});
|
||||
// },
|
||||
//
|
||||
// getMissingComponents: function(mutationsList,observer){
|
||||
// let els = [...this.sceneEl.getElementsByTagName("*")]
|
||||
// let seen = []
|
||||
//
|
||||
// els.map( async (el) => {
|
||||
// let attrs = el.getAttributeNames()
|
||||
// .filter( (a) => a.match(/(^aframe-injected|^data-aframe|^id$|^class$|^on)/) ? null : a )
|
||||
// for( let attr in attrs ){
|
||||
// let component = attrs[attr]
|
||||
// if( el.components && !el.components[component] ){
|
||||
// console.info(`require.js: lazy-loading missing <${el.tagName.toLowerCase()} ${component} ... > (TODO: fix selectors in schema)`)
|
||||
// // require && remount
|
||||
// try{
|
||||
// await AFRAME.utils.require([component])
|
||||
// el.removeAttribute(component)
|
||||
// el.setAttribute(component, el.getAttribute(component) )
|
||||
// }catch(e){ } // give up, normal AFRAME behaviour follows
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
//
|
||||
//})
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
AFRAME.registerComponent('wearable', {
|
||||
schema:{
|
||||
el: {type:"selector"},
|
||||
controlPos: {type:"vec3"},
|
||||
controlRot: {type:"vec3"},
|
||||
handPos: {type:"vec3"},
|
||||
handRot: {type:"vec3"}
|
||||
},
|
||||
init: function(){
|
||||
this.remember()
|
||||
|
||||
if( !this.data.el ) return console.warn(`wear.js: cannot find ${this.data.el}`)
|
||||
this.data.el.object3D.name = 'wearable'
|
||||
|
||||
// hand vs controller multi attach-heuristics (intended to survived AFRAME updates)
|
||||
this.data.el.addEventListener('controllermodelready', this.attachWhatEverController.bind(this) ) // downside: no model yet
|
||||
this.data.el.addEventListener('model-loaded', this.attachWhatEverController.bind(this) ) // downside: only called once [model not added yet]
|
||||
this.el.sceneEl.addEventListener('controllersupdated', this.attachWhatEverController.bind(this) ) // downside: only called when switching [no model yet]
|
||||
},
|
||||
|
||||
attachWhatEverController: function(e){
|
||||
setTimeout( () => { // needed because the events are called before the model was added via add())
|
||||
let wrist = false
|
||||
let hand = this.data.el.components['hand-tracking-controls']
|
||||
if( hand && hand.controllerPresent ) wrist = hand.wristObject3D
|
||||
this.attach( wrist || this.data.el.object3D)
|
||||
this.update( wrist ? 'hand' : 'control')
|
||||
},100)
|
||||
},
|
||||
|
||||
attach: function(target){
|
||||
if( this.target && target.uuid == this.target.uuid ) return// already attached
|
||||
target.add(this.el.object3D )
|
||||
this.target = target
|
||||
},
|
||||
|
||||
detach: function(){
|
||||
this.parent.add(this.el.object3D)
|
||||
this.el.object3D.position.copy( this.position )
|
||||
this.el.object3D.rotation.copy( this.rotation )
|
||||
},
|
||||
|
||||
remember: function(){
|
||||
this.position = this.el.object3D.position.clone()
|
||||
this.rotation = this.el.object3D.rotation.clone()
|
||||
this.parent = this.el.object3D.parent
|
||||
},
|
||||
|
||||
update: function(type){
|
||||
let position = type == 'hand' ? this.data.handPos : this.data.controlPos
|
||||
let rotation = type == 'hand' ? this.data.handRot : this.data.controlRot
|
||||
this.el.object3D.position.copy( position )
|
||||
this.el.object3D.rotation.set( rotation.x, rotation.y, rotation.z )
|
||||
}
|
||||
|
||||
})
|
|
@ -37,6 +37,7 @@ AFRAME.registerComponent('window', {
|
|||
x: {type:'string',"default":"center"},
|
||||
y: {type:'string',"default":"center"},
|
||||
"class": {type:'array',"default":[]},
|
||||
autoresize:{type:'bool', "default": false}
|
||||
},
|
||||
|
||||
dependencies:{
|
||||
|
@ -84,7 +85,7 @@ AFRAME.registerComponent('window', {
|
|||
this.el.emit('window.oncreate',{})
|
||||
// resize after the dom content has been rendered & updated
|
||||
setTimeout( () => {
|
||||
if( !this.data.max ) winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
|
||||
if( !this.data.max && this.data.autoresize ) winbox.resize( this.el.dom.offsetWidth+'px', this.el.dom.offsetHeight+'px' )
|
||||
// hint grabbable's obb-collider to track the window-object
|
||||
this.el.components['obb-collider'].data.trackedObject3D = 'components.html.el.object3D.children.0'
|
||||
this.el.components['obb-collider'].update()
|
||||
|
|
Loading…
Reference in New Issue