// reactive component for displaying the menu menuComponent = { html: `
`, init: (el) => new Proxy({ morelabel: '⚡', collapsed: false, logo: './../../assets/logo.png', buttons: [`🔗 share
`], $overlay: $overlay = el.querySelector('#overlay'), $logo: $logo = el.querySelector('.logo'), $uri: $uri = el.querySelector('#uri'), $buttons: $buttons = el.querySelector('#buttons'), $btnMore: $btnMore = el.querySelector('#more'), toggle: () => $menu.collapsed = !$menu.collapsed, install: (xrf) => { this.xrf = xrf $menu.bindToWindow() // bind functions like notify to window window.notify('loading '+document.location.search.substr(1)) document.body.appendChild(el) document.dispatchEvent( new CustomEvent("$menu:ready", {detail: xrf}) ) // add screenshot component with camera to capture bigger size equirects // document.querySelector('a-scene').components.screenshot.capture('perspective') $('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2}) if( window.outerWidth > 800 ) setTimeout( () => window.notify("use WASD-keys and mouse-drag to move around",{timeout:false}),2000 ) xrf.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false ) // enable user-uploaded asset files let fileLoaders = $menu.loadFile({ ".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ), ".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ) }) el.querySelector("#overlay > input[type=submit]").addEventListener("click", fileLoaders ) } },{ get(data,k,v){ return data[k] }, set(data,k,v){ data[k] = v switch( k ){ case "logo": $logo.style.backgroundImage = `url(${v})`; break; case "css": document.head.innerHTML += v; break; case "morelabel": $btnMore.innerText = data.morelabel; break; case "buttons": $buttons.innerHTML = this.renderButtons(data); document.dispatchEvent( new CustomEvent("$menu:buttons:render", {detail: el.querySelector('.menu') }) ) break; case "collapsed": $overlay.style.display = data.collapsed ? 'block' : 'none' $buttons.style.display = data.collapsed ? 'block' : 'none' break; } }, renderButtons: (data) => `${data.buttons.join('')}` }) } // reactify component! $menu = document.createElement('div') $menu.innerHTML = menuComponent.html $menu = menuComponent.init($menu) // attach menu functions which are less related to rendering let utils = { bindToWindow(opts){ window.notify = $menu.notify(window) // reroute console messages to snackbar notifications console.log = ( (log,console) => function(str){ if( String(str).match(/(:.*#|note:|:\/\/)/) ) window.notify( str ) log.call(console,str) })(console.log, console) // allow iframe to open url window.addEventListener('message', (event) => { if (event.data && event.data.url) { window.open(event.data.url, '_blank'); } }); }, loadFile(contentLoaders, multiple){ return () => { window.notify("if you're on Meta browser, file-uploads might be disabled") let input = document.createElement('input'); input.type = 'file'; input.multiple = multiple; input.accept = Object.keys(contentLoaders).join(","); input.onchange = () => { let files = Array.from(input.files); let file = files.slice ? files[0] : files for( var i in contentLoaders ){ let r = new RegExp('\\'+i+'$') if( file.name.match(r) ) return contentLoaders[i](file) } alert(file.name+" is not supported") }; input.click(); } }, SnackBar(userOptions) { var snackbar = this || (window.snackbar = {}); var _Interval; var _Message; var _Element; var _Container; var _OptionDefaults = { message: "Operation performed successfully.", dismissible: true, timeout: 7000, status: "" } var _Options = _OptionDefaults; function _Create() { _Container = document.querySelector(".js-snackbar-container") if( _Container ){ _Container.remove() } _Container = null if (!_Container) { // need to create a new container for notifications _Container = document.createElement("div"); _Container.classList.add("js-snackbar-container"); document.body.appendChild(_Container); } _Container.opts = _Options _Container.innerHTML = '' _Element = document.createElement("div"); _Element.classList.add("js-snackbar__wrapper","xrf"); let innerSnack = document.createElement("div"); innerSnack.classList.add("js-snackbar", "js-snackbar--show"); if (_Options.status) { _Options.status = _Options.status.toLowerCase().trim(); let status = document.createElement("span"); status.classList.add("js-snackbar__status"); if (_Options.status === "success" || _Options.status === "green") { status.classList.add("js-snackbar--success"); } else if (_Options.status === "warning" || _Options.status === "alert" || _Options.status === "orange") { status.classList.add("js-snackbar--warning"); } else if (_Options.status === "danger" || _Options.status === "error" || _Options.status === "red") { status.classList.add("js-snackbar--danger"); } else { status.classList.add("js-snackbar--info"); } innerSnack.appendChild(status); } _Message = document.createElement("span"); _Message.classList.add("js-snackbar__message"); if( typeof _Options.message == 'string' ){ _Message.innerHTML = _Options.message; }else _Message.appendChild(_Options.message) innerSnack.appendChild(_Message); if (_Options.dismissible) { let closeBtn = document.createElement("span"); closeBtn.classList.add("js-snackbar__close"); closeBtn.innerText = "\u00D7"; closeBtn.onclick = snackbar.Close; innerSnack.appendChild(closeBtn); } _Element.style.height = "0px"; _Element.style.opacity = "0"; _Element.style.marginTop = "0px"; _Element.style.marginBottom = "0px"; _Element.appendChild(innerSnack); _Container.appendChild(_Element); if (_Options.timeout !== false) { _Interval = setTimeout(snackbar.Close, _Options.timeout); } } snackbar.Open = function() { let contentHeight = _Element.firstElementChild.scrollHeight; // get the height of the content _Element.style.height = contentHeight + "px"; _Element.style.opacity = 1; _Element.style.marginTop = "5px"; _Element.style.marginBottom = "5px"; _Element.addEventListener("transitioned", function() { _Element.removeEventListener("transitioned", arguments.callee); _Element.style.height = null; }) } snackbar.Close = function () { if (_Interval) clearInterval(_Interval); let snackbarHeight = _Element.scrollHeight; // get the auto height as a px value let snackbarTransitions = _Element.style.transition; _Element.style.transition = ""; requestAnimationFrame(function() { _Element.style.height = snackbarHeight + "px"; // set the auto height to the px height _Element.style.opacity = 1; _Element.style.marginTop = "0px"; _Element.style.marginBottom = "0px"; _Element.style.transition = snackbarTransitions requestAnimationFrame(function() { _Element.style.height = "0px"; _Element.style.opacity = 0; }) }); setTimeout(function() { try { _Container.removeChild(_Element); } catch (e) { } }, 1000); }; _Options = { ..._OptionDefaults, ...userOptions } _Create(); snackbar.Open(); }, notify(scope){ return function notify(_str,opts){ if( accessibility.enabled ) return $chat.send({message:_str}) str = _str.replace(/(^\w+):/,"
\$1
") 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(`

${ inMeeting ? 'Meeting link ' : 'Link'} copied to clipboard!


Now share it with your friends ❤️



🖥 clone & selfhost this experience

To embed this experience in your blog,
copy/paste the following into your HTML:

`,{timeout:2000000}) // draw QR code setTimeout( () => { let QR = window.QR QR.canvas = document.getElementById('qrcode') QR.draw( url, QR.canvas ) },0) } } // map to component for( let i in utils ) $menu[i] = utils[i] //$('a-scene').addEventListener('XRF', this.onXRFready ) // // if( document.location.search.length > 2 ){ // $('[xrf]').setAttribute('xrf', document.location.search.substr(1)+document.location.hash ) // } // // }, // // onXRFready: function(){ // // let XRF = window.AFRAME.XRF // //setupMenu(XRF){ // // let aScene = document.querySelector('a-scene') // // let urlbar = $('input#uri') // // let inIframe = window.location !== window.parent.location // // let els = [ ...document.querySelectorAll('.menu .btn') ] // // els = els.filter( (el) => el.id != "more" ? el : false ) // // // // enable meetings // // let startMeeting = () => { // // aScene.setAttribute('meeting', 'id: xrfragments') // // $('a#meeting').innerText = '🧑‍🤝‍🧑 breakout meeting' // // $('a#meeting').setAttribute('aria-description','breakout room') // // } // // $('a#meeting').addEventListener('click', () => { // // if( aScene.getAttribute('meeting') ){ // meeting already, start breakout room // // let parentRoom = document.location.href // // $menu.updateHashPosition(true) // // let meeting = $('[meeting]').components['meeting'] // // meeting.data.parentRoom = parentRoom // // meeting.update() // // }else startMeeting() // // }) // // if( document.location.hash.match(/(#|&)meet/) ) startMeeting() // // // XRF.addEventListener('hash', () => reflectUrl() ) // // const reflectUrl = window.reflectUrl = (url) => { // // urlbar.value = url || document.location.search.substr(1) + document.location.hash // // } // // reflectUrl() // //}, // // // // on localhost enable debugging mode for developer convenience // let loc = document.location // if( loc.host.match(/^localhost/) ){ // $('a-scene').setAttribute('stats') // XRF.debug = 1 // } // // // add screenshot component with camera to capture bigger size equirects // // document.querySelector('a-scene').components.screenshot.capture('perspective') // $('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2}) // // if( window.outerWidth > 800 ) // setTimeout( () => window.notify("use WASD-keys and mouse-drag to move around",{timeout:false}),2000 ) // // window.AFRAME.XRF.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false ) // // // enable user-uploaded asset files // let fileLoaders = $menu.loadFile({ // ".gltf": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ), // ".glb": (file) => file.arrayBuffer().then( (data) => xrf.navigator.to(file.name,null, (new xrf.loaders.gltf()), data) ) // }) // $("#overlay > input[type=submit]").addEventListener("click", fileLoaders ) // finally add some css $menu.css = ` `