From 31111abbcbb43f018c5bcebb5a9e72719428a897 Mon Sep 17 00:00:00 2001 From: Me Here Date: Thu, 28 May 2026 10:58:19 -0500 Subject: [PATCH] =?UTF-8?q?Phase=20B=20=E2=80=94=20landing=20rebuild=20+?= =?UTF-8?q?=20editor=20program=20I/O=20+=20per-lane=20dB=20knob?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editor: per-lane gain knob (drag/scroll/double-click, dB, applied at schedule time — no stutter); program box now decodes base64 set-list codes + lints (clear ✓/✗ message). When embedded it posts its program to the parent. Landing (Concepts) rebuilt to the spec: description first, the EDITOR open by default in a live viewport, a summary pane per form factor (click loads it into the viewport), and a program I/O box that shows the current program decoded (not base64), accepts plain text OR a base64 set-list code, lints it, loads it into the viewport, and copies it. Viewport auto-sizes + reflects the device's posted program. Engine codec inlined for decode/lint. Co-Authored-By: Claude Opus 4.7 (1M context) --- editor.html | 69 ++++++++++++-- index.html | 254 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 214 insertions(+), 109 deletions(-) diff --git a/editor.html b/editor.html index 6dbca39..46baa35 100644 --- a/editor.html +++ b/editor.html @@ -129,6 +129,12 @@ .sum { font-family:"Courier New",monospace; font-size:12px; color:var(--muted); min-width:24px; } .bar { font-family:"Courier New",monospace; font-size:12px; color:var(--hot); min-width:46px; text-align:right; } .cmp { background:var(--panel); color:var(--txt); border:1px solid var(--edge); border-radius:8px; padding:7px 8px; font-size:12px; } + .gain { font-size:11px; color:var(--muted); border:1px solid var(--edge); border-radius:8px; padding:6px 7px; + cursor:ns-resize; user-select:none; min-width:42px; text-align:center; font-variant-numeric:tabular-nums; + touch-action:none; background:var(--panel); } + .gain:hover { color:var(--txt); } + .gain.boost { color:#5fd08a; border-color:#2f6b48; } + .gain.cut { color:#ff9a8a; border-color:#7a3b34; } .meter-card .led { width:26px; height:26px; border-radius:6px; } .meter-card .led.sub { width:17px; height:17px; } .x { background:transparent; border:1px solid var(--edge); color:var(--muted); padding:4px 9px; border-radius:7px; margin-left:auto; } @@ -148,6 +154,8 @@ .patchbar button { flex:0 0 auto; font-size:11px; padding:4px 10px; } .patchbar.applied { box-shadow:0 0 0 2px var(--cyan) inset; } .patchbar.err input { color:#ff8a7a; } + .patch-msg { flex:0 1 auto; font-size:11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:46%; } + .patch-msg.ok { color:#5fd08a; } .patch-msg.bad { color:#ff8a7a; } [data-embed] .patchbar { display:none !important; } .ex-item { display:flex; gap:8px; align-items:center; padding:7px 9px; border:1px solid var(--edge); border-radius:8px; margin-bottom:6px; font-size:13px; background:var(--panel); cursor:pointer; } .ex-item:hover { border-color:var(--muted); } @@ -369,11 +377,12 @@ /*@BUILD:include:src/footer.html@*/ - -
+ +
+ placeholder="v1;t120;kick:4;snare:4=.X.X;hat:4/2 (or paste a base64 set-list code)" aria-label="program string (editable)"> +
@@ -606,6 +615,7 @@ function buildLaneCard(m) { + 0 dB
@@ -624,6 +634,21 @@ function buildLaneCard(m) { sub.addEventListener("change", (e) => { const v = e.target.value; m.swing = /s$/.test(v); m.stepsPerBeat = parseInt(v, 10) || 1; recomputeLane(m); }); const sel = $c(`#m${m.id}_sound`); sel.value = m.sound; sel.addEventListener("change", (e) => m.sound = e.target.value); + // per-lane gain (dB) — drag up/down, scroll, or double-click to reset; applied at + // schedule time (no stutter), so no replay is needed when it changes mid-play. + const gainEl = $c(`#m${m.id}_gain`); + const showGain = () => { const d = m.gainDb || 0; gainEl.textContent = (d > 0 ? "+" : "") + d + " dB"; + gainEl.classList.toggle("boost", d > 0); gainEl.classList.toggle("cut", d < 0); }; + const setGain = (d) => { m.gainDb = Math.max(-24, Math.min(6, d)); showGain(); if (typeof refreshPatchField === "function") refreshPatchField(); }; + showGain(); + (function () { let down = false, lastY = 0, acc = 0; + gainEl.addEventListener("pointerdown", (e) => { down = true; lastY = e.clientY; acc = 0; try { gainEl.setPointerCapture(e.pointerId); } catch (_) {} }); + gainEl.addEventListener("pointermove", (e) => { if (!down) return; acc += lastY - e.clientY; lastY = e.clientY; while (Math.abs(acc) >= 4) { const d = acc > 0 ? 1 : -1; acc -= d * 4; setGain((m.gainDb || 0) + d); } }); + gainEl.addEventListener("pointerup", () => down = false); + gainEl.addEventListener("pointercancel", () => down = false); + gainEl.addEventListener("dblclick", () => setGain(0)); + gainEl.addEventListener("wheel", (e) => { e.preventDefault(); setGain((m.gainDb || 0) + (e.deltaY < 0 ? 1 : -1)); }, { passive: false }); + })(); const polyCb = $c(`#m${m.id}_poly`); polyCb.checked = m.poly; polyCb.addEventListener("change", (e) => m.poly = e.target.checked); const enCb = $c(`#m${m.id}_enable`); enCb.checked = m.enabled; @@ -1162,23 +1187,49 @@ function updateStatus() { ctxDisplay.textContent = s; ctxDisplay.classList.toggle("muted-cue", muted || !!pendingSwitch); } -function updateCtx() { updateStatus(); refreshPatchField(); } +function updateCtx() { updateStatus(); refreshPatchField(); postProg(); } +// when embedded (e.g. in the Concepts landing), report the current program to the parent +function postProg() { if (document.documentElement.dataset.embed !== "1") return; + try { parent.postMessage({ type: "varasys-prog", patch: currentPatch() }, "*"); } catch (e) {} } /* keep the subtle program-string bar in sync with what's loaded, unless the user is mid-edit in it (don't yank text out from under them). */ function refreshPatchField() { const el = $("patchField"); if (!el || document.activeElement === el) return; try { el.value = currentPatch(); } catch (e) {} - $("patchBar").classList.remove("err"); + const bar = $("patchBar"); if (bar) bar.classList.remove("err"); + const msg = $("patchMsg"); if (msg && !msg.dataset.sticky) msg.textContent = ""; } +function setPatchMsg(text, ok) { + const msg = $("patchMsg"); if (!msg) return; + msg.textContent = text || ""; msg.classList.toggle("ok", !!ok); msg.classList.toggle("bad", !ok && !!text); + msg.dataset.sticky = text ? "1" : ""; + if (text) setTimeout(() => { msg.dataset.sticky = ""; if (ok) msg.textContent = ""; }, ok ? 1800 : 4000); +} +// Accept a patch string OR a base64 set-list code (or a #p=/#sl= link); decode, lint, load. function commitPatchField() { const el = $("patchField"), bar = $("patchBar"); + let text = (el.value || "").trim(); + if (!text) { setPatchMsg("", true); return; } + const m = text.match(/[#?&](p|sl)=([^&\s]+)/); + let kind = null, payload = text; + if (m) { kind = m[1]; try { payload = decodeURIComponent(m[2]); } catch (e) { payload = m[2]; } } + const looksB64 = /^[A-Za-z0-9_-]{12,}$/.test(payload) && !/[;:]/.test(payload); try { - applyPatch(el.value.trim()); // parse + apply (throws on garbage) - bar.classList.remove("err"); - el.blur(); refreshPatchField(); // normalise to the canonical string + if (kind === "sl" || (kind !== "p" && looksB64)) { + const sl = codeToSetlist(payload); // base64 set-list code → decode + if (!sl.items || !sl.items.length) throw new Error("set-list code has no items"); + applySetup(sl.items[0]); + setPatchMsg("✓ decoded set list “" + sl.title + "” (" + sl.items.length + " item" + (sl.items.length > 1 ? "s" : "") + ") — loaded item 1", true); + } else { + const s = patchToSetup(payload); // plain-text patch + if (!s.lanes.length) throw new Error("no lanes — try e.g. kick:4"); + applyPatch(payload); + setPatchMsg("✓ " + s.lanes.length + " lane" + (s.lanes.length > 1 ? "s" : "") + " · " + s.bpm + " BPM", true); + } + bar.classList.remove("err"); el.blur(); refreshPatchField(); bar.classList.add("applied"); setTimeout(() => bar.classList.remove("applied"), 450); - } catch (e) { bar.classList.add("err"); } + } catch (e) { bar.classList.add("err"); setPatchMsg("✗ " + e.message, false); } } /* ========================================================================= diff --git a/index.html b/index.html index bc5e110..930ba6a 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ VARASYS PolyMeter — Concepts (polymetric groove trainer & metronome) - + -