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
* SPDX-License-Identifier: MPL-2.0
*/
@ -551,6 +551,15 @@ xrfragment_XRF.isDeep = new EReg("\\*","");
xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({});
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
// 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
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)
*
@ -1360,7 +662,9 @@ xrf.emit.normal = function(eventName, data) {
var callbacks = xrf._listeners[eventName]
if (callbacks) {
for (var i = 0; i < callbacks.length; i++) {
try{
callbacks[i](data);
}catch(e){ console.error(e) }
}
}
};
@ -1562,6 +866,7 @@ xrf.init = ((init) => function(opts){
xrf.navigator.init()
// return xrfragment lib as 'xrf' query functor (like jquery)
for ( let i in xrf ) xrf.query[i] = xrf[i]
return xrf.query
})(xrf.init)
@ -1643,12 +948,21 @@ xrf.reset = () => {
}
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)
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
const ext = file.split('.').pop()
return {urlObj,dir,file,hash,ext}
return {urlObj,dir,file,hash,ext,store}
}
xrf.add = (object) => {
@ -1672,7 +986,8 @@ xrf.navigator.to = (url,flags,loader,data) => {
return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
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)
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
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
document.location.hash = hash
xrf.emit('hash', {...opts, hash: `#${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
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}`)
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) => {
fetch(url, { method: 'HEAD' })
.then( (res) => {
console.log(`loading src ${url}`)
let mimetype = res.headers.get('Content-type')
if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf'
//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
xrf.addEventListener('href', (opts) => {
if( !opts.click || opts.xrf.string[0] != '#' ) return
@ -2522,7 +1834,7 @@ xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts
let objname = v.fragment
let autoscroll = v.z > 0 || v.w > 0
return // DISABLED
scene.traverse( (mesh) => {
if( mesh.name == objname ){
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
* SPDX-License-Identifier: MPL-2.0
*/
@ -551,6 +551,15 @@ xrfragment_XRF.isDeep = new EReg("\\*","");
xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
})({});
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
// 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
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)
*
@ -1360,7 +662,9 @@ xrf.emit.normal = function(eventName, data) {
var callbacks = xrf._listeners[eventName]
if (callbacks) {
for (var i = 0; i < callbacks.length; i++) {
try{
callbacks[i](data);
}catch(e){ console.error(e) }
}
}
};
@ -1562,6 +866,7 @@ xrf.init = ((init) => function(opts){
xrf.navigator.init()
// return xrfragment lib as 'xrf' query functor (like jquery)
for ( let i in xrf ) xrf.query[i] = xrf[i]
return xrf.query
})(xrf.init)
@ -1643,12 +948,21 @@ xrf.reset = () => {
}
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)
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
const ext = file.split('.').pop()
return {urlObj,dir,file,hash,ext}
return {urlObj,dir,file,hash,ext,store}
}
xrf.add = (object) => {
@ -1672,7 +986,8 @@ xrf.navigator.to = (url,flags,loader,data) => {
return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
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)
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
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
document.location.hash = hash
xrf.emit('hash', {...opts, hash: `#${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
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}`)
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) => {
fetch(url, { method: 'HEAD' })
.then( (res) => {
console.log(`loading src ${url}`)
let mimetype = res.headers.get('Content-type')
if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf'
//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
xrf.addEventListener('href', (opts) => {
if( !opts.click || opts.xrf.string[0] != '#' ) return
@ -2522,7 +1834,7 @@ xrf.addEventListener('dynamicKeyValue', (opts) => {
let {scene,match,v} = opts
let objname = v.fragment
let autoscroll = v.z > 0 || v.w > 0
return // DISABLED
scene.traverse( (mesh) => {
if( mesh.name == objname ){
if( !mesh.geometry ) return console.warn(`mesh '${objname}' has no uvcoordinates to offset`)

9
make
View File

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

View File

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

View File

@ -14,12 +14,15 @@ chatComponent = {
init: (el) => new Proxy({
scene: null,
visible: true,
messages: [],
scene: null,
visible: true,
visibleChatbar: false,
messages: [],
$messages: $messages = el.querySelector("#messages"),
$chatline: $chatline = el.querySelector("#chatline"),
$videos: el.querySelector("#videos"),
$messages: el.querySelector("#messages"),
$chatline: el.querySelector("#chatline"),
$chatbar: el.querySelector("#chatbar"),
install(opts){
this.opts = opts
@ -27,15 +30,18 @@ chatComponent = {
el.className = "xrf"
el.style.display = 'none' // start hidden
document.body.appendChild( el )
this.visibleChatbar = false
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(){
//opts.scene.addEventListener('meeting.peer.add', () => console.log("$chat.peer.add") )
//opts.scene.addEventListener('meeting.peer.remove', () => console.log("$chat.peer.remove") )
let {$chatline} = this
$chatline.addEventListener('keydown', (e) => {
if (e.key == 'Enter' ){
if( $chatline.value[0] != '/' ){
document.dispatchEvent( new CustomEvent("network.send", {detail: {message:$chatline.value}} ) )
}
this.send({message: $chatline.value })
$chatline.value = ''
}
@ -49,10 +55,13 @@ chatComponent = {
},
send(opts){
let {$messages} = this
opts = { linebreak:true, message:"", class:[], ...opts }
let div = document.createElement('div')
let msg = document.createElement('div')
let br = document.createElement('br')
if( window.frontend && window.frontend.emit ) window.frontend.emit('$chat.send', opts )
let div = document.createElement('div')
let msg = document.createElement('div')
let br = document.createElement('br')
let nick = document.createElement('div')
msg.className = "msg"
let html = `${ opts.message || ''}${ opts.html ? opts.html(opts) : ''}`
if( $messages.last == html ) return
@ -62,27 +71,47 @@ chatComponent = {
if( opts.class ){
msg.classList.add.apply(msg.classList, opts.class)
br.classList.add.apply(br.classList, opts.class)
div.classList.add.apply(div.classList, opts.class.concat(["envelope"]))
}
if( !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)
$messages.appendChild(div)
if( opts.linebreak ) div.appendChild(br)
$messages.scrollTop = $messages.scrollHeight // scroll down
document.dispatchEvent( new CustomEvent("$chat:receive", {detail: opts}) )
$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] },
set(data,k,v){
data[k] = v
get(me,k,v){ return me[k] },
set(me,k,v){
me[k] = v
switch( k ){
case "visible": {
el.style.display = data.visible ? 'block' : 'none'
if( !el.inited && (el.inited = true) ) data.initListeners()
$menu.collapsed = !data.visible
break;
}
case "visible": {
el.style.display = me.visible ? 'block' : 'none'
if( !el.inited && (el.inited = true) ) me.initListeners()
break;
}
case "visibleChatbar": {
me.$chatbar.style.display = v ? 'block' : 'none'
}
}
}
@ -93,7 +122,7 @@ chatComponent = {
document.addEventListener('$menu:ready', (opts) => {
opts = opts.detail
document.head.innerHTML += chatComponent.css
$chat = document.createElement('div')
window.$chat = document.createElement('div')
$chat.innerHTML = chatComponent.html
$chat = chatComponent.init($chat)
$chat.install(opts)
@ -167,29 +196,78 @@ chatComponent.css = `
}
#messages{
position: absolute;
top: 100px;
transition:1s;
top: 0px;
left: 0;
right: 0;
bottom: 88px;
bottom: 130px;
padding: 15px;
pointer-events: none;
overflow-y:auto;
overflow:hidden;
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{
transition:all 1s ease;
background: #fff;
display: inline-block;
padding: 6px 17px;
border-radius: 20px;
padding: 1px 17px;
border-radius: 20px 0px 20px 20px;
color: #000c;
margin-bottom: 10px;
line-height:23px;
pointer-events:visible;
border: 1px solid #ccc8;
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{
font-size: 14px;
padding: 3px 16px;
background: #473f7f;
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{
display:unset;
@ -200,12 +278,6 @@ chatComponent.css = `
br.guide{
display:inline-block;
}
#messages .msg.info a,
#messages .msg.info a:visited{
color: var(--xrf-primary);
text-decoration: underline;
transition:0.3s;
}
#messages .msg.info a:hover,
#messages button:hover{
filter: brightness(1.4);
@ -213,9 +285,6 @@ chatComponent.css = `
#messages .msg.multiline {
padding: 2px 14px;
}
#messages .msg.config {
background:#
}
#messages button {
text-decoration:none;
margin: 0px 15px 10px 0px;
@ -256,4 +325,19 @@ background:#
.nomargin{
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>`

View File

@ -2,31 +2,62 @@ connectionsComponent = {
html: `
<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>
<tr>
<td>Webcam/Audio</td>
<td></td>
<td>
<select id="webcam"></select>
</td>
</tr>
<tr>
<td>Chat</td>
<td>
<select id="chatnetwork"></select>
</td>
</tr>
<tr>
<td>Scene</td>
<td>
<select id="scene"></select>
<button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
</td>
</tr>
</table>
<div id="settings"></div>
<br>
<button id="connect" onclick="$connections.connect()">📡 Connect!</button>
<br><br>
</div>
`,
@ -44,40 +75,54 @@ connectionsComponent = {
$chatnetwork: $chatnetwork = el.querySelector("#chatnetwork"),
$scene: $scene = el.querySelector("#scene"),
$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){
this.opts = opts
document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) )
this.opts = opts;
(['change']).map( (e) => el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
this.reactToNetwork()
$menu.buttons = ([
`<a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick="$connections.show()"><i class="gg-user-add"></i>&nbsp;connect</a><br>`
]).concat($menu.buttons)
$webcam.addEventListener('change', () => this.renderSettings() )
$chatnetwork.addEventListener('change', () => this.renderSettings() )
$scene.addEventListener('change', () => this.renderSettings() )
// hide networking settings if entering thru meetinglink
if( document.location.href.match(/meet=/) ) this.show()
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(){
$chat.visible = true
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
if( !network.connected ){
if( el.parentElement ) el.parentElement.parentElement.remove()
$chat.send({message:"", el})
this.renderSettings()
$chat.send({message:"", el, class:['ui']})
if( !network.meetinglink ){ // set default
$webcam.value = 'Peer2Peer'
$chatnetwork.value = 'Peer2Peer'
$scene.value = 'Peer2Peer'
}
}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..")
this.renderSettings()
}else{
$chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
}
},
update(){
@ -102,13 +147,84 @@ connectionsComponent = {
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
this.update()
$settings.innerHTML = ''
this.forSelectedPluginsDo( (plugin) => {
console.log("configuring "+plugin.plugin.name)
console.dir(plugin)
$settings.appendChild( plugin.config(opts) )
this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config(opts) ) )
this.renderInputs()
},
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] },
@ -119,8 +235,13 @@ connectionsComponent = {
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 "selectedScene": $scene.value = v; data.renderSettings(); break;
case "selectedWebcam": $webcam.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!
document.addEventListener('network:ready', (opts) => {
document.addEventListener('$menu:ready', (opts) => {
opts = opts.detail
document.head.innerHTML += connectionsComponent.css
$connections = document.createElement('div')
window.$connections = document.createElement('div')
$connections.innerHTML = connectionsComponent.html
$connections = connectionsComponent.init($connections)
$connections.install(opts)
@ -143,8 +264,21 @@ document.addEventListener('network:ready', (opts) => {
connectionsComponent.css = `
<style type="text/css">
button#connect{
float: right;
height: 43px;
width:100%;
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>`

View File

@ -1,872 +1,66 @@
// reactive component for displaying the menu
menuComponent = {
menuComponent = (el) => new Proxy({
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="menu">
<div id="buttons"></div>
<a class="btn" id="more" aria-title="menu button" aria-description="menu with options, like extra accessibility" onclick="$menu.toggle()"></a><br>
<a class="btn" id="more" aria-title="menu button"><i id="icon" class="gg-menu"></i></a><br>
</div>
</div>
`,
init: (el) => new Proxy({
morelabel: '⚡',
collapsed: false,
logo: './../../assets/logo.png',
buttons: [`<a class="btn" aria-label="button" aria-title="share button" aria-description="share URL/screenshot/embed" id="share" onclick="$menu.share()">🔗 share</a><br>`],
collapsed: false,
logo: './../../assets/logo.png',
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>`],
$buttons: $buttons = el.querySelector('#buttons'),
$btnMore: $btnMore = el.querySelector('#more'),
$overlay: $overlay = el.querySelector('#overlay'),
$logo: $logo = el.querySelector('.logo'),
$uri: $uri = el.querySelector('#uri'),
$buttons: $buttons = el.querySelector('#buttons'),
$btnMore: $btnMore = el.querySelector('#more'),
toggle: () => $menu.collapsed = !$menu.collapsed,
install: (xrf) => {
this.xrf = xrf
$menu.bindToWindow() // bind functions like notify to window
window.notify('loading '+document.location.search.substr(1))
document.body.appendChild(el)
document.dispatchEvent( new CustomEvent("$menu:ready", {detail: xrf}) )
// add screenshot component with camera to capture bigger size equirects
// document.querySelector('a-scene').components.screenshot.capture('perspective')
$('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2})
if( window.outerWidth > 800 )
setTimeout( () => window.notify("use WASD-keys and mouse-drag to move around",{timeout:false}),2000 )
xrf.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false )
// enable user-uploaded asset files
let fileLoaders = $menu.loadFile({
".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ),
".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) )
})
el.querySelector("#overlay > input[type=submit]").addEventListener("click", fileLoaders )
}
},{
get(data,k,v){ return data[k] },
set(data,k,v){
data[k] = v
switch( k ){
case "logo": $logo.style.backgroundImage = `url(${v})`; break;
case "css": document.head.innerHTML += v; break;
case "morelabel": $btnMore.innerText = data.morelabel; break;
case "buttons": $buttons.innerHTML = this.renderButtons(data);
document.dispatchEvent( new CustomEvent("$menu:buttons:render", {detail: el.querySelector('.menu') }) )
break;
case "collapsed": $overlay.style.display = data.collapsed ? 'block' : 'none'
$buttons.style.display = data.collapsed ? 'block' : 'none'
break;
}
},
renderButtons: (data) => `${data.buttons.join('')}`
})
}
// reactify component!
$menu = document.createElement('div')
$menu.innerHTML = menuComponent.html
$menu = menuComponent.init($menu)
// attach menu functions which are less related to rendering
let utils = {
bindToWindow(opts){
window.notify = $menu.notify(window)
// reroute console messages to snackbar notifications
console.log = ( (log,console) => function(str){
if( String(str).match(/(:.*#|note:|:\/\/)/) ) window.notify( str )
log.call(console,str)
})(console.log, console)
// allow iframe to open url
window.addEventListener('message', (event) => {
if (event.data && event.data.url) {
window.open(event.data.url, '_blank');
}
});
},
loadFile(contentLoaders, multiple){
return () => {
window.notify("if you're on Meta browser, file-uploads might be disabled")
let input = document.createElement('input');
input.type = 'file';
input.multiple = multiple;
input.accept = Object.keys(contentLoaders).join(",");
input.onchange = () => {
let files = Array.from(input.files);
let file = files.slice ? files[0] : files
for( var i in contentLoaders ){
let r = new RegExp('\\'+i+'$')
if( file.name.match(r) ) return contentLoaders[i](file)
}
alert(file.name+" is not supported")
};
input.click();
}
toggle(){
this.collapsed = !this.collapsed
el.querySelector("i#icon").className = this.collapsed ? 'gg-close' : 'gg-menu'
document.body.classList[ this.collapsed ? 'add' : 'remove' ](['menu'])
},
SnackBar(userOptions) {
var snackbar = this || (window.snackbar = {});
var _Interval;
var _Message;
var _Element;
var _Container;
var _OptionDefaults = {
message: "Operation performed successfully.",
dismissible: true,
timeout: 7000,
status: ""
}
var _Options = _OptionDefaults;
function _Create() {
_Container = document.querySelector(".js-snackbar-container")
if( _Container ){
_Container.remove()
}
_Container = null
if (!_Container) {
// need to create a new container for notifications
_Container = document.createElement("div");
_Container.classList.add("js-snackbar-container");
document.body.appendChild(_Container);
}
_Container.opts = _Options
_Container.innerHTML = ''
_Element = document.createElement("div");
_Element.classList.add("js-snackbar__wrapper","xrf");
let innerSnack = document.createElement("div");
innerSnack.classList.add("js-snackbar", "js-snackbar--show");
if (_Options.status) {
_Options.status = _Options.status.toLowerCase().trim();
let status = document.createElement("span");
status.classList.add("js-snackbar__status");
if (_Options.status === "success" || _Options.status === "green") {
status.classList.add("js-snackbar--success");
}
else if (_Options.status === "warning" || _Options.status === "alert" || _Options.status === "orange") {
status.classList.add("js-snackbar--warning");
}
else if (_Options.status === "danger" || _Options.status === "error" || _Options.status === "red") {
status.classList.add("js-snackbar--danger");
}
else {
status.classList.add("js-snackbar--info");
}
innerSnack.appendChild(status);
}
_Message = document.createElement("span");
_Message.classList.add("js-snackbar__message");
if( typeof _Options.message == 'string' ){
_Message.innerHTML = _Options.message;
}else _Message.appendChild(_Options.message)
innerSnack.appendChild(_Message);
if (_Options.dismissible) {
let closeBtn = document.createElement("span");
closeBtn.classList.add("js-snackbar__close");
closeBtn.innerText = "\u00D7";
closeBtn.onclick = snackbar.Close;
innerSnack.appendChild(closeBtn);
}
_Element.style.height = "0px";
_Element.style.opacity = "0";
_Element.style.marginTop = "0px";
_Element.style.marginBottom = "0px";
_Element.appendChild(innerSnack);
_Container.appendChild(_Element);
if (_Options.timeout !== false) {
_Interval = setTimeout(snackbar.Close, _Options.timeout);
}
}
snackbar.Open = function() {
let contentHeight = _Element.firstElementChild.scrollHeight; // get the height of the content
_Element.style.height = contentHeight + "px";
_Element.style.opacity = 1;
_Element.style.marginTop = "5px";
_Element.style.marginBottom = "5px";
_Element.addEventListener("transitioned", function() {
_Element.removeEventListener("transitioned", arguments.callee);
_Element.style.height = null;
})
}
snackbar.Close = function () {
if (_Interval)
clearInterval(_Interval);
let snackbarHeight = _Element.scrollHeight; // get the auto height as a px value
let snackbarTransitions = _Element.style.transition;
_Element.style.transition = "";
requestAnimationFrame(function() {
_Element.style.height = snackbarHeight + "px"; // set the auto height to the px height
_Element.style.opacity = 1;
_Element.style.marginTop = "0px";
_Element.style.marginBottom = "0px";
_Element.style.transition = snackbarTransitions
requestAnimationFrame(function() {
_Element.style.height = "0px";
_Element.style.opacity = 0;
})
});
setTimeout(function() {
try {
_Container.removeChild(_Element);
} catch (e) { }
}, 1000);
};
_Options = { ..._OptionDefaults, ...userOptions }
_Create();
snackbar.Open();
},
notify(scope){
return function notify(_str,opts){
if( accessibility.enabled ) return $chat.send({message:_str})
str = _str.replace(/(^\w+):/,"<div class='badge'>\$1</div>")
opts = opts || {status:'info'}
opts = Object.assign({ status, timeout:4000 },opts)
if( typeof str == 'string' ){
if( !opts.status ){
if( str.match(/error/g) ) opts.status = "danger"
if( str.match(/warning/g) ) opts.status = "warning"
}
}
opts.message = str
window.$menu.SnackBar( opts )
opts.message = _str
document.dispatchEvent( new CustomEvent("notify", {detail:opts}) )
}
},
download(){
function fetchAndDownload(dataurl, filename) {
var a = document.createElement("a");
a.href = dataurl;
a.setAttribute("download", filename);
a.click();
return false;
}
let file = document.location.search.replace(/\?/,'')
fetchAndDownload( file, file )
},
updateHashPosition(randomize){
// *TODO* this should be part of the XRF Threejs framework
if( typeof THREE == 'undefined' ) THREE = xrf.THREE
let radToDeg = THREE.MathUtils.radToDeg
let toDeg = (x) => x / (Math.PI / 180)
let camera = document.querySelector('[camera]').object3D.parent // *TODO* fix for threejs
camera.position.x += Math.random()/10
camera.position.z += Math.random()/10
// *TODO* add camera direction
let direction = new xrf.THREE.Vector3()
camera.getWorldDirection(direction)
const pitch = Math.asin(direction.y);
const yaw = Math.atan2(direction.x, direction.z);
const pitchInDegrees = pitch * 180 / Math.PI;
const yawInDegrees = yaw * 180 / Math.PI;
let lastPos = `pos=${camera.position.x.toFixed(2)},${camera.position.y.toFixed(2)},${camera.position.z.toFixed(2)}`
let newHash = document.location.hash.replace(/[&]?(pos|rot)=[0-9\.-]+,[0-9\.-]+,[0-9\.-]+/,'')
newHash += `&${lastPos}`
document.location.hash = newHash.replace(/&&/,'&')
.replace(/#&/,'')
$menu.copyToClipboard( window.location.href );
},
copyToClipboard(text){
// copy url to clipboard
var dummy = document.createElement('input')
document.body.appendChild(dummy);
dummy.value = text;
dummy.select();
document.execCommand('copy');
document.body.removeChild(dummy);
},
share(){
let inMeeting = $('[meeting]')
let url = window.location.href
if( !inMeeting ) $menu.updateHashPosition()
else url = $('[meeting]').components['meeting'].data.link
$menu.copyToClipboard( url )
// End of *TODO*
window.notify(`<h2>${ inMeeting ? 'Meeting link ' : 'Link'} copied to clipboard!</h2> <br>Now share it with your friends ❤️<br>
<canvas id="qrcode" width="121" height="121"></canvas><br>
<button onclick="$menu.download()">💾 download scene file</button> <br>
<button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')">📷 download 360 screenshot</button> <br>
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld">🖥 clone & selfhost this experience</a><br>
<br>
To embed this experience in your blog,<br>
copy/paste the following into your HTML:<br><input type="text" value="&lt;iframe src='${document.location.href}'&gt;<br>&lt;/iframe&gt;" id="share"/>
<br>
`,{timeout:2000000})
// draw QR code
init(opts){
el.innerHTML = this.html
document.body.appendChild(el);
(['click']).map( (e) => el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
setTimeout( () => {
let QR = window.QR
QR.canvas = document.getElementById('qrcode')
QR.draw( url, QR.canvas )
},0)
// mobile share
if( typeof navigator.share != 'undefined'){
navigator.share({
url,
title: 'your meeting link'
})
document.dispatchEvent( new CustomEvent("$menu:ready", {detail: {$menu:this,xrf}}) )
},100)
return this
},
click(id,e){
switch(id){
case "icon":
case "more": this.toggle(); break;
}
}
}
},
{
// map to component
for( let i in utils ) $menu[i] = utils[i]
get(me,k,v){ return me[k] },
//$('a-scene').addEventListener('XRF', this.onXRFready )
//
// if( document.location.search.length > 2 ){
// $('[xrf]').setAttribute('xrf', document.location.search.substr(1)+document.location.hash )
// }
//
// },
//
// onXRFready: function(){
//
// let XRF = window.AFRAME.XRF
// //setupMenu(XRF){
// // let aScene = document.querySelector('a-scene')
// // let urlbar = $('input#uri')
// // let inIframe = window.location !== window.parent.location
// // let els = [ ...document.querySelectorAll('.menu .btn') ]
// // els = els.filter( (el) => el.id != "more" ? el : false )
//
// // // enable meetings
// // let startMeeting = () => {
// // aScene.setAttribute('meeting', 'id: xrfragments')
// // $('a#meeting').innerText = '🧑‍🤝‍🧑 breakout meeting'
// // $('a#meeting').setAttribute('aria-description','breakout room')
// // }
// // $('a#meeting').addEventListener('click', () => {
// // if( aScene.getAttribute('meeting') ){ // meeting already, start breakout room
// // let parentRoom = document.location.href
// // $menu.updateHashPosition(true)
// // let meeting = $('[meeting]').components['meeting']
// // meeting.data.parentRoom = parentRoom
// // meeting.update()
// // }else startMeeting()
// // })
// // if( document.location.hash.match(/(#|&)meet/) ) startMeeting()
//
// // XRF.addEventListener('hash', () => reflectUrl() )
// // const reflectUrl = window.reflectUrl = (url) => {
// // urlbar.value = url || document.location.search.substr(1) + document.location.hash
// // }
// // reflectUrl()
// //},
//
//
// // on localhost enable debugging mode for developer convenience
// let loc = document.location
// if( loc.host.match(/^localhost/) ){
// $('a-scene').setAttribute('stats')
// XRF.debug = 1
// }
//
// // add screenshot component with camera to capture bigger size equirects
// // document.querySelector('a-scene').components.screenshot.capture('perspective')
// $('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2})
//
// if( window.outerWidth > 800 )
// setTimeout( () => window.notify("use WASD-keys and mouse-drag to move around",{timeout:false}),2000 )
//
// window.AFRAME.XRF.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false )
//
// // enable user-uploaded asset files
// let fileLoaders = $menu.loadFile({
// ".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ),
// ".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) )
// })
// $("#overlay > input[type=submit]").addEventListener("click", fileLoaders )
// finally add some css
$menu.css = `
<style type="text/css">
:root {
--xrf-primary: #6839dc;
--xrf-primary-fg: #FFF;
--xrf-light-primary: #ea23cf;
--xrf-secondary: #872eff;
--xrf-light-xrf-secondary: #ce7df2;
--xrf-overlay-bg: #fffb;
--xrf-box-shadow: #0005;
--xrf-red: red;
--xrf-dark-gray: #343334;
--xrf-gray: #424280;
--xrf-white: #fdfdfd;
--xrf-light-gray: #efefef;
--xrf-lighter-gray: #e4e2fb96;
--xrf-font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
--xrf-font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
--xrf-font-size-0: 11px;
--xrf-font-size-1: 14px;
--xrf-font-size-2: 17px;
--xrf-font-size-3: 21px;
set(me,k,v){
me[k] = v
switch( k ){
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'
frontend.emit('$menu:collapse', v)
break;
}
},
/* 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}
renderButtons: (data) => `${data.buttons.join('')}`
.xrf table tr td{
vertical-align:top;
}
.xrf button,
.xrf input[type="submit"],
.xrf .btn {
text-decoration:none;
background: var(--xrf-primary);
border: 0;
border-radius: 25px;
padding: 11px 15px;
font-weight: bold;
transition: 0.3s;
height: 32px;
font-size: var(--xrf-font-size-1);
color: var(--xrf-primary-fg);
line-height: var(--xrf-font-size-1);
cursor:pointer;
white-space:pre;
min-width: 45px;
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
}
})
.xrf button:hover,
.xrf input[type="submit"]:hover,
.xrf .btn:hover {
background: var(--xrf-secondary);
text-decoration:none;
}
.xrf, .xrf *{
font-family: var(--xrf-font-sans-serif);
font-size: var(--xrf-font-size-1);
line-height:27px;
}
textarea, select, input[type="text"] {
background: transparent; /* linear-gradient( var(--xrf-lighter-gray), var(--xrf-gray) ) !important; */
}
input[type="submit"] {
color: var(--xrf-light-gray);
}
input[type=text]{
padding:7px 15px;
}
input{
border-radius:7px;
margin:5px 0px;
}
.title {
border-bottom: 2px solid var(--xrf-secondary);
padding-bottom: 20px;
}
#overlay{
background: var(--xrf-overlay-bg);
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 48px;
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
opacity: 0.9;
z-index:2000;
}
#overlay .logo{
width: 92px;
position: absolute;
top: 9px;
left: 93px;
height: 30px;
background-size: contain;
background-repeat: no-repeat;
}
#overlay > input[type="submit"] {
height: 32px;
position: absolute;
right: 20px;
top: 2px;
}
#overlay > button#navback,
#overlay > button#navforward {
height: 32px;
font-size: var(--xrf-font-size-1);
position: absolute;
left: 9px;
padding: 2px 13px;
border-radius:6px;
top: 8px;
color: var(--xrf-light-gray);
width: 36px;
min-width: unset;
}
#overlay > button#navforward {
left:49px;
}
#overlay > #uri {
height: 18px;
font-size: var(--xrf-font-size-3);
position: absolute;
left: 200px;
top: 9px;
max-width: 550px;
padding: 5px 0px 5px 5px;
width: calc( 63% - 200px);
background: #f0f0f0;
border-color: #Ccc;
border: 2px solid #CCC;
border-radius: 7px;
color: #555;
}
.menu .btn{
display:inline-block;
background: var(--xrf-primary);
border-radius: 25px;
border: 0;
padding: 5px 19px;
font-weight: 1000;
font-family: sans-serif;
font-size: var(--xrf-font-size-2);
color:var(--xrf-primary-fg);
height:33px;
z-index:2000;
cursor:pointer;
min-width:145px;
text-decoration:none;
margin-top: 15px;
line-height:36px;
margin-right:10px;
text-align:left;
}
.xrf a.btn#more{
z-index:3000;
width: 19px;
min-width: 19px;
font-size:16px;
text-align: center;
background:white;
color: var(--xrf-primary);
}
html{
max-width:unset;
}
.render {
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
}
.lil-gui.autoPlace{
right:0px !important;
top:48px !important;
height:33vh;
}
#VRButton {
margin-bottom:20vh;
}
@media (max-width: 450px) {
#uri{ display:none; }
}
@media (max-width: 640px) {
.lil-gui.root{
top:auto !important;
left:auto !important;
}
.js-snackbar__message{
overflow-y:auto;
max-height:600px;
}
.js-snackbar__message h1,h2,h3{
font-size:22px;
}
.xrf table tr td {
}
:root{
--xrf-font-size-1: 13px;
--xrf-font-size-2: 17px;
--xrf-font-size-3: 20px;
}
}
/* notifications */
.js-snackbar-container .btn,
.js-snackbar-container input[type=submit],
.js-snackbar-container button{
margin-bottom:15px;
}
.js-snackbar-container {
position: absolute;
top: 10px;
left: 0px;
display: flex;
align-items: center;
width:100%;
max-width: 100%;
padding: 10px;
z-index:1001;
justify-content: center;
overflow: hidden;
}
.js-snackbar-container * {
box-sizing: border-box;
}
.js-snackbar__wrapper {
--color-c: #555;
--color-a: #FFF;
}
.js-snackbar__wrapper {
overflow: hidden;
height: auto;
margin: 5px 0;
transition: all ease .5s;
border-radius: 3px;
box-shadow: 0 0 4px 0 var(--xrf-box-shadow);
right: 20px;
position: fixed;
top: 55px;
}
.js-snackbar {
display: inline-flex;
box-sizing: border-box;
border-radius: 3px;
color: var(--color-c);
background-color: var(--color-a);
vertical-align: bottom;
}
.js-snackbar__close,
.js-snackbar__status,
.js-snackbar__message {
position: relative;
}
.js-snackbar__message {
margin: 12px;
}
.js-snackbar__status {
display: none;
width: 15px;
margin-right: 5px;
border-radius: 3px 0 0 3px;
background-color: transparent;
}
.js-snackbar__status.js-snackbar--success,
.js-snackbar__status.js-snackbar--warning,
.js-snackbar__status.js-snackbar--danger,
.js-snackbar__status.js-snackbar--info {
display: block;
}
.js-snackbar__status.js-snackbar--success {
background-color: #4caf50;
}
.js-snackbar__status.js-snackbar--warning {
background-color: #ff9800;
}
.js-snackbar__status.js-snackbar--danger {
background-color: #ff6060;
}
.js-snackbar__status.js-snackbar--info {
background-color: #CCC;
}
.js-snackbar__close {
cursor: pointer;
display: flex;
align-items: center;
padding: 0 10px;
user-select: none;
}
.js-snackbar__close:hover {
background-color: #4443;
}
.a-enter-vr-button, .a-enter-ar-button{
height:41px;
}
#qrcode{
background: transparent;
overflow: hidden;
height: 121px;
display: inline-block;
position: relative;
}
input#share{
font-size: var(--xrf-font-size-1);
font-family: var(--xrf-font-monospace);
border:2px solid #AAA;
width:50vw;
max-width:400px;
}
.footer {
display: flex;
flex-direction: column-reverse; /* This reverses the stacking order of the flex container */
align-items: flex-end;
height: 100%;
position: fixed;
top: 71px;
right: 11px;
bottom: 0;
padding-bottom:149px;
box-sizing:border-box;
}
.footer .menu{
text-align:right;
}
.badge {
display:inline-block;
color: var(--xrf-white);
font-weight: bold;
background: var(--xrf-gray);
border-radius:5px;
padding:0px 4px;
font-size: var(--xrf-font-size-0);
margin-right:10px
}
.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>
`
// reactify component!
document.addEventListener('frontend:ready', (e) => {
window.$menu = menuComponent( document.createElement('div') ).init(e.detail)
})

View File

@ -17,18 +17,20 @@ window.accessibility = (opts) => new Proxy({
settings(){
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
str = str.replace(/\/\//,' ')
.replace(/:/,'')
.replace(/\//,' slash ')
.replace(/\./,' dot ')
.replace(/#/,' hash ')
.replace(/&/,' and ')
.replace(/=/,' is ')
if( opts.speaksigns ){
str = str.replace(/\/\//,' ')
.replace(/:/,'')
.replace(/\//,' slash ')
.replace(/\./,' dot ')
.replace(/#/,' hash ')
.replace(/&/,' and ')
.replace(/=/,' is ')
}
let speech = window.speechSynthesis
let utterance = new SpeechSynthesisUtterance( str )
if( this.speak_voice != -1) utterance.voice = speech.getVoices()[ this.speak_voice ];
@ -41,7 +43,7 @@ window.accessibility = (opts) => new Proxy({
utterance.rate = this.speak_rate
utterance.pitch = this.speak_pitch
utterance.volume = this.speak_volume
if( override ) speech.cancel()
if( opts.override ) speech.cancel()
speech.speak(utterance)
},
@ -56,7 +58,7 @@ window.accessibility = (opts) => new Proxy({
case "ArrowRight": k = "right"; break;
case "ArrowDown": k = "backward"; break;
}
this.speak(k,true)
this.speak(k,{override:true})
})
document.addEventListener('$menu:buttons:render', (e) => {
@ -66,38 +68,51 @@ window.accessibility = (opts) => new Proxy({
a.map( (btn) => {
if( !btn.href ) btn.setAttribute("href","javascript:void(0)") // important!
btn.setAttribute("aria-label","button")
btn.addEventListener('mouseover', (e) => {
let str = btn.getAttribute("aria-title") + btn.getAttribute('aria-description')
this.speak( str,true)
})
})
document.addEventListener('mouseover', (e) => {
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
opts.message = 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) => {
let obj
let description
let msg = "You've teleported to "
let pos = opts.frag.pos
if( pos.string.match(',') ) msg += `coordinates <a href="#pos=${pos.string}">${pos.string}</a>`
else{
msg += `location <a href="#pos=${pos.string}">${pos.string}</a>`
obj = opts.scene.getObjectByName(pos.string)
if( obj ){
description = obj.userData['aria-label'] || ''
}else msg += ", but your teleportation was refused because it cannot be found within this world"
}
$chat.send({html: () => msg, class:["info","guide"]})
opts.xrf.addEventListener('pos', (opts) => {
if( this.enabled ){
$chat.send({message: this.posToMessage(opts) })
}
network.send({message: this.posToMessage(opts), class:["info","guide"]})
network.pos = opts.frag.pos.string
})
},
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(){
return $chat.$messages.innerText
.replaceAll("<[^>]*>", "") // strip html
@ -114,7 +129,7 @@ window.accessibility = (opts) => new Proxy({
data[k] = v
switch( k ){
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)'
if( v ) $chat.visible = true
$chat.send({message,class:['info','guide']})
@ -131,9 +146,10 @@ window.accessibility = (opts) => new Proxy({
})
document.addEventListener('$menu:ready', (e) => {
window.accessibility = accessibility(e.detail)
accessibility.init()
document.dispatchEvent( new CustomEvent("accessibility:ready", e ) )
$menu.buttons = $menu.buttons.concat([`<a class="btn" style="background:var(--xrf-dark-gray);filter: brightness(0.5);" aria-label="button" aria-description="enable all accessibility features" id="accessibility" onclick="accessibility.settings()">👩‍🚀 accessibility</a><br>`])
try{
accessibility = accessibility(e.detail)
accessibility.init()
document.dispatchEvent( new CustomEvent("accessibility:ready", e ) )
$menu.buttons = $menu.buttons.concat([`<a class="btn" style="background:var(--xrf-dark-gray);filter: brightness(0.5);" aria-label="button" aria-description="enable all accessibility features" id="accessibility" onclick="accessibility.settings()"><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({
connected: false,
pos: '',
meetinglink: "",
peers: {},
plugin: {},
opts,
start(url){
console.log("starting network with url "+(url?url:"default"))
init(){
document.addEventListener('network.disconnect', () => this.connected = false )
document.addEventListener('network.connected', () => this.connected = true )
setTimeout( () => window.frontend.emit('network.init'), 100 )
return this
},
connect(opts){
window.frontend.emit(`network.${this.connected?'disconnect':'connect'}`,opts)
},
add(peerid,data){
data = {lastUpdated: new Date().getTime(), id: peerid, ...data }
this.peers[peerid] = data
opts.scene.dispatchEvent({type:'network.peer.add', peer})
window.frontend.emit(`network.peer.add`,{peer})
},
remove(peerid,data){
delete this.peers[peerid]
opts.scene.dispatchEvent({type:'network.peer.remove', peer})
window.frontend.emit(`network.peer.remove`,{peer})
},
send(opts){
window.frontend.emit('network.send',opts)
},
receive(opts){
},
getMeetingFromUrl(url){
let hash = url.replace(/.*#/,'')
let parts = hash.split("&")
let meeting = ''
parts.map( (p) => {
if( p.split("=")[0] == 'meet' ) meeting = p.split("=")[1]
})
return meeting
},
randomRoom(){
var names = []
let add = (s) => s.length < 6 && !s.match(/[0-9$]/) && !s.match(/_/) ? names.push(s) : false
for ( var i in window ) add(i)
for ( var i in Object.prototype ) add(i)
for ( var i in Function.prototype ) add(i)
for ( var i in Array.prototype ) add(i)
for ( var i in String.prototype ) add(i)
var a = names[Math.floor(Math.random() * names.length)];
var b = names[Math.floor(Math.random() * names.length)];
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){
let from = data[k]
data[k] = v
//switch( k ){
// default: network.opts.scene.dispatchEvent({type:`network.${k}.change`, from, to:v})
//}
}
})
document.addEventListener('$menu:ready', (e) => {
window.network = network(e.detail)
document.addEventListener('frontend:ready', (e) => {
window.network = network(e.detail).init()
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:{
type: 'network',
name: '[matrix] channel',
name: '[Matrix]',
description: 'a standardized decentralized privacy-friendly protocol',
url: 'https://matrix.org',
protocol: 'matrix://',
@ -53,14 +53,13 @@ window.matrix = (opts) => new Proxy({
</tr>
</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>
<br><br>
<br>
</div>
`
},
init(){
let network = window.network
network.plugin['matrix'] = this
frontend.plugin['matrix'] = this
$connections.chatnetwork = $connections.chatnetwork.concat([this])
$connections.scene = $connections.scene.concat([this])
this.reactToConnectionHrefs()

View File

@ -13,81 +13,242 @@ window.trystero = (opts) => new Proxy({
},
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
ip: null,
roomid: '',
selfId: null,
connected: false,
room: null, // { selfId: .... } when connected
link: '',
selfId: null,
selfStream: null,
nickname: '',
connected: false,
useWebcam: false,
useChat: false,
useScene: false,
videos: {},
names: {},
ping: { send: null, get: null },
chat: { send: null, get: null },
name: { send: null, get: null },
href: { send: null, get: null },
init(){
let network = window.network
network.plugin['trystero'] = this
$connections.webcam = $connections.webcam.concat([this])
frontend.plugin['trystero'] = this
$connections.webcam = $connections.webcam.concat([this])
$connections.chatnetwork = $connections.chatnetwork.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.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)
console.log("connecting "+this.plugin.name)
console.dir(opts)
//this.handle = joinRoom( room.config, room.link )
//this.send({message:'📡 [trystero] opening P2P WebRTC-channel via bittorrent',class:['info']})
this.createLink() // ensure link
if( opts.selectedWebcam == this.plugin.name ) this.useWebcam = true
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'}) },
createLink(opts){
this.link = document.location.href.replace(/#.*/,'')
if( this.link.match(/localhost/) ){
fetch('https://api.duckduckgo.com/?q=my+ip&format=json')
.then( (res) => res.json() )
.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 )
})
let hash = document.location.hash
if( !this.link ){
const meeting = network.getMeetingFromUrl(document.location.href)
this.link = meeting.match("trystero://") ? meeting : `trystero://r/${network.randomRoom()}:bittorrent`
}
if( !hash.match('meet=') ) document.location.hash += `${hash.length > 1 ? '&' : '#'}meet=${this.link}`
},
config(opts){
opts = {...opts, ...this.plugin }
let el = document.createElement('div')
let html = this.html.generic(opts)
for( let i in opts ){
if( this.html[i] ) html += this.html[i](opts)
}
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.`)
this.el = document.createElement('div')
this.el.innerHTML = this.html.generic(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.`)
this.el.querySelector('#nickname').value = this.nickname
this.el.querySelector('#nickname').addEventListener('change', (e) => localStorage.setItem("nickname",e.target.value) )
// resolve ip
if( !this.link ) this.createLink(opts)
return el
return this.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(){
xrf.addEventListener('href', (opts) => {
let {mesh} = opts
if( !opts.click ) return
if( mesh.userData.href.match(this.protocol) ){
let parts = mesh.userData.href.replace(this.plugin.protocol,'')
console.dir(parts)
if( parts[0] == 'r' ){ // room
this.roomid = parts.split("/")[1].replace(/:.*/,'')
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)
this.parseLink(mesh.userData.href)
let href = mesh.userData.href
let isLocal = href[0] == '#'
let isTeleport = href.match(/(pos=|http:)/)
if( isLocal && !isTeleport && this.href.send ) this.href.send({href})
})
}
@ -109,12 +270,12 @@ document.addEventListener('$connections:ready', (e) => {
})
//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)
// const { joinRoom } = await import("./../../../dist/trystero-torrent.min.js");
// this.room = {
// handle: null,
// this.room: null,
// link: null,
// selfId: null,
// names: {},
@ -128,18 +289,18 @@ document.addEventListener('$connections:ready', (e) => {
// this.send = (opts) => com.send({...opts, source: 'trystero'})
//
// el.addEventListener('remove', () => {
// if( this.room.handle ) this.room.handle.leave()
// if( this.room.room ) this.room.room.leave()
// })
//
// 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/) ){
// room.link = room.link.match(/#/) ? '&meet' : '#meet'
// this.room.link = this.room.link.match(/#/) ? '&meet' : '#meet'
// }
// room.handle = joinRoom( room.config, room.link )
// room.selfId = room.handle.selfId
// this.room.room = joinRoom( this.room.config, this.room.link )
// this.selfId = this.room.selfId
//
// this.send({
// message: "joined meeting at "+roomname.replace(/(#|&)meet/,''), // dont trigger init()
@ -154,56 +315,56 @@ document.addEventListener('$connections:ready', (e) => {
// })
//
// // setup trystero events
// const [sendName, getName] = room.makeAction('name')
// const [sendChat, getChat] = room.makeAction('chat')
// room.chat.send = sendChat
// room.chat.get = getChat
// room.name.send = sendName
// room.name.get = getName
// const [sendName, getName] = this.room.makeAction('name')
// const [sendChat, getChat] = this.room.makeAction('chat')
// this.chat.send = sendChat
// this.chat.get = getChat
// this.name.send = sendName
// this.name.get = getName
//
// // tell other peers currently in the room our name
// room.names[ room.selfId ] = com.data.visitorname.substr(0,15)
// room.name.send( com.data.visitorname )
// // tell other peers currently in the this.room our name
// this.names[ this.selfId ] = this.nickname.substr(0,15)
// this.name.send( this.nickname )
//
// // listen for peers naming themselves
// this.name.get((name, peerId) => (room.names[peerId] = name))
//
// // send self stream to peers currently in the room
// room.addStream(com.selfStream)
// // send self stream to peers currently in the this.room
// this.room.addStream(this.selfStream)
//
// // send stream + chatlog to peers who join later
// room.onPeerJoin( (peerId) => {
// room.addStream( com.selfStream, peerId)
// room.name.send( com.data.visitorname, peerId)
// room.chat.send({prime: com.log}, peerId )
// this.room.onPeerJoin( (peerId) => {
// this.room.addStream( this.selfStream, peerId)
// this.name.send( this.nickname, peerId)
// this.chat.send({prime: com.log}, peerId )
// })
//
// room.onPeerLeave( (peerId) => {
// this.room.onPeerLeave( (peerId) => {
// console.log(`${room.names[peerId] || 'a visitor'} left`)
// if( com.videos[peerId] ){
// com.videos[peerId].remove()
// delete com.videos[peerId]
// }
// delete room.names[peerId]
// delete this.names[peerId]
// })
//
// // handle streams from other peers
// room.onPeerStream((stream, peerId) => {
// // this.room streams from other peers
// this.room.onPeerStream((stream, peerId) => {
// // create an audio instance and set the incoming stream
// const audio = new Audio()
// audio.srcObject = stream
// audio.autoplay = true
// // add the audio to peerAudio object if you want to address it for something
// // later (volume, etc.)
// com.data.audios[peerId] = audio
// this.audios[peerId] = audio
// })
//
// room.onPeerStream((stream, peerId) => {
// this.room.onPeerStream((stream, peerId) => {
// com.createVideoElement(stream,peerId)
// })
//
// // listen for chatmsg
// room.chat.get((data, peerId) => {
// this.chat.get((data, peerId) => {
// if( data.prime ){
// if( com.log.length > 0 ) return // only prime once
// console.log("receiving prime")

View File

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

View File

@ -96,12 +96,21 @@ xrf.reset = () => {
}
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)
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
const ext = file.split('.').pop()
return {urlObj,dir,file,hash,ext}
return {urlObj,dir,file,hash,ext,store}
}
xrf.add = (object) => {

View File

@ -9,7 +9,8 @@ xrf.navigator.to = (url,flags,loader,data) => {
return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
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)
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
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
document.location.hash = hash
xrf.emit('hash', {...opts, hash: `#${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
xrf.addEventListener('href', (opts) => {
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
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
// *TODO* support for multiple protocols
if( !v.string.match(/^http/) ) return
// always commit current location (keep a trail of last positions before we navigate)
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}`)
xrf.navigator.to(v.string) // let's surf to HREF!
})