2024-01-03 14:23:34 +00:00
// 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" > < / d i v >
< button id = "navback" onclick = "history.back()" > & # 8249 ; < / b u t t o n >
< button id = "navforward" onclick = "history.forward()" > & # 8250 ; < / b u t t o n >
< input id = "load" type = "submit" value = "load 3D file" > < / i n p u t >
2024-02-29 14:07:20 +00:00
< input type = "text" id = "uri" value = "" onchange = "AFRAME.XRF.navigator.to( $('#uri').value )" style = "display:none" / >
2024-01-03 14:23:34 +00:00
< / d i v >
` ,
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 ( )
2024-01-10 22:01:21 +01:00
. setupNetworkListeners ( )
2024-01-03 14:23:34 +00:00
. hidetopbarWhenMenuCollapse ( )
2024-01-29 20:17:57 +01:00
. hideUIWhenNavigating ( )
2024-01-03 14:23:34 +00:00
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 ) )
2024-02-29 09:57:20 +00:00
2024-02-29 13:36:00 +00:00
setTimeout ( ( ) => {
window . notify ( "use WASD-keys and mouse-drag to move around" , { timeout : false } )
xrf . addEventListener ( 'navigate' , ( ) => SnackBar ( ) ) // close dialogs when url changes
} , 2000 )
2024-02-29 09:57:20 +00:00
xrf . addEventListener ( 'href' , ( data ) => {
2024-02-29 14:07:20 +00:00
if ( ! data . selected ) return
2024-02-29 13:36:00 +00:00
let html = ` <b class="badge"> ${ data . mesh . isSRC && ! data . mesh . portal ? 'src' : 'href' } </b> ${ data . xrf ? data . xrf . string : data . mesh . userData . src } <br> `
2024-02-29 14:07:20 +00:00
let metadata = data . mesh . userData
2024-02-29 09:57:20 +00:00
let meta = xrf . Parser . getMetaData ( )
let hasMeta = false
for ( let label in meta ) {
let fields = meta [ label ]
for ( let i = 0 ; i < fields . length ; i ++ ) {
let field = fields [ i ]
if ( metadata [ field ] ) {
hasMeta = true
html += ` <br><b style="min-width:110px;display:inline-block"> ${ label } :</b> ${ metadata [ field ] } \n `
break
}
}
}
let transcript = ''
let root = data . mesh . portal ? data . mesh . portal . stencilObject : data . mesh
root . traverse ( ( n ) => {
if ( n . userData [ 'aria-description' ] && n . uuid != data . mesh . uuid ) {
transcript += ` <b># ${ n . name } </b> ${ n . userData [ 'aria-description' ] } . `
}
} )
if ( transcript . length ) html += ` <br><b>transcript:</b><br><div class="transcript"> ${ transcript } </div> `
if ( hasMeta && ! data . mesh . portal ) html += ` <br><br><a class="btn" style="float:right" onclick="xrf.navigator.to(' ${ data . mesh . userData . href } ')">Visit embedded scene</a> `
window . notify ( html , { timeout : 7000 * ( hasMeta ? 1.5 : 1 ) } )
} )
2024-01-03 14:23:34 +00:00
} , 100 )
return this
} ,
2024-01-10 22:01:21 +01:00
setupNetworkListeners ( ) {
document . addEventListener ( 'network.connect' , ( e ) => {
console . log ( "network.connect" )
2024-02-29 14:07:20 +00:00
window . notify ( "🪐 connecting to awesomeness.." )
2024-01-10 22:01:21 +01:00
$chat . send ( { message : ` 🪐 connecting to awesomeness.. ` , class : [ 'info' ] , timeout : 5000 } )
} )
document . addEventListener ( 'network.connected' , ( e ) => {
2024-02-29 14:07:20 +00:00
window . notify ( "🪐 connected to awesomeness.." )
2024-01-10 22:01:21 +01:00
$chat . visibleChatbar = true
$chat . send ( { message : ` 🎉 ${ e . detail . plugin . profile . name || '' } connected! ` , class : [ 'info' ] , timeout : 5000 } )
} )
document . addEventListener ( 'network.disconnect' , ( ) => {
2024-02-29 14:07:20 +00:00
window . notify ( "🪐 disconnecting.." )
2024-01-10 22:01:21 +01:00
} )
document . addEventListener ( 'network.info' , ( e ) => {
window . notify ( e . detail . message )
$chat . send ( { ... e . detail , class : [ 'info' ] , timeout : 5000 } )
} )
document . addEventListener ( 'network.error' , ( e ) => {
window . notify ( e . detail . message )
$chat . send ( { ... e . detail , class : [ 'info' ] , timeout : 5000 } )
} )
return this
} ,
2024-01-03 14:23:34 +00:00
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
} ,
2024-01-29 20:17:57 +01:00
hideUIWhenNavigating ( ) {
// hide ui when user is navigating the scene using mouse/touch
let showUI = ( show ) => ( e ) => {
let isChatMsg = e . target . closest ( '.msg' )
let isChatLine = e . target . id == 'chatline'
let isChatEmptySpace = e . target . id == 'messages'
let isUI = e . target . closest ( '.ui' )
//console.dir({class: e.target.className, id: e.target.id, isChatMsg,isChatLine,isChatEmptySpace,isUI, tagName: e.target.tagName})
if ( isUI || e . target . tagName . match ( /^(BUTTON|TEXTAREA|INPUT|A)/ ) || e . target . className . match ( /(btn)/ ) ) return
if ( show ) {
$chat . visible = true
} else {
2024-02-29 14:07:20 +00:00
$chat . visible = false
2024-01-29 20:17:57 +01:00
$menu . toggle ( false )
}
return true
}
document . addEventListener ( 'mousedown' , showUI ( false ) )
document . addEventListener ( 'mouseup' , showUI ( true ) )
document . addEventListener ( 'touchstart' , showUI ( false ) )
document . addEventListener ( 'touchend' , showUI ( true ) )
} ,
2024-01-03 14:23:34 +00:00
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 ) ;
2024-02-29 14:07:20 +00:00
let file = files . slice ? files [ 0 ] : files
2024-01-03 14:23:34 +00:00
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' ] } )
2024-02-29 14:07:20 +00:00
opts = opts || { status : 'info' }
2024-01-03 14:23:34 +00:00
opts = Object . assign ( { status , timeout : 4000 } , opts )
opts . message = _str
if ( typeof str == 'string' ) {
2024-02-29 14:07:20 +00:00
str = _str . replace ( /(^\w+):/ , "<div class='badge'>\$1</div>" )
if ( ! opts . status ) {
2024-01-03 14:23:34 +00:00
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
2024-02-29 14:07:20 +00:00
if ( typeof THREE == 'undefined' ) THREE = xrf . THREE
2024-01-03 14:23:34 +00:00
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 ) {
2024-02-29 14:07:20 +00:00
// copy url to clipboard
2024-01-03 14:23:34 +00:00
var dummy = document . createElement ( 'input' )
document . body . appendChild ( dummy ) ;
dummy . value = text ;
dummy . select ( ) ;
document . execCommand ( 'copy' ) ;
2024-02-29 14:07:20 +00:00
document . body . removeChild ( dummy ) ;
2024-01-03 14:23:34 +00:00
} ,
share ( opts ) {
2024-01-10 22:01:21 +01:00
opts = opts || { notify : true , qr : true , share : true , linkonly : false }
2024-01-29 20:17:57 +01:00
if ( network . meetingLink && ! document . location . hash . match ( /meet=/ ) ) {
document . location . hash += ` &meet= ${ network . meetingLink } `
}
if ( ! document . location . hash . match ( /pos=/ ) ) {
document . location . hash += ` &pos= ${ network . posName || network . pos } `
2024-01-03 14:23:34 +00:00
}
let url = window . location . href
2024-02-29 14:07:20 +00:00
if ( opts . linkonly ) return url
2024-01-03 14:23:34 +00:00
this . copyToClipboard ( url )
2024-02-29 14:07:20 +00:00
// End of *TODO*
2024-01-03 14:23:34 +00:00
if ( opts . notify ) {
2024-01-09 11:05:13 +00:00
window . notify ( ` <h2> ${ network . connected ? 'Meeting link ' : 'Link' } copied to clipboard!</h2>
Now share it with your friends ❤ ️ < br >
2024-01-03 14:23:34 +00:00
< canvas id = "qrcode" width = "121" height = "121" > < / c a n v a s > < b r >
< button onclick = "frontend.download()" > < i class = "gg-software-download" > < / i > & n b s p ; & n b s p ; & n b s p ; d o w n l o a d s c e n e f i l e < / b u t t o n > < b r >
< button onclick = "alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')" > < i class = "gg-image" > < / i > & n b s p ; & n b s p ; d o w n l o a d 3 6 0 s c r e e n s h o t < / b u t t o n > < b r >
< a class = "btn" target = "_blank" href = "https://github.com/coderofsalvation/xrfragment-helloworld" > < i class = "gg-serverless" > < / i > & n b s p ; & n b s p ; & n b s p ; c l o n e & s e l f h o s t t h i s e x p e r i e n c e < / a > < b r >
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 >
2024-01-09 11:05:13 +00:00
< br >
2024-01-03 14:23:34 +00:00
` ,{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'
} )
}
2024-02-29 14:07:20 +00:00
$menu . collapse = true
2024-01-03 14:23:34 +00:00
}
} ,
2024-02-29 14:07:20 +00:00
{
// auto-trigger events on changes
2024-01-03 14:23:34 +00:00
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 ;
}
}
} )
2024-02-29 14:07:20 +00:00
2024-01-03 14:23:34 +00:00
frontend = frontend ( { xrf , document } ) . init ( )