2024-08-01 16:25:05 +02:00
/ *
2025-01-15 10:53:56 +01:00
* v0 . 5.1 generated at Wed Jan 15 10 : 52 : 05 AM CET 2025
2024-08-01 16:25:05 +02:00
* https : //xrfragment.org
* SPDX - License - Identifier : AGPL - 3.0 - or - later
* /
2024-01-03 15:23:34 +01:00
( function ( ) {
2024-03-19 10:53:22 +01:00
// a portable snackbar
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
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 ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
function _Create ( ) {
_Container = document . querySelector ( ".js-snackbar-container" )
if ( _Container ) {
_Container . remove ( )
}
_Container = null
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
if ( ! _Container ) {
// need to create a new container for notifications
_Container = document . createElement ( "div" ) ;
_Container . classList . add ( "js-snackbar-container" ) ;
2024-01-29 21:19:04 +01:00
2024-03-19 10:53:22 +01:00
document . body . appendChild ( _Container ) ;
}
_Container . opts = _Options
_Container . innerHTML = ''
_Element = document . createElement ( "div" ) ;
_Element . classList . add ( "js-snackbar__wrapper" , "xrf" ) ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
let innerSnack = document . createElement ( "div" ) ;
innerSnack . classList . add ( "js-snackbar" , "js-snackbar--show" ) ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
if ( _Options . status ) {
_Options . status = _Options . status . toLowerCase ( ) . trim ( ) ;
2024-01-29 21:19:04 +01:00
2024-03-19 10:53:22 +01:00
let status = document . createElement ( "span" ) ;
status . classList . add ( "js-snackbar__status" ) ;
2024-01-29 21:19:04 +01:00
2024-03-19 10:53:22 +01:00
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" ) ;
}
2024-01-29 21:19:04 +01:00
2024-03-19 10:53:22 +01:00
innerSnack . appendChild ( status ) ;
2024-01-03 15:23:34 +01:00
}
2024-03-19 10:53:22 +01:00
_Message = document . createElement ( "span" ) ;
_Message . classList . add ( "js-snackbar__message" ) ;
if ( typeof _Options . message == 'string' ) {
_Message . innerHTML = _Options . message ;
} else _Message . appendChild ( _Options . message )
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
innerSnack . appendChild ( _Message ) ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
if ( _Options . dismissible ) {
let closeBtn = document . createElement ( "span" ) ;
closeBtn . classList . add ( "js-snackbar__close" ) ;
closeBtn . innerText = "\u00D7" ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
closeBtn . onclick = snackbar . Close ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
innerSnack . appendChild ( closeBtn ) ;
}
2024-01-29 21:19:04 +01:00
2024-03-19 10:53:22 +01:00
_Element . style . height = "0px" ;
_Element . style . opacity = "0" ;
_Element . style . marginTop = "0px" ;
_Element . style . marginBottom = "0px" ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
_Element . appendChild ( innerSnack ) ;
_Container . appendChild ( _Element ) ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
if ( _Options . timeout !== false ) {
_Interval = setTimeout ( snackbar . Close , _Options . timeout ) ;
}
2024-01-03 15:23:34 +01:00
}
2024-03-19 10:53:22 +01:00
snackbar . Open = function ( ) {
let contentHeight = _Element . firstElementChild . scrollHeight ; // get the height of the content
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
_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 ;
} )
2024-01-03 15:23:34 +01:00
}
2024-03-19 10:53:22 +01:00
snackbar . Close = function ( ) {
if ( _Interval )
clearInterval ( _Interval ) ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
let snackbarHeight = _Element . scrollHeight ; // get the auto height as a px value
let snackbarTransitions = _Element . style . transition ;
_Element . style . transition = "" ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
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
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
requestAnimationFrame ( function ( ) {
_Element . style . height = "0px" ;
_Element . style . opacity = 0 ;
} )
} ) ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
setTimeout ( function ( ) {
try {
_Container . removeChild ( _Element ) ;
} catch ( e ) { }
} , 1000 ) ;
if ( _Options . onclose ) _Options . onclose ( )
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
} ;
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
_Options = { ... _OptionDefaults , ... userOptions }
_Create ( ) ;
if ( userOptions ) snackbar . Open ( ) ;
}
2024-01-29 21:19:04 +01:00
2024-03-19 10:53:22 +01:00
document . head . innerHTML += `
< style type = "text/css" >
2024-01-29 21:19:04 +01:00
2024-03-19 10:53:22 +01:00
. js - snackbar - container . btn ,
. js - snackbar - container input [ type = submit ] ,
. js - snackbar - container button {
margin - bottom : 15 px ;
}
. js - snackbar - container {
position : absolute ;
top : 10 px ;
left : 0 px ;
display : flex ;
align - items : center ;
2024-07-12 19:25:10 +02:00
width : 100 % ;
2024-03-19 10:53:22 +01:00
max - width : 100 % ;
2024-07-12 19:25:10 +02:00
max - height : 33 vh ;
2024-03-19 10:53:22 +01:00
padding : 10 px ;
z - index : 1001 ;
2024-07-12 19:25:10 +02:00
justify - content : center ;
2024-03-19 10:53:22 +01:00
overflow : hidden ;
2024-01-03 15:23:34 +01:00
}
2024-03-19 10:53:22 +01:00
. js - snackbar - container * {
box - sizing : border - box ;
}
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
. js - snackbar _ _wrapper {
-- color - c : # 555 ;
-- color - a : # FFF ;
2024-01-03 15:23:34 +01:00
}
2024-03-19 10:53:22 +01:00
. js - snackbar _ _wrapper {
transition : 1 s ;
overflow : hidden ;
height : auto ;
margin : 5 px 0 ;
transition : all ease . 5 s ;
border - radius : 15 px ;
box - shadow : 0 0 4 px 0 var ( -- xrf - box - shadow ) ;
right : 20 px ;
position : fixed ;
2024-06-04 19:00:48 +02:00
max - width : 500 px ;
2024-03-19 10:53:22 +01:00
top : 18 px ;
}
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
. js - snackbar {
display : inline - flex ;
box - sizing : border - box ;
border - radius : 3 px ;
color : var ( -- color - c ) ;
background - color : var ( -- color - a ) ;
vertical - align : bottom ;
}
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
. js - snackbar _ _close ,
. js - snackbar _ _status ,
. js - snackbar _ _message {
2024-01-03 15:23:34 +01:00
position : relative ;
2024-03-19 10:53:22 +01:00
}
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
. js - snackbar _ _message {
margin : 12 px ;
}
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
. js - snackbar _ _status {
display : none ;
width : 15 px ;
margin - right : 5 px ;
border - radius : 3 px 0 0 3 px ;
background - color : transparent ;
}
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
. 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 ;
}
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
. js - snackbar _ _status . js - snackbar -- success {
background - color : # 4 caf50 ;
2024-01-03 15:23:34 +01:00
}
2024-03-19 10:53:22 +01:00
. js - snackbar _ _status . js - snackbar -- warning {
background - color : # ff9800 ;
}
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
. js - snackbar _ _status . js - snackbar -- danger {
background - color : # ff6060 ;
2024-01-03 15:23:34 +01:00
}
2024-03-19 10:53:22 +01:00
. js - snackbar _ _status . js - snackbar -- info {
background - color : # CCC ;
}
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
. js - snackbar _ _close {
cursor : pointer ;
display : flex ;
align - items : top ;
padding : 8 px 13 px 0 px 0 px ;
user - select : none ;
}
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
. js - snackbar _ _close : hover {
background - color : # 4443 ;
}
< / s t y l e >
`
2024-01-03 15:23:34 +01:00
window . accessibility = ( opts ) => new Proxy ( {
opts ,
enabled : false ,
// features
2024-04-25 17:57:06 +02:00
speak _teleports : true ,
speak _keyboard : false ,
2024-01-03 15:23:34 +01:00
// audio settings
speak _rate : 1 ,
speak _pitch : 1 ,
speak _volume : 1 ,
speak _voice : - 1 ,
2024-04-25 17:57:06 +02:00
speak _voices : 0 ,
2024-01-03 15:23:34 +01:00
toggle ( ) { this . enabled = ! this . enabled } ,
settings ( ) {
this . toggle ( ) // *TODO* should show settings screen
} ,
speak ( str , opts ) {
opts = opts || { speaksigns : true }
if ( ! this . enabled || ! str ) return
if ( opts . speaksigns ) {
str = str . replace ( /\/\// , ' ' )
. replace ( /:/ , '' )
. replace ( /\// , ' slash ' )
. replace ( /\./ , ' dot ' )
. replace ( /#/ , ' hash ' )
. replace ( /&/ , ' and ' )
. replace ( /=/ , ' is ' )
}
2024-06-17 14:42:42 +02:00
if ( str == this . speak . lastStr ) return // no duplicates
this . speak . lastStr = str
2024-01-03 15:23:34 +01:00
let speech = window . speechSynthesis
let utterance = new SpeechSynthesisUtterance ( str )
2024-04-25 17:57:06 +02:00
this . speak _voices = speech . getVoices ( ) . length
if ( this . speak _voice != - 1 && this . speak _voice < this . speak _voices ) {
utterance . voice = speech . getVoices ( ) [ this . speak _voice ] ;
} else {
2024-01-03 15:23:34 +01:00
let voices = speech . getVoices ( )
for ( let i = 0 ; i < voices . length ; i ++ ) {
if ( voices [ i ] . lang == navigator . lang ) this . speak _voice = i ;
}
}
utterance . rate = this . speak _rate
utterance . pitch = this . speak _pitch
utterance . volume = this . speak _volume
if ( opts . override ) speech . cancel ( )
speech . speak ( utterance )
} ,
init ( ) {
2024-06-17 14:42:42 +02:00
this
. speakArrowKeys ( )
. setupListeners ( )
. setupPersistance ( )
. setupHrefCycling ( )
. setupSpeechKillOnEscape ( )
setTimeout ( ( ) => this . initCommands ( ) , 200 )
} ,
speakArrowKeys ( ) {
// speak arrow keys
2024-01-03 15:23:34 +01:00
window . addEventListener ( 'keydown' , ( e ) => {
if ( ! this . speak _keyboard ) return
let k = e . key
switch ( k ) {
case "ArrowUp" : k = "forward" ; break ;
case "ArrowLeft" : k = "left" ; break ;
case "ArrowRight" : k = "right" ; break ;
case "ArrowDown" : k = "backward" ; break ;
}
this . speak ( k , { override : true } )
} )
2024-06-17 14:42:42 +02:00
return this
} ,
setupSpeechKillOnEscape ( ) {
window . addEventListener ( 'keydown' , ( e ) => {
if ( e . key == "Escape" ) {
this . speak ( "stop" , { override : true } )
}
} )
} ,
setupHrefCycling ( ) {
// speak arrow keys
window . addEventListener ( 'keydown' , ( e ) => {
2024-06-17 15:53:47 +02:00
if ( e . key != "Tab" && e . key != "Enter" ) return
2024-06-17 14:42:42 +02:00
let subScene = xrf . scene . getObjectByName ( xrf . frag . pos . last )
if ( ! subScene ) subScene = xrf . scene
2024-06-17 15:53:47 +02:00
let cache = this . setupHrefCycling . cache = this . setupHrefCycling . cache || { current : - 1 }
2024-06-17 14:42:42 +02:00
let objects = [ ]
subScene . traverse ( ( n ) => ( n . userData . href || n . userData [ 'aria-description' ] ) && objects . push ( n ) )
const highlight = ( n ) => {
if ( this . helper ) {
if ( this . helper . selected == n . uuid ) return // already selected
xrf . scene . remove ( this . helper )
}
this . selected = n
this . helper = new THREE . BoxHelper ( n , 0xFF00FF )
this . helper . computeLineDistances ( )
this . helper . material . linewidth = 8
this . helper . material . color = xrf . focusLine . material . color
this . helper . material . dashSize = xrf . focusLine . material . dashSize
this . helper . material . gapSize = xrf . focusLine . material . gapSize
this . helper . selected = n . uuid
xrf . scene . add ( this . helper )
2024-06-18 12:00:14 +02:00
notify ( ` ${ n . userData [ 'aria-description' ] || '' } ` + ( n . userData . href ? ` <br><b>name:</b> ${ n . name } <br><b>href:</b> ${ n . userData [ 'href' ] } ` : '' ) )
2024-06-17 14:42:42 +02:00
}
2024-06-17 15:53:47 +02:00
if ( e . key == 'Enter' && objects [ cache . current ] . userData . href ) {
xrf . navigator . to ( objects [ cache . current ] . userData . href )
}
2024-06-17 14:42:42 +02:00
// increment to next
2024-06-17 15:53:47 +02:00
cache . current = ( cache . current + 1 ) % objects . length
if ( e . key == 'Tab' ) {
highlight ( objects [ cache . current ] )
}
2024-06-17 14:42:42 +02:00
e . preventDefault ( )
return false
} )
return this
} ,
setupListeners ( ) {
2024-01-03 15:23:34 +01:00
document . addEventListener ( '$menu:buttons:render' , ( e ) => {
let $ = e . detail
let a = [ ... $ . querySelectorAll ( 'a' ) ]
// make sure anchor buttons are accessible by tabbing to them
a . map ( ( btn ) => {
if ( ! btn . href ) btn . setAttribute ( "href" , "javascript:void(0)" ) // important!
btn . setAttribute ( "aria-label" , "button" )
} )
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 } )
}
} )
2024-04-25 17:57:06 +02:00
document . addEventListener ( '$chat.send' , ( opts ) => {
if ( opts . detail . message ) this . speak ( opts . detail . message )
} )
2024-01-03 15:23:34 +01:00
} )
document . addEventListener ( 'network.send' , ( e ) => {
let opts = e . detail
opts . message = opts . message || ''
this . speak ( opts . message )
} )
opts . xrf . addEventListener ( 'pos' , ( opts ) => {
2024-04-25 17:57:06 +02:00
if ( this . enabled && this . speak _teleports ) {
2024-01-29 21:19:04 +01:00
network . send ( { message : this . posToMessage ( opts ) , class : [ "info" , "guide" ] } )
}
if ( opts . frag . pos . string . match ( /,/ ) ) {
network . pos = opts . frag . pos . string
} else {
network . posName = opts . frag . pos . string
2024-01-03 15:23:34 +01:00
}
} )
2024-06-17 14:42:42 +02:00
return this
} ,
2024-01-03 15:23:34 +01:00
2024-06-17 14:42:42 +02:00
setupPersistance ( ) {
2024-04-25 17:57:06 +02:00
// auto-enable if previously enabled
2024-06-15 17:33:08 +02:00
if ( window . localStorage . getItem ( "accessibility" ) === 'true' || xrf . navigator . URI . XRF . accessible ) {
2024-04-25 17:57:06 +02:00
setTimeout ( ( ) => {
this . enabled = true
this . setFontSize ( )
} , 100 )
}
2024-06-17 14:42:42 +02:00
return this
2024-04-25 17:57:06 +02:00
} ,
initCommands ( ) {
document . addEventListener ( 'chat.command.help' , ( e ) => {
2024-06-17 14:42:42 +02:00
e . detail . message += ` <br><b class="badge"><Escape></b> silence TTS `
e . detail . message += ` <br><b class="badge"><Tab></b> cycle [href] buttons / silence TTS `
2024-04-25 17:57:06 +02:00
e . detail . message += ` <br><b class="badge">/fontsize <number></b> set fontsize (default=14) `
} )
document . addEventListener ( 'chat.command' , ( e ) => {
if ( e . detail . message . match ( /^fontsize/ ) ) {
try {
let fontsize = parseInt ( e . detail . message . replace ( /^fontsize / , '' ) . trim ( ) )
if ( fontsize == NaN ) throw 'not a number'
this . setFontSize ( fontsize )
$chat . send ( { message : 'fontsize set to ' + fontsize } )
} catch ( e ) {
console . error ( e )
$chat . send ( { message : 'example usage: /fontsize 20' } )
}
}
} )
} ,
setFontSize ( size ) {
if ( size ) {
window . localStorage . setItem ( "fontsize" , size )
} else size = window . localStorage . getItem ( "fontsize" )
if ( ! size ) return
document . head . innerHTML += `
< style type = "text/css" >
. accessibility # messages * {
font - size : $ { size } px ! important ;
line - height : $ { size * 2 } px ! important ;
}
< / s t y l e >
`
$messages = document . querySelector ( '#messages' )
setTimeout ( ( ) => $messages . scrollTop = $messages . scrollHeight , 1000 )
2024-01-03 15:23:34 +01:00
} ,
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
. split ( '\n' )
. map ( ( l ) => String ( l + '.' ) . replace ( /(^|:|;|!|\?|\.)\.$/g , '\$1' ) ) // add dot if needed
. join ( '\n' )
}
} ,
{
// auto-trigger events on changes
get ( data , k , receiver ) { return data [ k ] } ,
set ( data , k , v ) {
data [ k ] = v
switch ( k ) {
case "enabled" : {
2024-04-25 17:57:06 +02:00
let message = "accessibility mode has been " + ( v ? "activated" : "disabled" ) + ".<br>Type /help for help."
2024-01-03 15:23:34 +01:00
$ ( '#accessibility.btn' ) . style . filter = v ? 'brightness(1.0)' : 'brightness(0.5)'
if ( v ) $chat . visible = true
2024-04-25 17:57:06 +02:00
$chat . send ( { message , class : [ 'info' ] } )
2024-01-03 15:23:34 +01:00
data . enabled = true
data . speak ( message )
data . enabled = v
2024-04-25 17:57:06 +02:00
window . localStorage . setItem ( "accessibility" , v )
2024-01-03 15:23:34 +01:00
$chat . $messages . classList [ v ? 'add' : 'remove' ] ( 'guide' )
2024-04-25 17:57:06 +02:00
document . body . classList . toggle ( [ 'accessibility' ] )
2024-01-03 15:23:34 +01:00
if ( ! data . readTranscript && ( data . readTranscript = true ) ) {
data . speak ( data . sanitizeTranscript ( ) )
}
}
}
}
} )
document . addEventListener ( '$menu:ready' , ( e ) => {
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 ) }
} )
2024-04-25 17:57:06 +02:00
document . querySelector ( 'head' ) . innerHTML += `
< style type = "text/css" >
. accessibility # messages * {
2024-06-15 17:33:08 +02:00
font - size : 20 px ! important ;
line - height : 35 px ;
2024-04-25 17:57:06 +02:00
}
. accessibility # messages . msg . self {
background : var ( -- xrf - gray ) ;
color : # FFF ;
}
. accessibility # messages . msg . info ,
. accessibility # messages . msg . self {
line - height : unset ;
padding - top : 15 px ;
padding - bottom : 15 px ;
}
. accessibility # chatbar {
display : block ! important ;
}
. accessibility # chatsend {
display : block ! important ;
}
. accessibility # chatline {
text - indent : 25 px ;
}
< / s t y l e >
`
2024-03-19 10:53:22 +01:00
// reactive component for displaying the menu
menuComponent = ( el ) => new Proxy ( {
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
html : `
< div class = "xrf footer" >
< div class = "menu" >
< div id = "buttons" > < / d i v >
< a class = "btn" id = "more" aria - title = "menu button" > < i id = "icon" class = "gg-menu" > < / i > < / a > < b r >
< / d i v >
< / d i v >
` ,
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
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' ) ,
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
toggle ( state ) {
this . collapsed = state !== undefined ? state : ! this . collapsed
el . querySelector ( "i#icon" ) . className = this . collapsed ? 'gg-close' : 'gg-menu'
document . body . classList [ this . collapsed ? 'add' : 'remove' ] ( [ 'menu' ] )
} ,
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
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 ( ( ) => {
document . dispatchEvent ( new CustomEvent ( "$menu:ready" , { detail : { $menu : this , xrf } } ) )
} , 100 )
return this
} ,
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
click ( id , e ) {
switch ( id ) {
case "icon" :
2024-07-12 19:25:10 +02:00
case "more" : return this . toggle ( ) ; break ;
2024-01-03 15:23:34 +01:00
}
2024-07-12 19:25:10 +02:00
this . toggle ( false )
2024-01-03 15:23:34 +01:00
}
2024-03-19 10:53:22 +01:00
} ,
{
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
get ( me , k , v ) { return me [ k ] } ,
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
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 ;
}
} ,
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
renderButtons : ( data ) => ` ${ data . buttons . join ( '' ) } `
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
} )
2024-01-03 15:23:34 +01:00
2024-03-19 10:53:22 +01:00
// reactify component!
document . addEventListener ( 'frontend:ready' , ( e ) => {
window . $menu = menuComponent ( document . createElement ( 'div' ) ) . init ( e . detail )
} )
2024-01-03 15:23:34 +01: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 15:07:20 +01:00
< input type = "text" id = "uri" value = "" onchange = "AFRAME.XRF.navigator.to( $('#uri').value )" style = "display:none" / >
2024-01-03 15:23:34 +01:00
< / d i v >
` ,
el : null ,
2024-06-17 14:42:42 +02:00
notify _links : true ,
2024-01-03 15:23:34 +01:00
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-29 21:19:04 +01:00
. setupNetworkListeners ( )
2024-01-03 15:23:34 +01:00
. hidetopbarWhenMenuCollapse ( )
2024-01-29 21:19:04 +01:00
. hideUIWhenNavigating ( )
2024-01-03 15:23:34 +01: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 14:36:00 +01:00
setTimeout ( ( ) => {
2024-03-01 14:35:36 +01:00
let instructions = AFRAME . utils . device . isMobile ( )
? "hold 2-3 fingers to move forward/backward"
2024-06-17 14:42:42 +02:00
: "use W A S D keys and mouse-drag to move around"
2024-03-01 14:35:36 +01:00
window . notify ( instructions , { timeout : false } )
2024-04-25 17:57:06 +02:00
xrf . addEventListener ( 'pos' , ( opts ) => {
let pos = opts . frag . pos . string
window . notify ( '<b class="badge">teleporting</b> to <b>' + pos + "</b><br>use back/forward (browserbutton) to undo" )
2024-03-01 14:35:36 +01:00
} ) // close dialogs when url changes
2024-02-29 14:36:00 +01:00
} , 2000 )
xrf . addEventListener ( 'href' , ( data ) => {
2024-02-29 15:07:20 +01:00
if ( ! data . selected ) return
2024-02-29 14:36:00 +01:00
2024-07-12 19:25:10 +02:00
2024-10-13 14:03:31 +02:00
let topic = data . xrf ? data . xrf . string : data . mesh . userData . src
2024-10-14 11:54:39 +02:00
if ( topic . match ( /\.\.\// ) || ( topic . length > 20 && AFRAME . utils . device . isMobile ( ) ) ) {
topic = topic . replace ( /.*\// , '' )
}
2024-10-13 14:03:31 +02:00
let html = this . notify _links ? ` <b class="badge"> ${ data . mesh . isSRC && ! data . mesh . portal ? 'src' : 'href' } </b> ${ topic } <br> ` : ''
2024-02-29 15:07:20 +01:00
let metadata = data . mesh . userData
2024-02-29 14:36:00 +01: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 root = data . mesh . portal ? data . mesh . portal . stencilObject : data . mesh
2024-04-25 17:57:06 +02:00
let transcript = xrf . sceneToTranscript ( root , data . mesh )
2024-02-29 14:36:00 +01:00
if ( transcript . length ) html += ` <br><b>transcript:</b><br><div class="transcript"> ${ transcript } </div> `
2024-06-15 17:33:08 +02:00
if ( hasMeta && ! data . mesh . portal && metadata . XRF . src ) html += ` <br><br><a class="btn" style="float:right" onclick="xrf.navigator.to(' ${ data . mesh . userData . href } ')">Visit embedded scene</a> `
2024-06-17 14:42:42 +02:00
if ( ! html ) return
2024-07-12 19:25:10 +02:00
2024-02-29 14:36:00 +01:00
window . notify ( html , { timeout : 7000 * ( hasMeta ? 1.5 : 1 ) } )
} )
2024-01-03 15:23:34 +01:00
} , 100 )
return this
} ,
2024-01-29 21:19:04 +01:00
setupNetworkListeners ( ) {
document . addEventListener ( 'network.connect' , ( e ) => {
2024-02-29 15:07:20 +01:00
window . notify ( "🪐 connecting to awesomeness.." )
2024-01-29 21:19:04 +01:00
$chat . send ( { message : ` 🪐 connecting to awesomeness.. ` , class : [ 'info' ] , timeout : 5000 } )
} )
document . addEventListener ( 'network.connected' , ( e ) => {
2024-02-29 15:07:20 +01:00
window . notify ( "🪐 connected to awesomeness.." )
2024-01-29 21:19:04 +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 15:07:20 +01:00
window . notify ( "🪐 disconnecting.." )
2024-01-29 21:19:04 +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 15:23:34 +01: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 21:19:04 +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'
2024-03-19 10:53:22 +01:00
let isUI = e . target . closest ( '.ui' ) ||
e . target . closest ( '.btn' ) ||
e . target . closest ( 'button' ) ||
e . target . closest ( 'textarea' ) ||
e . target . closest ( 'input' ) ||
e . target . closest ( 'a' )
2024-01-29 21:19:04 +01:00
//console.dir({class: e.target.className, id: e.target.id, isChatMsg,isChatLine,isChatEmptySpace,isUI, tagName: e.target.tagName})
2024-03-19 10:53:22 +01:00
if ( isUI ) return
2024-01-29 21:19:04 +01:00
if ( show ) {
2024-06-11 19:30:32 +02:00
if ( typeof $chat != 'undefined' ) $chat . visible = true
2024-01-29 21:19:04 +01:00
} else {
2024-06-11 19:30:32 +02:00
if ( typeof $chat != 'undefined' ) $chat . visible = false
2024-01-29 21:19:04 +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 15:23:34 +01: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 15:07:20 +01:00
let file = files . slice ? files [ 0 ] : files
2024-01-03 15:23:34 +01:00
for ( var i in contentLoaders ) {
let r = new RegExp ( '\\' + i + '$' )
2024-06-11 19:30:32 +02:00
if ( file . name . match ( r ) ) {
xrf . navigator . URI . file = '' // bypass cached file (easy refresh same file for testing)
return contentLoaders [ i ] ( file )
}
2024-01-03 15:23:34 +01:00
}
alert ( file . name + " is not supported" )
} ;
input . click ( ) ;
}
} ,
notify ( _str , opts ) {
if ( window . accessibility && window . accessibility . enabled ) return $chat . send ( { message : _str , class : [ 'info' ] } )
2024-02-29 15:07:20 +01:00
opts = opts || { status : 'info' }
2024-01-03 15:23:34 +01:00
opts = Object . assign ( { status , timeout : 4000 } , opts )
opts . message = _str
if ( typeof str == 'string' ) {
2024-02-29 15:07:20 +01:00
str = _str . replace ( /(^\w+):/ , "<div class='badge'>\$1</div>" )
if ( ! opts . status ) {
2024-01-03 15:23:34 +01: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 ( ) {
2024-03-19 10:53:22 +01:00
function download ( dataurl , filename ) {
2024-01-03 15:23:34 +01:00
var a = document . createElement ( "a" ) ;
2024-03-19 10:53:22 +01:00
a . href = URL . createObjectURL ( new Blob ( [ dataurl ] ) ) ;
2024-01-03 15:23:34 +01:00
a . setAttribute ( "download" , filename ) ;
a . click ( ) ;
return false ;
}
2024-03-19 10:53:22 +01:00
function exportScene ( model , ext , file ) {
document . dispatchEvent ( new CustomEvent ( 'frontend.export' , { detail : { scene : model . scene , ext } } ) )
xrf . emit ( 'export' , { scene : model . scene , ext } )
. then ( ( ) => {
// setup exporters
let defaultExporter = THREE . GLTFExporter
2024-12-10 15:41:07 +01:00
if ( ! xrf . loaders [ 'gltf' ] . exporter ) xrf . loaders [ 'gltf' ] . exporter = defaultExporter
if ( ! xrf . loaders [ 'glb' ] . exporter ) xrf . loaders [ 'glb' ] . exporter = defaultExporter
const exporter = new xrf . loaders [ ext ] ( )
2024-03-19 10:53:22 +01:00
exporter . parse (
model . scene ,
function ( glb ) { download ( glb , ` ${ file } ` ) } , // ready
function ( error ) { console . error ( error ) } , // error
{
binary : true ,
onlyVisible : false ,
animations : model . animations ,
includeCustomExtensions : true ,
trs : true
}
) ;
} )
}
// load original scene and overwrite with updates
let url = document . location . search . replace ( /\?/ , '' )
2024-12-10 15:41:07 +01:00
let { urlObj , dir , file , hash , fileExt } = xrf . navigator . origin = xrf . URI . parse ( url )
debugger
const Loader = xrf . loaders [ fileExt ]
2024-03-19 10:53:22 +01:00
loader = new Loader ( ) . setPath ( dir )
notify ( 'exporting scene<br><br>please wait..' )
loader . load ( url , ( model ) => {
2024-12-10 15:41:07 +01:00
exportScene ( model , fileExt , file )
2024-03-19 10:53:22 +01:00
} )
2024-01-03 15:23:34 +01:00
} ,
updateHashPosition ( randomize ) {
2024-04-16 15:19:08 +02:00
const pos = xrf . frag . pos . get ( )
2024-06-26 14:42:00 +02:00
xrf . navigator . updateHash . active = false // prevent teleport
2024-04-16 15:19:08 +02:00
xrf . navigator . URI . hash . pos = ` ${ pos . x } , ${ pos . y } , ${ pos . z } `
2024-06-26 14:42:00 +02:00
document . location . hash = ` # ${ xrf . navigator . URI . fragment } `
xrf . navigator . updateHash . active = true
return document . location . href
2024-01-03 15:23:34 +01:00
} ,
copyToClipboard ( text ) {
2024-02-29 15:07:20 +01:00
// copy url to clipboard
2024-01-03 15:23:34 +01:00
var dummy = document . createElement ( 'input' )
document . body . appendChild ( dummy ) ;
dummy . value = text ;
dummy . select ( ) ;
document . execCommand ( 'copy' ) ;
2024-02-29 15:07:20 +01:00
document . body . removeChild ( dummy ) ;
2024-01-03 15:23:34 +01:00
} ,
share ( opts ) {
2024-01-29 21:19:04 +01:00
opts = opts || { notify : true , qr : true , share : true , linkonly : false }
2024-04-16 18:44:55 +02:00
if ( network . meetingLink && ! xrf . navigator . URI . hash . meet ) {
xrf . navigator . URI . hash . meet = network . meetingLink
2024-01-29 21:19:04 +01:00
}
2024-06-26 14:42:00 +02:00
let url = frontend . updateHashPosition ( )
console . log ( url )
2024-02-29 15:07:20 +01:00
if ( opts . linkonly ) return url
2024-01-03 15:23:34 +01:00
this . copyToClipboard ( url )
2024-02-29 15:07:20 +01:00
// End of *TODO*
2024-01-03 15:23:34 +01:00
if ( opts . notify ) {
2024-01-29 21:19:04 +01:00
window . notify ( ` <h2> ${ network . connected ? 'Meeting link ' : 'Link' } copied to clipboard!</h2>
Now share it with your friends ❤ ️ < br >
2024-01-03 15:23:34 +01: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-29 21:19:04 +01:00
< br >
2024-01-03 15:23:34 +01: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 15:07:20 +01:00
$menu . collapse = true
2024-01-03 15:23:34 +01:00
}
} ,
2024-02-29 15:07:20 +01:00
{
// auto-trigger events on changes
2024-01-03 15:23:34 +01: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 15:07:20 +01:00
2024-01-03 15:23:34 +01:00
frontend = frontend ( { xrf , document } ) . init ( )
2024-04-25 17:57:06 +02:00
// this allows surfing to a href by typing its node-name
// help screen
document . addEventListener ( 'chat.command.help' , ( e ) => {
e . detail . message += `
< br > < b class = "badge" > & lt ; destinationname & gt ; < / b > s u r f t o a d e s t i n a t i o n
`
} )
document . addEventListener ( 'chat.input' , ( e ) => {
let name = e . detail . message . trim ( )
xrf . scene . traverse ( ( n ) => {
if ( n . userData && n . userData . href && n . userData . href . match ( /pos=/ ) && n . name == name ) {
$chat . send ( { message : '<b class="badge">activating</b> ' + n . name , class : [ 'self' , 'info' ] } )
xrf . navigator . to ( n . userData . href )
}
} )
} )
// this allows a more-or-less MUD type interface
//
//
// help screen
document . addEventListener ( 'chat.command.help' , ( e ) => {
e . detail . message += `
< br > < b class = "badge" > ? < / b > h e l p s c r e e n
< br > < b class = "badge" > look < / b > v i e w s c e n e a n d d e s t i n a t i o n s
< br > < b class = "badge" > go [ left | right | forward | destination ] < / b > s u r f [ t o d e s t i n a t i o n ]
< br > < b class = "badge" > do [ action ] < / b > l i s t [ o r p e r f o r m ] a c t i o n ( s )
< br > < b class = "badge" > rotate & lt ; left | right | up | down & gt ; < / b > r o t a t e c a m e r a
< br > < b class = "badge" > back < / b > g o t o p r e v i o u s p o r t a l / l i n k
< br > < b class = "badge" > forward < / b > g o t o p r e v i o u s p o r t a l / l i n k
< br > < b class = "badge" > # ... . < / b > e x e c u t e X R F r a g m e n t s
< hr / >
`
} )
const listExits = ( scene ) => {
let message = ''
let destinations = { }
scene . traverse ( ( n ) => {
if ( n . userData && n . userData . href && n . userData . href . match ( /pos=/ ) ) {
destinations [ n . name ] = n . userData [ 'aria-label' ] || n . userData . href
}
} )
for ( let destination in destinations ) {
message += ` <br><b class="badge"> ${ destination } </b> ${ destinations [ destination ] } `
}
if ( ! message ) message += '<br>type <b class="badge">back</b> to go back'
return message
}
const listActions = ( scene ) => {
let message = ''
let destinations = { }
scene . traverse ( ( n ) => {
if ( n . userData && n . userData . href && ! n . userData . href . match ( /pos=/ ) ) {
destinations [ n . name ] = n . userData [ 'aria-description' ] || n . userData [ 'aria-label' ] || n . userData . href
}
} )
for ( let destination in destinations ) {
message += ` <br><b class="badge"> ${ destination } </b> ${ destinations [ destination ] } `
}
if ( ! message ) message += '<br>no actions found'
return message
}
document . addEventListener ( 'chat.input' , ( e ) => {
if ( e . detail . message . trim ( ) == '?' ) {
document . dispatchEvent ( new CustomEvent ( 'chat.command' , { detail : { message : "help" } } ) )
e . detail . halt = true // don't send to other peers
}
if ( e . detail . message . trim ( ) == 'look' ) {
let scene = xrf . frag . pos . last ? xrf . scene . getObjectByName ( xrf . frag . pos . last ) : xrf . scene
let message = ` <div class="transcript"> ${ xrf . sceneToTranscript ( scene ) } </div><br>possible destinations in this area: ${ listExits ( scene ) } `
e . detail . halt = true // dont print command to screen
$chat . send ( { message } )
}
if ( e . detail . message . match ( /^go($| )/ ) ) {
if ( e . detail . message . trim ( ) == 'go' ) {
$chat . send ( { message : ` all possible destinations: ${ listExits ( xrf . scene ) } ` } )
} else {
let destination = e . detail . message . replace ( /^go / , '' ) . trim ( )
if ( destination . match ( /(left|right|forward|backward)/ ) ) {
let key = ''
switch ( destination ) {
case "left" : key = 'ArrowLeft' ; break ;
case "right" : key = 'ArrowRight' ; break ;
case "forward" : key = 'ArrowUp' ; break ;
case "backward" : key = 'ArrowDown' ; break ;
}
if ( key ) {
let lookcontrols = document . querySelector ( '[look-controls]' )
if ( lookcontrols ) lookcontrols . removeAttribute ( 'look-controls' ) // workaround to unlock camera
var wasd = document . querySelector ( '[wasd-controls]' ) . components [ 'wasd-controls' ]
wasd . keys [ key ] = true
wasd . velocity = new THREE . Vector3 ( )
setTimeout ( ( ) => delete wasd . keys [ key ] , 100 )
wasd . el . object3D . matrixAutoUpdate = true ;
wasd . el . object3D . updateMatrix ( )
xrf . camera . getCam ( ) . updateMatrix ( )
}
} else {
let node
xrf . scene . traverse ( ( n ) => {
if ( n . userData && n . userData . href && n . name == destination ) node = n
} )
if ( node ) xrf . navigator . to ( node . userData . href )
else $chat . send ( { message : "type 'look' for possible destinations" } )
}
}
e . detail . halt = true // dont write input to chat
}
if ( e . detail . message . match ( /^do($| )/ ) ) {
if ( e . detail . message . trim ( ) == 'do' ) {
$chat . send ( { message : ` all possible actions: ${ listActions ( xrf . scene ) } ` } )
} else {
let action = e . detail . message . replace ( /^do / , '' ) . trim ( )
xrf . scene . traverse ( ( n ) => {
if ( n . userData && n . userData . href && n . name == action ) {
$chat . send ( { message : '<b class="badge">activating</b> ' + n . name , class : [ 'self' , 'info' ] } )
xrf . navigator . to ( n . userData . href )
}
} )
}
e . detail . halt = true // dont write input to chat
}
if ( e . detail . message . match ( /^rotate / ) ) {
let dir = e . detail . message . replace ( /^rotate / , '' ) . trim ( )
let y = 0 ;
let x = 0 ;
switch ( dir ) {
case "left" : y = 0.3 ; break ;
case "right" : y = - 0.3 ; break ;
case "up" : x = 0.3 ; break ;
case "down" : x = - 0.3 ; break ;
}
let lookcontrols = document . querySelector ( '[look-controls]' )
if ( lookcontrols ) lookcontrols . removeAttribute ( 'look-controls' ) // workaround to unlock camera
xrf . camera . rotation . y += y
xrf . camera . rotation . x += x
xrf . camera . matrixAutoUpdate = true
e . detail . halt = true // dont write input to chat
}
if ( e . detail . message . trim ( ) == 'back' ) {
window . history . back ( )
}
if ( e . detail . message . trim ( ) == 'forward' ) {
window . history . forward ( )
}
} )
// this allows surfing to a href by typing its node-name
// help screen
document . addEventListener ( 'chat.command.help' , ( e ) => {
e . detail . message += `
< br > < b class = "badge" > / s p e a k _ k e y b o a r d & l t ; t r u e | f a l s e & g t ; < / b > t u r n o n / o f f k e y b o a r d i n p u t T T S
< br > < b class = "badge" > / s p e a k _ t e l e p o r t s & l t ; t r u e | f a l s e & g t ; < / b > t u r n o n / o f f T T S f o r t e l e p o r t s
< br > < b class = "badge" > / s p e a k _ r a t e & l t ; 1 & g t ; < / b > a d j u s t T T S s p e e d
< br > < b class = "badge" > / s p e a k _ p i t c h & l t ; 1 & g t ; < / b > a d j u s t T T S p i t c h
< br > < b class = "badge" > / s p e a k _ v o l u m e & l t ; 1 & g t ; < / b > a d j u s t T T S v o l u m e
< br > < b class = "badge" > / s p e a k _ v o i c e & l t ; 0 & g t ; < / b > s e l e c t v o i c e ( m a x : $ { w i n d o w . a c c e s s i b i l i t y . s p e a k _ v o i c e s } )
`
} )
document . addEventListener ( 'chat.command' , ( e ) => {
if ( ! e . detail . message . trim ( ) . match ( / / ) ) return
let action = e . detail . message . trim ( ) . split ( " " ) [ 0 ]
let value = e . detail . message . trim ( ) . split ( " " ) [ 1 ]
if ( window . accessibility [ action ] == undefined ) return
window . accessibility [ action ] = value
window . localStorage . setItem ( action , value )
$chat . send ( { message : ` ${ action } set to ${ value } ` , class : [ 'info' ] } )
} )
2024-12-11 10:52:27 +01:00
// original site - https://github.com/mrturck/aframe-joystick
// modified
// copy of nippleJS library
! function ( t ) { if ( "object" == typeof exports && "undefined" != typeof module ) module . exports = t ( ) ; else if ( "function" == typeof define && define . amd ) define ( [ ] , t ) ; else { var e ; e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof self ? self : this , e . nipplejs = t ( ) } } ( function ( ) { function t ( ) { } function e ( t , i ) { return this . identifier = i . identifier , this . position = i . position , this . frontPosition = i . frontPosition , this . collection = t , this . defaults = { size : 100 , threshold : . 1 , color : "white" , fadeTime : 250 , dataOnly : ! 1 , restJoystick : ! 0 , restOpacity : . 5 , mode : "dynamic" , zone : document . body } , this . config ( i ) , "dynamic" === this . options . mode && ( this . options . restOpacity = 0 ) , this . id = e . id , e . id += 1 , this . buildEl ( ) . stylize ( ) , this . instance = { el : this . ui . el , on : this . on . bind ( this ) , off : this . off . bind ( this ) , show : this . show . bind ( this ) , hide : this . hide . bind ( this ) , add : this . addToDom . bind ( this ) , remove : this . removeFromDom . bind ( this ) , destroy : this . destroy . bind ( this ) , resetDirection : this . resetDirection . bind ( this ) , computeDirection : this . computeDirection . bind ( this ) , trigger : this . trigger . bind ( this ) , position : this . position , frontPosition : this . frontPosition , ui : this . ui , identifier : this . identifier , id : this . id , options : this . options } , this . instance } function i ( t , e ) { var n = this ; return n . nipples = [ ] , n . idles = [ ] , n . actives = [ ] , n . ids = [ ] , n . pressureIntervals = { } , n . manager = t , n . id = i . id , i . id += 1 , n . defaults = { zone : document . body , multitouch : ! 1 , maxNumberOfNipples : 10 , mode : "dynamic" , position : { top : 0 , left : 0 } , catchDistance : 200 , size : 100 , threshold : . 1 , color : "white" , fadeTime : 250 , dataOnly : ! 1 , restJoystick : ! 0 , restOpacity : . 5 } , n . config ( e ) , "static" !== n . options . mode && "semi" !== n . options . mode || ( n . options . multitouch = ! 1 ) , n . options . multitouch || ( n . options . maxNumberOfNipples = 1 ) , n . updateBox ( ) , n . prepareNipples ( ) , n . bindings ( ) , n . begin ( ) , n . nipples } function n ( t ) { var e = this ; e . ids = { } , e . index = 0 , e . collections = [ ] , e . config ( t ) , e . prepareCollections ( ) ; var i ; return c . bindEvt ( window , "resize" , function ( t ) { clearTimeout ( i ) , i = setTimeout ( function ( ) { var t , i = c . getScroll ( ) ; e . collections . forEach ( function ( e ) { e . forEach ( function ( e ) { t = e . el . getBoundingClientRect ( ) , e . position = { x : i . x + t . left , y : i . y + t . top } } ) } ) } , 100 ) } ) , e . collections } var o , r = ! ! ( "ontouchstart" in window ) , s = ! ! window . PointerEvent , d = ! ! window . MSPointerEvent , a = { touch : { start : "touchstart" , move : "touchmove" , end : "touchend, touchcancel" } , mouse : { start : "mousedown" , move : "mousemove" , end : "mouseup" } , pointer : { start : "pointerdown" , move : "pointermove" , end : "pointerup" } , MSPointer : { start : "MSPointerDown" , move : "MSPointerMove" , end : "MSPointerUp" } } , p = { } ; s ? o = a . pointer : d ? o = a . MSPointer : r ? ( o = a . touch , p = a . mouse ) : o = a . mouse ; var c = { } ; c . distance = function ( t , e ) { var i = e . x - t . x , n = e . y - t . y ; return Math . sqrt ( i * i + n * n ) } , c . angle = function ( t , e ) { var i = e . x - t . x , n = e . y - t . y ; return c . degrees ( Math . atan2 ( n , i ) ) } , c . findCoord = function ( t , e , i ) { var n = { x : 0 , y : 0 } ; return i = c . radians ( i ) , n . x = t . x - e * Math . cos ( i ) , n . y = t . y - e * Math . sin ( i ) , n } , c . radians = function ( t ) { return t * ( Math . PI / 180 ) } , c . degrees = function ( t ) { return t * ( 180 / Math . PI ) } , c . bindEvt = function ( t , e , i ) { for ( var n , o = e . split ( /[ ,]+/g ) , r = 0 ; r < o . length ; r += 1 ) n = o [ r ] , t . addEventListener ? t . addEventListener ( n , i , ! 1 ) : t . attachEvent && t . attachEvent ( n , i ) } , c . unbindEvt = function ( t , e , i ) { for ( var n , o = e . split ( /[ ,]+/g ) , r = 0 ; r < o . length ; r += 1 ) n = o [ r ] , t . removeEventListener ? t . removeEventListener ( n , i ) : t . detachEvent && t . detachEvent ( n , i ) } , c . trigger = function ( t , e , i ) { var n = new CustomEvent ( e , i ) ; t . dispatchEvent ( n ) } , c . prepareEvent = function ( t ) { return t . preventDefault ( ) , t . type . match ( /^touch/ ) ? t . changedTouches : t } , c . getScroll = function ( ) { return { x : void 0 !== window . pageXOffset ? window . pageXOffset : ( document . documentElement || document . body . parentNode || document . body ) . scrollLeft , y : void 0 !== window . pageYOffset ? window . pageYOffset : ( document . documentElement || document . body . parentNode || document . body ) . scrollTop } } , c . applyPosition = function ( t , e ) { e . x && e . y ? ( t . style . left = e . x + "px" , t . style . top = e . y + "px" ) : ( e . top || e . right || e . bottom || e . left ) && ( t . style . top = e . top , t . style . right = e . right , t . style . bottom = e . bottom , t . style . left = e . left ) } , c . getTransitionStyle = function ( t , e , i ) { var n = c . configStylePropertyObject ( t ) ; for ( var o in n ) if ( n . hasOwnProperty ( o ) ) if ( "string" == typeof e ) n [ o ] = e + " " + i ; else { for ( var r = "" , s = 0 , d = e . length ; s < d ; s += 1 )
//const style = "position:fixed;display:block;width:100px;height:100px;left:25px;bottom:20px;background-color:#c8c8c880;z-index:20;border-radius: 50%;border: 3px solid gray;background-image: url('/resources/image/upload.png');"
function initJoystick ( ) {
// create element
let d = document . createElement ( "DIV" ) ;
d . setAttribute ( "id" , "np" ) ;
d . setAttribute ( "class" , "controller" ) ;
//d.setAttribute("style",style)
document . querySelector ( "body" ) . appendChild ( d )
// create text overlay
//var p = document.createElement("p")
//p.setAttribute("style","text-align: center;margin-top:40px;font-size:12px Roboto; opacity:.5;");
//p.innerHTML="hold and drag to move"
//d.appendChild(p)
}
var moveData = "" ;
// create standard NipplesJS joystick
function createJoystick ( ) {
initJoystick ( )
var options = {
mode : 'dynamic' ,
zone : document . getElementById ( 'np' ) ,
color : "#0F0000" ,
fadeTime : 10
}
var manager = nipplejs . create ( options ) ;
bindNipple ( ) ;
function bindNipple ( ) {
manager . on ( 'move' , function ( evt , data ) {
moveData = data ;
} ) ;
manager . on ( 'end' , function ( evt , data ) {
moveData = "" ;
} ) ;
}
}
// turn joystick data into WASD movement in AFRAME
var f ; var ang ; var x _vec ; var y _vec ; var cam ;
function updatePosition ( data ) {
f = data . force ;
ang = data . angle . radian
cam = document . getElementById ( "camera" ) ;
x _vec = Math . cos ( ang + 3.14 / 180 * cam . getAttribute ( 'rotation' ) [ 'y' ] ) ;
y _vec = Math . sin ( ang + 3.14 / 180 * cam . getAttribute ( 'rotation' ) [ 'y' ] ) ;
x = cam . getAttribute ( "position" ) [ "x" ] + f / 15 * ( x _vec ) ;
y = cam . getAttribute ( "position" ) [ "y" ]
z = cam . getAttribute ( "position" ) [ "z" ] - f / 15 * ( y _vec ) ;
cam . setAttribute ( "position" , ` ${ x } ${ y } ${ z } ` )
}
AFRAME . registerComponent ( 'joystick' , {
init : function ( ) {
createJoystick ( ) ;
} ,
tick : function ( time , timeDelta ) {
if ( moveData != "" ) {
updatePosition ( moveData )
}
}
} ) ;
2024-01-03 15:23:34 +01:00
} ) . apply ( { } )