feat(web): control surface page
CI build oscope-of / build-check (push) Has been cancelled

Vanilla JS WS sender + pad armed-state toggle + OSC wiring.
10 pattern pads, tempo/filter ranges, concert/harmony/fx sections.
This commit is contained in:
L'électron rare
2026-06-27 22:54:18 +02:00
parent b145655892
commit c1b0cf07b6
3 changed files with 94 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
* { box-sizing: border-box; } body { margin: 0; background: #111; color: #eee;
font: 16px/1.3 -apple-system, system-ui, sans-serif; padding: 12px; }
header { display: flex; align-items: center; gap: 10px; }
h1 { font-size: 18px; margin: 4px 0; } h2 { font-size: 14px; color: #9af; margin: 16px 0 8px; text-transform: uppercase; letter-spacing: .06em; }
#status { width: 12px; height: 12px; border-radius: 50%; display: inline-block; }
#status.on { background: #4c8; } #status.off { background: #c44; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; }
button { background: #222; color: #eee; border: 1px solid #333; border-radius: 10px;
padding: 18px 10px; font-size: 16px; cursor: pointer; }
button:active { background: #2a2a2a; } button.armed { background: #2b6; color: #061; border-color: #4d9; font-weight: 700; }
button.wide { width: 100%; margin: 8px 0; background: #422; border-color: #633; }
label { display: block; margin: 10px 0; } input[type=range] { width: 100%; height: 36px; }
+31
View File
@@ -0,0 +1,31 @@
const ws = new WebSocket(`ws://${location.host}/ws`);
const dot = () => document.getElementById("status");
ws.addEventListener("open", () => dot().className = "on");
ws.addEventListener("close", () => dot().className = "off");
function send(address, ...args) {
if (ws.readyState === 1) ws.send(JSON.stringify({ address, args }));
}
const armed = new Set();
function togglePad(el, name) {
const on = !armed.has(name);
on ? armed.add(name) : armed.delete(name);
el.classList.toggle("armed", on);
send("/launch", name, on ? 1 : 0);
}
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("[data-pad]").forEach((el) =>
el.addEventListener("click", () => togglePad(el, el.dataset.pad)));
document.querySelectorAll("[data-osc]").forEach((el) =>
el.addEventListener("click", () => {
const a = el.dataset.arg;
send(el.dataset.osc, ...(a === undefined ? [] : [isNaN(+a) ? a : +a]));
}));
const tempo = document.getElementById("tempo");
tempo && tempo.addEventListener("input", () => send("/launch/tempo", +tempo.value));
const filt = document.getElementById("filter");
filt && filt.addEventListener("input", () => send("/control/fx/filter", +filt.value));
document.getElementById("clear").addEventListener("click", () => {
armed.clear(); document.querySelectorAll("[data-pad]").forEach((e) => e.classList.remove("armed"));
send("/launch/clear");
});
});
+51
View File
@@ -0,0 +1,51 @@
<!doctype html><html lang="fr"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>AV-Live — Control</title><link rel="stylesheet" href="control.css"></head>
<body>
<header><h1>AV-Live Control</h1><span id="status" class="off"></span></header>
<section><h2>Patterns</h2>
<div class="grid">
<button data-pad="kick">Kick</button><button data-pad="hats">Hats</button>
<button data-pad="clap">Clap</button><button data-pad="perc">Perc</button>
<button data-pad="sub">Sub</button><button data-pad="acid">Acid</button>
<button data-pad="arp">Arp</button><button data-pad="lead">Lead</button>
<button data-pad="stab">Stab</button><button data-pad="pad">Pad</button>
</div>
<button id="clear" class="wide">Tout couper</button>
<label>Tempo <input id="tempo" type="range" min="60" max="180" value="126"></label>
</section>
<section><h2>Concert</h2>
<div class="grid">
<button data-osc="/control/concertNext" data-arg="1">Morceau suivant</button>
<button data-osc="/control/doScene" data-arg="concert">Scène concert</button>
<button data-osc="/control/doScene" data-arg="play">Body-play</button>
</div>
</section>
<section><h2>Harmonie</h2>
<div class="grid">
<button data-osc="/control/harmony/root" data-arg="-2">Root </button>
<button data-osc="/control/harmony/root" data-arg="2">Root +</button>
<button data-osc="/control/harmony/scale" data-arg="minor">Minor</button>
<button data-osc="/control/harmony/scale" data-arg="dorian">Dorian</button>
<button data-osc="/control/harmony/scale" data-arg="phrygian">Phrygian</button>
<button data-osc="/control/harmony/scale" data-arg="penta">Penta</button>
<button data-osc="/control/harmony/octave" data-arg="0">Oct 0</button>
<button data-osc="/control/harmony/octave" data-arg="1">Oct +1</button>
<button data-osc="/control/harmony/phrase" data-arg="0">Phr 0</button>
<button data-osc="/control/harmony/phrase" data-arg="1">Phr 1</button>
<button data-osc="/control/harmony/phrase" data-arg="2">Phr 2</button>
<button data-osc="/control/harmony/phrase" data-arg="3">Phr 3</button>
</div>
</section>
<section><h2>FX</h2>
<label>Filtre <input id="filter" type="range" min="0" max="1" step="0.01" value="1"></label>
<div class="grid">
<button data-osc="/control/fx/stutter">Stutter</button>
<button data-osc="/control/fx/crash">Crash</button>
<button data-osc="/control/fx/kick">Kick</button>
<button data-osc="/control/fx/swell">Swell</button>
<button data-osc="/control/fx/breakdown" data-arg="1">Breakdown ↓</button>
<button data-osc="/control/fx/breakdown" data-arg="0">Breakdown ↑</button>
</div>
</section>
<script src="control.js"></script></body></html>