384 lines
11 KiB
HTML
384 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Generic STFX UI</title>
|
|
<style>
|
|
:root {
|
|
font-size:12pt;
|
|
}
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
body {
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
margin: 0;
|
|
padding: 0;
|
|
width: 100vw;
|
|
max-width: 100vw;
|
|
height: 100vh;
|
|
min-height: 100vh;
|
|
max-height: 100vh;
|
|
overflow: hidden;
|
|
|
|
background: linear-gradient(#FFF, #EEE, #CCC);
|
|
color: #000;
|
|
|
|
text-align: center;
|
|
word-wrap: break-word;
|
|
font-family: Bahnschrift, 'DIN Alternate', 'Alte DIN 1451 Mittelschrift', 'D-DIN', 'OpenDin', 'Clear Sans', 'Barlow', 'Abel', 'Franklin Gothic Medium', system-ui, sans-serif;
|
|
|
|
/* no text selectable by default */
|
|
user-select: none;
|
|
}
|
|
output {
|
|
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
|
font-weight: normal;
|
|
color: #000;
|
|
}
|
|
|
|
#header {
|
|
grid-area: header;
|
|
display: flex;
|
|
justify-content: center;
|
|
margin: 0.5rem;
|
|
|
|
font-weight: normal;
|
|
letter-spacing: 0.1ex;
|
|
transform: scale(0.95, 1);
|
|
}
|
|
#params {
|
|
flex-grow: 1;
|
|
display: flex;
|
|
align-content: space-around;
|
|
justify-content: space-evenly;
|
|
flex-wrap: wrap;
|
|
|
|
padding: 1rem;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.param-range {
|
|
display: grid;
|
|
width: 3.5rem;
|
|
grid-template-areas: "dial" "name";
|
|
grid-template-rows: 3.5rem 1fr;
|
|
}
|
|
.param-range-value {
|
|
position: relative;
|
|
grid-area: dial;
|
|
|
|
width: 3.5rem;
|
|
height: 3.5rem;
|
|
}
|
|
.param-range-dial {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: -1;
|
|
}
|
|
.param-range-text {
|
|
position: absolute;
|
|
top: 30%;
|
|
left: 0;
|
|
width: 100%;
|
|
letter-spacing: -0.1ex;
|
|
}
|
|
.param-range-units {
|
|
position: absolute;
|
|
top: 60%;
|
|
left: 0;
|
|
width: 100%;
|
|
|
|
color: #666;
|
|
font-size: 0.85rem;
|
|
}
|
|
.param-range-name {
|
|
grid-area: name;
|
|
}
|
|
|
|
#plots {
|
|
display: flex;
|
|
flex-grow: 10;
|
|
|
|
}
|
|
.plot {
|
|
display: block;
|
|
width: 100%;
|
|
flex-grow: 100;
|
|
|
|
position: relative;
|
|
background: linear-gradient(#222, #000 2rem);
|
|
}
|
|
.plot canvas {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* Alternative layouts */
|
|
/* compact: more compact, */
|
|
:root.compact {
|
|
font-size: 9pt;
|
|
}
|
|
:root.compact #params {
|
|
padding: 0.5rem;
|
|
}
|
|
/* columns: horizontal layout, not vertical */
|
|
:root.columns body {
|
|
flex-direction: row;
|
|
}
|
|
:root.columns #params {
|
|
flex-direction: column;
|
|
}
|
|
:root.columns #header {
|
|
display: none;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1 id="header">{name}</h1>
|
|
<section id="params">
|
|
<template @foreach>
|
|
<label class="param-range" @if="${d => d.$type == 'ParamRange'}">
|
|
<div class="param-range-name">{name}</div>
|
|
<script>
|
|
let gestureUnit = Symbol(), scrollTimeout = Symbol();
|
|
function move(data, dx, dy, element) {
|
|
let prev = data.gesture ? data[gestureUnit] : data.rangeUnit;
|
|
data[gestureUnit] = data.rangeUnit = Math.min(1, Math.max(0, prev - dy/250));
|
|
}
|
|
function scroll(data, dx, dy, element) {
|
|
let prev = data.gesture ? data[gestureUnit] : data.rangeUnit;
|
|
|
|
clearTimeout(element[scrollTimeout]);
|
|
data.gesture = true;
|
|
element[scrollTimeout] = setTimeout(_ => data.gesture = false, 500);
|
|
|
|
data[gestureUnit] = data.rangeUnit = Math.min(1, Math.max(0, prev + dy/250));
|
|
}
|
|
function press(data, count, event, element) {
|
|
clearTimeout(element[scrollTimeout]);
|
|
data.gesture = true;
|
|
data[gestureUnit] = data.rangeUnit;
|
|
if (count == 2) {
|
|
data.value = data.defaultValue;
|
|
data.gesture = false;
|
|
}
|
|
}
|
|
function unpress(data) {
|
|
data.gesture = false;
|
|
}
|
|
|
|
function drawDial(data, canvas) {
|
|
let scale = window.devicePixelRatio;
|
|
let pixels = canvas.offsetWidth*scale;
|
|
if (canvas.width != pixels) canvas.width = pixels;
|
|
if (canvas.height != pixels) canvas.height = pixels;
|
|
|
|
let context = canvas.getContext('2d');
|
|
context.resetTransform();
|
|
context.clearRect(0, 0, pixels, pixels);
|
|
|
|
context.scale(pixels/2, pixels/2);
|
|
context.translate(1, 1);
|
|
|
|
context.beginPath();
|
|
context.ellipse(0, 0, 0.8, 0.8, 0, 0, Math.PI*2);
|
|
context.fillStyle = '#FFF';
|
|
context.fill();
|
|
|
|
context.lineWidth = 0.2;
|
|
context.lineCap = 'round';
|
|
context.beginPath();
|
|
context.ellipse(0, 0, 0.9, 0.9, 0, Math.PI*-1.25, Math.PI*0.25);
|
|
context.strokeStyle = '#0001';
|
|
context.stroke();
|
|
|
|
let rangeUnit = data.rangeUnit;
|
|
if (data.gesture) rangeUnit = data[gestureUnit];
|
|
|
|
context.beginPath();
|
|
context.ellipse(0, 0, 0.9, 0.9, 0, Math.PI*-1.25, Math.PI*(-1.249 + rangeUnit*1.499));
|
|
context.strokeStyle = '#000';
|
|
context.stroke();
|
|
}
|
|
</script>
|
|
<div class="param-range-value" $move="${move}" $scroll="${scroll}" $press="${press}" $unpress="${unpress}">
|
|
<canvas class="param-range-dial" $update="${drawDial}"></canvas>
|
|
<output class="param-range-text">{text}</output><br>
|
|
<div class="param-range-units">{textUnits}</div>
|
|
</div>
|
|
</label>
|
|
</template>
|
|
</section>
|
|
<template @foreach>
|
|
<div class="plot" @if="${d => d.$type == 'Spectrum'}" $fullscreentoggle='foo'>
|
|
<canvas class="plot-grid" $update="${drawSpectrum}" $resize="${drawSpectrum}"></canvas>
|
|
<canvas class="plot-data" $update="${drawSpectrum}" $resize="${drawSpectrum}"></canvas>
|
|
<canvas class="plot-labels" $update="${drawSpectrum}" $resize="${drawSpectrum}"></canvas>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
// Query keys set CSS class on the root
|
|
new URL(location).searchParams.forEach((value, key) => {
|
|
document.body.parentNode.classList.add(key);
|
|
});
|
|
|
|
let plotColours = ['#8CF', '#FC8', '#8D8', '#F9B'];
|
|
function freqScale(width, lowHz, highHz) {
|
|
// let a = -289.614, b = 1176.76, c = 15.3385, d = -0.552833; // Bark
|
|
let low = lowHz/(1500 + lowHz);
|
|
let high = highHz/(1500 + highHz);
|
|
|
|
let scale = width/(high - low);
|
|
return hz => {
|
|
let v = hz/(1500 + hz);
|
|
return scale*(v - low);
|
|
};
|
|
}
|
|
function drawSpectrum(data, canvas) {
|
|
let context = canvas.getContext('2d');
|
|
let width = canvas.offsetWidth, height = canvas.offsetHeight;
|
|
{
|
|
let pixelWidth = Math.round(width*window.devicePixelRatio);
|
|
let pixelHeight = Math.round(height*devicePixelRatio);
|
|
if (canvas.width != pixelWidth) canvas.width = pixelWidth;
|
|
if (canvas.height != pixelHeight) canvas.height = pixelHeight;
|
|
|
|
context.resetTransform();
|
|
context.clearRect(0, 0, pixelWidth, pixelHeight);
|
|
context.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
}
|
|
|
|
let baseHz = 100, maxHz = data.hz[data.hz.length - 1];
|
|
let xScale = freqScale(width, data.hz[0], maxHz);
|
|
let yScale = e => height*Math.log10(e + 1e-30)*10/-105;
|
|
yScale = e => {
|
|
e = Math.pow(e, 0.12);
|
|
e = e*0.6/(1 - 0.6 - e + 2*e*0.6);
|
|
return height*(1 - e);
|
|
};
|
|
|
|
if (canvas.classList.contains('plot-grid')) {
|
|
context.beginPath();
|
|
context.lineWidth = 1;
|
|
context.strokeStyle = '#555';
|
|
for (let baseHz = 100; baseHz < maxHz; baseHz *= 10) {
|
|
for (let i = 1; i < 10; ++i) {
|
|
let hz = baseHz*i;
|
|
let x = xScale(hz);
|
|
context.moveTo(x, 0);
|
|
context.lineTo(x, height);
|
|
}
|
|
}
|
|
for (let db = 0; db >= -240; db -= 12) {
|
|
let e = Math.pow(10, db/10);
|
|
let y = yScale(e);
|
|
context.moveTo(0, y);
|
|
context.lineTo(width, y);
|
|
}
|
|
context.stroke();
|
|
} else if (canvas.classList.contains('plot-data')) {
|
|
context.lineWidth = 1.5;
|
|
context.lineCap = 'round';
|
|
context.lineJoin = 'round';
|
|
|
|
let xMapped = Matsui.getRaw(data.hz).map(xScale);
|
|
data.energy.forEach((line, index) => {
|
|
context.strokeStyle = plotColours[index%plotColours.length];
|
|
context.globalAlpha = 6/(6 + index);
|
|
context.beginPath();
|
|
for (let i = 0; i < xMapped.length; ++i) {
|
|
let e = line[i];
|
|
let y = yScale(e);
|
|
context.lineTo(xMapped[i], y);
|
|
}
|
|
context.stroke();
|
|
if (1) {
|
|
context.fillStyle = plotColours[index%plotColours.length];
|
|
context.lineTo(width, height);
|
|
context.lineTo(0, height);
|
|
context.globalAlpha = 2/(10 + index);
|
|
context.fill();
|
|
}
|
|
});
|
|
} else {
|
|
context.globalAlpha = 1;
|
|
context.lineWidth = 2;
|
|
context.strokeStyle = '#0006';
|
|
context.fillStyle = '#DDD';
|
|
for (let baseHz = 100; baseHz < maxHz; baseHz *= 10) {
|
|
let text = (baseHz < 1000) ? baseHz + "Hz" : baseHz/1000 + "kHz";
|
|
context.strokeText(text, xScale(baseHz) + 2, height - 2);
|
|
context.fillText(text, xScale(baseHz) + 2, height - 2);
|
|
}
|
|
for (let db = 0; db > -105; db -= (db <= -48 ? 24 : 12)) {
|
|
let e = Math.pow(10, db/10);
|
|
let y = yScale(e);
|
|
context.strokeText(db + "dB", 2, y + 4 + 4*(db == 0));
|
|
context.fillText(db + "dB", 2, y + 4 + 4*(db == 0));
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script src="cbor.min.js"></script>
|
|
<script src="matsui-bundle.min.js"></script>
|
|
<script>
|
|
Matsui.global.attributes.fullscreentoggle = (element) => {
|
|
Matsui.global.attributes.press(element, count => {
|
|
if (count == 2) {
|
|
if (document.fullscreenElement == element) {
|
|
document.exitFullscreen();
|
|
} else {
|
|
element.requestFullscreen();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
let state = Matsui.replace(document.body, {name: "..."});
|
|
state.trackMerges(merge => {
|
|
console.log(JSON.stringify(merge));
|
|
window.parent.postMessage(CBOR.encode(merge), '*');
|
|
});
|
|
let pendingMerge = null;
|
|
addEventListener('message', e => {
|
|
let merge = CBOR.decode(e.data);
|
|
if (pendingMerge !== null) {
|
|
Matsui.merge.apply(pendingMerge, merge);
|
|
} else {
|
|
pendingMerge = merge;
|
|
requestAnimationFrame(_ => {
|
|
pendingMerge = null;
|
|
state.merge(merge);
|
|
});
|
|
}
|
|
});
|
|
|
|
if (window.parent !== window) window.parent.postMessage(CBOR.encode("ready"), '*');
|
|
|
|
// Monitor framerate and send every 200ms
|
|
let frameMs = 100, prevFrame = Date.now(), prevSent = 100;
|
|
requestAnimationFrame(function nextFrame() {
|
|
let now = Date.now(), delta = now - prevFrame;
|
|
frameMs += (delta - frameMs)*frameMs/1000;
|
|
prevFrame = now;
|
|
|
|
prevSent += delta;
|
|
if (prevSent > 200) {
|
|
window.parent.postMessage(CBOR.encode(["meters", frameMs/1000, 0.3]));
|
|
prevSent = 0;
|
|
}
|
|
requestAnimationFrame(nextFrame);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|