work in progress [might break]

This commit is contained in:
Leon van Kammen 2023-12-27 17:25:49 +00:00
parent b61f6371d0
commit e2d1d47736
22 changed files with 2646 additions and 256 deletions

View File

@ -15,7 +15,7 @@
<script src="./../../../dist/xrfragment.aframe.js"></script> <script src="./../../../dist/xrfragment.aframe.js"></script>
<script src="./../../../dist/xrfragment.extras.js"></script> <script src="./../../../dist/xrfragment.extras.js"></script>
<a-scene stats xr-mode-ui="XRMode: xr" renderer="colorManagement: true; highRefreshRate:true" light="defaultLightsEnabled: false"> <a-scene xr-mode-ui="XRMode: xr" renderer="colorManagement: true; highRefreshRate:true" light="defaultLightsEnabled: false">
<a-entity id="player" wasd-controls look-controls> <a-entity id="player" wasd-controls look-controls>
<a-entity camera="fov:90" position="0 1.6 0" id="camera"></a-entity> <a-entity camera="fov:90" position="0 1.6 0" id="camera"></a-entity>
<a-entity id="left-hand" laser-controls="hand: left" raycaster="objects:.ray" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: .floor"> <a-entity id="left-hand" laser-controls="hand: left" raycaster="objects:.ray" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: .floor">
@ -33,12 +33,12 @@
<script> <script>
// xrfragment.extras.js is totally optional but can be handy // xrfragment.extras.js is totally optional but can be handy
// to quickly add a menu, logo, buttons, serverless meeting-functionality etc // to quickly add a menu, logo, buttons, serverless meeting-functionality etc
XRFMENU.logo = './../../assets/logo.png' $menu.logo = 'logo_file_or_url_here'
XRFMENU.morelabel = '⚡' $menu.morelabel = '⚡'
// add your menubuttons: // add your menubuttons:
XRFMENU.buttons = XRFMENU.buttons.concat([`<a class="btn" aria-label="button" aria-description="about menu" onclick="XRFMENU.showAbout()">💁 about</a><br>`]) $menu.buttons = $menu.buttons.concat([`<a class="btn" aria-label="button" aria-description="about menu" onclick="$menu.showAbout()">💁 about</a><br>`])
XRFMENU.collapsed = false $menu.collapsed = false
XRFMENU.showAbout = () => window.notify(` $menu.showAbout = () => window.notify(`
<h1>💁 Hi there!</h1> <h1>💁 Hi there!</h1>
This XR fragments experience works almost anywhere.<br> This XR fragments experience works almost anywhere.<br>
@ -62,7 +62,7 @@
<br><br> <br><br>
`,{timeout:false}) `,{timeout:false})
XRFMENU.install(xrf) $menu.install(xrf)
</script> </script>
</body> </body>

Binary file not shown.

2
example/matrix/dist/matrix-crtd.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,37 @@
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*!
* content-type
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! queue-microtask. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! simple-peer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */

6
example/matrix/make Executable file
View File

@ -0,0 +1,6 @@
#!/bin/sh
version=v30.3.0
test -f ${version}.zip || wget https://github.com/matrix-org/matrix-js-sdk/archive/refs/tags/${version}.zip
test -d node_modules || npm install
cat

2
make
View File

@ -86,7 +86,7 @@ build(){
example/assets/js/qr.js > dist/xrfragment.aframe.js example/assets/js/qr.js > dist/xrfragment.aframe.js
# html extras like menu & meetings # html extras like menu & meetings
cat src/3rd/js/extra/*.js > dist/xrfragment.extras.js cat src/3rd/js/extra/*.js dist/matrix-crdt.js src/3rd/js/extra/network/*.js dist/trystero-torrent.min.js > dist/xrfragment.extras.js
# fat all-in-one standalone xrf release # fat all-in-one standalone xrf release
test -f dist/aframe.min.js || { test -f dist/aframe.min.js || {

View File

@ -9,8 +9,6 @@ window.AFRAME.registerComponent('xrf', {
camera.setAttribute('xrf-fade','') camera.setAttribute('xrf-fade','')
AFRAME.fade = camera.components['xrf-fade'] AFRAME.fade = camera.components['xrf-fade']
if( document.location.host.match(/localhost/) ) document.querySelector('a-scene').setAttribute("stats",'')
let aScene = document.querySelector('a-scene') let aScene = document.querySelector('a-scene')
aScene.addEventListener('loaded', () => { aScene.addEventListener('loaded', () => {

237
src/3rd/js/extra/$chat.js Normal file
View File

@ -0,0 +1,237 @@
chatComponent = {
html: `
<div id="videos" style="pointer-events:none"></div>
<div id="messages" aria-live="assertive" aria-relevant></div>
<div id="chatfooter">
<div id="chatbar">
<input id="chatline" type="text" placeholder="type here"></input>
</div>
<button id="showchat" class="btn">show chat</button>
</div>
</div>
`,
init: (el) => new Proxy({
scene: null,
visible: true,
messages: [],
$messages: $messages = el.querySelector("#messages"),
$chatline: $chatline = el.querySelector("#chatline"),
install(opts){
this.opts = opts
this.scene = opts.scene
el.className = "xrf"
el.style.display = 'none' // start hidden
document.body.appendChild( el )
document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) )
$chat.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to others.<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","multiline"] })
},
initListeners(){
//opts.scene.addEventListener('meeting.peer.add', () => console.log("$chat.peer.add") )
//opts.scene.addEventListener('meeting.peer.remove', () => console.log("$chat.peer.remove") )
$chatline.addEventListener('keydown', (e) => {
if (e.key == 'Enter' ){
this.send({message: $chatline.value })
$chatline.value = ''
}
})
console.dir(this.scene)
},
toggle(){
this.visible = !this.visible
if( this.visible && window.meeting.status == 'offline' ) window.meeting.start(this.opts)
},
send(opts){
opts = { linebreak:true, message:"", class:[], ...opts }
let msg = document.createElement('div')
let br = document.createElement('br')
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)
}
$messages.appendChild(msg)
if( opts.linebreak ) $messages.appendChild(br)
$messages.scrollTop = $messages.scrollHeight // scroll down
document.dispatchEvent( new CustomEvent("$chat:receive", {detail: opts}) )
$messages.last = msg.innerHTML
}
},{
get(data,k,v){ return data[k] },
set(data,k,v){
data[k] = v
switch( k ){
case "visible": {
el.style.display = data.visible ? 'block' : 'none'
if( !el.inited && (el.inited = true) ) data.initListeners()
$menu.collapsed = !data.visible
break;
}
}
}
})
}
// reactify component!
document.addEventListener('$menu:ready', (opts) => {
opts = opts.detail
document.head.innerHTML += chatComponent.css
$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:1500;
}
#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#showchat{
z-index: 1500;
position: fixed;
bottom: 20px;
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#showchat{
z-index:1550;
color:white;
border:0;
display:none;
height: 44px;
background:#07F;
font-weight:bold;
}
#chatbar input{
border:none;
width:90%;
box-sizing:border-box;
}
#messages{
position: absolute;
top: 100px;
left: 0;
right: 0;
bottom: 88px;
padding: 15px;
pointer-events: none;
overflow-y:auto;
}
#messages .msg{
background: #fff;
display: inline-block;
padding: 6px 17px;
border-radius: 20px;
color: #000c;
margin-bottom: 10px;
line-height:23px;
pointer-events:visible;
border: 1px solid #ccc8;
line-height:33px;
}
#messages .msg.info{
border: 4px dotted #CCC
font-size: 14px;
padding: 3px 16px;
}
#messages.guide .guide{
display:unset;
}
$message .guide, .guide{
display:none;
}
br.guide{
display:inline-block;
}
#messages .msg.info a,
#messages .msg.info a:visited{
color: var(--xrf-primary);
text-decoration: underline;
transition:0.3s;
}
#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 input{
padding: 7px 15px;
border-block: none;
border-inline: none;
border: 1px solid #888;
background: var(--xrf-lighter-gray);
height: 18px;
max-width:168px;
}
}
</style>`

View File

@ -0,0 +1,114 @@
connectionsComponent = {
html: `
<div id="connections">
<h2>Network channels:</h2>
<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>Scene</td>
<td>
<select id="scene"></select>
</td>
</tr>
</table>
<div id="settings"></div>
<br>
<button id="connect" onclick="$connections.connect()">📡 Connect!</button>
<br>
</div>
`,
init: (el) => new Proxy({
webcam: [{plugin:{name:"No thanks"},config(){}}],
chatnetwork: [{plugin:{name:"No thanks"},config(){}}],
scene: [{plugin:{name:"No thanks"},config(){}}],
$webcam: $webcam = el.querySelector("#webcam"),
$chatnetwork: $chatnetwork = el.querySelector("#chatnetwork"),
$scene: $scene = el.querySelector("#scene"),
$settings: $settings = el.querySelector("#settings"),
install(opts){
this.opts = opts
document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) )
$webcam.addEventListener('change', () => this.renderSettings() )
$chatnetwork.addEventListener('change', () => this.renderSettings() )
$scene.addEventListener('change', () => this.renderSettings() )
},
show(){
if( !network.connected ){
$chat.send({message:"", el})
this.renderSettings()
}else $chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
},
connect(){
navigator.share({
url: 'https://foo.com',
title: 'your meeting link'
})
},
renderSettings(){
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
let theWebcam = this.webcam.find( (p) => p.plugin.name == $webcam.value )
let theChatnetwork = this.chatnetwork.find( (p) => p.plugin.name == $chatnetwork.value )
let theScene = this.scene.find( (p) => p.plugin.name == $scene.value )
$settings.innerHTML = ''
$settings.appendChild(theWebcam.config(opts))
if( theChatnetwork.plugin.name != theWebcam.plugin.name ) $settings.appendChild( theChatnetwork.config(opts) )
if( theScene.plugin.name != theWebcam.plugin.name && theScene.plugin.name != theChatnetwork.plugin.name )
$settings.appendChild( scene.config(opts) )
}
},{
get(data,k,v){ return data[k] },
set(data,k,v){
data[k] = v
switch( k ){
case "webcam": $webcam.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
}
}
})
}
// reactify component!
document.addEventListener('network:ready', (opts) => {
opts = opts.detail
document.head.innerHTML += connectionsComponent.css
$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{
float: right;
height: 43px;
margin: 0px;
}
</style>`

View File

@ -1,173 +0,0 @@
MEETING = {
html: `
<div id="videos" style="pointer-events:none"></div>
<div id="chat" aria-live="assertive" aria-relevant></div>
<div id="chatfooter">
<div id="chatbar">
<input id="chatline" type="text" placeholder=""></input>
</div>
<button id="showchat" class="btn">show chat</button>
</div>
</div>
`,
init: (el) => new Proxy({
scene: null,
visible: false,
//$overlay: $overlay = el.querySelector('#overlay'),
//
install(opts){
this.scene = opts.scene
document.body.appendChild( el )
document.dispatchEvent( new CustomEvent("MEETING:ready", {detail: opts}) )
},
start(){
this.scene.addEventListener('meeting.peer.add', () => console.log("$meeting.peer.add") )
this.scene.addEventListener('meeting.peer.remove', () => console.log("$meeting.peer.remove") )
},
toggle: () => MEETING.visible = !MEETING.visible,
},{
get(data,k,v){ return data[k] },
set(data,k,v){
data[k] = v
switch( k ){
case "visible": el.style.display = data.visible ? 'block' : 'none'
}
},
})
}
// reactify component!
document.addEventListener('XRFMENU:ready', (opts) => {
opts = opts.detail
XRFMENU.buttons = ([`<a class="btn" aria-label="button" aria-description="start text/audio/video chat" id="meeting" onclick="MEETING.toggle()" target="_blank">🧑‍🤝‍🧑 meeting</a><br>`])
.concat(XRFMENU.buttons)
document.head.innerHTML += MEETING.css
$meeting = document.createElement('div')
$meeting.innerHTML = MEETING.html
MEETING = MEETING.init($meeting)
MEETING.install(opts)
})
// alpine component for displaying meetings
MEETING.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:1500;
}
#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#showchat{
z-index: 1500;
position: fixed;
bottom: 20px;
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#showchat{
z-index:1550;
color:white;
border:0;
display:none;
height: 44px;
background:#07F;
font-weight:bold;
}
#chatbar input{
border:none;
width:90%;
box-sizing:border-box;
}
#chat{
position: absolute;
top: 100px;
left: 0;
right: 0;
bottom: 88px;
padding: 15px;
pointer-events: none;
overflow-y:auto;
}
#chat .msg{
background: #fff;
display: inline-block;
padding: 6px 17px;
border-radius: 20px;
color: #000c;
margin-bottom: 10px;
line-height:23px;
pointer-events:visible;
border: 1px solid #ccc;
}
#chat .msg.info{
background: #333;
color: #FFF;
font-size: 14px;
padding: 0px 16px;
}
#chat .msg.info a,
#chat .msg.info a:visited{
color: #aaf;
text-decoration: none;
transition:0.3s;
}
#chat .msg.info a:hover,
#chat button:hover{
filter: brightness(1.8);
text-decoration: underline;
}
#chat button {
margin: 0px 15px 10px 0px;
background: #07F;
color: #FFF;
border-radius: 7px;
padding: 11px 15px;
border: 0;
font-weight: bold;
box-shadow: 0px 0px 5px 5px #0002;
pointer-events:all;
}
#chat,#chatbar,#chatbar *, #chat *{
font-family:monospace;
font-size:15px;
}
</style>`

View File

@ -1,5 +1,5 @@
// reactive component for displaying the menu // reactive component for displaying the menu
XRFMENU = { menuComponent = {
html: ` html: `
<div id="overlay" class="xrf"> <div id="overlay" class="xrf">
@ -12,7 +12,7 @@ XRFMENU = {
<div class="xrf footer"> <div class="xrf footer">
<div class="menu"> <div class="menu">
<div id="buttons"></div> <div id="buttons"></div>
<a class="btn" id="more" onclick="XRFMENU.toggle()"></a><br> <a class="btn" id="more" aria-description="menu with options, like extra accessibility" onclick="$menu.toggle()"></a><br>
</div> </div>
</div> </div>
`, `,
@ -21,7 +21,7 @@ XRFMENU = {
morelabel: '⚡', morelabel: '⚡',
collapsed: false, collapsed: false,
logo: './../../assets/logo.png', logo: './../../assets/logo.png',
buttons: [`<a class="btn" aria-label="button" aria-description="share URL/screenshot/embed" id="share" onclick="XRFMENU.share()">🔗 share</a><br>`], buttons: [`<a class="btn" aria-label="button" aria-description="share URL/screenshot/embed" id="share" onclick="$menu.share()">🔗 share</a><br>`],
$overlay: $overlay = el.querySelector('#overlay'), $overlay: $overlay = el.querySelector('#overlay'),
$logo: $logo = el.querySelector('.logo'), $logo: $logo = el.querySelector('.logo'),
@ -29,12 +29,29 @@ XRFMENU = {
$buttons: $buttons = el.querySelector('#buttons'), $buttons: $buttons = el.querySelector('#buttons'),
$btnMore: $btnMore = el.querySelector('#more'), $btnMore: $btnMore = el.querySelector('#more'),
toggle: () => XRFMENU.collapsed = !XRFMENU.collapsed, toggle: () => $menu.collapsed = !$menu.collapsed,
install: (opts) => { install: (xrf) => {
XRFMENU.bindToWindow() // bind functions like notify to window this.xrf = xrf
$menu.bindToWindow() // bind functions like notify to window
window.notify('loading '+document.location.search.substr(1)) window.notify('loading '+document.location.search.substr(1))
document.body.appendChild(el) document.body.appendChild(el)
document.dispatchEvent( new CustomEvent("XRFMENU:ready", {detail: opts}) ) document.dispatchEvent( new CustomEvent("$menu:ready", {detail: xrf}) )
// add screenshot component with camera to capture bigger size equirects
// document.querySelector('a-scene').components.screenshot.capture('perspective')
$('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2})
if( window.outerWidth > 800 )
setTimeout( () => window.notify("use WASD-keys and mouse-drag to move around",{timeout:false}),2000 )
xrf.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false )
// enable user-uploaded asset files
let fileLoaders = $menu.loadFile({
".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ),
".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) )
})
el.querySelector("#overlay > input[type=submit]").addEventListener("click", fileLoaders )
} }
},{ },{
@ -46,7 +63,9 @@ XRFMENU = {
case "logo": $logo.style.backgroundImage = `url(${v})`; break; case "logo": $logo.style.backgroundImage = `url(${v})`; break;
case "css": document.head.innerHTML += v; break; case "css": document.head.innerHTML += v; break;
case "morelabel": $btnMore.innerText = data.morelabel; break; case "morelabel": $btnMore.innerText = data.morelabel; break;
case "buttons": $buttons.innerHTML = this.renderButtons(data); break; case "buttons": $buttons.innerHTML = this.renderButtons(data);
document.dispatchEvent( new CustomEvent("$menu:buttons:render", {detail: el.querySelector('.menu') }) )
break;
case "collapsed": $overlay.style.display = data.collapsed ? 'block' : 'none' case "collapsed": $overlay.style.display = data.collapsed ? 'block' : 'none'
$buttons.style.display = data.collapsed ? 'block' : 'none' $buttons.style.display = data.collapsed ? 'block' : 'none'
break; break;
@ -59,22 +78,25 @@ XRFMENU = {
} }
// reactify component! // reactify component!
$xrfmenu = document.createElement('div') $menu = document.createElement('div')
$xrfmenu.innerHTML = XRFMENU.html $menu.innerHTML = menuComponent.html
XRFMENU = XRFMENU.init($xrfmenu) $menu = menuComponent.init($menu)
// attach menu functions which are less related to rendering // attach menu functions which are less related to rendering
let utils = { let utils = {
bindToWindow(opts){ bindToWindow(opts){
window.notify = XRFMENU.notify(window) window.notify = $menu.notify(window)
// reroute console messages to snackbar notifications // reroute console messages to snackbar notifications
console.log = ( (log) => function(str){ console.log = ( (log,console) => function(str){
if( String(str).match(/(:.*#|note:)/) ) window.notify(str) if( String(str).match(/(:.*#|note:|:\/\/)/) ) window.notify( str )
log(str) log.call(console,str)
})(console.log) })(console.log, console)
// allow iframe to open url // allow iframe to open url
window.addEventListener('message', (event) => { window.addEventListener('message', (event) => {
if (event.data && event.data.url) { if (event.data && event.data.url) {
@ -242,7 +264,8 @@ let utils = {
}, },
notify(scope){ notify(scope){
return function notify(str,opts){ return function notify(_str,opts){
str = _str.replace(/(^\w+):/,"<div class='badge'>\$1</div>")
opts = opts || {status:'info'} opts = opts || {status:'info'}
opts = Object.assign({ status, timeout:4000 },opts) opts = Object.assign({ status, timeout:4000 },opts)
if( typeof str == 'string' ){ if( typeof str == 'string' ){
@ -252,7 +275,9 @@ let utils = {
} }
} }
opts.message = str opts.message = str
window.XRFMENU.SnackBar( opts ) window.$menu.SnackBar( opts )
opts.message = _str
document.dispatchEvent( new CustomEvent("notify", {detail:opts}) )
} }
}, },
@ -290,7 +315,7 @@ let utils = {
newHash += `&${lastPos}` newHash += `&${lastPos}`
document.location.hash = newHash.replace(/&&/,'&') document.location.hash = newHash.replace(/&&/,'&')
.replace(/#&/,'') .replace(/#&/,'')
XRFMENU.copyToClipboard( window.location.href ); $menu.copyToClipboard( window.location.href );
}, },
copyToClipboard(text){ copyToClipboard(text){
@ -306,13 +331,13 @@ let utils = {
share(){ share(){
let inMeeting = $('[meeting]') let inMeeting = $('[meeting]')
let url = window.location.href let url = window.location.href
if( !inMeeting ) XRFMENU.updateHashPosition() if( !inMeeting ) $menu.updateHashPosition()
else url = $('[meeting]').components['meeting'].data.link else url = $('[meeting]').components['meeting'].data.link
XRFMENU.copyToClipboard( url ) $menu.copyToClipboard( url )
// End of *TODO* // End of *TODO*
window.notify(`<h2>${ inMeeting ? 'Meeting link ' : 'Link'} copied to clipboard!</h2> <br>Now share it with your friends ❤️<br> window.notify(`<h2>${ inMeeting ? 'Meeting link ' : 'Link'} copied to clipboard!</h2> <br>Now share it with your friends ❤️<br>
<canvas id="qrcode" width="121" height="121"></canvas><br> <canvas id="qrcode" width="121" height="121"></canvas><br>
<button onclick="XRFMENU.download()">💾 download scene file</button> <br> <button onclick="$menu.download()">💾 download scene file</button> <br>
<button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')">📷 download 360 screenshot</button> <br> <button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')">📷 download 360 screenshot</button> <br>
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld">🖥 clone & selfhost this experience</a><br> <a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld">🖥 clone & selfhost this experience</a><br>
<br> <br>
@ -330,7 +355,7 @@ let utils = {
} }
// map to component // map to component
for( let i in utils ) XRFMENU[i] = utils[i] for( let i in utils ) $menu[i] = utils[i]
//$('a-scene').addEventListener('XRF', this.onXRFready ) //$('a-scene').addEventListener('XRF', this.onXRFready )
// //
@ -359,7 +384,7 @@ for( let i in utils ) XRFMENU[i] = utils[i]
// // $('a#meeting').addEventListener('click', () => { // // $('a#meeting').addEventListener('click', () => {
// // if( aScene.getAttribute('meeting') ){ // meeting already, start breakout room // // if( aScene.getAttribute('meeting') ){ // meeting already, start breakout room
// // let parentRoom = document.location.href // // let parentRoom = document.location.href
// // XRFMENU.updateHashPosition(true) // // $menu.updateHashPosition(true)
// // let meeting = $('[meeting]').components['meeting'] // // let meeting = $('[meeting]').components['meeting']
// // meeting.data.parentRoom = parentRoom // // meeting.data.parentRoom = parentRoom
// // meeting.update() // // meeting.update()
@ -392,7 +417,7 @@ for( let i in utils ) XRFMENU[i] = utils[i]
// window.AFRAME.XRF.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false ) // window.AFRAME.XRF.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false )
// //
// // enable user-uploaded asset files // // enable user-uploaded asset files
// let fileLoaders = XRFMENU.loadFile({ // let fileLoaders = $menu.loadFile({
// ".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ), // ".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ),
// ".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ) // ".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) )
// }) // })
@ -400,7 +425,7 @@ for( let i in utils ) XRFMENU[i] = utils[i]
// finally add some css // finally add some css
XRFMENU.css = ` $menu.css = `
<style type="text/css"> <style type="text/css">
:root { :root {
--xrf-primary: #6839dc; --xrf-primary: #6839dc;
@ -411,26 +436,25 @@ XRFMENU.css = `
--xrf-overlay-bg: #fffb; --xrf-overlay-bg: #fffb;
--xrf-box-shadow: #0005; --xrf-box-shadow: #0005;
--xrf-red: red; --xrf-red: red;
--xrf-black: #424280;
--xrf-white: #fdfdfd;
--xrf-dark-gray: #343334; --xrf-dark-gray: #343334;
--xrf-gray: #ecf7ff47; --xrf-gray: #424280;
--xrf-white: #fdfdfd;
--xrf-light-gray: #efefef; --xrf-light-gray: #efefef;
--xrf-lighter-gray: #e4e2fb96; --xrf-lighter-gray: #e4e2fb96;
--xrf-font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif; --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-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-1: 14px;
--xrf-font-size-2: 17px; --xrf-font-size-2: 17px;
--xrf-font-size-3: 21px; --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{ .xrf table tr td{
vertical-align:top; vertical-align:top;
} }
.xrf table tr td:nth-child(1){
padding-right:35px;
}
.xrf button, .xrf button,
.xrf input[type="submit"], .xrf input[type="submit"],
.xrf .btn { .xrf .btn {
@ -455,6 +479,7 @@ XRFMENU.css = `
.xrf input[type="submit"]:hover, .xrf input[type="submit"]:hover,
.xrf .btn:hover { .xrf .btn:hover {
background: var(--xrf-secondary); background: var(--xrf-secondary);
text-decoration:none;
} }
.xrf, .xrf *{ .xrf, .xrf *{
@ -561,7 +586,7 @@ XRFMENU.css = `
height:33px; height:33px;
z-index:2000; z-index:2000;
cursor:pointer; cursor:pointer;
min-width:107px; min-width:130px;
text-decoration:none; text-decoration:none;
margin-top: 15px; margin-top: 15px;
line-height:36px; line-height:36px;
@ -570,6 +595,7 @@ XRFMENU.css = `
} }
.xrf a.btn#more{ .xrf a.btn#more{
z-index:3000;
width: 19px; width: 19px;
min-width: 19px; min-width: 19px;
font-size:16px; font-size:16px;
@ -768,5 +794,39 @@ XRFMENU.css = `
text-align:right; text-align:right;
} }
.badge {
display:inline-block;
color: var(--xrf-white);
font-weight: bold;
background: var(--xrf-gray);
border-radius:5px;
padding:0px 4px;
font-size: var(--xrf-font-size-0);
margin-right:10px
}
a.badge {
text-decoration:none;
}
.xrf select{
min-width: 200px;oborder-inline: none;
border-inline: none;
border-block: none;
border: 1px solid #AAA;
box-shadow: 0px 0px 5px #0003;
height: 31px;
border-radius: 5px;
background: var(--xrf-lighter-gray);
}
.xrf table tr td {
vertical-align:middle;
}
.xrf table tr td:nth-child(1){
padding-right:35px;
min-width:80px;
}
</style> </style>
` `

View File

@ -0,0 +1,143 @@
window.accessibility = (opts) => new Proxy({
opts,
enabled: false,
// features
speak_movements: true,
speak_keyboard: true,
speak_notifications: true,
// audio settings
speak_rate: 1,
speak_pitch: 1,
speak_volume: 1,
speak_voice: -1,
toggle(){ this.enabled = !this.enabled },
settings(){
this.toggle() // *TODO* should show settings screen
},
speak(str, override){
if( !this.enabled || !str) return
str = str.replace(/\/\//,' ')
.replace(/:/,'')
.replace(/\//,' slash ')
.replace(/\./,' dot ')
.replace(/#/,' hash ')
.replace(/&/,' and ')
.replace(/=/,' is ')
let speech = window.speechSynthesis
let utterance = new SpeechSynthesisUtterance( str )
if( this.speak_voice != -1) utterance.voice = speech.getVoices()[ this.speak_voice ];
else{
let voices = speech.getVoices()
for(let i = 0; i < voices.length; i++ ){
if( voices[i].lang == navigator.lang ) this.speak_voice = i;
}
}
utterance.rate = this.speak_rate
utterance.pitch = this.speak_pitch
utterance.volume = this.speak_volume
if( override ) speech.cancel()
speech.speak(utterance)
},
init(){
window.addEventListener('keydown', (e) => {
if( !this.speak_keyboard ) return
let k = e.key
switch(k){
case "ArrowUp": k = "forward"; break;
case "ArrowLeft": k = "left"; break;
case "ArrowRight": k = "right"; break;
case "ArrowDown": k = "backward"; break;
}
this.speak(k,true)
})
document.addEventListener('notify', (e) => {
let opts = e.detail
let status = `${opts.status} ` || ''
if( !this.enabled ) return
this.speak(opts.message)
$chat.send({message: opts.message, class:["info","guide"]})
})
document.addEventListener('$menu:buttons:render', (e) => {
let $ = e.detail
let a = [...$.querySelectorAll('a')]
// make sure anchor buttons are accessible by tabbing to them
a.map( (btn) => {
if( !btn.href ) btn.setAttribute("href","javascript:void(0)") // important!
btn.setAttribute("aria-label","button")
})
})
document.addEventListener('$chat:receive', (e) => {
let opts = e.detail
opts.message = opts.message || ''
if( opts.class && ~opts.class.indexOf('info') ) opts.message = `info: ${opts.message}`
this.speak(e.detail.message)
})
opts.addEventListener('pos', (opts) => {
let obj
let description
let msg = "You've teleported to "
let pos = opts.frag.pos
if( pos.string.match(',') ) msg += `coordinates <a href="#pos=${pos.string}">${pos.string}</a>`
else{
msg += `location <a href="#pos=${pos.string}">${pos.string}</a>`
obj = opts.scene.getObjectByName(pos.string)
if( obj ){
description = obj.userData['aria-label'] || ''
}else msg += ", but your teleportation was refused because it cannot be found within this world"
}
$chat.send({html: () => msg, class:["info","guide"]})
})
},
sanitizeTranscript(){
return $chat.$messages.innerText
.replaceAll("<[^>]*>", "") // strip html
.split('\n')
.map( (l) => String(l+'.').replace(/(^|:|;|!|\?|\.)\.$/g,'\$1') ) // add dot if needed
.join('\n')
}
},
{
// auto-trigger events on changes
get(data,k,receiver){ return data[k] },
set(data,k,v){
data[k] = v
switch( k ){
case "enabled": {
let message = (v?"boosting":"unboosting") + " accessibility features"
$('#accessibility.btn').style.filter= v ? 'brightness(1.0)' : 'brightness(0.5)'
if( v ) $chat.visible = true
$chat.send({message,class:['info','guide']})
data.enabled = true
data.speak(message)
data.enabled = v
$chat.$messages.classList[ v ? 'add' : 'remove' ]('guide')
if( !data.readTranscript && (data.readTranscript = true) ){
data.speak( data.sanitizeTranscript() )
}
}
}
}
})
document.addEventListener('$menu:ready', (e) => {
window.accessibility = accessibility(e.detail)
accessibility.init()
document.dispatchEvent( new CustomEvent("accessibility: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 accessibility features" id="accessibility" onclick="accessibility.settings()">👩‍🚀 accessibility</a><br>`])
})

View File

@ -1,38 +0,0 @@
// this orchestrates multiplayer events from the scene graph
window.meeting = (THREE, scene) => new Proxy({
status: 'offline',
peers: {},
add(peerid,data){
data = {lastUpdated: new Date().getTime(), id: peerid, ...data }
this.peers[peerid] = data
scene.dispatchEvent({type:'meeting.peer.add', peer})
},
remove(peerid,data){
delete this.peers[peerid]
scene.dispatchEvent({type:'meeting.peer.remove', peer})
},
send(opts){
},
receive(opts){
}
},
{
// auto-trigger events on changes
get(meeting,k,receiver){ return meeting[k] },
set(meeting,k,v){
let from = meeting[k]
meeting[k] = v
switch( k ){
default: scene.dispatchEvent({type:`meeting.${k}.change`, from, to:v})
}
}
})

View File

@ -0,0 +1,51 @@
// this orchestrates multiplayer events from the scene graph
window.network = (opts) => new Proxy({
connected: false,
peers: {},
plugin: {},
opts,
start(url){
console.log("starting network with url "+(url?url:"default"))
},
add(peerid,data){
data = {lastUpdated: new Date().getTime(), id: peerid, ...data }
this.peers[peerid] = data
opts.scene.dispatchEvent({type:'network.peer.add', peer})
},
remove(peerid,data){
delete this.peers[peerid]
opts.scene.dispatchEvent({type:'network.peer.remove', peer})
},
send(opts){
},
receive(opts){
}
},
{
// auto-trigger events on changes
get(data,k,receiver){ return data[k] },
set(data,k,v){
let from = data[k]
data[k] = v
//switch( k ){
// default: network.opts.scene.dispatchEvent({type:`network.${k}.change`, from, to:v})
//}
}
})
document.addEventListener('$menu:ready', (e) => {
window.network = network(e.detail)
document.dispatchEvent( new CustomEvent("network:ready", e ) )
$menu.buttons = ([`<a class="btn" aria-label="button" aria-description="start text/audio/video chat" id="meeting" onclick="$connections.show()">🧑‍🤝‍🧑 connect</a><br>`])
.concat($menu.buttons)
})

View File

@ -0,0 +1,63 @@
window.matrix = (opts) => new Proxy({
plugin:{
type: 'network',
name: '[matrix] channel',
description: '[matrix] is a standardized decentralized privacy-friendly protocol',
url: 'https://matrix.org',
video: false,
audio: false,
chat: true,
scene: true
},
html: {
generic: (opts) => `<table>
<tr>
<td><a href="${opts.url}" target="_blank" class="badge">matrix</a></td>
<td>
<input type="text" id="channelname" placeholder="channel name"/>
</td>
</tr>
</table>
</div>
`
},
init(){
let network = window.network
network.plugin['matrix'] = this
$connections.chatnetwork = $connections.chatnetwork.concat([this])
$connections.scene = $connections.scene.concat([this])
},
config(opts){
opts = {...opts, ...this.plugin }
let el = document.createElement('div')
let html = this.html.generic(opts)
for( let i in opts ){
if( this.html[i] ) html += this.html[i](opts)
}
el.innerHTML = html
el.addEventListener('mouseover', () => {
window.notify(`${opts.name} is ${opts.description}.<br>You can basically make up your own channelname or use an existing one`)
})
return el
}
},
{
// auto-trigger events on changes
get(data,k,receiver){ return data[k] },
set(data,k,v){
let from = data[k]
data[k] = v
switch( k ){
default: matrix.opts.scene.dispatchEvent({type:`matrix.${k}.change`, from, to:v})
}
}
})
document.addEventListener('$connections:ready', (e) => {
matrix(e.detail).init()
})

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,3 @@
export { MatrixProvider } from "matrix-crdt";
//export * as Y from "yjs";
//export sdk from "matrix-js-sdk";

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
{
"name": "matrix",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
"author": "",
"license": "ISC",
"dependencies": {
"matrix-crdt": "^0.2.1-alpha.1",
"webpack-cli": "^5.1.4",
"yjs": "^13.6.10"
}
}

View File

@ -0,0 +1,11 @@
module.exports = {
entry: './lib.js',
output: {
library: {
type: "umd",
name: "matrix"
},
filename: "matrix-crdt.js",
path: require('path').resolve(__dirname, '../../../../../../dist')
}
};

View File

@ -1,3 +1,85 @@
window.trystero = (opts) => new Proxy({
plugin:{
type: 'network',
name: 'Peer2Peer',
description: 'P2P using WebRTC over bittorrent for signaling & encryption',
url: 'https://github.com/dmotz/trystero',
video: true,
audio: true,
chat: true,
scene: true
},
html: {
generic: (opts) => `<table id="trystero">
<tr>
<td><a href="${opts.url}" target="_blank" class="badge">P2P</a></td>
<td>
<input type="text" id="channelname" placeholder="channel name"/>
</td>
</tr>
</table>
</div>
`
},
handle: null, // { selfId: .... } when connected
link: null,
selfId: null,
connected: false,
names: {},
chat: { send: null, get: null },
name: { send: null, get: null },
init(){
let network = window.network
network.plugin['trystero'] = this
$connections.webcam = $connections.webcam.concat([this])
$connections.chatnetwork = $connections.chatnetwork.concat([this])
$connections.scene = $connections.scene.concat([this])
},
connect(){
// embedded https://github.com/dmotz/trystero (trystero-torrent.min.js build)
console.dir(opts)
this.handle = joinRoom( room.config, room.link )
this.send({message:'📡 [trystero] opening P2P WebRTC-channel via bittorrent',class:['info']})
},
send(opts){ $chat.send({...opts, source: 'trystero'}) },
config(opts){
opts = {...opts, ...this.plugin }
let el = document.createElement('div')
let html = this.html.generic(opts)
for( let i in opts ){
if( this.html[i] ) html += this.html[i](opts)
}
el.innerHTML = html
el.addEventListener('mouseover', () => {
window.notify(`${opts.name} is ${opts.description} <br>by using a serverless technology called <a href="${opts.url}" target="_blank">trystero</a>.<br>You can basically make up your own channelname or choose an existing one`)
})
return el
}
},
{
// auto-trigger events on changes
get(data,k,receiver){ return data[k] },
set(data,k,v){
let from = data[k]
data[k] = v
switch( k ){
default: trystero.opts.scene.dispatchEvent({type:`trystero.${k}.change`, from, to:v})
}
}
})
document.addEventListener('$connections:ready', (e) => {
trystero(e.detail).init()
})
//window.meeting = window.meeting||{} //window.meeting = window.meeting||{}
//window.meeting.trystero = async function(el,com,data){ //window.meeting.trystero = async function(el,com,data){
// //

View File

@ -62,7 +62,6 @@ xrf.frag.src.enableSourcePortation = (src) => {
xrf.frag.src.externalSRC = (url,frag,opts) => { xrf.frag.src.externalSRC = (url,frag,opts) => {
fetch(url, { method: 'HEAD' }) fetch(url, { method: 'HEAD' })
.then( (res) => { .then( (res) => {
console.log(`loading src ${url}`)
let mimetype = res.headers.get('Content-type') let mimetype = res.headers.get('Content-type')
if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf' if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf'
//if( url.match(/\.(fbx|stl|obj)$/) ) mimetype = //if( url.match(/\.(fbx|stl|obj)$/) ) mimetype =