moved over files [from xrfragment-haxe repo]
This commit is contained in:
commit
b9c9cc6ea7
49 changed files with 905401 additions and 0 deletions
1
.domains
Normal file
1
.domains
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
xrfragment.org
|
||||||
32
.env
Normal file
32
.env
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
which nix && test -z "$NIX_SHELL_XRF" && {
|
||||||
|
|
||||||
|
# automatically mirror main between forgejo<->codeberg
|
||||||
|
#git(){
|
||||||
|
# set -x
|
||||||
|
# test $1 = "push" && test $3 = main && mirror=1
|
||||||
|
# $(which git) "$@"
|
||||||
|
# test -n "$mirror" && {
|
||||||
|
# set -x
|
||||||
|
# shift ; shift # remove first to args
|
||||||
|
# $(which git) push codeberg "$@"
|
||||||
|
# }
|
||||||
|
# set +x
|
||||||
|
#}
|
||||||
|
|
||||||
|
echo '[i] installing nix-shell' && nix-shell
|
||||||
|
}
|
||||||
|
|
||||||
|
test "$GITEA_ROOT_URL" = "https://forgejo.isvery.ninja/" && {
|
||||||
|
# on the website xrfragment.org copy examples to root-dir
|
||||||
|
# (so https://xrfragment.org/index.glb can be requested remotely)
|
||||||
|
# because that really emphasizes a nice WebXR experience-at-website-root paradigm
|
||||||
|
wget "https://raw.codeberg.page/coderofsalvation/xrfragment-haxe/example/assets/index.glb"
|
||||||
|
test -d dist && rm -rf dist
|
||||||
|
mkdir dist
|
||||||
|
wget "https://raw.codeberg.page/coderofsalvation/xrfragment-haxe/dist/xrfragment.aframe.all.js"
|
||||||
|
wget "https://raw.codeberg.page/coderofsalvation/xrfragment-haxe/dist/xrfragment.aframe.js"
|
||||||
|
wget "https://raw.codeberg.page/coderofsalvation/xrfragment-haxe/dist/xrfragment.js"
|
||||||
|
wget "https://raw.codeberg.page/coderofsalvation/xrfragment-haxe/dist/xrfragment.module.js"
|
||||||
|
wget "https://raw.codeberg.page/coderofsalvation/xrfragment-haxe/dist/xrfragment.three.js"
|
||||||
|
wget "https://raw.codeberg.page/coderofsalvation/xrfragment-haxe/dist/xrfragment.three.module.js"
|
||||||
|
}
|
||||||
BIN
assets/library/assets.webp
Normal file
BIN
assets/library/assets.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
61
assets/library/datapackage.json
Normal file
61
assets/library/datapackage.json
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://manyfold.app/profiles/0.0/datapackage.json",
|
||||||
|
"name": "xr-fragments-assets",
|
||||||
|
"title": "XR Fragments assets",
|
||||||
|
"homepage": "http://xrfragment.org",
|
||||||
|
"image": "assets.webp",
|
||||||
|
"keywords": [
|
||||||
|
"!new",
|
||||||
|
"fontmap"
|
||||||
|
],
|
||||||
|
"licenses": [
|
||||||
|
{
|
||||||
|
"name": "CC-BY-SA-4.0",
|
||||||
|
"path": "https://spdx.org/licenses/CC-BY-SA-4.0.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"name": "assets",
|
||||||
|
"path": "assets.webp",
|
||||||
|
"mediatype": "image/webp",
|
||||||
|
"up": "+z",
|
||||||
|
"presupported": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fontmap",
|
||||||
|
"path": "fontmap.glb",
|
||||||
|
"mediatype": "model/gltf",
|
||||||
|
"up": "+z",
|
||||||
|
"presupported": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tree",
|
||||||
|
"path": "tree.glb",
|
||||||
|
"mediatype": "model/gltf",
|
||||||
|
"up": "+z",
|
||||||
|
"presupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sensitive": false,
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"title": "xrfragments",
|
||||||
|
"path": "http://localhost:3214/creators/xrfragments",
|
||||||
|
"roles": [
|
||||||
|
"creator"
|
||||||
|
],
|
||||||
|
"caption": "",
|
||||||
|
"description": "XR Fragments is an open specification for hyperlinking & deeplinking 3D fileformats .\nTurn 3D files into linkable AR/VR websites .\n3D files with XR Fragments enable interoperable, networkable and interactions via so-called extras and promote URL standards .",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"path": "https://xrfragment.org"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collections": [
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
assets/library/fontmap.glb
Normal file
BIN
assets/library/fontmap.glb
Normal file
Binary file not shown.
BIN
assets/library/glass.glb
Normal file
BIN
assets/library/glass.glb
Normal file
Binary file not shown.
BIN
assets/library/lakes.glb
Normal file
BIN
assets/library/lakes.glb
Normal file
Binary file not shown.
BIN
assets/library/tree.glb
Normal file
BIN
assets/library/tree.glb
Normal file
Binary file not shown.
54
assets/template/datapackage.json
Normal file
54
assets/template/datapackage.json
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://manyfold.app/profiles/0.0/datapackage.json",
|
||||||
|
"name": "xr-fragments-template-website",
|
||||||
|
"title": "Spatial website template",
|
||||||
|
"homepage": "http://xrfragment.org",
|
||||||
|
"image": "website.jpg",
|
||||||
|
"keywords": [
|
||||||
|
"!new",
|
||||||
|
"template"
|
||||||
|
],
|
||||||
|
"licenses": [
|
||||||
|
{
|
||||||
|
"name": "CC-BY-SA-4.0",
|
||||||
|
"path": "https://spdx.org/licenses/CC-BY-SA-4.0.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
{
|
||||||
|
"name": "website",
|
||||||
|
"path": "website.jpg",
|
||||||
|
"mediatype": "image/jpeg",
|
||||||
|
"up": "+z",
|
||||||
|
"presupported": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "website",
|
||||||
|
"path": "website.glb",
|
||||||
|
"mediatype": "model/gltf",
|
||||||
|
"up": "+z",
|
||||||
|
"presupported": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sensitive": false,
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"title": "xrfragments",
|
||||||
|
"path": "http://localhost:3214/creators/xrfragments",
|
||||||
|
"roles": [
|
||||||
|
"creator"
|
||||||
|
],
|
||||||
|
"caption": "",
|
||||||
|
"description": "XR Fragments is an open specification for hyperlinking & deeplinking 3D fileformats .\nTurn 3D files into linkable AR/VR websites .\n3D files with XR Fragments enable interoperable, networkable and interactions via so-called extras and promote URL standards .",
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"path": "https://xrfragment.org"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"collections": [
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
assets/template/explainer.xcf
Normal file
BIN
assets/template/explainer.xcf
Normal file
Binary file not shown.
BIN
assets/template/website.glb
Normal file
BIN
assets/template/website.glb
Normal file
Binary file not shown.
BIN
assets/template/website.jpg
Normal file
BIN
assets/template/website.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
7703
dist/xrfragment.aframe.all.js
vendored
Normal file
7703
dist/xrfragment.aframe.all.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7701
dist/xrfragment.aframe.js
vendored
Normal file
7701
dist/xrfragment.aframe.js
vendored
Normal file
File diff suppressed because one or more lines are too long
3241
dist/xrfragment.js
vendored
Normal file
3241
dist/xrfragment.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
3161
dist/xrfragment.lua
vendored
Normal file
3161
dist/xrfragment.lua
vendored
Normal file
File diff suppressed because it is too large
Load diff
7798
dist/xrfragment.model-viewer.js
vendored
Normal file
7798
dist/xrfragment.model-viewer.js
vendored
Normal file
File diff suppressed because one or more lines are too long
807912
dist/xrfragment.module.js
vendored
Normal file
807912
dist/xrfragment.module.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
268
dist/xrfragment.plugin.editor.js
vendored
Normal file
268
dist/xrfragment.plugin.editor.js
vendored
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
/*
|
||||||
|
* v0.5.1 generated at Thu May 22 07:30:49 PM CEST 2025
|
||||||
|
* https://xrfragment.org
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
// reactive component for displaying the menu
|
||||||
|
|
||||||
|
$editorPopup = (el) => new Proxy({
|
||||||
|
|
||||||
|
html: (opts) => `
|
||||||
|
<div>
|
||||||
|
<b>#${$editor.selected.name}</b>
|
||||||
|
<table class="editorPopup">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><b class="badge">href</a></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="href" placeholder="https://foo.com" maxlength="255" list="objects"
|
||||||
|
onkeydown="document.querySelector('#editActions').classList.add('show')"
|
||||||
|
onkeyup="$editor.selected.edited = $editor.selected.userData.href = this.value"
|
||||||
|
value="${$editor.selected.userData.href||''}" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b class="badge">src</a></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="src" placeholder="https://foo.com" maxlength="255" list="objects"
|
||||||
|
onkeydown="document.querySelector('#editActions').classList.add('show')"
|
||||||
|
onkeyup="$editor.selected.edited = $editor.selected.userData.src = this.value"
|
||||||
|
value="${$editor.selected.userData.src||''}" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><b class="badge">tag</a></td>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="tag" placeholder="foo bar" maxlength="255"
|
||||||
|
onkeydown="document.querySelector('#editActions').classList.add('show')"
|
||||||
|
onkeyup="$editor.selected.edited = $editor.selected.userData.tag = this.value"
|
||||||
|
value="${$editor.selected.userData.tag||''}" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<datalist id="objects">
|
||||||
|
<option>https://xrfragment.org/index.glb#pos=start</option>
|
||||||
|
<option>
|
||||||
|
${opts.objectNames.join('</option><option>')}
|
||||||
|
</option>
|
||||||
|
</datalist>
|
||||||
|
<br>
|
||||||
|
<div id="editActions">
|
||||||
|
<button class="download" onclick="$editor.export()"><i class="gg-software-download"></i> download scene file</button>
|
||||||
|
<br>
|
||||||
|
NOTE: updates to src-values will require reloading the scene
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<style type="text/css">
|
||||||
|
table.editorPopup input{
|
||||||
|
min-width:200px;
|
||||||
|
}
|
||||||
|
table.editorPopup tr td:nth-child(1){
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
#editActions{
|
||||||
|
visibility:hidden;
|
||||||
|
}
|
||||||
|
#editActions.show{
|
||||||
|
visibility:visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`,
|
||||||
|
|
||||||
|
init(opts){
|
||||||
|
el.innerHTML = this.html(opts)
|
||||||
|
return (this.el = el)
|
||||||
|
},
|
||||||
|
|
||||||
|
},{
|
||||||
|
|
||||||
|
get(me,k,v){ return me[k] },
|
||||||
|
|
||||||
|
set(me,k,v){
|
||||||
|
me[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
$editor = (el,opts) => new Proxy({
|
||||||
|
|
||||||
|
html: `
|
||||||
|
<div style="position:absolute; width:100%; text-align:right; right:166px;">
|
||||||
|
<button class="btn edit-btn">
|
||||||
|
<i class="gg-pen"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<style type="text/css">
|
||||||
|
.xrf button.edit-btn{
|
||||||
|
height: 32px;
|
||||||
|
width: 30px;
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
.edit-btn.enabled,
|
||||||
|
.edit-btn.enabled:hover{
|
||||||
|
background:black;
|
||||||
|
}
|
||||||
|
.edit-btn i.gg-pen{
|
||||||
|
margin-top: -26px;
|
||||||
|
margin-left: 4px;
|
||||||
|
width: 10px;
|
||||||
|
color: var(--xrf-white);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`,
|
||||||
|
|
||||||
|
selecting: false,
|
||||||
|
editing: false,
|
||||||
|
helper: null,
|
||||||
|
selected: null,
|
||||||
|
objectNames: [],
|
||||||
|
|
||||||
|
init(opts){
|
||||||
|
el.innerHTML = this.html
|
||||||
|
window.frontend.el.querySelector('#topbar').appendChild(el);
|
||||||
|
el.querySelector('.edit-btn').addEventListener('click', () => {
|
||||||
|
if( $editor.selecting || $editor.editing ) this.reset()
|
||||||
|
else{
|
||||||
|
$editor.selecting = true
|
||||||
|
$editor.editing = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
xrf.addEventListener('export', (e) => this.updateOriginalScene(e) )
|
||||||
|
xrf.addEventListener('href', (opts) => {
|
||||||
|
if( $editor.selecting || $editor.editing ) return opts.promise().reject("$editor should block hrefs while editing") // never resolve (block hrefs from interfering)
|
||||||
|
})
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
reset(){
|
||||||
|
if( this.helper) xrf.scene.remove(this.helper)
|
||||||
|
$editor.selecting = false
|
||||||
|
$editor.editing = false
|
||||||
|
},
|
||||||
|
|
||||||
|
export(){
|
||||||
|
window.frontend.download()
|
||||||
|
this.reset()
|
||||||
|
},
|
||||||
|
|
||||||
|
editNode(){
|
||||||
|
if( !this.selecting ) return console.log("not editing")
|
||||||
|
this.reset()
|
||||||
|
$editor.editing = true
|
||||||
|
this.collectObjects()
|
||||||
|
//`<b>XR Fragment:</b> #${this.selected.name}<br><br>${this.getMetaData(this.selected)}`),{
|
||||||
|
notify( $editorPopup( document.createElement('div') ).init(this) , {
|
||||||
|
timeout:false,
|
||||||
|
onclose: () => this.reset()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
collectObjects(){
|
||||||
|
this.objectNames = []
|
||||||
|
const escape = (str) => {
|
||||||
|
let d = document.createElement('div')
|
||||||
|
d.innerText = str
|
||||||
|
return d.innerHTML
|
||||||
|
}
|
||||||
|
xrf.scene.traverse( (n) => {
|
||||||
|
if( n.userData && n.userData.href ){
|
||||||
|
this.objectNames.push( escape(n.userData.href) )
|
||||||
|
}
|
||||||
|
})
|
||||||
|
xrf.scene.traverse( (n) => {
|
||||||
|
if( n.name ) this.objectNames.push( escape('#'+n.name) )
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
initEdit(scene){
|
||||||
|
if( !this.listenersInstalled ){
|
||||||
|
AFRAME.scenes[0].addEventListener('click', () => this.editNode() )
|
||||||
|
this.listenersInstalled = true
|
||||||
|
}
|
||||||
|
scene.traverse( (n) => {
|
||||||
|
let highlight = (n) => (e) => {
|
||||||
|
if( !this.selecting || this.editing ) return // do nothing
|
||||||
|
if( this.helper){
|
||||||
|
if( this.helper.selected == n.uuid ) return // already selected
|
||||||
|
xrf.scene.remove(this.helper)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selected = n
|
||||||
|
this.helper = new THREE.BoxHelper( n, 0xFF00FF )
|
||||||
|
this.helper.material.linewidth = 4
|
||||||
|
this.helper.material.color = xrf.focusLine.material.color
|
||||||
|
this.helper.material.dashSize = xrf.focusLine.material.dashSize
|
||||||
|
this.helper.material.gapSize = xrf.focusLine.material.gapSize
|
||||||
|
this.helper.selected = n.uuid
|
||||||
|
xrf.scene.add(this.helper)
|
||||||
|
|
||||||
|
let div = document.createElement('div')
|
||||||
|
notify(`<b>#${n.name}</b><br>${this.getMetaData(this.selected)}`)
|
||||||
|
|
||||||
|
}
|
||||||
|
if( n.material ) n.addEventListener('mousemove', n.highlightOnMouseMove = highlight(n) )
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getMetaData(n){
|
||||||
|
let html = `${n.userData.href ? `<b class="badge">href</b>${n.userData.href}<br>`:''}`
|
||||||
|
html += `${n.userData.src ? `<b class="badge">src</b>${n.userData.src}<br>` :''}`
|
||||||
|
html += `${n.userData.tag ? `<b class="badge">tag</b>${n.userData.tag}<br>` :''}`
|
||||||
|
return html
|
||||||
|
},
|
||||||
|
|
||||||
|
updateOriginalScene(e){
|
||||||
|
const {scene,ext} = e
|
||||||
|
scene.traverse( (n) => {
|
||||||
|
if( !n.name ) return
|
||||||
|
// overwrite node with modified userData from scene
|
||||||
|
let o = xrf.scene.getObjectByName(n.name)
|
||||||
|
if( o && o.edited ){
|
||||||
|
for( let i in o.userData ) n.userData[i] = o.userData[i]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
get(me,k,v){ return me[k] },
|
||||||
|
|
||||||
|
set(me,k,v){
|
||||||
|
me[k] = v
|
||||||
|
|
||||||
|
switch( k ){
|
||||||
|
|
||||||
|
case "selecting":{
|
||||||
|
lookctl = $('[look-controls]').components['look-controls']
|
||||||
|
if( v ){
|
||||||
|
lookctl.pause() // prevent click-conflict
|
||||||
|
notify("click an object to reveal XR Fragment metadata")
|
||||||
|
xrf.interactive.raycastAll = true
|
||||||
|
me.initEdit(xrf.scene)
|
||||||
|
lookctl.pause() // prevent click-conflict
|
||||||
|
el.querySelector('.edit-btn').classList.add(['enabled'])
|
||||||
|
}else{
|
||||||
|
lookctl.pause() // prevent click-conflict
|
||||||
|
xrf.scene.traverse( (n) => {
|
||||||
|
if( n.highlightOnMouseMove ){
|
||||||
|
n.removeEventListener( 'mousemove', n.highlightOnMouseMove )
|
||||||
|
}
|
||||||
|
})
|
||||||
|
lookctl.play() // prevent click-conflict (resume)
|
||||||
|
el.querySelector('.edit-btn').classList.remove(['enabled'])
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// reactify component!
|
||||||
|
document.addEventListener('frontend:ready', (e) => {
|
||||||
|
window.$editor = $editor( document.createElement('div') ).init(e.detail)
|
||||||
|
})
|
||||||
932
dist/xrfragment.plugin.frontend.css.js
vendored
Normal file
932
dist/xrfragment.plugin.frontend.css.js
vendored
Normal file
|
|
@ -0,0 +1,932 @@
|
||||||
|
/*
|
||||||
|
* v0.5.1 generated at Thu May 22 07:30:49 PM CEST 2025
|
||||||
|
* https://xrfragment.org
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
document.head.innerHTML += `
|
||||||
|
<style type="text/css">
|
||||||
|
:root {
|
||||||
|
--xrf-primary: #6839dc;
|
||||||
|
--xrf-primary-fg: #FFF;
|
||||||
|
--xrf-light-primary: #ea23cf;
|
||||||
|
--xrf-secondary: #872eff;
|
||||||
|
--xrf-light-xrf-secondary: #ce7df2;
|
||||||
|
--xrf-topbar-bg: #fffb;
|
||||||
|
--xrf-box-shadow: #0005;
|
||||||
|
--xrf-red: red;
|
||||||
|
--xrf-dark-gray: #343334;
|
||||||
|
--xrf-gray: #424280;
|
||||||
|
--xrf-white: #fdfdfd;
|
||||||
|
--xrf-light-gray: #efefef;
|
||||||
|
--xrf-lighter-gray: #e4e2fb96;
|
||||||
|
--xrf-font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
|
||||||
|
--xrf-font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
|
||||||
|
--xrf-font-size-0: 12px;
|
||||||
|
--xrf-font-size-1: 14px;
|
||||||
|
--xrf-font-size-2: 17px;
|
||||||
|
--xrf-font-size-3: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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}
|
||||||
|
|
||||||
|
.xrf table tr td{
|
||||||
|
vertical-align:top;
|
||||||
|
}
|
||||||
|
.xrf button,
|
||||||
|
.xrf input[type="submit"],
|
||||||
|
.xrf .btn {
|
||||||
|
text-decoration:none;
|
||||||
|
background: var(--xrf-primary);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 11px 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: 0.3s;
|
||||||
|
height: 40px;
|
||||||
|
font-size: var(--xrf-font-size-1);
|
||||||
|
color: var(--xrf-primary-fg);
|
||||||
|
line-height: var(--xrf-font-size-1);
|
||||||
|
cursor:pointer;
|
||||||
|
white-space:pre;
|
||||||
|
min-width: 45px;
|
||||||
|
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
|
||||||
|
display:inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xrf button:hover,
|
||||||
|
.xrf input[type="submit"]:hover,
|
||||||
|
.xrf .btn:hover {
|
||||||
|
background: var(--xrf-secondary);
|
||||||
|
text-decoration:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xrf, .xrf *{
|
||||||
|
font-family: var(--xrf-font-sans-serif);
|
||||||
|
font-size: var(--xrf-font-size-1);
|
||||||
|
line-height:27px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea, select, input[type="text"] {
|
||||||
|
background: transparent; /* linear-gradient( var(--xrf-lighter-gray), var(--xrf-gray) ) !important; */
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"] {
|
||||||
|
color: var(--xrf-light-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text]{
|
||||||
|
padding:7px 15px;
|
||||||
|
}
|
||||||
|
input{
|
||||||
|
border-radius:7px;
|
||||||
|
margin:5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
border-bottom: 2px solid var(--xrf-secondary);
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar{
|
||||||
|
background: var(--xrf-topbar-bg);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 48px;
|
||||||
|
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
|
||||||
|
opacity: 0.9;
|
||||||
|
z-index:2000;
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar .logo{
|
||||||
|
width: 92px;
|
||||||
|
position: absolute;
|
||||||
|
top: 9px;
|
||||||
|
left: 93px;
|
||||||
|
height: 30px;
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar > input[type="submit"] {
|
||||||
|
height: 32px;
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 2px;
|
||||||
|
min-width:135px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar > button#navback,
|
||||||
|
#topbar > button#navforward {
|
||||||
|
height: 32px;
|
||||||
|
font-size: 24px;
|
||||||
|
position: absolute;
|
||||||
|
left: 9px;
|
||||||
|
padding: 2px 13px;
|
||||||
|
border-radius:6px;
|
||||||
|
top: 8px;
|
||||||
|
color: var(--xrf-light-gray);
|
||||||
|
width: 36px;
|
||||||
|
min-width: unset;
|
||||||
|
}
|
||||||
|
#topbar > button#navforward {
|
||||||
|
left:49px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#topbar > #uri {
|
||||||
|
height: 18px;
|
||||||
|
font-size: var(--xrf-font-size-3);
|
||||||
|
position: absolute;
|
||||||
|
left: 200px;
|
||||||
|
top: 9px;
|
||||||
|
max-width: 550px;
|
||||||
|
padding: 5px 0px 5px 5px;
|
||||||
|
width: calc( 63% - 200px);
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-color: #Ccc;
|
||||||
|
border: 2px solid #CCC;
|
||||||
|
border-radius: 7px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.footer > .menu .btn{
|
||||||
|
display:inline-block;
|
||||||
|
background: var(--xrf-primary);
|
||||||
|
border-radius: 25px;
|
||||||
|
border: 0;
|
||||||
|
padding: 5px 19px;
|
||||||
|
font-weight: 1000;
|
||||||
|
font-family: sans-serif;
|
||||||
|
font-size: var(--xrf-font-size-2);
|
||||||
|
color:var(--xrf-primary-fg);
|
||||||
|
height:33px;
|
||||||
|
z-index:2000;
|
||||||
|
cursor:pointer;
|
||||||
|
min-width:145px;
|
||||||
|
text-decoration:none;
|
||||||
|
margin-top: 15px;
|
||||||
|
line-height:36px;
|
||||||
|
margin-right:10px;
|
||||||
|
text-align:left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xrf a.btn#more{
|
||||||
|
z-index:3000;
|
||||||
|
width: 19px;
|
||||||
|
min-width: 19px;
|
||||||
|
font-size:16px;
|
||||||
|
text-align: center;
|
||||||
|
background:white;
|
||||||
|
color: var(--xrf-primary);
|
||||||
|
}
|
||||||
|
.xrf a.btn#more i.gg-menu{
|
||||||
|
margin-top:15px;
|
||||||
|
}
|
||||||
|
.xrf a.btn#more i.gg-close,
|
||||||
|
.xrf a.btn#more i.gg-menu{
|
||||||
|
color:#888;
|
||||||
|
}
|
||||||
|
.xrf a.btn#meeting i.gg-user-add{
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xrf a.btn#share i.gg-link{
|
||||||
|
margin-right:24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xrf a.btn#accessibility i.gg-yinyang{
|
||||||
|
margin-right:13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html{
|
||||||
|
max-width:unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.render {
|
||||||
|
position:absolute;
|
||||||
|
top:0;
|
||||||
|
left:0;
|
||||||
|
right:0;
|
||||||
|
bottom:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lil-gui.autoPlace{
|
||||||
|
right:0px !important;
|
||||||
|
top:48px !important;
|
||||||
|
height:33vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
#VRButton {
|
||||||
|
margin-bottom:20vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 450px) {
|
||||||
|
#uri{ display:none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.lil-gui.root{
|
||||||
|
top:auto !important;
|
||||||
|
left:auto !important;
|
||||||
|
}
|
||||||
|
.js-snackbar__message{
|
||||||
|
overflow-y:auto;
|
||||||
|
max-height:600px;
|
||||||
|
}
|
||||||
|
.js-snackbar__message h1,h2,h3{
|
||||||
|
font-size:22px;
|
||||||
|
}
|
||||||
|
.xrf table tr td {
|
||||||
|
|
||||||
|
}
|
||||||
|
:root{
|
||||||
|
--xrf-font-size-1: 13px;
|
||||||
|
--xrf-font-size-2: 17px;
|
||||||
|
--xrf-font-size-3: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.a-enter-vr-button, .a-enter-ar-button{
|
||||||
|
height:41px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#qrcode{
|
||||||
|
background: transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 121px;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
input#share{
|
||||||
|
font-size: var(--xrf-font-size-1);
|
||||||
|
font-family: var(--xrf-font-monospace);
|
||||||
|
border:2px solid #AAA;
|
||||||
|
width:50vw;
|
||||||
|
max-width:400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
z-index:1000;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse; /* This reverses the stacking order of the flex container */
|
||||||
|
align-items: flex-end;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 71px;
|
||||||
|
right: 11px;
|
||||||
|
bottom: 0;
|
||||||
|
padding-bottom:140px;
|
||||||
|
box-sizing:border-box;
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
.footer *{
|
||||||
|
pointer-events:all;
|
||||||
|
}
|
||||||
|
.footer .menu{
|
||||||
|
text-align:right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge,
|
||||||
|
#messages .msg .badge{
|
||||||
|
box-sizing:border-box;
|
||||||
|
display:inline-block;
|
||||||
|
color: var(--xrf-white);
|
||||||
|
font-weight: bold;
|
||||||
|
background: var(--xrf-dark-gray);
|
||||||
|
border-radius:7px;
|
||||||
|
padding:3px 12px;
|
||||||
|
font-size: var(--xrf-font-size-0);
|
||||||
|
margin-right:10px;
|
||||||
|
text-decoration:none !important;
|
||||||
|
}
|
||||||
|
#messages .msg.ui div.badge a{
|
||||||
|
color:#FFF;
|
||||||
|
}
|
||||||
|
#messages .msg .badge{
|
||||||
|
display:inline;
|
||||||
|
color: var(--xrf-primary-fg);
|
||||||
|
background: var(--xrf-dark-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ruler{
|
||||||
|
width:97%;
|
||||||
|
margin:7px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
a.badge {
|
||||||
|
text-decoration:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xrf select{
|
||||||
|
border-inline: none;
|
||||||
|
border-inline: none;
|
||||||
|
border-block: none;
|
||||||
|
border: 3px solid var(--xrf-primary);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: none;
|
||||||
|
border-radius:30px;
|
||||||
|
}
|
||||||
|
.xrf select,
|
||||||
|
.xrf option{
|
||||||
|
padding: 0px 16px;
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: 150px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xrf input{
|
||||||
|
border-radius:30px;
|
||||||
|
padding: 7px 15px;
|
||||||
|
border-block: none;
|
||||||
|
border-inline: none;
|
||||||
|
border: 1px solid #888;
|
||||||
|
background: transparent;
|
||||||
|
max-width:105px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xrf table tr td {
|
||||||
|
vertical-align:middle;
|
||||||
|
text-align:right;
|
||||||
|
}
|
||||||
|
.xrf table tr td:nth-child(1){
|
||||||
|
min-width:82px;
|
||||||
|
height:40px;
|
||||||
|
padding-right:15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.xrf small{
|
||||||
|
font-size: var(--xrf-font-size-0);
|
||||||
|
}
|
||||||
|
.disabled{
|
||||||
|
opacity:0.5
|
||||||
|
}
|
||||||
|
|
||||||
|
body.menu .js-snackbar__wrapper,
|
||||||
|
body.topbar .js-snackbar__wrapper {
|
||||||
|
transform: translateY(40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript{
|
||||||
|
max-height:132px;
|
||||||
|
width:100%;
|
||||||
|
overflow-y:auto;
|
||||||
|
border: 1px solid var(--xrf-gray);
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right { float:right }
|
||||||
|
.left { float:left }
|
||||||
|
|
||||||
|
/*
|
||||||
|
* tabs
|
||||||
|
*/
|
||||||
|
div.tab-frame > input{ display:none;}
|
||||||
|
div.tab-frame > label{ display:block; float:left;padding:5px 10px; cursor:pointer; }
|
||||||
|
div.tab-frame > input:checked + label{ cursor:default; border-bottom:1px solid #888; font-weight:bold; }
|
||||||
|
div.tab-frame > div.tab{ display:none; padding:15px 10px 5px 10px;clear:left}
|
||||||
|
|
||||||
|
div.tab-frame > input:nth-of-type(1):checked ~ .tab:nth-of-type(1),
|
||||||
|
div.tab-frame > input:nth-of-type(2):checked ~ .tab:nth-of-type(2),
|
||||||
|
div.tab-frame > input:nth-of-type(3):checked ~ .tab:nth-of-type(3){ display:block;}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* joystick.js controller
|
||||||
|
*/
|
||||||
|
.controller {
|
||||||
|
position: fixed;
|
||||||
|
display: block;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
left: 25px;
|
||||||
|
bottom: 20px;
|
||||||
|
cursor:pointer;
|
||||||
|
z-index: 999;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 5px solid #333;
|
||||||
|
filter: alpha(opacity=50);
|
||||||
|
-khtml-opacity: 0.3;
|
||||||
|
-moz-opacity: 0.3;
|
||||||
|
opacity:0.3;
|
||||||
|
transition: opacity 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* css icons from https://css.gg
|
||||||
|
*/
|
||||||
|
|
||||||
|
.gg-close-o {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
transform: scale(var(--ggs,1));
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 40px
|
||||||
|
}
|
||||||
|
.gg-close-o::after,
|
||||||
|
.gg-close-o::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
width: 12px;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
border-radius: 5px;
|
||||||
|
top: 8px;
|
||||||
|
left: 3px
|
||||||
|
}
|
||||||
|
.gg-close-o::after {
|
||||||
|
transform: rotate(-45deg)
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-user-add {
|
||||||
|
display: inline-block;
|
||||||
|
transform: scale(var(--ggs,1));
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 20px;
|
||||||
|
height: 18px;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
to left,
|
||||||
|
currentColor 8px,
|
||||||
|
transparent 0)
|
||||||
|
no-repeat 14px 6px/6px 2px,
|
||||||
|
linear-gradient(
|
||||||
|
to left,
|
||||||
|
currentColor 8px,
|
||||||
|
transparent 0)
|
||||||
|
no-repeat 16px 4px/2px 6px
|
||||||
|
}
|
||||||
|
.gg-user-add::after,.gg-user-add::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid
|
||||||
|
}
|
||||||
|
.gg-user-add::before {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 30px;
|
||||||
|
top: 0;
|
||||||
|
left: 2px
|
||||||
|
}
|
||||||
|
.gg-user-add::after {
|
||||||
|
width: 12px;
|
||||||
|
height: 9px;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
top: 9px
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-user {
|
||||||
|
display: inline-block;
|
||||||
|
transform: scale(var(--ggs,1));
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 12px;
|
||||||
|
height: 18px
|
||||||
|
}
|
||||||
|
.gg-user::after,
|
||||||
|
.gg-user::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid
|
||||||
|
}
|
||||||
|
.gg-user::before {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 30px;
|
||||||
|
top: 0;
|
||||||
|
left: 2px
|
||||||
|
}
|
||||||
|
.gg-user::after {
|
||||||
|
width: 12px;
|
||||||
|
height: 9px;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-top-left-radius: 3px;
|
||||||
|
border-top-right-radius: 3px;
|
||||||
|
top: 9px
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-menu {
|
||||||
|
transform: scale(var(--ggs,1))
|
||||||
|
}
|
||||||
|
.gg-menu,
|
||||||
|
.gg-menu::after,
|
||||||
|
.gg-menu::before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 20px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: currentColor
|
||||||
|
}
|
||||||
|
.gg-menu::after,
|
||||||
|
.gg-menu::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -6px
|
||||||
|
}
|
||||||
|
.gg-menu::after {
|
||||||
|
top: 6px
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-close {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
transform: scale(var(--ggs,1)) scale(var(--ggs,1)) translate(-2px,5px);
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: 40px
|
||||||
|
}
|
||||||
|
.gg-close::after,
|
||||||
|
.gg-close::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
border-radius: 5px;
|
||||||
|
top: 8px;
|
||||||
|
left: 1px
|
||||||
|
}
|
||||||
|
.gg-close::after {
|
||||||
|
transform: rotate(-45deg)
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-link {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
-moz-transform: rotate(-45deg) scale(var(--ggs,1));
|
||||||
|
transform: translate(4px,-5px) rotate(-45deg) scale(var(--ggs,1));
|
||||||
|
width: 8px;
|
||||||
|
height: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
line-height:11px;
|
||||||
|
border-radius: 4px
|
||||||
|
}
|
||||||
|
.gg-link::after,
|
||||||
|
.gg-link::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 8px;
|
||||||
|
height: 10px;
|
||||||
|
border: 2px solid;
|
||||||
|
top: -4px
|
||||||
|
}
|
||||||
|
.gg-link::before {
|
||||||
|
border-right: 0;
|
||||||
|
border-top-left-radius: 40px;
|
||||||
|
border-bottom-left-radius: 40px;
|
||||||
|
left: -6px
|
||||||
|
}
|
||||||
|
.gg-link::after {
|
||||||
|
border-left: 0;
|
||||||
|
border-top-right-radius: 40px;
|
||||||
|
border-bottom-right-radius: 40px;
|
||||||
|
right: -6px
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-info {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transform: scale(var(--ggs,1)) translate(-3px, 3px);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 40px
|
||||||
|
}
|
||||||
|
.gg-info::after,
|
||||||
|
.gg-info::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 2px;
|
||||||
|
background: currentColor;
|
||||||
|
left: 7px
|
||||||
|
}
|
||||||
|
.gg-info::after {
|
||||||
|
bottom: 2px;
|
||||||
|
height: 8px
|
||||||
|
}
|
||||||
|
.gg-info::before {
|
||||||
|
height: 2px;
|
||||||
|
top: 2px
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-yinyang {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transform: rotate(95deg) scale(var(--ggs,1)) translate(4px,4px);
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 22px
|
||||||
|
}
|
||||||
|
.gg-yinyang::after,
|
||||||
|
.gg-yinyang::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
top: 4px
|
||||||
|
}
|
||||||
|
.gg-yinyang::before {
|
||||||
|
border: 2px solid;
|
||||||
|
left: 0
|
||||||
|
}
|
||||||
|
.gg-yinyang::after {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
right: 0;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 4px,
|
||||||
|
0 -3px 0 1px,
|
||||||
|
-2px -4px 0 1px,
|
||||||
|
-8px -5px 0 -1px,
|
||||||
|
-11px -3px 0 -2px,
|
||||||
|
-12px -1px 0 -3px,
|
||||||
|
-6px -6px 0 -1px
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-image {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transform: scale(var(--ggs,1)) translate(1px,2px);
|
||||||
|
width: 20px;
|
||||||
|
height: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 0 2px;
|
||||||
|
border-radius: 2px
|
||||||
|
}
|
||||||
|
.gg-image::after,
|
||||||
|
.gg-image::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid
|
||||||
|
}
|
||||||
|
.gg-image::after {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
top: 9px;
|
||||||
|
left: 6px
|
||||||
|
}
|
||||||
|
.gg-image::before {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 100%;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px
|
||||||
|
}
|
||||||
|
.gg-serverless {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transform: scale(var(--ggs,1)) translate(2px,1px);
|
||||||
|
width: 15px;
|
||||||
|
height: 13px;
|
||||||
|
overflow: hidden
|
||||||
|
}
|
||||||
|
.gg-serverless::after,
|
||||||
|
.gg-serverless::before {
|
||||||
|
background: currentColor;
|
||||||
|
content: "";
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
height: 3px;
|
||||||
|
box-shadow: 0 5px 0,0 10px 0;
|
||||||
|
transform: skew(-20deg)
|
||||||
|
}
|
||||||
|
.gg-serverless::before {
|
||||||
|
width: 8px;
|
||||||
|
left: -2px
|
||||||
|
}
|
||||||
|
.gg-serverless::after {
|
||||||
|
width: 12px;
|
||||||
|
right: -5px
|
||||||
|
}
|
||||||
|
.gg-software-download {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right:5px;
|
||||||
|
transform: scale(var(--ggs,1)) translate(3px,3px);
|
||||||
|
width: 16px;
|
||||||
|
height: 6px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-top: 0;
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
line-height:15px;
|
||||||
|
}
|
||||||
|
.gg-software-download::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-left: 2px solid;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
left: 2px;
|
||||||
|
bottom: 4px
|
||||||
|
}
|
||||||
|
.gg-software-download::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 2px;
|
||||||
|
height: 10px;
|
||||||
|
background: currentColor;
|
||||||
|
left: 5px;
|
||||||
|
bottom: 5px
|
||||||
|
}
|
||||||
|
.gg-arrow-left-r {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 2px solid;
|
||||||
|
transform: scale(var(--ggs,1));
|
||||||
|
border-radius: 4px
|
||||||
|
}
|
||||||
|
.gg-arrow-left-r::after,
|
||||||
|
.gg-arrow-left-r::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
left: 4px
|
||||||
|
}
|
||||||
|
.gg-arrow-left-r::after {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-left: 2px solid;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
bottom: 6px
|
||||||
|
}
|
||||||
|
.gg-arrow-left-r::before {
|
||||||
|
width: 10px;
|
||||||
|
height: 2px;
|
||||||
|
bottom: 8px;
|
||||||
|
background: currentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-pen {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
transform: rotate(-45deg) scale(var(--ggs,1));
|
||||||
|
width: 14px;
|
||||||
|
height: 4px;
|
||||||
|
border-right: 2px solid transparent;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px,
|
||||||
|
inset -2px 0 0;
|
||||||
|
border-top-right-radius: 1px;
|
||||||
|
border-bottom-right-radius: 1px;
|
||||||
|
margin-right: -2px
|
||||||
|
}
|
||||||
|
.gg-pen::after,
|
||||||
|
.gg-pen::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute
|
||||||
|
}
|
||||||
|
.gg-pen::before {
|
||||||
|
background: currentColor;
|
||||||
|
border-left: 0;
|
||||||
|
right: -6px;
|
||||||
|
width: 3px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 1px;
|
||||||
|
top: 0
|
||||||
|
}
|
||||||
|
.gg-pen::after {
|
||||||
|
width: 8px;
|
||||||
|
height: 7px;
|
||||||
|
border-top: 4px solid transparent;
|
||||||
|
border-bottom: 4px solid transparent;
|
||||||
|
border-right: 7px solid;
|
||||||
|
left: -11px;
|
||||||
|
top: -2px
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-software-upload {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right:5px;
|
||||||
|
transform: scale(var(--ggs, 1));
|
||||||
|
width: 16px;
|
||||||
|
height: 6px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-top: 0;
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.gg-software-upload::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-left: 2px solid;
|
||||||
|
border-top: 2px solid;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
left: 2px;
|
||||||
|
bottom: 4px;
|
||||||
|
}
|
||||||
|
.gg-software-upload::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 2px;
|
||||||
|
height: 10px;
|
||||||
|
background: currentColor;
|
||||||
|
left: 5px;
|
||||||
|
bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-globe-alt,
|
||||||
|
.gg-globe-alt::after,
|
||||||
|
.gg-globe-alt::before {
|
||||||
|
display: inline-block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid;
|
||||||
|
}
|
||||||
|
.gg-globe-alt {
|
||||||
|
position: relative;
|
||||||
|
transform: scale(var(--ggs, 1));
|
||||||
|
width: 18px;
|
||||||
|
border-radius: 22px;
|
||||||
|
}
|
||||||
|
.gg-globe-alt::after,
|
||||||
|
.gg-globe-alt::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 100%;
|
||||||
|
top: -2px;
|
||||||
|
left: 3px;
|
||||||
|
}
|
||||||
|
.gg-globe-alt::after {
|
||||||
|
width: 24px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
top: -11px;
|
||||||
|
left: -5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
||||||
|
`
|
||||||
1480
dist/xrfragment.plugin.frontend.js
vendored
Normal file
1480
dist/xrfragment.plugin.frontend.js
vendored
Normal file
File diff suppressed because one or more lines are too long
343
dist/xrfragment.plugin.matrix.js
vendored
Normal file
343
dist/xrfragment.plugin.matrix.js
vendored
Normal file
File diff suppressed because one or more lines are too long
998
dist/xrfragment.plugin.network.js
vendored
Normal file
998
dist/xrfragment.plugin.network.js
vendored
Normal file
|
|
@ -0,0 +1,998 @@
|
||||||
|
/*
|
||||||
|
* v0.5.1 generated at Thu May 22 07:30:49 PM CEST 2025
|
||||||
|
* https://xrfragment.org
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
(function(){
|
||||||
|
// this orchestrates multiplayer events from the scene graph
|
||||||
|
|
||||||
|
window.network = (opts) => new Proxy({
|
||||||
|
|
||||||
|
connected: false,
|
||||||
|
pos: '',
|
||||||
|
posName: '',
|
||||||
|
meetinglink: "",
|
||||||
|
peers: {},
|
||||||
|
plugin: {},
|
||||||
|
opts,
|
||||||
|
|
||||||
|
init(){
|
||||||
|
document.addEventListener('network.disconnect', () => this.connected = false )
|
||||||
|
document.addEventListener('network.connected', () => this.connected = true )
|
||||||
|
setTimeout( () => window.frontend.emit('network.init'), 100 )
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
connect(opts){
|
||||||
|
window.frontend.emit(`network.${this.connected?'disconnect':'connect'}`,opts)
|
||||||
|
},
|
||||||
|
|
||||||
|
add(peerid,data){
|
||||||
|
data = {lastUpdated: new Date().getTime(), id: peerid, ...data }
|
||||||
|
this.peers[peerid] = data
|
||||||
|
window.frontend.emit(`network.peer.add`,{peer})
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(peerid,data){
|
||||||
|
delete this.peers[peerid]
|
||||||
|
window.frontend.emit(`network.peer.remove`,{peer})
|
||||||
|
},
|
||||||
|
|
||||||
|
send(opts){
|
||||||
|
window.frontend.emit('network.send',opts)
|
||||||
|
},
|
||||||
|
|
||||||
|
receive(opts){
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getMeetingFromUrl(url){
|
||||||
|
let hash = url.replace(/.*#/,'')
|
||||||
|
let parts = hash.split("&")
|
||||||
|
let meeting = ''
|
||||||
|
parts.map( (p) => {
|
||||||
|
if( p.split("=")[0] == 'meet' ) meeting = p.split("=")[1]
|
||||||
|
})
|
||||||
|
return meeting
|
||||||
|
},
|
||||||
|
|
||||||
|
randomRoom(){
|
||||||
|
var names = []
|
||||||
|
let add = (s) => s.length < 6 && !s.match(/[0-9$]/) && !s.match(/_/) ? names.push(s) : false
|
||||||
|
for ( var i in window ) add(i)
|
||||||
|
for ( var i in Object.prototype ) add(i)
|
||||||
|
for ( var i in Function.prototype ) add(i)
|
||||||
|
for ( var i in Array.prototype ) add(i)
|
||||||
|
for ( var i in String.prototype ) add(i)
|
||||||
|
var a = names[Math.floor(Math.random() * names.length)];
|
||||||
|
var b = names[Math.floor(Math.random() * names.length)];
|
||||||
|
return String(`${a}-${b}-${String(Math.random()).substr(13)}`).toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// auto-trigger events on changes
|
||||||
|
get(data,k,receiver){ return data[k] },
|
||||||
|
set(data,k,v){
|
||||||
|
let from = data[k]
|
||||||
|
data[k] = v
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('frontend:ready', (e) => {
|
||||||
|
window.network = network(e.detail).init()
|
||||||
|
document.dispatchEvent( new CustomEvent("network:ready", e ) )
|
||||||
|
})
|
||||||
|
connectionsComponent = {
|
||||||
|
|
||||||
|
html: `
|
||||||
|
<div id="connections">
|
||||||
|
<i class="gg-close-o" id="close" onclick="$connections.visible = false"></i>
|
||||||
|
<br>
|
||||||
|
<div class="tab-frame">
|
||||||
|
<input type="radio" name="tab" id="login" checked>
|
||||||
|
<label for="login">login</label>
|
||||||
|
|
||||||
|
<input type="radio" name="tab" id="io">
|
||||||
|
<label for="io">devices</label>
|
||||||
|
|
||||||
|
<input type="radio" name="tab" id="networks">
|
||||||
|
<label for="networks">advanced</label>
|
||||||
|
|
||||||
|
<div class="tab">
|
||||||
|
<div id="settings"></div>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
<button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab">
|
||||||
|
<div id="devices">
|
||||||
|
<a class="badge ruler">Webcam and/or Audio</a>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Video</td>
|
||||||
|
<td>
|
||||||
|
<select id="videoInput"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Mic</td>
|
||||||
|
<td>
|
||||||
|
<select id="audioInput"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="display:none"> <!-- not used (for now) -->
|
||||||
|
<td>Audio</td>
|
||||||
|
<td>
|
||||||
|
<select id="audioOutput"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab">
|
||||||
|
<div id="networking">
|
||||||
|
Networking a la carte:<br>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Webcam</td>
|
||||||
|
<td>
|
||||||
|
<select id="webcam"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Chat</td>
|
||||||
|
<td>
|
||||||
|
<select id="chatnetwork"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>World sync</td>
|
||||||
|
<td>
|
||||||
|
<select id="scene"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
init: (el) => new Proxy({
|
||||||
|
|
||||||
|
visible: true,
|
||||||
|
|
||||||
|
webcam: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
|
||||||
|
chatnetwork: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
|
||||||
|
scene: [{profile:{name:"No thanks"},config: () => document.createElement('div')}],
|
||||||
|
|
||||||
|
selectedWebcam: '',
|
||||||
|
selectedChatnetwork:'',
|
||||||
|
selectedScene: '',
|
||||||
|
|
||||||
|
$webcam: $webcam = el.querySelector("#webcam"),
|
||||||
|
$chatnetwork: $chatnetwork = el.querySelector("#chatnetwork"),
|
||||||
|
$scene: $scene = el.querySelector("#scene"),
|
||||||
|
$settings: $settings = el.querySelector("#settings"),
|
||||||
|
$devices: $devices = el.querySelector("#devices"),
|
||||||
|
$connect: $connect = el.querySelector("#connect"),
|
||||||
|
$networking: $networking = el.querySelector("#networking"),
|
||||||
|
|
||||||
|
$audioInput: el.querySelector('select#audioInput'),
|
||||||
|
$audioOutput: el.querySelector('select#audioOutput'),
|
||||||
|
$videoInput: el.querySelector('select#videoInput'),
|
||||||
|
|
||||||
|
install(opts){
|
||||||
|
this.opts = opts;
|
||||||
|
(['change']).map( (e) => el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
|
||||||
|
this.reactToNetwork()
|
||||||
|
$menu.buttons = ([
|
||||||
|
`<a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick="$connections.show()"><i class="gg-user-add"></i> connect</a><br>`
|
||||||
|
]).concat($menu.buttons)
|
||||||
|
|
||||||
|
if( document.location.href.match(/meet=/) ) this.show()
|
||||||
|
|
||||||
|
setTimeout( () => document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ), 1 )
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle(){
|
||||||
|
$chat.visible = !$chat.visible
|
||||||
|
},
|
||||||
|
|
||||||
|
change(id,e){
|
||||||
|
if( id.match(/^(webcam|chatnetwork|scene)$/) ){
|
||||||
|
this.renderSettings() // trigger this when 'change' event fires on children dom elements
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
show(opts){
|
||||||
|
opts = opts || {}
|
||||||
|
if( opts.hide ){
|
||||||
|
if( el.parentElement ) el.parentElement.parentElement.style.display = 'none' // hide along with wrapper elements
|
||||||
|
if( !opts.showChat ) $chat.visible = false
|
||||||
|
}else{
|
||||||
|
$chat.visible = true
|
||||||
|
this.visible = true
|
||||||
|
// hide networking settings if entering thru meetinglink
|
||||||
|
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
|
||||||
|
if( !network.connected ){
|
||||||
|
document.querySelector('body > .xrf').appendChild(el)
|
||||||
|
$chat.send({message:"", el, class:['ui']})
|
||||||
|
if( !network.meetinglink ){ // set default
|
||||||
|
$webcam.value = opts.webcam || 'Peer2Peer'
|
||||||
|
$chatnetwork.value = opts.chatnetwork || 'Peer2Peer'
|
||||||
|
$scene.value = opts.scene || 'Peer2Peer'
|
||||||
|
}
|
||||||
|
this.renderSettings()
|
||||||
|
}else{
|
||||||
|
$chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
update(){
|
||||||
|
this.selectedWebcam = $webcam.value
|
||||||
|
this.selectedChatnetwork = $chatnetwork.value
|
||||||
|
this.selectedScene = $scene.value
|
||||||
|
},
|
||||||
|
|
||||||
|
forSelectedPluginsDo(cb){
|
||||||
|
// this function looks weird but it's handy to prevent the same plugins rendering duplicate configurations
|
||||||
|
let plugins = {}
|
||||||
|
let select = (name) => (o) => o.profile.name == name ? plugins[ o.profile.name ] = o : ''
|
||||||
|
this.webcam.find( select(this.selectedWebcam) )
|
||||||
|
this.chatnetwork.find( select(this.selectedChatnetwork) )
|
||||||
|
this.scene.find( select(this.selectedScene) )
|
||||||
|
for( let i in plugins ){
|
||||||
|
try{ cb(plugins[i]) }catch(e){ console.error(e) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSettings(){
|
||||||
|
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
|
||||||
|
this.update()
|
||||||
|
$settings.innerHTML = ''
|
||||||
|
this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config({...opts,plugin}) ) )
|
||||||
|
this.renderInputs()
|
||||||
|
},
|
||||||
|
|
||||||
|
renderInputs(){
|
||||||
|
if( !this.selectedWebcam || this.selectedWebcam == 'No thanks' ){
|
||||||
|
return this.$devices.style.display = 'none'
|
||||||
|
}else this.$devices.style.display = ''
|
||||||
|
|
||||||
|
navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: true,
|
||||||
|
video: true
|
||||||
|
})
|
||||||
|
.then( () => {
|
||||||
|
|
||||||
|
const selectors = [this.$audioInput, this.$audioOutput, this.$videoInput];
|
||||||
|
|
||||||
|
const gotDevices = (deviceInfos) => {
|
||||||
|
// Handles being called several times to update labels. Preserve values.
|
||||||
|
const values = selectors.map(select => select.value);
|
||||||
|
selectors.forEach(select => {
|
||||||
|
while (select.firstChild) {
|
||||||
|
select.removeChild(select.firstChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (let i = 0; i !== deviceInfos.length; ++i) {
|
||||||
|
const deviceInfo = deviceInfos[i];
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = deviceInfo.deviceId;
|
||||||
|
if (deviceInfo.kind === 'audioinput') {
|
||||||
|
option.text = deviceInfo.label || `microphone ${this.$audioInput.length + 1}`;
|
||||||
|
this.$audioInput.appendChild(option);
|
||||||
|
} else if (deviceInfo.kind === 'audiooutput') {
|
||||||
|
option.text = deviceInfo.label || `speaker ${this.$audioOutput.length + 1}`;
|
||||||
|
this.$audioOutput.appendChild(option);
|
||||||
|
} else if (deviceInfo.kind === 'videoinput') {
|
||||||
|
option.text = deviceInfo.label || `camera this.${this.$videoInput.length + 1}`;
|
||||||
|
this.$videoInput.appendChild(option);
|
||||||
|
} else {
|
||||||
|
console.log('Some other kind of source/device: ', deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// hide if there's nothing to choose
|
||||||
|
let totalDevices = this.$audioInput.options.length + this.$audioOutput.options.length + this.$videoInput.options.length
|
||||||
|
this.$devices.style.display = totalDevices > 3 ? 'block' : 'none'
|
||||||
|
|
||||||
|
selectors.forEach((select, selectorIndex) => {
|
||||||
|
if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
|
||||||
|
select.value = values[selectorIndex];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// after getUserMedia we can enumerate
|
||||||
|
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(console.warn);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
reactToNetwork(){ // *TODO* move to network?
|
||||||
|
|
||||||
|
document.addEventListener('network.connect', () => {
|
||||||
|
this.show({hide:true, showChat: true})
|
||||||
|
})
|
||||||
|
document.addEventListener('network.disconnect', () => {
|
||||||
|
this.connected = false
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},{
|
||||||
|
|
||||||
|
get(data,k,v){ return data[k] },
|
||||||
|
set(data,k,v){
|
||||||
|
data[k] = v
|
||||||
|
switch( k ){
|
||||||
|
case "visible": el.style.display = v ? '' : 'none';
|
||||||
|
if( !v && el.parentNode && el.parentNode.parentNode ) el.parentNode.parentNode.remove()
|
||||||
|
break;
|
||||||
|
case "webcam": $webcam.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
|
||||||
|
case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
|
||||||
|
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.profile.name).join('</option><option>')}</option>`; break;
|
||||||
|
case "selectedScene": $scene.value = v; data.renderSettings(); break;
|
||||||
|
case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break;
|
||||||
|
case "selectedWebcam": {
|
||||||
|
$webcam.value = v;
|
||||||
|
data.renderSettings();
|
||||||
|
$devices.style.display = v ? 'block' : 'none'
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// reactify component!
|
||||||
|
document.addEventListener('$chat:ready', (opts) => {
|
||||||
|
opts = opts.detail
|
||||||
|
document.head.innerHTML += connectionsComponent.css
|
||||||
|
window.$connections = document.createElement('div')
|
||||||
|
$connections.innerHTML = connectionsComponent.html
|
||||||
|
$connections = connectionsComponent.init($connections)
|
||||||
|
$connections.install(opts)
|
||||||
|
})
|
||||||
|
|
||||||
|
// alpine component for displaying meetings
|
||||||
|
|
||||||
|
connectionsComponent.css = `
|
||||||
|
<style type="text/css">
|
||||||
|
button#connect{
|
||||||
|
height: 43px;
|
||||||
|
width:100%;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
#messages .msg #connections{
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
.connecthide {
|
||||||
|
transform:translateY(-1000px);
|
||||||
|
}
|
||||||
|
#close{
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
float: right;
|
||||||
|
top: 16px;
|
||||||
|
}
|
||||||
|
#messages .msg.ui div.tab-frame > div.tab{ padding:25px 10px 5px 10px;}
|
||||||
|
</style>`
|
||||||
|
chatComponent = {
|
||||||
|
|
||||||
|
html: `
|
||||||
|
<div id="chat">
|
||||||
|
<div id="videos" style="pointer-events:none"></div>
|
||||||
|
<div id="messages" aria-live="assertive" role="log" aria-relevant="additions"></div>
|
||||||
|
<div id="chatfooter">
|
||||||
|
<div id="chatbar">
|
||||||
|
<input id="chatline" type="text" placeholder="chat here"></input>
|
||||||
|
</div>
|
||||||
|
<button id="chatsend" class="btn" aria-label="send message">
|
||||||
|
<i class="gg-chevron-right-o"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
|
||||||
|
init: (el) => new Proxy({
|
||||||
|
|
||||||
|
scene: null,
|
||||||
|
visible: true,
|
||||||
|
messages: [],
|
||||||
|
oneMessagePerUser: false,
|
||||||
|
|
||||||
|
username: '', // configured by 'network.connected' event
|
||||||
|
|
||||||
|
$videos: el.querySelector("#videos"),
|
||||||
|
$messages: el.querySelector("#messages"),
|
||||||
|
$chatline: el.querySelector("#chatline"),
|
||||||
|
$chatbar: el.querySelector("#chatbar"),
|
||||||
|
$chatsend: el.querySelector("#chatsend"),
|
||||||
|
|
||||||
|
install(opts){
|
||||||
|
this.opts = opts
|
||||||
|
this.scene = opts.scene
|
||||||
|
this.$chatbar.style.display = 'none'
|
||||||
|
el.className = "xrf"
|
||||||
|
el.style.display = 'none' // start hidden
|
||||||
|
document.body.appendChild( el )
|
||||||
|
document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) )
|
||||||
|
this.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br>`, class: ["info","guide","multiline"] })
|
||||||
|
},
|
||||||
|
|
||||||
|
sendInput(value){
|
||||||
|
if( value[0] == '#' ) return xrf.navigator.to(value)
|
||||||
|
let event = value.match(/^[!\/]/) ? "chat.command" : "network.send"
|
||||||
|
let message = value.replace(/^[!\/]/,'')
|
||||||
|
let raw = {detail:{message:value, halt:false}}
|
||||||
|
document.dispatchEvent( new CustomEvent( event, {detail: {message}} ) )
|
||||||
|
document.dispatchEvent( new CustomEvent( "chat.input", raw ) )
|
||||||
|
if( event == "network.send" && !raw.detail.halt ) this.send({message: value })
|
||||||
|
this.$chatline.lastValue = value
|
||||||
|
this.$chatline.value = ''
|
||||||
|
if( window.innerHeight < 600 ) this.$chatline.blur()
|
||||||
|
},
|
||||||
|
|
||||||
|
initListeners(){
|
||||||
|
let {$chatline} = this
|
||||||
|
|
||||||
|
$chatline.addEventListener('click', (e) => this.inform() )
|
||||||
|
|
||||||
|
$chatline.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key == 'Enter' ){
|
||||||
|
this.sendInput($chatline.value)
|
||||||
|
}
|
||||||
|
if (e.key == 'ArrowUp' ){
|
||||||
|
$chatline.value = $chatline.lastValue || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('network.connect', (e) => {
|
||||||
|
this.visible = true
|
||||||
|
this.$chatbar.style.display = '' // show
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('network.connected', (e) => {
|
||||||
|
if( e.detail.username ) this.username = e.detail.username
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('chat.command', (e) => {
|
||||||
|
if( String(e.detail.message).trim() == 'help' ){
|
||||||
|
let detail = {message:`The following commands are available
|
||||||
|
<br><br>
|
||||||
|
<b class="badge">/help</b> shows this help screen
|
||||||
|
`}
|
||||||
|
document.dispatchEvent( new CustomEvent( 'chat.command.help', {detail}))
|
||||||
|
this.send({message: detail.message})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$chatsend.addEventListener('click', (e) => {
|
||||||
|
this.sendInput($chatline.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
inform(){
|
||||||
|
if( !this.inform.informed && (this.inform.informed = true) ){
|
||||||
|
window.notify("You can now type messages in the textfield below.")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle(){
|
||||||
|
this.visible = !this.visible
|
||||||
|
if( this.visible && window.meeting.status == 'offline' ) window.meeting.start(this.opts)
|
||||||
|
},
|
||||||
|
|
||||||
|
hyphenate(str){
|
||||||
|
return String(str).replace(/[^a-zA-Z0-9]/g,'-')
|
||||||
|
},
|
||||||
|
|
||||||
|
// sending messages to the #messages div
|
||||||
|
// every user can post maximum one msg at a time
|
||||||
|
// it's more like a 'status' which is more friendly
|
||||||
|
// for accessibility reasons
|
||||||
|
// for a fullfledged chat/transcript see matrix clients
|
||||||
|
send(opts){
|
||||||
|
let {$messages} = this
|
||||||
|
opts = { linebreak:true, message:"", class:[], ...opts }
|
||||||
|
if( window.frontend && window.frontend.emit ) window.frontend.emit('$chat.send', opts )
|
||||||
|
opts.pos = opts.pos || network.posName || network.pos
|
||||||
|
let div = document.createElement('div')
|
||||||
|
let msg = document.createElement('div')
|
||||||
|
let br = document.createElement('br')
|
||||||
|
let nick = document.createElement('div')
|
||||||
|
msg.className = "msg"
|
||||||
|
let html = `${ opts.message || ''}${ opts.html ? opts.html(opts) : ''}`
|
||||||
|
if( $messages.last == html ) return
|
||||||
|
msg.innerHTML = html
|
||||||
|
if( opts.el ) msg.appendChild(opts.el)
|
||||||
|
opts.id = Math.random()
|
||||||
|
if( opts.class ){
|
||||||
|
msg.classList.add.apply(msg.classList, opts.class)
|
||||||
|
br.classList.add.apply(br.classList, opts.class)
|
||||||
|
div.classList.add.apply(div.classList, opts.class.concat(["envelope"]))
|
||||||
|
}
|
||||||
|
if( !msg.className.match(/(info|guide|ui)/) && !opts.from ){
|
||||||
|
let frag = xrf.URI.parse(document.location.hash).XRF
|
||||||
|
opts.from = 'you'
|
||||||
|
if( frag.pos ) opts.pos = frag.pos.string
|
||||||
|
msg.classList.add('self')
|
||||||
|
}
|
||||||
|
if( opts.from ){
|
||||||
|
nick.className = "user"
|
||||||
|
nick.innerText = opts.from+' '
|
||||||
|
div.appendChild(nick)
|
||||||
|
if( opts.pos ){
|
||||||
|
let a = document.createElement("a")
|
||||||
|
a.href = a.innerText = `#pos=${opts.pos}`
|
||||||
|
nick.appendChild(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div.appendChild(msg)
|
||||||
|
// force one message per user
|
||||||
|
if( this.oneMessagePerUser && opts.from ){
|
||||||
|
div.id = this.hyphenate(opts.from)
|
||||||
|
let oldMsg = $messages.querySelector(`#${div.id}`)
|
||||||
|
if( oldMsg ) oldMsg.remove()
|
||||||
|
}
|
||||||
|
// remove after timeout
|
||||||
|
if( opts.timeout ) setTimeout( (div) => div.remove(), opts.timeout, div )
|
||||||
|
// finally add the message on top
|
||||||
|
$messages.appendChild(div)
|
||||||
|
if( opts.linebreak ) div.appendChild(br)
|
||||||
|
$messages.scrollTop = $messages.scrollHeight // scroll down
|
||||||
|
$messages.last = msg.innerHTML
|
||||||
|
},
|
||||||
|
|
||||||
|
getChatLog(){
|
||||||
|
return ([...this.$messages.querySelectorAll('.envelope')])
|
||||||
|
.filter( (d) => !d.className.match(/(info|ui)/) )
|
||||||
|
.map( (d) => d.innerHTML )
|
||||||
|
.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
},{
|
||||||
|
|
||||||
|
get(me,k,v){ return me[k] },
|
||||||
|
set(me,k,v){
|
||||||
|
me[k] = v
|
||||||
|
switch( k ){
|
||||||
|
case "visible": {
|
||||||
|
el.style.display = me.visible ? 'block' : 'none'
|
||||||
|
if( !el.inited && (el.inited = true) ) me.initListeners()
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// reactify component!
|
||||||
|
document.addEventListener('$menu:ready', (opts) => {
|
||||||
|
opts = opts.detail
|
||||||
|
document.head.innerHTML += chatComponent.css
|
||||||
|
window.$chat = document.createElement('div')
|
||||||
|
$chat.innerHTML = chatComponent.html
|
||||||
|
$chat = chatComponent.init($chat)
|
||||||
|
$chat.install(opts)
|
||||||
|
//$menu.buttons = ([`<a class="btn" aria-label="button" aria-description="toggle text" id="meeting" onclick="$chat.toggle()">📜 toggle text</a><br>`])
|
||||||
|
// .concat($menu.buttons)
|
||||||
|
})
|
||||||
|
|
||||||
|
// alpine component for displaying meetings
|
||||||
|
|
||||||
|
chatComponent.css = `
|
||||||
|
<style type="text/css">
|
||||||
|
#videos{
|
||||||
|
display:grid-auto-columns;
|
||||||
|
grid-column-gap:5px;
|
||||||
|
margin-bottom:15px;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: 15px;
|
||||||
|
z-index:1000;
|
||||||
|
}
|
||||||
|
#videos > video{
|
||||||
|
border-radius:7px;
|
||||||
|
display:inline-block;
|
||||||
|
background:black;
|
||||||
|
width:80px;
|
||||||
|
height:60px;
|
||||||
|
margin-right:5px;
|
||||||
|
margin-bottom:5px;
|
||||||
|
vertical-align:top;
|
||||||
|
pointer-events:all;
|
||||||
|
}
|
||||||
|
#videos > video:hover{
|
||||||
|
filter: brightness(1.8);
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chatbar,
|
||||||
|
button#chatsend{
|
||||||
|
z-index: 1500;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
height: 34px;
|
||||||
|
left: 20px;
|
||||||
|
width: 48%;
|
||||||
|
background: white;
|
||||||
|
padding: 0px 0px 0px 15px;
|
||||||
|
border-radius: 30px;
|
||||||
|
max-width: 500px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: 0px 0px 5px 5px #0002;
|
||||||
|
}
|
||||||
|
button#chatsend{
|
||||||
|
line-height:0px;
|
||||||
|
display:none;
|
||||||
|
z-index: 1550;
|
||||||
|
color: white;
|
||||||
|
border: 0;
|
||||||
|
height: 35px;
|
||||||
|
background: var(--xrf-dark-gray);
|
||||||
|
font-weight: bold;
|
||||||
|
width: 20px;
|
||||||
|
max-width: 20px;
|
||||||
|
border-radius: 20px 0px 0px 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
#chatbar input{
|
||||||
|
border:none;
|
||||||
|
width:90%;
|
||||||
|
box-sizing:border-box;
|
||||||
|
height: 24px;
|
||||||
|
font-size: var(--xrf-font-size-2);
|
||||||
|
max-width:unset;
|
||||||
|
}
|
||||||
|
#messages{
|
||||||
|
/*
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 40%;
|
||||||
|
*/
|
||||||
|
width:100%;
|
||||||
|
box-sizing:border-box;
|
||||||
|
align-items: flex-start;
|
||||||
|
position: absolute;
|
||||||
|
transition:1s;
|
||||||
|
top: 77px;
|
||||||
|
left: 0;
|
||||||
|
bottom: 49px;
|
||||||
|
padding: 20px;
|
||||||
|
overflow:hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
pointer-events:none;
|
||||||
|
transition:1s;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
body.menu #messages{
|
||||||
|
top:50px;
|
||||||
|
}
|
||||||
|
#messages:hover {
|
||||||
|
pointer-events:all;
|
||||||
|
}
|
||||||
|
#messages *{
|
||||||
|
box-sizing:border-box;
|
||||||
|
/*
|
||||||
|
pointer-events:none;
|
||||||
|
-webkit-user-select:none;
|
||||||
|
-moz-user-select:-moz-none;
|
||||||
|
-ms-user-select:none;
|
||||||
|
user-select:none;
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
#messages .msg{
|
||||||
|
transition:all 1s ease;
|
||||||
|
background: #fff;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 17px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: #000c;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
line-height:23px;
|
||||||
|
line-height:33px;
|
||||||
|
cursor:grabbing;
|
||||||
|
border: 1px solid #0002;
|
||||||
|
}
|
||||||
|
#messages .msg *,
|
||||||
|
#messages .user *{
|
||||||
|
pointer-events:all;
|
||||||
|
-webkit-user-select:text;
|
||||||
|
-moz-user-select:-moz-text;
|
||||||
|
-ms-user-select:text;
|
||||||
|
user-select:text;
|
||||||
|
}
|
||||||
|
|
||||||
|
#messages .msg.self{
|
||||||
|
border-radius: 20px;
|
||||||
|
background:var(--xrf-dark-gray);
|
||||||
|
}
|
||||||
|
#messages .msg.self,
|
||||||
|
#messages .msg.self div{
|
||||||
|
color:#FFF;
|
||||||
|
}
|
||||||
|
#messages .msg.info{
|
||||||
|
background: var(--xrf-white);
|
||||||
|
border-radius: 20px;
|
||||||
|
color: var(--xrf-dark-gray);
|
||||||
|
text-align: left;
|
||||||
|
line-height: 19px;
|
||||||
|
}
|
||||||
|
#messages .msg.info,
|
||||||
|
#messages .msg.info *{
|
||||||
|
font-size: var(--xrf-font-size-0);
|
||||||
|
}
|
||||||
|
#messages .msg a {
|
||||||
|
text-decoration:underline;
|
||||||
|
color: var(--xrf-light-xrf-secondary);
|
||||||
|
font-weight:bold;
|
||||||
|
transition:0.3s;
|
||||||
|
}
|
||||||
|
#messages .msg.info a,
|
||||||
|
#messages a.ruler{
|
||||||
|
color:#FFF;
|
||||||
|
}
|
||||||
|
#messages .msg a:hover{
|
||||||
|
color:#000;
|
||||||
|
}
|
||||||
|
#messages .msg.ui,
|
||||||
|
#messages .msg.ui div{
|
||||||
|
background: white;
|
||||||
|
border:none;
|
||||||
|
color: #333;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin:0;
|
||||||
|
padding:0px 5px 5px 5px;
|
||||||
|
}
|
||||||
|
#messages.guide, .guide{
|
||||||
|
display:unset;
|
||||||
|
}
|
||||||
|
#messages .guide, .guide{
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
br.guide{
|
||||||
|
display:inline-block;
|
||||||
|
}
|
||||||
|
#messages .msg.info a:hover,
|
||||||
|
#messages button:hover{
|
||||||
|
filter: brightness(1.4);
|
||||||
|
}
|
||||||
|
#messages .msg.multiline {
|
||||||
|
padding: 2px 14px;
|
||||||
|
}
|
||||||
|
#messages button {
|
||||||
|
text-decoration:none;
|
||||||
|
margin: 0px 15px 10px 0px;
|
||||||
|
background: var(--xrf-primary);
|
||||||
|
font-family: var(--xrf-font-sans-serif);
|
||||||
|
color: #FFF;
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 11px 15px;
|
||||||
|
border: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0px 0px 5px 5px #0002;
|
||||||
|
pointer-events:all;
|
||||||
|
}
|
||||||
|
#messages,#chatbar,#chatbar *, #messages *{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#messages button.emoticon,
|
||||||
|
#messages .btn.emoticon {
|
||||||
|
line-height:2px;
|
||||||
|
width: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0px 0px;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
min-width: 31px;
|
||||||
|
box-shadow:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#messages button.emoticon:hover,
|
||||||
|
#messages .btn.emoticon:hover {
|
||||||
|
border: 1px solid #ccc !important;
|
||||||
|
background:#EEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nomargin{
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.envelope{
|
||||||
|
margin-right:15px;
|
||||||
|
width:100%;
|
||||||
|
max-width:40%;
|
||||||
|
margin-bottom:7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.envelope,
|
||||||
|
.envelope * {
|
||||||
|
transition:1s;
|
||||||
|
pointer-events:none;
|
||||||
|
}
|
||||||
|
.envelope a,
|
||||||
|
.envelope button,
|
||||||
|
.envelope input,
|
||||||
|
.envelope textarea,
|
||||||
|
.envelope msg,
|
||||||
|
.envelope msg * {
|
||||||
|
pointer-events:all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user{
|
||||||
|
margin-left:13px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--xrf-dark-gray);
|
||||||
|
}
|
||||||
|
.user, .user *{
|
||||||
|
font-size: var(--xrf-font-size-0);
|
||||||
|
}
|
||||||
|
.gg-chevron-right-o {
|
||||||
|
color:#FFF;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
transform: scale(var(--ggs,1));
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 100px
|
||||||
|
}
|
||||||
|
|
||||||
|
.gg-chevron-right-o::after {
|
||||||
|
color:#FFF;
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-right: 2px solid;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
left: 5px;
|
||||||
|
top: 6px
|
||||||
|
}
|
||||||
|
</style>`
|
||||||
|
window.debug = (opts) => new Proxy({
|
||||||
|
opts,
|
||||||
|
|
||||||
|
enabled: false,
|
||||||
|
$console: false,
|
||||||
|
|
||||||
|
|
||||||
|
toggle(){ this.enabled = !this.enabled },
|
||||||
|
|
||||||
|
settings(){
|
||||||
|
this.toggle()
|
||||||
|
},
|
||||||
|
|
||||||
|
init(){
|
||||||
|
},
|
||||||
|
|
||||||
|
setupConsole(){
|
||||||
|
// add onscreen console
|
||||||
|
let $console = this.$console = document.createElement('pre')
|
||||||
|
$console.style.position = 'fixed'
|
||||||
|
$console.style.overflow = 'auto'
|
||||||
|
$console.style.top = $console.style.left = $console.style.bottom = $console.style.right = '0px'
|
||||||
|
$console.style.height = '98.5vh';
|
||||||
|
$console.style.width = '100%'
|
||||||
|
$console.style.pointerEvents = 'none'
|
||||||
|
$console.id = 'console'
|
||||||
|
document.body.appendChild($console)
|
||||||
|
const wrapper = (scope, fn, name) => {
|
||||||
|
return function(msg) {
|
||||||
|
$console.innerHTML += `[${name}] ${msg}<br>`;
|
||||||
|
if( name == 'err'){
|
||||||
|
let err = new Error()
|
||||||
|
String(err.stack).split("\n").slice(2).map( (l) => $console.innerHTML += ` └☑ ${l}\n` )
|
||||||
|
}
|
||||||
|
$console.scrollTop = $console.scrollHeight;
|
||||||
|
fn.call(scope,msg);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
window.console.log = wrapper(console, console.log, "log");
|
||||||
|
window.console.warn = wrapper(console, console.warn, "wrn");
|
||||||
|
window.console.error = wrapper(console, console.error, "err");
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// auto-trigger events on changes
|
||||||
|
get(data,k,receiver){ return data[k] },
|
||||||
|
set(data,k,v){
|
||||||
|
data[k] = v
|
||||||
|
switch( k ){
|
||||||
|
case "enabled": {
|
||||||
|
if( !data.$console ) data.setupConsole()
|
||||||
|
$('#debug.btn').style.filter= v ? 'brightness(1.0)' : 'brightness(0.5)'
|
||||||
|
data.$console.style.display = v ? 'block' : 'none'
|
||||||
|
data.enabled = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('$menu:ready', (e) => {
|
||||||
|
try{
|
||||||
|
debug = debug(e.detail)
|
||||||
|
debug.init()
|
||||||
|
document.dispatchEvent( new CustomEvent("debug:ready", e ) )
|
||||||
|
$menu.buttons = $menu.buttons.concat([`<a class="btn" style="background:var(--xrf-dark-gray);filter: brightness(0.5);" aria-label="button" aria-description="enable all debug features" id="debug" onclick="debug.enabled = !debug.enabled"><i class="gg-debug"></i>debug</a><br>`])
|
||||||
|
}catch(e){console.error(e)}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.querySelector('head').innerHTML += `
|
||||||
|
<style type="text/css">
|
||||||
|
.gg-debug {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
transform: scale(var(--ggs,1));
|
||||||
|
width: 12px;
|
||||||
|
height: 18px;
|
||||||
|
border: 2px solid;
|
||||||
|
border-radius: 22px;
|
||||||
|
display: inline-block;
|
||||||
|
transform: translate(0px,4px);
|
||||||
|
margin-right: 21px;
|
||||||
|
}
|
||||||
|
.gg-debug::after,
|
||||||
|
.gg-debug::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: absolute
|
||||||
|
}
|
||||||
|
.gg-debug::before {
|
||||||
|
width: 8px;
|
||||||
|
height: 4px;
|
||||||
|
border: 2px solid;
|
||||||
|
top: -4px;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
border-bottom-right-radius: 10px;
|
||||||
|
border-top: 0
|
||||||
|
}
|
||||||
|
.gg-debug::after {
|
||||||
|
background: currentColor;
|
||||||
|
width: 4px;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 5px;
|
||||||
|
top: 4px;
|
||||||
|
left: 2px;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 0,
|
||||||
|
-6px -2px 0,
|
||||||
|
-6px 2px 0,
|
||||||
|
-6px 6px 0,
|
||||||
|
6px -2px 0,
|
||||||
|
6px 2px 0,
|
||||||
|
6px 6px 0
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`
|
||||||
|
|
||||||
|
}).apply({})
|
||||||
392
dist/xrfragment.plugin.p2p.js
vendored
Normal file
392
dist/xrfragment.plugin.p2p.js
vendored
Normal file
File diff suppressed because one or more lines are too long
293
dist/xrfragment.plugin.remotestorage.js
vendored
Normal file
293
dist/xrfragment.plugin.remotestorage.js
vendored
Normal file
|
|
@ -0,0 +1,293 @@
|
||||||
|
/*
|
||||||
|
* v0.5.1 generated at Thu May 22 07:30:49 PM CEST 2025
|
||||||
|
* https://xrfragment.org
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
(function(){
|
||||||
|
// this demonstrates the remotestorage aframe component
|
||||||
|
|
||||||
|
// reactive component for displaying the menu
|
||||||
|
remoteStorageComponent = (el) => new Proxy({
|
||||||
|
|
||||||
|
html: (data) => (`
|
||||||
|
<style type="text/css">
|
||||||
|
body #files .rs-button-big{
|
||||||
|
background: #FFF;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #CCC;
|
||||||
|
padding: 10px 0px 49px 10px;
|
||||||
|
}
|
||||||
|
#files .rs-button{
|
||||||
|
background:#CCC;
|
||||||
|
}
|
||||||
|
#files input {
|
||||||
|
min-width:345px;
|
||||||
|
}
|
||||||
|
#files #buttons select,
|
||||||
|
#files #buttons button{
|
||||||
|
width:255px;
|
||||||
|
max-width:unset;
|
||||||
|
}
|
||||||
|
#files #listing{
|
||||||
|
margin-bottom:15px;
|
||||||
|
}
|
||||||
|
#files #delete{
|
||||||
|
display: none;
|
||||||
|
transform: translate(10px, 5px);
|
||||||
|
}
|
||||||
|
#links{
|
||||||
|
display:none
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="remoteFilesTab">
|
||||||
|
<div id="rswidget"></div>
|
||||||
|
<br>
|
||||||
|
<div id="buttons" style="display:none">
|
||||||
|
<select id="listing" alt="your files"></select>
|
||||||
|
<i class="gg-close-o" id="delete" alt="delete" onclick="$remotestorage.remove()"></i>
|
||||||
|
<a href="https://inspektor.5apps.com/?path=%2Fwebxr%2F" target="_blank" style="margin-left:15px">manage files</a>
|
||||||
|
<br>
|
||||||
|
<button onclick="$remotestorage.savePrivate()"><i class="gg-software-download"></i> save experience</button>
|
||||||
|
<br>
|
||||||
|
<button onclick="$remotestorage.savePublic()" ><i class="gg-globe-alt"></i> publish online</button>
|
||||||
|
<br>
|
||||||
|
<div id="links">
|
||||||
|
<br>
|
||||||
|
<div><i class="gg-link"></i> 3D file</div>
|
||||||
|
<input type="text" value='' id="file">
|
||||||
|
<br>
|
||||||
|
<div><i class="gg-link"></i> 3D file in webviewer</div>
|
||||||
|
<input type="text" value='' id="webviewer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`),
|
||||||
|
|
||||||
|
connected: false,
|
||||||
|
$listing: false,
|
||||||
|
$links: false,
|
||||||
|
|
||||||
|
init(opts){
|
||||||
|
// create HTML element
|
||||||
|
$files.tabs = $files.tabs.concat({id:"remoteFiles", name: "remote storage"})
|
||||||
|
el.innerHTML = this.html(this)
|
||||||
|
el.className = "tab"
|
||||||
|
document.querySelector("#files .tab-frame").appendChild(el);
|
||||||
|
|
||||||
|
// setup references
|
||||||
|
this.$listing = document.querySelector('#files .tab-frame select#listing');
|
||||||
|
this.$links = document.querySelector('#files .tab-frame #links');
|
||||||
|
|
||||||
|
// setup input listeners
|
||||||
|
(['click']).map( (e) => el.addEventListener(e, (ev) => typeof this[e] == 'function' && this[e](ev.target.id,ev) ) )
|
||||||
|
|
||||||
|
// signal ready
|
||||||
|
setTimeout( () => {
|
||||||
|
document.dispatchEvent( new CustomEvent("remotestorage:ready", {detail: {$files:this,xrf}}) )
|
||||||
|
},100)
|
||||||
|
|
||||||
|
this.loadScript( () => this.initRemoteStorage() )
|
||||||
|
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
loadScript(cb){
|
||||||
|
let el = document.createElement("script")
|
||||||
|
el.setAttribute("defer","")
|
||||||
|
el.src = "https://unpkg.com/remotestoragejs@2.0.0-beta.7/release/remotestorage.js"
|
||||||
|
document.head.appendChild(el)
|
||||||
|
|
||||||
|
el = document.createElement("script")
|
||||||
|
el.src = "https://unpkg.com/remotestorage-widget@1.6.0/build/widget.js"
|
||||||
|
el.addEventListener('load', () => setTimeout(cb,2000) )
|
||||||
|
document.head.appendChild(el)
|
||||||
|
},
|
||||||
|
|
||||||
|
initRemoteStorage(){
|
||||||
|
let apis = {
|
||||||
|
dropbox: "4jc8nx1lbarp472"
|
||||||
|
}
|
||||||
|
const modules = []
|
||||||
|
if( typeof WebXR != undefined ){
|
||||||
|
modules.push(WebXR) // defined in remotestorage-module-webXRF.js
|
||||||
|
}
|
||||||
|
window.remoteStorage = new RemoteStorage({logging: true, modules })
|
||||||
|
if( Object.keys(apis).length ) remoteStorage.setApiKeys(apis)
|
||||||
|
|
||||||
|
remoteStorage.on('not-connected', (e) => { this.connected = false })
|
||||||
|
remoteStorage.on('ready', (e) => { })
|
||||||
|
remoteStorage.on('connected', (e) => {
|
||||||
|
this.connected = true
|
||||||
|
// force open dialog and click remote-tab
|
||||||
|
frontend.$topbar.toggle(true)
|
||||||
|
$files.toggle(true)
|
||||||
|
document.querySelector("#files input#remoteFiles").click()
|
||||||
|
|
||||||
|
this.updateFiles()
|
||||||
|
})
|
||||||
|
|
||||||
|
remoteStorage.access.claim( `webxr`, 'rw'); // our data dir
|
||||||
|
remoteStorage.caching.enable( `/webxr/` ) // local-first, remotestorage-second
|
||||||
|
remoteStorage.caching.enable( `/public/webxr/` ) // local-first, remotestorage-second
|
||||||
|
|
||||||
|
// create widget
|
||||||
|
let opts = {}
|
||||||
|
opts.modalBackdrop = false
|
||||||
|
opts.leaveOpen = true
|
||||||
|
widget = new window.Widget(window.remoteStorage, opts)
|
||||||
|
widget.attach( "rswidget" );
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
savePrivate(){
|
||||||
|
frontend.download( (data,filename) => {
|
||||||
|
filename = prompt('save-as filename', filename)
|
||||||
|
remoteStorage.webxr.add(data,{public:false,filename,mimetype: 'model/glb-binary'})
|
||||||
|
.then( () => window.notify(`saved webxr/${filename} to remote storage`) )
|
||||||
|
.catch( (e) => {
|
||||||
|
console.error(e)
|
||||||
|
window.notify(`failed to save webxr/${filename} to remote storage`,{status:'error'})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
savePublic(){
|
||||||
|
frontend.download( (data,filename) => {
|
||||||
|
filename = prompt('save-as filename', filename)
|
||||||
|
opts = {public:true,filename,mimetype: 'model/glb-binary'}
|
||||||
|
remoteStorage.webxr.add(data,opts)
|
||||||
|
.then( (res) => {
|
||||||
|
window.notify(`saved webxr/${filename} to remote storage`)
|
||||||
|
const link = opts.client.storage.remote.href + opts.client.base + filename
|
||||||
|
const linkWebView = document.location.href.replace(/(\?|#).*/,'') + `?${link}`
|
||||||
|
this.$links.querySelector("#file").value = link
|
||||||
|
this.$links.querySelector("#webviewer").value = linkWebView
|
||||||
|
this.$links.style.display = 'block'
|
||||||
|
})
|
||||||
|
.catch( (e) => {
|
||||||
|
console.error(e)
|
||||||
|
window.notify(`failed to save webxr/${filename} to remote storage`,{status:'error'})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
openPrivate(file){
|
||||||
|
if( !confirm(`teleport to ${file} on your remotestorage?`) ) return
|
||||||
|
remoteStorage.webxr.getFile(file)
|
||||||
|
.then( (res) => {
|
||||||
|
for( var i in xrf.loaders ){
|
||||||
|
if( file.replace(/.*\./).match(i) ){
|
||||||
|
xrf.navigator.URI.file = '' // bypass cached file (easy refresh same file for testing)
|
||||||
|
xrf.navigator.to(file,null, (new xrf.loaders[i]()), res.data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw 'unknown filetype: '+file
|
||||||
|
})
|
||||||
|
.catch( (e) => {
|
||||||
|
console.error(e)
|
||||||
|
window.notify("could not load webxr/"+file)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(){
|
||||||
|
const currentFile = el.querySelector('select#listing').value
|
||||||
|
if( confirm("remove "+currentFile+" from remote storage?") ){
|
||||||
|
remoteStorage.webxr.remove(currentFile)
|
||||||
|
.then( () => {
|
||||||
|
window.notify(`removed webxr/${filename} from remote storage`)
|
||||||
|
this.updateFiles()
|
||||||
|
})
|
||||||
|
.catch( (e) => {
|
||||||
|
console.error(e)
|
||||||
|
window.notify(`could not webxr/${filename} from remote storage`,{status:'error'})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateFiles(){
|
||||||
|
remoteStorage.webxr.getListing()
|
||||||
|
.then( (listing) => {
|
||||||
|
|
||||||
|
this.$listing.innerHTML = '' // empty
|
||||||
|
|
||||||
|
const addOption = (value,text) => {
|
||||||
|
let opt = document.createElement("option")
|
||||||
|
opt.text = text
|
||||||
|
opt.value = value
|
||||||
|
this.$listing.appendChild(opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
addOption("","--- your experiences ---")
|
||||||
|
for( let file in listing ){
|
||||||
|
if( file.match(/\.(glb|gltf|usd|obj|col|fbx)$/) ) addOption(file,file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// autoload selection
|
||||||
|
if( !this.updateFiles.autoload ){ // run once
|
||||||
|
this.$listing.addEventListener('change', () => {
|
||||||
|
if( this.$listing.options.selectedIndex > 0 ) this.openPrivate(this.$listing.value)
|
||||||
|
document.querySelector("#delete").style.display = this.$listing.options.selectedIndex == 0 ? "none" : "inline-block"
|
||||||
|
this.$links.style.display = 'none'
|
||||||
|
})
|
||||||
|
this.updateFiles.autoload = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
click(id,e){
|
||||||
|
//switch(id){
|
||||||
|
// case "more": return this.toggle(); break;
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
get(me,k,v){ return me[k] },
|
||||||
|
|
||||||
|
set(me,k,v){
|
||||||
|
me[k] = v
|
||||||
|
switch( k ){
|
||||||
|
case 'connected': el.querySelector("#buttons").style.display = v ? 'block' : 'none'; break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// reactify component!
|
||||||
|
document.addEventListener('$files:ready', (e) => {
|
||||||
|
window.$remotestorage = remoteStorageComponent( document.createElement('div') ).init(e.detail)
|
||||||
|
})
|
||||||
|
const WebXR = { name: 'webxr', builder: function(privateClient, publicClient) {
|
||||||
|
return {
|
||||||
|
exports: {
|
||||||
|
add: function(data,opts) {
|
||||||
|
if( !data || !opts.filename || !opts.mimetype) throw 'webxr.add() needs filedata + filename + mimetype'
|
||||||
|
const client = opts.client = opts.public ? publicClient : privateClient;
|
||||||
|
return client.storeFile(opts.mimetype, opts.filename,data)
|
||||||
|
},
|
||||||
|
|
||||||
|
getListing: function(a,opts){
|
||||||
|
opts = opts || {}
|
||||||
|
const client = opts.public ? publicClient : privateClient;
|
||||||
|
return client.getListing(a)
|
||||||
|
},
|
||||||
|
|
||||||
|
getFile: function(file,opts){
|
||||||
|
opts = opts || {}
|
||||||
|
const client = opts.public ? publicClient : privateClient;
|
||||||
|
return client.getFile(file)
|
||||||
|
},
|
||||||
|
|
||||||
|
remove: function(file,opts){
|
||||||
|
opts = opts || {}
|
||||||
|
const client = opts.public ? publicClient : privateClient;
|
||||||
|
return client.remove(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
}).apply({})
|
||||||
2722
dist/xrfragment.py
vendored
Normal file
2722
dist/xrfragment.py
vendored
Normal file
File diff suppressed because it is too large
Load diff
5882
dist/xrfragment.three.js
vendored
Normal file
5882
dist/xrfragment.three.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5883
dist/xrfragment.three.module.js
vendored
Normal file
5883
dist/xrfragment.three.module.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
doc/.RFC_XR_Fragments.md.swp
Normal file
BIN
doc/.RFC_XR_Fragments.md.swp
Normal file
Binary file not shown.
BIN
doc/RF6_XR_Fragments.png
Normal file
BIN
doc/RF6_XR_Fragments.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
1640
doc/RFC_XR_Fragments.html
Normal file
1640
doc/RFC_XR_Fragments.html
Normal file
File diff suppressed because it is too large
Load diff
1007
doc/RFC_XR_Fragments.md
Normal file
1007
doc/RFC_XR_Fragments.md
Normal file
File diff suppressed because it is too large
Load diff
614
doc/RFC_XR_Fragments.md.bak
Normal file
614
doc/RFC_XR_Fragments.md.bak
Normal file
|
|
@ -0,0 +1,614 @@
|
||||||
|
%%%
|
||||||
|
Title = "XR Fragments"
|
||||||
|
area = "Internet"
|
||||||
|
workgroup = "Internet Engineering Task Force"
|
||||||
|
|
||||||
|
[seriesInfo]
|
||||||
|
name = "XR-Fragments"
|
||||||
|
value = "draft-XRFRAGMENTS-leonvankammen-00"
|
||||||
|
stream = "IETF"
|
||||||
|
status = "informational"
|
||||||
|
|
||||||
|
date = 2023-04-12T00:00:00Z
|
||||||
|
|
||||||
|
[[author]]
|
||||||
|
initials="L.R."
|
||||||
|
surname="van Kammen"
|
||||||
|
fullname="L.R. van Kammen"
|
||||||
|
|
||||||
|
%%%
|
||||||
|
|
||||||
|
<!-- for annotated version see: https://raw.githubusercontent.com/ietf-tools/rfcxml-templates-and-schemas/main/draft-rfcxml-general-template-annotated-00.xml -->
|
||||||
|
|
||||||
|
<!--{
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
body{
|
||||||
|
font-family: monospace;
|
||||||
|
max-width: 1000px;
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 0% 20%;
|
||||||
|
line-height: 30px;
|
||||||
|
color:#555;
|
||||||
|
background:#F0F0F3
|
||||||
|
}
|
||||||
|
h1 { margin-top:40px; }
|
||||||
|
pre{ line-height:18px; }
|
||||||
|
a,a:visited,a:active{ color: #70f; }
|
||||||
|
code{
|
||||||
|
border: 1px solid #AAA;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0px 5px 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre{
|
||||||
|
line-height: 18px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
pre + code {
|
||||||
|
background:#DDD;
|
||||||
|
}
|
||||||
|
pre>code{
|
||||||
|
border:none;
|
||||||
|
border-radius:0px;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
blockquote{
|
||||||
|
padding-left: 30px;
|
||||||
|
margin: 0;
|
||||||
|
border-left: 5px solid #CCC;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
text-align: left;
|
||||||
|
padding-right:45px;
|
||||||
|
padding-left:7px;
|
||||||
|
background: #DDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-bottom: 1px solid #CCC;
|
||||||
|
font-size:13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<h1>XR Fragments</h1>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
stream: IETF
|
||||||
|
area: Internet
|
||||||
|
status: informational
|
||||||
|
author: Leon van Kammen
|
||||||
|
date: 2023-04-12T00:00:00Z
|
||||||
|
workgroup: Internet Engineering Task Force
|
||||||
|
value: draft-XRFRAGMENTS-leonvankammen-00
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
|
||||||
|
}-->
|
||||||
|
|
||||||
|
.# Abstract
|
||||||
|
|
||||||
|
This draft offers a specification for 4D URLs & navigation, to link 3D scenes and text together with- or without a network-connection.<br>
|
||||||
|
The specification promotes spatial addressibility, sharing, navigation, query-ing and tagging interactive (text)objects across for (XR) Browsers.<br>
|
||||||
|
XR Fragments allows us to enrich existing dataformats, by recursive use of existing proven technologies like [URI Fragments](https://en.wikipedia.org/wiki/URI_fragment) and BibTags notation.<br>
|
||||||
|
|
||||||
|
> Almost every idea in this document is demonstrated at [https://xrfragment.org](https://xrfragment.org)
|
||||||
|
|
||||||
|
{mainmatter}
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
How can we add more features to existing text & 3D scenes, without introducing new dataformats?<br>
|
||||||
|
Historically, there's many attempts to create the ultimate markuplanguage or 3D fileformat.<br>
|
||||||
|
However, thru the lens of authoring, their lowest common denominator is still: plain text.<br>
|
||||||
|
XR Fragments allows us to enrich/connect existing dataformats, by recursive use of existing technologies:<br>
|
||||||
|
|
||||||
|
1. addressibility and navigation of 3D scenes/objects: [URI Fragments](https://en.wikipedia.org/wiki/URI_fragment) + src/href spatial metadata
|
||||||
|
1. hasslefree tagging across text and spatial objects using [BibTags](https://en.wikipedia.org/wiki/BibTeX) as appendix (see [visual-meta](https://visual-meta.info) e.g.)
|
||||||
|
|
||||||
|
> NOTE: The chapters in this document are ordered from highlevel to lowlevel (technical) as much as possible
|
||||||
|
|
||||||
|
# Core principle
|
||||||
|
|
||||||
|
XR Fragments strives to serve (nontechnical/fuzzy) humans first, and machine(implementations) later, by ensuring hasslefree text-vs-thought feedback loops.<br>
|
||||||
|
This also means that the repair-ability of machine-matters should be human friendly too (not too complex).<br>
|
||||||
|
|
||||||
|
> "When a car breaks down, the ones **without** turbosupercharger are easier to fix"
|
||||||
|
|
||||||
|
Let's always focus on average humans: the 'fuzzy symbolical mind' must be served first, before serving the greater ['categorized typesafe RDF hive mind'](https://en.wikipedia.org/wiki/Borg)).
|
||||||
|
|
||||||
|
> Humans first, machines (AI) later.
|
||||||
|
|
||||||
|
# Conventions and Definitions
|
||||||
|
|
||||||
|
|definition | explanation |
|
||||||
|
|----------------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
|human | a sentient being who thinks fuzzy, absorbs, and shares thought (by plain text, not markuplanguage) |
|
||||||
|
|scene | a (local/remote) 3D scene or 3D file (index.gltf e.g.) |
|
||||||
|
|3D object | an object inside a scene characterized by vertex-, face- and customproperty data. |
|
||||||
|
|metadata | custom properties of text, 3D Scene or Object(nodes), relevant to machines and a human minority (academics/developers) |
|
||||||
|
|XR fragment | URI Fragment with spatial hints like `#pos=0,0,0&t=1,100` e.g. |
|
||||||
|
|src | (HTML-piggybacked) metadata of a 3D object which instances content |
|
||||||
|
|href | (HTML-piggybacked) metadata of a 3D object which links to content |
|
||||||
|
|query | an URI Fragment-operator which queries object(s) from a scene like `#q=cube` |
|
||||||
|
|visual-meta | [visual-meta](https://visual.meta.info) data appended to text/books/papers which is indirectly visible/editable in XR. |
|
||||||
|
|requestless metadata | opposite of networked metadata (RDF/HTML requests can easily fan out into framerate-dropping, hence not used a lot in games). |
|
||||||
|
|FPS | frames per second in spatial experiences (games,VR,AR e.g.), should be as high as possible |
|
||||||
|
|introspective | inward sensemaking ("I feel this belongs to that") |
|
||||||
|
|extrospective | outward sensemaking ("I'm fairly sure John is a person who lives in oklahoma") |
|
||||||
|
|`◻` | ascii representation of an 3D object/mesh |
|
||||||
|
|(un)obtrusive | obtrusive: wrapping human text/thought in XML/HTML/JSON obfuscates human text into a salad of machine-symbols and words |
|
||||||
|
|BibTeX | simple tagging/citing/referencing standard for plaintext |
|
||||||
|
|BibTag | a BibTeX tag |
|
||||||
|
|
||||||
|
# List of URI Fragments
|
||||||
|
|
||||||
|
| fragment | type | example | info |
|
||||||
|
|--------------|----------|-------------------|-------------------------------------------------------------------|
|
||||||
|
| `#pos` | vector3 | `#pos=0.5,0,0` | positions camera to xyz-coord 0.5,0,0 |
|
||||||
|
| `#rot` | vector3 | `#rot=0,90,0` | rotates camera to xyz-coord 0.5,0,0 |
|
||||||
|
| `#t` | vector2 | `#t=500,1000` | sets animation-loop range between frame 500 and 1000 |
|
||||||
|
| `#......` | string | `#.cubes` `#cube` | object(s) of interest (fragment to object name or class mapping) |
|
||||||
|
|
||||||
|
> xyz coordinates are similar to ones found in SVG Media Fragments
|
||||||
|
|
||||||
|
# List of metadata for 3D nodes
|
||||||
|
|
||||||
|
| key | type | example (JSON) | info |
|
||||||
|
|--------------|----------|--------------------|--------------------------------------------------------|
|
||||||
|
| `name` | string | `"name": "cube"` | available in all 3D fileformats & scenes |
|
||||||
|
| `class` | string | `"class": "cubes"` | available through custom property in 3D fileformats |
|
||||||
|
| `href` | string | `"href": "b.gltf"` | available through custom property in 3D fileformats |
|
||||||
|
| `src` | string | `"src": "#q=cube"` | available through custom property in 3D fileformats |
|
||||||
|
|
||||||
|
Popular compatible 3D fileformats: `.gltf`, `.obj`, `.fbx`, `.usdz`, `.json` (THREEjs), `COLLADA` and so on.
|
||||||
|
|
||||||
|
> NOTE: XR Fragments are file-agnostic, which means that the metadata exist in programmatic 3D scene(nodes) too.
|
||||||
|
|
||||||
|
# Navigating 3D
|
||||||
|
|
||||||
|
Here's an ascii representation of a 3D scene-graph which contains 3D objects `◻` and their metadata:
|
||||||
|
|
||||||
|
```
|
||||||
|
+--------------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| index.gltf |
|
||||||
|
| │ |
|
||||||
|
| ├── ◻ buttonA |
|
||||||
|
| │ └ href: #pos=1,0,1&t=100,200 |
|
||||||
|
| │ |
|
||||||
|
| └── ◻ buttonB |
|
||||||
|
| └ href: other.fbx | <-- file-agnostic (can be .gltf .obj etc)
|
||||||
|
| |
|
||||||
|
+--------------------------------------------------------+
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
An XR Fragment-compatible browser viewing this scene, allows the end-user to interact with the `buttonA` and `buttonB`.<br>
|
||||||
|
In case of `buttonA` the end-user will be teleported to another location and time in the **current loaded scene**, but `buttonB` will
|
||||||
|
**replace the current scene** with a new one, like `other.fbx`.
|
||||||
|
|
||||||
|
# Embedding 3D content
|
||||||
|
|
||||||
|
Here's an ascii representation of a 3D scene-graph with 3D objects `◻` which embeds remote & local 3D objects `◻` (without) using queries:
|
||||||
|
|
||||||
|
```
|
||||||
|
+--------------------------------------------------------+ +-------------------------+
|
||||||
|
| | | |
|
||||||
|
| index.gltf | | ocean.com/aquarium.fbx |
|
||||||
|
| │ | | │ |
|
||||||
|
| ├── ◻ canvas | | └── ◻ fishbowl |
|
||||||
|
| │ └ src: painting.png | | ├─ ◻ bass |
|
||||||
|
| │ | | └─ ◻ tuna |
|
||||||
|
| ├── ◻ aquariumcube | | |
|
||||||
|
| │ └ src: ://rescue.com/fish.gltf#q=bass%20tuna | +-------------------------+
|
||||||
|
| │ |
|
||||||
|
| ├── ◻ bedroom |
|
||||||
|
| │ └ src: #q=canvas |
|
||||||
|
| │ |
|
||||||
|
| └── ◻ livingroom |
|
||||||
|
| └ src: #q=canvas |
|
||||||
|
| |
|
||||||
|
+--------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
An XR Fragment-compatible browser viewing this scene, lazy-loads and projects `painting.png` onto the (plane) object called `canvas` (which is copy-instanced in the bed and livingroom).<br>
|
||||||
|
Also, after lazy-loading `ocean.com/aquarium.gltf`, only the queried objects `bass` and `tuna` will be instanced inside `aquariumcube`.<br>
|
||||||
|
Resizing will be happen accordingly to its placeholder object `aquariumcube`, see chapter Scaling.<br>
|
||||||
|
|
||||||
|
# XR Fragment queries
|
||||||
|
|
||||||
|
Include, exclude, hide/shows objects using space-separated strings:
|
||||||
|
|
||||||
|
* `#q=cube`
|
||||||
|
* `#q=cube -ball_inside_cube`
|
||||||
|
* `#q=* -sky`
|
||||||
|
* `#q=-.language .english`
|
||||||
|
* `#q=cube&rot=0,90,0`
|
||||||
|
* `#q=price:>2 price:<5`
|
||||||
|
|
||||||
|
It's simple but powerful syntax which allows <b>css</b>-like class/id-selectors with a searchengine prompt-style feeling:
|
||||||
|
|
||||||
|
1. queries are showing/hiding objects **only** when defined as `src` value (prevents sharing of scene-tampered URL's).
|
||||||
|
1. queries are highlighting objects when defined in the top-Level (browser) URL (bar).
|
||||||
|
1. search words like `cube` and `foo` in `#q=cube foo` are matched against 3D object names or custom metadata-key(values)
|
||||||
|
1. search words like `cube` and `foo` in `#q=cube foo` are matched against tags (BibTeX) inside plaintext `src` values like `@cube{redcube, ...` e.g.
|
||||||
|
1. `#` equals `#q=*`
|
||||||
|
1. words starting with `.` like `.german` match class-metadata of 3D objects like `"class":"german"`
|
||||||
|
1. words starting with `.` like `.german` match class-metadata of (BibTeX) tags in XR Text objects like `@german{KarlHeinz, ...` e.g.
|
||||||
|
|
||||||
|
> **For example**: `#q=.foo` is a shorthand for `#q=class:foo`, which will select objects with custom property `class`:`foo`. Just a simple `#q=cube` will simply select an object named `cube`.
|
||||||
|
|
||||||
|
* see [an example video here](https://coderofsalvation.github.io/xrfragment.media/queries.mp4)
|
||||||
|
|
||||||
|
## including/excluding
|
||||||
|
|
||||||
|
| operator | info |
|
||||||
|
|----------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| `*` | select all objects (only useful in `src` custom property) |
|
||||||
|
| `-` | removes/hides object(s) |
|
||||||
|
| `:` | indicates an object-embedded custom property key/value |
|
||||||
|
| `.` | alias for `"class" :".foo"` equals `class:foo` |
|
||||||
|
| `>` `<` | compare float or int number |
|
||||||
|
| `/` | reference to root-scene.<br>Useful in case of (preventing) showing/hiding objects in nested scenes (instanced by `src`) (*) |
|
||||||
|
|
||||||
|
> \* = `#q=-/cube` hides object `cube` only in the root-scene (not nested `cube` objects)<br> `#q=-cube` hides both object `cube` in the root-scene <b>AND</b> nested `skybox` objects |
|
||||||
|
|
||||||
|
[» example implementation](https://github.com/coderofsalvation/xrfragment/blob/main/src/3rd/js/three/xrf/q.js)
|
||||||
|
[» example 3D asset](https://github.com/coderofsalvation/xrfragment/blob/main/example/assets/query.gltf#L192)
|
||||||
|
[» discussion](https://github.com/coderofsalvation/xrfragment/issues/3)
|
||||||
|
|
||||||
|
## Query Parser
|
||||||
|
|
||||||
|
Here's how to write a query parser:
|
||||||
|
|
||||||
|
1. create an associative array/object to store query-arguments as objects
|
||||||
|
1. detect object id's & properties `foo:1` and `foo` (reference regex: `/^.*:[><=!]?/` )
|
||||||
|
1. detect excluders like `-foo`,`-foo:1`,`-.foo`,`-/foo` (reference regex: `/^-/` )
|
||||||
|
1. detect root selectors like `/foo` (reference regex: `/^[-]?\//` )
|
||||||
|
1. detect class selectors like `.foo` (reference regex: `/^[-]?class$/` )
|
||||||
|
1. detect number values like `foo:1` (reference regex: `/^[0-9\.]+$/` )
|
||||||
|
1. expand aliases like `.foo` into `class:foo`
|
||||||
|
1. for every query token split string on `:`
|
||||||
|
1. create an empty array `rules`
|
||||||
|
1. then strip key-operator: convert "-foo" into "foo"
|
||||||
|
1. add operator and value to rule-array
|
||||||
|
1. therefore we we set `id` to `true` or `false` (false=excluder `-`)
|
||||||
|
1. and we set `root` to `true` or `false` (true=`/` root selector is present)
|
||||||
|
1. we convert key '/foo' into 'foo'
|
||||||
|
1. finally we add the key/value to the store like `store.foo = {id:false,root:true}` e.g.
|
||||||
|
|
||||||
|
> An example query-parser (which compiles to many languages) can be [found here](https://github.com/coderofsalvation/xrfragment/blob/main/src/xrfragment/Query.hx)
|
||||||
|
|
||||||
|
## XR Fragment URI Grammar
|
||||||
|
|
||||||
|
```
|
||||||
|
reserved = gen-delims / sub-delims
|
||||||
|
gen-delims = "#" / "&"
|
||||||
|
sub-delims = "," / "="
|
||||||
|
```
|
||||||
|
|
||||||
|
> Example: `://foo.com/my3d.gltf#pos=1,0,0&prio=-5&t=0,100`
|
||||||
|
|
||||||
|
| Demo | Explanation |
|
||||||
|
|-------------------------------|---------------------------------|
|
||||||
|
| `pos=1,2,3` | vector/coordinate argument e.g. |
|
||||||
|
| `pos=1,2,3&rot=0,90,0&q=.foo` | combinators |
|
||||||
|
|
||||||
|
|
||||||
|
# Text in XR (tagging,linking to spatial objects)
|
||||||
|
|
||||||
|
We still think and speak in simple text, not in HTML or RDF.<br>
|
||||||
|
The most advanced human will probably not shout `<h1>FIRE!</h1>` in case of emergency.<br>
|
||||||
|
Given the new dawn of (non-keyboard) XR interfaces, keeping text as is (not obscuring with markup) is preferred.<br>
|
||||||
|
Ideally metadata must come **later with** text, but not **obfuscate** the text, or **in another** file.<br>
|
||||||
|
|
||||||
|
> Humans first, machines (AI) later ([core principle](#core-principle)
|
||||||
|
|
||||||
|
This way:
|
||||||
|
|
||||||
|
1. XR Fragments allows <b id="tagging-text">hasslefree XR text tagging</b>, using BibTeX metadata **at the end of content** (like [visual-meta](https://visual.meta.info)).
|
||||||
|
1. XR Fragments allows hasslefree <a href="#textual-tag">textual tagging</a>, <a href="#spatial-tag">spatial tagging</a>, and <a href="#supra-tagging">supra tagging</a>, by mapping 3D/text object (class)names using BibTeX 'tags'
|
||||||
|
1. Bibs/BibTeX-appendices is first-choice **requestless metadata**-layer for XR text, HTML/RDF/JSON is great (but fits better in the application-layer)
|
||||||
|
1. Default font (unless specified otherwise) is a modern monospace font, for maximized tabular expressiveness (see [the core principle](#core-principle)).
|
||||||
|
1. anti-pattern: hardcoupling a mandatory **obtrusive markuplanguage** or framework with an XR browsers (HTML/VRML/Javascript) (see [the core principle](#core-principle))
|
||||||
|
1. anti-pattern: limiting human introspection, by immediately funneling human thought into typesafe, precise, pre-categorized metadata like RDF (see [the core principle](#core-principle))
|
||||||
|
|
||||||
|
This allows recursive connections between text itself, as well as 3D objects and vice versa, using **BibTags** :
|
||||||
|
|
||||||
|
```
|
||||||
|
+---------------------------------------------+ +------------------+
|
||||||
|
| My Notes | | / \ |
|
||||||
|
| | | / \ |
|
||||||
|
| The houses here are built in baroque style. | | /house\ |
|
||||||
|
| | | |_____| |
|
||||||
|
| | +---------|--------+
|
||||||
|
| @house{houses, >----'house'--------| class/name match?
|
||||||
|
| url = {#.house} >----'houses'-------` class/name match?
|
||||||
|
| } |
|
||||||
|
+---------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
> The enduser can add connections by speaking/typing/scanning [hashtagbibs](https://github.com/coderofsalvation/hashtagbibs) which the XR Browser can expand to BibTags.
|
||||||
|
|
||||||
|
This allows instant realtime tagging of objects at various scopes:
|
||||||
|
|
||||||
|
| scope | matching algo |
|
||||||
|
|---------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| <b id="textual-tagging">textual</b> | text containing 'houses' is now automatically tagged with 'house' (incl. plaintext `src` child nodes) |
|
||||||
|
| <b id="spatial-tagging">spatial</b> | spatial object(s) with `"class":"house"` (because of `{#.house}`) are now automatically tagged with 'house' (incl. child nodes) |
|
||||||
|
| <b id="supra-tagging">supra</b> | text- or spatial-object(s) (non-descendant nodes) elsewhere, named 'house', are automatically tagged with 'house' (current node to root node) |
|
||||||
|
| <b id="omni-tagging">omni</b> | text- or spatial-object(s) (non-descendant nodes) elsewhere, containing class/name 'house', are automatically tagged with 'house' (too node to all nodes) |
|
||||||
|
| <b id="infinite-tagging">infinite</b> | text- or spatial-object(s) (non-descendant nodes) elsewhere, containing class/name 'house' or 'houses', are automatically tagged with 'house' (too node to all nodes) |
|
||||||
|
|
||||||
|
This empowers the enduser spatial expressiveness (see [the core principle](#core-principle)): spatial wires can be rendered, words can be highlighted, spatial objects can be highlighted/moved/scaled, links can be manipulated by the user.<br>
|
||||||
|
The simplicity of appending BibTeX 'tags' (humans first, machines later) is also demonstrated by [visual-meta](https://visual-meta.info) in greater detail.
|
||||||
|
|
||||||
|
1. The XR Browser needs to adjust tag-scope based on the endusers needs/focus (infinite tagging only makes sense when environment is scaled down significantly)
|
||||||
|
1. The XR Browser should always allow the human to view/edit the metadata, by clicking 'toggle metadata' on the 'back' (contextmenu e.g.) of any XR text, anywhere anytime.
|
||||||
|
|
||||||
|
> NOTE: infinite matches both 'house' and 'houses' in text, as well as spatial objects with `"class":"house"` or name "house". This multiplexing of id/category is deliberate because of [the core principle](#core-principle).
|
||||||
|
|
||||||
|
## Default Data URI mimetype
|
||||||
|
|
||||||
|
The `src`-values work as expected (respecting mime-types), however:
|
||||||
|
|
||||||
|
The XR Fragment specification bumps the traditional default browser-mimetype
|
||||||
|
|
||||||
|
`text/plain;charset=US-ASCII`
|
||||||
|
|
||||||
|
to a hashtagbib(tex)-friendly one:
|
||||||
|
|
||||||
|
`text/plain;charset=utf-8;bib=^@`
|
||||||
|
|
||||||
|
This indicates that:
|
||||||
|
|
||||||
|
* utf-8 is supported by default
|
||||||
|
* [hashtagbibs](https://github.com/coderofsalvation/hashtagbibs) are expanded to [bibtags](https://en.wikipedia.org/wiki/BibTeX)
|
||||||
|
* lines matching regex `^@` will automatically get filtered out, in order to:
|
||||||
|
* links between textual/spatial objects can automatically be detected
|
||||||
|
* bibtag appendices ([visual-meta](https://visual-meta.info) can be interpreted e.g.
|
||||||
|
|
||||||
|
> for more info on this mimetype see [bibs](https://github.com/coderofsalvation/hashtagbibs)
|
||||||
|
|
||||||
|
Advantages:
|
||||||
|
|
||||||
|
* out-of-the-box (de)multiplex human text and metadata in one go (see [the core principle](#core-principle))
|
||||||
|
* no network-overhead for metadata (see [the core principle](#core-principle))
|
||||||
|
* ensuring high FPS: HTML/RDF historically is too 'requesty'/'parsy' for game studios
|
||||||
|
* rich send/receive/copy-paste everywhere by default, metadata being retained (see [the core principle](#core-principle))
|
||||||
|
* netto result: less webservices, therefore less servers, and overall better FPS in XR
|
||||||
|
|
||||||
|
> This significantly expands expressiveness and portability of human tagged text, by **postponing machine-concerns to the end of the human text** in contrast to literal interweaving of content and markupsymbols (or extra network requests, webservices e.g.).
|
||||||
|
|
||||||
|
For all other purposes, regular mimetypes can be used (but are not required by the spec).<br>
|
||||||
|
|
||||||
|
## URL and Data URI
|
||||||
|
|
||||||
|
```
|
||||||
|
+--------------------------------------------------------------+ +------------------------+
|
||||||
|
| | | author.com/article.txt |
|
||||||
|
| index.gltf | +------------------------+
|
||||||
|
| │ | | |
|
||||||
|
| ├── ◻ article_canvas | | Hello friends. |
|
||||||
|
| │ └ src: ://author.com/article.txt | | |
|
||||||
|
| │ | | @friend{friends |
|
||||||
|
| └── ◻ note_canvas | | ... |
|
||||||
|
| └ src:`data:welcome human\n@...` | | } |
|
||||||
|
| | +------------------------+
|
||||||
|
| |
|
||||||
|
+--------------------------------------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
The enduser will only see `welcome human` and `Hello friends` rendered spatially.
|
||||||
|
The beauty is that text (AND visual-meta) in Data URI promotes rich copy-paste.
|
||||||
|
In both cases, the text gets rendered immediately (onto a plane geometry, hence the name '_canvas').
|
||||||
|
The XR Fragment-compatible browser can let the enduser access visual-meta(data)-fields after interacting with the object (contextmenu e.g.).
|
||||||
|
|
||||||
|
> additional tagging using [bibs](https://github.com/coderofsalvation/hashtagbibs): to tag spatial object `note_canvas` with 'todo', the enduser can type or speak `@note_canvas@todo`
|
||||||
|
|
||||||
|
The mapping between 3D objects and text (src-data) is simple (the :
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------------------------------+
|
||||||
|
| |
|
||||||
|
| index.gltf |
|
||||||
|
| │ |
|
||||||
|
| └── ◻ rentalhouse |
|
||||||
|
| └ class: house <----------------- matches -------+
|
||||||
|
| └ ◻ note | |
|
||||||
|
| └ src:`data: todo: call owner | hashtagbib |
|
||||||
|
| #owner@house@todo | ----> expands to @house{owner,
|
||||||
|
| | bibtex: }
|
||||||
|
| ` | @contact{
|
||||||
|
+------------------------------------------------+ }
|
||||||
|
```
|
||||||
|
|
||||||
|
Bi-directional mapping between 3D object names and/or classnames and text using bibs,BibTags & XR Fragments, allows for rich interlinking between text and 3D objects:
|
||||||
|
|
||||||
|
1. When the user surfs to https://.../index.gltf#rentalhouse the XR Fragments-parser points the enduser to the rentalhouse object, and can show contextual info about it.
|
||||||
|
2. When (partial) remote content is embedded thru XR Fragment queries (see XR Fragment queries), indirectly related metadata can be embedded along.
|
||||||
|
|
||||||
|
## Bibs & BibTeX: lowest common denominator for linking data
|
||||||
|
|
||||||
|
> "When a car breaks down, the ones **without** turbosupercharger are easier to fix"
|
||||||
|
|
||||||
|
Unlike XML or JSON, BibTex is typeless, unnested, and uncomplicated, hence a great advantage for introspection.<br>
|
||||||
|
It's a missing sensemaking precursor to extrospective RDF.<br>
|
||||||
|
BibTeX-appendices are already used in the digital AND physical world (academic books, [visual-meta](https://visual-meta.info)), perhaps due to its terseness & simplicity.<br>
|
||||||
|
In that sense, it's one step up from the `.ini` fileformat (which has never leaked into the physical world like BibTex):
|
||||||
|
|
||||||
|
1. <b id="frictionless-copy-paste">frictionless copy/pasting</b> (by humans) of (unobtrusive) content AND metadata
|
||||||
|
1. an introspective 'sketchpad' for metadata, which can (optionally) mature into RDF later
|
||||||
|
|
||||||
|
| characteristic | UTF8 Plain Text (with BibTeX) | RDF |
|
||||||
|
|------------------------------------|-------------------------------|---------------------------|
|
||||||
|
| perspective | introspective | extrospective |
|
||||||
|
| structure | fuzzy (sensemaking) | precise |
|
||||||
|
| space/scope | local | world |
|
||||||
|
| everything is text (string) | yes | no |
|
||||||
|
| voice/paper-friendly | [bibs](https://github.com/coderofsalvation/hashtagbibs) | no |
|
||||||
|
| leaves (dictated) text intact | yes | no |
|
||||||
|
| markup language | just an appendix | ~4 different |
|
||||||
|
| polyglot format | no | yes |
|
||||||
|
| easy to copy/paste content+metadata| yes | up to application |
|
||||||
|
| easy to write/repair for layman | yes | depends |
|
||||||
|
| easy to (de)serialize | yes (fits on A4 paper) | depends |
|
||||||
|
| infrastructure | selfcontained (plain text) | (semi)networked |
|
||||||
|
| freeform tagging/annotation | yes, terse | yes, verbose |
|
||||||
|
| can be appended to text-content | yes | up to application |
|
||||||
|
| copy-paste text preserves metadata | yes | up to application |
|
||||||
|
| emoji | yes | depends on encoding |
|
||||||
|
| predicates | free | semi pre-determined |
|
||||||
|
| implementation/network overhead | no | depends |
|
||||||
|
| used in (physical) books/PDF | yes (visual-meta) | no |
|
||||||
|
| terse non-verb predicates | yes | no |
|
||||||
|
| nested structures | no (but: BibTex rulers) | yes |
|
||||||
|
|
||||||
|
> To keep XR Fragments a lightweight spec, BibTeX is used for rudimentary text/spatial tagging (not JSON, RDF or a scripting language because they're harder to write/speak/repair.).
|
||||||
|
|
||||||
|
Applications are also free to attach any JSON(LD / RDF) to spatial objects using custom properties (but is not interpreted by this spec).
|
||||||
|
|
||||||
|
## XR Text example parser
|
||||||
|
|
||||||
|
|
||||||
|
1. The XR Fragments spec does not aim to harden the BiBTeX format
|
||||||
|
2. respect multi-line BibTex values because of [the core principle](#core-principle)
|
||||||
|
3. Expand hashtag(bibs) and rulers (like `${visual-meta-start}`) according to the [hashtagbibs spec](https://github.com/coderofsalvation/hashtagbibs)
|
||||||
|
4. BibTeX snippets should always start in the beginning of a line (regex: ^@), hence mimetype `text/plain;charset=utf-8;bib=^@`
|
||||||
|
|
||||||
|
Here's an XR Text (de)multiplexer in javascript, which ticks all the above boxes:
|
||||||
|
|
||||||
|
```
|
||||||
|
xrtext = {
|
||||||
|
|
||||||
|
expandBibs: (text) => {
|
||||||
|
let bibs = { regex: /(#[a-zA-Z0-9_+@\-]+(#)?)/g, tags: {}}
|
||||||
|
text.replace( bibs.regex , (m,k,v) => {
|
||||||
|
tok = m.substr(1).split("@")
|
||||||
|
match = tok.shift()
|
||||||
|
if( tok.length ) tok.map( (t) => bibs.tags[t] = `@${t}{${match},\n}` )
|
||||||
|
else if( match.substr(-1) == '#' )
|
||||||
|
bibs.tags[match] = `@{${match.replace(/#/,'')}}`
|
||||||
|
else bibs.tags[match] = `@${match}{${match},\n}`
|
||||||
|
})
|
||||||
|
return text.replace( bibs.regex, '') + Object.values(bibs.tags).join('\n')
|
||||||
|
},
|
||||||
|
|
||||||
|
decode: (str) => {
|
||||||
|
// bibtex: ↓@ ↓<tag|tag{phrase,|{ruler}> ↓property ↓end
|
||||||
|
let pat = [ /@/, /^\S+[,{}]/, /},/, /}/ ]
|
||||||
|
let tags = [], text='', i=0, prop=''
|
||||||
|
let lines = xrtext.expandBibs(str).replace(/\r?\n/g,'\n').split(/\n/)
|
||||||
|
for( let i = 0; i < lines.length && !String(lines[i]).match( /^@/ ); i++ )
|
||||||
|
text += lines[i]+'\n'
|
||||||
|
|
||||||
|
bibtex = lines.join('\n').substr( text.length )
|
||||||
|
bibtex.split( pat[0] ).map( (t) => {
|
||||||
|
try{
|
||||||
|
let v = {}
|
||||||
|
if( !(t = t.trim()) ) return
|
||||||
|
if( tag = t.match( pat[1] ) ) tag = tag[0]
|
||||||
|
if( tag.match( /^{.*}$/ ) ) return tags.push({ruler:tag})
|
||||||
|
t = t.substr( tag.length )
|
||||||
|
t.split( pat[2] )
|
||||||
|
.map( kv => {
|
||||||
|
if( !(kv = kv.trim()) || kv == "}" ) return
|
||||||
|
v[ kv.match(/\s?(\S+)\s?=/)[1] ] = kv.substr( kv.indexOf("{")+1 )
|
||||||
|
})
|
||||||
|
tags.push( { k:tag, v } )
|
||||||
|
}catch(e){ console.error(e) }
|
||||||
|
})
|
||||||
|
return {text, tags}
|
||||||
|
},
|
||||||
|
|
||||||
|
encode: (text,tags) => {
|
||||||
|
let str = text+"\n"
|
||||||
|
for( let i in tags ){
|
||||||
|
let item = tags[i]
|
||||||
|
if( item.ruler ){
|
||||||
|
str += `@${item.ruler}\n`
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
str += `@${item.k}\n`
|
||||||
|
for( let j in item.v ) str += ` ${j} = {${item.v[j]}}\n`
|
||||||
|
str += `}\n`
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The above functions (de)multiplexe text/metadata, expands bibs, (de)serialize bibtex (and all fits more or less on one A4 paper)
|
||||||
|
|
||||||
|
> above can be used as a startingpoint for LLVM's to translate/steelman to a more formal form/language.
|
||||||
|
|
||||||
|
```
|
||||||
|
str = `
|
||||||
|
hello world
|
||||||
|
here are some hashtagbibs followed by bibtex:
|
||||||
|
|
||||||
|
#world
|
||||||
|
#hello@greeting
|
||||||
|
#another-section#
|
||||||
|
|
||||||
|
@{some-section}
|
||||||
|
@flap{
|
||||||
|
asdf = {23423}
|
||||||
|
}`
|
||||||
|
|
||||||
|
var {tags,text} = xrtext.decode(str) // demultiplex text & bibtex
|
||||||
|
tags.find( (t) => t.k == 'flap{' ).v.asdf = 1 // edit tag
|
||||||
|
tags.push({ k:'bar{', v:{abc:123} }) // add tag
|
||||||
|
console.log( xrtext.encode(text,tags) ) // multiplex text & bibtex back together
|
||||||
|
```
|
||||||
|
This expands to the following (hidden by default) BibTex appendix:
|
||||||
|
|
||||||
|
```
|
||||||
|
hello world
|
||||||
|
here are some hashtagbibs followed by bibtex:
|
||||||
|
|
||||||
|
@{some-section}
|
||||||
|
@flap{
|
||||||
|
asdf = {1}
|
||||||
|
}
|
||||||
|
@world{world,
|
||||||
|
}
|
||||||
|
@greeting{hello,
|
||||||
|
}
|
||||||
|
@{another-section}
|
||||||
|
@bar{
|
||||||
|
abc = {123}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# HYPER copy/paste
|
||||||
|
|
||||||
|
The previous example, offers something exciting compared to simple copy/paste of 3D objects or text.
|
||||||
|
XR Text according to the XR Fragment spec, allows HYPER-copy/paste: time, space and text interlinked.
|
||||||
|
Therefore, the enduser in an XR Fragment-compatible browser can copy/paste/share data in these ways:
|
||||||
|
|
||||||
|
1. time/space: 3D object (current animation-loop)
|
||||||
|
1. text: TeXt object (including BibTeX/visual-meta if any)
|
||||||
|
1. interlinked: Collected objects by visual-meta tag
|
||||||
|
|
||||||
|
# Security Considerations
|
||||||
|
|
||||||
|
Since XR Text contains metadata too, the user should be able to set up tagging-rules, so the copy-paste feature can :
|
||||||
|
|
||||||
|
* filter out sensitive data when copy/pasting (XR text with `class:secret` e.g.)
|
||||||
|
|
||||||
|
# IANA Considerations
|
||||||
|
|
||||||
|
This document has no IANA actions.
|
||||||
|
|
||||||
|
# Acknowledgments
|
||||||
|
|
||||||
|
TODO acknowledge.
|
||||||
BIN
doc/RFC_XR_Fragments.png
Normal file
BIN
doc/RFC_XR_Fragments.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
35994
doc/RFC_XR_Fragments.svg
Normal file
35994
doc/RFC_XR_Fragments.svg
Normal file
File diff suppressed because it is too large
Load diff
|
After Width: | Height: | Size: 1,002 KiB |
1736
doc/RFC_XR_Fragments.txt
Normal file
1736
doc/RFC_XR_Fragments.txt
Normal file
File diff suppressed because it is too large
Load diff
1259
doc/RFC_XR_Fragments.xml
Normal file
1259
doc/RFC_XR_Fragments.xml
Normal file
File diff suppressed because it is too large
Load diff
465
doc/RFC_XR_Macros.html
Normal file
465
doc/RFC_XR_Macros.html
Normal file
|
|
@ -0,0 +1,465 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>XR Macros</title>
|
||||||
|
<meta name="GENERATOR" content="github.com/mmarkdown/mmark Mmark Markdown Processor - mmark.miek.nl">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- for annotated version see: https://raw.githubusercontent.com/ietf-tools/rfcxml-templates-and-schemas/main/draft-rfcxml-general-template-annotated-00.xml -->
|
||||||
|
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
body{
|
||||||
|
font-family: monospace;
|
||||||
|
max-width: 1000px;
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 0% 20%;
|
||||||
|
line-height: 30px;
|
||||||
|
color:#555;
|
||||||
|
background:#F0F0F3
|
||||||
|
}
|
||||||
|
h1 { margin-top:40px; }
|
||||||
|
pre{ line-height:18px; }
|
||||||
|
a,a:visited,a:active{ color: #70f; }
|
||||||
|
code{
|
||||||
|
border: 1px solid #AAA;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0px 5px 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre{
|
||||||
|
line-height: 18px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
pre + code {
|
||||||
|
background:#DDD;
|
||||||
|
}
|
||||||
|
pre>code{
|
||||||
|
border:none;
|
||||||
|
border-radius:0px;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
blockquote{
|
||||||
|
padding-left: 30px;
|
||||||
|
margin: 0;
|
||||||
|
border-left: 5px solid #CCC;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
text-align: left;
|
||||||
|
padding-right:45px;
|
||||||
|
padding-left:7px;
|
||||||
|
background: #DDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-bottom: 1px solid #CCC;
|
||||||
|
font-size:13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<h1>XR Macros</h1>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
stream: IETF
|
||||||
|
area: Internet
|
||||||
|
status: informational
|
||||||
|
author: Leon van Kammen
|
||||||
|
date: 2023-04-12T00:00:00Z
|
||||||
|
workgroup: Internet Engineering Task Force
|
||||||
|
value: draft-XRMACROS-leonvankammen-00
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<h1 class="special" id="abstract">Abstract</h1>
|
||||||
|
|
||||||
|
<p>This draft offers a specification for embedding macros in existing 3D scenes/assets, to offer simple interactions and configure the renderer further.<br>
|
||||||
|
Together with URI Fragments, it allows for rich immersive experiences without the need of a complicated sandboxed scripting languages.</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>Almost every idea in this document is demonstrated at <a href="https://xrfragment.org">https://xrfragment.org</a>, as this spec was created during the <a href="https://xrfragment.org">XR Fragments</a> spec.</p>
|
||||||
|
</blockquote>
|
||||||
|
<section data-matter="main">
|
||||||
|
<h1 id="introduction">Introduction</h1>
|
||||||
|
|
||||||
|
<p>How can we add more features to existing text & 3D scenes, without introducing new dataformats?<br>
|
||||||
|
Historically, there’s many attempts to create the ultimate markuplanguage or 3D fileformat.<br>
|
||||||
|
Their lowest common denominator is: (co)authoring using plain text.<br>
|
||||||
|
Therefore, XR Macros allows us to enrich/connect existing dataformats, by offering a polyglot notation based on existing notations:<br></p>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>getting/setting common used 3D properties using querystring- or JSON-notation</li>
|
||||||
|
<li>targeting 3D properties using the lightweight query notation present in <a href="https://xrfragment.org">XR Fragments</a></li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>NOTE: The chapters in this document are ordered from highlevel to lowlevel (technical) as much as possible</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h1 id="core-principle">Core principle</h1>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>XR Macros use querystrings, but are HTML-agnostic (though pseudo-XR Fragment browsers <strong>can</strong> be implemented on top of HTML/Javascript).</li>
|
||||||
|
<li>An XR Macro is 3D metadata which starts with ‘!’ (<code>!clickme: fog=0,10</code> e.g.)</li>
|
||||||
|
<li>Metadata-values can contain the <code>|</code> symbol to 🎲 roundrobin variable values (<code>!toggleme: fog=0,10|fog=0,1000</code> e.g.)</li>
|
||||||
|
<li>XR Macros acts as simple eventhandlers for URI Fragments: they are automatically published on the (<a href="https://xrfragment.org">XR Fragments</a>) hashbus, to act as events (so more serious scripting languages can react to them as well).</li>
|
||||||
|
<li>XR Macros can assign object metadata (<code>!setlocal: foo=1</code> writes <code>foo:1</code> metadata to the object containing the <code>!setlocal</code> metadata)</li>
|
||||||
|
<li>XR Macros can assign global metadata (<code>!setfoo: #foo=1</code> writes <code>foo:1</code> metadata to the root scene-node)</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>These very simple principles allow for rich interactions and dynamic querying</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h1 id="conventions-and-definitions">Conventions and Definitions</h1>
|
||||||
|
|
||||||
|
<p>See appendix below in case certain terms are not clear.</p>
|
||||||
|
|
||||||
|
<h1 id="list-of-xr-macros">List of XR Macros</h1>
|
||||||
|
|
||||||
|
<p>(XR) Macros can be embedded in 3D assets/scenes.<br>
|
||||||
|
Macros enrich existing spatial content with a lowcode, limited logic-layer, by recursive (economic) use of the querystring syntax (which search engines and <a href="https://xrfragment.org">XR Fragments</a> already uses.<br>
|
||||||
|
This is done by allowing string/integer variables, and the <code>|</code> symbol to roundrobin variable values.<br>
|
||||||
|
Macros also act as events, so more serious scripting languages can react to them as well.<br></p>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>key</th>
|
||||||
|
<th>type</th>
|
||||||
|
<th>example (JSON)</th>
|
||||||
|
<th>function</th>
|
||||||
|
<th>existing compatibility</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><code>@bg</code></td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><code>"@bg":"#cube"</code></td>
|
||||||
|
<td>bg: binds fog near/far based to cube x/y/z (anim) values</td>
|
||||||
|
<td>custom property in 3D fileformats</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><code>@fog</code></td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><code>"@fog":"#cube"</code></td>
|
||||||
|
<td>fog: binds fog near/far based to cube x/y (anim) values</td>
|
||||||
|
<td>custom property in 3D fileformats</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><code>@scroll</code></td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><code>"@scroll":"#cube"</code></td>
|
||||||
|
<td>texturescrolling: binds texture x/y/rot based to cube x/y/z (anim) values</td>
|
||||||
|
<td>custom property in 3D fileformats</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><code>@emissive</code></td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><code>"@emissive":"#cube"</code></td>
|
||||||
|
<td>day/night/mood: binds material’s emissive value to cube x/y/z (anim) values</td>
|
||||||
|
<td>custom property in 3D fileformats</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 id="usecase-click-object">Usecase: click object</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>custom property</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>trigger when</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>!clickme</td>
|
||||||
|
<td>bg=1,1,1&foo=2</td>
|
||||||
|
<td>object clicked</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 id="usecase-conditional-click-object">Usecase: conditional click object</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>custom property</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>trigger when</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>#</td>
|
||||||
|
<td>foo=1</td>
|
||||||
|
<td>scene</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>!clickme</td>
|
||||||
|
<td>q=foo>2&bg=1,1,1</td>
|
||||||
|
<td>object clicked and foo > 2</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>when a user clicks an object with the custom properties above, it should set the backgroundcolor to <code>1,1,1</code> when <code>foo</code> is greater than <code>2</code> (see previous example)</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h2 id="usecase-click-object-roundrobin">Usecase: click object (roundrobin)</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>custom property</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>trigger when</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>!cycleme</td>
|
||||||
|
<td>day|noon|night</td>
|
||||||
|
<td>object clicked</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>day</td>
|
||||||
|
<td>bg=1,1,1</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>noon</td>
|
||||||
|
<td>bg=0.5,0.5,0.5</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>night</td>
|
||||||
|
<td>bg=0,0,0&foo=2</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>when a user clicks an object with the custom properties above, it should trigger either <code>day</code> <code>noon</code> or <code>night</code> in roundrobin fashion.</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h2 id="usecase-click-object-or-uri-fragment-and-scene-load-trigger">Usecase: click object or URI fragment, and scene load trigger</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>custom property</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>trigger when</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>#</td>
|
||||||
|
<td>random</td>
|
||||||
|
<td>scene loaded</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>#random</td>
|
||||||
|
<td>random</td>
|
||||||
|
<td>URL contains #random</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>!random</td>
|
||||||
|
<td>day|noon|night</td>
|
||||||
|
<td>#random, # or click</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>day</td>
|
||||||
|
<td>bg=1,1,1</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>noon</td>
|
||||||
|
<td>bg=0.5,0.5,0.5</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>night</td>
|
||||||
|
<td>bg=0,0,0&foo=2</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 id="usecase-present-context-menu-with-options">Usecase: present context menu with options</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>custom property</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>trigger when</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>!random</td>
|
||||||
|
<td>!day</td>
|
||||||
|
<td>!noon</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>!day</td>
|
||||||
|
<td>bg=1,1,1</td>
|
||||||
|
<td>clicked in contextmenu</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>!noon</td>
|
||||||
|
<td>bg=0.5,0.5,0.5</td>
|
||||||
|
<td>clicked in contextmenu</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>!night</td>
|
||||||
|
<td>bg=0,0,0&foo=2</td>
|
||||||
|
<td>clicked in contextmenu</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>When interacting with an object with more than one <code>!</code>-macro, the XR Browser should offer a contextmenu to execute a macro.</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<p>In a similar way, when <strong>any</strong> <code>!</code>-macro is present on the sceneroot, the XR Browser should offer a context-menu to execute those macro’s.</p>
|
||||||
|
|
||||||
|
<h2 id="event-bubble-flow">Event Bubble-flow</h2>
|
||||||
|
|
||||||
|
<p>click object with (<code>!clickme</code>:<code>AR</code> or <code>!clickme</code>: <code>!reset</code> e.g.)</p>
|
||||||
|
|
||||||
|
<pre><code> ◻
|
||||||
|
│
|
||||||
|
└── does current object contain this property-key (`AR` or `!reset` e.g.)?
|
||||||
|
└── no: is there any (root)object containing property `AR`
|
||||||
|
└── yes: evaluate its (roundrobin) XR macro-value(s) (and exit)
|
||||||
|
└── no: trigger URL: #AR
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<p>click object with (<code>!clickme</code>:<code>#AR|#VR</code> e.g.)</p>
|
||||||
|
|
||||||
|
<pre><code> ◻
|
||||||
|
│
|
||||||
|
└── apply the roundrobin (rotate the options, value `#AR` becomes `#VR` upon next click)
|
||||||
|
└── is there any object with property-key (`#AR` e.g.)?
|
||||||
|
└── no: just update the URL to `#AR`
|
||||||
|
└── yes: apply its value to the scene, and update the URL to `#AR`
|
||||||
|
|
||||||
|
click object with (`!clickme`:`!foo|!bar|!flop` e.g.)
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<p>◻
|
||||||
|
│<br>
|
||||||
|
└── apply the roundrobin (rotate the options, value <code>!foo</code> becomes <code>!bar</code> upon next click)
|
||||||
|
└── is there any object with property-key (<code>!foo</code> e.g.)?
|
||||||
|
└── no: do nothing
|
||||||
|
└── yes: apply its value to the scene
|
||||||
|
“`</p>
|
||||||
|
|
||||||
|
<blockquote>
|
||||||
|
<p>Note that only macro’s can trigger roundrobin values or contextmenu’s, as well as roundrobin values never ending up in the toplevel URL.</p>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
|
<h1 id="security-considerations">Security Considerations</h1>
|
||||||
|
|
||||||
|
<h1 id="iana-considerations">IANA Considerations</h1>
|
||||||
|
|
||||||
|
<p>This document has no IANA actions.</p>
|
||||||
|
|
||||||
|
<h1 id="acknowledgments">Acknowledgments</h1>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://nlnet.nl">NLNET</a></li>
|
||||||
|
<li><a href="https://futureoftext.org">Future of Text</a></li>
|
||||||
|
<li><a href="https://visual-meta.info">visual-meta.info</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h1 id="appendix-definitions">Appendix: Definitions</h1>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>definition</th>
|
||||||
|
<th>explanation</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>scene</td>
|
||||||
|
<td>a (local/remote) 3D scene or 3D file (index.gltf e.g.)</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>3D object</td>
|
||||||
|
<td>an object inside a scene characterized by vertex-, face- and customproperty data.</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>XR fragments</td>
|
||||||
|
<td>URI Fragment with spatial hints like <code>#pos=0,0,0&t=1,100</code> e.g.</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>query</td>
|
||||||
|
<td>an URI Fragment-operator which queries object(s) from a scene like <code>#q=cube</code></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>FPS</td>
|
||||||
|
<td>frames per second in spatial experiences (games,VR,AR e.g.), should be as high as possible</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><code>◻</code></td>
|
||||||
|
<td>ascii representation of an 3D object/mesh</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>(un)obtrusive</td>
|
||||||
|
<td>obtrusive: wrapping human text/thought in XML/HTML/JSON obfuscates human text into a salad of machine-symbols and words</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
251
doc/RFC_XR_Macros.md
Normal file
251
doc/RFC_XR_Macros.md
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
%%%
|
||||||
|
Title = "XR Macros"
|
||||||
|
area = "Internet"
|
||||||
|
workgroup = "Internet Engineering Task Force"
|
||||||
|
|
||||||
|
[seriesInfo]
|
||||||
|
name = "XR-Macros"
|
||||||
|
value = "draft-XRMACROS-leonvankammen-00"
|
||||||
|
stream = "IETF"
|
||||||
|
status = "informational"
|
||||||
|
|
||||||
|
date = 2023-04-12T00:00:00Z
|
||||||
|
|
||||||
|
[[author]]
|
||||||
|
initials="L.R."
|
||||||
|
surname="van Kammen"
|
||||||
|
fullname="L.R. van Kammen"
|
||||||
|
|
||||||
|
%%%
|
||||||
|
|
||||||
|
<!-- for annotated version see: https://raw.githubusercontent.com/ietf-tools/rfcxml-templates-and-schemas/main/draft-rfcxml-general-template-annotated-00.xml -->
|
||||||
|
|
||||||
|
<!--{
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
body{
|
||||||
|
font-family: monospace;
|
||||||
|
max-width: 1000px;
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 0% 20%;
|
||||||
|
line-height: 30px;
|
||||||
|
color:#555;
|
||||||
|
background:#F0F0F3
|
||||||
|
}
|
||||||
|
h1 { margin-top:40px; }
|
||||||
|
pre{ line-height:18px; }
|
||||||
|
a,a:visited,a:active{ color: #70f; }
|
||||||
|
code{
|
||||||
|
border: 1px solid #AAA;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0px 5px 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre{
|
||||||
|
line-height: 18px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
pre + code {
|
||||||
|
background:#DDD;
|
||||||
|
}
|
||||||
|
pre>code{
|
||||||
|
border:none;
|
||||||
|
border-radius:0px;
|
||||||
|
padding:0;
|
||||||
|
}
|
||||||
|
blockquote{
|
||||||
|
padding-left: 30px;
|
||||||
|
margin: 0;
|
||||||
|
border-left: 5px solid #CCC;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
text-align: left;
|
||||||
|
padding-right:45px;
|
||||||
|
padding-left:7px;
|
||||||
|
background: #DDD;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-bottom: 1px solid #CCC;
|
||||||
|
font-size:13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<h1>XR Macros</h1>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
stream: IETF
|
||||||
|
area: Internet
|
||||||
|
status: informational
|
||||||
|
author: Leon van Kammen
|
||||||
|
date: 2023-04-12T00:00:00Z
|
||||||
|
workgroup: Internet Engineering Task Force
|
||||||
|
value: draft-XRMACROS-leonvankammen-00
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
|
||||||
|
}-->
|
||||||
|
|
||||||
|
.# Abstract
|
||||||
|
|
||||||
|
This draft offers a specification for embedding macros in existing 3D scenes/assets, to offer simple interactions and configure the renderer further.<br>
|
||||||
|
Together with URI Fragments, it allows for rich immersive experiences without the need of a complicated sandboxed scripting languages.
|
||||||
|
|
||||||
|
> Almost every idea in this document is demonstrated at [https://xrfragment.org](https://xrfragment.org), as this spec was created during the [XR Fragments](https://xrfragment.org) spec.
|
||||||
|
|
||||||
|
{mainmatter}
|
||||||
|
|
||||||
|
# Introduction
|
||||||
|
|
||||||
|
How can we add more features to existing text & 3D scenes, without introducing new dataformats?<br>
|
||||||
|
Historically, there's many attempts to create the ultimate markuplanguage or 3D fileformat.<br>
|
||||||
|
Their lowest common denominator is: (co)authoring using plain text.<br>
|
||||||
|
Therefore, XR Macros allows us to enrich/connect existing dataformats, by offering a polyglot notation based on existing notations:<br>
|
||||||
|
|
||||||
|
1. getting/setting common used 3D properties using querystring- or JSON-notation
|
||||||
|
1. targeting 3D properties using the lightweight query notation present in [XR Fragments](https://xrfragment.org)
|
||||||
|
|
||||||
|
> NOTE: The chapters in this document are ordered from highlevel to lowlevel (technical) as much as possible
|
||||||
|
|
||||||
|
# Core principle
|
||||||
|
|
||||||
|
1. XR Macros use querystrings, but are HTML-agnostic (though pseudo-XR Fragment browsers **can** be implemented on top of HTML/Javascript).
|
||||||
|
1. An XR Macro is 3D metadata which starts with '!' (`!clickme: fog=0,10` e.g.)
|
||||||
|
1. Metadata-values can contain the `|` symbol to 🎲 roundrobin variable values (`!toggleme: fog=0,10|fog=0,1000` e.g.)
|
||||||
|
1. XR Macros acts as simple eventhandlers for URI Fragments: they are automatically published on the ([XR Fragments](https://xrfragment.org)) hashbus, to act as events (so more serious scripting languages can react to them as well).
|
||||||
|
1. XR Macros can assign object metadata (`!setlocal: foo=1` writes `foo:1` metadata to the object containing the `!setlocal` metadata)
|
||||||
|
1. XR Macros can assign global metadata (`!setfoo: #foo=1` writes `foo:1` metadata to the root scene-node)
|
||||||
|
|
||||||
|
> These very simple principles allow for rich interactions and dynamic querying
|
||||||
|
|
||||||
|
# Conventions and Definitions
|
||||||
|
|
||||||
|
See appendix below in case certain terms are not clear.
|
||||||
|
|
||||||
|
# List of XR Macros
|
||||||
|
|
||||||
|
(XR) Macros can be embedded in 3D assets/scenes.<br>
|
||||||
|
Macros enrich existing spatial content with a lowcode, limited logic-layer, by recursive (economic) use of the querystring syntax (which search engines and [XR Fragments](https://xrfragment.org) already uses.<br>
|
||||||
|
This is done by allowing string/integer variables, and the `|` symbol to roundrobin variable values.<br>
|
||||||
|
Macros also act as events, so more serious scripting languages can react to them as well.<br>
|
||||||
|
|
||||||
|
| key | type | example (JSON) | function | existing compatibility |
|
||||||
|
|--------------|----------|------------------------|---------------------|----------------------------------------|
|
||||||
|
| `@bg` | string | `"@bg":"#cube"` | bg: binds fog near/far based to cube x/y/z (anim) values | custom property in 3D fileformats |
|
||||||
|
| `@fog` | string | `"@fog":"#cube"` | fog: binds fog near/far based to cube x/y (anim) values | custom property in 3D fileformats |
|
||||||
|
| `@scroll` | string | `"@scroll":"#cube"` | texturescrolling: binds texture x/y/rot based to cube x/y/z (anim) values | custom property in 3D fileformats |
|
||||||
|
| `@emissive` | string | `"@emissive":"#cube"` | day/night/mood: binds material's emissive value to cube x/y/z (anim) values | custom property in 3D fileformats |
|
||||||
|
|
||||||
|
## Usecase: click object
|
||||||
|
|
||||||
|
| custom property | value | trigger when |
|
||||||
|
|-----------------|--------------------------|------------------------|
|
||||||
|
| !clickme | bg=1,1,1&foo=2 | object clicked |
|
||||||
|
|
||||||
|
## Usecase: conditional click object
|
||||||
|
|
||||||
|
| custom property | value | trigger when |
|
||||||
|
|-----------------|--------------------------|-----------------------------|
|
||||||
|
| # | foo=1 | scene |
|
||||||
|
| !clickme | q=foo>2&bg=1,1,1 | object clicked and foo > 2 |
|
||||||
|
|
||||||
|
> when a user clicks an object with the custom properties above, it should set the backgroundcolor to `1,1,1` when `foo` is greater than `2` (see previous example)
|
||||||
|
|
||||||
|
## Usecase: click object (roundrobin)
|
||||||
|
|
||||||
|
| custom property | value | trigger when |
|
||||||
|
|-----------------|--------------------------|------------------------|
|
||||||
|
| !cycleme | day|noon|night | object clicked |
|
||||||
|
| day | bg=1,1,1 | roundrobin |
|
||||||
|
| noon | bg=0.5,0.5,0.5 | roundrobin |
|
||||||
|
| night | bg=0,0,0&foo=2 | roundrobin |
|
||||||
|
|
||||||
|
> when a user clicks an object with the custom properties above, it should trigger either `day` `noon` or `night` in roundrobin fashion.
|
||||||
|
|
||||||
|
## Usecase: click object or URI fragment, and scene load trigger
|
||||||
|
|
||||||
|
| custom property | value | trigger when |
|
||||||
|
|-----------------|--------------------------|------------------------|
|
||||||
|
| # | random | scene loaded |
|
||||||
|
| #random | random | URL contains #random |
|
||||||
|
| !random | day|noon|night | #random, # or click |
|
||||||
|
| day | bg=1,1,1 | roundrobin |
|
||||||
|
| noon | bg=0.5,0.5,0.5 | roundrobin |
|
||||||
|
| night | bg=0,0,0&foo=2 | roundrobin |
|
||||||
|
|
||||||
|
## Usecase: present context menu with options
|
||||||
|
|
||||||
|
| custom property | value | trigger when |
|
||||||
|
|-----------------|--------------------------|------------------------|
|
||||||
|
| !random | !day|!noon|!night | clicked in contextmenu |
|
||||||
|
| !day | bg=1,1,1 | clicked in contextmenu |
|
||||||
|
| !noon | bg=0.5,0.5,0.5 | clicked in contextmenu |
|
||||||
|
| !night | bg=0,0,0&foo=2 | clicked in contextmenu |
|
||||||
|
|
||||||
|
> When interacting with an object with more than one `!`-macro, the XR Browser should offer a contextmenu to execute a macro.
|
||||||
|
|
||||||
|
In a similar way, when **any** `!`-macro is present on the sceneroot, the XR Browser should offer a context-menu to execute those macro's.
|
||||||
|
|
||||||
|
## Event Bubble-flow
|
||||||
|
|
||||||
|
click object with (`!clickme`:`AR` or `!clickme`: `!reset` e.g.)
|
||||||
|
```
|
||||||
|
◻
|
||||||
|
│
|
||||||
|
└── does current object contain this property-key (`AR` or `!reset` e.g.)?
|
||||||
|
└── no: is there any (root)object containing property `AR`
|
||||||
|
└── yes: evaluate its (roundrobin) XR macro-value(s) (and exit)
|
||||||
|
└── no: trigger URL: #AR
|
||||||
|
```
|
||||||
|
|
||||||
|
click object with (`!clickme`:`#AR|#VR` e.g.)
|
||||||
|
```
|
||||||
|
◻
|
||||||
|
│
|
||||||
|
└── apply the roundrobin (rotate the options, value `#AR` becomes `#VR` upon next click)
|
||||||
|
└── is there any object with property-key (`#AR` e.g.)?
|
||||||
|
└── no: just update the URL to `#AR`
|
||||||
|
└── yes: apply its value to the scene, and update the URL to `#AR`
|
||||||
|
|
||||||
|
click object with (`!clickme`:`!foo|!bar|!flop` e.g.)
|
||||||
|
```
|
||||||
|
◻
|
||||||
|
│
|
||||||
|
└── apply the roundrobin (rotate the options, value `!foo` becomes `!bar` upon next click)
|
||||||
|
└── is there any object with property-key (`!foo` e.g.)?
|
||||||
|
└── no: do nothing
|
||||||
|
└── yes: apply its value to the scene
|
||||||
|
```
|
||||||
|
|
||||||
|
> Note that only macro's can trigger roundrobin values or contextmenu's, as well as roundrobin values never ending up in the toplevel URL.
|
||||||
|
|
||||||
|
# Security Considerations
|
||||||
|
|
||||||
|
|
||||||
|
# IANA Considerations
|
||||||
|
|
||||||
|
This document has no IANA actions.
|
||||||
|
|
||||||
|
# Acknowledgments
|
||||||
|
|
||||||
|
* [NLNET](https://nlnet.nl)
|
||||||
|
* [Future of Text](https://futureoftext.org)
|
||||||
|
* [visual-meta.info](https://visual-meta.info)
|
||||||
|
|
||||||
|
# Appendix: Definitions
|
||||||
|
|
||||||
|
|definition | explanation |
|
||||||
|
|----------------------|-------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
|scene | a (local/remote) 3D scene or 3D file (index.gltf e.g.) |
|
||||||
|
|3D object | an object inside a scene characterized by vertex-, face- and customproperty data. |
|
||||||
|
|XR fragments | URI Fragment with spatial hints like `#pos=0,0,0&t=1,100` e.g. |
|
||||||
|
|query | an URI Fragment-operator which queries object(s) from a scene like `#q=cube` |
|
||||||
|
|FPS | frames per second in spatial experiences (games,VR,AR e.g.), should be as high as possible |
|
||||||
|
|`◻` | ascii representation of an 3D object/mesh |
|
||||||
|
|(un)obtrusive | obtrusive: wrapping human text/thought in XML/HTML/JSON obfuscates human text into a salad of machine-symbols and words |
|
||||||
|
|
||||||
392
doc/RFC_XR_Macros.txt
Normal file
392
doc/RFC_XR_Macros.txt
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Internet Engineering Task Force L.R. van Kammen
|
||||||
|
Internet-Draft 24 September 2025
|
||||||
|
Intended status: Informational
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
XR Macros
|
||||||
|
draft-XRMACROS-leonvankammen-00
|
||||||
|
|
||||||
|
Abstract
|
||||||
|
|
||||||
|
This draft offers a specification for embedding macros in existing 3D
|
||||||
|
scenes/assets, to offer simple interactions and configure the
|
||||||
|
renderer further.
|
||||||
|
Together with URI Fragments, it allows for rich immersive experiences
|
||||||
|
without the need of a complicated sandboxed scripting languages.
|
||||||
|
|
||||||
|
Almost every idea in this document is demonstrated at
|
||||||
|
https://xrfragment.org (https://xrfragment.org), as this spec was
|
||||||
|
created during the XR Fragments (https://xrfragment.org) spec.
|
||||||
|
|
||||||
|
Status of This Memo
|
||||||
|
|
||||||
|
This Internet-Draft is submitted in full conformance with the
|
||||||
|
provisions of BCP 78 and BCP 79.
|
||||||
|
|
||||||
|
Internet-Drafts are working documents of the Internet Engineering
|
||||||
|
Task Force (IETF). Note that other groups may also distribute
|
||||||
|
working documents as Internet-Drafts. The list of current Internet-
|
||||||
|
Drafts is at https://datatracker.ietf.org/drafts/current/.
|
||||||
|
|
||||||
|
Internet-Drafts are draft documents valid for a maximum of six months
|
||||||
|
and may be updated, replaced, or obsoleted by other documents at any
|
||||||
|
time. It is inappropriate to use Internet-Drafts as reference
|
||||||
|
material or to cite them other than as "work in progress."
|
||||||
|
|
||||||
|
This Internet-Draft will expire on 28 March 2026.
|
||||||
|
|
||||||
|
Copyright Notice
|
||||||
|
|
||||||
|
Copyright (c) 2025 IETF Trust and the persons identified as the
|
||||||
|
document authors. All rights reserved.
|
||||||
|
|
||||||
|
This document is subject to BCP 78 and the IETF Trust's Legal
|
||||||
|
Provisions Relating to IETF Documents (https://trustee.ietf.org/
|
||||||
|
license-info) in effect on the date of publication of this document.
|
||||||
|
Please review these documents carefully, as they describe your rights
|
||||||
|
and restrictions with respect to this document. Code Components
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
van Kammen Expires 28 March 2026 [Page 1]
|
||||||
|
|
||||||
|
Internet-Draft XR Macros September 2025
|
||||||
|
|
||||||
|
|
||||||
|
extracted from this document must include Revised BSD License text as
|
||||||
|
described in Section 4.e of the Trust Legal Provisions and are
|
||||||
|
provided without warranty as described in the Revised BSD License.
|
||||||
|
|
||||||
|
Table of Contents
|
||||||
|
|
||||||
|
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2
|
||||||
|
2. Core principle . . . . . . . . . . . . . . . . . . . . . . . 2
|
||||||
|
3. Conventions and Definitions . . . . . . . . . . . . . . . . . 3
|
||||||
|
4. List of XR Macros . . . . . . . . . . . . . . . . . . . . . . 3
|
||||||
|
4.1. Usecase: click object . . . . . . . . . . . . . . . . . . 4
|
||||||
|
4.2. Usecase: conditional click object . . . . . . . . . . . . 4
|
||||||
|
4.3. Usecase: click object (roundrobin) . . . . . . . . . . . 5
|
||||||
|
4.4. Usecase: click object or URI fragment, and scene load
|
||||||
|
trigger . . . . . . . . . . . . . . . . . . . . . . . . . 5
|
||||||
|
4.5. Usecase: present context menu with options . . . . . . . 6
|
||||||
|
4.6. Event Bubble-flow . . . . . . . . . . . . . . . . . . . . 6
|
||||||
|
5. Security Considerations . . . . . . . . . . . . . . . . . . . 7
|
||||||
|
6. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 7
|
||||||
|
7. Acknowledgments . . . . . . . . . . . . . . . . . . . . . . . 7
|
||||||
|
8. Appendix: Definitions . . . . . . . . . . . . . . . . . . . . 7
|
||||||
|
|
||||||
|
1. Introduction
|
||||||
|
|
||||||
|
How can we add more features to existing text & 3D scenes, without
|
||||||
|
introducing new dataformats?
|
||||||
|
Historically, there's many attempts to create the ultimate
|
||||||
|
markuplanguage or 3D fileformat.
|
||||||
|
Their lowest common denominator is: (co)authoring using plain text.
|
||||||
|
Therefore, XR Macros allows us to enrich/connect existing
|
||||||
|
dataformats, by offering a polyglot notation based on existing
|
||||||
|
notations:
|
||||||
|
|
||||||
|
1. getting/setting common used 3D properties using querystring- or
|
||||||
|
JSON-notation
|
||||||
|
2. targeting 3D properties using the lightweight query notation
|
||||||
|
present in XR Fragments (https://xrfragment.org)
|
||||||
|
|
||||||
|
| NOTE: The chapters in this document are ordered from highlevel to
|
||||||
|
| lowlevel (technical) as much as possible
|
||||||
|
|
||||||
|
2. Core principle
|
||||||
|
|
||||||
|
1. XR Macros use querystrings, but are HTML-agnostic (though pseudo-
|
||||||
|
XR Fragment browsers *can* be implemented on top of HTML/
|
||||||
|
Javascript).
|
||||||
|
2. An XR Macro is 3D metadata which starts with '!' (!clickme:
|
||||||
|
fog=0,10 e.g.)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
van Kammen Expires 28 March 2026 [Page 2]
|
||||||
|
|
||||||
|
Internet-Draft XR Macros September 2025
|
||||||
|
|
||||||
|
|
||||||
|
3. Metadata-values can contain the | symbol to 🎲 roundrobin variable
|
||||||
|
values (!toggleme: fog=0,10|fog=0,1000 e.g.)
|
||||||
|
4. XR Macros acts as simple eventhandlers for URI Fragments: they
|
||||||
|
are automatically published on the (XR Fragments
|
||||||
|
(https://xrfragment.org)) hashbus, to act as events (so more
|
||||||
|
serious scripting languages can react to them as well).
|
||||||
|
5. XR Macros can assign object metadata (!setlocal: foo=1 writes
|
||||||
|
foo:1 metadata to the object containing the !setlocal metadata)
|
||||||
|
6. XR Macros can assign global metadata (!setfoo: #foo=1 writes
|
||||||
|
foo:1 metadata to the root scene-node)
|
||||||
|
|
||||||
|
| These very simple principles allow for rich interactions and
|
||||||
|
| dynamic querying
|
||||||
|
|
||||||
|
3. Conventions and Definitions
|
||||||
|
|
||||||
|
See appendix below in case certain terms are not clear.
|
||||||
|
|
||||||
|
4. List of XR Macros
|
||||||
|
|
||||||
|
(XR) Macros can be embedded in 3D assets/scenes.
|
||||||
|
Macros enrich existing spatial content with a lowcode, limited logic-
|
||||||
|
layer, by recursive (economic) use of the querystring syntax (which
|
||||||
|
search engines and XR Fragments (https://xrfragment.org) already
|
||||||
|
uses.
|
||||||
|
This is done by allowing string/integer variables, and the | symbol
|
||||||
|
to roundrobin variable values.
|
||||||
|
Macros also act as events, so more serious scripting languages can
|
||||||
|
react to them as well.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
van Kammen Expires 28 March 2026 [Page 3]
|
||||||
|
|
||||||
|
Internet-Draft XR Macros September 2025
|
||||||
|
|
||||||
|
|
||||||
|
+=========+======+===================+=================+=============+
|
||||||
|
|key |type |example (JSON) |function |existing |
|
||||||
|
| | | | |compatibility|
|
||||||
|
+=========+======+===================+=================+=============+
|
||||||
|
|@bg |string|"@bg":"#cube" |bg: binds fog |custom |
|
||||||
|
| | | |near/far based to|property in |
|
||||||
|
| | | |cube x/y/z (anim)|3D |
|
||||||
|
| | | |values |fileformats |
|
||||||
|
+---------+------+-------------------+-----------------+-------------+
|
||||||
|
|@fog |string|"@fog":"#cube" |fog: binds fog |custom |
|
||||||
|
| | | |near/far based to|property in |
|
||||||
|
| | | |cube x/y (anim) |3D |
|
||||||
|
| | | |values |fileformats |
|
||||||
|
+---------+------+-------------------+-----------------+-------------+
|
||||||
|
|@scroll |string|"@scroll":"#cube" |texturescrolling:|custom |
|
||||||
|
| | | |binds texture |property in |
|
||||||
|
| | | |x/y/rot based to |3D |
|
||||||
|
| | | |cube x/y/z (anim)|fileformats |
|
||||||
|
| | | |values | |
|
||||||
|
+---------+------+-------------------+-----------------+-------------+
|
||||||
|
|@emissive|string|"@emissive":"#cube"|day/night/mood: |custom |
|
||||||
|
| | | |binds material's |property in |
|
||||||
|
| | | |emissive value to|3D |
|
||||||
|
| | | |cube x/y/z (anim)|fileformats |
|
||||||
|
| | | |values | |
|
||||||
|
+---------+------+-------------------+-----------------+-------------+
|
||||||
|
|
||||||
|
Table 1
|
||||||
|
|
||||||
|
4.1. Usecase: click object
|
||||||
|
|
||||||
|
+=================+================+================+
|
||||||
|
| custom property | value | trigger when |
|
||||||
|
+=================+================+================+
|
||||||
|
| !clickme | bg=1,1,1&foo=2 | object clicked |
|
||||||
|
+-----------------+----------------+----------------+
|
||||||
|
|
||||||
|
Table 2
|
||||||
|
|
||||||
|
4.2. Usecase: conditional click object
|
||||||
|
|
||||||
|
+=================+==================+============================+
|
||||||
|
| custom property | value | trigger when |
|
||||||
|
+=================+==================+============================+
|
||||||
|
| # | foo=1 | scene |
|
||||||
|
+-----------------+------------------+----------------------------+
|
||||||
|
| !clickme | q=foo>2&bg=1,1,1 | object clicked and foo > 2 |
|
||||||
|
+-----------------+------------------+----------------------------+
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
van Kammen Expires 28 March 2026 [Page 4]
|
||||||
|
|
||||||
|
Internet-Draft XR Macros September 2025
|
||||||
|
|
||||||
|
|
||||||
|
Table 3
|
||||||
|
|
||||||
|
| when a user clicks an object with the custom properties above, it
|
||||||
|
| should set the backgroundcolor to 1,1,1 when foo is greater than 2
|
||||||
|
| (see previous example)
|
||||||
|
|
||||||
|
4.3. Usecase: click object (roundrobin)
|
||||||
|
|
||||||
|
+=================+================+================+
|
||||||
|
| custom property | value | trigger when |
|
||||||
|
+=================+================+================+
|
||||||
|
| !cycleme | day|noon|night | object clicked |
|
||||||
|
+-----------------+----------------+----------------+
|
||||||
|
| day | bg=1,1,1 | roundrobin |
|
||||||
|
+-----------------+----------------+----------------+
|
||||||
|
| noon | bg=0.5,0.5,0.5 | roundrobin |
|
||||||
|
+-----------------+----------------+----------------+
|
||||||
|
| night | bg=0,0,0&foo=2 | roundrobin |
|
||||||
|
+-----------------+----------------+----------------+
|
||||||
|
|
||||||
|
Table 4
|
||||||
|
|
||||||
|
| when a user clicks an object with the custom properties above, it
|
||||||
|
| should trigger either day noon or night in roundrobin fashion.
|
||||||
|
|
||||||
|
4.4. Usecase: click object or URI fragment, and scene load trigger
|
||||||
|
|
||||||
|
+=================+================+======================+
|
||||||
|
| custom property | value | trigger when |
|
||||||
|
+=================+================+======================+
|
||||||
|
| # | random | scene loaded |
|
||||||
|
+-----------------+----------------+----------------------+
|
||||||
|
| #random | random | URL contains #random |
|
||||||
|
+-----------------+----------------+----------------------+
|
||||||
|
| !random | day|noon|night | #random, # or click |
|
||||||
|
+-----------------+----------------+----------------------+
|
||||||
|
| day | bg=1,1,1 | roundrobin |
|
||||||
|
+-----------------+----------------+----------------------+
|
||||||
|
| noon | bg=0.5,0.5,0.5 | roundrobin |
|
||||||
|
+-----------------+----------------+----------------------+
|
||||||
|
| night | bg=0,0,0&foo=2 | roundrobin |
|
||||||
|
+-----------------+----------------+----------------------+
|
||||||
|
|
||||||
|
Table 5
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
van Kammen Expires 28 March 2026 [Page 5]
|
||||||
|
|
||||||
|
Internet-Draft XR Macros September 2025
|
||||||
|
|
||||||
|
|
||||||
|
4.5. Usecase: present context menu with options
|
||||||
|
|
||||||
|
+=================+================+========================+
|
||||||
|
| custom property | value | trigger when |
|
||||||
|
+=================+================+========================+
|
||||||
|
| !random | !day | !noon |
|
||||||
|
+-----------------+----------------+------------------------+
|
||||||
|
| !day | bg=1,1,1 | clicked in contextmenu |
|
||||||
|
+-----------------+----------------+------------------------+
|
||||||
|
| !noon | bg=0.5,0.5,0.5 | clicked in contextmenu |
|
||||||
|
+-----------------+----------------+------------------------+
|
||||||
|
| !night | bg=0,0,0&foo=2 | clicked in contextmenu |
|
||||||
|
+-----------------+----------------+------------------------+
|
||||||
|
|
||||||
|
Table 6
|
||||||
|
|
||||||
|
| When interacting with an object with more than one !-macro, the XR
|
||||||
|
| Browser should offer a contextmenu to execute a macro.
|
||||||
|
|
||||||
|
In a similar way, when *any* !-macro is present on the sceneroot, the
|
||||||
|
XR Browser should offer a context-menu to execute those macro's.
|
||||||
|
|
||||||
|
4.6. Event Bubble-flow
|
||||||
|
|
||||||
|
click object with (!clickme:AR or !clickme: !reset e.g.)
|
||||||
|
|
||||||
|
◻
|
||||||
|
│
|
||||||
|
└── does current object contain this property-key (`AR` or `!reset` e.g.)?
|
||||||
|
└── no: is there any (root)object containing property `AR`
|
||||||
|
└── yes: evaluate its (roundrobin) XR macro-value(s) (and exit)
|
||||||
|
└── no: trigger URL: #AR
|
||||||
|
|
||||||
|
click object with (!clickme:#AR|#VR e.g.)
|
||||||
|
|
||||||
|
◻
|
||||||
|
│
|
||||||
|
└── apply the roundrobin (rotate the options, value `#AR` becomes `#VR` upon next click)
|
||||||
|
└── is there any object with property-key (`#AR` e.g.)?
|
||||||
|
└── no: just update the URL to `#AR`
|
||||||
|
└── yes: apply its value to the scene, and update the URL to `#AR`
|
||||||
|
|
||||||
|
click object with (`!clickme`:`!foo|!bar|!flop` e.g.)
|
||||||
|
|
||||||
|
◻ │
|
||||||
|
└── apply the roundrobin (rotate the options, value !foo becomes !bar
|
||||||
|
upon next click) └── is there any object with property-key (!foo
|
||||||
|
e.g.)? └── no: do nothing └── yes: apply its value to the scene ```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
van Kammen Expires 28 March 2026 [Page 6]
|
||||||
|
|
||||||
|
Internet-Draft XR Macros September 2025
|
||||||
|
|
||||||
|
|
||||||
|
| Note that only macro's can trigger roundrobin values or
|
||||||
|
| contextmenu's, as well as roundrobin values never ending up in the
|
||||||
|
| toplevel URL.
|
||||||
|
|
||||||
|
5. Security Considerations
|
||||||
|
|
||||||
|
6. IANA Considerations
|
||||||
|
|
||||||
|
This document has no IANA actions.
|
||||||
|
|
||||||
|
7. Acknowledgments
|
||||||
|
|
||||||
|
* NLNET (https://nlnet.nl)
|
||||||
|
* Future of Text (https://futureoftext.org)
|
||||||
|
* visual-meta.info (https://visual-meta.info)
|
||||||
|
|
||||||
|
8. Appendix: Definitions
|
||||||
|
|
||||||
|
+===============+===================================================+
|
||||||
|
| definition | explanation |
|
||||||
|
+===============+===================================================+
|
||||||
|
| scene | a (local/remote) 3D scene or 3D file |
|
||||||
|
| | (index.gltf e.g.) |
|
||||||
|
+---------------+---------------------------------------------------+
|
||||||
|
| 3D object | an object inside a scene characterized by |
|
||||||
|
| | vertex-, face- and customproperty data. |
|
||||||
|
+---------------+---------------------------------------------------+
|
||||||
|
| XR fragments | URI Fragment with spatial hints like |
|
||||||
|
| | #pos=0,0,0&t=1,100 e.g. |
|
||||||
|
+---------------+---------------------------------------------------+
|
||||||
|
| query | an URI Fragment-operator which queries |
|
||||||
|
| | object(s) from a scene like #q=cube |
|
||||||
|
+---------------+---------------------------------------------------+
|
||||||
|
| FPS | frames per second in spatial experiences |
|
||||||
|
| | (games,VR,AR e.g.), should be as high as |
|
||||||
|
| | possible |
|
||||||
|
+---------------+---------------------------------------------------+
|
||||||
|
| ◻ | ascii representation of an 3D object/mesh |
|
||||||
|
+---------------+---------------------------------------------------+
|
||||||
|
| (un)obtrusive | obtrusive: wrapping human text/thought in |
|
||||||
|
| | XML/HTML/JSON obfuscates human text into |
|
||||||
|
| | a salad of machine-symbols and words |
|
||||||
|
+---------------+---------------------------------------------------+
|
||||||
|
|
||||||
|
Table 7
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
van Kammen Expires 28 March 2026 [Page 7]
|
||||||
381
doc/RFC_XR_Macros.xml
Normal file
381
doc/RFC_XR_Macros.xml
Normal file
|
|
@ -0,0 +1,381 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- name="GENERATOR" content="github.com/mmarkdown/mmark Mmark Markdown Processor - mmark.miek.nl" -->
|
||||||
|
<rfc version="3" ipr="trust200902" docName="draft-XRMACROS-leonvankammen-00" submissionType="IETF" category="info" xml:lang="en" xmlns:xi="http://www.w3.org/2001/XInclude" indexInclude="true" consensus="true">
|
||||||
|
|
||||||
|
<front>
|
||||||
|
<title>XR Macros</title><seriesInfo value="draft-XRMACROS-leonvankammen-00" stream="IETF" status="informational" name="XR-Macros"></seriesInfo>
|
||||||
|
<author initials="L.R." surname="van Kammen" fullname="L.R. van Kammen"><organization></organization><address><postal><street></street>
|
||||||
|
</postal></address></author><date/>
|
||||||
|
<area>Internet</area>
|
||||||
|
<workgroup>Internet Engineering Task Force</workgroup>
|
||||||
|
|
||||||
|
<abstract>
|
||||||
|
<t>This draft offers a specification for embedding macros in existing 3D scenes/assets, to offer simple interactions and configure the renderer further.<br />
|
||||||
|
|
||||||
|
Together with URI Fragments, it allows for rich immersive experiences without the need of a complicated sandboxed scripting languages.</t>
|
||||||
|
<t>Almost every idea in this document is demonstrated at <eref target="https://xrfragment.org">https://xrfragment.org</eref>, as this spec was created during the <eref target="https://xrfragment.org">XR Fragments</eref> spec.</t>
|
||||||
|
</abstract>
|
||||||
|
|
||||||
|
</front>
|
||||||
|
|
||||||
|
<middle>
|
||||||
|
|
||||||
|
<section anchor="introduction"><name>Introduction</name>
|
||||||
|
<t>How can we add more features to existing text & 3D scenes, without introducing new dataformats?<br />
|
||||||
|
|
||||||
|
Historically, there's many attempts to create the ultimate markuplanguage or 3D fileformat.<br />
|
||||||
|
|
||||||
|
Their lowest common denominator is: (co)authoring using plain text.<br />
|
||||||
|
|
||||||
|
Therefore, XR Macros allows us to enrich/connect existing dataformats, by offering a polyglot notation based on existing notations:<br />
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<ol spacing="compact">
|
||||||
|
<li>getting/setting common used 3D properties using querystring- or JSON-notation</li>
|
||||||
|
<li>targeting 3D properties using the lightweight query notation present in <eref target="https://xrfragment.org">XR Fragments</eref></li>
|
||||||
|
</ol>
|
||||||
|
<blockquote><t>NOTE: The chapters in this document are ordered from highlevel to lowlevel (technical) as much as possible</t>
|
||||||
|
</blockquote></section>
|
||||||
|
|
||||||
|
<section anchor="core-principle"><name>Core principle</name>
|
||||||
|
|
||||||
|
<ol spacing="compact">
|
||||||
|
<li>XR Macros use querystrings, but are HTML-agnostic (though pseudo-XR Fragment browsers <strong>can</strong> be implemented on top of HTML/Javascript).</li>
|
||||||
|
<li>An XR Macro is 3D metadata which starts with '!' (<tt>!clickme: fog=0,10</tt> e.g.)</li>
|
||||||
|
<li>Metadata-values can contain the <tt>|</tt> symbol to 🎲 roundrobin variable values (<tt>!toggleme: fog=0,10|fog=0,1000</tt> e.g.)</li>
|
||||||
|
<li>XR Macros acts as simple eventhandlers for URI Fragments: they are automatically published on the (<eref target="https://xrfragment.org">XR Fragments</eref>) hashbus, to act as events (so more serious scripting languages can react to them as well).</li>
|
||||||
|
<li>XR Macros can assign object metadata (<tt>!setlocal: foo=1</tt> writes <tt>foo:1</tt> metadata to the object containing the <tt>!setlocal</tt> metadata)</li>
|
||||||
|
<li>XR Macros can assign global metadata (<tt>!setfoo: #foo=1</tt> writes <tt>foo:1</tt> metadata to the root scene-node)</li>
|
||||||
|
</ol>
|
||||||
|
<blockquote><t>These very simple principles allow for rich interactions and dynamic querying</t>
|
||||||
|
</blockquote></section>
|
||||||
|
|
||||||
|
<section anchor="conventions-and-definitions"><name>Conventions and Definitions</name>
|
||||||
|
<t>See appendix below in case certain terms are not clear.</t>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section anchor="list-of-xr-macros"><name>List of XR Macros</name>
|
||||||
|
<t>(XR) Macros can be embedded in 3D assets/scenes.<br />
|
||||||
|
|
||||||
|
Macros enrich existing spatial content with a lowcode, limited logic-layer, by recursive (economic) use of the querystring syntax (which search engines and <eref target="https://xrfragment.org">XR Fragments</eref> already uses.<br />
|
||||||
|
|
||||||
|
This is done by allowing string/integer variables, and the <tt>|</tt> symbol to roundrobin variable values.<br />
|
||||||
|
|
||||||
|
Macros also act as events, so more serious scripting languages can react to them as well.<br />
|
||||||
|
</t>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>key</th>
|
||||||
|
<th>type</th>
|
||||||
|
<th>example (JSON)</th>
|
||||||
|
<th>function</th>
|
||||||
|
<th>existing compatibility</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><tt>@bg</tt></td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><tt>"@bg":"#cube"</tt></td>
|
||||||
|
<td>bg: binds fog near/far based to cube x/y/z (anim) values</td>
|
||||||
|
<td>custom property in 3D fileformats</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><tt>@fog</tt></td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><tt>"@fog":"#cube"</tt></td>
|
||||||
|
<td>fog: binds fog near/far based to cube x/y (anim) values</td>
|
||||||
|
<td>custom property in 3D fileformats</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><tt>@scroll</tt></td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><tt>"@scroll":"#cube"</tt></td>
|
||||||
|
<td>texturescrolling: binds texture x/y/rot based to cube x/y/z (anim) values</td>
|
||||||
|
<td>custom property in 3D fileformats</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><tt>@emissive</tt></td>
|
||||||
|
<td>string</td>
|
||||||
|
<td><tt>"@emissive":"#cube"</tt></td>
|
||||||
|
<td>day/night/mood: binds material's emissive value to cube x/y/z (anim) values</td>
|
||||||
|
<td>custom property in 3D fileformats</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<section anchor="usecase-click-object"><name>Usecase: click object</name>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>custom property</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>trigger when</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>!clickme</td>
|
||||||
|
<td>bg=1,1,1&foo=2</td>
|
||||||
|
<td>object clicked</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table></section>
|
||||||
|
|
||||||
|
<section anchor="usecase-conditional-click-object"><name>Usecase: conditional click object</name>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>custom property</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>trigger when</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>#</td>
|
||||||
|
<td>foo=1</td>
|
||||||
|
<td>scene</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>!clickme</td>
|
||||||
|
<td>q=foo>2&bg=1,1,1</td>
|
||||||
|
<td>object clicked and foo > 2</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table><blockquote><t>when a user clicks an object with the custom properties above, it should set the backgroundcolor to <tt>1,1,1</tt> when <tt>foo</tt> is greater than <tt>2</tt> (see previous example)</t>
|
||||||
|
</blockquote></section>
|
||||||
|
|
||||||
|
<section anchor="usecase-click-object-roundrobin"><name>Usecase: click object (roundrobin)</name>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>custom property</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>trigger when</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>!cycleme</td>
|
||||||
|
<td>day|noon|night</td>
|
||||||
|
<td>object clicked</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>day</td>
|
||||||
|
<td>bg=1,1,1</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>noon</td>
|
||||||
|
<td>bg=0.5,0.5,0.5</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>night</td>
|
||||||
|
<td>bg=0,0,0&foo=2</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table><blockquote><t>when a user clicks an object with the custom properties above, it should trigger either <tt>day</tt> <tt>noon</tt> or <tt>night</tt> in roundrobin fashion.</t>
|
||||||
|
</blockquote></section>
|
||||||
|
|
||||||
|
<section anchor="usecase-click-object-or-uri-fragment-and-scene-load-trigger"><name>Usecase: click object or URI fragment, and scene load trigger</name>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>custom property</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>trigger when</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>#</td>
|
||||||
|
<td>random</td>
|
||||||
|
<td>scene loaded</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>#random</td>
|
||||||
|
<td>random</td>
|
||||||
|
<td>URL contains #random</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>!random</td>
|
||||||
|
<td>day|noon|night</td>
|
||||||
|
<td>#random, # or click</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>day</td>
|
||||||
|
<td>bg=1,1,1</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>noon</td>
|
||||||
|
<td>bg=0.5,0.5,0.5</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>night</td>
|
||||||
|
<td>bg=0,0,0&foo=2</td>
|
||||||
|
<td>roundrobin</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table></section>
|
||||||
|
|
||||||
|
<section anchor="usecase-present-context-menu-with-options"><name>Usecase: present context menu with options</name>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>custom property</th>
|
||||||
|
<th>value</th>
|
||||||
|
<th>trigger when</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>!random</td>
|
||||||
|
<td>!day</td>
|
||||||
|
<td>!noon</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>!day</td>
|
||||||
|
<td>bg=1,1,1</td>
|
||||||
|
<td>clicked in contextmenu</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>!noon</td>
|
||||||
|
<td>bg=0.5,0.5,0.5</td>
|
||||||
|
<td>clicked in contextmenu</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>!night</td>
|
||||||
|
<td>bg=0,0,0&foo=2</td>
|
||||||
|
<td>clicked in contextmenu</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table><blockquote><t>When interacting with an object with more than one <tt>!</tt>-macro, the XR Browser should offer a contextmenu to execute a macro.</t>
|
||||||
|
</blockquote><t>In a similar way, when <strong>any</strong> <tt>!</tt>-macro is present on the sceneroot, the XR Browser should offer a context-menu to execute those macro's.</t>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section anchor="event-bubble-flow"><name>Event Bubble-flow</name>
|
||||||
|
<t>click object with (<tt>!clickme</tt>:<tt>AR</tt> or <tt>!clickme</tt>: <tt>!reset</tt> e.g.)</t>
|
||||||
|
|
||||||
|
<artwork><![CDATA[ ◻
|
||||||
|
│
|
||||||
|
└── does current object contain this property-key (`AR` or `!reset` e.g.)?
|
||||||
|
└── no: is there any (root)object containing property `AR`
|
||||||
|
└── yes: evaluate its (roundrobin) XR macro-value(s) (and exit)
|
||||||
|
└── no: trigger URL: #AR
|
||||||
|
]]>
|
||||||
|
</artwork>
|
||||||
|
<t>click object with (<tt>!clickme</tt>:<tt>#AR|#VR</tt> e.g.)</t>
|
||||||
|
|
||||||
|
<artwork><![CDATA[ ◻
|
||||||
|
│
|
||||||
|
└── apply the roundrobin (rotate the options, value `#AR` becomes `#VR` upon next click)
|
||||||
|
└── is there any object with property-key (`#AR` e.g.)?
|
||||||
|
└── no: just update the URL to `#AR`
|
||||||
|
└── yes: apply its value to the scene, and update the URL to `#AR`
|
||||||
|
|
||||||
|
click object with (`!clickme`:`!foo|!bar|!flop` e.g.)
|
||||||
|
]]>
|
||||||
|
</artwork>
|
||||||
|
<t>◻
|
||||||
|
│<br />
|
||||||
|
└── apply the roundrobin (rotate the options, value <tt>!foo</tt> becomes <tt>!bar</tt> upon next click)
|
||||||
|
└── is there any object with property-key (<tt>!foo</tt> e.g.)?
|
||||||
|
└── no: do nothing
|
||||||
|
└── yes: apply its value to the scene
|
||||||
|
```</t>
|
||||||
|
<blockquote><t>Note that only macro's can trigger roundrobin values or contextmenu's, as well as roundrobin values never ending up in the toplevel URL.</t>
|
||||||
|
</blockquote></section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section anchor="security-considerations"><name>Security Considerations</name>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section anchor="iana-considerations"><name>IANA Considerations</name>
|
||||||
|
<t>This document has no IANA actions.</t>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section anchor="acknowledgments"><name>Acknowledgments</name>
|
||||||
|
|
||||||
|
<ul spacing="compact">
|
||||||
|
<li><eref target="https://nlnet.nl">NLNET</eref></li>
|
||||||
|
<li><eref target="https://futureoftext.org">Future of Text</eref></li>
|
||||||
|
<li><eref target="https://visual-meta.info">visual-meta.info</eref></li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section anchor="appendix-definitions"><name>Appendix: Definitions</name>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>definition</th>
|
||||||
|
<th>explanation</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>scene</td>
|
||||||
|
<td>a (local/remote) 3D scene or 3D file (index.gltf e.g.)</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>3D object</td>
|
||||||
|
<td>an object inside a scene characterized by vertex-, face- and customproperty data.</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>XR fragments</td>
|
||||||
|
<td>URI Fragment with spatial hints like <tt>#pos=0,0,0&t=1,100</tt> e.g.</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>query</td>
|
||||||
|
<td>an URI Fragment-operator which queries object(s) from a scene like <tt>#q=cube</tt></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>FPS</td>
|
||||||
|
<td>frames per second in spatial experiences (games,VR,AR e.g.), should be as high as possible</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td><tt>◻</tt></td>
|
||||||
|
<td>ascii representation of an 3D object/mesh</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>(un)obtrusive</td>
|
||||||
|
<td>obtrusive: wrapping human text/thought in XML/HTML/JSON obfuscates human text into a salad of machine-symbols and words</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table></section>
|
||||||
|
|
||||||
|
</middle>
|
||||||
|
|
||||||
|
</rfc>
|
||||||
53
doc/fragments.awk
Normal file
53
doc/fragments.awk
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
BEGIN{
|
||||||
|
ROUNDROBIN="🎲"
|
||||||
|
ASSET="🔒"
|
||||||
|
OVERRIDE="🔓"
|
||||||
|
PV_OVERRIDE="💥"
|
||||||
|
NAVIGATOR="👩"
|
||||||
|
PROMPT="✋?"
|
||||||
|
EMBEDDED="🔗"
|
||||||
|
print "| fragment | type | access | scope |"
|
||||||
|
print "|----------|------|--------------|-------|"
|
||||||
|
}
|
||||||
|
|
||||||
|
END{
|
||||||
|
print ""
|
||||||
|
print ASSET" = value(s) can only defined in 3D asset (immutable)<br>"
|
||||||
|
print OVERRIDE" = value(s) can be overwritten in certain context<br>"
|
||||||
|
print ROUNDROBIN" = multiple values will be roundrobin'ed (`#pos=0,0,0|1,0,0` e.g.)<br>"
|
||||||
|
print PV_OVERRIDE" = value(s) can be overwritten by [predefined_view](#predefined_view)<br>"
|
||||||
|
print NAVIGATOR" = value(s) can be overwritten when user clicks `href` (value) or top-level URL change(see [How it works](#How%20it%20works))<br>"
|
||||||
|
print EMBEDDED" = value(s) can be overwritten when 3D asset is embedded/linked as `src` value<br>"
|
||||||
|
print PROMPT" = value(s) can be overwritten by offering confirmation/undo to user<br><br>"
|
||||||
|
print ""
|
||||||
|
print "for more info see [How it works](#How%20it%20works)"
|
||||||
|
}
|
||||||
|
|
||||||
|
/category:/ {
|
||||||
|
$1=$2=""
|
||||||
|
sub(/^[[:space:]]+/, "", $0 ) # remove leading spaces
|
||||||
|
sub(/[[:space:]]+$/, "", $0 ) # remove trailing spaces
|
||||||
|
scope=$0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/Frag.*XRF\.*/ {
|
||||||
|
gsub(/.*\("/,"",$1)
|
||||||
|
gsub(/".*/,"",$1)
|
||||||
|
type="string"
|
||||||
|
perms = $0 ~ /OVERRIDE/ ? OVERRIDE : ASSET
|
||||||
|
frag=$1
|
||||||
|
$1=""
|
||||||
|
if( $0 ~ /T_INT/ ) type="int"
|
||||||
|
if( $0 ~ /T_STRING_OBJ/ ) type="[string object](string object ) "
|
||||||
|
if( $0 ~ /T_VECTOR2/ ) type="[vector2](#vector ) "
|
||||||
|
if( $0 ~ /T_VECTOR3/ ) type="[vector3](#vector ) "
|
||||||
|
if( $0 ~ /T_URL/ ) type="[url](#url ) "
|
||||||
|
if( $0 ~ /T_PREDEFINED_VIEW/ ) type="[predefined view](#predefined_view ) "
|
||||||
|
if( $0 ~ /ROUNDROBIN/ ) perms=perms" "ROUNDROBIN
|
||||||
|
if( $0 ~ /PV_OVERRIDE/ ) perms=perms" "PV_OVERRIDE
|
||||||
|
if( $0 ~ /NAVIGATOR/ ) perms=perms" "NAVIGATOR
|
||||||
|
if( $0 ~ /EMBEDDED/ ) perms=perms" "EMBEDDED
|
||||||
|
if( $0 ~ /PROMPT/ ) perms=perms" "PROMPT
|
||||||
|
print "| **"frag"** |" type "|" perms "|" scope "|"
|
||||||
|
}
|
||||||
33
doc/generate.awk
Normal file
33
doc/generate.awk
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
# a no-nonsense source-to-markdown generator which scans for:
|
||||||
|
#
|
||||||
|
# /**
|
||||||
|
# * # foo
|
||||||
|
# *
|
||||||
|
# * this is markdown $(cat bar.md)
|
||||||
|
# */
|
||||||
|
#
|
||||||
|
# var foo; // comment with 2 leading spaces is markdown too $(date)
|
||||||
|
#
|
||||||
|
# easily refactorable to hash-based languages (py/bash/perl/lua e.g.)
|
||||||
|
# by changing the regexes
|
||||||
|
#
|
||||||
|
/\$\(/ { cmd=$0;
|
||||||
|
gsub(/^.*\$\(/,"",cmd);
|
||||||
|
gsub(/\).*/,"",cmd);
|
||||||
|
cmd | getline stdout; close(cmd);
|
||||||
|
sub(/\$\(.*\)/,stdout);
|
||||||
|
}
|
||||||
|
/\/\*\*/ { doc=1; sub(/^.*\/\*/,""); }
|
||||||
|
doc && /\*\// { doc=0;
|
||||||
|
sub(/[[:space:]]*\*\/.*/,"");
|
||||||
|
sub(/^[[:space:]]*\*[[:space:]]?/,"");
|
||||||
|
print
|
||||||
|
}
|
||||||
|
doc && /^[[:space:]]*\*/ { sub(/^[[:space:]]*\*[[:space:]]?/,"");
|
||||||
|
print
|
||||||
|
}
|
||||||
|
!doc && /\/\/ / { sub(".*// ","");
|
||||||
|
sub("# ","\n# ");
|
||||||
|
sub("> ","\n> ");
|
||||||
|
print
|
||||||
|
}
|
||||||
10
doc/generate.sh
Executable file
10
doc/generate.sh
Executable file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
for topic in Fragments Macros; do
|
||||||
|
mmark RFC_XR_$topic.md > RFC_XR_$topic.xml
|
||||||
|
mmark --html RFC_XR_$topic.md | grep -vE '(<!--{|}-->)' > RFC_XR_$topic.html
|
||||||
|
xml2rfc --v3 RFC_XR_$topic.xml # RFC_XR_$topic.txt
|
||||||
|
sed -i 's/Expires: .*//g' RFC_XR_$topic.txt
|
||||||
|
convert -size 700x2400 xc:white -font "FreeMono" +antialias -pointsize 12 -fill black -annotate +15+15 "@RFC_XR_Fragments.txt" -colorspace gray +dither -posterize 6 RF6_XR_Fragments.png
|
||||||
|
done
|
||||||
3
doc/notes/prio.md
Normal file
3
doc/notes/prio.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
\#static allow client to ignore lower-prio objects in the renderloop, to compensate frame-drop/cpu/gpu-overload scenario’s
|
||||||
|
|
||||||
|
Q: should `-` be used to indicate 'lowering' priority (css's `z-index` 0-??? range is a bit confusing in that sense)
|
||||||
25
doc/shell.nix
Normal file
25
doc/shell.nix
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
{ pkgs ? import <nixos-unstable> {} } :
|
||||||
|
{
|
||||||
|
|
||||||
|
pkgs = import (builtins.fetchGit {
|
||||||
|
name = "nixos-23.05";
|
||||||
|
url = "https://github.com/nixos/nixpkgs/";
|
||||||
|
ref = "refs/heads/nixos-unstable";
|
||||||
|
rev = "ef99fa5c5ed624460217c31ac4271cfb5cb2502c";
|
||||||
|
});
|
||||||
|
|
||||||
|
foo = pkgs.mkShell {
|
||||||
|
# nativeBuildInputs is usually what you want -- tools you need to run
|
||||||
|
nativeBuildInputs = with pkgs.buildPackages; [
|
||||||
|
|
||||||
|
mmark
|
||||||
|
xml2rfc
|
||||||
|
python312Packages.lxml
|
||||||
|
wkhtmltopdf-bin
|
||||||
|
imagemagick
|
||||||
|
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# to create [markdown] table of contents use LLM with this input: awk '/id="/ { print $0 }' RFC_XR_Fragments.html | grep -v idx
|
||||||
|
}
|
||||||
489
doc/style.css
Normal file
489
doc/style.css
Normal file
|
|
@ -0,0 +1,489 @@
|
||||||
|
:root {
|
||||||
|
--color-a: black;
|
||||||
|
--color-at: #000000CC;
|
||||||
|
--color-b: #222;
|
||||||
|
--color-c: #444;
|
||||||
|
--color-d: #888;
|
||||||
|
--color-e: #FF0;
|
||||||
|
--color-f: #CCC;
|
||||||
|
--color-g: #FFF;
|
||||||
|
--color-gt: #FFF5;
|
||||||
|
--params-width: 320px;
|
||||||
|
--font-1: 'Montserrat', sans-serif;
|
||||||
|
--font-2: monospace, courier;
|
||||||
|
--font-size-1: 57px;
|
||||||
|
--font-size-2: 26px;
|
||||||
|
--font-size-3: 18px;
|
||||||
|
--font-size-4: 15px;
|
||||||
|
--gutter-1: 10%;
|
||||||
|
--gradient-1: linear-gradient(45deg, #dc2454 0%, #944a9d 51%, #4824dc 100%);
|
||||||
|
--gradient-2: linear-gradient(45deg, #dc2454 0%, #944a9d 21%, #4824dc 50%,#dc2454 70%, #944a9d 88%, #4824dc 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
body>h1:nth-child(1) {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body{
|
||||||
|
position:relative;
|
||||||
|
width:100%;
|
||||||
|
height:100vh;
|
||||||
|
color: var(--color-d);
|
||||||
|
line-height: 38px;
|
||||||
|
margin:0;
|
||||||
|
padding:0;
|
||||||
|
font-size: var(--font-size-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: var(--font-1);
|
||||||
|
font-size: var(--font-size-3);
|
||||||
|
font-weight:500;
|
||||||
|
color: var(--color-c);
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
.img{
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
|
||||||
|
#jumbotron{
|
||||||
|
margin-top:50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
b{
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: var(--color-c);
|
||||||
|
font-size: var(--font-size-1);
|
||||||
|
font-weight:700;
|
||||||
|
margin: 0;
|
||||||
|
line-height: 11vh;
|
||||||
|
padding-right:40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: var(--font-size-2);
|
||||||
|
font-weight:400;
|
||||||
|
padding-right:40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
background-image: var(--gradient-1);
|
||||||
|
color: var(--color-g);
|
||||||
|
display:inline;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 2px 10px 2px 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: var(--font-size-4);
|
||||||
|
text-transform:uppercase;
|
||||||
|
white-space:nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 700px) {
|
||||||
|
body {
|
||||||
|
font-size: var(--font-size-3);
|
||||||
|
line-height: 40px;
|
||||||
|
}
|
||||||
|
svg.img{
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
.content{
|
||||||
|
padding-top: 60px;
|
||||||
|
margin-left: 27px !important;
|
||||||
|
padding-right: 54px !important;
|
||||||
|
}
|
||||||
|
.heading{
|
||||||
|
transform:scale(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border:none;
|
||||||
|
box-shadow: 0px 0px 10px #0005;
|
||||||
|
padding:20px;
|
||||||
|
background: transparent !important;
|
||||||
|
border-radius:4px;
|
||||||
|
}
|
||||||
|
table tr td,
|
||||||
|
table tr th {
|
||||||
|
vertical-align:top;
|
||||||
|
text-align:left;
|
||||||
|
border-bottom:1px solid #555;
|
||||||
|
color: var(--color-d);
|
||||||
|
}
|
||||||
|
table tr td a:visited,
|
||||||
|
table tr td a:active,
|
||||||
|
table tr td a{
|
||||||
|
color: var(--color-e);
|
||||||
|
}
|
||||||
|
table tr:last-child td{
|
||||||
|
border:none;
|
||||||
|
}
|
||||||
|
table tr th {
|
||||||
|
border-bottom:2px solid var(--color-d);
|
||||||
|
}
|
||||||
|
|
||||||
|
code{
|
||||||
|
display: inline-block;
|
||||||
|
unicode-bidi: embed;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre;
|
||||||
|
background: var(--color-c);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding: 0px 5px;
|
||||||
|
font-size:16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
height:99vh;
|
||||||
|
max-width: 50em;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-top:25px;
|
||||||
|
box-sizing:border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,a:visited,a:active{
|
||||||
|
color: var(--color-a);
|
||||||
|
display:inline-block;
|
||||||
|
text-decoration:none;
|
||||||
|
}
|
||||||
|
a:hover{
|
||||||
|
opacity:0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* lato-regular - latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Lato';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('/assets/font/lato-v17-latin-regular.eot'); /* IE9 Compat Modes */
|
||||||
|
src: local(''),
|
||||||
|
url('/assets/font/lato-v17-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||||
|
url('/assets/font/lato-v17-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
||||||
|
url('/assets/font/lato-v17-latin-regular.woff') format('woff'), /* Modern Browsers */
|
||||||
|
url('/assets/font/lato-v17-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||||
|
url('/assets/font/lato-v17-latin-regular.svg#Lato') format('svg'); /* Legacy iOS */
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidemenu {
|
||||||
|
position:absolute;
|
||||||
|
top:0;
|
||||||
|
left:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidemenu nav {
|
||||||
|
width: 300px;
|
||||||
|
box-shadow: 0px 0px 8px #0005;
|
||||||
|
background:#FFF;
|
||||||
|
z-index:10;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 99;
|
||||||
|
height:100vh;
|
||||||
|
}
|
||||||
|
#sidemenu .sidemenu__btn {
|
||||||
|
display: block;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
top:20px;
|
||||||
|
left:20px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
#sidemenu .sidemenu__btn span {
|
||||||
|
display: block;
|
||||||
|
width: 30px;
|
||||||
|
height: 3px;
|
||||||
|
margin: auto;
|
||||||
|
background:var(--color-d);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
-webkit-transition: all .4s ease;
|
||||||
|
transition: all .4s ease;
|
||||||
|
}
|
||||||
|
#sidemenu .sidemenu__btn span.top {
|
||||||
|
-webkit-transform: translateY(-8px);
|
||||||
|
transform: translateY(-8px);
|
||||||
|
}
|
||||||
|
#sidemenu .sidemenu__btn span.bottom {
|
||||||
|
-webkit-transform: translateY(8px);
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
#sidemenu .sidemenu__btn.active .top {
|
||||||
|
-webkit-transform: rotate(-45deg);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
}
|
||||||
|
#sidemenu .sidemenu__btn.active .mid {
|
||||||
|
-webkit-transform: translateX(-20px) rotate(360deg);
|
||||||
|
transform: translateX(-20px) rotate(360deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
#sidemenu .sidemenu__btn.active .bottom {
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
#sidemenu .sidemenu__wrapper {
|
||||||
|
padding-top: 70px;
|
||||||
|
margin-left:20px;
|
||||||
|
}
|
||||||
|
#sidemenu .sidemenu__list {
|
||||||
|
overflow-y: scroll;
|
||||||
|
max-height: 73vh;
|
||||||
|
padding-top: 50px;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#sidemenu .sidemenu__item {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#sidemenu li > a {
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
-webkit-transition: .4s ease;
|
||||||
|
transition: .4s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
-webkit-transition: .2s ease;
|
||||||
|
transition: .2s ease;
|
||||||
|
-webkit-transform: translateX(0px);
|
||||||
|
transform: translateX(0px);
|
||||||
|
opacity:1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.hide {
|
||||||
|
-webkit-transform: translateX(-300px);
|
||||||
|
transform: translateX(-300px);
|
||||||
|
opacity:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin-left:30px !important;
|
||||||
|
margin-top: 35px !important;
|
||||||
|
margin-bottom: 35px !important;
|
||||||
|
}
|
||||||
|
ul:nth-child(1){
|
||||||
|
margin:0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,li{
|
||||||
|
text-decoration:none !important;
|
||||||
|
/*list-style:none;*/
|
||||||
|
padding-left:10px;
|
||||||
|
padding-right:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li > a,
|
||||||
|
li > a.visited,
|
||||||
|
li > a.active{
|
||||||
|
color:#555;
|
||||||
|
padding:5px;
|
||||||
|
}
|
||||||
|
li > b {
|
||||||
|
background:#FFF;
|
||||||
|
padding:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidemenu__list > li,
|
||||||
|
.sidemenu__list > ul > li{
|
||||||
|
display:inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li {
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content ul > li::before {content: "•"; padding-right:10px;color: #54F;}
|
||||||
|
|
||||||
|
li > b {
|
||||||
|
font-weight:100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touchscreen {
|
||||||
|
border:30px solid #aab5bb;
|
||||||
|
border-radius:20px;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
.touchscreen:after {
|
||||||
|
content:"";
|
||||||
|
display:block;
|
||||||
|
position:absolute;
|
||||||
|
border-radius:50%;
|
||||||
|
width:15px;
|
||||||
|
height:15px;
|
||||||
|
background:#FFF;
|
||||||
|
top:136px;
|
||||||
|
right:-22px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
div.pretty{
|
||||||
|
border-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
background: #372e42;
|
||||||
|
overflow-x: scroll;
|
||||||
|
white-space:pre-wrap;
|
||||||
|
color:#65e;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.pretty,
|
||||||
|
div.pretty > pre > code{
|
||||||
|
background: -webkit-linear-gradient(-70deg,#98a7f0,#0d4e50);
|
||||||
|
background-clip: border-box;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
border: none;
|
||||||
|
text-shadow: 0px 0px 5px #fff9;
|
||||||
|
filter: brightness(1.8);
|
||||||
|
font-size:14px;
|
||||||
|
width:98%;
|
||||||
|
padding:0px 15px;
|
||||||
|
margin:0;
|
||||||
|
box-sizing:border-box;
|
||||||
|
line-height:20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote{
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: transparent !important;
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 20px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 30px 0px 20px 0px;
|
||||||
|
color: var(--color-f) !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
blockquote a {
|
||||||
|
color:#000 !important;
|
||||||
|
}
|
||||||
|
blockquote a:hover {
|
||||||
|
color:#FFF !important;
|
||||||
|
background:#000;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width:100%;
|
||||||
|
filter: brightness(0.97)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* GRID SYSTEM */
|
||||||
|
.block{
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
margin-bottom: var(--gutter-1);
|
||||||
|
min-height:55vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.block .img{
|
||||||
|
border-radius:15px;
|
||||||
|
width:100%;
|
||||||
|
height:35vh;
|
||||||
|
margin-bottom:15px;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
p{ margin:0; padding:0; }
|
||||||
|
|
||||||
|
//the grid:
|
||||||
|
|
||||||
|
.grid{
|
||||||
|
width: 90%;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: $grid-color;
|
||||||
|
//just for styling:
|
||||||
|
padding-top: var(--gutter-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gutter{
|
||||||
|
/*
|
||||||
|
margin-left: calc( var(--gutter-1) /2 );
|
||||||
|
margin-right: calc( var(--gutter-1) /2 );
|
||||||
|
*/
|
||||||
|
margin-right: var(--gutter-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-33{
|
||||||
|
width: 33.3%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.col-50{
|
||||||
|
width: 50%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear{
|
||||||
|
clear: both;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
//responsive
|
||||||
|
|
||||||
|
@media all and (max-width:800px) {
|
||||||
|
.col-33{
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width:600px) {
|
||||||
|
.col-50, .col-33{
|
||||||
|
width: 100%;
|
||||||
|
float: none;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 50px;
|
||||||
|
line-height:70px;
|
||||||
|
}
|
||||||
|
h2{
|
||||||
|
font-size: var(--font-size-3);
|
||||||
|
}
|
||||||
|
.block.gutter{
|
||||||
|
margin-right:0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.darken{
|
||||||
|
filter: brightness(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
button{
|
||||||
|
cursor:pointer;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-image: var(--gradient-1);
|
||||||
|
border: none;
|
||||||
|
border-radius: 20px;
|
||||||
|
display:inline-block;
|
||||||
|
color: #FFF;
|
||||||
|
border: none;
|
||||||
|
opacity:1.0;
|
||||||
|
font-size: var(--font-size-4);
|
||||||
|
font-weight:600;
|
||||||
|
}
|
||||||
|
button:hover{
|
||||||
|
opacity:0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient{
|
||||||
|
background-image: var(--gradient-2);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
1
doc/tags
Symbolic link
1
doc/tags
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/home/leon/.ctags.js
|
||||||
4191
index.html
Normal file
4191
index.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue