1
0
basics/stfx/ui/html/generic.html
2025-07-01 11:55:12 +01:00

337 lines
9.3 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>Generic STFX UI</title>
<style>
* {
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%;
}
</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>
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 press(data, count) {
data.gesture = true;
data._gestureUnit = data.rangeUnit;
if (count == 2) {
data.value = data.defaultValue;
data.gesture = false;
}
}
function unpress(data) {
data.gesture = false;
}
</script>
<div class="param-range-value" $move="${move}" $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>
<script>
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>
</section>
<template @foreach>
<div class="plot" @if="${d => d.$type == 'Spectrum'}">
<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>
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.resize = (element, fn) => {
element.classList.add('_matsuiResize');
element._matsuiResize = fn;
};
addEventListener('resize', e => {
document.querySelectorAll('._matsuiResize').forEach(e => {
let fn = e._matsuiResize;
if (fn) fn(e, e.offsetWidth, e.offsetHeight);
});
});
let state = Matsui.replace(document.body, {name: "..."});
state.trackMerges(merge => {
window.parent.postMessage(CBOR.encode(merge), '*');
});
addEventListener('message', e => {
let merge = CBOR.decode(e.data);
window.merge = merge;//console.log(merge);
state.merge(merge);
});
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>