work in progress [might break]
This commit is contained in:
parent
2e01822029
commit
13f96e0506
26 changed files with 606383 additions and 4848 deletions
2876
dist/xrfragment.aframe.all.js
vendored
2876
dist/xrfragment.aframe.all.js
vendored
File diff suppressed because one or more lines are too long
1197
dist/xrfragment.aframe.js
vendored
1197
dist/xrfragment.aframe.js
vendored
File diff suppressed because it is too large
Load diff
1671
dist/xrfragment.extras.js
vendored
Normal file
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
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
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
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
383
dist/xrfragment.plugin.p2p.js
vendored
Normal file
File diff suppressed because one or more lines are too long
750
dist/xrfragment.three.js
vendored
750
dist/xrfragment.three.js
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* v0.5.1 generated at Fri Dec 15 07:00:04 PM CET 2023
|
* v0.5.1 generated at Sat Dec 30 09:46:42 PM UTC 2023
|
||||||
* https://xrfragment.org
|
* https://xrfragment.org
|
||||||
* SPDX-License-Identifier: MPL-2.0
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
*/
|
*/
|
||||||
|
|
@ -551,6 +551,15 @@ xrfragment_XRF.isDeep = new EReg("\\*","");
|
||||||
xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
|
xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
|
||||||
})({});
|
})({});
|
||||||
var xrfragment = $hx_exports["xrfragment"];
|
var xrfragment = $hx_exports["xrfragment"];
|
||||||
|
// the core project uses #vanillajs #proxies #clean #noframework
|
||||||
|
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||||
|
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||||
|
|
||||||
|
$el = (html,tag) => {
|
||||||
|
let el = document.createElement('div')
|
||||||
|
el.innerHTML = html
|
||||||
|
return el.children[0]
|
||||||
|
}
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
// Copyright (c) 2023 Leon van Kammen/NLNET
|
// Copyright (c) 2023 Leon van Kammen/NLNET
|
||||||
|
|
||||||
|
|
@ -602,713 +611,6 @@ xrf.hasTag = (tag,tags) => String(tags).match( new RegExp(`(^| )${tag}( |$)`,`g`
|
||||||
|
|
||||||
// map library functions to xrf
|
// map library functions to xrf
|
||||||
for ( let i in xrfragment ) xrf[i] = xrfragment[i]
|
for ( let i in xrfragment ) xrf[i] = xrfragment[i]
|
||||||
// handy shortcuts
|
|
||||||
if( !window.$ ) window.$ = (s) => s ? document.querySelector(s) : false
|
|
||||||
if( !window.$$ ) window.$$ = (s) => s ? [ ...document.querySelectorAll(s) ] : false
|
|
||||||
|
|
||||||
window.XRFMENU = {
|
|
||||||
|
|
||||||
logo: './../../assets/logo.png',
|
|
||||||
|
|
||||||
html: [
|
|
||||||
`<a class="btn" aria-label="button" aria-description="start text/audio/video chat" id="meeting" target="_blank">🧑🤝🧑 meeting</a><br>`,
|
|
||||||
`<a class="btn" aria-label="button" aria-description="share URL/screenshot/embed" id="share" target="_blank" onclick="window.share()">🔗 share</a><br>`
|
|
||||||
],
|
|
||||||
|
|
||||||
loadFile(contentLoaders, multiple){
|
|
||||||
return () => {
|
|
||||||
window.notify("if you're on Meta browser, file-uploads might be disabled")
|
|
||||||
let input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.multiple = multiple;
|
|
||||||
input.accept = Object.keys(contentLoaders).join(",");
|
|
||||||
input.onchange = () => {
|
|
||||||
let files = Array.from(input.files);
|
|
||||||
let file = files.slice ? files[0] : files
|
|
||||||
for( var i in contentLoaders ){
|
|
||||||
let r = new RegExp('\\'+i+'$')
|
|
||||||
if( file.name.match(r) ) return contentLoaders[i](file)
|
|
||||||
}
|
|
||||||
alert(file.name+" is not supported")
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setupMenu(XRF){
|
|
||||||
let aScene = document.querySelector('a-scene')
|
|
||||||
let urlbar = $('input#uri')
|
|
||||||
let inIframe = window.location !== window.parent.location
|
|
||||||
let els = [ ...document.querySelectorAll('.menu .btn') ]
|
|
||||||
els = els.filter( (el) => el.id != "more" ? el : false )
|
|
||||||
|
|
||||||
let showMenu = (state) => {
|
|
||||||
els.map( (el) => el.style.display = state ? 'inline-block' : 'none' )
|
|
||||||
$('a#more').style.display = state ? 'none' : 'inline-block'
|
|
||||||
$('#overlay').style.display = state ? 'inline-block' : 'none'
|
|
||||||
if( inIframe ) $('#uri').style.display = 'block'
|
|
||||||
}
|
|
||||||
|
|
||||||
els.map( (el) => el.addEventListener('click', () => showMenu(false) ) )
|
|
||||||
$('a#more').addEventListener('click', () => showMenu(true) )
|
|
||||||
$('.a-canvas').addEventListener('click', () => showMenu(false) )
|
|
||||||
|
|
||||||
// enable meetings
|
|
||||||
let startMeeting = () => {
|
|
||||||
aScene.setAttribute('meeting', 'id: xrfragments')
|
|
||||||
$('a#meeting').innerText = '🧑🤝🧑 breakout meeting'
|
|
||||||
$('a#meeting').setAttribute('aria-description','breakout room')
|
|
||||||
}
|
|
||||||
$('a#meeting').addEventListener('click', () => {
|
|
||||||
if( aScene.getAttribute('meeting') ){ // meeting already, start breakout room
|
|
||||||
let parentRoom = document.location.href
|
|
||||||
XRFMENU.updateHashPosition(true)
|
|
||||||
let meeting = $('[meeting]').components['meeting']
|
|
||||||
meeting.data.parentRoom = parentRoom
|
|
||||||
meeting.update()
|
|
||||||
}else startMeeting()
|
|
||||||
})
|
|
||||||
if( document.location.hash.match(/(#|&)meet/) ) startMeeting()
|
|
||||||
|
|
||||||
XRF.addEventListener('hash', () => reflectUrl() )
|
|
||||||
const reflectUrl = window.reflectUrl = (url) => {
|
|
||||||
urlbar.value = url || document.location.search.substr(1) + document.location.hash
|
|
||||||
}
|
|
||||||
reflectUrl()
|
|
||||||
},
|
|
||||||
|
|
||||||
SnackBar(userOptions) {
|
|
||||||
var snackbar = this || (window.snackbar = {});
|
|
||||||
var _Interval;
|
|
||||||
var _Message;
|
|
||||||
var _Element;
|
|
||||||
var _Container;
|
|
||||||
|
|
||||||
var _OptionDefaults = {
|
|
||||||
message: "Operation performed successfully.",
|
|
||||||
dismissible: true,
|
|
||||||
timeout: 7000,
|
|
||||||
status: ""
|
|
||||||
}
|
|
||||||
var _Options = _OptionDefaults;
|
|
||||||
|
|
||||||
function _Create() {
|
|
||||||
_Container = document.querySelector(".js-snackbar-container")
|
|
||||||
if( _Container ){
|
|
||||||
_Container.remove()
|
|
||||||
}
|
|
||||||
_Container = null
|
|
||||||
|
|
||||||
if (!_Container) {
|
|
||||||
// need to create a new container for notifications
|
|
||||||
_Container = document.createElement("div");
|
|
||||||
_Container.classList.add("js-snackbar-container");
|
|
||||||
|
|
||||||
document.body.appendChild(_Container);
|
|
||||||
}
|
|
||||||
_Container.opts = _Options
|
|
||||||
_Container.innerHTML = ''
|
|
||||||
_Element = document.createElement("div");
|
|
||||||
_Element.classList.add("js-snackbar__wrapper","xrf");
|
|
||||||
|
|
||||||
let innerSnack = document.createElement("div");
|
|
||||||
innerSnack.classList.add("js-snackbar", "js-snackbar--show");
|
|
||||||
|
|
||||||
if (_Options.status) {
|
|
||||||
_Options.status = _Options.status.toLowerCase().trim();
|
|
||||||
|
|
||||||
let status = document.createElement("span");
|
|
||||||
status.classList.add("js-snackbar__status");
|
|
||||||
|
|
||||||
|
|
||||||
if (_Options.status === "success" || _Options.status === "green") {
|
|
||||||
status.classList.add("js-snackbar--success");
|
|
||||||
}
|
|
||||||
else if (_Options.status === "warning" || _Options.status === "alert" || _Options.status === "orange") {
|
|
||||||
status.classList.add("js-snackbar--warning");
|
|
||||||
}
|
|
||||||
else if (_Options.status === "danger" || _Options.status === "error" || _Options.status === "red") {
|
|
||||||
status.classList.add("js-snackbar--danger");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
status.classList.add("js-snackbar--info");
|
|
||||||
}
|
|
||||||
|
|
||||||
innerSnack.appendChild(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
_Message = document.createElement("span");
|
|
||||||
_Message.classList.add("js-snackbar__message");
|
|
||||||
if( typeof _Options.message == 'string' ){
|
|
||||||
_Message.innerHTML = _Options.message;
|
|
||||||
}else _Message.appendChild(_Options.message)
|
|
||||||
|
|
||||||
innerSnack.appendChild(_Message);
|
|
||||||
|
|
||||||
if (_Options.dismissible) {
|
|
||||||
let closeBtn = document.createElement("span");
|
|
||||||
closeBtn.classList.add("js-snackbar__close");
|
|
||||||
closeBtn.innerText = "\u00D7";
|
|
||||||
|
|
||||||
closeBtn.onclick = snackbar.Close;
|
|
||||||
|
|
||||||
innerSnack.appendChild(closeBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
_Element.style.height = "0px";
|
|
||||||
_Element.style.opacity = "0";
|
|
||||||
_Element.style.marginTop = "0px";
|
|
||||||
_Element.style.marginBottom = "0px";
|
|
||||||
|
|
||||||
_Element.appendChild(innerSnack);
|
|
||||||
_Container.appendChild(_Element);
|
|
||||||
|
|
||||||
if (_Options.timeout !== false) {
|
|
||||||
_Interval = setTimeout(snackbar.Close, _Options.timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
snackbar.Open = function() {
|
|
||||||
let contentHeight = _Element.firstElementChild.scrollHeight; // get the height of the content
|
|
||||||
|
|
||||||
_Element.style.height = contentHeight + "px";
|
|
||||||
_Element.style.opacity = 1;
|
|
||||||
_Element.style.marginTop = "5px";
|
|
||||||
_Element.style.marginBottom = "5px";
|
|
||||||
|
|
||||||
_Element.addEventListener("transitioned", function() {
|
|
||||||
_Element.removeEventListener("transitioned", arguments.callee);
|
|
||||||
_Element.style.height = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
snackbar.Close = function () {
|
|
||||||
if (_Interval)
|
|
||||||
clearInterval(_Interval);
|
|
||||||
|
|
||||||
let snackbarHeight = _Element.scrollHeight; // get the auto height as a px value
|
|
||||||
let snackbarTransitions = _Element.style.transition;
|
|
||||||
_Element.style.transition = "";
|
|
||||||
|
|
||||||
requestAnimationFrame(function() {
|
|
||||||
_Element.style.height = snackbarHeight + "px"; // set the auto height to the px height
|
|
||||||
_Element.style.opacity = 1;
|
|
||||||
_Element.style.marginTop = "0px";
|
|
||||||
_Element.style.marginBottom = "0px";
|
|
||||||
_Element.style.transition = snackbarTransitions
|
|
||||||
|
|
||||||
requestAnimationFrame(function() {
|
|
||||||
_Element.style.height = "0px";
|
|
||||||
_Element.style.opacity = 0;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
try {
|
|
||||||
_Container.removeChild(_Element);
|
|
||||||
} catch (e) { }
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
_Options = { ..._OptionDefaults, ...userOptions }
|
|
||||||
_Create();
|
|
||||||
snackbar.Open();
|
|
||||||
},
|
|
||||||
|
|
||||||
notify(scope){
|
|
||||||
return function notify(str,opts){
|
|
||||||
opts = opts || {status:'info'}
|
|
||||||
opts = Object.assign({ status, timeout:4000 },opts)
|
|
||||||
if( typeof str == 'string' ){
|
|
||||||
if( !opts.status ){
|
|
||||||
if( str.match(/error/g) ) opts.status = "danger"
|
|
||||||
if( str.match(/warning/g) ) opts.status = "warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opts.message = str
|
|
||||||
window.XRFMENU.SnackBar( opts )
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
download(){
|
|
||||||
function fetchAndDownload(dataurl, filename) {
|
|
||||||
var a = document.createElement("a");
|
|
||||||
a.href = dataurl;
|
|
||||||
a.setAttribute("download", filename);
|
|
||||||
a.click();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let file = document.location.search.replace(/\?/,'')
|
|
||||||
fetchAndDownload( file, file )
|
|
||||||
},
|
|
||||||
|
|
||||||
updateHashPosition(randomize){
|
|
||||||
// *TODO* this should be part of the XRF Threejs framework
|
|
||||||
if( typeof THREE == 'undefined' ) THREE = xrf.THREE
|
|
||||||
let radToDeg = THREE.MathUtils.radToDeg
|
|
||||||
let toDeg = (x) => x / (Math.PI / 180)
|
|
||||||
let camera = document.querySelector('[camera]').object3D.parent // *TODO* fix for threejs
|
|
||||||
camera.position.x += Math.random()/10
|
|
||||||
camera.position.z += Math.random()/10
|
|
||||||
|
|
||||||
// *TODO* add camera direction
|
|
||||||
let direction = new xrf.THREE.Vector3()
|
|
||||||
camera.getWorldDirection(direction)
|
|
||||||
const pitch = Math.asin(direction.y);
|
|
||||||
const yaw = Math.atan2(direction.x, direction.z);
|
|
||||||
const pitchInDegrees = pitch * 180 / Math.PI;
|
|
||||||
const yawInDegrees = yaw * 180 / Math.PI;
|
|
||||||
|
|
||||||
let lastPos = `pos=${camera.position.x.toFixed(2)},${camera.position.y.toFixed(2)},${camera.position.z.toFixed(2)}`
|
|
||||||
let newHash = document.location.hash.replace(/[&]?(pos|rot)=[0-9\.-]+,[0-9\.-]+,[0-9\.-]+/,'')
|
|
||||||
newHash += `&${lastPos}`
|
|
||||||
document.location.hash = newHash.replace(/&&/,'&')
|
|
||||||
.replace(/#&/,'')
|
|
||||||
XRFMENU.copyToClipboard( window.location.href );
|
|
||||||
},
|
|
||||||
|
|
||||||
copyToClipboard(text){
|
|
||||||
// copy url to clipboard
|
|
||||||
var dummy = document.createElement('input')
|
|
||||||
document.body.appendChild(dummy);
|
|
||||||
dummy.value = text;
|
|
||||||
dummy.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(dummy);
|
|
||||||
},
|
|
||||||
|
|
||||||
share(){
|
|
||||||
let inMeeting = $('[meeting]')
|
|
||||||
let url = window.location.href
|
|
||||||
if( !inMeeting ) XRFMENU.updateHashPosition()
|
|
||||||
else url = $('[meeting]').components['meeting'].data.link
|
|
||||||
XRFMENU.copyToClipboard( url )
|
|
||||||
// End of *TODO*
|
|
||||||
window.notify(`<h2>${ inMeeting ? 'Meeting link ' : 'Link'} copied to clipboard!</h2> <br>Now share it with your friends ❤️<br>
|
|
||||||
<canvas id="qrcode" width="121" height="121"></canvas><br>
|
|
||||||
<button onclick="window.download()">💾 download scene file</button> <br>
|
|
||||||
<button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')">📷 download 360 screenshot</button> <br>
|
|
||||||
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld">🖥 clone & selfhost this experience</a><br>
|
|
||||||
<br>
|
|
||||||
To embed this experience in your blog,<br>
|
|
||||||
copy/paste the following into your HTML:<br><input type="text" value="<iframe src='${document.location.href}'><br></iframe>" id="share"/>
|
|
||||||
<br>
|
|
||||||
`,{timeout:2000000})
|
|
||||||
// draw QR code
|
|
||||||
setTimeout( () => {
|
|
||||||
let QR = window.QR
|
|
||||||
QR.canvas = document.getElementById('qrcode')
|
|
||||||
QR.draw( url, QR.canvas )
|
|
||||||
},0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()"><</button>
|
|
||||||
<button id="navforward" onclick="history.forward()">></button>
|
|
||||||
<input type="submit" value="load 3D file"></input>
|
|
||||||
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )" style="display:none"/>
|
|
||||||
</div>
|
|
||||||
<!-- open AFRAME inspector: $('a-scene').components.inspector.openInspector() -->
|
|
||||||
<div class="xrf footer">
|
|
||||||
<div id="buttons" class="menu">
|
|
||||||
${window.XRFMENU.html.map( (html) => typeof html == "function" ? html() : html ).join('\n')}
|
|
||||||
<a class="btn" id="more" style="display:inline-block">${window.XRFMENU.morelabel}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
document.body.appendChild(el)
|
|
||||||
|
|
||||||
if( XRFMENU.logo ) $('.logo').style['background-image'] = `url(${XRFMENU.logo})`
|
|
||||||
|
|
||||||
window.notify = XRFMENU.notify(window)
|
|
||||||
window.share = XRFMENU.share
|
|
||||||
window.download = XRFMENU.download
|
|
||||||
window.notify('loading '+document.location.search.substr(1))
|
|
||||||
// reroute console messages to snackbar notifications
|
|
||||||
console.log = ( (log) => function(str){
|
|
||||||
if( String(str).match(/(:.*#|note:)/) ) window.notify(str)
|
|
||||||
log(str)
|
|
||||||
})(console.log)
|
|
||||||
// allow iframe to open url
|
|
||||||
window.addEventListener('message', (event) => {
|
|
||||||
if (event.data && event.data.url) {
|
|
||||||
window.open(event.data.url, '_blank');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
/*
|
/*
|
||||||
* (promise-able) EVENTS (optionally continue after listeners are finished using .then)
|
* (promise-able) EVENTS (optionally continue after listeners are finished using .then)
|
||||||
*
|
*
|
||||||
|
|
@ -1360,7 +662,9 @@ xrf.emit.normal = function(eventName, data) {
|
||||||
var callbacks = xrf._listeners[eventName]
|
var callbacks = xrf._listeners[eventName]
|
||||||
if (callbacks) {
|
if (callbacks) {
|
||||||
for (var i = 0; i < callbacks.length; i++) {
|
for (var i = 0; i < callbacks.length; i++) {
|
||||||
|
try{
|
||||||
callbacks[i](data);
|
callbacks[i](data);
|
||||||
|
}catch(e){ console.error(e) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1562,6 +866,7 @@ xrf.init = ((init) => function(opts){
|
||||||
xrf.navigator.init()
|
xrf.navigator.init()
|
||||||
// return xrfragment lib as 'xrf' query functor (like jquery)
|
// return xrfragment lib as 'xrf' query functor (like jquery)
|
||||||
for ( let i in xrf ) xrf.query[i] = xrf[i]
|
for ( let i in xrf ) xrf.query[i] = xrf[i]
|
||||||
|
|
||||||
return xrf.query
|
return xrf.query
|
||||||
})(xrf.init)
|
})(xrf.init)
|
||||||
|
|
||||||
|
|
@ -1643,12 +948,21 @@ xrf.reset = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
xrf.parseUrl = (url) => {
|
xrf.parseUrl = (url) => {
|
||||||
const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
|
let urlExHash = url.replace(/#.*/,'')
|
||||||
|
let urlObj,file
|
||||||
|
let store = {}
|
||||||
|
try{
|
||||||
|
urlObj = new URL( urlExHash.match(/:\/\//) ? urlExHash : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
|
||||||
|
file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
|
||||||
|
let search = urlObj.search.substr(1).split("&")
|
||||||
|
for( let i in search ) store[ (search[i].split("=")[0]) ] = search[i].split("=")[1] || ''
|
||||||
|
}catch(e){ }
|
||||||
|
let hashmap = url.match("#") ? url.replace(/.*#/,'').split("&") : []
|
||||||
|
for( let i in hashmap ) store[ (hashmap[i].split("=")[0]) ] = hashmap[i].split("=")[1] || ''
|
||||||
let dir = url.substring(0, url.lastIndexOf('/') + 1)
|
let dir = url.substring(0, url.lastIndexOf('/') + 1)
|
||||||
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
|
|
||||||
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
|
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
|
||||||
const ext = file.split('.').pop()
|
const ext = file.split('.').pop()
|
||||||
return {urlObj,dir,file,hash,ext}
|
return {urlObj,dir,file,hash,ext,store}
|
||||||
}
|
}
|
||||||
|
|
||||||
xrf.add = (object) => {
|
xrf.add = (object) => {
|
||||||
|
|
@ -1672,7 +986,8 @@ xrf.navigator.to = (url,flags,loader,data) => {
|
||||||
return new Promise( (resolve,reject) => {
|
return new Promise( (resolve,reject) => {
|
||||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
||||||
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
|
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
|
||||||
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
|
if( hash == document.location.hash.substr(1) ) return // block duplicate calls
|
||||||
|
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
|
||||||
xrf.navigator.updateHash(hash)
|
xrf.navigator.updateHash(hash)
|
||||||
return resolve(xrf.model)
|
return resolve(xrf.model)
|
||||||
}
|
}
|
||||||
|
|
@ -1735,7 +1050,6 @@ xrf.navigator.updateHash = (hash,opts) => {
|
||||||
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
|
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
|
||||||
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
|
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
|
||||||
document.location.hash = hash
|
document.location.hash = hash
|
||||||
xrf.emit('hash', {...opts, hash: `#${hash}` })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xrf.navigator.pushState = (file,hash) => {
|
xrf.navigator.pushState = (file,hash) => {
|
||||||
|
|
@ -1808,7 +1122,9 @@ xrf.frag.href = function(v, opts){
|
||||||
//}
|
//}
|
||||||
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
|
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
|
||||||
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
|
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
|
||||||
// always commit current location (keep a trail of last positions before we navigate)
|
// *TODO* support for multiple protocols
|
||||||
|
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
|
||||||
|
// always commit current location in case of teleport (keep a trail of last positions before we navigate)
|
||||||
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
|
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
|
||||||
xrf.navigator.to(v.string) // let's surf to HREF!
|
xrf.navigator.to(v.string) // let's surf to HREF!
|
||||||
})
|
})
|
||||||
|
|
@ -1967,7 +1283,6 @@ xrf.frag.src.enableSourcePortation = (src) => {
|
||||||
xrf.frag.src.externalSRC = (url,frag,opts) => {
|
xrf.frag.src.externalSRC = (url,frag,opts) => {
|
||||||
fetch(url, { method: 'HEAD' })
|
fetch(url, { method: 'HEAD' })
|
||||||
.then( (res) => {
|
.then( (res) => {
|
||||||
console.log(`loading src ${url}`)
|
|
||||||
let mimetype = res.headers.get('Content-type')
|
let mimetype = res.headers.get('Content-type')
|
||||||
if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf'
|
if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf'
|
||||||
//if( url.match(/\.(fbx|stl|obj)$/) ) mimetype =
|
//if( url.match(/\.(fbx|stl|obj)$/) ) mimetype =
|
||||||
|
|
@ -2510,9 +1825,6 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// react to enduser typing url
|
|
||||||
xrf.addEventListener('hash', (opts) => xrf.hashbus.pub( opts.hash ) )
|
|
||||||
|
|
||||||
// clicking href url with predefined view
|
// clicking href url with predefined view
|
||||||
xrf.addEventListener('href', (opts) => {
|
xrf.addEventListener('href', (opts) => {
|
||||||
if( !opts.click || opts.xrf.string[0] != '#' ) return
|
if( !opts.click || opts.xrf.string[0] != '#' ) return
|
||||||
|
|
@ -2522,7 +1834,7 @@ xrf.addEventListener('dynamicKeyValue', (opts) => {
|
||||||
let {scene,match,v} = opts
|
let {scene,match,v} = opts
|
||||||
let objname = v.fragment
|
let objname = v.fragment
|
||||||
let autoscroll = v.z > 0 || v.w > 0
|
let autoscroll = v.z > 0 || v.w > 0
|
||||||
|
return // DISABLED
|
||||||
scene.traverse( (mesh) => {
|
scene.traverse( (mesh) => {
|
||||||
if( mesh.name == objname ){
|
if( mesh.name == objname ){
|
||||||
if( !mesh.geometry ) return console.warn(`mesh '${objname}' has no uvcoordinates to offset`)
|
if( !mesh.geometry ) return console.warn(`mesh '${objname}' has no uvcoordinates to offset`)
|
||||||
|
|
|
||||||
750
dist/xrfragment.three.module.js
vendored
750
dist/xrfragment.three.module.js
vendored
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* v0.5.1 generated at Fri Dec 15 07:00:04 PM CET 2023
|
* v0.5.1 generated at Sat Dec 30 09:46:42 PM UTC 2023
|
||||||
* https://xrfragment.org
|
* https://xrfragment.org
|
||||||
* SPDX-License-Identifier: MPL-2.0
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
*/
|
*/
|
||||||
|
|
@ -551,6 +551,15 @@ xrfragment_XRF.isDeep = new EReg("\\*","");
|
||||||
xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
|
xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$","");
|
||||||
})({});
|
})({});
|
||||||
var xrfragment = $hx_exports["xrfragment"];
|
var xrfragment = $hx_exports["xrfragment"];
|
||||||
|
// the core project uses #vanillajs #proxies #clean #noframework
|
||||||
|
$ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery
|
||||||
|
$$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc.
|
||||||
|
|
||||||
|
$el = (html,tag) => {
|
||||||
|
let el = document.createElement('div')
|
||||||
|
el.innerHTML = html
|
||||||
|
return el.children[0]
|
||||||
|
}
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
// Copyright (c) 2023 Leon van Kammen/NLNET
|
// Copyright (c) 2023 Leon van Kammen/NLNET
|
||||||
|
|
||||||
|
|
@ -602,713 +611,6 @@ xrf.hasTag = (tag,tags) => String(tags).match( new RegExp(`(^| )${tag}( |$)`,`g`
|
||||||
|
|
||||||
// map library functions to xrf
|
// map library functions to xrf
|
||||||
for ( let i in xrfragment ) xrf[i] = xrfragment[i]
|
for ( let i in xrfragment ) xrf[i] = xrfragment[i]
|
||||||
// handy shortcuts
|
|
||||||
if( !window.$ ) window.$ = (s) => s ? document.querySelector(s) : false
|
|
||||||
if( !window.$$ ) window.$$ = (s) => s ? [ ...document.querySelectorAll(s) ] : false
|
|
||||||
|
|
||||||
window.XRFMENU = {
|
|
||||||
|
|
||||||
logo: './../../assets/logo.png',
|
|
||||||
|
|
||||||
html: [
|
|
||||||
`<a class="btn" aria-label="button" aria-description="start text/audio/video chat" id="meeting" target="_blank">🧑🤝🧑 meeting</a><br>`,
|
|
||||||
`<a class="btn" aria-label="button" aria-description="share URL/screenshot/embed" id="share" target="_blank" onclick="window.share()">🔗 share</a><br>`
|
|
||||||
],
|
|
||||||
|
|
||||||
loadFile(contentLoaders, multiple){
|
|
||||||
return () => {
|
|
||||||
window.notify("if you're on Meta browser, file-uploads might be disabled")
|
|
||||||
let input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.multiple = multiple;
|
|
||||||
input.accept = Object.keys(contentLoaders).join(",");
|
|
||||||
input.onchange = () => {
|
|
||||||
let files = Array.from(input.files);
|
|
||||||
let file = files.slice ? files[0] : files
|
|
||||||
for( var i in contentLoaders ){
|
|
||||||
let r = new RegExp('\\'+i+'$')
|
|
||||||
if( file.name.match(r) ) return contentLoaders[i](file)
|
|
||||||
}
|
|
||||||
alert(file.name+" is not supported")
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
setupMenu(XRF){
|
|
||||||
let aScene = document.querySelector('a-scene')
|
|
||||||
let urlbar = $('input#uri')
|
|
||||||
let inIframe = window.location !== window.parent.location
|
|
||||||
let els = [ ...document.querySelectorAll('.menu .btn') ]
|
|
||||||
els = els.filter( (el) => el.id != "more" ? el : false )
|
|
||||||
|
|
||||||
let showMenu = (state) => {
|
|
||||||
els.map( (el) => el.style.display = state ? 'inline-block' : 'none' )
|
|
||||||
$('a#more').style.display = state ? 'none' : 'inline-block'
|
|
||||||
$('#overlay').style.display = state ? 'inline-block' : 'none'
|
|
||||||
if( inIframe ) $('#uri').style.display = 'block'
|
|
||||||
}
|
|
||||||
|
|
||||||
els.map( (el) => el.addEventListener('click', () => showMenu(false) ) )
|
|
||||||
$('a#more').addEventListener('click', () => showMenu(true) )
|
|
||||||
$('.a-canvas').addEventListener('click', () => showMenu(false) )
|
|
||||||
|
|
||||||
// enable meetings
|
|
||||||
let startMeeting = () => {
|
|
||||||
aScene.setAttribute('meeting', 'id: xrfragments')
|
|
||||||
$('a#meeting').innerText = '🧑🤝🧑 breakout meeting'
|
|
||||||
$('a#meeting').setAttribute('aria-description','breakout room')
|
|
||||||
}
|
|
||||||
$('a#meeting').addEventListener('click', () => {
|
|
||||||
if( aScene.getAttribute('meeting') ){ // meeting already, start breakout room
|
|
||||||
let parentRoom = document.location.href
|
|
||||||
XRFMENU.updateHashPosition(true)
|
|
||||||
let meeting = $('[meeting]').components['meeting']
|
|
||||||
meeting.data.parentRoom = parentRoom
|
|
||||||
meeting.update()
|
|
||||||
}else startMeeting()
|
|
||||||
})
|
|
||||||
if( document.location.hash.match(/(#|&)meet/) ) startMeeting()
|
|
||||||
|
|
||||||
XRF.addEventListener('hash', () => reflectUrl() )
|
|
||||||
const reflectUrl = window.reflectUrl = (url) => {
|
|
||||||
urlbar.value = url || document.location.search.substr(1) + document.location.hash
|
|
||||||
}
|
|
||||||
reflectUrl()
|
|
||||||
},
|
|
||||||
|
|
||||||
SnackBar(userOptions) {
|
|
||||||
var snackbar = this || (window.snackbar = {});
|
|
||||||
var _Interval;
|
|
||||||
var _Message;
|
|
||||||
var _Element;
|
|
||||||
var _Container;
|
|
||||||
|
|
||||||
var _OptionDefaults = {
|
|
||||||
message: "Operation performed successfully.",
|
|
||||||
dismissible: true,
|
|
||||||
timeout: 7000,
|
|
||||||
status: ""
|
|
||||||
}
|
|
||||||
var _Options = _OptionDefaults;
|
|
||||||
|
|
||||||
function _Create() {
|
|
||||||
_Container = document.querySelector(".js-snackbar-container")
|
|
||||||
if( _Container ){
|
|
||||||
_Container.remove()
|
|
||||||
}
|
|
||||||
_Container = null
|
|
||||||
|
|
||||||
if (!_Container) {
|
|
||||||
// need to create a new container for notifications
|
|
||||||
_Container = document.createElement("div");
|
|
||||||
_Container.classList.add("js-snackbar-container");
|
|
||||||
|
|
||||||
document.body.appendChild(_Container);
|
|
||||||
}
|
|
||||||
_Container.opts = _Options
|
|
||||||
_Container.innerHTML = ''
|
|
||||||
_Element = document.createElement("div");
|
|
||||||
_Element.classList.add("js-snackbar__wrapper","xrf");
|
|
||||||
|
|
||||||
let innerSnack = document.createElement("div");
|
|
||||||
innerSnack.classList.add("js-snackbar", "js-snackbar--show");
|
|
||||||
|
|
||||||
if (_Options.status) {
|
|
||||||
_Options.status = _Options.status.toLowerCase().trim();
|
|
||||||
|
|
||||||
let status = document.createElement("span");
|
|
||||||
status.classList.add("js-snackbar__status");
|
|
||||||
|
|
||||||
|
|
||||||
if (_Options.status === "success" || _Options.status === "green") {
|
|
||||||
status.classList.add("js-snackbar--success");
|
|
||||||
}
|
|
||||||
else if (_Options.status === "warning" || _Options.status === "alert" || _Options.status === "orange") {
|
|
||||||
status.classList.add("js-snackbar--warning");
|
|
||||||
}
|
|
||||||
else if (_Options.status === "danger" || _Options.status === "error" || _Options.status === "red") {
|
|
||||||
status.classList.add("js-snackbar--danger");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
status.classList.add("js-snackbar--info");
|
|
||||||
}
|
|
||||||
|
|
||||||
innerSnack.appendChild(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
_Message = document.createElement("span");
|
|
||||||
_Message.classList.add("js-snackbar__message");
|
|
||||||
if( typeof _Options.message == 'string' ){
|
|
||||||
_Message.innerHTML = _Options.message;
|
|
||||||
}else _Message.appendChild(_Options.message)
|
|
||||||
|
|
||||||
innerSnack.appendChild(_Message);
|
|
||||||
|
|
||||||
if (_Options.dismissible) {
|
|
||||||
let closeBtn = document.createElement("span");
|
|
||||||
closeBtn.classList.add("js-snackbar__close");
|
|
||||||
closeBtn.innerText = "\u00D7";
|
|
||||||
|
|
||||||
closeBtn.onclick = snackbar.Close;
|
|
||||||
|
|
||||||
innerSnack.appendChild(closeBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
_Element.style.height = "0px";
|
|
||||||
_Element.style.opacity = "0";
|
|
||||||
_Element.style.marginTop = "0px";
|
|
||||||
_Element.style.marginBottom = "0px";
|
|
||||||
|
|
||||||
_Element.appendChild(innerSnack);
|
|
||||||
_Container.appendChild(_Element);
|
|
||||||
|
|
||||||
if (_Options.timeout !== false) {
|
|
||||||
_Interval = setTimeout(snackbar.Close, _Options.timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
snackbar.Open = function() {
|
|
||||||
let contentHeight = _Element.firstElementChild.scrollHeight; // get the height of the content
|
|
||||||
|
|
||||||
_Element.style.height = contentHeight + "px";
|
|
||||||
_Element.style.opacity = 1;
|
|
||||||
_Element.style.marginTop = "5px";
|
|
||||||
_Element.style.marginBottom = "5px";
|
|
||||||
|
|
||||||
_Element.addEventListener("transitioned", function() {
|
|
||||||
_Element.removeEventListener("transitioned", arguments.callee);
|
|
||||||
_Element.style.height = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
snackbar.Close = function () {
|
|
||||||
if (_Interval)
|
|
||||||
clearInterval(_Interval);
|
|
||||||
|
|
||||||
let snackbarHeight = _Element.scrollHeight; // get the auto height as a px value
|
|
||||||
let snackbarTransitions = _Element.style.transition;
|
|
||||||
_Element.style.transition = "";
|
|
||||||
|
|
||||||
requestAnimationFrame(function() {
|
|
||||||
_Element.style.height = snackbarHeight + "px"; // set the auto height to the px height
|
|
||||||
_Element.style.opacity = 1;
|
|
||||||
_Element.style.marginTop = "0px";
|
|
||||||
_Element.style.marginBottom = "0px";
|
|
||||||
_Element.style.transition = snackbarTransitions
|
|
||||||
|
|
||||||
requestAnimationFrame(function() {
|
|
||||||
_Element.style.height = "0px";
|
|
||||||
_Element.style.opacity = 0;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
try {
|
|
||||||
_Container.removeChild(_Element);
|
|
||||||
} catch (e) { }
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
_Options = { ..._OptionDefaults, ...userOptions }
|
|
||||||
_Create();
|
|
||||||
snackbar.Open();
|
|
||||||
},
|
|
||||||
|
|
||||||
notify(scope){
|
|
||||||
return function notify(str,opts){
|
|
||||||
opts = opts || {status:'info'}
|
|
||||||
opts = Object.assign({ status, timeout:4000 },opts)
|
|
||||||
if( typeof str == 'string' ){
|
|
||||||
if( !opts.status ){
|
|
||||||
if( str.match(/error/g) ) opts.status = "danger"
|
|
||||||
if( str.match(/warning/g) ) opts.status = "warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opts.message = str
|
|
||||||
window.XRFMENU.SnackBar( opts )
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
download(){
|
|
||||||
function fetchAndDownload(dataurl, filename) {
|
|
||||||
var a = document.createElement("a");
|
|
||||||
a.href = dataurl;
|
|
||||||
a.setAttribute("download", filename);
|
|
||||||
a.click();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let file = document.location.search.replace(/\?/,'')
|
|
||||||
fetchAndDownload( file, file )
|
|
||||||
},
|
|
||||||
|
|
||||||
updateHashPosition(randomize){
|
|
||||||
// *TODO* this should be part of the XRF Threejs framework
|
|
||||||
if( typeof THREE == 'undefined' ) THREE = xrf.THREE
|
|
||||||
let radToDeg = THREE.MathUtils.radToDeg
|
|
||||||
let toDeg = (x) => x / (Math.PI / 180)
|
|
||||||
let camera = document.querySelector('[camera]').object3D.parent // *TODO* fix for threejs
|
|
||||||
camera.position.x += Math.random()/10
|
|
||||||
camera.position.z += Math.random()/10
|
|
||||||
|
|
||||||
// *TODO* add camera direction
|
|
||||||
let direction = new xrf.THREE.Vector3()
|
|
||||||
camera.getWorldDirection(direction)
|
|
||||||
const pitch = Math.asin(direction.y);
|
|
||||||
const yaw = Math.atan2(direction.x, direction.z);
|
|
||||||
const pitchInDegrees = pitch * 180 / Math.PI;
|
|
||||||
const yawInDegrees = yaw * 180 / Math.PI;
|
|
||||||
|
|
||||||
let lastPos = `pos=${camera.position.x.toFixed(2)},${camera.position.y.toFixed(2)},${camera.position.z.toFixed(2)}`
|
|
||||||
let newHash = document.location.hash.replace(/[&]?(pos|rot)=[0-9\.-]+,[0-9\.-]+,[0-9\.-]+/,'')
|
|
||||||
newHash += `&${lastPos}`
|
|
||||||
document.location.hash = newHash.replace(/&&/,'&')
|
|
||||||
.replace(/#&/,'')
|
|
||||||
XRFMENU.copyToClipboard( window.location.href );
|
|
||||||
},
|
|
||||||
|
|
||||||
copyToClipboard(text){
|
|
||||||
// copy url to clipboard
|
|
||||||
var dummy = document.createElement('input')
|
|
||||||
document.body.appendChild(dummy);
|
|
||||||
dummy.value = text;
|
|
||||||
dummy.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(dummy);
|
|
||||||
},
|
|
||||||
|
|
||||||
share(){
|
|
||||||
let inMeeting = $('[meeting]')
|
|
||||||
let url = window.location.href
|
|
||||||
if( !inMeeting ) XRFMENU.updateHashPosition()
|
|
||||||
else url = $('[meeting]').components['meeting'].data.link
|
|
||||||
XRFMENU.copyToClipboard( url )
|
|
||||||
// End of *TODO*
|
|
||||||
window.notify(`<h2>${ inMeeting ? 'Meeting link ' : 'Link'} copied to clipboard!</h2> <br>Now share it with your friends ❤️<br>
|
|
||||||
<canvas id="qrcode" width="121" height="121"></canvas><br>
|
|
||||||
<button onclick="window.download()">💾 download scene file</button> <br>
|
|
||||||
<button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')">📷 download 360 screenshot</button> <br>
|
|
||||||
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld">🖥 clone & selfhost this experience</a><br>
|
|
||||||
<br>
|
|
||||||
To embed this experience in your blog,<br>
|
|
||||||
copy/paste the following into your HTML:<br><input type="text" value="<iframe src='${document.location.href}'><br></iframe>" id="share"/>
|
|
||||||
<br>
|
|
||||||
`,{timeout:2000000})
|
|
||||||
// draw QR code
|
|
||||||
setTimeout( () => {
|
|
||||||
let QR = window.QR
|
|
||||||
QR.canvas = document.getElementById('qrcode')
|
|
||||||
QR.draw( url, QR.canvas )
|
|
||||||
},0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()"><</button>
|
|
||||||
<button id="navforward" onclick="history.forward()">></button>
|
|
||||||
<input type="submit" value="load 3D file"></input>
|
|
||||||
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )" style="display:none"/>
|
|
||||||
</div>
|
|
||||||
<!-- open AFRAME inspector: $('a-scene').components.inspector.openInspector() -->
|
|
||||||
<div class="xrf footer">
|
|
||||||
<div id="buttons" class="menu">
|
|
||||||
${window.XRFMENU.html.map( (html) => typeof html == "function" ? html() : html ).join('\n')}
|
|
||||||
<a class="btn" id="more" style="display:inline-block">${window.XRFMENU.morelabel}</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
document.body.appendChild(el)
|
|
||||||
|
|
||||||
if( XRFMENU.logo ) $('.logo').style['background-image'] = `url(${XRFMENU.logo})`
|
|
||||||
|
|
||||||
window.notify = XRFMENU.notify(window)
|
|
||||||
window.share = XRFMENU.share
|
|
||||||
window.download = XRFMENU.download
|
|
||||||
window.notify('loading '+document.location.search.substr(1))
|
|
||||||
// reroute console messages to snackbar notifications
|
|
||||||
console.log = ( (log) => function(str){
|
|
||||||
if( String(str).match(/(:.*#|note:)/) ) window.notify(str)
|
|
||||||
log(str)
|
|
||||||
})(console.log)
|
|
||||||
// allow iframe to open url
|
|
||||||
window.addEventListener('message', (event) => {
|
|
||||||
if (event.data && event.data.url) {
|
|
||||||
window.open(event.data.url, '_blank');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
/*
|
/*
|
||||||
* (promise-able) EVENTS (optionally continue after listeners are finished using .then)
|
* (promise-able) EVENTS (optionally continue after listeners are finished using .then)
|
||||||
*
|
*
|
||||||
|
|
@ -1360,7 +662,9 @@ xrf.emit.normal = function(eventName, data) {
|
||||||
var callbacks = xrf._listeners[eventName]
|
var callbacks = xrf._listeners[eventName]
|
||||||
if (callbacks) {
|
if (callbacks) {
|
||||||
for (var i = 0; i < callbacks.length; i++) {
|
for (var i = 0; i < callbacks.length; i++) {
|
||||||
|
try{
|
||||||
callbacks[i](data);
|
callbacks[i](data);
|
||||||
|
}catch(e){ console.error(e) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1562,6 +866,7 @@ xrf.init = ((init) => function(opts){
|
||||||
xrf.navigator.init()
|
xrf.navigator.init()
|
||||||
// return xrfragment lib as 'xrf' query functor (like jquery)
|
// return xrfragment lib as 'xrf' query functor (like jquery)
|
||||||
for ( let i in xrf ) xrf.query[i] = xrf[i]
|
for ( let i in xrf ) xrf.query[i] = xrf[i]
|
||||||
|
|
||||||
return xrf.query
|
return xrf.query
|
||||||
})(xrf.init)
|
})(xrf.init)
|
||||||
|
|
||||||
|
|
@ -1643,12 +948,21 @@ xrf.reset = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
xrf.parseUrl = (url) => {
|
xrf.parseUrl = (url) => {
|
||||||
const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
|
let urlExHash = url.replace(/#.*/,'')
|
||||||
|
let urlObj,file
|
||||||
|
let store = {}
|
||||||
|
try{
|
||||||
|
urlObj = new URL( urlExHash.match(/:\/\//) ? urlExHash : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
|
||||||
|
file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
|
||||||
|
let search = urlObj.search.substr(1).split("&")
|
||||||
|
for( let i in search ) store[ (search[i].split("=")[0]) ] = search[i].split("=")[1] || ''
|
||||||
|
}catch(e){ }
|
||||||
|
let hashmap = url.match("#") ? url.replace(/.*#/,'').split("&") : []
|
||||||
|
for( let i in hashmap ) store[ (hashmap[i].split("=")[0]) ] = hashmap[i].split("=")[1] || ''
|
||||||
let dir = url.substring(0, url.lastIndexOf('/') + 1)
|
let dir = url.substring(0, url.lastIndexOf('/') + 1)
|
||||||
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
|
|
||||||
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
|
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
|
||||||
const ext = file.split('.').pop()
|
const ext = file.split('.').pop()
|
||||||
return {urlObj,dir,file,hash,ext}
|
return {urlObj,dir,file,hash,ext,store}
|
||||||
}
|
}
|
||||||
|
|
||||||
xrf.add = (object) => {
|
xrf.add = (object) => {
|
||||||
|
|
@ -1672,7 +986,8 @@ xrf.navigator.to = (url,flags,loader,data) => {
|
||||||
return new Promise( (resolve,reject) => {
|
return new Promise( (resolve,reject) => {
|
||||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
||||||
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
|
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
|
||||||
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
|
if( hash == document.location.hash.substr(1) ) return // block duplicate calls
|
||||||
|
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
|
||||||
xrf.navigator.updateHash(hash)
|
xrf.navigator.updateHash(hash)
|
||||||
return resolve(xrf.model)
|
return resolve(xrf.model)
|
||||||
}
|
}
|
||||||
|
|
@ -1735,7 +1050,6 @@ xrf.navigator.updateHash = (hash,opts) => {
|
||||||
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
|
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
|
||||||
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
|
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
|
||||||
document.location.hash = hash
|
document.location.hash = hash
|
||||||
xrf.emit('hash', {...opts, hash: `#${hash}` })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xrf.navigator.pushState = (file,hash) => {
|
xrf.navigator.pushState = (file,hash) => {
|
||||||
|
|
@ -1808,7 +1122,9 @@ xrf.frag.href = function(v, opts){
|
||||||
//}
|
//}
|
||||||
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
|
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
|
||||||
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
|
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
|
||||||
// always commit current location (keep a trail of last positions before we navigate)
|
// *TODO* support for multiple protocols
|
||||||
|
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
|
||||||
|
// always commit current location in case of teleport (keep a trail of last positions before we navigate)
|
||||||
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
|
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
|
||||||
xrf.navigator.to(v.string) // let's surf to HREF!
|
xrf.navigator.to(v.string) // let's surf to HREF!
|
||||||
})
|
})
|
||||||
|
|
@ -1967,7 +1283,6 @@ xrf.frag.src.enableSourcePortation = (src) => {
|
||||||
xrf.frag.src.externalSRC = (url,frag,opts) => {
|
xrf.frag.src.externalSRC = (url,frag,opts) => {
|
||||||
fetch(url, { method: 'HEAD' })
|
fetch(url, { method: 'HEAD' })
|
||||||
.then( (res) => {
|
.then( (res) => {
|
||||||
console.log(`loading src ${url}`)
|
|
||||||
let mimetype = res.headers.get('Content-type')
|
let mimetype = res.headers.get('Content-type')
|
||||||
if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf'
|
if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf'
|
||||||
//if( url.match(/\.(fbx|stl|obj)$/) ) mimetype =
|
//if( url.match(/\.(fbx|stl|obj)$/) ) mimetype =
|
||||||
|
|
@ -2510,9 +1825,6 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// react to enduser typing url
|
|
||||||
xrf.addEventListener('hash', (opts) => xrf.hashbus.pub( opts.hash ) )
|
|
||||||
|
|
||||||
// clicking href url with predefined view
|
// clicking href url with predefined view
|
||||||
xrf.addEventListener('href', (opts) => {
|
xrf.addEventListener('href', (opts) => {
|
||||||
if( !opts.click || opts.xrf.string[0] != '#' ) return
|
if( !opts.click || opts.xrf.string[0] != '#' ) return
|
||||||
|
|
@ -2522,7 +1834,7 @@ xrf.addEventListener('dynamicKeyValue', (opts) => {
|
||||||
let {scene,match,v} = opts
|
let {scene,match,v} = opts
|
||||||
let objname = v.fragment
|
let objname = v.fragment
|
||||||
let autoscroll = v.z > 0 || v.w > 0
|
let autoscroll = v.z > 0 || v.w > 0
|
||||||
|
return // DISABLED
|
||||||
scene.traverse( (mesh) => {
|
scene.traverse( (mesh) => {
|
||||||
if( mesh.name == objname ){
|
if( mesh.name == objname ){
|
||||||
if( !mesh.geometry ) return console.warn(`mesh '${objname}' has no uvcoordinates to offset`)
|
if( !mesh.geometry ) return console.warn(`mesh '${objname}' has no uvcoordinates to offset`)
|
||||||
|
|
|
||||||
9
make
9
make
|
|
@ -62,6 +62,9 @@ build(){
|
||||||
}
|
}
|
||||||
|
|
||||||
js(){
|
js(){
|
||||||
|
|
||||||
|
jscat(){ echo "(function(){"; cat "$@"; echo "}).apply({})"; }
|
||||||
|
|
||||||
# add js module
|
# add js module
|
||||||
cat dist/xrfragment.js >> dist/xrfragment.module.js
|
cat dist/xrfragment.js >> dist/xrfragment.module.js
|
||||||
echo "export default xrfragment;" >> dist/xrfragment.module.js
|
echo "export default xrfragment;" >> dist/xrfragment.module.js
|
||||||
|
|
@ -86,9 +89,9 @@ build(){
|
||||||
example/assets/js/qr.js > dist/xrfragment.aframe.js
|
example/assets/js/qr.js > dist/xrfragment.aframe.js
|
||||||
|
|
||||||
# plugins
|
# plugins
|
||||||
cat src/3rd/js/plugin/frontend/*.js > dist/xrfragment.plugin.frontend.js
|
jscat src/3rd/js/plugin/frontend/*.js > dist/xrfragment.plugin.frontend.js
|
||||||
cat src/3rd/js/plugin/matrix/{matrix-crdt,matrix}.js > dist/xrfragment.plugin.matrix.js
|
jscat src/3rd/js/plugin/matrix/{matrix-crdt,matrix}.js > dist/xrfragment.plugin.matrix.js
|
||||||
cat src/3rd/js/plugin/p2p/{trystero-torrent.min,trystero}.js > dist/xrfragment.plugin.p2p.js
|
jscat src/3rd/js/plugin/p2p/{trystero-torrent.min,trystero}.js > dist/xrfragment.plugin.p2p.js
|
||||||
|
|
||||||
# fat all-in-one standalone xrf release
|
# fat all-in-one standalone xrf release
|
||||||
test -f dist/aframe.min.js || {
|
test -f dist/aframe.min.js || {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
haxe
|
haxe
|
||||||
mmark
|
mmark
|
||||||
xml2rfc
|
xml2rfc
|
||||||
dendrite
|
esbuild
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,15 @@ chatComponent = {
|
||||||
|
|
||||||
init: (el) => new Proxy({
|
init: (el) => new Proxy({
|
||||||
|
|
||||||
scene: null,
|
scene: null,
|
||||||
visible: true,
|
visible: true,
|
||||||
messages: [],
|
visibleChatbar: false,
|
||||||
|
messages: [],
|
||||||
|
|
||||||
$messages: $messages = el.querySelector("#messages"),
|
$videos: el.querySelector("#videos"),
|
||||||
$chatline: $chatline = el.querySelector("#chatline"),
|
$messages: el.querySelector("#messages"),
|
||||||
|
$chatline: el.querySelector("#chatline"),
|
||||||
|
$chatbar: el.querySelector("#chatbar"),
|
||||||
|
|
||||||
install(opts){
|
install(opts){
|
||||||
this.opts = opts
|
this.opts = opts
|
||||||
|
|
@ -27,15 +30,18 @@ chatComponent = {
|
||||||
el.className = "xrf"
|
el.className = "xrf"
|
||||||
el.style.display = 'none' // start hidden
|
el.style.display = 'none' // start hidden
|
||||||
document.body.appendChild( el )
|
document.body.appendChild( el )
|
||||||
|
this.visibleChatbar = false
|
||||||
document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) )
|
document.dispatchEvent( new CustomEvent("$chat:ready", {detail: opts}) )
|
||||||
$chat.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br>`, class: ["info","multiline"] })
|
this.send({message:`Welcome to <b>${document.location.search.substr(1)}</b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br>`, class: ["info","guide","multiline"] })
|
||||||
},
|
},
|
||||||
|
|
||||||
initListeners(){
|
initListeners(){
|
||||||
//opts.scene.addEventListener('meeting.peer.add', () => console.log("$chat.peer.add") )
|
let {$chatline} = this
|
||||||
//opts.scene.addEventListener('meeting.peer.remove', () => console.log("$chat.peer.remove") )
|
|
||||||
$chatline.addEventListener('keydown', (e) => {
|
$chatline.addEventListener('keydown', (e) => {
|
||||||
if (e.key == 'Enter' ){
|
if (e.key == 'Enter' ){
|
||||||
|
if( $chatline.value[0] != '/' ){
|
||||||
|
document.dispatchEvent( new CustomEvent("network.send", {detail: {message:$chatline.value}} ) )
|
||||||
|
}
|
||||||
this.send({message: $chatline.value })
|
this.send({message: $chatline.value })
|
||||||
$chatline.value = ''
|
$chatline.value = ''
|
||||||
}
|
}
|
||||||
|
|
@ -49,10 +55,13 @@ chatComponent = {
|
||||||
},
|
},
|
||||||
|
|
||||||
send(opts){
|
send(opts){
|
||||||
|
let {$messages} = this
|
||||||
opts = { linebreak:true, message:"", class:[], ...opts }
|
opts = { linebreak:true, message:"", class:[], ...opts }
|
||||||
let div = document.createElement('div')
|
if( window.frontend && window.frontend.emit ) window.frontend.emit('$chat.send', opts )
|
||||||
let msg = document.createElement('div')
|
let div = document.createElement('div')
|
||||||
let br = document.createElement('br')
|
let msg = document.createElement('div')
|
||||||
|
let br = document.createElement('br')
|
||||||
|
let nick = document.createElement('div')
|
||||||
msg.className = "msg"
|
msg.className = "msg"
|
||||||
let html = `${ opts.message || ''}${ opts.html ? opts.html(opts) : ''}`
|
let html = `${ opts.message || ''}${ opts.html ? opts.html(opts) : ''}`
|
||||||
if( $messages.last == html ) return
|
if( $messages.last == html ) return
|
||||||
|
|
@ -62,27 +71,47 @@ chatComponent = {
|
||||||
if( opts.class ){
|
if( opts.class ){
|
||||||
msg.classList.add.apply(msg.classList, opts.class)
|
msg.classList.add.apply(msg.classList, opts.class)
|
||||||
br.classList.add.apply(br.classList, opts.class)
|
br.classList.add.apply(br.classList, opts.class)
|
||||||
|
div.classList.add.apply(div.classList, opts.class.concat(["envelope"]))
|
||||||
|
}
|
||||||
|
if( !opts.from && !msg.className.match(/(info|guide)/) ) msg.classList.add('self')
|
||||||
|
if( opts.from ){
|
||||||
|
nick.className = "user"
|
||||||
|
nick.innerText = opts.from+' '
|
||||||
|
div.appendChild(nick)
|
||||||
|
if( opts.pos ){
|
||||||
|
let a = document.createElement("a")
|
||||||
|
a.href = a.innerText = `#pos=${opts.pos}`
|
||||||
|
nick.appendChild(a)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
div.appendChild(msg)
|
div.appendChild(msg)
|
||||||
$messages.appendChild(div)
|
$messages.appendChild(div)
|
||||||
if( opts.linebreak ) div.appendChild(br)
|
if( opts.linebreak ) div.appendChild(br)
|
||||||
$messages.scrollTop = $messages.scrollHeight // scroll down
|
$messages.scrollTop = $messages.scrollHeight // scroll down
|
||||||
document.dispatchEvent( new CustomEvent("$chat:receive", {detail: opts}) )
|
|
||||||
$messages.last = msg.innerHTML
|
$messages.last = msg.innerHTML
|
||||||
|
},
|
||||||
|
|
||||||
|
getChatLog(){
|
||||||
|
return ([...this.$messages.querySelectorAll('.envelope')])
|
||||||
|
.filter( (d) => !d.className.match(/(info|ui)/) )
|
||||||
|
.map( (d) => d.innerHTML )
|
||||||
|
.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
},{
|
},{
|
||||||
|
|
||||||
get(data,k,v){ return data[k] },
|
get(me,k,v){ return me[k] },
|
||||||
set(data,k,v){
|
set(me,k,v){
|
||||||
data[k] = v
|
me[k] = v
|
||||||
switch( k ){
|
switch( k ){
|
||||||
case "visible": {
|
case "visible": {
|
||||||
el.style.display = data.visible ? 'block' : 'none'
|
el.style.display = me.visible ? 'block' : 'none'
|
||||||
if( !el.inited && (el.inited = true) ) data.initListeners()
|
if( !el.inited && (el.inited = true) ) me.initListeners()
|
||||||
$menu.collapsed = !data.visible
|
break;
|
||||||
break;
|
}
|
||||||
}
|
case "visibleChatbar": {
|
||||||
|
me.$chatbar.style.display = v ? 'block' : 'none'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +122,7 @@ chatComponent = {
|
||||||
document.addEventListener('$menu:ready', (opts) => {
|
document.addEventListener('$menu:ready', (opts) => {
|
||||||
opts = opts.detail
|
opts = opts.detail
|
||||||
document.head.innerHTML += chatComponent.css
|
document.head.innerHTML += chatComponent.css
|
||||||
$chat = document.createElement('div')
|
window.$chat = document.createElement('div')
|
||||||
$chat.innerHTML = chatComponent.html
|
$chat.innerHTML = chatComponent.html
|
||||||
$chat = chatComponent.init($chat)
|
$chat = chatComponent.init($chat)
|
||||||
$chat.install(opts)
|
$chat.install(opts)
|
||||||
|
|
@ -167,29 +196,78 @@ chatComponent.css = `
|
||||||
}
|
}
|
||||||
#messages{
|
#messages{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100px;
|
transition:1s;
|
||||||
|
top: 0px;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
bottom: 130px;
|
||||||
bottom: 88px;
|
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
pointer-events: none;
|
overflow:hidden;
|
||||||
overflow-y:auto;
|
pointer-events:none;
|
||||||
|
transition:1s;
|
||||||
|
width: 91%;
|
||||||
|
max-width: 500px;
|
||||||
|
z-index: 100;
|
||||||
|
-webkit-user-select:none;
|
||||||
|
-moz-user-select:-moz-none;
|
||||||
|
-ms-user-select:none;
|
||||||
|
user-select:none;
|
||||||
|
}
|
||||||
|
body.menu #messages{
|
||||||
|
top:50px;
|
||||||
|
}
|
||||||
|
#messages *{
|
||||||
|
pointer-events:all;
|
||||||
}
|
}
|
||||||
#messages .msg{
|
#messages .msg{
|
||||||
|
transition:all 1s ease;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 6px 17px;
|
padding: 1px 17px;
|
||||||
border-radius: 20px;
|
border-radius: 20px 0px 20px 20px;
|
||||||
color: #000c;
|
color: #000c;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
line-height:23px;
|
line-height:23px;
|
||||||
pointer-events:visible;
|
pointer-events:visible;
|
||||||
border: 1px solid #ccc8;
|
|
||||||
line-height:33px;
|
line-height:33px;
|
||||||
|
cursor:grabbing;
|
||||||
|
border: 1px solid #0002;
|
||||||
|
}
|
||||||
|
#messages .msg.self{
|
||||||
|
border-radius: 0px 20px 20px 20px;
|
||||||
|
background:var(--xrf-primary);
|
||||||
|
}
|
||||||
|
#messages .msg.self,
|
||||||
|
#messages .msg.self div{
|
||||||
|
color:#FFF;
|
||||||
}
|
}
|
||||||
#messages .msg.info{
|
#messages .msg.info{
|
||||||
font-size: 14px;
|
background: #473f7f;
|
||||||
padding: 3px 16px;
|
border-radius: 20px;
|
||||||
|
color: #FFF;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 19px;
|
||||||
|
}
|
||||||
|
#messages .msg.info,
|
||||||
|
#messages .msg.info *{
|
||||||
|
font-size: var(--xrf-font-size-0);
|
||||||
|
}
|
||||||
|
#messages .msg a {
|
||||||
|
text-decoration:underline;
|
||||||
|
color: #EEE;
|
||||||
|
font-weight:bold;
|
||||||
|
transition:1s;
|
||||||
|
}
|
||||||
|
#messages .msg a:hover{
|
||||||
|
color:#FFF;
|
||||||
|
}
|
||||||
|
#messages .msg.ui,
|
||||||
|
#messages .msg.ui div{
|
||||||
|
background: white;
|
||||||
|
border:none;
|
||||||
|
color: #333;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin:0;
|
||||||
|
padding:0px 5px 5px 5px;
|
||||||
}
|
}
|
||||||
#messages.guide, .guide{
|
#messages.guide, .guide{
|
||||||
display:unset;
|
display:unset;
|
||||||
|
|
@ -200,12 +278,6 @@ chatComponent.css = `
|
||||||
br.guide{
|
br.guide{
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
}
|
}
|
||||||
#messages .msg.info a,
|
|
||||||
#messages .msg.info a:visited{
|
|
||||||
color: var(--xrf-primary);
|
|
||||||
text-decoration: underline;
|
|
||||||
transition:0.3s;
|
|
||||||
}
|
|
||||||
#messages .msg.info a:hover,
|
#messages .msg.info a:hover,
|
||||||
#messages button:hover{
|
#messages button:hover{
|
||||||
filter: brightness(1.4);
|
filter: brightness(1.4);
|
||||||
|
|
@ -213,9 +285,6 @@ chatComponent.css = `
|
||||||
#messages .msg.multiline {
|
#messages .msg.multiline {
|
||||||
padding: 2px 14px;
|
padding: 2px 14px;
|
||||||
}
|
}
|
||||||
#messages .msg.config {
|
|
||||||
background:#
|
|
||||||
}
|
|
||||||
#messages button {
|
#messages button {
|
||||||
text-decoration:none;
|
text-decoration:none;
|
||||||
margin: 0px 15px 10px 0px;
|
margin: 0px 15px 10px 0px;
|
||||||
|
|
@ -256,4 +325,19 @@ background:#
|
||||||
.nomargin{
|
.nomargin{
|
||||||
margin:0;
|
margin:0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.envelope{
|
||||||
|
height:unset;
|
||||||
|
overflow:hidden;
|
||||||
|
transition:1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user{
|
||||||
|
margin-left:13px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--xrf-dark-gray);
|
||||||
|
}
|
||||||
|
.user, .user *{
|
||||||
|
font-size: var(--xrf-font-size-0);
|
||||||
|
}
|
||||||
</style>`
|
</style>`
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,62 @@ connectionsComponent = {
|
||||||
|
|
||||||
html: `
|
html: `
|
||||||
<div id="connections">
|
<div id="connections">
|
||||||
<h2>Connection layers:</h2>
|
<i class="gg-close-o" id="close" onclick="$connections.toggle()"></i>
|
||||||
|
<div id="networking">
|
||||||
|
<h2>Network channels:</h2>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Webcam</td>
|
||||||
|
<td>
|
||||||
|
<select id="webcam"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Chat</td>
|
||||||
|
<td>
|
||||||
|
<select id="chatnetwork"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>World sync</td>
|
||||||
|
<td>
|
||||||
|
<select id="scene"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="devices">
|
||||||
|
<a class="badge ruler">Webcam</a>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>Video</td>
|
||||||
|
<td>
|
||||||
|
<select id="videoInput"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Mic</td>
|
||||||
|
<td>
|
||||||
|
<select id="audioInput"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="display:none"> <!-- not used (for now) -->
|
||||||
|
<td>Audio</td>
|
||||||
|
<td>
|
||||||
|
<select id="audioOutput"></select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div id="settings"></div>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Webcam/Audio</td>
|
<td></td>
|
||||||
<td>
|
<td>
|
||||||
<select id="webcam"></select>
|
<button id="connect" onclick="network.connect( $connections )">📡 Connect!</button>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Chat</td>
|
|
||||||
<td>
|
|
||||||
<select id="chatnetwork"></select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>Scene</td>
|
|
||||||
<td>
|
|
||||||
<select id="scene"></select>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<div id="settings"></div>
|
|
||||||
<br>
|
|
||||||
<button id="connect" onclick="$connections.connect()">📡 Connect!</button>
|
|
||||||
<br><br>
|
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
||||||
|
|
@ -44,40 +75,54 @@ connectionsComponent = {
|
||||||
$chatnetwork: $chatnetwork = el.querySelector("#chatnetwork"),
|
$chatnetwork: $chatnetwork = el.querySelector("#chatnetwork"),
|
||||||
$scene: $scene = el.querySelector("#scene"),
|
$scene: $scene = el.querySelector("#scene"),
|
||||||
$settings: $settings = el.querySelector("#settings"),
|
$settings: $settings = el.querySelector("#settings"),
|
||||||
$connect: $connect = el.querySelector("#connect"),
|
$devices: $devices = el.querySelector("#devices"),
|
||||||
|
$connect: $connect = el.querySelector("#connect"),
|
||||||
|
$networking: $networking = el.querySelector("#networking"),
|
||||||
|
|
||||||
|
$audioInput: el.querySelector('select#audioInput'),
|
||||||
|
$audioOutput: el.querySelector('select#audioOutput'),
|
||||||
|
$videoInput: el.querySelector('select#videoInput'),
|
||||||
|
|
||||||
install(opts){
|
install(opts){
|
||||||
this.opts = opts
|
this.opts = opts;
|
||||||
document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) )
|
(['change']).map( (e) => el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
|
||||||
|
this.reactToNetwork()
|
||||||
|
$menu.buttons = ([
|
||||||
|
`<a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick="$connections.show()"><i class="gg-user-add"></i> connect</a><br>`
|
||||||
|
]).concat($menu.buttons)
|
||||||
|
|
||||||
$webcam.addEventListener('change', () => this.renderSettings() )
|
// hide networking settings if entering thru meetinglink
|
||||||
$chatnetwork.addEventListener('change', () => this.renderSettings() )
|
if( document.location.href.match(/meet=/) ) this.show()
|
||||||
$scene.addEventListener('change', () => this.renderSettings() )
|
|
||||||
|
|
||||||
|
setTimeout( () => document.dispatchEvent( new CustomEvent("$connections:ready", {detail: opts}) ), 1 )
|
||||||
|
},
|
||||||
|
|
||||||
|
toggle(){
|
||||||
|
let parent = el.closest('.envelope')
|
||||||
|
parent.style.display = parent.style.display == 'none' ? parent.style.display = '' : 'none'
|
||||||
|
},
|
||||||
|
|
||||||
|
change(id,e){
|
||||||
|
if( id.match(/^(webcam|chatnetwork|scene)$/) ){
|
||||||
|
this.renderSettings() // trigger this when 'change' event fires on children dom elements
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
show(){
|
show(){
|
||||||
$chat.visible = true
|
$chat.visible = true
|
||||||
|
$networking.style.display = document.location.href.match(/meet=/) ? 'none' : 'block'
|
||||||
if( !network.connected ){
|
if( !network.connected ){
|
||||||
if( el.parentElement ) el.parentElement.parentElement.remove()
|
if( el.parentElement ) el.parentElement.parentElement.remove()
|
||||||
$chat.send({message:"", el})
|
$chat.send({message:"", el, class:['ui']})
|
||||||
this.renderSettings()
|
|
||||||
if( !network.meetinglink ){ // set default
|
if( !network.meetinglink ){ // set default
|
||||||
$webcam.value = 'Peer2Peer'
|
$webcam.value = 'Peer2Peer'
|
||||||
$chatnetwork.value = 'Peer2Peer'
|
$chatnetwork.value = 'Peer2Peer'
|
||||||
$scene.value = 'Peer2Peer'
|
$scene.value = 'Peer2Peer'
|
||||||
}
|
}
|
||||||
}else $chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
|
this.renderSettings()
|
||||||
},
|
}else{
|
||||||
|
$chat.send({message:"you are already connected, refresh page to create new connection",class:['info']})
|
||||||
connect(){
|
}
|
||||||
this.update()
|
|
||||||
this.webcam.selected.connect({webcam:true})
|
|
||||||
this.chatnetwork.selected.connect({chat:true})
|
|
||||||
this.scene.selected.connect({scene:true})
|
|
||||||
this.$connect.setAttribute('disabled','disabled')
|
|
||||||
this.$connect.classList.add('disabled')
|
|
||||||
window.notify("🪐 connecting to awesomeness..")
|
|
||||||
},
|
},
|
||||||
|
|
||||||
update(){
|
update(){
|
||||||
|
|
@ -102,13 +147,84 @@ connectionsComponent = {
|
||||||
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
|
let opts = {webcam: $webcam.value, chatnetwork: $chatnetwork.value, scene: $scene.value }
|
||||||
this.update()
|
this.update()
|
||||||
$settings.innerHTML = ''
|
$settings.innerHTML = ''
|
||||||
this.forSelectedPluginsDo( (plugin) => {
|
this.forSelectedPluginsDo( (plugin) => $settings.appendChild( plugin.config(opts) ) )
|
||||||
console.log("configuring "+plugin.plugin.name)
|
this.renderInputs()
|
||||||
console.dir(plugin)
|
},
|
||||||
$settings.appendChild( plugin.config(opts) )
|
|
||||||
|
renderInputs(){
|
||||||
|
if( this.selectedWebcam == 'No thanks' ){
|
||||||
|
return this.$devices.style.display = 'none'
|
||||||
|
}else this.$devices.style.display = ''
|
||||||
|
|
||||||
|
navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: true,
|
||||||
|
video: true
|
||||||
|
})
|
||||||
|
.then( () => {
|
||||||
|
|
||||||
|
const selectors = [this.$audioInput, this.$audioOutput, this.$videoInput];
|
||||||
|
|
||||||
|
const gotDevices = (deviceInfos) => {
|
||||||
|
// Handles being called several times to update labels. Preserve values.
|
||||||
|
const values = selectors.map(select => select.value);
|
||||||
|
selectors.forEach(select => {
|
||||||
|
while (select.firstChild) {
|
||||||
|
select.removeChild(select.firstChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
for (let i = 0; i !== deviceInfos.length; ++i) {
|
||||||
|
const deviceInfo = deviceInfos[i];
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = deviceInfo.deviceId;
|
||||||
|
if (deviceInfo.kind === 'audioinput') {
|
||||||
|
option.text = deviceInfo.label || `microphone ${this.$audioInput.length + 1}`;
|
||||||
|
this.$audioInput.appendChild(option);
|
||||||
|
} else if (deviceInfo.kind === 'audiooutput') {
|
||||||
|
option.text = deviceInfo.label || `speaker ${this.$audioOutput.length + 1}`;
|
||||||
|
this.$audioOutput.appendChild(option);
|
||||||
|
} else if (deviceInfo.kind === 'videoinput') {
|
||||||
|
option.text = deviceInfo.label || `camera this.${this.$videoInput.length + 1}`;
|
||||||
|
this.$videoInput.appendChild(option);
|
||||||
|
} else {
|
||||||
|
console.log('Some other kind of source/device: ', deviceInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// hide if there's nothing to choose
|
||||||
|
let totalDevices = this.$audioInput.options.length + this.$audioOutput.options.length + this.$videoInput.options.length
|
||||||
|
this.$devices.style.display = totalDevices > 3 ? 'block' : 'none'
|
||||||
|
|
||||||
|
selectors.forEach((select, selectorIndex) => {
|
||||||
|
if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
|
||||||
|
select.value = values[selectorIndex];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// after getUserMedia we can enumerate
|
||||||
|
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(console.warn);
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
reactToNetwork(){
|
||||||
|
document.addEventListener('network.connected', () => {
|
||||||
|
console.log("network.connected")
|
||||||
|
window.notify("🪐 connected to awesomeness..")
|
||||||
|
$chat.visibleChatbar = true
|
||||||
|
$chat.send({message:`🎉 connected!`,class:['info']})
|
||||||
|
})
|
||||||
|
document.addEventListener('network.connect', () => {
|
||||||
|
console.log("network.connect")
|
||||||
|
el.parentElement.classList.add('connecthide')
|
||||||
|
window.notify("🪐 connecting to awesomeness..")
|
||||||
|
$connect.innerText = 'connecting..'
|
||||||
|
})
|
||||||
|
document.addEventListener('network.disconnect', () => {
|
||||||
|
window.notify("🪐 disconnecting..")
|
||||||
|
$connect.innerText = 'disconnecting..'
|
||||||
|
setTimeout( () => $connect.innerText = 'connect', 1000)
|
||||||
|
if( !window.accessibility.enabled ) $chat.visibleChatbar = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
},{
|
},{
|
||||||
|
|
||||||
get(data,k,v){ return data[k] },
|
get(data,k,v){ return data[k] },
|
||||||
|
|
@ -119,8 +235,13 @@ connectionsComponent = {
|
||||||
case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
|
case "chatnetwork": $chatnetwork.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
|
||||||
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
|
case "scene": $scene.innerHTML = `<option>${data[k].map((p)=>p.plugin.name).join('</option><option>')}</option>`; break;
|
||||||
case "selectedScene": $scene.value = v; data.renderSettings(); break;
|
case "selectedScene": $scene.value = v; data.renderSettings(); break;
|
||||||
case "selectedWebcam": $webcam.value = v; data.renderSettings(); break;
|
|
||||||
case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break;
|
case "selectedChatnetwork": $chatnetwork.value = v; data.renderSettings(); break;
|
||||||
|
case "selectedWebcam": {
|
||||||
|
$webcam.value = v;
|
||||||
|
data.renderSettings();
|
||||||
|
$devices.style.display = v ? 'block' : 'none'
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,10 +250,10 @@ connectionsComponent = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// reactify component!
|
// reactify component!
|
||||||
document.addEventListener('network:ready', (opts) => {
|
document.addEventListener('$menu:ready', (opts) => {
|
||||||
opts = opts.detail
|
opts = opts.detail
|
||||||
document.head.innerHTML += connectionsComponent.css
|
document.head.innerHTML += connectionsComponent.css
|
||||||
$connections = document.createElement('div')
|
window.$connections = document.createElement('div')
|
||||||
$connections.innerHTML = connectionsComponent.html
|
$connections.innerHTML = connectionsComponent.html
|
||||||
$connections = connectionsComponent.init($connections)
|
$connections = connectionsComponent.init($connections)
|
||||||
$connections.install(opts)
|
$connections.install(opts)
|
||||||
|
|
@ -143,8 +264,21 @@ document.addEventListener('network:ready', (opts) => {
|
||||||
connectionsComponent.css = `
|
connectionsComponent.css = `
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
button#connect{
|
button#connect{
|
||||||
float: right;
|
|
||||||
height: 43px;
|
height: 43px;
|
||||||
|
width:100%;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
#messages .msg #connections{
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
.connecthide {
|
||||||
|
transform:translateY(-1000px);
|
||||||
|
}
|
||||||
|
#close{
|
||||||
|
display: block;
|
||||||
|
margin-top: 16px;
|
||||||
|
position: relative;
|
||||||
|
float: right;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
}
|
||||||
</style>`
|
</style>`
|
||||||
|
|
|
||||||
|
|
@ -1,872 +1,66 @@
|
||||||
// reactive component for displaying the menu
|
// reactive component for displaying the menu
|
||||||
menuComponent = {
|
menuComponent = (el) => new Proxy({
|
||||||
|
|
||||||
html: `
|
html: `
|
||||||
<div id="overlay" class="xrf">
|
|
||||||
<div class="logo" ></div>
|
|
||||||
<button id="navback" onclick="history.back()"><</button>
|
|
||||||
<button id="navforward" onclick="history.forward()">></button>
|
|
||||||
<input id="load" type="submit" value="load 3D file"></input>
|
|
||||||
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )" style="display:none"/>
|
|
||||||
</div>
|
|
||||||
<div class="xrf footer">
|
<div class="xrf footer">
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<div id="buttons"></div>
|
<div id="buttons"></div>
|
||||||
<a class="btn" id="more" aria-title="menu button" aria-description="menu with options, like extra accessibility" onclick="$menu.toggle()"></a><br>
|
<a class="btn" id="more" aria-title="menu button"><i id="icon" class="gg-menu"></i></a><br>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|
||||||
init: (el) => new Proxy({
|
collapsed: false,
|
||||||
morelabel: '⚡',
|
logo: './../../assets/logo.png',
|
||||||
collapsed: false,
|
buttons: [`<a class="btn" aria-label="button" aria-title="share button" aria-description="this allows embedding and sharing of this URL or make a screenshot of it" id="share" onclick="frontend.share()"><i class="gg-link"></i> share</a><br>`],
|
||||||
logo: './../../assets/logo.png',
|
$buttons: $buttons = el.querySelector('#buttons'),
|
||||||
buttons: [`<a class="btn" aria-label="button" aria-title="share button" aria-description="share URL/screenshot/embed" id="share" onclick="$menu.share()">🔗 share</a><br>`],
|
$btnMore: $btnMore = el.querySelector('#more'),
|
||||||
|
|
||||||
$overlay: $overlay = el.querySelector('#overlay'),
|
toggle(){
|
||||||
$logo: $logo = el.querySelector('.logo'),
|
this.collapsed = !this.collapsed
|
||||||
$uri: $uri = el.querySelector('#uri'),
|
el.querySelector("i#icon").className = this.collapsed ? 'gg-close' : 'gg-menu'
|
||||||
$buttons: $buttons = el.querySelector('#buttons'),
|
document.body.classList[ this.collapsed ? 'add' : 'remove' ](['menu'])
|
||||||
$btnMore: $btnMore = el.querySelector('#more'),
|
|
||||||
|
|
||||||
toggle: () => $menu.collapsed = !$menu.collapsed,
|
|
||||||
install: (xrf) => {
|
|
||||||
this.xrf = xrf
|
|
||||||
$menu.bindToWindow() // bind functions like notify to window
|
|
||||||
window.notify('loading '+document.location.search.substr(1))
|
|
||||||
document.body.appendChild(el)
|
|
||||||
document.dispatchEvent( new CustomEvent("$menu:ready", {detail: xrf}) )
|
|
||||||
|
|
||||||
// add screenshot component with camera to capture bigger size equirects
|
|
||||||
// document.querySelector('a-scene').components.screenshot.capture('perspective')
|
|
||||||
$('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2})
|
|
||||||
|
|
||||||
if( window.outerWidth > 800 )
|
|
||||||
setTimeout( () => window.notify("use WASD-keys and mouse-drag to move around",{timeout:false}),2000 )
|
|
||||||
|
|
||||||
xrf.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false )
|
|
||||||
|
|
||||||
// enable user-uploaded asset files
|
|
||||||
let fileLoaders = $menu.loadFile({
|
|
||||||
".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ),
|
|
||||||
".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) )
|
|
||||||
})
|
|
||||||
el.querySelector("#overlay > input[type=submit]").addEventListener("click", fileLoaders )
|
|
||||||
}
|
|
||||||
|
|
||||||
},{
|
|
||||||
|
|
||||||
get(data,k,v){ return data[k] },
|
|
||||||
set(data,k,v){
|
|
||||||
data[k] = v
|
|
||||||
switch( k ){
|
|
||||||
case "logo": $logo.style.backgroundImage = `url(${v})`; break;
|
|
||||||
case "css": document.head.innerHTML += v; break;
|
|
||||||
case "morelabel": $btnMore.innerText = data.morelabel; break;
|
|
||||||
case "buttons": $buttons.innerHTML = this.renderButtons(data);
|
|
||||||
document.dispatchEvent( new CustomEvent("$menu:buttons:render", {detail: el.querySelector('.menu') }) )
|
|
||||||
break;
|
|
||||||
case "collapsed": $overlay.style.display = data.collapsed ? 'block' : 'none'
|
|
||||||
$buttons.style.display = data.collapsed ? 'block' : 'none'
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
renderButtons: (data) => `${data.buttons.join('')}`
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// reactify component!
|
|
||||||
$menu = document.createElement('div')
|
|
||||||
$menu.innerHTML = menuComponent.html
|
|
||||||
$menu = menuComponent.init($menu)
|
|
||||||
|
|
||||||
// attach menu functions which are less related to rendering
|
|
||||||
let utils = {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
bindToWindow(opts){
|
|
||||||
|
|
||||||
window.notify = $menu.notify(window)
|
|
||||||
|
|
||||||
// reroute console messages to snackbar notifications
|
|
||||||
console.log = ( (log,console) => function(str){
|
|
||||||
if( String(str).match(/(:.*#|note:|:\/\/)/) ) window.notify( str )
|
|
||||||
log.call(console,str)
|
|
||||||
})(console.log, console)
|
|
||||||
|
|
||||||
// allow iframe to open url
|
|
||||||
window.addEventListener('message', (event) => {
|
|
||||||
if (event.data && event.data.url) {
|
|
||||||
window.open(event.data.url, '_blank');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
loadFile(contentLoaders, multiple){
|
|
||||||
return () => {
|
|
||||||
window.notify("if you're on Meta browser, file-uploads might be disabled")
|
|
||||||
let input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.multiple = multiple;
|
|
||||||
input.accept = Object.keys(contentLoaders).join(",");
|
|
||||||
input.onchange = () => {
|
|
||||||
let files = Array.from(input.files);
|
|
||||||
let file = files.slice ? files[0] : files
|
|
||||||
for( var i in contentLoaders ){
|
|
||||||
let r = new RegExp('\\'+i+'$')
|
|
||||||
if( file.name.match(r) ) return contentLoaders[i](file)
|
|
||||||
}
|
|
||||||
alert(file.name+" is not supported")
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
SnackBar(userOptions) {
|
init(opts){
|
||||||
var snackbar = this || (window.snackbar = {});
|
el.innerHTML = this.html
|
||||||
var _Interval;
|
document.body.appendChild(el);
|
||||||
var _Message;
|
(['click']).map( (e) => el.addEventListener(e, (ev) => this[e] && this[e](ev.target.id,ev) ) )
|
||||||
var _Element;
|
|
||||||
var _Container;
|
|
||||||
|
|
||||||
var _OptionDefaults = {
|
|
||||||
message: "Operation performed successfully.",
|
|
||||||
dismissible: true,
|
|
||||||
timeout: 7000,
|
|
||||||
status: ""
|
|
||||||
}
|
|
||||||
var _Options = _OptionDefaults;
|
|
||||||
|
|
||||||
function _Create() {
|
|
||||||
_Container = document.querySelector(".js-snackbar-container")
|
|
||||||
if( _Container ){
|
|
||||||
_Container.remove()
|
|
||||||
}
|
|
||||||
_Container = null
|
|
||||||
|
|
||||||
if (!_Container) {
|
|
||||||
// need to create a new container for notifications
|
|
||||||
_Container = document.createElement("div");
|
|
||||||
_Container.classList.add("js-snackbar-container");
|
|
||||||
|
|
||||||
document.body.appendChild(_Container);
|
|
||||||
}
|
|
||||||
_Container.opts = _Options
|
|
||||||
_Container.innerHTML = ''
|
|
||||||
_Element = document.createElement("div");
|
|
||||||
_Element.classList.add("js-snackbar__wrapper","xrf");
|
|
||||||
|
|
||||||
let innerSnack = document.createElement("div");
|
|
||||||
innerSnack.classList.add("js-snackbar", "js-snackbar--show");
|
|
||||||
|
|
||||||
if (_Options.status) {
|
|
||||||
_Options.status = _Options.status.toLowerCase().trim();
|
|
||||||
|
|
||||||
let status = document.createElement("span");
|
|
||||||
status.classList.add("js-snackbar__status");
|
|
||||||
|
|
||||||
|
|
||||||
if (_Options.status === "success" || _Options.status === "green") {
|
|
||||||
status.classList.add("js-snackbar--success");
|
|
||||||
}
|
|
||||||
else if (_Options.status === "warning" || _Options.status === "alert" || _Options.status === "orange") {
|
|
||||||
status.classList.add("js-snackbar--warning");
|
|
||||||
}
|
|
||||||
else if (_Options.status === "danger" || _Options.status === "error" || _Options.status === "red") {
|
|
||||||
status.classList.add("js-snackbar--danger");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
status.classList.add("js-snackbar--info");
|
|
||||||
}
|
|
||||||
|
|
||||||
innerSnack.appendChild(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
_Message = document.createElement("span");
|
|
||||||
_Message.classList.add("js-snackbar__message");
|
|
||||||
if( typeof _Options.message == 'string' ){
|
|
||||||
_Message.innerHTML = _Options.message;
|
|
||||||
}else _Message.appendChild(_Options.message)
|
|
||||||
|
|
||||||
innerSnack.appendChild(_Message);
|
|
||||||
|
|
||||||
if (_Options.dismissible) {
|
|
||||||
let closeBtn = document.createElement("span");
|
|
||||||
closeBtn.classList.add("js-snackbar__close");
|
|
||||||
closeBtn.innerText = "\u00D7";
|
|
||||||
|
|
||||||
closeBtn.onclick = snackbar.Close;
|
|
||||||
|
|
||||||
innerSnack.appendChild(closeBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
_Element.style.height = "0px";
|
|
||||||
_Element.style.opacity = "0";
|
|
||||||
_Element.style.marginTop = "0px";
|
|
||||||
_Element.style.marginBottom = "0px";
|
|
||||||
|
|
||||||
_Element.appendChild(innerSnack);
|
|
||||||
_Container.appendChild(_Element);
|
|
||||||
|
|
||||||
if (_Options.timeout !== false) {
|
|
||||||
_Interval = setTimeout(snackbar.Close, _Options.timeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
snackbar.Open = function() {
|
|
||||||
let contentHeight = _Element.firstElementChild.scrollHeight; // get the height of the content
|
|
||||||
|
|
||||||
_Element.style.height = contentHeight + "px";
|
|
||||||
_Element.style.opacity = 1;
|
|
||||||
_Element.style.marginTop = "5px";
|
|
||||||
_Element.style.marginBottom = "5px";
|
|
||||||
|
|
||||||
_Element.addEventListener("transitioned", function() {
|
|
||||||
_Element.removeEventListener("transitioned", arguments.callee);
|
|
||||||
_Element.style.height = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
snackbar.Close = function () {
|
|
||||||
if (_Interval)
|
|
||||||
clearInterval(_Interval);
|
|
||||||
|
|
||||||
let snackbarHeight = _Element.scrollHeight; // get the auto height as a px value
|
|
||||||
let snackbarTransitions = _Element.style.transition;
|
|
||||||
_Element.style.transition = "";
|
|
||||||
|
|
||||||
requestAnimationFrame(function() {
|
|
||||||
_Element.style.height = snackbarHeight + "px"; // set the auto height to the px height
|
|
||||||
_Element.style.opacity = 1;
|
|
||||||
_Element.style.marginTop = "0px";
|
|
||||||
_Element.style.marginBottom = "0px";
|
|
||||||
_Element.style.transition = snackbarTransitions
|
|
||||||
|
|
||||||
requestAnimationFrame(function() {
|
|
||||||
_Element.style.height = "0px";
|
|
||||||
_Element.style.opacity = 0;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
try {
|
|
||||||
_Container.removeChild(_Element);
|
|
||||||
} catch (e) { }
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
_Options = { ..._OptionDefaults, ...userOptions }
|
|
||||||
_Create();
|
|
||||||
snackbar.Open();
|
|
||||||
},
|
|
||||||
|
|
||||||
notify(scope){
|
|
||||||
return function notify(_str,opts){
|
|
||||||
if( accessibility.enabled ) return $chat.send({message:_str})
|
|
||||||
str = _str.replace(/(^\w+):/,"<div class='badge'>\$1</div>")
|
|
||||||
opts = opts || {status:'info'}
|
|
||||||
opts = Object.assign({ status, timeout:4000 },opts)
|
|
||||||
if( typeof str == 'string' ){
|
|
||||||
if( !opts.status ){
|
|
||||||
if( str.match(/error/g) ) opts.status = "danger"
|
|
||||||
if( str.match(/warning/g) ) opts.status = "warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
opts.message = str
|
|
||||||
window.$menu.SnackBar( opts )
|
|
||||||
opts.message = _str
|
|
||||||
document.dispatchEvent( new CustomEvent("notify", {detail:opts}) )
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
download(){
|
|
||||||
function fetchAndDownload(dataurl, filename) {
|
|
||||||
var a = document.createElement("a");
|
|
||||||
a.href = dataurl;
|
|
||||||
a.setAttribute("download", filename);
|
|
||||||
a.click();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let file = document.location.search.replace(/\?/,'')
|
|
||||||
fetchAndDownload( file, file )
|
|
||||||
},
|
|
||||||
|
|
||||||
updateHashPosition(randomize){
|
|
||||||
// *TODO* this should be part of the XRF Threejs framework
|
|
||||||
if( typeof THREE == 'undefined' ) THREE = xrf.THREE
|
|
||||||
let radToDeg = THREE.MathUtils.radToDeg
|
|
||||||
let toDeg = (x) => x / (Math.PI / 180)
|
|
||||||
let camera = document.querySelector('[camera]').object3D.parent // *TODO* fix for threejs
|
|
||||||
camera.position.x += Math.random()/10
|
|
||||||
camera.position.z += Math.random()/10
|
|
||||||
|
|
||||||
// *TODO* add camera direction
|
|
||||||
let direction = new xrf.THREE.Vector3()
|
|
||||||
camera.getWorldDirection(direction)
|
|
||||||
const pitch = Math.asin(direction.y);
|
|
||||||
const yaw = Math.atan2(direction.x, direction.z);
|
|
||||||
const pitchInDegrees = pitch * 180 / Math.PI;
|
|
||||||
const yawInDegrees = yaw * 180 / Math.PI;
|
|
||||||
|
|
||||||
let lastPos = `pos=${camera.position.x.toFixed(2)},${camera.position.y.toFixed(2)},${camera.position.z.toFixed(2)}`
|
|
||||||
let newHash = document.location.hash.replace(/[&]?(pos|rot)=[0-9\.-]+,[0-9\.-]+,[0-9\.-]+/,'')
|
|
||||||
newHash += `&${lastPos}`
|
|
||||||
document.location.hash = newHash.replace(/&&/,'&')
|
|
||||||
.replace(/#&/,'')
|
|
||||||
$menu.copyToClipboard( window.location.href );
|
|
||||||
},
|
|
||||||
|
|
||||||
copyToClipboard(text){
|
|
||||||
// copy url to clipboard
|
|
||||||
var dummy = document.createElement('input')
|
|
||||||
document.body.appendChild(dummy);
|
|
||||||
dummy.value = text;
|
|
||||||
dummy.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(dummy);
|
|
||||||
},
|
|
||||||
|
|
||||||
share(){
|
|
||||||
let inMeeting = $('[meeting]')
|
|
||||||
let url = window.location.href
|
|
||||||
if( !inMeeting ) $menu.updateHashPosition()
|
|
||||||
else url = $('[meeting]').components['meeting'].data.link
|
|
||||||
$menu.copyToClipboard( url )
|
|
||||||
// End of *TODO*
|
|
||||||
window.notify(`<h2>${ inMeeting ? 'Meeting link ' : 'Link'} copied to clipboard!</h2> <br>Now share it with your friends ❤️<br>
|
|
||||||
<canvas id="qrcode" width="121" height="121"></canvas><br>
|
|
||||||
<button onclick="$menu.download()">💾 download scene file</button> <br>
|
|
||||||
<button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')">📷 download 360 screenshot</button> <br>
|
|
||||||
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld">🖥 clone & selfhost this experience</a><br>
|
|
||||||
<br>
|
|
||||||
To embed this experience in your blog,<br>
|
|
||||||
copy/paste the following into your HTML:<br><input type="text" value="<iframe src='${document.location.href}'><br></iframe>" id="share"/>
|
|
||||||
<br>
|
|
||||||
`,{timeout:2000000})
|
|
||||||
// draw QR code
|
|
||||||
setTimeout( () => {
|
setTimeout( () => {
|
||||||
let QR = window.QR
|
document.dispatchEvent( new CustomEvent("$menu:ready", {detail: {$menu:this,xrf}}) )
|
||||||
QR.canvas = document.getElementById('qrcode')
|
},100)
|
||||||
QR.draw( url, QR.canvas )
|
return this
|
||||||
},0)
|
},
|
||||||
// mobile share
|
|
||||||
if( typeof navigator.share != 'undefined'){
|
click(id,e){
|
||||||
navigator.share({
|
switch(id){
|
||||||
url,
|
case "icon":
|
||||||
title: 'your meeting link'
|
case "more": this.toggle(); break;
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
|
||||||
// map to component
|
get(me,k,v){ return me[k] },
|
||||||
for( let i in utils ) $menu[i] = utils[i]
|
|
||||||
|
|
||||||
//$('a-scene').addEventListener('XRF', this.onXRFready )
|
set(me,k,v){
|
||||||
//
|
me[k] = v
|
||||||
// if( document.location.search.length > 2 ){
|
switch( k ){
|
||||||
// $('[xrf]').setAttribute('xrf', document.location.search.substr(1)+document.location.hash )
|
case "buttons": el.querySelector("#buttons").innerHTML = this.renderButtons(me);
|
||||||
// }
|
document.dispatchEvent( new CustomEvent("$menu:buttons:render", {detail: el.querySelector('.menu') }) )
|
||||||
//
|
break;
|
||||||
// },
|
case "collapsed":
|
||||||
//
|
el.querySelector("#buttons").style.display = me.collapsed ? 'block' : 'none'
|
||||||
// onXRFready: function(){
|
frontend.emit('$menu:collapse', v)
|
||||||
//
|
break;
|
||||||
// let XRF = window.AFRAME.XRF
|
|
||||||
// //setupMenu(XRF){
|
|
||||||
// // let aScene = document.querySelector('a-scene')
|
|
||||||
// // let urlbar = $('input#uri')
|
|
||||||
// // let inIframe = window.location !== window.parent.location
|
|
||||||
// // let els = [ ...document.querySelectorAll('.menu .btn') ]
|
|
||||||
// // els = els.filter( (el) => el.id != "more" ? el : false )
|
|
||||||
//
|
|
||||||
// // // enable meetings
|
|
||||||
// // let startMeeting = () => {
|
|
||||||
// // aScene.setAttribute('meeting', 'id: xrfragments')
|
|
||||||
// // $('a#meeting').innerText = '🧑🤝🧑 breakout meeting'
|
|
||||||
// // $('a#meeting').setAttribute('aria-description','breakout room')
|
|
||||||
// // }
|
|
||||||
// // $('a#meeting').addEventListener('click', () => {
|
|
||||||
// // if( aScene.getAttribute('meeting') ){ // meeting already, start breakout room
|
|
||||||
// // let parentRoom = document.location.href
|
|
||||||
// // $menu.updateHashPosition(true)
|
|
||||||
// // let meeting = $('[meeting]').components['meeting']
|
|
||||||
// // meeting.data.parentRoom = parentRoom
|
|
||||||
// // meeting.update()
|
|
||||||
// // }else startMeeting()
|
|
||||||
// // })
|
|
||||||
// // if( document.location.hash.match(/(#|&)meet/) ) startMeeting()
|
|
||||||
//
|
|
||||||
// // XRF.addEventListener('hash', () => reflectUrl() )
|
|
||||||
// // const reflectUrl = window.reflectUrl = (url) => {
|
|
||||||
// // urlbar.value = url || document.location.search.substr(1) + document.location.hash
|
|
||||||
// // }
|
|
||||||
// // reflectUrl()
|
|
||||||
// //},
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// // on localhost enable debugging mode for developer convenience
|
|
||||||
// let loc = document.location
|
|
||||||
// if( loc.host.match(/^localhost/) ){
|
|
||||||
// $('a-scene').setAttribute('stats')
|
|
||||||
// XRF.debug = 1
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // add screenshot component with camera to capture bigger size equirects
|
|
||||||
// // document.querySelector('a-scene').components.screenshot.capture('perspective')
|
|
||||||
// $('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2})
|
|
||||||
//
|
|
||||||
// if( window.outerWidth > 800 )
|
|
||||||
// setTimeout( () => window.notify("use WASD-keys and mouse-drag to move around",{timeout:false}),2000 )
|
|
||||||
//
|
|
||||||
// window.AFRAME.XRF.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false )
|
|
||||||
//
|
|
||||||
// // enable user-uploaded asset files
|
|
||||||
// let fileLoaders = $menu.loadFile({
|
|
||||||
// ".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ),
|
|
||||||
// ".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) )
|
|
||||||
// })
|
|
||||||
// $("#overlay > input[type=submit]").addEventListener("click", fileLoaders )
|
|
||||||
|
|
||||||
|
|
||||||
// finally add some css
|
|
||||||
$menu.css = `
|
|
||||||
<style type="text/css">
|
|
||||||
:root {
|
|
||||||
--xrf-primary: #6839dc;
|
|
||||||
--xrf-primary-fg: #FFF;
|
|
||||||
--xrf-light-primary: #ea23cf;
|
|
||||||
--xrf-secondary: #872eff;
|
|
||||||
--xrf-light-xrf-secondary: #ce7df2;
|
|
||||||
--xrf-overlay-bg: #fffb;
|
|
||||||
--xrf-box-shadow: #0005;
|
|
||||||
--xrf-red: red;
|
|
||||||
--xrf-dark-gray: #343334;
|
|
||||||
--xrf-gray: #424280;
|
|
||||||
--xrf-white: #fdfdfd;
|
|
||||||
--xrf-light-gray: #efefef;
|
|
||||||
--xrf-lighter-gray: #e4e2fb96;
|
|
||||||
--xrf-font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
|
|
||||||
--xrf-font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
|
|
||||||
--xrf-font-size-0: 11px;
|
|
||||||
--xrf-font-size-1: 14px;
|
|
||||||
--xrf-font-size-2: 17px;
|
|
||||||
--xrf-font-size-3: 21px;
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/* CSS reset */
|
renderButtons: (data) => `${data.buttons.join('')}`
|
||||||
html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:0.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type="button"],[type="reset"],[type="submit"],button{-webkit-appearance:button}[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}
|
|
||||||
|
|
||||||
.xrf table tr td{
|
})
|
||||||
vertical-align:top;
|
|
||||||
}
|
|
||||||
.xrf button,
|
|
||||||
.xrf input[type="submit"],
|
|
||||||
.xrf .btn {
|
|
||||||
text-decoration:none;
|
|
||||||
background: var(--xrf-primary);
|
|
||||||
border: 0;
|
|
||||||
border-radius: 25px;
|
|
||||||
padding: 11px 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: 0.3s;
|
|
||||||
height: 32px;
|
|
||||||
font-size: var(--xrf-font-size-1);
|
|
||||||
color: var(--xrf-primary-fg);
|
|
||||||
line-height: var(--xrf-font-size-1);
|
|
||||||
cursor:pointer;
|
|
||||||
white-space:pre;
|
|
||||||
min-width: 45px;
|
|
||||||
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.xrf button:hover,
|
// reactify component!
|
||||||
.xrf input[type="submit"]:hover,
|
document.addEventListener('frontend:ready', (e) => {
|
||||||
.xrf .btn:hover {
|
window.$menu = menuComponent( document.createElement('div') ).init(e.detail)
|
||||||
background: var(--xrf-secondary);
|
})
|
||||||
text-decoration:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xrf, .xrf *{
|
|
||||||
font-family: var(--xrf-font-sans-serif);
|
|
||||||
font-size: var(--xrf-font-size-1);
|
|
||||||
line-height:27px;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea, select, input[type="text"] {
|
|
||||||
background: transparent; /* linear-gradient( var(--xrf-lighter-gray), var(--xrf-gray) ) !important; */
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="submit"] {
|
|
||||||
color: var(--xrf-light-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type=text]{
|
|
||||||
padding:7px 15px;
|
|
||||||
}
|
|
||||||
input{
|
|
||||||
border-radius:7px;
|
|
||||||
margin:5px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
border-bottom: 2px solid var(--xrf-secondary);
|
|
||||||
padding-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#overlay{
|
|
||||||
background: var(--xrf-overlay-bg);
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 48px;
|
|
||||||
box-shadow: 0px 0px 10px var(--xrf-box-shadow);
|
|
||||||
opacity: 0.9;
|
|
||||||
z-index:2000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#overlay .logo{
|
|
||||||
width: 92px;
|
|
||||||
position: absolute;
|
|
||||||
top: 9px;
|
|
||||||
left: 93px;
|
|
||||||
height: 30px;
|
|
||||||
background-size: contain;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
#overlay > input[type="submit"] {
|
|
||||||
height: 32px;
|
|
||||||
position: absolute;
|
|
||||||
right: 20px;
|
|
||||||
top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#overlay > button#navback,
|
|
||||||
#overlay > button#navforward {
|
|
||||||
height: 32px;
|
|
||||||
font-size: var(--xrf-font-size-1);
|
|
||||||
position: absolute;
|
|
||||||
left: 9px;
|
|
||||||
padding: 2px 13px;
|
|
||||||
border-radius:6px;
|
|
||||||
top: 8px;
|
|
||||||
color: var(--xrf-light-gray);
|
|
||||||
width: 36px;
|
|
||||||
min-width: unset;
|
|
||||||
}
|
|
||||||
#overlay > button#navforward {
|
|
||||||
left:49px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#overlay > #uri {
|
|
||||||
height: 18px;
|
|
||||||
font-size: var(--xrf-font-size-3);
|
|
||||||
position: absolute;
|
|
||||||
left: 200px;
|
|
||||||
top: 9px;
|
|
||||||
max-width: 550px;
|
|
||||||
padding: 5px 0px 5px 5px;
|
|
||||||
width: calc( 63% - 200px);
|
|
||||||
background: #f0f0f0;
|
|
||||||
border-color: #Ccc;
|
|
||||||
border: 2px solid #CCC;
|
|
||||||
border-radius: 7px;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.menu .btn{
|
|
||||||
display:inline-block;
|
|
||||||
background: var(--xrf-primary);
|
|
||||||
border-radius: 25px;
|
|
||||||
border: 0;
|
|
||||||
padding: 5px 19px;
|
|
||||||
font-weight: 1000;
|
|
||||||
font-family: sans-serif;
|
|
||||||
font-size: var(--xrf-font-size-2);
|
|
||||||
color:var(--xrf-primary-fg);
|
|
||||||
height:33px;
|
|
||||||
z-index:2000;
|
|
||||||
cursor:pointer;
|
|
||||||
min-width:145px;
|
|
||||||
text-decoration:none;
|
|
||||||
margin-top: 15px;
|
|
||||||
line-height:36px;
|
|
||||||
margin-right:10px;
|
|
||||||
text-align:left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xrf a.btn#more{
|
|
||||||
z-index:3000;
|
|
||||||
width: 19px;
|
|
||||||
min-width: 19px;
|
|
||||||
font-size:16px;
|
|
||||||
text-align: center;
|
|
||||||
background:white;
|
|
||||||
color: var(--xrf-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
html{
|
|
||||||
max-width:unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
.render {
|
|
||||||
position:absolute;
|
|
||||||
top:0;
|
|
||||||
left:0;
|
|
||||||
right:0;
|
|
||||||
bottom:0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lil-gui.autoPlace{
|
|
||||||
right:0px !important;
|
|
||||||
top:48px !important;
|
|
||||||
height:33vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
#VRButton {
|
|
||||||
margin-bottom:20vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 450px) {
|
|
||||||
#uri{ display:none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.lil-gui.root{
|
|
||||||
top:auto !important;
|
|
||||||
left:auto !important;
|
|
||||||
}
|
|
||||||
.js-snackbar__message{
|
|
||||||
overflow-y:auto;
|
|
||||||
max-height:600px;
|
|
||||||
}
|
|
||||||
.js-snackbar__message h1,h2,h3{
|
|
||||||
font-size:22px;
|
|
||||||
}
|
|
||||||
.xrf table tr td {
|
|
||||||
|
|
||||||
}
|
|
||||||
:root{
|
|
||||||
--xrf-font-size-1: 13px;
|
|
||||||
--xrf-font-size-2: 17px;
|
|
||||||
--xrf-font-size-3: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* notifications */
|
|
||||||
|
|
||||||
.js-snackbar-container .btn,
|
|
||||||
.js-snackbar-container input[type=submit],
|
|
||||||
.js-snackbar-container button{
|
|
||||||
margin-bottom:15px;
|
|
||||||
}
|
|
||||||
.js-snackbar-container {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 0px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width:100%;
|
|
||||||
max-width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
z-index:1001;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar-container * {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__wrapper {
|
|
||||||
--color-c: #555;
|
|
||||||
--color-a: #FFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.js-snackbar__wrapper {
|
|
||||||
overflow: hidden;
|
|
||||||
height: auto;
|
|
||||||
margin: 5px 0;
|
|
||||||
transition: all ease .5s;
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: 0 0 4px 0 var(--xrf-box-shadow);
|
|
||||||
right: 20px;
|
|
||||||
position: fixed;
|
|
||||||
top: 55px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar {
|
|
||||||
display: inline-flex;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-radius: 3px;
|
|
||||||
color: var(--color-c);
|
|
||||||
background-color: var(--color-a);
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__close,
|
|
||||||
.js-snackbar__status,
|
|
||||||
.js-snackbar__message {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__message {
|
|
||||||
margin: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__status {
|
|
||||||
display: none;
|
|
||||||
width: 15px;
|
|
||||||
margin-right: 5px;
|
|
||||||
border-radius: 3px 0 0 3px;
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__status.js-snackbar--success,
|
|
||||||
.js-snackbar__status.js-snackbar--warning,
|
|
||||||
.js-snackbar__status.js-snackbar--danger,
|
|
||||||
.js-snackbar__status.js-snackbar--info {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__status.js-snackbar--success {
|
|
||||||
background-color: #4caf50;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__status.js-snackbar--warning {
|
|
||||||
background-color: #ff9800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__status.js-snackbar--danger {
|
|
||||||
background-color: #ff6060;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__status.js-snackbar--info {
|
|
||||||
background-color: #CCC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__close {
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 10px;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.js-snackbar__close:hover {
|
|
||||||
background-color: #4443;
|
|
||||||
}
|
|
||||||
|
|
||||||
.a-enter-vr-button, .a-enter-ar-button{
|
|
||||||
height:41px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#qrcode{
|
|
||||||
background: transparent;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 121px;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
input#share{
|
|
||||||
font-size: var(--xrf-font-size-1);
|
|
||||||
font-family: var(--xrf-font-monospace);
|
|
||||||
border:2px solid #AAA;
|
|
||||||
width:50vw;
|
|
||||||
max-width:400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column-reverse; /* This reverses the stacking order of the flex container */
|
|
||||||
align-items: flex-end;
|
|
||||||
height: 100%;
|
|
||||||
position: fixed;
|
|
||||||
top: 71px;
|
|
||||||
right: 11px;
|
|
||||||
bottom: 0;
|
|
||||||
padding-bottom:149px;
|
|
||||||
box-sizing:border-box;
|
|
||||||
}
|
|
||||||
.footer .menu{
|
|
||||||
text-align:right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display:inline-block;
|
|
||||||
color: var(--xrf-white);
|
|
||||||
font-weight: bold;
|
|
||||||
background: var(--xrf-gray);
|
|
||||||
border-radius:5px;
|
|
||||||
padding:0px 4px;
|
|
||||||
font-size: var(--xrf-font-size-0);
|
|
||||||
margin-right:10px
|
|
||||||
}
|
|
||||||
.ruler{
|
|
||||||
width:97%;
|
|
||||||
margin:7px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
a.badge {
|
|
||||||
text-decoration:none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xrf select{
|
|
||||||
min-width: 200px;
|
|
||||||
border-inline: none;
|
|
||||||
border-inline: none;
|
|
||||||
border-block: none;
|
|
||||||
border: 3px solid var(--xrf-primary);
|
|
||||||
border-radius: 5px;
|
|
||||||
background: none;
|
|
||||||
border-radius:30px;
|
|
||||||
}
|
|
||||||
.xrf select,
|
|
||||||
.xrf option{
|
|
||||||
padding: 0px 16px;
|
|
||||||
min-width: 200px;
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xrf input{
|
|
||||||
border-radius:30px;
|
|
||||||
padding: 7px 15px;
|
|
||||||
border-block: none;
|
|
||||||
border-inline: none;
|
|
||||||
border: 1px solid #888;
|
|
||||||
background: transparent;
|
|
||||||
height: 18px;
|
|
||||||
max-width:168px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xrf table tr td {
|
|
||||||
vertical-align:middle;
|
|
||||||
text-align:right;
|
|
||||||
}
|
|
||||||
.xrf table tr td:nth-child(1){
|
|
||||||
min-width:70px;
|
|
||||||
height:40px;
|
|
||||||
padding-right:15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xrf small{
|
|
||||||
font-size: var(--xrf-font-size-0);
|
|
||||||
}
|
|
||||||
.disabled{
|
|
||||||
opacity:0.5
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
`
|
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,20 @@ window.accessibility = (opts) => new Proxy({
|
||||||
|
|
||||||
settings(){
|
settings(){
|
||||||
this.toggle() // *TODO* should show settings screen
|
this.toggle() // *TODO* should show settings screen
|
||||||
if( this.enabled ) window.notify(`accessibility boosted, click <a href="#">here</a> to tweak settings`)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
speak(str, override){
|
speak(str, opts){
|
||||||
|
opts = opts || {speaksigns:true}
|
||||||
if( !this.enabled || !str) return
|
if( !this.enabled || !str) return
|
||||||
str = str.replace(/\/\//,' ')
|
if( opts.speaksigns ){
|
||||||
.replace(/:/,'')
|
str = str.replace(/\/\//,' ')
|
||||||
.replace(/\//,' slash ')
|
.replace(/:/,'')
|
||||||
.replace(/\./,' dot ')
|
.replace(/\//,' slash ')
|
||||||
.replace(/#/,' hash ')
|
.replace(/\./,' dot ')
|
||||||
.replace(/&/,' and ')
|
.replace(/#/,' hash ')
|
||||||
.replace(/=/,' is ')
|
.replace(/&/,' and ')
|
||||||
|
.replace(/=/,' is ')
|
||||||
|
}
|
||||||
let speech = window.speechSynthesis
|
let speech = window.speechSynthesis
|
||||||
let utterance = new SpeechSynthesisUtterance( str )
|
let utterance = new SpeechSynthesisUtterance( str )
|
||||||
if( this.speak_voice != -1) utterance.voice = speech.getVoices()[ this.speak_voice ];
|
if( this.speak_voice != -1) utterance.voice = speech.getVoices()[ this.speak_voice ];
|
||||||
|
|
@ -41,7 +43,7 @@ window.accessibility = (opts) => new Proxy({
|
||||||
utterance.rate = this.speak_rate
|
utterance.rate = this.speak_rate
|
||||||
utterance.pitch = this.speak_pitch
|
utterance.pitch = this.speak_pitch
|
||||||
utterance.volume = this.speak_volume
|
utterance.volume = this.speak_volume
|
||||||
if( override ) speech.cancel()
|
if( opts.override ) speech.cancel()
|
||||||
speech.speak(utterance)
|
speech.speak(utterance)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -56,7 +58,7 @@ window.accessibility = (opts) => new Proxy({
|
||||||
case "ArrowRight": k = "right"; break;
|
case "ArrowRight": k = "right"; break;
|
||||||
case "ArrowDown": k = "backward"; break;
|
case "ArrowDown": k = "backward"; break;
|
||||||
}
|
}
|
||||||
this.speak(k,true)
|
this.speak(k,{override:true})
|
||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('$menu:buttons:render', (e) => {
|
document.addEventListener('$menu:buttons:render', (e) => {
|
||||||
|
|
@ -66,38 +68,51 @@ window.accessibility = (opts) => new Proxy({
|
||||||
a.map( (btn) => {
|
a.map( (btn) => {
|
||||||
if( !btn.href ) btn.setAttribute("href","javascript:void(0)") // important!
|
if( !btn.href ) btn.setAttribute("href","javascript:void(0)") // important!
|
||||||
btn.setAttribute("aria-label","button")
|
btn.setAttribute("aria-label","button")
|
||||||
btn.addEventListener('mouseover', (e) => {
|
})
|
||||||
let str = btn.getAttribute("aria-title") + btn.getAttribute('aria-description')
|
document.addEventListener('mouseover', (e) => {
|
||||||
this.speak( str,true)
|
if( e.target.getAttribute("aria-title") ){
|
||||||
})
|
let lines = []
|
||||||
|
lines.push( e.target.getAttribute("aria-title") )
|
||||||
|
lines.push( e.target.getAttribute("aria-description") )
|
||||||
|
lines = lines.filter( (l) => l )
|
||||||
|
this.speak( lines.join("."), {override:true,speaksigns:false} )
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('$chat:receive', (e) => {
|
document.addEventListener('network.send', (e) => {
|
||||||
let opts = e.detail
|
let opts = e.detail
|
||||||
opts.message = opts.message || ''
|
opts.message = opts.message || ''
|
||||||
if( opts.class && ~opts.class.indexOf('info') ) opts.message = `info: ${opts.message}`
|
if( opts.class && ~opts.class.indexOf('info') ) opts.message = `info: ${opts.message}`
|
||||||
this.speak(e.detail.message)
|
this.speak(opts.message)
|
||||||
})
|
})
|
||||||
|
|
||||||
opts.addEventListener('pos', (opts) => {
|
opts.xrf.addEventListener('pos', (opts) => {
|
||||||
let obj
|
if( this.enabled ){
|
||||||
let description
|
$chat.send({message: this.posToMessage(opts) })
|
||||||
let msg = "You've teleported to "
|
}
|
||||||
let pos = opts.frag.pos
|
network.send({message: this.posToMessage(opts), class:["info","guide"]})
|
||||||
if( pos.string.match(',') ) msg += `coordinates <a href="#pos=${pos.string}">${pos.string}</a>`
|
network.pos = opts.frag.pos.string
|
||||||
else{
|
|
||||||
msg += `location <a href="#pos=${pos.string}">${pos.string}</a>`
|
|
||||||
obj = opts.scene.getObjectByName(pos.string)
|
|
||||||
if( obj ){
|
|
||||||
description = obj.userData['aria-label'] || ''
|
|
||||||
}else msg += ", but your teleportation was refused because it cannot be found within this world"
|
|
||||||
}
|
|
||||||
$chat.send({html: () => msg, class:["info","guide"]})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
posToMessage(opts){
|
||||||
|
let obj
|
||||||
|
let description
|
||||||
|
let msg = "teleported to "
|
||||||
|
let pos = opts.frag.pos
|
||||||
|
if( pos.string.match(',') ) msg += `coordinates <a href="#pos=${pos.string}">${pos.string}</a>`
|
||||||
|
else{
|
||||||
|
msg += `location <a href="#pos=${pos.string}">${pos.string}</a>`
|
||||||
|
obj = opts.scene.getObjectByName(pos.string)
|
||||||
|
if( obj ){
|
||||||
|
description = obj.userData['aria-label'] || ''
|
||||||
|
}else msg += ", but the teleportation was refused because it cannot be found within this world"
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
},
|
||||||
|
|
||||||
sanitizeTranscript(){
|
sanitizeTranscript(){
|
||||||
return $chat.$messages.innerText
|
return $chat.$messages.innerText
|
||||||
.replaceAll("<[^>]*>", "") // strip html
|
.replaceAll("<[^>]*>", "") // strip html
|
||||||
|
|
@ -114,7 +129,7 @@ window.accessibility = (opts) => new Proxy({
|
||||||
data[k] = v
|
data[k] = v
|
||||||
switch( k ){
|
switch( k ){
|
||||||
case "enabled": {
|
case "enabled": {
|
||||||
let message = (v?"boosting":"unboosting") + " accessibility features"
|
let message = "accessibility has been"+(v?"boosted":"lowered")
|
||||||
$('#accessibility.btn').style.filter= v ? 'brightness(1.0)' : 'brightness(0.5)'
|
$('#accessibility.btn').style.filter= v ? 'brightness(1.0)' : 'brightness(0.5)'
|
||||||
if( v ) $chat.visible = true
|
if( v ) $chat.visible = true
|
||||||
$chat.send({message,class:['info','guide']})
|
$chat.send({message,class:['info','guide']})
|
||||||
|
|
@ -131,9 +146,10 @@ window.accessibility = (opts) => new Proxy({
|
||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('$menu:ready', (e) => {
|
document.addEventListener('$menu:ready', (e) => {
|
||||||
window.accessibility = accessibility(e.detail)
|
try{
|
||||||
accessibility.init()
|
accessibility = accessibility(e.detail)
|
||||||
document.dispatchEvent( new CustomEvent("accessibility:ready", e ) )
|
accessibility.init()
|
||||||
$menu.buttons = $menu.buttons.concat([`<a class="btn" style="background:var(--xrf-dark-gray);filter: brightness(0.5);" aria-label="button" aria-description="enable all accessibility features" id="accessibility" onclick="accessibility.settings()">👩🚀 accessibility</a><br>`])
|
document.dispatchEvent( new CustomEvent("accessibility:ready", e ) )
|
||||||
|
$menu.buttons = $menu.buttons.concat([`<a class="btn" style="background:var(--xrf-dark-gray);filter: brightness(0.5);" aria-label="button" aria-description="enable all accessibility features" id="accessibility" onclick="accessibility.settings()"><i class="gg-yinyang"></i>accessibility</a><br>`])
|
||||||
|
}catch(e){console.error(e)}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
753
src/3rd/js/plugin/frontend/css.js
Normal file
753
src/3rd/js/plugin/frontend/css.js
Normal 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>
|
||||||
|
`
|
||||||
240
src/3rd/js/plugin/frontend/frontend.js
Normal file
240
src/3rd/js/plugin/frontend/frontend.js
Normal 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()">‹</button>
|
||||||
|
<button id="navforward" onclick="history.forward()">›</button>
|
||||||
|
<input id="load" type="submit" value="load 3D file"></input>
|
||||||
|
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )" style="display:none"/>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
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> download scene file</button> <br>
|
||||||
|
<button onclick="alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')"><i class="gg-image"></i> download 360 screenshot</button> <br>
|
||||||
|
<a class="btn" target="_blank" href="https://github.com/coderofsalvation/xrfragment-helloworld"><i class="gg-serverless"></i> clone & selfhost this experience</a><br>
|
||||||
|
<br>
|
||||||
|
To embed this experience in your blog,<br>
|
||||||
|
copy/paste the following into your HTML:<br><input type="text" value="<iframe src='${document.location.href}'></iframe>" 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()
|
||||||
|
|
@ -3,32 +3,64 @@
|
||||||
window.network = (opts) => new Proxy({
|
window.network = (opts) => new Proxy({
|
||||||
|
|
||||||
connected: false,
|
connected: false,
|
||||||
|
pos: '',
|
||||||
meetinglink: "",
|
meetinglink: "",
|
||||||
peers: {},
|
peers: {},
|
||||||
plugin: {},
|
plugin: {},
|
||||||
opts,
|
opts,
|
||||||
|
|
||||||
start(url){
|
init(){
|
||||||
console.log("starting network with url "+(url?url:"default"))
|
document.addEventListener('network.disconnect', () => this.connected = false )
|
||||||
|
document.addEventListener('network.connected', () => this.connected = true )
|
||||||
|
setTimeout( () => window.frontend.emit('network.init'), 100 )
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
connect(opts){
|
||||||
|
window.frontend.emit(`network.${this.connected?'disconnect':'connect'}`,opts)
|
||||||
},
|
},
|
||||||
|
|
||||||
add(peerid,data){
|
add(peerid,data){
|
||||||
data = {lastUpdated: new Date().getTime(), id: peerid, ...data }
|
data = {lastUpdated: new Date().getTime(), id: peerid, ...data }
|
||||||
this.peers[peerid] = data
|
this.peers[peerid] = data
|
||||||
opts.scene.dispatchEvent({type:'network.peer.add', peer})
|
window.frontend.emit(`network.peer.add`,{peer})
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(peerid,data){
|
remove(peerid,data){
|
||||||
delete this.peers[peerid]
|
delete this.peers[peerid]
|
||||||
opts.scene.dispatchEvent({type:'network.peer.remove', peer})
|
window.frontend.emit(`network.peer.remove`,{peer})
|
||||||
},
|
},
|
||||||
|
|
||||||
send(opts){
|
send(opts){
|
||||||
|
window.frontend.emit('network.send',opts)
|
||||||
},
|
},
|
||||||
|
|
||||||
receive(opts){
|
receive(opts){
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getMeetingFromUrl(url){
|
||||||
|
let hash = url.replace(/.*#/,'')
|
||||||
|
let parts = hash.split("&")
|
||||||
|
let meeting = ''
|
||||||
|
parts.map( (p) => {
|
||||||
|
if( p.split("=")[0] == 'meet' ) meeting = p.split("=")[1]
|
||||||
|
})
|
||||||
|
return meeting
|
||||||
|
},
|
||||||
|
|
||||||
|
randomRoom(){
|
||||||
|
var names = []
|
||||||
|
let add = (s) => s.length < 6 && !s.match(/[0-9$]/) && !s.match(/_/) ? names.push(s) : false
|
||||||
|
for ( var i in window ) add(i)
|
||||||
|
for ( var i in Object.prototype ) add(i)
|
||||||
|
for ( var i in Function.prototype ) add(i)
|
||||||
|
for ( var i in Array.prototype ) add(i)
|
||||||
|
for ( var i in String.prototype ) add(i)
|
||||||
|
var a = names[Math.floor(Math.random() * names.length)];
|
||||||
|
var b = names[Math.floor(Math.random() * names.length)];
|
||||||
|
var c = names[Math.floor(Math.random() * names.length)];
|
||||||
|
return String(`${a}-${b}-${c}`).toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
@ -38,15 +70,10 @@ window.network = (opts) => new Proxy({
|
||||||
set(data,k,v){
|
set(data,k,v){
|
||||||
let from = data[k]
|
let from = data[k]
|
||||||
data[k] = v
|
data[k] = v
|
||||||
//switch( k ){
|
|
||||||
// default: network.opts.scene.dispatchEvent({type:`network.${k}.change`, from, to:v})
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
document.addEventListener('$menu:ready', (e) => {
|
document.addEventListener('frontend:ready', (e) => {
|
||||||
window.network = network(e.detail)
|
window.network = network(e.detail).init()
|
||||||
document.dispatchEvent( new CustomEvent("network:ready", e ) )
|
document.dispatchEvent( new CustomEvent("network:ready", e ) )
|
||||||
$menu.buttons = ([`<a class="btn" aria-label="button" aria-title="connect button" aria-description="start text/audio/video chat" id="meeting" onclick="$connections.show()">🧑🤝🧑 connect</a><br>`])
|
|
||||||
.concat($menu.buttons)
|
|
||||||
})
|
})
|
||||||
|
|
|
||||||
248
src/3rd/js/plugin/frontend/snackbar.js
Normal file
248
src/3rd/js/plugin/frontend/snackbar.js
Normal 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>
|
||||||
|
`
|
||||||
|
|
@ -4,7 +4,7 @@ window.matrix = (opts) => new Proxy({
|
||||||
|
|
||||||
plugin:{
|
plugin:{
|
||||||
type: 'network',
|
type: 'network',
|
||||||
name: '[matrix] channel',
|
name: '[Matrix]',
|
||||||
description: 'a standardized decentralized privacy-friendly protocol',
|
description: 'a standardized decentralized privacy-friendly protocol',
|
||||||
url: 'https://matrix.org',
|
url: 'https://matrix.org',
|
||||||
protocol: 'matrix://',
|
protocol: 'matrix://',
|
||||||
|
|
@ -53,14 +53,13 @@ window.matrix = (opts) => new Proxy({
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<small style="display:inline-block;float:right">Support for Oauth / OpenID is <a href="https://matrix.org/blog/#openid-connect" target="_blank">in progress</a></small>
|
<small style="display:inline-block;float:right">Support for Oauth / OpenID is <a href="https://matrix.org/blog/#openid-connect" target="_blank">in progress</a></small>
|
||||||
<br><br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
init(){
|
init(){
|
||||||
let network = window.network
|
frontend.plugin['matrix'] = this
|
||||||
network.plugin['matrix'] = this
|
|
||||||
$connections.chatnetwork = $connections.chatnetwork.concat([this])
|
$connections.chatnetwork = $connections.chatnetwork.concat([this])
|
||||||
$connections.scene = $connections.scene.concat([this])
|
$connections.scene = $connections.scene.concat([this])
|
||||||
this.reactToConnectionHrefs()
|
this.reactToConnectionHrefs()
|
||||||
|
|
|
||||||
|
|
@ -13,81 +13,242 @@ window.trystero = (opts) => new Proxy({
|
||||||
},
|
},
|
||||||
|
|
||||||
html: {
|
html: {
|
||||||
generic: (opts) => ``
|
generic: (opts) => `<div>
|
||||||
|
<a href="${opts.url}" target="_blank" class="badge ruler">P2P</a>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>nickname</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="nickname" placeholder="your nickname" maxlength="18"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
},
|
},
|
||||||
|
|
||||||
handle: null, // { selfId: .... } when connected
|
room: null, // { selfId: .... } when connected
|
||||||
ip: null,
|
link: '',
|
||||||
roomid: '',
|
selfId: null,
|
||||||
selfId: null,
|
selfStream: null,
|
||||||
connected: false,
|
nickname: '',
|
||||||
|
connected: false,
|
||||||
|
|
||||||
|
useWebcam: false,
|
||||||
|
useChat: false,
|
||||||
|
useScene: false,
|
||||||
|
|
||||||
|
videos: {},
|
||||||
|
|
||||||
names: {},
|
names: {},
|
||||||
|
ping: { send: null, get: null },
|
||||||
chat: { send: null, get: null },
|
chat: { send: null, get: null },
|
||||||
name: { send: null, get: null },
|
name: { send: null, get: null },
|
||||||
|
href: { send: null, get: null },
|
||||||
|
|
||||||
init(){
|
init(){
|
||||||
let network = window.network
|
frontend.plugin['trystero'] = this
|
||||||
network.plugin['trystero'] = this
|
$connections.webcam = $connections.webcam.concat([this])
|
||||||
$connections.webcam = $connections.webcam.concat([this])
|
|
||||||
$connections.chatnetwork = $connections.chatnetwork.concat([this])
|
$connections.chatnetwork = $connections.chatnetwork.concat([this])
|
||||||
$connections.scene = $connections.scene.concat([this])
|
$connections.scene = $connections.scene.concat([this])
|
||||||
|
if( localStorage.getItem("selfId") ){
|
||||||
|
this.selfId = localStorage.getItem("selfId")
|
||||||
|
}else{
|
||||||
|
this.selfId = String(Math.random()).substr(2)
|
||||||
|
localStorage.setItem("selfId",this.selfId)
|
||||||
|
}
|
||||||
this.reactToConnectionHrefs()
|
this.reactToConnectionHrefs()
|
||||||
|
this.nickname = localStorage.getItem("nickname") || `human${String(Math.random()).substr(5,4)}`
|
||||||
|
document.addEventListener('network.connect', (e) => this.connect(e.detail) )
|
||||||
|
document.addEventListener('network.init', () => {
|
||||||
|
let meeting = network.getMeetingFromUrl(document.location.href)
|
||||||
|
if( meeting.match(this.plugin.protocol) ){
|
||||||
|
this.parseLink( meeting )
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
connect(opts){
|
confirmConnected(){
|
||||||
|
if( !this.connected ){
|
||||||
|
this.connected = true
|
||||||
|
frontend.emit('network.connected',{plugin:this})
|
||||||
|
this.names[ this.selfId ] = this.nickname
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async connect(opts){
|
||||||
// embedded https://github.com/dmotz/trystero (trystero-torrent.min.js build)
|
// embedded https://github.com/dmotz/trystero (trystero-torrent.min.js build)
|
||||||
console.log("connecting "+this.plugin.name)
|
console.log("connecting "+this.plugin.name)
|
||||||
console.dir(opts)
|
this.createLink() // ensure link
|
||||||
//this.handle = joinRoom( room.config, room.link )
|
if( opts.selectedWebcam == this.plugin.name ) this.useWebcam = true
|
||||||
//this.send({message:'📡 [trystero] opening P2P WebRTC-channel via bittorrent',class:['info']})
|
if( opts.selectedChatnetwork == this.plugin.name ) this.useChat = true
|
||||||
|
if( opts.selectedScene == this.plugin.name ) this.useScene = true
|
||||||
|
if( this.useWebcam || this.useChat || this.useScene ){
|
||||||
|
|
||||||
|
console.log("trystero link: "+this.link)
|
||||||
|
this.room = joinRoom( {appId: 'xrfragment'}, this.link )
|
||||||
|
|
||||||
|
$chat.send({message:`Share the meeting link <a onclick="$menu.share()">by clicking here</a>`,class:['info']})
|
||||||
|
$chat.send({message:"waiting for other humans..",class:['info']})
|
||||||
|
|
||||||
|
// setup trystero events
|
||||||
|
const [sendPing, getPing] = this.room.makeAction('ping')
|
||||||
|
this.ping.send = sendPing
|
||||||
|
this.ping.get = getPing
|
||||||
|
|
||||||
|
const [sendName, getName] = this.room.makeAction('name')
|
||||||
|
this.name.send = sendName
|
||||||
|
this.name.get = getName
|
||||||
|
|
||||||
|
// start pinging
|
||||||
|
this.ping.pinger = setInterval( () => this.ping.send({ping:true}), 3000 )
|
||||||
|
this.ping.get((data,peerId) => this.confirmConnected() )
|
||||||
|
|
||||||
|
// listen for peers naming themselves
|
||||||
|
this.name.get((name, peerId) => {
|
||||||
|
this.confirmConnected()
|
||||||
|
this.names[peerId] = name
|
||||||
|
})
|
||||||
|
// send name to peers who join later
|
||||||
|
this.room.onPeerJoin( (peerId) => {
|
||||||
|
this.confirmConnected()
|
||||||
|
this.names[peerId] = name
|
||||||
|
this.name.send(this.nickname, peerId )
|
||||||
|
$chat.send({message:"a new human joined",class:['info']})
|
||||||
|
})
|
||||||
|
// delete name of people leaving
|
||||||
|
this.room.onPeerLeave( (peerId) => delete this.names[peerId] )
|
||||||
|
|
||||||
|
if( this.useWebcam ) this.initWebcam()
|
||||||
|
if( this.useChat ) this.initChat()
|
||||||
|
if( this.useScene ) this.initScene()
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
initChat(){
|
||||||
|
const [sendChat, getChat] = this.room.makeAction('chat')
|
||||||
|
this.chat.send = sendChat
|
||||||
|
this.chat.get = getChat
|
||||||
|
|
||||||
|
document.addEventListener('network.send', (e) => {
|
||||||
|
this.chat.send({...e.detail, from: this.nickname, pos: network.pos }) // send to P2P network
|
||||||
|
})
|
||||||
|
// prime chatlog of other people joining
|
||||||
|
this.room.onPeerJoin( (peerId) => {
|
||||||
|
if( $chat.getChatLog().length > 0 ) this.chat.send({prime: $chat.getChatLog() }, peerId )
|
||||||
|
})
|
||||||
|
// listen for chatmsg
|
||||||
|
this.chat.get((data, peerId) => {
|
||||||
|
if( data.prime ){ // first prime is 'truth'
|
||||||
|
if( this.chat.primed || $chat.getChatLog().length > 0 ) return // only prime once
|
||||||
|
$chat.$messages.innerHTML += data.prime
|
||||||
|
$chat.$messages.scrollTop = $chat.$messages.scrollHeight // scroll down
|
||||||
|
this.chat.primed = true
|
||||||
|
}else $chat.send({ ...data}) // send to screen
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
async initWebcam(){
|
||||||
|
// get a local audio stream from the microphone
|
||||||
|
this.selfStream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: $connections.$audioInput.value,
|
||||||
|
video: $connections.$videoInput.value
|
||||||
|
})
|
||||||
|
this.room.addStream(this.selfStream)
|
||||||
|
this.videos[ this.selfId ] = this.getVideo(this.selfId,{stream: this.selfStream})
|
||||||
|
|
||||||
|
// send stream + chatlog to peers who join later
|
||||||
|
this.room.onPeerJoin( (peerId) => this.room.addStream( this.selfStream, peerId))
|
||||||
|
|
||||||
|
this.room.onPeerStream((stream, peerId) => {
|
||||||
|
let video = this.getVideo(peerId,{create:true, stream})
|
||||||
|
this.videos[ this.names[peerId] || peerId ] = video
|
||||||
|
})
|
||||||
|
|
||||||
|
this.room.onPeerLeave( (peerId) => {
|
||||||
|
let video = this.getVideo(peerId)
|
||||||
|
if( video ){
|
||||||
|
video.remove()
|
||||||
|
delete this.videos[peerId]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
initScene(){
|
||||||
|
// setup trystero events
|
||||||
|
const [sendHref, getHref] = this.room.makeAction('name')
|
||||||
|
this.href.send = sendHref
|
||||||
|
this.href.get = getHref
|
||||||
|
this.href.get((data,peerId) => {
|
||||||
|
xrf.hashbus.pub(data.href)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getVideo(peerId,opts){
|
||||||
|
opts = opts || {}
|
||||||
|
let video = this.videos[ this.names[peerId] ] || this.videos[ peerId ]
|
||||||
|
if (!video && opts.create) {
|
||||||
|
video = document.createElement('video')
|
||||||
|
video.autoplay = true
|
||||||
|
|
||||||
|
// add video element to the DOM
|
||||||
|
if( opts.stream ) video.srcObject = opts.stream
|
||||||
|
console.log("creating video for peerId")
|
||||||
|
$chat.$videos.appendChild(video)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
send(opts){ $chat.send({...opts, source: 'trystero'}) },
|
send(opts){ $chat.send({...opts, source: 'trystero'}) },
|
||||||
|
|
||||||
createLink(opts){
|
createLink(opts){
|
||||||
this.link = document.location.href.replace(/#.*/,'')
|
let hash = document.location.hash
|
||||||
if( this.link.match(/localhost/) ){
|
if( !this.link ){
|
||||||
fetch('https://api.duckduckgo.com/?q=my+ip&format=json')
|
const meeting = network.getMeetingFromUrl(document.location.href)
|
||||||
.then( (res) => res.json() )
|
this.link = meeting.match("trystero://") ? meeting : `trystero://r/${network.randomRoom()}:bittorrent`
|
||||||
.then( (res) => {
|
|
||||||
const ipRegex = /Your IP address is ([0-9]+[.][0-9]+[.][0-9]+[.][0-9]+)/g;
|
|
||||||
const ip = ipRegex.exec(res.Answer)[1]
|
|
||||||
this.link = this.link.replace(/localhost/, ip )
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
if( !hash.match('meet=') ) document.location.hash += `${hash.length > 1 ? '&' : '#'}meet=${this.link}`
|
||||||
},
|
},
|
||||||
|
|
||||||
config(opts){
|
config(opts){
|
||||||
opts = {...opts, ...this.plugin }
|
opts = {...opts, ...this.plugin }
|
||||||
let el = document.createElement('div')
|
this.el = document.createElement('div')
|
||||||
let html = this.html.generic(opts)
|
this.el.innerHTML = this.html.generic(opts)
|
||||||
for( let i in opts ){
|
// window.notify(`${opts.name} is ${opts.description} <br>by using a serverless technology called <a href="https://webrtc.org/" target="_blank">webRTC</a> via <a href="${opts.url}" target="_blank">trystero</a>.<br>You can basically make up your own channelname or choose an existing one.<br>Use this for hasslefree anonymous meetings.`)
|
||||||
if( this.html[i] ) html += this.html[i](opts)
|
this.el.querySelector('#nickname').value = this.nickname
|
||||||
}
|
this.el.querySelector('#nickname').addEventListener('change', (e) => localStorage.setItem("nickname",e.target.value) )
|
||||||
window.notify(`${opts.name} is ${opts.description} <br>by using a serverless technology called <a href="https://webrtc.org/" target="_blank">webRTC</a> via <a href="${opts.url}" target="_blank">trystero</a>.<br>You can basically make up your own channelname or choose an existing one.<br>Use this for hasslefree anonymous meetings.`)
|
|
||||||
// resolve ip
|
// resolve ip
|
||||||
if( !this.link ) this.createLink(opts)
|
return this.el
|
||||||
return el
|
},
|
||||||
|
|
||||||
|
parseLink(url){
|
||||||
|
if( !url.match(this.plugin.protocol) ) return
|
||||||
|
let parts = url.replace(this.plugin.protocol,'').split("/")
|
||||||
|
if( parts[0] == 'r' ){ // this.room
|
||||||
|
let roomid = parts[1].replace(/:.*/,'')
|
||||||
|
let server = parts[1].replace(/.*:/,'')
|
||||||
|
if( server != 'bittorrent' ) return window.notify("only bittorrent is supported for trystero (for now) :/")
|
||||||
|
this.link = url
|
||||||
|
$connections.show()
|
||||||
|
$connections.selectedWebcam = this.plugin.name
|
||||||
|
$connections.selectedChatnetwork= this.plugin.name
|
||||||
|
$connections.selectedScene = this.plugin.name
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
},
|
},
|
||||||
|
|
||||||
reactToConnectionHrefs(){
|
reactToConnectionHrefs(){
|
||||||
xrf.addEventListener('href', (opts) => {
|
xrf.addEventListener('href', (opts) => {
|
||||||
let {mesh} = opts
|
let {mesh} = opts
|
||||||
if( !opts.click ) return
|
if( !opts.click ) return
|
||||||
if( mesh.userData.href.match(this.protocol) ){
|
this.parseLink(mesh.userData.href)
|
||||||
let parts = mesh.userData.href.replace(this.plugin.protocol,'')
|
let href = mesh.userData.href
|
||||||
console.dir(parts)
|
let isLocal = href[0] == '#'
|
||||||
if( parts[0] == 'r' ){ // room
|
let isTeleport = href.match(/(pos=|http:)/)
|
||||||
this.roomid = parts.split("/")[1].replace(/:.*/,'')
|
if( isLocal && !isTeleport && this.href.send ) this.href.send({href})
|
||||||
this.server = parts.split("/")[1].replace(/.*:/,'')
|
|
||||||
if( this.server != 'bittorrent' ) window.notify("only bittorrent is supported for trystero (for now) :/")
|
|
||||||
$connections.show()
|
|
||||||
$connections.selectedWebcam = this.plugin.name
|
|
||||||
$connections.selectedChatnetwork= this.plugin.name
|
|
||||||
$connections.selectedScene = this.plugin.name
|
|
||||||
console.log("configured trystero")
|
|
||||||
}
|
|
||||||
}else window.notify("malformed connection URI: "+mesh.userData.href)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,12 +270,12 @@ document.addEventListener('$connections:ready', (e) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
//window.meeting = window.meeting||{}
|
//window.meeting = window.meeting||{}
|
||||||
//window.meeting.trystero = async function(el,com,data){
|
//window.meeting.trystero = async function(el,this){
|
||||||
//
|
//
|
||||||
// // embed https://github.com/dmotz/trystero (trystero-torrent.min.js build)
|
// // embed https://github.com/dmotz/trystero (trystero-torrent.min.js build)
|
||||||
// const { joinRoom } = await import("./../../../dist/trystero-torrent.min.js");
|
// const { joinRoom } = await import("./../../../dist/trystero-torrent.min.js");
|
||||||
// this.room = {
|
// this.room = {
|
||||||
// handle: null,
|
// this.room: null,
|
||||||
// link: null,
|
// link: null,
|
||||||
// selfId: null,
|
// selfId: null,
|
||||||
// names: {},
|
// names: {},
|
||||||
|
|
@ -128,18 +289,18 @@ document.addEventListener('$connections:ready', (e) => {
|
||||||
// this.send = (opts) => com.send({...opts, source: 'trystero'})
|
// this.send = (opts) => com.send({...opts, source: 'trystero'})
|
||||||
//
|
//
|
||||||
// el.addEventListener('remove', () => {
|
// el.addEventListener('remove', () => {
|
||||||
// if( this.room.handle ) this.room.handle.leave()
|
// if( this.room.room ) this.room.room.leave()
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// el.addEventListener('connect', async () => {
|
// el.addEventListener('connect', async () => {
|
||||||
// let room = this.room
|
// let this.room = this.room
|
||||||
//
|
//
|
||||||
// room.link = this.data.link
|
// this.room.link = this.data.link
|
||||||
// if( !room.linkmatch(/(#|&)meet/) ){
|
// if( !room.linkmatch(/(#|&)meet/) ){
|
||||||
// room.link = room.link.match(/#/) ? '&meet' : '#meet'
|
// this.room.link = this.room.link.match(/#/) ? '&meet' : '#meet'
|
||||||
// }
|
// }
|
||||||
// room.handle = joinRoom( room.config, room.link )
|
// this.room.room = joinRoom( this.room.config, this.room.link )
|
||||||
// room.selfId = room.handle.selfId
|
// this.selfId = this.room.selfId
|
||||||
//
|
//
|
||||||
// this.send({
|
// this.send({
|
||||||
// message: "joined meeting at "+roomname.replace(/(#|&)meet/,''), // dont trigger init()
|
// message: "joined meeting at "+roomname.replace(/(#|&)meet/,''), // dont trigger init()
|
||||||
|
|
@ -154,56 +315,56 @@ document.addEventListener('$connections:ready', (e) => {
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// // setup trystero events
|
// // setup trystero events
|
||||||
// const [sendName, getName] = room.makeAction('name')
|
// const [sendName, getName] = this.room.makeAction('name')
|
||||||
// const [sendChat, getChat] = room.makeAction('chat')
|
// const [sendChat, getChat] = this.room.makeAction('chat')
|
||||||
// room.chat.send = sendChat
|
// this.chat.send = sendChat
|
||||||
// room.chat.get = getChat
|
// this.chat.get = getChat
|
||||||
// room.name.send = sendName
|
// this.name.send = sendName
|
||||||
// room.name.get = getName
|
// this.name.get = getName
|
||||||
//
|
//
|
||||||
// // tell other peers currently in the room our name
|
// // tell other peers currently in the this.room our name
|
||||||
// room.names[ room.selfId ] = com.data.visitorname.substr(0,15)
|
// this.names[ this.selfId ] = this.nickname.substr(0,15)
|
||||||
// room.name.send( com.data.visitorname )
|
// this.name.send( this.nickname )
|
||||||
//
|
//
|
||||||
// // listen for peers naming themselves
|
// // listen for peers naming themselves
|
||||||
// this.name.get((name, peerId) => (room.names[peerId] = name))
|
// this.name.get((name, peerId) => (room.names[peerId] = name))
|
||||||
//
|
//
|
||||||
// // send self stream to peers currently in the room
|
// // send self stream to peers currently in the this.room
|
||||||
// room.addStream(com.selfStream)
|
// this.room.addStream(this.selfStream)
|
||||||
//
|
//
|
||||||
// // send stream + chatlog to peers who join later
|
// // send stream + chatlog to peers who join later
|
||||||
// room.onPeerJoin( (peerId) => {
|
// this.room.onPeerJoin( (peerId) => {
|
||||||
// room.addStream( com.selfStream, peerId)
|
// this.room.addStream( this.selfStream, peerId)
|
||||||
// room.name.send( com.data.visitorname, peerId)
|
// this.name.send( this.nickname, peerId)
|
||||||
// room.chat.send({prime: com.log}, peerId )
|
// this.chat.send({prime: com.log}, peerId )
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// room.onPeerLeave( (peerId) => {
|
// this.room.onPeerLeave( (peerId) => {
|
||||||
// console.log(`${room.names[peerId] || 'a visitor'} left`)
|
// console.log(`${room.names[peerId] || 'a visitor'} left`)
|
||||||
// if( com.videos[peerId] ){
|
// if( com.videos[peerId] ){
|
||||||
// com.videos[peerId].remove()
|
// com.videos[peerId].remove()
|
||||||
// delete com.videos[peerId]
|
// delete com.videos[peerId]
|
||||||
// }
|
// }
|
||||||
// delete room.names[peerId]
|
// delete this.names[peerId]
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// // handle streams from other peers
|
// // this.room streams from other peers
|
||||||
// room.onPeerStream((stream, peerId) => {
|
// this.room.onPeerStream((stream, peerId) => {
|
||||||
// // create an audio instance and set the incoming stream
|
// // create an audio instance and set the incoming stream
|
||||||
// const audio = new Audio()
|
// const audio = new Audio()
|
||||||
// audio.srcObject = stream
|
// audio.srcObject = stream
|
||||||
// audio.autoplay = true
|
// audio.autoplay = true
|
||||||
// // add the audio to peerAudio object if you want to address it for something
|
// // add the audio to peerAudio object if you want to address it for something
|
||||||
// // later (volume, etc.)
|
// // later (volume, etc.)
|
||||||
// com.data.audios[peerId] = audio
|
// this.audios[peerId] = audio
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// room.onPeerStream((stream, peerId) => {
|
// this.room.onPeerStream((stream, peerId) => {
|
||||||
// com.createVideoElement(stream,peerId)
|
// com.createVideoElement(stream,peerId)
|
||||||
// })
|
// })
|
||||||
//
|
//
|
||||||
// // listen for chatmsg
|
// // listen for chatmsg
|
||||||
// room.chat.get((data, peerId) => {
|
// this.chat.get((data, peerId) => {
|
||||||
// if( data.prime ){
|
// if( data.prime ){
|
||||||
// if( com.log.length > 0 ) return // only prime once
|
// if( com.log.length > 0 ) return // only prime once
|
||||||
// console.log("receiving prime")
|
// console.log("receiving prime")
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@ xrf.emit.normal = function(eventName, data) {
|
||||||
var callbacks = xrf._listeners[eventName]
|
var callbacks = xrf._listeners[eventName]
|
||||||
if (callbacks) {
|
if (callbacks) {
|
||||||
for (var i = 0; i < callbacks.length; i++) {
|
for (var i = 0; i < callbacks.length; i++) {
|
||||||
|
try{
|
||||||
callbacks[i](data);
|
callbacks[i](data);
|
||||||
|
}catch(e){ console.error(e) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -96,12 +96,21 @@ xrf.reset = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
xrf.parseUrl = (url) => {
|
xrf.parseUrl = (url) => {
|
||||||
const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
|
let urlExHash = url.replace(/#.*/,'')
|
||||||
|
let urlObj,file
|
||||||
|
let store = {}
|
||||||
|
try{
|
||||||
|
urlObj = new URL( urlExHash.match(/:\/\//) ? urlExHash : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
|
||||||
|
file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
|
||||||
|
let search = urlObj.search.substr(1).split("&")
|
||||||
|
for( let i in search ) store[ (search[i].split("=")[0]) ] = search[i].split("=")[1] || ''
|
||||||
|
}catch(e){ }
|
||||||
|
let hashmap = url.match("#") ? url.replace(/.*#/,'').split("&") : []
|
||||||
|
for( let i in hashmap ) store[ (hashmap[i].split("=")[0]) ] = hashmap[i].split("=")[1] || ''
|
||||||
let dir = url.substring(0, url.lastIndexOf('/') + 1)
|
let dir = url.substring(0, url.lastIndexOf('/') + 1)
|
||||||
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
|
|
||||||
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
|
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
|
||||||
const ext = file.split('.').pop()
|
const ext = file.split('.').pop()
|
||||||
return {urlObj,dir,file,hash,ext}
|
return {urlObj,dir,file,hash,ext,store}
|
||||||
}
|
}
|
||||||
|
|
||||||
xrf.add = (object) => {
|
xrf.add = (object) => {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,8 @@ xrf.navigator.to = (url,flags,loader,data) => {
|
||||||
return new Promise( (resolve,reject) => {
|
return new Promise( (resolve,reject) => {
|
||||||
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
|
||||||
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
|
if( !file || (!data && xrf.model.file == file) ){ // we're already loaded
|
||||||
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
|
if( hash == document.location.hash.substr(1) ) return // block duplicate calls
|
||||||
|
hashbus.pub( url, xrf.model, flags ) // and eval local URI XR fragments
|
||||||
xrf.navigator.updateHash(hash)
|
xrf.navigator.updateHash(hash)
|
||||||
return resolve(xrf.model)
|
return resolve(xrf.model)
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +73,6 @@ xrf.navigator.updateHash = (hash,opts) => {
|
||||||
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
|
if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers
|
||||||
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
|
console.log(`URL: ${document.location.search.substr(1)}#${hash}`)
|
||||||
document.location.hash = hash
|
document.location.hash = hash
|
||||||
xrf.emit('hash', {...opts, hash: `#${hash}` })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
xrf.navigator.pushState = (file,hash) => {
|
xrf.navigator.pushState = (file,hash) => {
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ xrf.frag.defaultPredefinedViews = (opts) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// react to enduser typing url
|
|
||||||
xrf.addEventListener('hash', (opts) => xrf.hashbus.pub( opts.hash ) )
|
|
||||||
|
|
||||||
// clicking href url with predefined view
|
// clicking href url with predefined view
|
||||||
xrf.addEventListener('href', (opts) => {
|
xrf.addEventListener('href', (opts) => {
|
||||||
if( !opts.click || opts.xrf.string[0] != '#' ) return
|
if( !opts.click || opts.xrf.string[0] != '#' ) return
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,8 @@ xrf.frag.href = function(v, opts){
|
||||||
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
|
const flags = v.string[0] == '#' ? xrf.XRF.PV_OVERRIDE : undefined
|
||||||
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
|
let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA )
|
||||||
// *TODO* support for multiple protocols
|
// *TODO* support for multiple protocols
|
||||||
if( !v.string.match(/^http/) ) return
|
if( v.string[0] != '#' && !v.string.match(/^http/) ) return
|
||||||
// always commit current location (keep a trail of last positions before we navigate)
|
// always commit current location in case of teleport (keep a trail of last positions before we navigate)
|
||||||
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
|
if( !e.nocommit && !document.location.hash.match(lastPos) ) xrf.navigator.to(`#${lastPos}`)
|
||||||
xrf.navigator.to(v.string) // let's surf to HREF!
|
xrf.navigator.to(v.string) // let's surf to HREF!
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue