web: hide PM_E-1 from landing (focus PM_E-2); clarify pm_e-2 device controls

Landing (index.html):
- Remove the PM_E-1 (editor.html) viewport; default the live viewport to PM_E-2.
- Repoint the vp-bar + the "design in the editor" link to pm_e-2.html. editor.html
  still exists, just not featured on the landing.

PM_E-2 editor (pm_e-2.html) - the device-connection badge + Device-audio toggle:
- Group both in the header as matching .devctrl pills, side by side.
- Clear tooltips spelling out exactly what each does: the badge only REPORTS the
  USB-MIDI link (green + name when a PM device is plugged in); Device audio is an
  on/off switch that routes a connected device through the computer speakers and
  does not require a device to toggle.
- Device-audio button now shows on/off state via colour (green when on), matching
  the badge, instead of the .primary class (which clashed with the pill style).
- Fix _isDevicePort: it only matched pico/circuitpython/pimoroni/varasys, so the
  native Rust devices ("PM_G-1 Grid" etc.) were never recognized -> the badge
  stayed "no device" even when connected. Now matches pm_g/pm_k/pm_x/grid/polymeter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-06-04 12:23:15 -05:00
parent 5f7c85d910
commit 15392174aa
2 changed files with 31 additions and 15 deletions

View file

@ -95,7 +95,7 @@
<div class="phil-grid"> <div class="phil-grid">
<div class="phil"> <div class="phil">
<h3>🛠️ Program on the web, play on any device</h3> <h3>🛠️ Program on the web, play on any device</h3>
<p>The website is the workbench: design in the <a href="/editor.html">editor</a>, and the same <p>The website is the workbench: design in the <a href="/pm_e-2.html">editor</a>, and the same
<b>program string</b> loads into whichever form factor fits the moment. One engine, one language.</p> <b>program string</b> loads into whichever form factor fits the moment. One engine, one language.</p>
</div> </div>
<div class="phil"> <div class="phil">
@ -110,7 +110,7 @@
<div class="panes" id="panes"></div> <div class="panes" id="panes"></div>
<div class="viewport"> <div class="viewport">
<div class="vp-bar"><span id="vpName"><b>PM_E1 Editor</b></span><span><a id="vpInfo" href="/info-editor.html" target="_blank" rel="noopener">Specs &amp; info ⓘ</a> &nbsp;·&nbsp; <a id="vpOpen" href="/editor.html" target="_blank" rel="noopener">Open full page ↗</a></span></div> <div class="vp-bar"><span id="vpName"><b>PM_E2 Editor</b></span><span><a id="vpInfo" href="/info-pm_e-2.html" target="_blank" rel="noopener">Specs &amp; info ⓘ</a> &nbsp;·&nbsp; <a id="vpOpen" href="/pm_e-2.html" target="_blank" rel="noopener">Open full page ↗</a></span></div>
<iframe id="vp" title="PolyMeter — live viewport" allow="autoplay"></iframe> <iframe id="vp" title="PolyMeter — live viewport" allow="autoplay"></iframe>
</div> </div>
@ -159,8 +159,8 @@ const SAMPLES = {}; let state = { bpm:120, volume:0.85 }, meters = [], muteWindo
/*@BUILD:include:src/setlists.js@*/ /*@BUILD:include:src/setlists.js@*/
const VERSIONS = [ const VERSIONS = [
{ key:"editor", file:"/editor.html", name:"PM_E1 Editor", chip:"app", h:620, sum:"Design grooves: stack meter lanes, perstep accents/ghosts/mutes, swing &amp; polyrhythm, set lists, perlane dB gain." }, // PM_E-1 (editor.html) is hidden from the landing — PM_E-2 is the focus. The page still exists.
{ key:"pme2", file:"/pm_e-2.html", name:"PM_E2 Editor", chip:"app", h:640, sum:"Secondgeneration editor built around engraved drum notation — a 5line percussion staff (Bravura/SMuFL) with Staff / TUBS / Konnakol views, editonstaff, plus flams/drags/rolls, odd meters &amp; clave." }, { key:"pme2", file:"/pm_e-2.html", name:"PM_E2 Editor", chip:"app", h:640, sum:"The PolyMeter editor, built around engraved drum notation — a 5line percussion staff (Bravura/SMuFL) with Staff / TUBS / Konnakol views, editonstaff, plus flams/drags/rolls, odd meters &amp; clave." },
{ key:"kit", file:"/kit.html", name:"PM_K1 Kit", chip:"hw", h:560, sum:"Build it today — a Raspberry Pi Pico on the 52Pi touchscreen kit; tap the 3.5″ screen, joystick tempo, RGB beat light, buzzer. MicroPython firmware included." }, { key:"kit", file:"/kit.html", name:"PM_K1 Kit", chip:"hw", h:560, sum:"Build it today — a Raspberry Pi Pico on the 52Pi touchscreen kit; tap the 3.5″ screen, joystick tempo, RGB beat light, buzzer. MicroPython firmware included." },
{ key:"explorer", file:"/explorer.html", name:"PM_X1 Explorer", chip:"hw", h:500, sum:"Offtheshelf — the Pimoroni Explorer (RP2350, 2.8″ LCD, 6 buttons, piezo) as a buttondriven sibling to the Kit. Edit on the web with Live sync; the device mirrors play/stop/tempo/track changes both ways." }, { key:"explorer", file:"/explorer.html", name:"PM_X1 Explorer", chip:"hw", h:500, sum:"Offtheshelf — the Pimoroni Explorer (RP2350, 2.8″ LCD, 6 buttons, piezo) as a buttondriven sibling to the Kit. Edit on the web with Live sync; the device mirrors play/stop/tempo/track changes both ways." },
{ key:"grid", file:"/grid.html", name:"PM_G1 Grid", chip:"hw", h:470, sum:"Offtheshelf — a Pimoroni Pico Scroll Pack (17×7 white LED matrix + 4 buttons) on a Raspberry Pi Pico. The matrix IS the editor's lane × step pad grid in miniature; edit on the web with Live sync." }, { key:"grid", file:"/grid.html", name:"PM_G1 Grid", chip:"hw", h:470, sum:"Offtheshelf — a Pimoroni Pico Scroll Pack (17×7 white LED matrix + 4 buttons) on a Raspberry Pi Pico. The matrix IS the editor's lane × step pad grid in miniature; edit on the web with Live sync." },
@ -175,7 +175,7 @@ const VERSIONS = [
const infoOf = (f) => f.startsWith("/info-") ? f : f.replace("/", "/info-"); const infoOf = (f) => f.startsWith("/info-") ? f : f.replace("/", "/info-");
const DEFAULT_PROG = (typeof SEED_SETLISTS !== "undefined" && SEED_SETLISTS[0] && SEED_SETLISTS[0].items[0] && SEED_SETLISTS[0].items[0][1]) || "v1;t120;kick:4;snare:4=.X.X;hat:4/2"; const DEFAULT_PROG = (typeof SEED_SETLISTS !== "undefined" && SEED_SETLISTS[0] && SEED_SETLISTS[0].items[0] && SEED_SETLISTS[0].items[0][1]) || "v1;t120;kick:4;snare:4=.X.X;hat:4/2";
let cur = "editor", userEditing = false; let cur = "pme2", userEditing = false;
const vp = $("vp"), box = $("prog"); const vp = $("vp"), box = $("prog");
const verOf = (k) => VERSIONS.find((v) => v.key === k); const verOf = (k) => VERSIONS.find((v) => v.key === k);
@ -246,7 +246,7 @@ addEventListener("message", (e) => {
renderPanes(); renderPanes();
// default = each device's built-in set lists (no forced program); the box fills from what the device reports // default = each device's built-in set lists (no forced program); the box fills from what the device reports
loadVersion("editor"); loadVersion("pme2");
/*@BUILD:include:src/chrome.js@*/ /*@BUILD:include:src/chrome.js@*/
</script> </script>
</body> </body>

View file

@ -274,6 +274,11 @@
.sect > summary::before { content:"\25be\00a0"; } /* ▾ open */ .sect > summary::before { content:"\25be\00a0"; } /* ▾ open */
.sect:not([open]) > summary::before { content:"\25b8\00a0"; } /* ▸ closed */ .sect:not([open]) > summary::before { content:"\25b8\00a0"; } /* ▸ closed */
.sect > summary .shint { text-transform:none; letter-spacing:normal; font-size:11px; opacity:.7; } .sect > summary .shint { text-transform:none; letter-spacing:normal; font-size:11px; opacity:.7; }
/* the two device controls in the header — matching pills (state shown via colour/border in JS) */
.devctrl{ font:inherit; font-size:12px; line-height:1.5; padding:3px 10px; border-radius:7px;
border:1px solid var(--edge); background:transparent; color:var(--muted);
white-space:nowrap; cursor:pointer; }
.devctrl:hover{ border-color:var(--muted); }
</style> </style>
</head> </head>
<body> <body>
@ -288,8 +293,11 @@
<img class="brand-logo brand-light" src="data:image/png;base64,@BUILD:logo-light@" alt="VARASYS — Simplifying Complexity" /> <img class="brand-logo brand-light" src="data:image/png;base64,@BUILD:logo-light@" alt="VARASYS — Simplifying Complexity" />
</a> </a>
<h1 style="margin:0">PM_E2 <span style="font-weight:400; opacity:.75">PolyMeter Editor</span> <span style="font-weight:400; opacity:.5; font-size:12px">Notation</span></h1> <h1 style="margin:0">PM_E2 <span style="font-weight:400; opacity:.75">PolyMeter Editor</span> <span style="font-weight:400; opacity:.5; font-size:12px">Notation</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:8px">
<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> <span id="devBadge" class="devctrl"
title="Device link (USB-MIDI). Click to connect — Chrome, Edge or Firefox. Turns green and shows the name while a PM device (PM_K-1 / PM_X-1 / PM_G-1) is plugged in; ◎ means none is detected. This only reports the connection — it does not make sound on its own.">◎ connect device</span>
<button id="midiBtn" class="devctrl"
title="Device audio — on/off switch. When ON, the notes a connected device sends over USB-MIDI are played through THIS computer's speakers (the device drives the sound, locked to its clock). You can switch it on before plugging in — it doesn't need a device to toggle; plug one in while it's on and it sounds through the computer.">🎹 Device audio</button>
<button id="helpBtn" title="keyboard shortcuts (?)">?</button> <button id="helpBtn" title="keyboard shortcuts (?)">?</button>
</div> </div>
</div> </div>
@ -311,7 +319,7 @@
</div> </div>
<div class="ctx" id="ctxDisplay">&nbsp;</div> <div class="ctx" id="ctxDisplay">&nbsp;</div>
</div> </div>
<div class="btnrow" style="margin-top:10px"><button class="primary" id="startBtn">▶ Start</button><button id="tapBtn">Tap</button><span id="saveItemWrap" style="display:inline-flex" title="Select a set-list item to enable Save"><button id="saveItemBtn" disabled>💾 Save</button></span><button id="midiBtn" title="Play a connected PM_K-1 device through this computer's speakers (Web MIDI · Chrome/Edge)">🎹 Device audio</button></div> <div class="btnrow" style="margin-top:10px"><button class="primary" id="startBtn">▶ Start</button><button id="tapBtn">Tap</button><span id="saveItemWrap" style="display:inline-flex" title="Select a set-list item to enable Save"><button id="saveItemBtn" disabled>💾 Save</button></span></div>
</div> </div>
<div style="flex:1; min-width:200px"> <div style="flex:1; min-width:200px">
@ -1211,10 +1219,14 @@ async function loadFromDevice() {
let _midiAccess = null, _midiOn = false, _midiFlash = 0, _midiBeat = 0, _saveCb = null, _verCb = null; let _midiAccess = null, _midiOn = false, _midiFlash = 0, _midiBeat = 0, _saveCb = null, _verCb = null;
function _midiInputs() { return _midiAccess ? [..._midiAccess.inputs.values()] : []; } function _midiInputs() { return _midiAccess ? [..._midiAccess.inputs.values()] : []; }
function _midiOutputs() { return _midiAccess ? [..._midiAccess.outputs.values()] : []; } function _midiOutputs() { return _midiAccess ? [..._midiAccess.outputs.values()] : []; }
function _isDevicePort(p) { // recognise PM_K-1 (Pico) and PM_X-1 (Pimoroni Explorer RP2350) USB-MIDI ports; function _isDevicePort(p) { // recognise the PM devices' USB-MIDI ports by name;
const n = (p.name || "").toLowerCase(); // anything unrecognised triggers a broadcast to all outputs - bad for ACK routing. const n = (p.name || "").toLowerCase(); // anything unrecognised triggers a broadcast to all outputs - bad for ACK routing.
return n.includes("pico") || n.includes("circuitpython") || n.includes("usb_midi") || return n.includes("pico") || n.includes("circuitpython") || n.includes("usb_midi") ||
n.includes("pimoroni") || n.includes("explorer") || n.includes("rp2350") || n.includes("varasys"); n.includes("pimoroni") || n.includes("explorer") || n.includes("rp2350") || n.includes("varasys") ||
// native Rust firmware enumerates as e.g. "PM_G-1 Grid" / "PM_K-1" / "PM_X-1" (VARASYS / PolyMeter)
n.includes("pm_g") || n.includes("pm_k") || n.includes("pm_x") ||
n.includes("pm-g") || n.includes("pm-k") || n.includes("pm-x") ||
n.includes("grid") || n.includes("polymeter");
} }
function _send(bytes) { // send only to the PM_K-1 (not loopback ports like "Midi Through", which just echo) function _send(bytes) { // send only to the PM_K-1 (not loopback ports like "Midi Through", which just echo)
const outs = _midiOutputs(), dev = outs.filter(_isDevicePort); const outs = _midiOutputs(), dev = outs.filter(_isDevicePort);
@ -1260,10 +1272,14 @@ function _heartbeat(on) { // while Device audio is on: tell the device a host
} }
function updateMidiBtn() { function updateMidiBtn() {
const b = $("midiBtn"); if (!b) return; const b = $("midiBtn"); if (!b) return;
if (!_midiOn) { b.textContent = "🎹 Device audio"; b.classList.remove("primary"); b.style.boxShadow = ""; return; } if (!_midiOn) { // off: muted pill, matching the device badge's idle look
const names = _midiInputs().map((i) => i.name || "MIDI"); b.textContent = "🎹 Device audio";
b.textContent = names.length ? "🎹 " + names[0].slice(0, 16) : "🎹 no device"; b.style.color = "var(--muted)"; b.style.borderColor = "var(--edge)"; b.style.boxShadow = "";
b.classList.add("primary"); return;
}
const names = _midiInputs().map((i) => i.name || "MIDI"); // on: green, like the connected badge
b.textContent = "🎹 " + (names.length ? names[0].slice(0, 16) : "audio on");
b.style.color = "#2fe07a"; b.style.borderColor = "#2fe07a";
} }
async function toggleDeviceAudio() { async function toggleDeviceAudio() {
if (_midiOn) { _midiOn = false; _heartbeat(false); updateMidiBtn(); return; } // inputs stay bound (for Save ACKs) if (_midiOn) { _midiOn = false; _heartbeat(false); updateMidiBtn(); return; } // inputs stay bound (for Save ACKs)