window.accessibility = (opts) => new Proxy({ opts, enabled: false, // features speak_movements: true, speak_keyboard: true, // audio settings speak_rate: 1, speak_pitch: 1, speak_volume: 1, speak_voice: -1, 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 ') } let speech = window.speechSynthesis let utterance = new SpeechSynthesisUtterance( str ) if( this.speak_voice != -1) utterance.voice = speech.getVoices()[ this.speak_voice ]; else{ 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(){ 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}) }) 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} ) } }) }) document.addEventListener('network.send', (e) => { let opts = e.detail opts.message = opts.message || '' this.speak(opts.message) }) opts.xrf.addEventListener('pos', (opts) => { if( this.enabled ){ $chat.send({message: this.posToMessage(opts) }) 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 } }) setTimeout( () => this.initCommands(), 200 ) // auto-enable if previously enabled if( window.localStorage.getItem("accessibility") ){ setTimeout( () => this.enabled = true, 100 ) } }, initCommands(){ document.addEventListener('chat.command.help', (e) => { e.detail.message += `
/fontsize set fontsize (default=14) ` }) }, posToMessage(opts){ let obj let description let msg = "teleported to " let pos = opts.frag.pos if( pos.string.match(',') ) msg += `coordinates ${pos.string}` else{ msg += `location ${pos.string}` 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": { 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']}) data.enabled = true data.speak(message) data.enabled = v window.localStorage.setItem("accessibility", v) $chat.$messages.classList[ v ? 'add' : 'remove' ]('guide') document.body.classList.toggle(['accessibility']) 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([`accessibility
`]) }catch(e){console.error(e)} }) document.querySelector('head').innerHTML += ` `