Created
May 15, 2023 12:48
-
-
Save damiencorpataux/8dd36be7bce81febeac30f3571e0b03b to your computer and use it in GitHub Desktop.
IO Example: HTML client (webpage)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>io!</title> | |
</head> | |
<body> | |
<div class="container min-vh-100 pt-4"> | |
<div class="row flex-grow-1"> | |
<div class="col-md-8"> | |
<div class="form-check float-start" title="Toggle sound"> | |
<input type="checkbox" tabindex="3" id="audio" class="form-check-input"> | |
<label for="audio" class="form-check-label">Audio</label> | |
</div> | |
<span class="text-nowrap float-end" title="Real-time values: [ratio/stress|fps~avgfps] distance"> | |
<code id="meter"><i data-feather="loader" height="1em" class="align-baseline"></i></code> mm | |
</span> | |
<svg width="100%"> | |
<circle id="circle" cx="50" cy="50" r="50" fill="white" stroke="black" stroke-width="6"></circle> | |
</svg> | |
</div> | |
<div class="col-md-4 my-3 shadow small"> | |
<h2> | |
<i data-feather="activity"></i> | |
Websocket client</h2> | |
<form class="mb-2"> | |
<input id="url" tabindex="1" title="Websocket URL" placeholder="Websocket URL"> | |
<input id="msg" tabindex="2" autofocus autocomplete="off" title="Websocket message" placeholder="Websocket message"> | |
</form> | |
<div style="max-height:300px;overflow:hidden"> | |
<code id="out" style="white-space:pre-wrap;filter:brightness(.75)" class="text-success"></code> | |
</div> | |
<br> | |
</div> | |
</div> | |
</div> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous"> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script> | |
<script> | |
feather.replace({'aria-hidden':'true'}) | |
// Let feather handle attribute title | |
for (element of document.querySelectorAll('svg.feather[title]')) element.insertAdjacentHTML('afterbegin', `<title>${element.attributes.title.value}</title>`); | |
</script> | |
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> | |
<!-- WARNING: Respond.js doesn't work if you view the page via file:// --> | |
<!--[if lt IE 9]> | |
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> | |
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> | |
<![endif]--> | |
<script> | |
const autoconnect = true; | |
const max = 1000; | |
// const min = 300; | |
// const ease_in_exp = e => x => x**e; | |
const ease_out_exp = e => x => 1 - (1 - x)**e; | |
const ease = ease_out_exp(1.75) | |
const out = document.querySelector('#out'); | |
const meter = document.querySelector('#meter'); | |
const form = document.querySelector('form'); | |
const msg = document.querySelector('#msg'); | |
const url = document.querySelector('#url'); | |
const circle = document.querySelector('#circle'); | |
const audio = document.querySelector('#audio'); | |
window.addEventListener('load', () => { | |
if (!url.value) url.value = `ws://${window.location.host ? window.location.host : 'host'}/ws`; | |
url.onkeyup = evt => { | |
if (evt.key == 'Enter') websocket(url.value, true); | |
} | |
msg.onkeyup = evt => { | |
if (evt.key == 'Enter') { | |
send(msg.value); | |
msg.value = ''; | |
} | |
}; | |
window.dispatchEvent(new Event('resize')); // trigger container resize | |
if (autoconnect) url.dispatchEvent(new KeyboardEvent('keyup', {'key':'Enter'})); // trigger websocket connection | |
else log('Press <kbd>enter</kbd> to connect...', true); | |
}); | |
let websocket_singleton; | |
function websocket(url, force) { | |
if (force || !websocket_singleton || websocket_singleton.url != url) { | |
log(`+ Connecting to ${url}...`); | |
if (websocket_singleton) websocket_singleton.close(); | |
websocket_singleton = new WebSocket(url); | |
websocket_singleton.addEventListener('error', event => log(`+ Error with ${url}`)); | |
websocket_singleton.addEventListener('message', event => log(`> ${event.data}`)); | |
// const average = a => a.reduce((m, x, i) => m + (x - m) / (i + 1), 0) | |
// const average = a => a.reduce((a,e,i) => (a*i+e)/(i+1)); | |
const average = a => a.reduce((acc,v,i,a)=>(acc+v/a.length),0); | |
let last_fps = []; | |
let last_message = Date.now(); | |
websocket_singleton.addEventListener('message', event => { | |
const message = JSON.parse(event.data); | |
const distance = message?.value?.distance || max; | |
const ratio = Math.min(1, distance / max); | |
const stress = Math.max(0.1, ease(1-ratio)); | |
// Values animation | |
last_fps.push(1e3 * 1/(Date.now()-last_message)) | |
last_fps = last_fps.slice(-30); | |
meter.innerHTML = `[${ratio.toFixed(3)}/${stress.toFixed(2)}|${last_fps.at(-1).toFixed(1)}~${average(last_fps).toFixed(0)}] ${distance}`; | |
last_message = Date.now(); | |
// Visual animation | |
const bbox = circle.parentElement.getBoundingClientRect(); | |
circle.setAttribute('r', Math.max(0.1, stress/1.66) * Math.max(bbox.width, bbox.height)); | |
circle.setAttribute('fill', `rgb(${(stress*1.5)*255}, ${(1-stress)*255}, 0)`); | |
// Audio animation | |
if (oscillator) { | |
oscillator.frequency.setValueAtTime((220*16 * stress**16), 0); | |
masterVolume.gain.value = (stress**8); | |
} | |
}); | |
websocket_singleton.addEventListener('open', event => { | |
send(JSON.stringify({"query": {"distance": ["value"]}})); // trigger setting initial value to animations | |
}); | |
} | |
return websocket_singleton; | |
} | |
function send(message) { | |
websocket(url.value).send(message); | |
log(`< ${message}`); | |
} | |
function log(message, no_escape) { | |
const message_escaped = no_escape ? message : document.createElement('div').appendChild(document.createTextNode(message)).parentNode.innerHTML; | |
out.innerHTML = message_escaped + '\n' + out.innerHTML.slice(0, 1e3); | |
} | |
// Audio oscillator toggler | |
let oscillator; | |
const AudioContext = window.AudioContext || window.webkitAudioContext; | |
const context = new AudioContext(); | |
const masterVolume = context.createGain(); | |
masterVolume.connect(context.destination); | |
masterVolume.gain.value = .1; | |
audio.checked = false; | |
audio.addEventListener('change', event => { | |
if (audio.checked) { | |
if (oscillator) { | |
context.resume(); | |
return; | |
} | |
oscillator = context.createOscillator(); | |
oscillator.frequency.setValueAtTime(0, 0); | |
oscillator.connect(masterVolume); | |
oscillator.start(0); | |
oscillator.type = 'square'; | |
// Trigger setting initial value to audio | |
websocket_singleton.addEventListener('open', event => { | |
send(JSON.stringify({"query": {"distance": ["value"]}})); | |
}); | |
} else { | |
context.suspend(); | |
// oscillator.stop(0); | |
// delete oscillator; | |
} | |
}); | |
// Fix container size for <svg> | |
window.addEventListener('resize', evt => { | |
const svg = circle.closest('svg'); | |
const height = -15 + document.documentElement.clientHeight - svg.getBoundingClientRect().top; | |
circle.parentElement.setAttribute('height', height); | |
circle.setAttribute('cx', svg.getBoundingClientRect().width / 2); | |
circle.setAttribute('cy', svg.getBoundingClientRect().height / 2); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment