1672 lines
9.0 MiB
JavaScript
1672 lines
9.0 MiB
JavaScript
|
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 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","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{
|
||
|
font-size: 14px;
|
||
|
padding: 3px 16px;
|
||
|
}
|
||
|
#messages.guide, .guide{
|
||
|
display:unset;
|
||
|
}
|
||
|
#messages .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;
|
||
|
}
|
||
|
|
||
|
#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;
|
||
|
}
|
||
|
</style>`
|
||
|
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(){
|
||
|
$chat.visible = true
|
||
|
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) )
|
||
|
},
|
||
|
|
||
|
randomName(){
|
||
|
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)];
|
||
|
var c = names[Math.floor(Math.random() * names.length)];
|
||
|
return String(`${a}-${b}-${c}`).toLowerCase()
|
||
|
}
|
||
|
|
||
|
},{
|
||
|
|
||
|
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>`
|
||
|
// reactive component for displaying the menu
|
||
|
menuComponent = {
|
||
|
|
||
|
html: `
|
||
|
<div id="overlay" class="xrf">
|
||
|
<div class="logo" ></div>
|
||
|
<button id="navback" onclick="history.back()"><</button>
|
||
|
<button id="navforward" onclick="history.forward()">></button>
|
||
|
<input id="load" 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 class="menu">
|
||
|
<div id="buttons"></div>
|
||
|
<a class="btn" id="more" aria-title="menu button" aria-description="menu with options, like extra accessibility" onclick="$menu.toggle()"></a><br>
|
||
|
</div>
|
||
|
</div>
|
||
|
`,
|
||
|
|
||
|
init: (el) => new Proxy({
|
||
|
morelabel: '⚡',
|
||
|
collapsed: false,
|
||
|
logo: './../../assets/logo.png',
|
||
|
buttons: [`<a class="btn" aria-label="button" aria-title="share button" aria-description="share URL/screenshot/embed" id="share" onclick="$menu.share()">🔗 share</a><br>`],
|
||
|
|
||
|
$overlay: $overlay = el.querySelector('#overlay'),
|
||
|
$logo: $logo = el.querySelector('.logo'),
|
||
|
$uri: $uri = el.querySelector('#uri'),
|
||
|
$buttons: $buttons = el.querySelector('#buttons'),
|
||
|
$btnMore: $btnMore = el.querySelector('#more'),
|
||
|
|
||
|
toggle: () => $menu.collapsed = !$menu.collapsed,
|
||
|
install: (xrf) => {
|
||
|
this.xrf = xrf
|
||
|
$menu.bindToWindow() // bind functions like notify to window
|
||
|
window.notify('loading '+document.location.search.substr(1))
|
||
|
document.body.appendChild(el)
|
||
|
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 )
|
||
|
}
|
||
|
|
||
|
},{
|
||
|
|
||
|
get(data,k,v){ return data[k] },
|
||
|
set(data,k,v){
|
||
|
data[k] = v
|
||
|
switch( k ){
|
||
|
case "logo": $logo.style.backgroundImage = `url(${v})`; break;
|
||
|
case "css": document.head.innerHTML += v; break;
|
||
|
case "morelabel": $btnMore.innerText = data.morelabel; 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'
|
||
|
$buttons.style.display = data.collapsed ? 'block' : 'none'
|
||
|
break;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
renderButtons: (data) => `${data.buttons.join('')}`
|
||
|
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// reactify component!
|
||
|
$menu = document.createElement('div')
|
||
|
$menu.innerHTML = menuComponent.html
|
||
|
$menu = menuComponent.init($menu)
|
||
|
|
||
|
// attach menu functions which are less related to rendering
|
||
|
let utils = {
|
||
|
|
||
|
|
||
|
|
||
|
bindToWindow(opts){
|
||
|
|
||
|
window.notify = $menu.notify(window)
|
||
|
|
||
|
// reroute console messages to snackbar notifications
|
||
|
console.log = ( (log,console) => function(str){
|
||
|
if( String(str).match(/(:.*#|note:|:\/\/)/) ) window.notify( str )
|
||
|
log.call(console,str)
|
||
|
})(console.log, console)
|
||
|
|
||
|
// allow iframe to open url
|
||
|
window.addEventListener('message', (event) => {
|
||
|
if (event.data && event.data.url) {
|
||
|
window.open(event.data.url, '_blank');
|
||
|
}
|
||
|
});
|
||
|
},
|
||
|
|
||
|
loadFile(contentLoaders, multiple){
|
||
|
return () => {
|
||
|
window.notify("if you're on Meta browser, file-uploads might be disabled")
|
||
|
let input = document.createElement('input');
|
||
|
input.type = 'file';
|
||
|
input.multiple = multiple;
|
||
|
input.accept = Object.keys(contentLoaders).join(",");
|
||
|
input.onchange = () => {
|
||
|
let files = Array.from(input.files);
|
||
|
let file = files.slice ? files[0] : files
|
||
|
for( var i in contentLoaders ){
|
||
|
let r = new RegExp('\\'+i+'$')
|
||
|
if( file.name.match(r) ) return contentLoaders[i](file)
|
||
|
}
|
||
|
alert(file.name+" is not supported")
|
||
|
};
|
||
|
input.click();
|
||
|
}
|
||
|
},
|
||
|
|
||
|
SnackBar(userOptions) {
|
||
|
var snackbar = this || (window.snackbar = {});
|
||
|
var _Interval;
|
||
|
var _Message;
|
||
|
var _Element;
|
||
|
var _Container;
|
||
|
|
||
|
var _OptionDefaults = {
|
||
|
message: "Operation performed successfully.",
|
||
|
dismissible: true,
|
||
|
timeout: 7000,
|
||
|
status: ""
|
||
|
}
|
||
|
var _Options = _OptionDefaults;
|
||
|
|
||
|
function _Create() {
|
||
|
_Container = document.querySelector(".js-snackbar-container")
|
||
|
if( _Container ){
|
||
|
_Container.remove()
|
||
|
}
|
||
|
_Container = null
|
||
|
|
||
|
if (!_Container) {
|
||
|
// need to create a new container for notifications
|
||
|
_Container = document.createElement("div");
|
||
|
_Container.classList.add("js-snackbar-container");
|
||
|
|
||
|
document.body.appendChild(_Container);
|
||
|
}
|
||
|
_Container.opts = _Options
|
||
|
_Container.innerHTML = ''
|
||
|
_Element = document.createElement("div");
|
||
|
_Element.classList.add("js-snackbar__wrapper","xrf");
|
||
|
|
||
|
let innerSnack = document.createElement("div");
|
||
|
innerSnack.classList.add("js-snackbar", "js-snackbar--show");
|
||
|
|
||
|
if (_Options.status) {
|
||
|
_Options.status = _Options.status.toLowerCase().trim();
|
||
|
|
||
|
let status = document.createElement("span");
|
||
|
status.classList.add("js-snackbar__status");
|
||
|
|
||
|
|
||
|
if (_Options.status === "success" || _Options.status === "green") {
|
||
|
status.classList.add("js-snackbar--success");
|
||
|
}
|
||
|
else if (_Options.status === "warning" || _Options.status === "alert" || _Options.status === "orange") {
|
||
|
status.classList.add("js-snackbar--warning");
|
||
|
}
|
||
|
else if (_Options.status === "danger" || _Options.status === "error" || _Options.status === "red") {
|
||
|
status.classList.add("js-snackbar--danger");
|
||
|
}
|
||
|
else {
|
||
|
status.classList.add("js-snackbar--info");
|
||
|
}
|
||
|
|
||
|
innerSnack.appendChild(status);
|
||
|
}
|
||
|
|
||
|
_Message = document.createElement("span");
|
||
|
_Message.classList.add("js-snackbar__message");
|
||
|
if( typeof _Options.message == 'string' ){
|
||
|
_Message.innerHTML = _Options.message;
|
||
|
}else _Message.appendChild(_Options.message)
|
||
|
|
||
|
innerSnack.appendChild(_Message);
|
||
|
|
||
|
if (_Options.dismissible) {
|
||
|
let closeBtn = document.createElement("span");
|
||
|
closeBtn.classList.add("js-snackbar__close");
|
||
|
closeBtn.innerText = "\u00D7";
|
||
|
|
||
|
closeBtn.onclick = snackbar.Close;
|
||
|
|
||
|
innerSnack.appendChild(closeBtn);
|
||
|
}
|
||
|
|
||
|
_Element.style.height = "0px";
|
||
|
_Element.style.opacity = "0";
|
||
|
_Element.style.marginTop = "0px";
|
||
|
_Element.style.marginBottom = "0px";
|
||
|
|
||
|
_Element.appendChild(innerSnack);
|
||
|
_Container.appendChild(_Element);
|
||
|
|
||
|
if (_Options.timeout !== false) {
|
||
|
_Interval = setTimeout(snackbar.Close, _Options.timeout);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
snackbar.Open = function() {
|
||
|
let contentHeight = _Element.firstElementChild.scrollHeight; // get the height of the content
|
||
|
|
||
|
_Element.style.height = contentHeight + "px";
|
||
|
_Element.style.opacity = 1;
|
||
|
_Element.style.marginTop = "5px";
|
||
|
_Element.style.marginBottom = "5px";
|
||
|
|
||
|
_Element.addEventListener("transitioned", function() {
|
||
|
_Element.removeEventListener("transitioned", arguments.callee);
|
||
|
_Element.style.height = null;
|
||
|
})
|
||
|
}
|
||
|
|
||
|
snackbar.Close = function () {
|
||
|
if (_Interval)
|
||
|
clearInterval(_Interval);
|
||
|
|
||
|
let snackbarHeight = _Element.scrollHeight; // get the auto height as a px value
|
||
|
let snackbarTransitions = _Element.style.transition;
|
||
|
_Element.style.transition = "";
|
||
|
|
||
|
requestAnimationFrame(function() {
|
||
|
_Element.style.height = snackbarHeight + "px"; // set the auto height to the px height
|
||
|
_Element.style.opacity = 1;
|
||
|
_Element.style.marginTop = "0px";
|
||
|
_Element.style.marginBottom = "0px";
|
||
|
_Element.style.transition = snackbarTransitions
|
||
|
|
||
|
requestAnimationFrame(function() {
|
||
|
_Element.style.height = "0px";
|
||
|
_Element.style.opacity = 0;
|
||
|
})
|
||
|
});
|
||
|
|
||
|
setTimeout(function() {
|
||
|
try {
|
||
|
_Container.removeChild(_Element);
|
||
|
} catch (e) { }
|
||
|
}, 1000);
|
||
|
};
|
||
|
|
||
|
_Options = { ..._OptionDefaults, ...userOptions }
|
||
|
_Create();
|
||
|
snackbar.Open();
|
||
|
},
|
||
|
|
||
|
notify(scope){
|
||
|
return function notify(_str,opts){
|
||
|
if( accessibility.enabled ) return $chat.send({message:_str})
|
||
|
str = _str.replace(/(^\w+):/,"<div class='badge'>\$1</div>")
|
||
|
opts = opts || {status:'info'}
|
||
|
opts = Object.assign({ status, timeout:4000 },opts)
|
||
|
if( typeof str == 'string' ){
|
||
|
if( !opts.status ){
|
||
|
if( str.match(/error/g) ) opts.status = "danger"
|
||
|
if( str.match(/warning/g) ) opts.status = "warning"
|
||
|
}
|
||
|
}
|
||
|
opts.message = str
|
||
|
window.$menu.SnackBar( opts )
|
||
|
opts.message = _str
|
||
|
document.dispatchEvent( new CustomEvent("notify", {detail:opts}) )
|
||
|
}
|
||
|
},
|
||
|
|
||
|
download(){
|
||
|
function fetchAndDownload(dataurl, filename) {
|
||
|
var a = document.createElement("a");
|
||
|
a.href = dataurl;
|
||
|
a.setAttribute("download", filename);
|
||
|
a.click();
|
||
|
return false;
|
||
|
}
|
||
|
let file = document.location.search.replace(/\?/,'')
|
||
|
fetchAndDownload( file, file )
|
||
|
},
|
||
|
|
||
|
updateHashPosition(randomize){
|
||
|
// *TODO* this should be part of the XRF Threejs framework
|
||
|
if( typeof THREE == 'undefined' ) THREE = xrf.THREE
|
||
|
let radToDeg = THREE.MathUtils.radToDeg
|
||
|
let toDeg = (x) => x / (Math.PI / 180)
|
||
|
let camera = document.querySelector('[camera]').object3D.parent // *TODO* fix for threejs
|
||
|
camera.position.x += Math.random()/10
|
||
|
camera.position.z += Math.random()/10
|
||
|
|
||
|
// *TODO* add camera direction
|
||
|
let direction = new xrf.THREE.Vector3()
|
||
|
camera.getWorldDirection(direction)
|
||
|
const pitch = Math.asin(direction.y);
|
||
|
const yaw = Math.atan2(direction.x, direction.z);
|
||
|
const pitchInDegrees = pitch * 180 / Math.PI;
|
||
|
const yawInDegrees = yaw * 180 / Math.PI;
|
||
|
|
||
|
let lastPos = `pos=${camera.position.x.toFixed(2)},${camera.position.y.toFixed(2)},${camera.position.z.toFixed(2)}`
|
||
|
let newHash = document.location.hash.replace(/[&]?(pos|rot)=[0-9\.-]+,[0-9\.-]+,[0-9\.-]+/,'')
|
||
|
newHash += `&${lastPos}`
|
||
|
document.location.hash = newHash.replace(/&&/,'&')
|
||
|
.replace(/#&/,'')
|
||
|
$menu.copyToClipboard( window.location.href );
|
||
|
},
|
||
|
|
||
|
copyToClipboard(text){
|
||
|
// copy url to clipboard
|
||
|
var dummy = document.createElement('input')
|
||
|
document.body.appendChild(dummy);
|
||
|
dummy.value = text;
|
||
|
dummy.select();
|
||
|
document.execCommand('copy');
|
||
|
document.body.removeChild(dummy);
|
||
|
},
|
||
|
|
||
|
share(){
|
||
|
let inMeeting = $('[meeting]')
|
||
|
let url = window.location.href
|
||
|
if( !inMeeting ) $menu.updateHashPosition()
|
||
|
else url = $('[meeting]').components['meeting'].data.link
|
||
|
$menu.copyToClipboard( url )
|
||
|
// End of *TODO*
|
||
|
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>
|
||
|
<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>
|
||
|
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld">🖥 clone & selfhost this experience</a><br>
|
||
|
<br>
|
||
|
To embed this experience in your blog,<br>
|
||
|
copy/paste the following into your HTML:<br><input type="text" value="<iframe src='${document.location.href}'><br></iframe>" id="share"/>
|
||
|
<br>
|
||
|
`,{timeout:2000000})
|
||
|
// draw QR code
|
||
|
setTimeout( () => {
|
||
|
let QR = window.QR
|
||
|
QR.canvas = document.getElementById('qrcode')
|
||
|
QR.draw( url, QR.canvas )
|
||
|
},0)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// map to component
|
||
|
for( let i in utils ) $menu[i] = utils[i]
|
||
|
|
||
|
//$('a-scene').addEventListener('XRF', this.onXRFready )
|
||
|
//
|
||
|
// if( document.location.search.length > 2 ){
|
||
|
// $('[xrf]').setAttribute('xrf', document.location.search.substr(1)+document.location.hash )
|
||
|
// }
|
||
|
//
|
||
|
// },
|
||
|
//
|
||
|
// onXRFready: function(){
|
||
|
//
|
||
|
// let XRF = window.AFRAME.XRF
|
||
|
// //setupMenu(XRF){
|
||
|
// // let aScene = document.querySelector('a-scene')
|
||
|
// // let urlbar = $('input#uri')
|
||
|
// // let inIframe = window.location !== window.parent.location
|
||
|
// // let els = [ ...document.querySelectorAll('.menu .btn') ]
|
||
|
// // els = els.filter( (el) => el.id != "more" ? el : false )
|
||
|
//
|
||
|
// // // enable meetings
|
||
|
// // let startMeeting = () => {
|
||
|
// // aScene.setAttribute('meeting', 'id: xrfragments')
|
||
|
// // $('a#meeting').innerText = '🧑🤝🧑 breakout meeting'
|
||
|
// // $('a#meeting').setAttribute('aria-description','breakout room')
|
||
|
// // }
|
||
|
// // $('a#meeting').addEventListener('click', () => {
|
||
|
// // if( aScene.getAttribute('meeting') ){ // meeting already, start breakout room
|
||
|
// // let parentRoom = document.location.href
|
||
|
// // $menu.updateHashPosition(true)
|
||
|
// // let meeting = $('[meeting]').components['meeting']
|
||
|
// // meeting.data.parentRoom = parentRoom
|
||
|
// // meeting.update()
|
||
|
// // }else startMeeting()
|
||
|
// // })
|
||
|
// // if( document.location.hash.match(/(#|&)meet/) ) startMeeting()
|
||
|
//
|
||
|
// // XRF.addEventListener('hash', () => reflectUrl() )
|
||
|
// // const reflectUrl = window.reflectUrl = (url) => {
|
||
|
// // urlbar.value = url || document.location.search.substr(1) + document.location.hash
|
||
|
// // }
|
||
|
// // reflectUrl()
|
||
|
// //},
|
||
|
//
|
||
|
//
|
||
|
// // on localhost enable debugging mode for developer convenience
|
||
|
// let loc = document.location
|
||
|
// if( loc.host.match(/^localhost/) ){
|
||
|
// $('a-scene').setAttribute('stats')
|
||
|
// XRF.debug = 1
|
||
|
// }
|
||
|
//
|
||
|
// // 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 )
|
||
|
//
|
||
|
// window.AFRAME.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) )
|
||
|
// })
|
||
|
// $("#overlay > input[type=submit]").addEventListener("click", fileLoaders )
|
||
|
|
||
|
|
||
|
// finally add some css
|
||
|
$menu.css = `
|
||
|
<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-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: 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);
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
#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{
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
.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;
|
||
|
height: 31px;
|
||
|
border-radius: 5px;
|
||
|
background: var(--xrf-lighter-gray);
|
||
|
padding: 0px 16px;
|
||
|
}
|
||
|
|
||
|
.xrf table tr td {
|
||
|
vertical-align:middle;
|
||
|
text-align:right;
|
||
|
}
|
||
|
.xrf table tr td:nth-child(1){
|
||
|
min-width:95px;
|
||
|
padding-right:15px;
|
||
|
}
|
||
|
|
||
|
|
||
|
</style>
|
||
|
`
|
||
|
window.accessibility = (opts) => new Proxy({
|
||
|
opts,
|
||
|
|
||
|
enabled: false,
|
||
|
|
||
|
// features
|
||
|
speak_movements: true,
|
||
|
speak_keyboard: 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('$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")
|
||
|
btn.addEventListener('mouseover', (e) => {
|
||
|
let str = btn.getAttribute("aria-title") + btn.getAttribute('aria-description')
|
||
|
this.speak( str,true)
|
||
|
})
|
||
|
})
|
||
|
})
|
||
|
|
||
|
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>`])
|
||
|
|
||
|
})
|
||
|
// 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-title="connect button" aria-description="start text/audio/video chat" id="meeting" onclick="$connections.show()">🧑🤝🧑 connect</a><br>`])
|
||
|
.concat($menu.buttons)
|
||
|
})
|
||
|
/*! For license information please see matrix-crdt.js.LICENSE.txt */
|
||
|
!function(A,g){"object"==typeof exports&&"object"==typeof module?module.exports=g():"function"==typeof define&&define.amd?define([],g):"object"==typeof exports?exports.matrix=g():A.matrix=g()}(self,(()=>(()=>{var A={7746:(A,g,I)=>{A=I.nmd(A);let Q,B={};B.__wbindgen_placeholder__=A.exports;let C=new TextDecoder("utf-8",{ignoreBOM:!0,fatal:!0});C.decode();let E=null;function o(){return null!==E&&0!==E.byteLength||(E=new Uint8Array(Q.memory.buffer)),E}function n(A,g){return A>>>=0,C.decode(o().subarray(A,A+g))}const i=new Array(128).fill(void 0);i.push(void 0,null,!0,!1);let D=i.length;function a(A){D===i.length&&i.push(i.length+1);const g=D;return D=i[g],i[g]=A,g}function G(A){return i[A]}function c(A){const g=G(A);return function(A){A<132||(i[A]=D,D=A)}(A),g}function t(A){return null==A}let e=null;function s(){return null!==e&&0!==e.byteLength||(e=new Float64Array(Q.memory.buffer)),e}let N=null;function h(){return null!==N&&0!==N.byteLength||(N=new Int32Array(Q.memory.buffer)),N}let w=0,Y=new TextEncoder("utf-8");const y="function"==typeof Y.encodeInto?function(A,g){return Y.encodeInto(A,g)}:function(A,g){const I=Y.encode(A);return g.set(I),{read:A.length,written:I.length}};function M(A,g,I){if(void 0===I){const I=Y.encode(A),Q=g(I.length,1)>>>0;return o().subarray(Q,Q+I.length).set(I),w=I.length,Q}let Q=A.length,B=g(Q,1)>>>0;const C=o();let E=0;for(;E<Q;E++){const g=A.charCodeAt(E);if(g>127)break;C[B+E]=g}if(E!==Q){0!==E&&(A=A.slice(E)),B=I(B,Q,Q=E+3*A.length,1)>>>0;const g=o().subarray(B+E,B+Q);E+=y(A,g).written}return w=E,B}let k=null;function l(A){const g=typeof A;if("number"==g||"boolean"==g||null==A)return`${A}`;if("string"==g)return`"${A}"`;if("symbol"==g){const g=A.description;return null==g?"Symbol":`Symbol(${g})`}if("function"==g){const g=A.name;return"string"==typeof g&&g.length>0?`Function(${g})`:"Function"}if(Array.isArray(A)){const g=A.length;let I="[";g>0&&(I+=l(A[0]));for(let Q=1;Q<g;Q++)I+=", "+l(A[Q]);return I+="]",I}const I=/\[object ([^\]]+)\]/.exec(toString.call(A));let Q;if(!(I.length>1))return toString.call(A);if(Q=I[1],"Object"==Q)try{return"Object("+JSON.stringify(A)+")"}catch(A){return"Object"}return A instanceof Error?`${A.name}: ${A.message}\n${A.stack}`:Q}const R=new FinalizationRegistry((A=>{Q.__wbindgen_export_2.get(A.dtor)(A.a,A.b)}));function J(A,g,I,B){const C={a:A,b:g,cnt:1,dtor:I},E=(...A)=>{C.cnt++;const g=C.a;C.a=0;try{return B(g,C.b,...A)}finally{0==--C.cnt?(Q.__wbindgen_export_2.get(C.dtor)(g,C.b),R.unregister(C)):C.a=g}};return E.original=C,R.register(E,C,C),E}function F(A,g,I){try{const C=Q.__wbindgen_add_to_stack_pointer(-16);Q.wasm_bindgen__convert__closures__invoke1_mut__hbc098da5d2b7498e(C,A,g,a(I));var B=h()[C/4+0];if(h()[C/4+1])throw c(B)}finally{Q.__wbindgen_add_to_stack_pointer(16)}}function O(A,g,I,B){const C={a:A,b:g,cnt:1,dtor:I},E=(...A)=>{C.cnt++;try{return B(C.a,C.b,...A)}finally{0==--C.cnt&&(Q.__wbindgen_export_2.get(C.dtor)(C.a,C.b),C.a=0,R.unregister(C))}};return E.original=C,R.register(E,C,C),E}function Z(A,g){Q._dyn_core__ops__function__Fn_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hbff97dce5937efeb(A,g)}function m(A,g,I){Q._dyn_core__ops__function__Fn__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6d3bd38ffc42664a(A,g,a(I))}function d(A,g,I){Q._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h8cef6383e8d1f614(A,g,a(I))}function U(A,g){Q._dyn_core__ops__function__FnMut_____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd4892df90e815ea9(A,g)}function j(A,g){if(!(A instanceof g))throw new Error(`expected instance of ${g.name}`);return A.ptr}let p=128;function r(A){if(1==p)throw new Error("out of js stack");return i[--p]=A,p}function b(A,g){const I=g(1*A.length,1)>>>0;return o().set(A,I/1),w=A.length,I}function L(A,g){return A>>>=0,o().subarray(A/1,A/1+g)}function S(A,g){try{return A.apply(this,g)}catch(A){Q.__wbindgen_exn_store(a(A))}}A.exports.getVersions=function(){const A=Q.getVersions();return Kg.__wrap(A)},A.exports.star
|
||
|
|
||
|
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.querySelector('.badge').addEventListener('mouseover', () => {
|
||
|
window.notify(`${opts.name} is ${opts.description}.<br>You can basically make up a new 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()
|
||
|
})
|
||
|
|
||
|
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>
|
||
|
<button class="emoticon" id="randomize" aria-label="button" aria-title="randomize" onclick="$('#trystero #channelname').value = $connections.randomName()">🎲</button>
|
||
|
<a href="${opts.url}" target="_blank" class="badge nomargin">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.querySelector('#randomize').addEventListener('mouseover', () => window.notify("generate random channel name") )
|
||
|
el.querySelector('.badge').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.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
|
||
|
// })
|
||
|
//
|
||
|
// }
|
||
|
//
|
||
|
//
|
||
|
//}
|
||
|
const MAX_BUFFERED_AMOUNT=65536,ICECOMPLETE_TIMEOUT=5e3,CHANNEL_CLOSING_TIMEOUT=5e3;function randombytes(e){const t=new Uint8Array(e);for(let s=0;s<e;s++)t[s]=256*Math.random()|0;return t}function getBrowserRTC(){if("undefined"==typeof globalThis)return null;const e={RTCPeerConnection:globalThis.RTCPeerConnection||globalThis.mozRTCPeerConnection||globalThis.webkitRTCPeerConnection,RTCSessionDescription:globalThis.RTCSessionDescription||globalThis.mozRTCSessionDescription||globalThis.webkitRTCSessionDescription,RTCIceCandidate:globalThis.RTCIceCandidate||globalThis.mozRTCIceCandidate||globalThis.webkitRTCIceCandidate};return e.RTCPeerConnection?e:null}function errCode(e,t){return Object.defineProperty(e,"code",{value:t,enumerable:!0,configurable:!0}),e}function filterTrickle(e){return e.replace(/a=ice-options:trickle\s\n/g,"")}function warn(e){console.warn(e)}class Peer{constructor(e={}){if(this._map=new Map,this._id=randombytes(4).toString("hex").slice(0,7),this._doDebug=e.debug,this._debug("new peer %o",e),this.channelName=e.initiator?e.channelName||randombytes(20).toString("hex"):null,this.initiator=e.initiator||!1,this.channelConfig=e.channelConfig||Peer.channelConfig,this.channelNegotiated=this.channelConfig.negotiated,this.config=Object.assign({},Peer.config,e.config),this.offerOptions=e.offerOptions||{},this.answerOptions=e.answerOptions||{},this.sdpTransform=e.sdpTransform||(e=>e),this.streams=e.streams||(e.stream?[e.stream]:[]),this.trickle=void 0===e.trickle||e.trickle,this.allowHalfTrickle=void 0!==e.allowHalfTrickle&&e.allowHalfTrickle,this.iceCompleteTimeout=e.iceCompleteTimeout||5e3,this.destroyed=!1,this.destroying=!1,this._connected=!1,this.remoteAddress=void 0,this.remoteFamily=void 0,this.remotePort=void 0,this.localAddress=void 0,this.localFamily=void 0,this.localPort=void 0,this._wrtc=e.wrtc&&"object"==typeof e.wrtc?e.wrtc:getBrowserRTC(),!this._wrtc)throw"undefined"==typeof window?errCode(new Error("No WebRTC support: Specify `opts.wrtc` option in this environment"),"ERR_WEBRTC_SUPPORT"):errCode(new Error("No WebRTC support: Not a supported browser"),"ERR_WEBRTC_SUPPORT");this._pcReady=!1,this._channelReady=!1,this._iceComplete=!1,this._iceCompleteTimer=null,this._channel=null,this._pendingCandidates=[],this._isNegotiating=!1,this._firstNegotiation=!0,this._batchedNegotiation=!1,this._queuedNegotiation=!1,this._sendersAwaitingStable=[],this._senderMap=new Map,this._closingInterval=null,this._remoteTracks=[],this._remoteStreams=[],this._chunk=null,this._cb=null,this._interval=null;try{this._pc=new this._wrtc.RTCPeerConnection(this.config)}catch(e){return void this.destroy(errCode(e,"ERR_PC_CONSTRUCTOR"))}this._isReactNativeWebrtc="number"==typeof this._pc._peerConnectionId,this._pc.oniceconnectionstatechange=()=>{this._onIceStateChange()},this._pc.onicegatheringstatechange=()=>{this._onIceStateChange()},this._pc.onconnectionstatechange=()=>{this._onConnectionStateChange()},this._pc.onsignalingstatechange=()=>{this._onSignalingStateChange()},this._pc.onicecandidate=e=>{this._onIceCandidate(e)},"object"==typeof this._pc.peerIdentity&&this._pc.peerIdentity.catch((e=>{this.destroy(errCode(e,"ERR_PC_PEER_IDENTITY"))})),this.initiator||this.channelNegotiated?this._setupData({channel:this._pc.createDataChannel(this.channelName,this.channelConfig)}):this._pc.ondatachannel=e=>{this._setupData(e)},this.streams&&this.streams.forEach((e=>{this.addStream(e)})),this._pc.ontrack=e=>{this._onTrack(e)},this._debug("initial negotiation"),this._needsNegotiation()}get bufferSize(){return this._channel&&this._channel.bufferedAmount||0}get connected(){return this._connected&&"open"===this._channel.readyState}address(){return{port:this.localPort,family:this.localFamily,address:this.localAddress}}signal(e){if(!this.destroying){if(this.destroyed)throw errCode(new Error("cannot signal after peer is destroyed"),"ERR_DESTROYED");if("string"==typeof e)try{e=JSON.parse(e)}catch(t){e={}}this._debug("signal()"),e.renegotiate&&this.initiator&&(this._debug("got request to renegotiate"),this._needsNegotiation()),e.transceiverRequest&&t
|