Skip to content

Instantly share code, notes, and snippets.

@damiencorpataux
Created May 15, 2023 12:48
Show Gist options
  • Save damiencorpataux/8dd36be7bce81febeac30f3571e0b03b to your computer and use it in GitHub Desktop.
Save damiencorpataux/8dd36be7bce81febeac30f3571e0b03b to your computer and use it in GitHub Desktop.
IO Example: HTML client (webpage)
<!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