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