This commit is contained in:
Leon van Kammen 2023-12-19 16:58:46 +01:00
parent f6132a29f4
commit d3b767b665
7 changed files with 961 additions and 810 deletions

View file

@ -6,11 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script> <script src="https://aframe.io/releases/1.5.0/aframe.min.js"></script>
<script src="./../../../dist/xrfragment.aframe.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script>
</head> </head>
<body> <body>
<script src="./../../../dist/xrfragment.aframe.js"></script>
<a-scene 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>
@ -28,15 +28,15 @@
</a-scene> </a-scene>
<script> <script>
// add your logo // the xrf-menu is totally optional but can be handy
// to quickly add your logo & buttons etc
XRFMENU.logo = './../../assets/logo.png' XRFMENU.logo = './../../assets/logo.png'
XRFMENU.morelabel = '⚡' XRFMENU.morelabel = '⚡'
// add your menubuttons: // add your menubuttons:
XRFMENU.html.push(`<a class="btn" aria-label="button" aria-description="about menu" onclick="window.showAbout()">💁 about</a><br>`) XRFMENU.buttons = XRFMENU.buttons.concat([`<a class="btn" aria-label="button" aria-description="about menu" onclick="XRFMENU.showAbout()">💁 about</a><br>`])
showAbout = () => window.notify(` XRFMENU.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>
Allowing rich audiovisual events with(out)<br> Allowing rich audiovisual events with(out)<br>
VR/AR headsets (it's awesome though ♥️)<br> VR/AR headsets (it's awesome though ♥️)<br>

39
src/3rd/js/$.js Normal file
View file

@ -0,0 +1,39 @@
// this project uses #vanillajs #proxies #clean #noframework
//
// menu = $proxy({
// attach: 'body',
// html: (el) => `<ul><li>${el.items.join('</li><li>')}</li></lu>`,
// items: [1,2],
// on: (el,k,v) => {
// switch(k){
// default: return el.outerHTML = el.html(el)
// }
// }
// })
//
// menu.items = menu.items.concat([3,4])
// $('#foo')
// $$('.someclass').map( (el) => el.classList.toggle() )
//
$proxy = (opts) => {
let el = document.createElement('div')
el.innerHTML = opts.html(opts)
el.querySelector = el.querySelector.bind(el)
el.appendChild = el.appendChild.bind(el)
let parent = typeof opts.attach == 'string' ? document.querySelector(opts.attach) : opts.attach
parent.appendChild( el )
el.on = el.addEventListener.bind(el)
for( let i in opts ) el[i] = opts[i]
return new Proxy( el, {
get: (el,k,receiver) => el[k] || '',
set: (el,k,v) => { el[k] = v; el.on(el,k,v) }
})
}
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html) => {
let el = document.createElement('div')
el.innerHTML = html
return el.children[0]
}

View file

@ -62,8 +62,6 @@ window.AFRAME.registerComponent('xrf', {
let url = opts.xrf.string let url = opts.xrf.string
let isLocal = url.match(/^#/) let isLocal = url.match(/^#/)
let hasPos = url.match(/pos=/) let hasPos = url.match(/pos=/)
let meeting = $('[meeting]') ? $('[meeting]').components['meeting'] : false
if(meeting) meeting.notifyTeleport(url)
if( isLocal && hasPos ){ if( isLocal && hasPos ){
// local teleports only // local teleports only
let fastFadeMs = 200 let fastFadeMs = 200

View file

@ -1,391 +1,391 @@
AFRAME.registerComponent('meeting', { //
schema:{ //AFRAME.registerComponent('meeting', {
id:{ required:true, type:'string'}, // schema:{
visitorname:{required:false,type:'string'}, // id:{ required:true, type:'string'},
parentRoom:{required:false,type:'string'}, // visitorname:{required:false,type:'string'},
link:{required:false,type:'string'} // parentRoom:{required:false,type:'string'},
}, // link:{required:false,type:'string'},
remove: function(){ // },
if( this.room ) this.room.leave() //
this.meeting.remove() // // reactive HTML elements
}, // $: document.createElement('div'),
update: function(){ // cameras: {},
setTimeout( () => { // audios: {},
this.remove() // messages: [],
this.init() // placeholderInput: 'enter name here',
},100) // hideChat: false,
}, // log: [],
init: function(){ // selfStream: null,
// embed https://github.com/dmotz/trystero (trystero-torrent.min.js build) //
// events:{
// add css+html // connect: function(e){
let el = this.meeting = document.createElement("div") // if( !this.data.visitorname ){
el.id = 'meeting' // this.data.appendHTMLMessage("Please enter your name below",["info"])
el.innerHTML += `<style type="text/css"> // }else{
#videos{ // if( this.data.parentRoom ) this.data.$chat.add(`leaving ${this.data.parentRoom}`,["info"]);
display:grid-auto-columns; // }
grid-column-gap:5px; // },
margin-bottom:15px; // },
position: fixed; //
top: 0; // remove: function(){
left: 0; // this.el.emit("remove",{})
bottom: 0; // this.data.$.remove()
right: 0; // },
margin: 15px; //
z-index:1500; // update: function(){
} // setTimeout( () => {
#videos > video{ // this.remove()
border-radius:7px; // this.initMeeting()
display:inline-block; // },100)
background:black; // },
width:80px; //
height:60px; // init: function(){
margin-right:5px; //
margin-bottom:5px; // //// when teleport is clicked
vertical-align:top; // //AFRAME.XRF.addEventListener('href', (opts) => {
pointer-events:all; // // if( !opts.click ) return // ignore mouseovers etc
} // // let url = opts.xrf.string
#videos > video:hover{ // // let isTeleport = url.match(/(:\/\/|pos=)/)
filter: brightness(1.8); // // if( isTeleport ){
cursor:pointer; // // url = url[0] == '#' ? document.location.href.replace(/#.*/, opts.xrf.string ) : '?'+opts.xrf.string
} // // this.notifyTeleport( url )
// // }
#chatbar, // //})
button#showchat{ // this.initMeeting()
z-index: 1500; //
position: fixed; // },
bottom: 20px; //
left: 20px; // initMeeting: function(){
width: 48%; // this.data.link = document.location.href
background: white; // this.initHTML()
padding: 0px 0px 0px 15px; //
border-radius: 30px; // // load plugins
max-width: 500px; // if( window.meeting.trystero ) new window.meeting.trystero(this.el,this,this.data)
box-sizing: border-box; //
box-shadow: 0px 0px 5px 5px #0002; // this.getMedia()
} // this.emit("connect",{})
button#showchat{ // },
z-index:1550; //
color:white; // getMedia: async function(){
border:0; // this.data.selfStream = await navigator.mediaDevices.getUserMedia({
display:none; // audio: true,
height: 44px; // video: {
background:#07F; // width: 320,
font-weight:bold; // height: 240,
} // frameRate: {
#chatbar input{ // ideal: 30,
border:none; // min: 10
width:90%; // }
box-sizing:border-box; // }
} // })
#chat{ // let meVideo = this.createVideoElement(this.data.selfStream)
position: absolute; // meVideo.muted = true
top: 100px; // },
left: 0; //
right: 0; // // central function to broadcast stuff to chat
bottom: 88px; // send: function(opts){
padding: 15px; // opts = { sendLocal: true, sendNetwork:true, ...opts }
pointer-events: none; // if( opts.sendNetwork ){
overflow-y:auto; // if( !this.sendChat ) return
} // this.sendChat({opts}) // send to network
#chat .msg{ // }
background: #fff; // if( opts.sendLocal ){
display: inline-block; // this.data.$chat.add( opts ) // send to HTML screen
padding: 6px 17px; // }
border-radius: 20px; // },
color: #000c; //
margin-bottom: 10px; // createVideoElement: function(stream,id){
line-height:23px; // let video = this.data.videos[peerId]
pointer-events:visible; // const videoContainer = document.getElementById('videos')
border: 1px solid #ccc; // // if this peer hasn't sent a stream before, create a video element
} // if (!video) {
#chat .msg.info{ // video = document.createElement('video')
background: #333; // video.autoplay = true
color: #FFF; //
font-size: 14px; // // add video element to the DOM
padding: 0px 16px; // videoContainer.appendChild(video)
} // }
#chat .msg.info a, //
#chat .msg.info a:visited{ // video.srcObject = stream
color: #aaf; // video.resize = (state) => {
text-decoration: none; // if( video.resize.state == undefined ) video.resize.state = false
transition:0.3s; // if( state == undefined ) state = (video.resize.state = !video.resize.state )
} // video.style.width = state ? '320px' : '80px'
#chat .msg.info a:hover, // video.style.height = state ? '200px' : '60px'
#chat button:hover{ // }
filter: brightness(1.8); // this.data.videos[ id || 'me' ] = video
text-decoration: underline; // return video
} // },
#chat button { //
margin: 0px 15px 10px 0px; // initHTML: function(){
background: #07F; //
color: #FFF; // // add css
border-radius: 7px; // this.data.$.innerHTML += `<style type="text/css">
padding: 11px 15px; // #videos{
border: 0; // display:grid-auto-columns;
font-weight: bold; // grid-column-gap:5px;
box-shadow: 0px 0px 5px 5px #0002; // margin-bottom:15px;
pointer-events:all; // position: fixed;
} // top: 0;
#chat,#chatbar,#chatbar *, #chat *{ // left: 0;
font-family:monospace; // bottom: 0;
font-size:15px; // right: 0;
} // margin: 15px;
</style> // z-index:1500;
<div id="videos" style="pointer-events:none"></div> // }
<div id="chat" aria-live="assertive" aria-relevant></div> // #videos > video{
<button id="showchat" class="btn">show chat</button> // border-radius:7px;
<div id="chatbar"> // display:inline-block;
<input id="chatline" type="text" placeholder="enter name"></input> // background:black;
</div>` // width:80px;
document.body.appendChild(el) // height:60px;
// margin-right:5px;
let chatline = this.chatline = document.querySelector("#chatline") // margin-bottom:5px;
let chat = this.chat = document.querySelector("#chat") // vertical-align:top;
chat.log = [] // save raw chatlog to prime new visitors // pointer-events:all;
chat.append = (str,classes,buttons) => this.chatAppend(str,classes,buttons) // }
// #videos > video:hover{
this.initChatLine() // filter: brightness(1.8);
// cursor:pointer;
if( !this.data.visitorname ) this.chat.append("Please enter your name below",["info"]) // }
else{ //
if( this.data.parentRoom ) this.chat.append(`leaving ${this.data.parentRoom}`,["info"]); // #chatbar,
this.trysteroInit() // button#showchat{
} // z-index: 1500;
}, // position: fixed;
// bottom: 20px;
chatAppend: function(str,classes,buttons){ // left: 20px;
if( str ){ // width: 48%;
str = str.replace('\n', "<br>") // background: white;
.replace(/^[a-zA-Z-0-9]+?[:\.]/,'<b>$&</b>') // padding: 0px 0px 0px 15px;
let el = this.createMsg(str) // border-radius: 30px;
if( classes ) classes.map( (c) => el.classList.add(c) ) // max-width: 500px;
this.chat.appendChild(el) // send to screen // box-sizing: border-box;
this.chat.innerHTML += '<br>' // box-shadow: 0px 0px 5px 5px #0002;
if( !classes ) this.chat.log.push(str) // }
} // button#showchat{
if( buttons ){ // z-index:1550;
for( let i in buttons ){ // color:white;
let btn = document.createElement("button") // border:0;
btn.innerText = i // display:none;
btn.addEventListener('click', () => buttons[i]() ) // height: 44px;
this.chat.appendChild(btn) // background:#07F;
} // font-weight:bold;
this.chat.innerHTML += '<br>' // }
} // #chatbar input{
this.chat.scrollTop = this.chat.scrollHeight; // scroll to bottom // border:none;
}, // width:90%;
// box-sizing:border-box;
trysteroInit: async function(){ // }
const { joinRoom } = await import("./../../../dist/trystero-torrent.min.js"); // #chat{
// position: absolute;
if( !document.location.hash.match(/meet/) ){ // top: 100px;
document.location.hash += document.location.hash.match(/#/) ? '&meet' : '#meet' // left: 0;
} // right: 0;
let roomname = this.roomname = this.data.link = document.location.href // bottom: 88px;
const config = this.config = {appId: this.data.id } // padding: 15px;
const room = this.room = joinRoom(config, roomname ) // pointer-events: none;
this.chat.append("joined meeting at "+roomname,["info"]); // overflow-y:auto;
this.chat.append("copied meeting link to clipboard",["info"]); // }
// #chat .msg{
const idsToNames = this.idsToNames = {} // background: #fff;
const [sendName, getName] = room.makeAction('name') // display: inline-block;
const [sendChat, getChat] = this.room.makeAction('chat') // padding: 6px 17px;
this.sendChat = sendChat // border-radius: 20px;
this.sendName = sendName // color: #000c;
this.getChat = getChat // margin-bottom: 10px;
this.getName = getName // line-height:23px;
// pointer-events:visible;
// tell other peers currently in the room our name // border: 1px solid #ccc;
idsToNames[ room.selfId ] = this.data.visitorname.substr(0,15) // }
sendName( this.data.visitorname ) // #chat .msg.info{
// background: #333;
// listen for peers naming themselves // color: #FFF;
getName((name, peerId) => (idsToNames[peerId] = name)) // font-size: 14px;
// padding: 0px 16px;
this.initChat() // }
// #chat .msg.info a,
// this object can store audio instances for later // #chat .msg.info a:visited{
const peerAudios = this.peerAudios = {} // color: #aaf;
const peerVideos = this.peerVideos = {} // text-decoration: none;
// get a local audio stream from the microphone // transition:0.3s;
const selfStream = await navigator.mediaDevices.getUserMedia({ // }
audio: true, // #chat .msg.info a:hover,
video: { // #chat button:hover{
width: 320, // filter: brightness(1.8);
height: 240, // text-decoration: underline;
frameRate: { // }
ideal: 30, // #chat button {
min: 10 // margin: 0px 15px 10px 0px;
} // background: #07F;
} // color: #FFF;
}) // border-radius: 7px;
let meVideo = this.addVideo(selfStream, room.selfId) // padding: 11px 15px;
meVideo.muted = true // border: 0;
// font-weight: bold;
// send stream to peers currently in the room // box-shadow: 0px 0px 5px 5px #0002;
room.addStream(selfStream) // pointer-events:all;
// }
// send stream + chatlog to peers who join later // #chat,#chatbar,#chatbar *, #chat *{
room.onPeerJoin( (peerId) => { // font-family:monospace;
room.addStream(selfStream, peerId) // font-size:15px;
sendName( name, peerId) // }
this.sendChat({prime: this.chat.log}, peerId ) // </style>
}) // <div id="videos" style="pointer-events:none"></div>
// <div id="chat" aria-live="assertive" aria-relevant></div>
room.onPeerLeave( (peerId) => { // <div id="chatfooter"></div>
console.log(`${idsToNames[peerId] || 'a visitor'} left`) // `
if( peerVideos[peerId] ){ // document.body.appendChild(this.data.$)
peerVideos[peerId].remove() //
delete peerVideos[peerId] // // reactive logic
} // this.data = new Proxy(this.data, {
delete idsToNames[peerId] // html: {
}) // chatbar: (data) => data.showChat
// ? `<div id="chatbar">
// handle streams from other peers // <input id="chatline" type="text" placeholder="${data.placeholderInput}"></input>
room.onPeerStream((stream, peerId) => { // </div>`
// create an audio instance and set the incoming stream // : `<button id="showchat" class="btn">show chat</button>`
const audio = new Audio() // },
audio.srcObject = stream // get(data,k ){ return data[k] },
audio.autoplay = true // set(data,k,v){
// data[k] = v
// add the audio to peerAudio object if you want to address it for something // switch( k ){
// later (volume, etc.) //
peerAudios[peerId] = audio // case 'cameras':{
}) // data.$videos.innerHTML = ''
// for( let k in data.cameras ) el.appendChild( data.cameras[k] ) )
room.onPeerStream((stream, peerId) => { // return
this.addVideo(stream,peerId) // }
}) //
// case 'audios':{
// show hide chat on small screens // data.$videos.innerHTML = ''
}, // for( let k in data.cameras ) el.appendChild( data.cameras[k] ) )
// return
addVideo: function(stream,peerId){ // }
let video = this.peerVideos[peerId] //
const videoContainer = document.getElementById('videos') // case 'showChat':{
// if this peer hasn't sent a stream before, create a video element // data.$chatfooter.innerHTML = this.html.chatbar(data)
if (!video) { // data.$chatline.addEventListener("keydown", this.onChatInput )
video = document.createElement('video') // if( v ){
video.autoplay = true // data.$chatfooter.querySelector('#chatline').focus()
// }
// add video element to the DOM // return
videoContainer.appendChild(video) // }
} //
// }
video.srcObject = stream // },
video.resize = (state) => { // })
if( video.resize.state == undefined ) video.resize.state = false //
if( state == undefined ) state = (video.resize.state = !video.resize.state ) // // trigger 1st render
video.style.width = state ? '320px' : '80px' // this.data.showChat = true
video.style.height = state ? '200px' : '60px' //
} // // setup handles
// this.data.$videos = this.data.$.querySelector('#meeting #videos')
video.addEventListener('click', () => video.resize() ) // this.data.$chatfooter = this.data.$.querySelector('#meeting #chatfooter')
// this.data.$chatline = this.data.$chatfooter.querySelector('#chatline')
this.peerVideos[peerId] = video // this.data.$chat = this.data.$.querySelector('#meeting #chat')
return video // this.data.$chat.add = this.addMessage
}, //
// },
createMsg: function(str){ //
let el = document.createElement("div") // addMessage: function(opts){
el.className = "msg" // let {str,classes,buttons) = opts
el.innerHTML = this.linkify(str) // let el = this.data.$chat
return el //
}, // const createMsg = (str) => {
// let el = document.createElement("div")
// central function to broadcast stuff to chat // el.className = "msg"
send: function(str,classes,buttons){ // el.innerHTML = this.linkify(str)
if( !this.sendChat ) return // return el
this.sendChat({content:str}) // send to network // }
this.chat.append(str,classes,buttons) // send to screen //
}, // if( str ){
// str = str.replace('\n', "<br>")
initChatLine: function(){ // .replace(/^[a-zA-Z-0-9]+?[:\.]/,'<b>$&</b>')
let chatline = this.chatline = document.querySelector("#chatline") // let msg = this.createMsg(str)
chatline.addEventListener("keydown", (e) => { // if( classes ) classes.map( (c) => msg.classList.add(c) )
if( e.key !== "Enter" ) return // el.appendChild(msg) // send to screen
if( !this.data.visitorname ){ // el.innerHTML += '<br>'
this.data.visitorname = chatline.value.toLowerCase() // if( !classes ) this.data.log.push(str)
this.data.visitorname = this.data.visitorname.replace(/[^a-z]+/g,'-') // }
this.chat.append("note: camera/mic access is totally optional ♥️",["info"]) // if( buttons ){
this.chatline.setAttribute("placeholder","chat here") // for( let i in buttons ){
this.trysteroInit() // let btn = document.createElement("button")
}else{ // btn.innerText = i
let str = `${this.idsToNames[ this.room.selfId ]}: ${chatline.value.substr(0,65515).trim()}` // btn.addEventListener('click', () => buttons[i]() )
this.send(str) // el.appendChild(btn)
} // }
chatline.value = '' // el.innerHTML += '<br>'
event.preventDefault(); // }
event.target.blur() // el.scrollTop = el.scrollHeight; // scroll to bottom
}) // }
// },
// on small screens/mobile make chat toggle-able //
if( window.outerWidth < 1024 ){ // onChatInput: function(e){
let show = (state) => () => { // if( e.key !== "Enter" ) return
$('#chat').style.display = state ? '' : 'none' // let $chatline = this.data.$chatline
$('#chatline').style.display = state ? '' : 'none' // if( !this.data.visitorname ){
$('button#showchat').style.display = state ? 'none' : 'block' // this.data.visitorname = chatline.value.toLowerCase()
} // this.data.visitorname = this.data.visitorname.replace(/[^a-z]+/g,'-')
$('.a-canvas').addEventListener('click', show(false) ) // this.send({message:"note: camera/mic access is totally optional ♥️", classes:["info"], isLocal:false})
$('.a-canvas').addEventListener('touchstart', show(false) ) // $chatline.setAttribute("placeholder","chat here")
$('#showchat').addEventListener('touchstart', show(true) ) // }else{
$('#showchat').addEventListener('click', show(true) ) // let str = `${this.data.visitorname}: ${$chatline.value.substr(0,65515).trim()}`
} // this.send({message:str})
}, // }
// $chatline.value = ''
initChat: function(){ // event.preventDefault();
// listen for chatmsg // event.target.blur()
this.getChat((data, peerId) => { // },
if( data.prime ){ //
if( this.chat.primed ) return // only prime once // enableSmallScreen: function(){
console.log("receiving prime") // // on small screens/mobile make chat toggle-able
data.prime.map( (l) => chat.append(l) ) // send log to screen // if( window.outerWidth < 1024 ){
this.chat.primed = true // let show = (state) => () => this.data.showChat = state
} // $('.a-canvas').addEventListener('click', () => show(false) )
chat.append(data.content,data.classes,data.buttons) // send to screen // $('.a-canvas').addEventListener('touchstart', () => show(false) )
}) // $('#showchat').addEventListener('touchstart', () => show(true) )
// $('#showchat').addEventListener('click', () => show(true) )
this.notifyTeleport() // }
return this // }
}, //
// //notifyTeleport: function(url){
notifyTeleport: function(url,buttons){ // // url = url || this.roomname
// send to network // // url = url.replace(/(#|&)meet/,'')
this.sendChat({ // // let message = `${this.data.visitorname} teleported to ${url}`
content: `${this.data.visitorname} teleported to ${this.roomname}`, // // this.send({
classes: ["info"], // // message,
buttons // // classes: ["info"]
}) // // })
}, // //},
//
linkify: function(t){ // //linkify: function(t){
const m = t.match(/(?<=\s|^)[a-zA-Z0-9-:/]+[?\.][a-zA-Z0-9-].+?(?=[.,;:?!-]?(?:\s|$))/g) // // const m = t.match(/(?<=\s|^)[a-zA-Z0-9-:/]+[?\.][a-zA-Z0-9-].+?(?=[.,;:?!-]?(?:\s|$))/g)
if (!m) return t // // if (!m) return t
const a = [] // // const a = []
m.forEach(x => { // // m.forEach(x => {
const [t1, ...t2] = t.split(x) // // const [t1, ...t2] = t.split(x)
a.push(t1) // // a.push(t1)
t = t2.join(x) // // t = t2.join(x)
let y = (!(x.match(/:\/\//)) ? 'https://' : '') + x // // let url = (!(x.match(/:\/\//)) ? 'https://' : '') + x
let attr = 'target="_blank"' // // let attr = `href="${url}" target="_blank"`
if (isNaN(x) ){ // // if (isNaN(x) ){
let url_human = y.split('/')[2] // // let url_human = url.split('/')[2]
let isXRFragment = y.match(/\.(glb|gltf|obj|usdz|fbx|col)/) // // let isXRFragment = url.match(/\.(glb|gltf|obj|usdz|fbx|col)/)
if( isXRFragment ){ // detect xr fragments // // if( isXRFragment ){ // detect xr fragments
isMeeting = y.match(/(#|&)meet/) // // isMeeting = url.match(/(#|&)meet/)
url_human = y.replace(/.*[?\?]/,'') // shorten xr fragment links // // url_human = url.replace(/.*[?\?]/,'') // shorten xr fragment links
.replace(/[?\&]meet/,'') // // .replace(/[?\&]meet/,'')
y = y.replace(/.*[\?]/, '?') // start from search (to prevent page-refresh) // // let onclick = [ `xrf.navigator.to('${url}')` ]
attr = '' // // if( isMeeting ) onclick.push(`$('[meeting]').coms['meeting'].update()`)
if( isMeeting ) attr = `onclick="$('[meeting]').components['meeting'].update()"` // // attr = `onclick="${onclick.join(';')}"`
} // // }
a.push(`<a href="${y}" ${attr} style="pointer-events:all">${url_human}</a>`) // // a.push(`<a ${attr} style="pointer-events:all">${url_human}</a>`)
}else // // }else
a.push(x) // // a.push(x)
}) // // })
a.push(t) // // a.push(t)
return a.join('') // // return a.join('')
} // //}
//
}); //})

View file

@ -0,0 +1,109 @@
//window.meeting = window.meeting||{}
//window.meeting.trystero = async function(el,com,data){
//
// // embed https://github.com/dmotz/trystero (trystero-torrent.min.js build)
// const { joinRoom } = await import("./../../../dist/trystero-torrent.min.js");
// this.room = {
// handle: null,
// link: null,
// selfId: null,
// names: {},
// chat: { send: null, get: null },
// name: { send: null, get: null },
// config: {appId: this.data.id }
// }
//
// this.sendName = null
//
// this.send = (opts) => com.send({...opts, source: 'trystero'})
//
// el.addEventListener('remove', () => {
// if( this.room.handle ) this.room.handle.leave()
// })
//
// el.addEventListener('connect', async () => {
// let room = this.room
//
// room.link = this.data.link
// if( !room.linkmatch(/(#|&)meet/) ){
// room.link = room.link.match(/#/) ? '&meet' : '#meet'
// }
// room.handle = joinRoom( room.config, room.link )
// room.selfId = room.handle.selfId
//
// this.send({
// message: "joined meeting at "+roomname.replace(/(#|&)meet/,''), // dont trigger init()
// classes: ["info"],
// sendNetwork:false
// })
//
// this.send({
// message:"copied meeting link to clipboard",
// classes: ["info"],
// sendNetwork:false
// })
//
// // setup trystero events
// const [sendName, getName] = room.makeAction('name')
// const [sendChat, getChat] = room.makeAction('chat')
// room.chat.send = sendChat
// room.chat.get = getChat
// room.name.send = sendName
// room.name.get = getName
//
// // tell other peers currently in the room our name
// room.names[ room.selfId ] = com.data.visitorname.substr(0,15)
// room.name.send( com.data.visitorname )
//
// // listen for peers naming themselves
// this.name.get((name, peerId) => (room.names[peerId] = name))
//
// // send self stream to peers currently in the room
// room.addStream(com.selfStream)
//
// // send stream + chatlog to peers who join later
// room.onPeerJoin( (peerId) => {
// room.addStream( com.selfStream, peerId)
// room.name.send( com.data.visitorname, peerId)
// room.chat.send({prime: com.log}, peerId )
// })
//
// room.onPeerLeave( (peerId) => {
// console.log(`${room.names[peerId] || 'a visitor'} left`)
// if( com.videos[peerId] ){
// com.videos[peerId].remove()
// delete com.videos[peerId]
// }
// delete room.names[peerId]
// })
//
// // handle streams from other peers
// room.onPeerStream((stream, peerId) => {
// // create an audio instance and set the incoming stream
// const audio = new Audio()
// audio.srcObject = stream
// audio.autoplay = true
// // add the audio to peerAudio object if you want to address it for something
// // later (volume, etc.)
// com.data.audios[peerId] = audio
// })
//
// room.onPeerStream((stream, peerId) => {
// com.createVideoElement(stream,peerId)
// })
//
// // listen for chatmsg
// room.chat.get((data, peerId) => {
// if( data.prime ){
// if( com.log.length > 0 ) return // only prime once
// console.log("receiving prime")
// data.prime.map( (l) => this.send({message:l, sendLocal:false ) ) // send log to screen
// this.chat.primed = true
// }
// this.send({ ...data, sendLocal: false}) // send to screen
// })
//
// }
//
//
//}

View file

@ -4,7 +4,7 @@ AFRAME.registerComponent('xrf-menu', {
}, },
init: function(){ init: function(){
// add css+html // add css+html
window.XRFMENU.addHTML() window.XRFMENU.init()
$('a-scene').addEventListener('XRF', this.onXRFready ) $('a-scene').addEventListener('XRF', this.onXRFready )
@ -17,6 +17,7 @@ AFRAME.registerComponent('xrf-menu', {
onXRFready: function(){ onXRFready: function(){
let XRF = window.AFRAME.XRF let XRF = window.AFRAME.XRF
return
XRFMENU.setupMenu( XRF ) XRFMENU.setupMenu( XRF )
// on localhost enable debugging mode for developer convenience // on localhost enable debugging mode for developer convenience

View file

@ -1,16 +1,426 @@
// handy shortcuts
if( !window.$ ) window.$ = (s) => s ? document.querySelector(s) : false $XRFMENU = $el(
if( !window.$$ ) window.$$ = (s) => s ? [ ...document.querySelectorAll(s) ] : false `<div id="menu" x-bind="XRFMENU">
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" type="text/javascript"></script>
<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-overlay-bg: #fffb;
--xrf-box-shadow: #0005;
--xrf-red: red;
--xrf-black: #424280;
--xrf-white: #fdfdfd;
--xrf-dark-gray: #343334;
--xrf-gray: #ecf7ff47;
--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-1: 14px;
--xrf-font-size-2: 17px;
--xrf-font-size-3: 21px;
}
.xrf table tr td{
vertical-align:top;
}
.xrf table tr td:nth-child(1){
padding-right:35px;
}
.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: 32px;
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);
}
.xrf button:hover,
.xrf input[type="submit"]:hover,
.xrf .btn:hover {
background: var(--xrf-secondary);
}
.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;
}
#overlay{
background: var(--xrf-overlay-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;
}
#overlay .logo{
width: 92px;
position: absolute;
top: 9px;
left: 93px;
height: 30px;
background-size: contain;
background-repeat: no-repeat;
}
#overlay > input[type="submit"] {
height: 32px;
position: absolute;
right: 20px;
top: 2px;
}
#overlay > button#navback,
#overlay > button#navforward {
height: 32px;
font-size: var(--xrf-font-size-1);
position: absolute;
left: 9px;
padding: 2px 13px;
border-radius:6px;
top: 8px;
color: var(--xrf-light-gray);
width: 36px;
min-width: unset;
}
#overlay > button#navforward {
left:49px;
}
#overlay > #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;
}
.menu .btn{
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:107px;
text-decoration:none;
display:none;
margin-top: 15px;
line-height:36px;
margin-right:10px;
text-align:left;
}
.xrf a.btn#more{
width: 19px;
min-width: 19px;
font-size:16px;
text-align: center;
background:white;
}
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;
}
}
/* notifications */
.js-snackbar-container .btn,
.js-snackbar-container input[type=submit],
.js-snackbar-container button{
margin-bottom:15px;
}
.js-snackbar-container {
position: absolute;
top: 10px;
left: 0px;
display: flex;
align-items: center;
width:100%;
max-width: 100%;
padding: 10px;
z-index:1001;
justify-content: center;
overflow: hidden;
}
.js-snackbar-container * {
box-sizing: border-box;
}
.js-snackbar__wrapper {
--color-c: #555;
--color-a: #FFF;
}
.js-snackbar__wrapper {
overflow: hidden;
height: auto;
margin: 5px 0;
transition: all ease .5s;
border-radius: 3px;
box-shadow: 0 0 4px 0 var(--xrf-box-shadow);
right: 20px;
position: fixed;
top: 55px;
}
.js-snackbar {
display: inline-flex;
box-sizing: border-box;
border-radius: 3px;
color: var(--color-c);
background-color: var(--color-a);
vertical-align: bottom;
}
.js-snackbar__close,
.js-snackbar__status,
.js-snackbar__message {
position: relative;
}
.js-snackbar__message {
margin: 12px;
}
.js-snackbar__status {
display: none;
width: 15px;
margin-right: 5px;
border-radius: 3px 0 0 3px;
background-color: transparent;
}
.js-snackbar__status.js-snackbar--success,
.js-snackbar__status.js-snackbar--warning,
.js-snackbar__status.js-snackbar--danger,
.js-snackbar__status.js-snackbar--info {
display: block;
}
.js-snackbar__status.js-snackbar--success {
background-color: #4caf50;
}
.js-snackbar__status.js-snackbar--warning {
background-color: #ff9800;
}
.js-snackbar__status.js-snackbar--danger {
background-color: #ff6060;
}
.js-snackbar__status.js-snackbar--info {
background-color: #CCC;
}
.js-snackbar__close {
cursor: pointer;
display: flex;
align-items: center;
padding: 0 10px;
user-select: none;
}
.js-snackbar__close:hover {
background-color: #4443;
}
.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 {
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:149px;
box-sizing:border-box;
}
.footer .menu{
text-align:right;
}
</style>
<div id="overlay" class="xrf" style="display:none">
<div class="logo" :style="'background-image: url('+$store.XRFMENU.logo+')'"></div>
<button id="navback" onclick="history.back()">&lt;</button>
<button id="navforward" onclick="history.forward()">&gt;</button>
<input type="submit" value="load 3D file"></input>
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )" style="display:none"/>
</div>
<div class="xrf footer">
<div id="buttons" class="menu">
<a class="btn" id="more" style="display:inline-block" x-text="$store.XRFMENU.morelabel"></a>
</div>
</div>
</div>`
)
//${window.XRFMENU.html.map( (html) => typeof html == "function" ? html() : html ).join('\n')}
window.XRFMENU = { window.XRFMENU = {
logo: './../../assets/logo.png', $: $XRFMENU,
html: [ morelabel: '⚡',
enabled: false,
logo: './../../assets/logo.png',
buttons: [
`<a class="btn" aria-label="button" aria-description="start text/audio/video chat" id="meeting" target="_blank">🧑‍🤝‍🧑 meeting</a><br>`, `<a class="btn" aria-label="button" aria-description="start text/audio/video chat" id="meeting" target="_blank">🧑‍🤝‍🧑 meeting</a><br>`,
`<a class="btn" aria-label="button" aria-description="share URL/screenshot/embed" id="share" target="_blank" onclick="window.share()">🔗 share</a><br>` `<a class="btn" aria-label="button" aria-description="share URL/screenshot/embed" id="share" target="_blank" onclick="window.share()">🔗 share</a><br>`
], ],
init: () => {
// bind object as alpine app
document.body.appendChild( XRFMENU.$ )
document.addEventListener('alpine:init', () => Alpine.bind('XRFMENU', () => XRFMENU ) )
//if( XRFMENU.logo ) $('.logo').style['background-image'] = `url(${XRFMENU.logo})`
window.notify = XRFMENU.notify(window)
window.share = XRFMENU.share
window.download = XRFMENU.download
window.notify('loading '+document.location.search.substr(1))
// reroute console messages to snackbar notifications
console.log = ( (log) => function(str){
if( String(str).match(/(:.*#|note:)/) ) window.notify(str)
log(str)
})(console.log)
// allow iframe to open url
window.addEventListener('message', (event) => {
if (event.data && event.data.url) {
window.open(event.data.url, '_blank');
}
});
},
loadFile(contentLoaders, multiple){ loadFile(contentLoaders, multiple){
return () => { return () => {
window.notify("if you're on Meta browser, file-uploads might be disabled") window.notify("if you're on Meta browser, file-uploads might be disabled")
@ -299,409 +709,3 @@ window.XRFMENU = {
} }
} }
window.XRFMENU.addHTML = () => {
let el = document.createElement("div")
el.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-overlay-bg: #fffb;
--xrf-box-shadow: #0005;
--xrf-red: red;
--xrf-black: #424280;
--xrf-white: #fdfdfd;
--xrf-dark-gray: #343334;
--xrf-gray: #ecf7ff47;
--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-1: 14px;
--xrf-font-size-2: 17px;
--xrf-font-size-3: 21px;
}
.xrf table tr td{
vertical-align:top;
}
.xrf table tr td:nth-child(1){
padding-right:35px;
}
.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: 32px;
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);
}
.xrf button:hover,
.xrf input[type="submit"]:hover,
.xrf .btn:hover {
background: var(--xrf-secondary);
}
.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;
}
#overlay{
background: var(--xrf-overlay-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;
}
#overlay .logo{
width: 92px;
position: absolute;
top: 9px;
left: 93px;
height: 30px;
background-size: contain;
background-repeat: no-repeat;
}
#overlay > input[type="submit"] {
height: 32px;
position: absolute;
right: 20px;
top: 2px;
}
#overlay > button#navback,
#overlay > button#navforward {
height: 32px;
font-size: var(--xrf-font-size-1);
position: absolute;
left: 9px;
padding: 2px 13px;
border-radius:6px;
top: 8px;
color: var(--xrf-light-gray);
width: 36px;
min-width: unset;
}
#overlay > button#navforward {
left:49px;
}
#overlay > #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;
}
.menu .btn{
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:107px;
text-decoration:none;
display:none;
margin-top: 15px;
line-height:36px;
margin-right:10px;
text-align:left;
}
.xrf a.btn#more{
width: 19px;
min-width: 19px;
font-size:16px;
text-align: center;
background:white;
}
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;
}
}
/* notifications */
.js-snackbar-container .btn,
.js-snackbar-container input[type=submit],
.js-snackbar-container button{
margin-bottom:15px;
}
.js-snackbar-container {
position: absolute;
top: 10px;
left: 0px;
display: flex;
align-items: center;
width:100%;
max-width: 100%;
padding: 10px;
z-index:1001;
justify-content: center;
overflow: hidden;
}
.js-snackbar-container * {
box-sizing: border-box;
}
.js-snackbar__wrapper {
--color-c: #555;
--color-a: #FFF;
}
.js-snackbar__wrapper {
overflow: hidden;
height: auto;
margin: 5px 0;
transition: all ease .5s;
border-radius: 3px;
box-shadow: 0 0 4px 0 var(--xrf-box-shadow);
right: 20px;
position: fixed;
top: 55px;
}
.js-snackbar {
display: inline-flex;
box-sizing: border-box;
border-radius: 3px;
color: var(--color-c);
background-color: var(--color-a);
vertical-align: bottom;
}
.js-snackbar__close,
.js-snackbar__status,
.js-snackbar__message {
position: relative;
}
.js-snackbar__message {
margin: 12px;
}
.js-snackbar__status {
display: none;
width: 15px;
margin-right: 5px;
border-radius: 3px 0 0 3px;
background-color: transparent;
}
.js-snackbar__status.js-snackbar--success,
.js-snackbar__status.js-snackbar--warning,
.js-snackbar__status.js-snackbar--danger,
.js-snackbar__status.js-snackbar--info {
display: block;
}
.js-snackbar__status.js-snackbar--success {
background-color: #4caf50;
}
.js-snackbar__status.js-snackbar--warning {
background-color: #ff9800;
}
.js-snackbar__status.js-snackbar--danger {
background-color: #ff6060;
}
.js-snackbar__status.js-snackbar--info {
background-color: #CCC;
}
.js-snackbar__close {
cursor: pointer;
display: flex;
align-items: center;
padding: 0 10px;
user-select: none;
}
.js-snackbar__close:hover {
background-color: #4443;
}
.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 {
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:149px;
box-sizing:border-box;
}
.footer .menu{
text-align:right;
}
</style>
<div id="overlay" class="xrf" style="display:none">
<div class="logo"></div>
<button id="navback" onclick="history.back()">&lt;</button>
<button id="navforward" onclick="history.forward()">&gt;</button>
<input type="submit" value="load 3D file"></input>
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )" style="display:none"/>
</div>
<!-- open AFRAME inspector: $('a-scene').components.inspector.openInspector() -->
<div class="xrf footer">
<div id="buttons" class="menu">
${window.XRFMENU.html.map( (html) => typeof html == "function" ? html() : html ).join('\n')}
<a class="btn" id="more" style="display:inline-block">${window.XRFMENU.morelabel}</a>
</div>
</div>
`
document.body.appendChild(el)
if( XRFMENU.logo ) $('.logo').style['background-image'] = `url(${XRFMENU.logo})`
window.notify = XRFMENU.notify(window)
window.share = XRFMENU.share
window.download = XRFMENU.download
window.notify('loading '+document.location.search.substr(1))
// reroute console messages to snackbar notifications
console.log = ( (log) => function(str){
if( String(str).match(/(:.*#|note:)/) ) window.notify(str)
log(str)
})(console.log)
// allow iframe to open url
window.addEventListener('message', (event) => {
if (event.data && event.data.url) {
window.open(event.data.url, '_blank');
}
});
}