work in progress [might break]

This commit is contained in:
Leon van Kammen 2024-01-03 14:23:34 +00:00
parent 2e01822029
commit 13f96e0506
26 changed files with 606383 additions and 4848 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

1671
dist/xrfragment.extras.js vendored Normal file

File diff suppressed because one or more lines are too long

598285
dist/xrfragment.module.js vendored

File diff suppressed because it is too large Load diff

2170
dist/xrfragment.plugin.frontend.js vendored Normal file

File diff suppressed because it is too large Load diff

126
dist/xrfragment.plugin.matrix.js vendored Normal file

File diff suppressed because one or more lines are too long

383
dist/xrfragment.plugin.p2p.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
/* /*
* v0.5.1 generated at Fri Dec 15 07:00:04 PM CET 2023 * v0.5.1 generated at Sat Dec 30 09:46:42 PM UTC 2023
* https://xrfragment.org * https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
@ -551,6 +551,15 @@ xrfragment_XRF.isDeep = new EReg("\\*","");
xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$",""); xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({}); })({});
var xrfragment = $hx_exports["xrfragment"]; var xrfragment = $hx_exports["xrfragment"];
// the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => {
let el = document.createElement('div')
el.innerHTML = html
return el.children[0]
}
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 Leon van Kammen/NLNET // Copyright (c) 2023 Leon van Kammen/NLNET
@ -602,713 +611,6 @@ xrf.hasTag = (tag,tags) => String(tags).match( new RegExp(`(^| )${tag}( |$)`,`g`
// map library functions to xrf // map library functions to xrf
for ( let i in xrfragment ) xrf[i] = xrfragment[i] for ( let i in xrfragment ) xrf[i] = xrfragment[i]
// handy shortcuts
if( !window.$ ) window.$ = (s) => s ? document.querySelector(s) : false
if( !window.$$ ) window.$$ = (s) => s ? [ ...document.querySelectorAll(s) ] : false
window.XRFMENU = {
logo: './../../assets/logo.png',
html: [
`<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>`
],
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();
}
},
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 )
let showMenu = (state) => {
els.map( (el) => el.style.display = state ? 'inline-block' : 'none' )
$('a#more').style.display = state ? 'none' : 'inline-block'
$('#overlay').style.display = state ? 'inline-block' : 'none'
if( inIframe ) $('#uri').style.display = 'block'
}
els.map( (el) => el.addEventListener('click', () => showMenu(false) ) )
$('a#more').addEventListener('click', () => showMenu(true) )
$('.a-canvas').addEventListener('click', () => showMenu(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
XRFMENU.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()
},
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){
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.XRFMENU.SnackBar( 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(/#&/,'')
XRFMENU.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 ) XRFMENU.updateHashPosition()
else url = $('[meeting]').components['meeting'].data.link
XRFMENU.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="window.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="&lt;iframe src='${document.location.href}'&gt;<br>&lt;/iframe&gt;" id="share"/>
<br>
`,{timeout:2000000})
// draw QR code
setTimeout( () => {
let QR = window.QR
QR.canvas = document.getElementById('qrcode')
QR.draw( url, QR.canvas )
},0)
}
}
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');
}
});
}
/* /*
* (promise-able) EVENTS (optionally continue after listeners are finished using .then) * (promise-able) EVENTS (optionally continue after listeners are finished using .then)
* *
@ -1360,7 +662,9 @@ xrf.emit.normal = function(eventName, data) {
var callbacks = xrf._listeners[eventName] var callbacks = xrf._listeners[eventName]
if (callbacks) { if (callbacks) {
for (var i = 0; i < callbacks.length; i++) { for (var i = 0; i < callbacks.length; i++) {
try{
callbacks[i](data); callbacks[i](data);
}catch(e){ console.error(e) }
} }
} }
}; };
@ -1562,6 +866,7 @@ xrf.init = ((init) => function(opts){
xrf.navigator.init() xrf.navigator.init()
// return xrfragment lib as 'xrf' query functor (like jquery) // return xrfragment lib as 'xrf' query functor (like jquery)
for ( let i in xrf ) xrf.query[i] = xrf[i] for ( let i in xrf ) xrf.query[i] = xrf[i]
return xrf.query return xrf.query
})(xrf.init) })(xrf.init)
@ -1643,12 +948,21 @@ xrf.reset = () => {
} }
xrf.parseUrl = (url) => { xrf.parseUrl = (url) => {
const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') ) let urlExHash = url.replace(/#.*/,'')
let urlObj,file
let store = {}
try{
urlObj = new URL( urlExHash.match(/:\/\//) ? urlExHash : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
let search = urlObj.search.substr(1).split("&")
for( let i in search ) store[ (search[i].split("=")[0]) ] = search[i].split("=")[1] || ''
}catch(e){ }
let hashmap = url.match("#") ? url.replace(/.*#/,'').split("&") : []
for( let i in hashmap ) store[ (hashmap[i].split("=")[0]) ] = hashmap[i].split("=")[1] || ''
let dir = url.substring(0, url.lastIndexOf('/') + 1) let dir = url.substring(0, url.lastIndexOf('/') + 1)
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
const hash = url.match(/#/) ? url.replace(/.*#/,'') : '' const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
const ext = file.split('.').pop() const ext = file.split('.').pop()
return {urlObj,dir,file,hash,ext} return {urlObj,dir,file,hash,ext,store}
} }
xrf.add = (object) => { xrf.add = (object) => {
@ -1672,7 +986,8 @@ xrf.navigator.to = (url,flags,loader,data) => {
return new Promise( (resolve,reject) => { return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments if( hash == document.location.hash.substr(1) ) return // block duplicate calls
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash) xrf.navigator.updateHash(hash)
return resolve(xrf.model) return resolve(xrf.model)
} }
@ -1735,7 +1050,6 @@ xrf.navigator.updateHash = (hash,opts) => {
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
console.log(`URL: ${document.location.search.substr(1)}#${hash}`) console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
document.location.hash = hash document.location.hash = hash
xrf.emit('hash', {...opts, hash: `#${hash}` })
} }
xrf.navigator.pushState = (file,hash) => { xrf.navigator.pushState = (file,hash) => {
@ -1808,7 +1122,9 @@ xrf.frag.href = function(v, opts){
//} //}
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
// always commit current location (keep a trail of last positions before we navigate) // *TODO* support for multiple protocols
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// always commit current location in case of teleport (keep a trail of last positions before we navigate)
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`) if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
xrf.navigator.to(v.string) // let's surf to HREF! xrf.navigator.to(v.string) // let's surf to HREF!
}) })
@ -1967,7 +1283,6 @@ xrf.frag.src.enableSourcePortation = (src) => {
xrf.frag.src.externalSRC = (url,frag,opts) => { xrf.frag.src.externalSRC = (url,frag,opts) => {
fetch(url, { method: 'HEAD' }) fetch(url, { method: 'HEAD' })
.then( (res) => { .then( (res) => {
console.log(`loading src ${url}`)
let mimetype = res.headers.get('Content-type') let mimetype = res.headers.get('Content-type')
if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf' if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf'
//if( url.match(/\.(fbx|stl|obj)$/) ) mimetype = //if( url.match(/\.(fbx|stl|obj)$/) ) mimetype =
@ -2510,9 +1825,6 @@ xrf.frag.defaultPredefinedViews = (opts) => {
}) })
} }
// react to enduser typing url
xrf.addEventListener('hash', (opts) => xrf.hashbus.pub( opts.hash ) )
// clicking href url with predefined view // clicking href url with predefined view
xrf.addEventListener('href', (opts) => { xrf.addEventListener('href', (opts) => {
if( !opts.click || opts.xrf.string[0] != '#' ) return if( !opts.click || opts.xrf.string[0] != '#' ) return
@ -2522,7 +1834,7 @@ xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts let {scene,match,v} = opts
let objname = v.fragment let objname = v.fragment
let autoscroll = v.z > 0 || v.w > 0 let autoscroll = v.z > 0 || v.w > 0
return // DISABLED
scene.traverse( (mesh) => { scene.traverse( (mesh) => {
if( mesh.name == objname ){ if( mesh.name == objname ){
if( !mesh.geometry ) return console.warn(`mesh '${objname}' has no uvcoordinates to offset`) if( !mesh.geometry ) return console.warn(`mesh '${objname}' has no uvcoordinates to offset`)

View file

@ -1,5 +1,5 @@
/* /*
* v0.5.1 generated at Fri Dec 15 07:00:04 PM CET 2023 * v0.5.1 generated at Sat Dec 30 09:46:42 PM UTC 2023
* https://xrfragment.org * https://xrfragment.org
* SPDX-License-Identifier: MPL-2.0 * SPDX-License-Identifier: MPL-2.0
*/ */
@ -551,6 +551,15 @@ xrfragment_XRF.isDeep = new EReg("\\*","");
xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$",""); xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({}); })({});
var xrfragment = $hx_exports["xrfragment"]; var xrfragment = $hx_exports["xrfragment"];
// the core project uses #vanillajs #proxies #clean #noframework
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
$el = (html,tag) => {
let el = document.createElement('div')
el.innerHTML = html
return el.children[0]
}
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 Leon van Kammen/NLNET // Copyright (c) 2023 Leon van Kammen/NLNET
@ -602,713 +611,6 @@ xrf.hasTag = (tag,tags) => String(tags).match( new RegExp(`(^| )${tag}( |$)`,`g`
// map library functions to xrf // map library functions to xrf
for ( let i in xrfragment ) xrf[i] = xrfragment[i] for ( let i in xrfragment ) xrf[i] = xrfragment[i]
// handy shortcuts
if( !window.$ ) window.$ = (s) => s ? document.querySelector(s) : false
if( !window.$$ ) window.$$ = (s) => s ? [ ...document.querySelectorAll(s) ] : false
window.XRFMENU = {
logo: './../../assets/logo.png',
html: [
`<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>`
],
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();
}
},
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 )
let showMenu = (state) => {
els.map( (el) => el.style.display = state ? 'inline-block' : 'none' )
$('a#more').style.display = state ? 'none' : 'inline-block'
$('#overlay').style.display = state ? 'inline-block' : 'none'
if( inIframe ) $('#uri').style.display = 'block'
}
els.map( (el) => el.addEventListener('click', () => showMenu(false) ) )
$('a#more').addEventListener('click', () => showMenu(true) )
$('.a-canvas').addEventListener('click', () => showMenu(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
XRFMENU.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()
},
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){
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.XRFMENU.SnackBar( 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(/#&/,'')
XRFMENU.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 ) XRFMENU.updateHashPosition()
else url = $('[meeting]').components['meeting'].data.link
XRFMENU.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="window.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="&lt;iframe src='${document.location.href}'&gt;<br>&lt;/iframe&gt;" id="share"/>
<br>
`,{timeout:2000000})
// draw QR code
setTimeout( () => {
let QR = window.QR
QR.canvas = document.getElementById('qrcode')
QR.draw( url, QR.canvas )
},0)
}
}
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');
}
});
}
/* /*
* (promise-able) EVENTS (optionally continue after listeners are finished using .then) * (promise-able) EVENTS (optionally continue after listeners are finished using .then)
* *
@ -1360,7 +662,9 @@ xrf.emit.normal = function(eventName, data) {
var callbacks = xrf._listeners[eventName] var callbacks = xrf._listeners[eventName]
if (callbacks) { if (callbacks) {
for (var i = 0; i < callbacks.length; i++) { for (var i = 0; i < callbacks.length; i++) {
try{
callbacks[i](data); callbacks[i](data);
}catch(e){ console.error(e) }
} }
} }
}; };
@ -1562,6 +866,7 @@ xrf.init = ((init) => function(opts){
xrf.navigator.init() xrf.navigator.init()
// return xrfragment lib as 'xrf' query functor (like jquery) // return xrfragment lib as 'xrf' query functor (like jquery)
for ( let i in xrf ) xrf.query[i] = xrf[i] for ( let i in xrf ) xrf.query[i] = xrf[i]
return xrf.query return xrf.query
})(xrf.init) })(xrf.init)
@ -1643,12 +948,21 @@ xrf.reset = () => {
} }
xrf.parseUrl = (url) => { xrf.parseUrl = (url) => {
const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') ) let urlExHash = url.replace(/#.*/,'')
let urlObj,file
let store = {}
try{
urlObj = new URL( urlExHash.match(/:\/\//) ? urlExHash : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
let search = urlObj.search.substr(1).split("&")
for( let i in search ) store[ (search[i].split("=")[0]) ] = search[i].split("=")[1] || ''
}catch(e){ }
let hashmap = url.match("#") ? url.replace(/.*#/,'').split("&") : []
for( let i in hashmap ) store[ (hashmap[i].split("=")[0]) ] = hashmap[i].split("=")[1] || ''
let dir = url.substring(0, url.lastIndexOf('/') + 1) let dir = url.substring(0, url.lastIndexOf('/') + 1)
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
const hash = url.match(/#/) ? url.replace(/.*#/,'') : '' const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
const ext = file.split('.').pop() const ext = file.split('.').pop()
return {urlObj,dir,file,hash,ext} return {urlObj,dir,file,hash,ext,store}
} }
xrf.add = (object) => { xrf.add = (object) => {
@ -1672,7 +986,8 @@ xrf.navigator.to = (url,flags,loader,data) => {
return new Promise( (resolve,reject) => { return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments if( hash == document.location.hash.substr(1) ) return // block duplicate calls
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash) xrf.navigator.updateHash(hash)
return resolve(xrf.model) return resolve(xrf.model)
} }
@ -1735,7 +1050,6 @@ xrf.navigator.updateHash = (hash,opts) => {
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
console.log(`URL: ${document.location.search.substr(1)}#${hash}`) console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
document.location.hash = hash document.location.hash = hash
xrf.emit('hash', {...opts, hash: `#${hash}` })
} }
xrf.navigator.pushState = (file,hash) => { xrf.navigator.pushState = (file,hash) => {
@ -1808,7 +1122,9 @@ xrf.frag.href = function(v, opts){
//} //}
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
// always commit current location (keep a trail of last positions before we navigate) // *TODO* support for multiple protocols
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// always commit current location in case of teleport (keep a trail of last positions before we navigate)
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`) if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
xrf.navigator.to(v.string) // let's surf to HREF! xrf.navigator.to(v.string) // let's surf to HREF!
}) })
@ -1967,7 +1283,6 @@ xrf.frag.src.enableSourcePortation = (src) => {
xrf.frag.src.externalSRC = (url,frag,opts) => { xrf.frag.src.externalSRC = (url,frag,opts) => {
fetch(url, { method: 'HEAD' }) fetch(url, { method: 'HEAD' })
.then( (res) => { .then( (res) => {
console.log(`loading src ${url}`)
let mimetype = res.headers.get('Content-type') let mimetype = res.headers.get('Content-type')
if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf' if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf'
//if( url.match(/\.(fbx|stl|obj)$/) ) mimetype = //if( url.match(/\.(fbx|stl|obj)$/) ) mimetype =
@ -2510,9 +1825,6 @@ xrf.frag.defaultPredefinedViews = (opts) => {
}) })
} }
// react to enduser typing url
xrf.addEventListener('hash', (opts) => xrf.hashbus.pub( opts.hash ) )
// clicking href url with predefined view // clicking href url with predefined view
xrf.addEventListener('href', (opts) => { xrf.addEventListener('href', (opts) => {
if( !opts.click || opts.xrf.string[0] != '#' ) return if( !opts.click || opts.xrf.string[0] != '#' ) return
@ -2522,7 +1834,7 @@ xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts let {scene,match,v} = opts
let objname = v.fragment let objname = v.fragment
let autoscroll = v.z > 0 || v.w > 0 let autoscroll = v.z > 0 || v.w > 0
return // DISABLED
scene.traverse( (mesh) => { scene.traverse( (mesh) => {
if( mesh.name == objname ){ if( mesh.name == objname ){
if( !mesh.geometry ) return console.warn(`mesh '${objname}' has no uvcoordinates to offset`) if( !mesh.geometry ) return console.warn(`mesh '${objname}' has no uvcoordinates to offset`)

9
make
View file

@ -62,6 +62,9 @@ build(){
} }
js(){ js(){
jscat(){ echo "(function(){"; cat "$@"; echo "}).apply({})"; }
# add js module # add js module
cat dist/xrfragment.js >> dist/xrfragment.module.js cat dist/xrfragment.js >> dist/xrfragment.module.js
echo "export default xrfragment;" >> dist/xrfragment.module.js echo "export default xrfragment;" >> dist/xrfragment.module.js
@ -86,9 +89,9 @@ build(){
example/assets/js/qr.js > dist/xrfragment.aframe.js example/assets/js/qr.js > dist/xrfragment.aframe.js
# plugins # plugins
cat src/3rd/js/plugin/frontend/*.js > dist/xrfragment.plugin.frontend.js jscat src/3rd/js/plugin/frontend/*.js > dist/xrfragment.plugin.frontend.js
cat src/3rd/js/plugin/matrix/{matrix-crdt,matrix}.js > dist/xrfragment.plugin.matrix.js jscat src/3rd/js/plugin/matrix/{matrix-crdt,matrix}.js > dist/xrfragment.plugin.matrix.js
cat src/3rd/js/plugin/p2p/{trystero-torrent.min,trystero}.js > dist/xrfragment.plugin.p2p.js jscat src/3rd/js/plugin/p2p/{trystero-torrent.min,trystero}.js > dist/xrfragment.plugin.p2p.js
# fat all-in-one standalone xrf release # fat all-in-one standalone xrf release
test -f dist/aframe.min.js || { test -f dist/aframe.min.js || {

View file

@ -8,7 +8,7 @@
haxe haxe
mmark mmark
xml2rfc xml2rfc
dendrite esbuild
]; ];

View file

@ -14,12 +14,15 @@ chatComponent = {
init: (el) => new Proxy({ init: (el) => new Proxy({
scene: null, scene: null,
visible: true, visible: true,
messages: [], visibleChatbar: false,
messages: [],
$messages: $messages = el.querySelector("#messages"), $videos: el.querySelector("#videos"),
$chatline: $chatline = el.querySelector("#chatline"), $messages: el.querySelector("#messages"),
$chatline: el.querySelector("#chatline"),
$chatbar: el.querySelector("#chatbar"),
install(opts){ install(opts){
this.opts = opts this.opts = opts
@ -27,15 +30,18 @@ chatComponent = {
el.className = "xrf" el.className = "xrf"
el.style.display = 'none' // start hidden el.style.display = 'none' // start hidden
document.body.appendChild( el ) document.body.appendChild( el )
this.visibleChatbar = false
document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) ) 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"] }) this.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br>`, class: ["info","guide","multiline"] })
}, },
initListeners(){ initListeners(){
//opts.scene.addEventListener('meeting.peer.add', () => console.log("$chat.peer.add") ) let {$chatline} = this
//opts.scene.addEventListener('meeting.peer.remove', () => console.log("$chat.peer.remove") )
$chatline.addEventListener('keydown', (e) => { $chatline.addEventListener('keydown', (e) => {
if (e.key == 'Enter' ){ if (e.key == 'Enter' ){
if( $chatline.value[0] != '/' ){
document.dispatchEvent( new CustomEvent("network.send", {detail: {message:$chatline.value}} ) )
}
this.send({message: $chatline.value }) this.send({message: $chatline.value })
$chatline.value = '' $chatline.value = ''
} }
@ -49,10 +55,13 @@ chatComponent = {
}, },
send(opts){ send(opts){
let {$messages} = this
opts = { linebreak:true, message:"", class:[], ...opts } opts = { linebreak:true, message:"", class:[], ...opts }
let div = document.createElement('div') if( window.frontend && window.frontend.emit ) window.frontend.emit('$chat.send', opts )
let msg = document.createElement('div') let div = document.createElement('div')
let br = document.createElement('br') let msg = document.createElement('div')
let br = document.createElement('br')
let nick = document.createElement('div')
msg.className = "msg" msg.className = "msg"
let html = `${ opts.message || ''}${ opts.html ? opts.html(opts) : ''}` let html = `${ opts.message || ''}${ opts.html ? opts.html(opts) : ''}`
if( $messages.last == html ) return if( $messages.last == html ) return
@ -62,27 +71,47 @@ chatComponent = {
if( opts.class ){ if( opts.class ){
msg.classList.add.apply(msg.classList, opts.class) msg.classList.add.apply(msg.classList, opts.class)
br.classList.add.apply(br.classList, opts.class) br.classList.add.apply(br.classList, opts.class)
div.classList.add.apply(div.classList, opts.class.concat(["envelope"]))
}
if( !opts.from && !msg.className.match(/(info|guide)/) ) msg.classList.add('self')
if( opts.from ){
nick.className = "user"
nick.innerText = opts.from+' '
div.appendChild(nick)
if( opts.pos ){
let a = document.createElement("a")
a.href = a.innerText = `#pos=${opts.pos}`
nick.appendChild(a)
}
} }
div.appendChild(msg) div.appendChild(msg)
$messages.appendChild(div) $messages.appendChild(div)
if( opts.linebreak ) div.appendChild(br) if( opts.linebreak ) div.appendChild(br)
$messages.scrollTop = $messages.scrollHeight // scroll down $messages.scrollTop = $messages.scrollHeight // scroll down
document.dispatchEvent( new CustomEvent("$chat:receive", {detail: opts}) )
$messages.last = msg.innerHTML $messages.last = msg.innerHTML
},
getChatLog(){
return ([...this.$messages.querySelectorAll('.envelope')])
.filter( (d) => !d.className.match(/(info|ui)/) )
.map( (d) => d.innerHTML )
.join('\n')
} }
},{ },{
get(data,k,v){ return data[k] }, get(me,k,v){ return me[k] },
set(data,k,v){ set(me,k,v){
data[k] = v me[k] = v
switch( k ){ switch( k ){
case "visible": { case "visible": {
el.style.display = data.visible ? 'block' : 'none' el.style.display = me.visible ? 'block' : 'none'
if( !el.inited && (el.inited = true) ) data.initListeners() if( !el.inited && (el.inited = true) ) me.initListeners()
$menu.collapsed = !data.visible break;
break; }
} case "visibleChatbar": {
me.$chatbar.style.display = v ? 'block' : 'none'
}
} }
} }
@ -93,7 +122,7 @@ chatComponent = {
document.addEventListener('$menu:ready', (opts) => { document.addEventListener('$menu:ready', (opts) => {
opts = opts.detail opts = opts.detail
document.head.innerHTML += chatComponent.css document.head.innerHTML += chatComponent.css
$chat = document.createElement('div') window.$chat = document.createElement('div')
$chat.innerHTML = chatComponent.html $chat.innerHTML = chatComponent.html
$chat = chatComponent.init($chat) $chat = chatComponent.init($chat)
$chat.install(opts) $chat.install(opts)
@ -167,29 +196,78 @@ chatComponent.css = `
} }
#messages{ #messages{
position: absolute; position: absolute;
top: 100px; transition:1s;
top: 0px;
left: 0; left: 0;
right: 0; bottom: 130px;
bottom: 88px;
padding: 15px; padding: 15px;
pointer-events: none; overflow:hidden;
overflow-y:auto; pointer-events:none;
transition:1s;
width: 91%;
max-width: 500px;
z-index: 100;
-webkit-user-select:none;
-moz-user-select:-moz-none;
-ms-user-select:none;
user-select:none;
}
body.menu #messages{
top:50px;
}
#messages *{
pointer-events:all;
} }
#messages .msg{ #messages .msg{
transition:all 1s ease;
background: #fff; background: #fff;
display: inline-block; display: inline-block;
padding: 6px 17px; padding: 1px 17px;
border-radius: 20px; border-radius: 20px 0px 20px 20px;
color: #000c; color: #000c;
margin-bottom: 10px; margin-bottom: 10px;
line-height:23px; line-height:23px;
pointer-events:visible; pointer-events:visible;
border: 1px solid #ccc8;
line-height:33px; line-height:33px;
cursor:grabbing;
border: 1px solid #0002;
}
#messages .msg.self{
border-radius: 0px 20px 20px 20px;
background:var(--xrf-primary);
}
#messages .msg.self,
#messages .msg.self div{
color:#FFF;
} }
#messages .msg.info{ #messages .msg.info{
font-size: 14px; background: #473f7f;
padding: 3px 16px; border-radius: 20px;
color: #FFF;
text-align: right;
line-height: 19px;
}
#messages .msg.info,
#messages .msg.info *{
font-size: var(--xrf-font-size-0);
}
#messages .msg a {
text-decoration:underline;
color: #EEE;
font-weight:bold;
transition:1s;
}
#messages .msg a:hover{
color:#FFF;
}
#messages .msg.ui,
#messages .msg.ui div{
background: white;
border:none;
color: #333;
border-radius: 20px;
margin:0;
padding:0px 5px 5px 5px;
} }
#messages.guide, .guide{ #messages.guide, .guide{
display:unset; display:unset;
@ -200,12 +278,6 @@ chatComponent.css = `
br.guide{ br.guide{
display:inline-block; 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 .msg.info a:hover,
#messages button:hover{ #messages button:hover{
filter: brightness(1.4); filter: brightness(1.4);
@ -213,9 +285,6 @@ chatComponent.css = `
#messages .msg.multiline { #messages .msg.multiline {
padding: 2px 14px; padding: 2px 14px;
} }
#messages .msg.config {
background:#
}
#messages button { #messages button {
text-decoration:none; text-decoration:none;
margin: 0px 15px 10px 0px; margin: 0px 15px 10px 0px;
@ -256,4 +325,19 @@ background:#
.nomargin{ .nomargin{
margin:0; margin:0;
} }
.envelope{
height:unset;
overflow:hidden;
transition:1s;
}
.user{
margin-left:13px;
font-weight: bold;
color: var(--xrf-dark-gray);
}
.user, .user *{
font-size: var(--xrf-font-size-0);
}
</style>` </style>`

View file

@ -2,31 +2,62 @@ connectionsComponent = {
html: ` html: `
<div id="connections"> <div id="connections">
<h2>Connection layers:</h2> <i class="gg-close-o" id="close" onclick="$connections.toggle()"></i>
<div id="networking">
<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>World sync</td>
<td>
<select id="scene"></select>
</td>
</tr>
</table>
</div>
<div id="devices">
<a class="badge ruler">Webcam</a>
<table>
<tr>
<td>Video</td>
<td>
<select id="videoInput"></select>
</td>
</tr>
<tr>
<td>Mic</td>
<td>
<select id="audioInput"></select>
</td>
</tr>
<tr style="display:none"> <!-- not used (for now) -->
<td>Audio</td>
<td>
<select id="audioOutput"></select>
</td>
</tr>
</table>
</div>
<div id="settings"></div>
<table> <table>
<tr> <tr>
<td>Webcam/Audio</td> <td></td>
<td> <td>
<select id="webcam"></select> <button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
</td>
</tr>
<tr>
<td>Chat</td>
<td>
<select id="chatnetwork"></select>
</td>
</tr>
<tr>
<td>Scene</td>
<td>
<select id="scene"></select>
</td> </td>
</tr> </tr>
</table> </table>
<div id="settings"></div>
<br>
<button id="connect" onclick="$connections.connect()">📡 Connect!</button>
<br><br>
</div> </div>
`, `,
@ -44,40 +75,54 @@ connectionsComponent = {
$chatnetwork: $chatnetwork = el.querySelector("#chatnetwork"), $chatnetwork: $chatnetwork = el.querySelector("#chatnetwork"),
$scene: $scene = el.querySelector("#scene"), $scene: $scene = el.querySelector("#scene"),
$settings: $settings = el.querySelector("#settings"), $settings: $settings = el.querySelector("#settings"),
$connect: $connect = el.querySelector("#connect"), $devices: $devices = el.querySelector("#devices"),
$connect: $connect = el.querySelector("#connect"),
$networking: $networking = el.querySelector("#networking"),
$audioInput: el.querySelector('select#audioInput'),
$audioOutput: el.querySelector('select#audioOutput'),
$videoInput: el.querySelector('select#videoInput'),
install(opts){ install(opts){
this.opts = opts this.opts = opts;
document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ) (['change']).map( (e) => el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
this.reactToNetwork()
$menu.buttons = ([
`<a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick="$connections.show()"><i class="gg-user-add"></i>&nbsp;connect</a><br>`
]).concat($menu.buttons)
$webcam.addEventListener('change', () => this.renderSettings() ) // hide networking settings if entering thru meetinglink
$chatnetwork.addEventListener('change', () => this.renderSettings() ) if( document.location.href.match(/meet=/) ) this.show()
$scene.addEventListener('change', () => this.renderSettings() )
setTimeout( () => document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ), 1 )
},
toggle(){
let parent = el.closest('.envelope')
parent.style.display = parent.style.display == 'none' ? parent.style.display = '' : 'none'
},
change(id,e){
if( id.match(/^(webcam|chatnetwork|scene)$/) ){
this.renderSettings() // trigger this when 'change' event fires on children dom elements
}
}, },
show(){ show(){
$chat.visible = true $chat.visible = true
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
if( !network.connected ){ if( !network.connected ){
if( el.parentElement ) el.parentElement.parentElement.remove() if( el.parentElement ) el.parentElement.parentElement.remove()
$chat.send({message:"", el}) $chat.send({message:"", el, class:['ui']})
this.renderSettings()
if( !network.meetinglink ){ // set default if( !network.meetinglink ){ // set default
$webcam.value = 'Peer2Peer' $webcam.value = 'Peer2Peer'
$chatnetwork.value = 'Peer2Peer' $chatnetwork.value = 'Peer2Peer'
$scene.value = 'Peer2Peer' $scene.value = 'Peer2Peer'
} }
}else $chat.send({message:"you are already connected, refresh page to create new connection",class:['info']}) this.renderSettings()
}, }else{
$chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
connect(){ }
this.update()
this.webcam.selected.connect({webcam:true})
this.chatnetwork.selected.connect({chat:true})
this.scene.selected.connect({scene:true})
this.$connect.setAttribute('disabled','disabled')
this.$connect.classList.add('disabled')
window.notify("🪐 connecting to awesomeness..")
}, },
update(){ update(){
@ -102,13 +147,84 @@ connectionsComponent = {
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value } let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
this.update() this.update()
$settings.innerHTML = '' $settings.innerHTML = ''
this.forSelectedPluginsDo( (plugin) => { this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config(opts) ) )
console.log("configuring "+plugin.plugin.name) this.renderInputs()
console.dir(plugin) },
$settings.appendChild( plugin.config(opts) )
renderInputs(){
if( this.selectedWebcam == 'No thanks' ){
return this.$devices.style.display = 'none'
}else this.$devices.style.display = ''
navigator.mediaDevices.getUserMedia({
audio: true,
video: true
})
.then( () => {
const selectors = [this.$audioInput, this.$audioOutput, this.$videoInput];
const gotDevices = (deviceInfos) => {
// Handles being called several times to update labels. Preserve values.
const values = selectors.map(select => select.value);
selectors.forEach(select => {
while (select.firstChild) {
select.removeChild(select.firstChild);
}
});
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === 'audioinput') {
option.text = deviceInfo.label || `microphone ${this.$audioInput.length + 1}`;
this.$audioInput.appendChild(option);
} else if (deviceInfo.kind === 'audiooutput') {
option.text = deviceInfo.label || `speaker ${this.$audioOutput.length + 1}`;
this.$audioOutput.appendChild(option);
} else if (deviceInfo.kind === 'videoinput') {
option.text = deviceInfo.label || `camera this.${this.$videoInput.length + 1}`;
this.$videoInput.appendChild(option);
} else {
console.log('Some other kind of source/device: ', deviceInfo);
}
}
// hide if there's nothing to choose
let totalDevices = this.$audioInput.options.length + this.$audioOutput.options.length + this.$videoInput.options.length
this.$devices.style.display = totalDevices > 3 ? 'block' : 'none'
selectors.forEach((select, selectorIndex) => {
if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
select.value = values[selectorIndex];
}
});
}
// after getUserMedia we can enumerate
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(console.warn);
}) })
}, },
reactToNetwork(){
document.addEventListener('network.connected', () => {
console.log("network.connected")
window.notify("🪐 connected to awesomeness..")
$chat.visibleChatbar = true
$chat.send({message:`🎉 connected!`,class:['info']})
})
document.addEventListener('network.connect', () => {
console.log("network.connect")
el.parentElement.classList.add('connecthide')
window.notify("🪐 connecting to awesomeness..")
$connect.innerText = 'connecting..'
})
document.addEventListener('network.disconnect', () => {
window.notify("🪐 disconnecting..")
$connect.innerText = 'disconnecting..'
setTimeout( () => $connect.innerText = 'connect', 1000)
if( !window.accessibility.enabled ) $chat.visibleChatbar = false
})
}
},{ },{
get(data,k,v){ return data[k] }, get(data,k,v){ return data[k] },
@ -119,8 +235,13 @@ connectionsComponent = {
case "chatnetwork": $chatnetwork.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; case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
case "selectedScene": $scene.value = v; data.renderSettings(); break; case "selectedScene": $scene.value = v; data.renderSettings(); break;
case "selectedWebcam": $webcam.value = v; data.renderSettings(); break;
case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break; case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break;
case "selectedWebcam": {
$webcam.value = v;
data.renderSettings();
$devices.style.display = v ? 'block' : 'none'
break;
}
} }
} }
@ -129,10 +250,10 @@ connectionsComponent = {
} }
// reactify component! // reactify component!
document.addEventListener('network:ready', (opts) => { document.addEventListener('$menu:ready', (opts) => {
opts = opts.detail opts = opts.detail
document.head.innerHTML += connectionsComponent.css document.head.innerHTML += connectionsComponent.css
$connections = document.createElement('div') window.$connections = document.createElement('div')
$connections.innerHTML = connectionsComponent.html $connections.innerHTML = connectionsComponent.html
$connections = connectionsComponent.init($connections) $connections = connectionsComponent.init($connections)
$connections.install(opts) $connections.install(opts)
@ -143,8 +264,21 @@ document.addEventListener('network:ready', (opts) => {
connectionsComponent.css = ` connectionsComponent.css = `
<style type="text/css"> <style type="text/css">
button#connect{ button#connect{
float: right;
height: 43px; height: 43px;
width:100%;
margin: 0px; margin: 0px;
} }
#messages .msg #connections{
position:relative;
}
.connecthide {
transform:translateY(-1000px);
}
#close{
display: block;
margin-top: 16px;
position: relative;
float: right;
margin-bottom: 7px;
}
</style>` </style>`

View file

@ -1,872 +1,66 @@
// reactive component for displaying the menu // reactive component for displaying the menu
menuComponent = { menuComponent = (el) => new Proxy({
html: ` html: `
<div id="overlay" class="xrf">
<div class="logo" ></div>
<button id="navback" onclick="history.back()">&lt;</button>
<button id="navforward" onclick="history.forward()">&gt;</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="xrf footer">
<div class="menu"> <div class="menu">
<div id="buttons"></div> <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> <a class="btn" id="more" aria-title="menu button"><i id="icon" class="gg-menu"></i></a><br>
</div> </div>
</div> </div>
`, `,
init: (el) => new Proxy({ collapsed: false,
morelabel: '⚡', logo: './../../assets/logo.png',
collapsed: false, buttons: [`<a class="btn" aria-label="button" aria-title="share button" aria-description="this allows embedding and sharing of this URL or make a screenshot of it" id="share" onclick="frontend.share()"><i class="gg-link"></i>&nbsp;share</a><br>`],
logo: './../../assets/logo.png', $buttons: $buttons = el.querySelector('#buttons'),
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>`], $btnMore: $btnMore = el.querySelector('#more'),
$overlay: $overlay = el.querySelector('#overlay'), toggle(){
$logo: $logo = el.querySelector('.logo'), this.collapsed = !this.collapsed
$uri: $uri = el.querySelector('#uri'), el.querySelector("i#icon").className = this.collapsed ? 'gg-close' : 'gg-menu'
$buttons: $buttons = el.querySelector('#buttons'), document.body.classList[ this.collapsed ? 'add' : 'remove' ](['menu'])
$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) { init(opts){
var snackbar = this || (window.snackbar = {}); el.innerHTML = this.html
var _Interval; document.body.appendChild(el);
var _Message; (['click']).map( (e) => el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
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="&lt;iframe src='${document.location.href}'&gt;<br>&lt;/iframe&gt;" id="share"/>
<br>
`,{timeout:2000000})
// draw QR code
setTimeout( () => { setTimeout( () => {
let QR = window.QR document.dispatchEvent( new CustomEvent("$menu:ready", {detail: {$menu:this,xrf}}) )
QR.canvas = document.getElementById('qrcode') },100)
QR.draw( url, QR.canvas ) return this
},0) },
// mobile share
if( typeof navigator.share != 'undefined'){ click(id,e){
navigator.share({ switch(id){
url, case "icon":
title: 'your meeting link' case "more": this.toggle(); break;
})
} }
} }
} },
{
// map to component get(me,k,v){ return me[k] },
for( let i in utils ) $menu[i] = utils[i]
//$('a-scene').addEventListener('XRF', this.onXRFready ) set(me,k,v){
// me[k] = v
// if( document.location.search.length > 2 ){ switch( k ){
// $('[xrf]').setAttribute('xrf', document.location.search.substr(1)+document.location.hash ) case "buttons": el.querySelector("#buttons").innerHTML = this.renderButtons(me);
// } document.dispatchEvent( new CustomEvent("$menu:buttons:render", {detail: el.querySelector('.menu') }) )
// break;
// }, case "collapsed":
// el.querySelector("#buttons").style.display = me.collapsed ? 'block' : 'none'
// onXRFready: function(){ frontend.emit('$menu:collapse', v)
// break;
// 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: 11px;
--xrf-font-size-1: 14px;
--xrf-font-size-2: 17px;
--xrf-font-size-3: 21px;
} }
},
/* CSS reset */ renderButtons: (data) => `${data.buttons.join('')}`
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, // reactify component!
.xrf input[type="submit"]:hover, document.addEventListener('frontend:ready', (e) => {
.xrf .btn:hover { window.$menu = menuComponent( document.createElement('div') ).init(e.detail)
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
}
.ruler{
width:97%;
margin:7px 0px;
}
a.badge {
text-decoration:none;
}
.xrf select{
min-width: 200px;
border-inline: none;
border-inline: none;
border-block: none;
border: 3px solid var(--xrf-primary);
border-radius: 5px;
background: none;
border-radius:30px;
}
.xrf select,
.xrf option{
padding: 0px 16px;
min-width: 200px;
height: 35px;
}
.xrf input{
border-radius:30px;
padding: 7px 15px;
border-block: none;
border-inline: none;
border: 1px solid #888;
background: transparent;
height: 18px;
max-width:168px;
}
.xrf table tr td {
vertical-align:middle;
text-align:right;
}
.xrf table tr td:nth-child(1){
min-width:70px;
height:40px;
padding-right:15px;
}
.xrf small{
font-size: var(--xrf-font-size-0);
}
.disabled{
opacity:0.5
}
</style>
`

View file

@ -17,18 +17,20 @@ window.accessibility = (opts) => new Proxy({
settings(){ settings(){
this.toggle() // *TODO* should show settings screen this.toggle() // *TODO* should show settings screen
if( this.enabled ) window.notify(`accessibility boosted, click <a href="#">here</a> to tweak settings`)
}, },
speak(str, override){ speak(str, opts){
opts = opts || {speaksigns:true}
if( !this.enabled || !str) return if( !this.enabled || !str) return
str = str.replace(/\/\//,' ') if( opts.speaksigns ){
.replace(/:/,'') str = str.replace(/\/\//,' ')
.replace(/\//,' slash ') .replace(/:/,'')
.replace(/\./,' dot ') .replace(/\//,' slash ')
.replace(/#/,' hash ') .replace(/\./,' dot ')
.replace(/&/,' and ') .replace(/#/,' hash ')
.replace(/=/,' is ') .replace(/&/,' and ')
.replace(/=/,' is ')
}
let speech = window.speechSynthesis let speech = window.speechSynthesis
let utterance = new SpeechSynthesisUtterance( str ) let utterance = new SpeechSynthesisUtterance( str )
if( this.speak_voice != -1) utterance.voice = speech.getVoices()[ this.speak_voice ]; if( this.speak_voice != -1) utterance.voice = speech.getVoices()[ this.speak_voice ];
@ -41,7 +43,7 @@ window.accessibility = (opts) => new Proxy({
utterance.rate = this.speak_rate utterance.rate = this.speak_rate
utterance.pitch = this.speak_pitch utterance.pitch = this.speak_pitch
utterance.volume = this.speak_volume utterance.volume = this.speak_volume
if( override ) speech.cancel() if( opts.override ) speech.cancel()
speech.speak(utterance) speech.speak(utterance)
}, },
@ -56,7 +58,7 @@ window.accessibility = (opts) => new Proxy({
case "ArrowRight": k = "right"; break; case "ArrowRight": k = "right"; break;
case "ArrowDown": k = "backward"; break; case "ArrowDown": k = "backward"; break;
} }
this.speak(k,true) this.speak(k,{override:true})
}) })
document.addEventListener('$menu:buttons:render', (e) => { document.addEventListener('$menu:buttons:render', (e) => {
@ -66,38 +68,51 @@ window.accessibility = (opts) => new Proxy({
a.map( (btn) => { a.map( (btn) => {
if( !btn.href ) btn.setAttribute("href","javascript:void(0)") // important! if( !btn.href ) btn.setAttribute("href","javascript:void(0)") // important!
btn.setAttribute("aria-label","button") btn.setAttribute("aria-label","button")
btn.addEventListener('mouseover', (e) => { })
let str = btn.getAttribute("aria-title") + btn.getAttribute('aria-description') document.addEventListener('mouseover', (e) => {
this.speak( str,true) if( e.target.getAttribute("aria-title") ){
}) let lines = []
lines.push( e.target.getAttribute("aria-title") )
lines.push( e.target.getAttribute("aria-description") )
lines = lines.filter( (l) => l )
this.speak( lines.join("."), {override:true,speaksigns:false} )
}
}) })
}) })
document.addEventListener('$chat:receive', (e) => { document.addEventListener('network.send', (e) => {
let opts = e.detail let opts = e.detail
opts.message = opts.message || '' opts.message = opts.message || ''
if( opts.class && ~opts.class.indexOf('info') ) opts.message = `info: ${opts.message}` if( opts.class && ~opts.class.indexOf('info') ) opts.message = `info: ${opts.message}`
this.speak(e.detail.message) this.speak(opts.message)
}) })
opts.addEventListener('pos', (opts) => { opts.xrf.addEventListener('pos', (opts) => {
let obj if( this.enabled ){
let description $chat.send({message: this.posToMessage(opts) })
let msg = "You've teleported to " }
let pos = opts.frag.pos network.send({message: this.posToMessage(opts), class:["info","guide"]})
if( pos.string.match(',') ) msg += `coordinates <a href="#pos=${pos.string}">${pos.string}</a>` network.pos = opts.frag.pos.string
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"]})
}) })
}, },
posToMessage(opts){
let obj
let description
let msg = "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 the teleportation was refused because it cannot be found within this world"
}
return msg
},
sanitizeTranscript(){ sanitizeTranscript(){
return $chat.$messages.innerText return $chat.$messages.innerText
.replaceAll("<[^>]*>", "") // strip html .replaceAll("<[^>]*>", "") // strip html
@ -114,7 +129,7 @@ window.accessibility = (opts) => new Proxy({
data[k] = v data[k] = v
switch( k ){ switch( k ){
case "enabled": { case "enabled": {
let message = (v?"boosting":"unboosting") + " accessibility features" let message = "accessibility has been"+(v?"boosted":"lowered")
$('#accessibility.btn').style.filter= v ? 'brightness(1.0)' : 'brightness(0.5)' $('#accessibility.btn').style.filter= v ? 'brightness(1.0)' : 'brightness(0.5)'
if( v ) $chat.visible = true if( v ) $chat.visible = true
$chat.send({message,class:['info','guide']}) $chat.send({message,class:['info','guide']})
@ -131,9 +146,10 @@ window.accessibility = (opts) => new Proxy({
}) })
document.addEventListener('$menu:ready', (e) => { document.addEventListener('$menu:ready', (e) => {
window.accessibility = accessibility(e.detail) try{
accessibility.init() accessibility = accessibility(e.detail)
document.dispatchEvent( new CustomEvent("accessibility:ready", e ) ) accessibility.init()
$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>`]) 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()"><i class="gg-yinyang"></i>accessibility</a><br>`])
}catch(e){console.error(e)}
}) })

View file

@ -0,0 +1,753 @@
document.head.innerHTML += `
<style type="text/css">
:root {
--xrf-primary: #6839dc;
--xrf-primary-fg: #FFF;
--xrf-light-primary: #ea23cf;
--xrf-secondary: #872eff;
--xrf-light-xrf-secondary: #ce7df2;
--xrf-topbar-bg: #fffb;
--xrf-box-shadow: #0005;
--xrf-red: red;
--xrf-dark-gray: #343334;
--xrf-gray: #424280;
--xrf-white: #fdfdfd;
--xrf-light-gray: #efefef;
--xrf-lighter-gray: #e4e2fb96;
--xrf-font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
--xrf-font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
--xrf-font-size-0: 12px;
--xrf-font-size-1: 14px;
--xrf-font-size-2: 17px;
--xrf-font-size-3: 21px;
}
/* CSS reset */
html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:0.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type="button"],[type="reset"],[type="submit"],button{-webkit-appearance:button}[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}
.xrf table tr td{
vertical-align:top;
}
.xrf button,
.xrf input[type="submit"],
.xrf .btn {
text-decoration:none;
background: var(--xrf-primary);
border: 0;
border-radius: 25px;
padding: 11px 15px;
font-weight: bold;
transition: 0.3s;
height: 40px;
font-size: var(--xrf-font-size-1);
color: var(--xrf-primary-fg);
line-height: var(--xrf-font-size-1);
cursor:pointer;
white-space:pre;
min-width: 45px;
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
}
.xrf button:hover,
.xrf input[type="submit"]:hover,
.xrf .btn:hover {
background: var(--xrf-secondary);
text-decoration:none;
}
.xrf, .xrf *{
font-family: var(--xrf-font-sans-serif);
font-size: var(--xrf-font-size-1);
line-height:27px;
}
textarea, select, input[type="text"] {
background: transparent; /* linear-gradient( var(--xrf-lighter-gray), var(--xrf-gray) ) !important; */
}
input[type="submit"] {
color: var(--xrf-light-gray);
}
input[type=text]{
padding:7px 15px;
}
input{
border-radius:7px;
margin:5px 0px;
}
.title {
border-bottom: 2px solid var(--xrf-secondary);
padding-bottom: 20px;
}
#topbar{
background: var(--xrf-topbar-bg);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 48px;
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
opacity: 0.9;
z-index:2000;
display:none;
}
#topbar .logo{
width: 92px;
position: absolute;
top: 9px;
left: 93px;
height: 30px;
background-size: contain;
background-repeat: no-repeat;
}
#topbar > input[type="submit"] {
height: 32px;
position: absolute;
right: 20px;
top: 2px;
min-width:135px;
}
#topbar > button#navback,
#topbar > button#navforward {
height: 32px;
font-size: 24px;
position: absolute;
left: 9px;
padding: 2px 13px;
border-radius:6px;
top: 8px;
color: var(--xrf-light-gray);
width: 36px;
min-width: unset;
}
#topbar > button#navforward {
left:49px;
}
#topbar > #uri {
height: 18px;
font-size: var(--xrf-font-size-3);
position: absolute;
left: 200px;
top: 9px;
max-width: 550px;
padding: 5px 0px 5px 5px;
width: calc( 63% - 200px);
background: #f0f0f0;
border-color: #Ccc;
border: 2px solid #CCC;
border-radius: 7px;
color: #555;
}
.menu .btn{
display:inline-block;
background: var(--xrf-primary);
border-radius: 25px;
border: 0;
padding: 5px 19px;
font-weight: 1000;
font-family: sans-serif;
font-size: var(--xrf-font-size-2);
color:var(--xrf-primary-fg);
height:33px;
z-index:2000;
cursor:pointer;
min-width:145px;
text-decoration:none;
margin-top: 15px;
line-height:36px;
margin-right:10px;
text-align:left;
}
.xrf a.btn#more{
z-index:3000;
width: 19px;
min-width: 19px;
font-size:16px;
text-align: center;
background:white;
color: var(--xrf-primary);
}
.xrf a.btn#more i.gg-menu{
margin-top:15px;
}
.xrf a.btn#more i.gg-close,
.xrf a.btn#more i.gg-menu{
color:#888;
}
.xrf a.btn#meeting i.gg-user-add{
margin-right: 12px;
}
.xrf a.btn#share i.gg-link{
margin-right:24px;
}
.xrf a.btn#accessibility i.gg-yinyang{
margin-right:13px;
}
html{
max-width:unset;
}
.render {
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
}
.lil-gui.autoPlace{
right:0px !important;
top:48px !important;
height:33vh;
}
#VRButton {
margin-bottom:20vh;
}
@media (max-width: 450px) {
#uri{ display:none; }
}
@media (max-width: 640px) {
.lil-gui.root{
top:auto !important;
left:auto !important;
}
.js-snackbar__message{
overflow-y:auto;
max-height:600px;
}
.js-snackbar__message h1,h2,h3{
font-size:22px;
}
.xrf table tr td {
}
:root{
--xrf-font-size-1: 13px;
--xrf-font-size-2: 17px;
--xrf-font-size-3: 20px;
}
}
.a-enter-vr-button, .a-enter-ar-button{
height:41px;
}
#qrcode{
background: transparent;
overflow: hidden;
height: 121px;
display: inline-block;
position: relative;
}
input#share{
font-size: var(--xrf-font-size-1);
font-family: var(--xrf-font-monospace);
border:2px solid #AAA;
width:50vw;
max-width:400px;
}
.footer {
z-index:1000;
display: flex;
flex-direction: column-reverse; /* This reverses the stacking order of the flex container */
align-items: flex-end;
height: 100%;
position: fixed;
top: 71px;
right: 11px;
bottom: 0;
padding-bottom:140px;
box-sizing:border-box;
pointer-events:none;
}
.footer *{
pointer-events:all;
}
.footer .menu{
text-align:right;
}
.badge {
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;
text-decoration:none !important;
}
.ruler{
width:97%;
margin:7px 0px;
}
a.badge {
text-decoration:none;
}
.xrf select{
border-inline: none;
border-inline: none;
border-block: none;
border: 3px solid var(--xrf-primary);
border-radius: 5px;
background: none;
border-radius:30px;
}
.xrf select,
.xrf option{
padding: 0px 16px;
min-width: 150px;
max-width: 150px;
height: 35px;
}
.xrf input{
border-radius:30px;
padding: 7px 15px;
border-block: none;
border-inline: none;
border: 1px solid #888;
background: transparent;
max-width:105px;
}
.xrf table tr td {
vertical-align:middle;
text-align:right;
}
.xrf table tr td:nth-child(1){
min-width:82px;
height:40px;
padding-right:15px;
}
.xrf small{
font-size: var(--xrf-font-size-0);
}
.disabled{
opacity:0.5
}
body.menu .js-snackbar__wrapper {
top: 64px;
}
/*
* css icons from https://css.gg
*/
.gg-close-o {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--ggs,1));
width: 22px;
height: 22px;
border: 2px solid;
border-radius: 40px
}
.gg-close-o::after,
.gg-close-o::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 12px;
height: 2px;
background: currentColor;
transform: rotate(45deg);
border-radius: 5px;
top: 8px;
left: 3px
}
.gg-close-o::after {
transform: rotate(-45deg)
}
.gg-user-add {
display: inline-block;
transform: scale(var(--ggs,1));
box-sizing: border-box;
width: 20px;
height: 18px;
background:
linear-gradient(
to left,
currentColor 8px,
transparent 0)
no-repeat 14px 6px/6px 2px,
linear-gradient(
to left,
currentColor 8px,
transparent 0)
no-repeat 16px 4px/2px 6px
}
.gg-user-add::after,.gg-user-add::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
border: 2px solid
}
.gg-user-add::before {
width: 8px;
height: 8px;
border-radius: 30px;
top: 0;
left: 2px
}
.gg-user-add::after {
width: 12px;
height: 9px;
border-bottom: 0;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
top: 9px
}
.gg-user {
display: inline-block;
transform: scale(var(--ggs,1));
box-sizing: border-box;
width: 12px;
height: 18px
}
.gg-user::after,
.gg-user::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
border: 2px solid
}
.gg-user::before {
width: 8px;
height: 8px;
border-radius: 30px;
top: 0;
left: 2px
}
.gg-user::after {
width: 12px;
height: 9px;
border-bottom: 0;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
top: 9px
}
.gg-menu {
transform: scale(var(--ggs,1))
}
.gg-menu,
.gg-menu::after,
.gg-menu::before {
box-sizing: border-box;
position: relative;
display: block;
width: 20px;
height: 2px;
border-radius: 3px;
background: currentColor
}
.gg-menu::after,
.gg-menu::before {
content: "";
position: absolute;
top: -6px
}
.gg-menu::after {
top: 6px
}
.gg-close {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--ggs,1)) scale(var(--ggs,1)) translate(-2px,5px);
width: 22px;
height: 22px;
border: 2px solid transparent;
border-radius: 40px
}
.gg-close::after,
.gg-close::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 16px;
height: 2px;
background: currentColor;
transform: rotate(45deg);
border-radius: 5px;
top: 8px;
left: 1px
}
.gg-close::after {
transform: rotate(-45deg)
}
.gg-link {
box-sizing: border-box;
position: relative;
display: inline-block;
-moz-transform: rotate(-45deg) scale(var(--ggs,1));
transform: translate(4px,-5px) rotate(-45deg) scale(var(--ggs,1));
width: 8px;
height: 2px;
background: currentColor;
line-height:11px;
border-radius: 4px
}
.gg-link::after,
.gg-link::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
border-radius: 3px;
width: 8px;
height: 10px;
border: 2px solid;
top: -4px
}
.gg-link::before {
border-right: 0;
border-top-left-radius: 40px;
border-bottom-left-radius: 40px;
left: -6px
}
.gg-link::after {
border-left: 0;
border-top-right-radius: 40px;
border-bottom-right-radius: 40px;
right: -6px
}
.gg-info {
box-sizing: border-box;
position: relative;
display: inline-block;
transform: scale(var(--ggs,1)) translate(-3px, 3px);
width: 20px;
height: 20px;
border: 2px solid;
border-radius: 40px
}
.gg-info::after,
.gg-info::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
border-radius: 3px;
width: 2px;
background: currentColor;
left: 7px
}
.gg-info::after {
bottom: 2px;
height: 8px
}
.gg-info::before {
height: 2px;
top: 2px
}
.gg-yinyang {
box-sizing: border-box;
position: relative;
display: inline-block;
transform: rotate(95deg) scale(var(--ggs,1)) translate(4px,4px);
width: 20px;
height: 20px;
border: 2px solid;
border-radius: 22px
}
.gg-yinyang::after,
.gg-yinyang::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 8px;
height: 8px;
border-radius: 10px;
top: 4px
}
.gg-yinyang::before {
border: 2px solid;
left: 0
}
.gg-yinyang::after {
border: 2px solid transparent;
right: 0;
box-shadow:
inset 0 0 0 4px,
0 -3px 0 1px,
-2px -4px 0 1px,
-8px -5px 0 -1px,
-11px -3px 0 -2px,
-12px -1px 0 -3px,
-6px -6px 0 -1px
}
.gg-image {
box-sizing: border-box;
position: relative;
display: inline-block;
transform: scale(var(--ggs,1)) translate(1px,2px);
width: 20px;
height: 16px;
overflow: hidden;
box-shadow: 0 0 0 2px;
border-radius: 2px
}
.gg-image::after,
.gg-image::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
border: 2px solid
}
.gg-image::after {
transform: rotate(45deg);
border-radius: 3px;
width: 16px;
height: 16px;
top: 9px;
left: 6px
}
.gg-image::before {
width: 6px;
height: 6px;
border-radius: 100%;
top: 2px;
left: 2px
}
.gg-serverless {
box-sizing: border-box;
position: relative;
display: inline-block;
transform: scale(var(--ggs,1)) translate(2px,1px);
width: 15px;
height: 13px;
overflow: hidden
}
.gg-serverless::after,
.gg-serverless::before {
background: currentColor;
content: "";
box-sizing: border-box;
position: absolute;
display: block;
height: 3px;
box-shadow: 0 5px 0,0 10px 0;
transform: skew(-20deg)
}
.gg-serverless::before {
width: 8px;
left: -2px
}
.gg-serverless::after {
width: 12px;
right: -5px
}
.gg-software-download {
box-sizing: border-box;
position: relative;
display: inline-block;
transform: scale(var(--ggs,1)) translate(3px,2px);
width: 16px;
height: 6px;
border: 2px solid;
border-top: 0;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
margin-top: 8px
}
.gg-software-download::after {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 8px;
height: 8px;
border-left: 2px solid;
border-bottom: 2px solid;
transform: rotate(-45deg);
left: 2px;
bottom: 4px
}
.gg-software-download::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
border-radius: 3px;
width: 2px;
height: 10px;
background: currentColor;
left: 5px;
bottom: 5px
}
.gg-arrow-left-r {
box-sizing: border-box;
position: relative;
display: inline-block;
width: 22px;
height: 22px;
border: 2px solid;
transform: scale(var(--ggs,1));
border-radius: 4px
}
.gg-arrow-left-r::after,
.gg-arrow-left-r::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
left: 4px
}
.gg-arrow-left-r::after {
width: 6px;
height: 6px;
border-bottom: 2px solid;
border-left: 2px solid;
transform: rotate(45deg);
bottom: 6px
}
.gg-arrow-left-r::before {
width: 10px;
height: 2px;
bottom: 8px;
background: currentColor
}
</style>
`

View file

@ -0,0 +1,240 @@
// this has some overlap with $menu.js
// frontend serves as a basis for shared functions (download, share e.g.)
window.frontend = (opts) => new Proxy({
html: `
<div id="topbar" class="xrf">
<div class="logo" ></div>
<button id="navback" onclick="history.back()">&#8249;</button>
<button id="navforward" onclick="history.forward()">&#8250;</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>
`,
el: null,
plugin: {},
xrf,
// this SUPER-emit forwards custom events to all objects supporting dispatchEvent
// perfect to broadcast events simultaniously to document + 3D scene
emit(k,v){
v = v || {event:k}
for( let i in opts ){
if( opts[i].dispatchEvent ){
if( opts.debug ) console.log(`${i}.emit(${k},{...})`)
opts[i].dispatchEvent( new CustomEvent(k,{detail:v}) )
}
}
},
init(){
// setup element and delegate events
this.el = document.createElement("div")
this.el.innerHTML = this.html
document.body.appendChild(this.el);
(['click']).map( (e) => this.el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
this
.setupFileLoaders()
.setupIframeUrlHandler()
.setupCapture()
.setupUserHints()
.hidetopbarWhenMenuCollapse()
window.notify = this.notify
setTimeout( () => {
document.dispatchEvent( new CustomEvent("frontend:ready", {detail:opts} ) )
},1)
return this
},
click(id,ev){
switch( id ){
case "load": this.fileLoaders()
}
},
setupFileLoaders(){
// enable user-uploaded asset files (activated by load button)
this.fileLoaders = this.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) )
})
return this
},
setupIframeUrlHandler(){
// allow iframe to open url
window.addEventListener('message', (event) => {
if (event.data && event.data.url) {
window.open(event.data.url, '_blank');
}
});
return this
},
setupCapture(){
// 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})
return this
},
setupUserHints(){
// notify navigation + href mouseovers to user
setTimeout( () => {
window.notify('loading '+document.location.search.substr(1))
setTimeout( () => window.notify("use WASD-keys and mouse-drag to move around",{timeout:false}),2000 )
setTimeout( () => xrf.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false ), 5000)
},100)
return this
},
hidetopbarWhenMenuCollapse(){
// hide topbar when menu collapse button is pressed
document.addEventListener('$menu:collapse', (e) => this.el.querySelector("#topbar").style.display = e.detail === true ? 'block' : 'none')
return this
},
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();
}
},
notify(_str,opts){
if( window.outerWidth < 800 ) return
if( window.accessibility && window.accessibility.enabled ) return $chat.send({message:_str,class:['info']})
opts = opts || {status:'info'}
opts = Object.assign({ status, timeout:4000 },opts)
opts.message = _str
if( typeof str == 'string' ){
str = _str.replace(/(^\w+):/,"<div class='badge'>\$1</div>")
if( !opts.status ){
if( str.match(/error/g) ) opts.status = "danger"
if( str.match(/warning/g) ) opts.status = "warning"
}
opts.message = str
}
window.SnackBar( opts )
opts.message = typeof _str == 'string' ? _str : _str.innerText
window.frontend.emit("notify",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(/#&/,'')
this.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(opts){
opts = opts || {notify:true,qr:true,share:true}
if( network.connected && !document.location.hash.match(/meet=/) ){
let p = $connections.chatnetwork.find( (p) => p.plugin.name == $connections.selectedChatnetwork )
if( p.link ) document.location.hash += `&meet=${p.link}`
}
let url = window.location.href
this.copyToClipboard( url )
// End of *TODO*
if( opts.notify ){
window.notify(`<h2>${ network.connected ? '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="frontend.download()"><i class="gg-software-download"></i>&nbsp;&nbsp;&nbsp;download scene file</button> <br>
<button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')"><i class="gg-image"></i>&nbsp;&nbsp;download 360 screenshot</button> <br>
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld"><i class="gg-serverless"></i>&nbsp;&nbsp;&nbsp;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="&lt;iframe src='${document.location.href}'&gt;&lt;/iframe&gt;" id="share"/>
<br>
`,{timeout:false})
}
// draw QR code
if( opts.qr ){
setTimeout( () => {
let QR = window.QR
QR.canvas = document.getElementById('qrcode')
QR.draw( url, QR.canvas )
},1)
}
// mobile share
if( opts.share && typeof navigator.share != 'undefined'){
navigator.share({
url,
title: 'your meeting link'
})
}
$menu.collapse = true
}
},
{
// auto-trigger events on changes
get(me,k,receiver){ return me[k] },
set(me,k,v){
let from = me[k]
me[k] = v
switch( k ){
case "logo": $logo.style.backgroundImage = `url(${v})`; break;
default: me.emit(`me.${k}.change`, {from,to:v}); break;
}
}
})
frontend = frontend({xrf,document}).init()

View file

@ -3,32 +3,64 @@
window.network = (opts) => new Proxy({ window.network = (opts) => new Proxy({
connected: false, connected: false,
pos: '',
meetinglink: "", meetinglink: "",
peers: {}, peers: {},
plugin: {}, plugin: {},
opts, opts,
start(url){ init(){
console.log("starting network with url "+(url?url:"default")) document.addEventListener('network.disconnect', () => this.connected = false )
document.addEventListener('network.connected', () => this.connected = true )
setTimeout( () => window.frontend.emit('network.init'), 100 )
return this
},
connect(opts){
window.frontend.emit(`network.${this.connected?'disconnect':'connect'}`,opts)
}, },
add(peerid,data){ add(peerid,data){
data = {lastUpdated: new Date().getTime(), id: peerid, ...data } data = {lastUpdated: new Date().getTime(), id: peerid, ...data }
this.peers[peerid] = data this.peers[peerid] = data
opts.scene.dispatchEvent({type:'network.peer.add', peer}) window.frontend.emit(`network.peer.add`,{peer})
}, },
remove(peerid,data){ remove(peerid,data){
delete this.peers[peerid] delete this.peers[peerid]
opts.scene.dispatchEvent({type:'network.peer.remove', peer}) window.frontend.emit(`network.peer.remove`,{peer})
}, },
send(opts){ send(opts){
window.frontend.emit('network.send',opts)
}, },
receive(opts){ receive(opts){
},
getMeetingFromUrl(url){
let hash = url.replace(/.*#/,'')
let parts = hash.split("&")
let meeting = ''
parts.map( (p) => {
if( p.split("=")[0] == 'meet' ) meeting = p.split("=")[1]
})
return meeting
},
randomRoom(){
var names = []
let add = (s) => s.length < 6 && !s.match(/[0-9$]/) && !s.match(/_/) ? names.push(s) : false
for ( var i in window ) add(i)
for ( var i in Object.prototype ) add(i)
for ( var i in Function.prototype ) add(i)
for ( var i in Array.prototype ) add(i)
for ( var i in String.prototype ) add(i)
var a = names[Math.floor(Math.random() * names.length)];
var b = names[Math.floor(Math.random() * names.length)];
var c = names[Math.floor(Math.random() * names.length)];
return String(`${a}-${b}-${c}`).toLowerCase()
} }
}, },
@ -38,15 +70,10 @@ window.network = (opts) => new Proxy({
set(data,k,v){ set(data,k,v){
let from = data[k] let from = data[k]
data[k] = v data[k] = v
//switch( k ){
// default: network.opts.scene.dispatchEvent({type:`network.${k}.change`, from, to:v})
//}
} }
}) })
document.addEventListener('$menu:ready', (e) => { document.addEventListener('frontend:ready', (e) => {
window.network = network(e.detail) window.network = network(e.detail).init()
document.dispatchEvent( new CustomEvent("network:ready", e ) ) 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)
}) })

View file

@ -0,0 +1,248 @@
// a portable snackbar
window.SnackBar = function(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();
}
document.head.innerHTML += `
<style type="text/css">
.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 {
transition:1s;
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: 18px;
}
.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;
}
</style>
`

View file

@ -4,7 +4,7 @@ window.matrix = (opts) => new Proxy({
plugin:{ plugin:{
type: 'network', type: 'network',
name: '[matrix] channel', name: '[Matrix]',
description: 'a standardized decentralized privacy-friendly protocol', description: 'a standardized decentralized privacy-friendly protocol',
url: 'https://matrix.org', url: 'https://matrix.org',
protocol: 'matrix://', protocol: 'matrix://',
@ -53,14 +53,13 @@ window.matrix = (opts) => new Proxy({
</tr> </tr>
</table> </table>
<small style="display:inline-block;float:right">Support for Oauth / OpenID is <a href="https://matrix.org/blog/#openid-connect" target="_blank">in progress</a></small> <small style="display:inline-block;float:right">Support for Oauth / OpenID is <a href="https://matrix.org/blog/#openid-connect" target="_blank">in progress</a></small>
<br><br> <br>
</div> </div>
` `
}, },
init(){ init(){
let network = window.network frontend.plugin['matrix'] = this
network.plugin['matrix'] = this
$connections.chatnetwork = $connections.chatnetwork.concat([this]) $connections.chatnetwork = $connections.chatnetwork.concat([this])
$connections.scene = $connections.scene.concat([this]) $connections.scene = $connections.scene.concat([this])
this.reactToConnectionHrefs() this.reactToConnectionHrefs()

View file

@ -13,81 +13,242 @@ window.trystero = (opts) => new Proxy({
}, },
html: { html: {
generic: (opts) => `` generic: (opts) => `<div>
<a href="${opts.url}" target="_blank" class="badge ruler">P2P</a>
<table>
<tr>
<td>nickname</td>
<td>
<input type="text" id="nickname" placeholder="your nickname" maxlength="18"/>
</td>
</tr>
</table>
</div>
`
}, },
handle: null, // { selfId: .... } when connected room: null, // { selfId: .... } when connected
ip: null, link: '',
roomid: '', selfId: null,
selfId: null, selfStream: null,
connected: false, nickname: '',
connected: false,
useWebcam: false,
useChat: false,
useScene: false,
videos: {},
names: {}, names: {},
ping: { send: null, get: null },
chat: { send: null, get: null }, chat: { send: null, get: null },
name: { send: null, get: null }, name: { send: null, get: null },
href: { send: null, get: null },
init(){ init(){
let network = window.network frontend.plugin['trystero'] = this
network.plugin['trystero'] = this $connections.webcam = $connections.webcam.concat([this])
$connections.webcam = $connections.webcam.concat([this])
$connections.chatnetwork = $connections.chatnetwork.concat([this]) $connections.chatnetwork = $connections.chatnetwork.concat([this])
$connections.scene = $connections.scene.concat([this]) $connections.scene = $connections.scene.concat([this])
if( localStorage.getItem("selfId") ){
this.selfId = localStorage.getItem("selfId")
}else{
this.selfId = String(Math.random()).substr(2)
localStorage.setItem("selfId",this.selfId)
}
this.reactToConnectionHrefs() this.reactToConnectionHrefs()
this.nickname = localStorage.getItem("nickname") || `human${String(Math.random()).substr(5,4)}`
document.addEventListener('network.connect', (e) => this.connect(e.detail) )
document.addEventListener('network.init', () => {
let meeting = network.getMeetingFromUrl(document.location.href)
if( meeting.match(this.plugin.protocol) ){
this.parseLink( meeting )
}
})
}, },
connect(opts){ confirmConnected(){
if( !this.connected ){
this.connected = true
frontend.emit('network.connected',{plugin:this})
this.names[ this.selfId ] = this.nickname
}
},
async connect(opts){
// embedded https://github.com/dmotz/trystero (trystero-torrent.min.js build) // embedded https://github.com/dmotz/trystero (trystero-torrent.min.js build)
console.log("connecting "+this.plugin.name) console.log("connecting "+this.plugin.name)
console.dir(opts) this.createLink() // ensure link
//this.handle = joinRoom( room.config, room.link ) if( opts.selectedWebcam == this.plugin.name ) this.useWebcam = true
//this.send({message:'📡 [trystero] opening P2P WebRTC-channel via bittorrent',class:['info']}) if( opts.selectedChatnetwork == this.plugin.name ) this.useChat = true
if( opts.selectedScene == this.plugin.name ) this.useScene = true
if( this.useWebcam || this.useChat || this.useScene ){
console.log("trystero link: "+this.link)
this.room = joinRoom( {appId: 'xrfragment'}, this.link )
$chat.send({message:`Share the meeting link <a onclick="$menu.share()">by clicking here</a>`,class:['info']})
$chat.send({message:"waiting for other humans..",class:['info']})
// setup trystero events
const [sendPing, getPing] = this.room.makeAction('ping')
this.ping.send = sendPing
this.ping.get = getPing
const [sendName, getName] = this.room.makeAction('name')
this.name.send = sendName
this.name.get = getName
// start pinging
this.ping.pinger = setInterval( () => this.ping.send({ping:true}), 3000 )
this.ping.get((data,peerId) => this.confirmConnected() )
// listen for peers naming themselves
this.name.get((name, peerId) => {
this.confirmConnected()
this.names[peerId] = name
})
// send name to peers who join later
this.room.onPeerJoin( (peerId) => {
this.confirmConnected()
this.names[peerId] = name
this.name.send(this.nickname, peerId )
$chat.send({message:"a new human joined",class:['info']})
})
// delete name of people leaving
this.room.onPeerLeave( (peerId) => delete this.names[peerId] )
if( this.useWebcam ) this.initWebcam()
if( this.useChat ) this.initChat()
if( this.useScene ) this.initScene()
}
},
initChat(){
const [sendChat, getChat] = this.room.makeAction('chat')
this.chat.send = sendChat
this.chat.get = getChat
document.addEventListener('network.send', (e) => {
this.chat.send({...e.detail, from: this.nickname, pos: network.pos }) // send to P2P network
})
// prime chatlog of other people joining
this.room.onPeerJoin( (peerId) => {
if( $chat.getChatLog().length > 0 ) this.chat.send({prime: $chat.getChatLog() }, peerId )
})
// listen for chatmsg
this.chat.get((data, peerId) => {
if( data.prime ){ // first prime is 'truth'
if( this.chat.primed || $chat.getChatLog().length > 0 ) return // only prime once
$chat.$messages.innerHTML += data.prime
$chat.$messages.scrollTop = $chat.$messages.scrollHeight // scroll down
this.chat.primed = true
}else $chat.send({ ...data}) // send to screen
})
},
async initWebcam(){
// get a local audio stream from the microphone
this.selfStream = await navigator.mediaDevices.getUserMedia({
audio: $connections.$audioInput.value,
video: $connections.$videoInput.value
})
this.room.addStream(this.selfStream)
this.videos[ this.selfId ] = this.getVideo(this.selfId,{stream: this.selfStream})
// send stream + chatlog to peers who join later
this.room.onPeerJoin( (peerId) => this.room.addStream( this.selfStream, peerId))
this.room.onPeerStream((stream, peerId) => {
let video = this.getVideo(peerId,{create:true, stream})
this.videos[ this.names[peerId] || peerId ] = video
})
this.room.onPeerLeave( (peerId) => {
let video = this.getVideo(peerId)
if( video ){
video.remove()
delete this.videos[peerId]
}
})
},
initScene(){
// setup trystero events
const [sendHref, getHref] = this.room.makeAction('name')
this.href.send = sendHref
this.href.get = getHref
this.href.get((data,peerId) => {
xrf.hashbus.pub(data.href)
})
},
getVideo(peerId,opts){
opts = opts || {}
let video = this.videos[ this.names[peerId] ] || this.videos[ peerId ]
if (!video && opts.create) {
video = document.createElement('video')
video.autoplay = true
// add video element to the DOM
if( opts.stream ) video.srcObject = opts.stream
console.log("creating video for peerId")
$chat.$videos.appendChild(video)
}
}, },
send(opts){ $chat.send({...opts, source: 'trystero'}) }, send(opts){ $chat.send({...opts, source: 'trystero'}) },
createLink(opts){ createLink(opts){
this.link = document.location.href.replace(/#.*/,'') let hash = document.location.hash
if( this.link.match(/localhost/) ){ if( !this.link ){
fetch('https://api.duckduckgo.com/?q=my+ip&format=json') const meeting = network.getMeetingFromUrl(document.location.href)
.then( (res) => res.json() ) this.link = meeting.match("trystero://") ? meeting : `trystero://r/${network.randomRoom()}:bittorrent`
.then( (res) => {
const ipRegex = /Your IP address is ([0-9]+[.][0-9]+[.][0-9]+[.][0-9]+)/g;
const ip = ipRegex.exec(res.Answer)[1]
this.link = this.link.replace(/localhost/, ip )
})
} }
if( !hash.match('meet=') ) document.location.hash += `${hash.length > 1 ? '&' : '#'}meet=${this.link}`
}, },
config(opts){ config(opts){
opts = {...opts, ...this.plugin } opts = {...opts, ...this.plugin }
let el = document.createElement('div') this.el = document.createElement('div')
let html = this.html.generic(opts) this.el.innerHTML = this.html.generic(opts)
for( let i in opts ){ // window.notify(`${opts.name} is ${opts.description} <br>by using a serverless technology called <a href="https://webrtc.org/" target="_blank">webRTC</a> via <a href="${opts.url}" target="_blank">trystero</a>.<br>You can basically make up your own channelname or choose an existing one.<br>Use this for hasslefree anonymous meetings.`)
if( this.html[i] ) html += this.html[i](opts) this.el.querySelector('#nickname').value = this.nickname
} this.el.querySelector('#nickname').addEventListener('change', (e) => localStorage.setItem("nickname",e.target.value) )
window.notify(`${opts.name} is ${opts.description} <br>by using a serverless technology called <a href="https://webrtc.org/" target="_blank">webRTC</a> via <a href="${opts.url}" target="_blank">trystero</a>.<br>You can basically make up your own channelname or choose an existing one.<br>Use this for hasslefree anonymous meetings.`)
// resolve ip // resolve ip
if( !this.link ) this.createLink(opts) return this.el
return el },
parseLink(url){
if( !url.match(this.plugin.protocol) ) return
let parts = url.replace(this.plugin.protocol,'').split("/")
if( parts[0] == 'r' ){ // this.room
let roomid = parts[1].replace(/:.*/,'')
let server = parts[1].replace(/.*:/,'')
if( server != 'bittorrent' ) return window.notify("only bittorrent is supported for trystero (for now) :/")
this.link = url
$connections.show()
$connections.selectedWebcam = this.plugin.name
$connections.selectedChatnetwork= this.plugin.name
$connections.selectedScene = this.plugin.name
return true
}
return false
}, },
reactToConnectionHrefs(){ reactToConnectionHrefs(){
xrf.addEventListener('href', (opts) => { xrf.addEventListener('href', (opts) => {
let {mesh} = opts let {mesh} = opts
if( !opts.click ) return if( !opts.click ) return
if( mesh.userData.href.match(this.protocol) ){ this.parseLink(mesh.userData.href)
let parts = mesh.userData.href.replace(this.plugin.protocol,'') let href = mesh.userData.href
console.dir(parts) let isLocal = href[0] == '#'
if( parts[0] == 'r' ){ // room let isTeleport = href.match(/(pos=|http:)/)
this.roomid = parts.split("/")[1].replace(/:.*/,'') if( isLocal && !isTeleport && this.href.send ) this.href.send({href})
this.server = parts.split("/")[1].replace(/.*:/,'')
if( this.server != 'bittorrent' ) window.notify("only bittorrent is supported for trystero (for now) :/")
$connections.show()
$connections.selectedWebcam = this.plugin.name
$connections.selectedChatnetwork= this.plugin.name
$connections.selectedScene = this.plugin.name
console.log("configured trystero")
}
}else window.notify("malformed connection URI: "+mesh.userData.href)
}) })
} }
@ -109,12 +270,12 @@ document.addEventListener('$connections:ready', (e) => {
}) })
//window.meeting = window.meeting||{} //window.meeting = window.meeting||{}
//window.meeting.trystero = async function(el,com,data){ //window.meeting.trystero = async function(el,this){
// //
// // embed https://github.com/dmotz/trystero (trystero-torrent.min.js build) // // embed https://github.com/dmotz/trystero (trystero-torrent.min.js build)
// const { joinRoom } = await import("./../../../dist/trystero-torrent.min.js"); // const { joinRoom } = await import("./../../../dist/trystero-torrent.min.js");
// this.room = { // this.room = {
// handle: null, // this.room: null,
// link: null, // link: null,
// selfId: null, // selfId: null,
// names: {}, // names: {},
@ -128,18 +289,18 @@ document.addEventListener('$connections:ready', (e) => {
// this.send = (opts) => com.send({...opts, source: 'trystero'}) // this.send = (opts) => com.send({...opts, source: 'trystero'})
// //
// el.addEventListener('remove', () => { // el.addEventListener('remove', () => {
// if( this.room.handle ) this.room.handle.leave() // if( this.room.room ) this.room.room.leave()
// }) // })
// //
// el.addEventListener('connect', async () => { // el.addEventListener('connect', async () => {
// let room = this.room // let this.room = this.room
// //
// room.link = this.data.link // this.room.link = this.data.link
// if( !room.linkmatch(/(#|&)meet/) ){ // if( !room.linkmatch(/(#|&)meet/) ){
// room.link = room.link.match(/#/) ? '&meet' : '#meet' // this.room.link = this.room.link.match(/#/) ? '&meet' : '#meet'
// } // }
// room.handle = joinRoom( room.config, room.link ) // this.room.room = joinRoom( this.room.config, this.room.link )
// room.selfId = room.handle.selfId // this.selfId = this.room.selfId
// //
// this.send({ // this.send({
// message: "joined meeting at "+roomname.replace(/(#|&)meet/,''), // dont trigger init() // message: "joined meeting at "+roomname.replace(/(#|&)meet/,''), // dont trigger init()
@ -154,56 +315,56 @@ document.addEventListener('$connections:ready', (e) => {
// }) // })
// //
// // setup trystero events // // setup trystero events
// const [sendName, getName] = room.makeAction('name') // const [sendName, getName] = this.room.makeAction('name')
// const [sendChat, getChat] = room.makeAction('chat') // const [sendChat, getChat] = this.room.makeAction('chat')
// room.chat.send = sendChat // this.chat.send = sendChat
// room.chat.get = getChat // this.chat.get = getChat
// room.name.send = sendName // this.name.send = sendName
// room.name.get = getName // this.name.get = getName
// //
// // tell other peers currently in the room our name // // tell other peers currently in the this.room our name
// room.names[ room.selfId ] = com.data.visitorname.substr(0,15) // this.names[ this.selfId ] = this.nickname.substr(0,15)
// room.name.send( com.data.visitorname ) // this.name.send( this.nickname )
// //
// // listen for peers naming themselves // // listen for peers naming themselves
// this.name.get((name, peerId) => (room.names[peerId] = name)) // this.name.get((name, peerId) => (room.names[peerId] = name))
// //
// // send self stream to peers currently in the room // // send self stream to peers currently in the this.room
// room.addStream(com.selfStream) // this.room.addStream(this.selfStream)
// //
// // send stream + chatlog to peers who join later // // send stream + chatlog to peers who join later
// room.onPeerJoin( (peerId) => { // this.room.onPeerJoin( (peerId) => {
// room.addStream( com.selfStream, peerId) // this.room.addStream( this.selfStream, peerId)
// room.name.send( com.data.visitorname, peerId) // this.name.send( this.nickname, peerId)
// room.chat.send({prime: com.log}, peerId ) // this.chat.send({prime: com.log}, peerId )
// }) // })
// //
// room.onPeerLeave( (peerId) => { // this.room.onPeerLeave( (peerId) => {
// console.log(`${room.names[peerId] || 'a visitor'} left`) // console.log(`${room.names[peerId] || 'a visitor'} left`)
// if( com.videos[peerId] ){ // if( com.videos[peerId] ){
// com.videos[peerId].remove() // com.videos[peerId].remove()
// delete com.videos[peerId] // delete com.videos[peerId]
// } // }
// delete room.names[peerId] // delete this.names[peerId]
// }) // })
// //
// // handle streams from other peers // // this.room streams from other peers
// room.onPeerStream((stream, peerId) => { // this.room.onPeerStream((stream, peerId) => {
// // create an audio instance and set the incoming stream // // create an audio instance and set the incoming stream
// const audio = new Audio() // const audio = new Audio()
// audio.srcObject = stream // audio.srcObject = stream
// audio.autoplay = true // audio.autoplay = true
// // add the audio to peerAudio object if you want to address it for something // // add the audio to peerAudio object if you want to address it for something
// // later (volume, etc.) // // later (volume, etc.)
// com.data.audios[peerId] = audio // this.audios[peerId] = audio
// }) // })
// //
// room.onPeerStream((stream, peerId) => { // this.room.onPeerStream((stream, peerId) => {
// com.createVideoElement(stream,peerId) // com.createVideoElement(stream,peerId)
// }) // })
// //
// // listen for chatmsg // // listen for chatmsg
// room.chat.get((data, peerId) => { // this.chat.get((data, peerId) => {
// if( data.prime ){ // if( data.prime ){
// if( com.log.length > 0 ) return // only prime once // if( com.log.length > 0 ) return // only prime once
// console.log("receiving prime") // console.log("receiving prime")

View file

@ -49,7 +49,9 @@ xrf.emit.normal = function(eventName, data) {
var callbacks = xrf._listeners[eventName] var callbacks = xrf._listeners[eventName]
if (callbacks) { if (callbacks) {
for (var i = 0; i < callbacks.length; i++) { for (var i = 0; i < callbacks.length; i++) {
try{
callbacks[i](data); callbacks[i](data);
}catch(e){ console.error(e) }
} }
} }
}; };

View file

@ -96,12 +96,21 @@ xrf.reset = () => {
} }
xrf.parseUrl = (url) => { xrf.parseUrl = (url) => {
const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') ) let urlExHash = url.replace(/#.*/,'')
let urlObj,file
let store = {}
try{
urlObj = new URL( urlExHash.match(/:\/\//) ? urlExHash : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
let search = urlObj.search.substr(1).split("&")
for( let i in search ) store[ (search[i].split("=")[0]) ] = search[i].split("=")[1] || ''
}catch(e){ }
let hashmap = url.match("#") ? url.replace(/.*#/,'').split("&") : []
for( let i in hashmap ) store[ (hashmap[i].split("=")[0]) ] = hashmap[i].split("=")[1] || ''
let dir = url.substring(0, url.lastIndexOf('/') + 1) let dir = url.substring(0, url.lastIndexOf('/') + 1)
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
const hash = url.match(/#/) ? url.replace(/.*#/,'') : '' const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
const ext = file.split('.').pop() const ext = file.split('.').pop()
return {urlObj,dir,file,hash,ext} return {urlObj,dir,file,hash,ext,store}
} }
xrf.add = (object) => { xrf.add = (object) => {

View file

@ -9,7 +9,8 @@ xrf.navigator.to = (url,flags,loader,data) => {
return new Promise( (resolve,reject) => { return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments if( hash == document.location.hash.substr(1) ) return // block duplicate calls
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
xrf.navigator.updateHash(hash) xrf.navigator.updateHash(hash)
return resolve(xrf.model) return resolve(xrf.model)
} }
@ -72,7 +73,6 @@ xrf.navigator.updateHash = (hash,opts) => {
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
console.log(`URL: ${document.location.search.substr(1)}#${hash}`) console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
document.location.hash = hash document.location.hash = hash
xrf.emit('hash', {...opts, hash: `#${hash}` })
} }
xrf.navigator.pushState = (file,hash) => { xrf.navigator.pushState = (file,hash) => {

View file

@ -8,9 +8,6 @@ xrf.frag.defaultPredefinedViews = (opts) => {
}) })
} }
// react to enduser typing url
xrf.addEventListener('hash', (opts) => xrf.hashbus.pub( opts.hash ) )
// clicking href url with predefined view // clicking href url with predefined view
xrf.addEventListener('href', (opts) => { xrf.addEventListener('href', (opts) => {
if( !opts.click || opts.xrf.string[0] != '#' ) return if( !opts.click || opts.xrf.string[0] != '#' ) return

View file

@ -49,8 +49,8 @@ xrf.frag.href = function(v, opts){
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
// *TODO* support for multiple protocols // *TODO* support for multiple protocols
if( !v.string.match(/^http/) ) return if( v.string[0] != '#' && !v.string.match(/^http/) ) return
// always commit current location (keep a trail of last positions before we navigate) // always commit current location in case of teleport (keep a trail of last positions before we navigate)
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`) if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
xrf.navigator.to(v.string) // let's surf to HREF! xrf.navigator.to(v.string) // let's surf to HREF!
}) })