PE-1 editor display: match the device (always-elapsed, ramp, device link)

Bring the web editor's display in line with the PM_K-1 device screen:
- Elapsed stopwatch is always visible and counts while playing (was gated behind
  the Timers switch); the switch now governs only the countdown.
- Tempo-ramp indicator in the display (↗/↘ amount/everyBars), shown whenever a
  ramp is active — mirrors the device's ramp arrow.
- Header "device" badge that lights green with the port name while a PM_K-1 /
  PM_X-1 is connected over USB-MIDI, updated live on connect/disconnect. The
  editor requests MIDI on load (Chrome remembers the grant) so it reflects the
  link automatically; the badge is also click-to-connect.

editor-beta.html (separate live-sync variant) left as-is.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-31 18:29:53 -05:00
parent 8cbd53a2ee
commit 6aeca94222

View file

@ -253,6 +253,7 @@
<div class="row appheader" style="align-items:center; flex-wrap:wrap; gap:6px 14px; margin-bottom:8px"> <div class="row appheader" style="align-items:center; flex-wrap:wrap; gap:6px 14px; margin-bottom:8px">
<h1 style="margin:0">PM_E1 <span style="font-weight:400; opacity:.75">PolyMeter Editor</span></h1> <h1 style="margin:0">PM_E1 <span style="font-weight:400; opacity:.75">PolyMeter Editor</span></h1>
<div class="appheader-ctrls" style="display:flex; align-items:center; gap:10px"> <div class="appheader-ctrls" style="display:flex; align-items:center; gap:10px">
<span id="devBadge" title="USB-MIDI link to a PM_K-1 / PM_X-1 (Chrome/Edge/Firefox). Click to connect." style="cursor:pointer; font-size:12px; padding:3px 9px; border-radius:7px; border:1px solid var(--edge); color:var(--muted); white-space:nowrap">◎ connect device</span>
<button id="helpBtn" title="keyboard shortcuts (?)">?</button> <button id="helpBtn" title="keyboard shortcuts (?)">?</button>
</div> </div>
</div> </div>
@ -267,6 +268,7 @@
<div class="big" id="bpmDisplay">120</div> <div class="big" id="bpmDisplay">120</div>
<div class="dtimers" id="dtimers"> <div class="dtimers" id="dtimers">
<span title="elapsed (stopwatch)"><span id="elapsedVal">0:00</span></span> <span title="elapsed (stopwatch)"><span id="elapsedVal">0:00</span></span>
<span id="rampWrap" class="tval" title="tempo ramp" hidden></span>
<span id="countWrap" title="time countdown" hidden><span id="countVal" class="tval">0:00</span></span> <span id="countWrap" title="time countdown" hidden><span id="countVal" class="tval">0:00</span></span>
<span id="barWrap" title="bars remaining in this segment" hidden><span id="barVal" class="tval">0</span></span> <span id="barWrap" title="bars remaining in this segment" hidden><span id="barVal" class="tval">0</span></span>
</div> </div>
@ -1201,7 +1203,13 @@ async function _ensureMidi() { // MIDI access WITH SysEx (needed to send/
_midiAccess.onstatechange = _wireMidi; _wireMidi(); _midiAccess.onstatechange = _wireMidi; _wireMidi();
return true; return true;
} }
function _wireMidi() { for (const inp of _midiInputs()) inp.onmidimessage = onDeviceMidi; updateMidiBtn(); } function _wireMidi() { for (const inp of _midiInputs()) inp.onmidimessage = onDeviceMidi; updateMidiBtn(); updateDevBadge(); }
function updateDevBadge() { // header badge: lights green while a PM_K-1 / PM_X-1 is connected over USB-MIDI
const el = $("devBadge"); if (!el) return;
const dev = _midiAccess ? [..._midiOutputs(), ..._midiInputs()].filter(_isDevicePort) : [];
if (dev.length) { el.textContent = "● " + (dev[0].name || "device").slice(0, 18); el.style.color = "#2fe07a"; el.style.borderColor = "#2fe07a"; }
else { el.textContent = _midiAccess ? "◎ no device" : "◎ connect device"; el.style.color = "var(--muted)"; el.style.borderColor = "var(--edge)"; }
}
function onDeviceMidi(e) { function onDeviceMidi(e) {
const d = e.data; if (!d) return; const d = e.data; if (!d) return;
if (d[0] === 0xF0 && d[1] === 0x7D) { // our SysEx reply if (d[0] === 0xF0 && d[1] === 0x7D) { // our SysEx reply
@ -1403,9 +1411,9 @@ function tickTimers() {
const now = Date.now(); const now = Date.now();
const dt = timers.last ? Math.min(now - timers.last, 1000) : 0; // clamp so backgrounded gaps don't jump const dt = timers.last ? Math.min(now - timers.last, 1000) : 0; // clamp so backgrounded gaps don't jump
timers.last = now; timers.last = now;
if (timersOn && state.running) { if (state.running) {
timers.elapsedMs += dt; timers.elapsedMs += dt; // elapsed stopwatch always runs while playing (like the device)
if (timers.totalMs > 0) { if (timersOn && timers.totalMs > 0) {
const before = timers.remainingMs; const before = timers.remainingMs;
timers.remainingMs -= dt; timers.remainingMs -= dt;
// time countdown hit 0 → auto-advance (smooth). Bar-length segments use the // time countdown hit 0 → auto-advance (smooth). Bar-length segments use the
@ -1420,10 +1428,12 @@ function tickTimers() {
renderTimers(); renderTimers();
} }
function renderTimers() { function renderTimers() {
$("dtimers").hidden = !timersOn; $("dtimers").hidden = false; // elapsed + ramp always visible (like the device)
if (!timersOn) return;
$("elapsedVal").textContent = fmtClock(timers.elapsedMs); $("elapsedVal").textContent = fmtClock(timers.elapsedMs);
const off = timers.totalMs <= 0; const rw = $("rampWrap"); // tempo-ramp indicator (matches the device's ramp arrow)
if (ramp.on) { rw.hidden = false; rw.textContent = (ramp.amount < 0 ? " " : " ") + (ramp.amount >= 0 ? "+" : "") + ramp.amount + "/" + ramp.everyBars + "b"; }
else rw.hidden = true;
const off = !timersOn || timers.totalMs <= 0;
$("countWrap").hidden = off; // hide time countdown when off $("countWrap").hidden = off; // hide time countdown when off
if (!off) { if (!off) {
const cd = $("countVal"); const cd = $("countVal");
@ -1576,6 +1586,7 @@ $("saveDeviceBtn").addEventListener("click", () => { $("trayMenu").hidden = true
$("loadDeviceBtn").addEventListener("click", () => { $("trayMenu").hidden = true; loadFromDevice(); }); $("loadDeviceBtn").addEventListener("click", () => { $("trayMenu").hidden = true; loadFromDevice(); });
$("updateFwBtn").addEventListener("click", () => { $("trayMenu").hidden = true; updateFirmware(); }); $("updateFwBtn").addEventListener("click", () => { $("trayMenu").hidden = true; updateFirmware(); });
$("midiBtn").addEventListener("click", toggleDeviceAudio); $("midiBtn").addEventListener("click", toggleDeviceAudio);
$("devBadge").addEventListener("click", () => { _ensureMidi().then(updateDevBadge); });
$("clearLogBtn").addEventListener("click", () => { $("trayMenu").hidden = true; clearLog(); }); $("clearLogBtn").addEventListener("click", () => { $("trayMenu").hidden = true; clearLog(); });
$("resetAllBtn").addEventListener("click", () => { $("trayMenu").hidden = true; resetAll(); }); $("resetAllBtn").addEventListener("click", () => { $("trayMenu").hidden = true; resetAll(); });
$("shareSettingsBtn").addEventListener("click", () => { $("trayMenu").hidden = true; shareSettings(); }); $("shareSettingsBtn").addEventListener("click", () => { $("trayMenu").hidden = true; shareSettings(); });
@ -1663,6 +1674,10 @@ updateCtx();
refreshFeatureBoxes(); refreshFeatureBoxes();
$("continueMode").checked = continueMode; $("continueMode").checked = continueMode;
$("timersOn").checked = timersOn; $("timersOn").checked = timersOn;
// Connect to a PM_K-1 / PM_X-1 over USB-MIDI on load so the header badge reflects the link
// (Chrome remembers the grant; first visit prompts once). updateDevBadge runs via _wireMidi.
if (navigator.requestMIDIAccess) _ensureMidi().then(updateDevBadge).catch(() => updateDevBadge());
else updateDevBadge();
requestAnimationFrame(drawLoop); requestAnimationFrame(drawLoop);
/*@BUILD:include:src/chrome.js@*/ /*@BUILD:include:src/chrome.js@*/
</script> </script>