Phase C — per-page plain/info split + per-device program box
Each form-factor page is now plain by default: title + summary + the front view + a program I/O box (shared src/progbox.* — paste a patch OR a base64 set-list code, decoded + linted + loaded; copy; reflects the device state and posts it to an embedding parent). All technical content — description, BOM, dimensions, top/side views, embedding — hidden behind a "Show technical info" checkbox; ?info=1 opens it checked. Teacher's top-edge view + dimensions are marked .tech so they hide too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
271abfe43a
commit
f1146e720a
8 changed files with 172 additions and 13 deletions
21
micro.html
21
micro.html
|
|
@ -127,6 +127,9 @@
|
||||||
|
|
||||||
/*@BUILD:include:src/header.html@*/
|
/*@BUILD:include:src/header.html@*/
|
||||||
|
|
||||||
|
<h1 class="ff-title">PM‑µ Micro</h1>
|
||||||
|
<p class="ff-sum">Inline practice bar — patch your instrument through it (in one end, amp/headphones out the other) with the click mixed in. One clickable thumb‑roller, an amber 14‑segment display.</p>
|
||||||
|
|
||||||
<div class="device">
|
<div class="device">
|
||||||
<!-- LEFT END: instrument / aux in -->
|
<!-- LEFT END: instrument / aux in -->
|
||||||
<div class="endcap left">
|
<div class="endcap left">
|
||||||
|
|
@ -168,8 +171,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hint">Roll = <b>tempo</b> · press = <b>start / stop</b> · hold & roll = <b>switch track</b>.
|
<div class="hint">Roll = <b>tempo</b> · press = <b>start / stop</b> · hold & roll = <b>switch track</b>.</div>
|
||||||
Instrument in one end, amp/headphones out the other — the click is mixed into your signal in the analog domain.</div>
|
|
||||||
|
/*@BUILD:include:src/progbox.html@*/
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_VERSION = "v0.0.1-dev";
|
const APP_VERSION = "v0.0.1-dev";
|
||||||
|
|
@ -268,6 +272,7 @@ function render(){
|
||||||
$("indBpm").classList.toggle("on", displayMode==="bpm");
|
$("indBpm").classList.toggle("on", displayMode==="bpm");
|
||||||
$("indTrk").classList.toggle("on", displayMode==="track");
|
$("indTrk").classList.toggle("on", displayMode==="track");
|
||||||
$("indPlay").classList.toggle("on", state.running);
|
$("indPlay").classList.toggle("on", state.running);
|
||||||
|
if(window.progRefresh) progRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========================= ROLLER (the only control) ========================= */
|
/* ========================= ROLLER (the only control) ========================= */
|
||||||
|
|
@ -310,9 +315,15 @@ addEventListener("keydown",(e)=>{
|
||||||
{ const ht=tracksFromHash(); if(ht) tracks=ht; } // a #p=/#sl= link (or embed config) overrides the built-ins
|
{ const ht=tracksFromHash(); if(ht) tracks=ht; } // a #p=/#sl= link (or embed config) overrides the built-ins
|
||||||
loadTrack(0);
|
loadTrack(0);
|
||||||
render();
|
render();
|
||||||
|
window.currentProgramString = function(){ var t=tracks[trackIdx]||{}; return setupToPatch({bpm:state.bpm, volume:state.volume, lanes:t.lanes||[]}); };
|
||||||
|
window.loadProgramString = function(plain){ var s=patchToSetup(plain); tracks=[{name:"Program", ...s}]; trackIdx=0; loadTrack(0); };
|
||||||
|
/*@BUILD:include:src/progbox.js@*/
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="about pageonly">
|
<label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info (dimensions, BOM, embedding)</label>
|
||||||
|
<div id="techinfo" class="pageonly" hidden>
|
||||||
|
|
||||||
|
<section class="about">
|
||||||
<h2>PM‑µ — Micro</h2>
|
<h2>PM‑µ — Micro</h2>
|
||||||
<div class="ff-tags"><span class="hw">Hardware</span><span>Inline practice bar</span><span>~$35 one‑off</span></div>
|
<div class="ff-tags"><span class="hw">Hardware</span><span>Inline practice bar</span><span>~$35 one‑off</span></div>
|
||||||
<p>A long, narrow practice bar you patch <i>into</i> your signal: instrument in one end, amp or headphones out
|
<p>A long, narrow practice bar you patch <i>into</i> your signal: instrument in one end, amp or headphones out
|
||||||
|
|
@ -353,6 +364,10 @@ render();
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<p class="sub" style="max-width:760px;margin:14px auto 0">Embed this widget elsewhere with one <code><div></code> + a script —
|
||||||
|
see <a href="/embed.html">the embed docs</a>.</p>
|
||||||
|
</div><!-- /#techinfo -->
|
||||||
|
|
||||||
/*@BUILD:include:src/footer.html@*/
|
/*@BUILD:include:src/footer.html@*/
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
10
player.html
10
player.html
|
|
@ -211,6 +211,9 @@
|
||||||
|
|
||||||
/*@BUILD:include:src/header.html@*/
|
/*@BUILD:include:src/header.html@*/
|
||||||
|
|
||||||
|
<h1 class="ff-title">PM‑1 Initial</h1>
|
||||||
|
<p class="ff-sum">The idealized concept render — a clean screen‑first player with a colour beat display, set‑list navigation, theming and a fullscreen landscape view.</p>
|
||||||
|
|
||||||
<!-- fullscreen "stage mode" toggle — floats over the page (was a header button before the shared header) -->
|
<!-- fullscreen "stage mode" toggle — floats over the page (was a header button before the shared header) -->
|
||||||
<button id="fsBtn" class="fs-float" title="Full screen (landscape)">⛶</button>
|
<button id="fsBtn" class="fs-float" title="Full screen (landscape)">⛶</button>
|
||||||
|
|
||||||
|
|
@ -487,8 +490,12 @@ if(location.hash && /(p|sl)=/.test(location.hash)) loadConfig(location.hash, tru
|
||||||
if(!setlist){ setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); }
|
if(!setlist){ setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); }
|
||||||
renderAll();
|
renderAll();
|
||||||
requestAnimationFrame(draw);
|
requestAnimationFrame(draw);
|
||||||
|
/*@BUILD:include:src/progbox.js@*/
|
||||||
</script>
|
</script>
|
||||||
<section class="about pageonly">
|
|
||||||
|
<label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info</label>
|
||||||
|
<div id="techinfo" class="pageonly" hidden>
|
||||||
|
<section class="about">
|
||||||
<h2>PM‑1 — Initial</h2>
|
<h2>PM‑1 — Initial</h2>
|
||||||
<div class="ff-tags"><span>Concept</span><span>Idealized device</span><span>Not buildable as drawn</span></div>
|
<div class="ff-tags"><span>Concept</span><span>Idealized device</span><span>Not buildable as drawn</span></div>
|
||||||
<p>The idealized PM‑1: the player as a clean, screen‑first device with no concession to mechanical parts yet.
|
<p>The idealized PM‑1: the player as a clean, screen‑first device with no concession to mechanical parts yet.
|
||||||
|
|
@ -500,6 +507,7 @@ requestAnimationFrame(draw);
|
||||||
buildable realization of this idea is the <a href="/teacher.html">PM‑1 Teacher</a> (full priced BOM there);
|
buildable realization of this idea is the <a href="/teacher.html">PM‑1 Teacher</a> (full priced BOM there);
|
||||||
for the smallest practical unit, see the <a href="/micro.html">PM‑µ Micro</a>.</p>
|
for the smallest practical unit, see the <a href="/micro.html">PM‑µ Micro</a>.</p>
|
||||||
</section>
|
</section>
|
||||||
|
</div><!-- /#techinfo -->
|
||||||
|
|
||||||
/*@BUILD:include:src/footer.html@*/
|
/*@BUILD:include:src/footer.html@*/
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,9 @@
|
||||||
/*@BUILD:include:src/header.html@*/
|
/*@BUILD:include:src/header.html@*/
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
|
<h1 class="ff-title">PM‑S Showcase</h1>
|
||||||
|
<p class="ff-sum">A display‑piece metronome — the pendulum is an RGB light bar that combines every lane's subdivisions & accents; a printed tempo scale with a sliding weight sets the tempo.</p>
|
||||||
|
|
||||||
<div class="device"><canvas id="stage" width="300" height="470" aria-label="RGB pendulum metronome"></canvas></div>
|
<div class="device"><canvas id="stage" width="300" height="470" aria-label="RGB pendulum metronome"></canvas></div>
|
||||||
|
|
||||||
<div class="ctrls">
|
<div class="ctrls">
|
||||||
|
|
@ -70,7 +73,12 @@
|
||||||
moving RGB light. Drag the <b>weight</b> up/down (or scroll) to set tempo — the scale is printed on the bar,
|
moving RGB light. Drag the <b>weight</b> up/down (or scroll) to set tempo — the scale is printed on the bar,
|
||||||
just like a wind‑up metronome. (No power switch: the real one starts when you lift it from its holder.)</div>
|
just like a wind‑up metronome. (No power switch: the real one starts when you lift it from its holder.)</div>
|
||||||
|
|
||||||
<section class="about pageonly">
|
/*@BUILD:include:src/progbox.html@*/
|
||||||
|
|
||||||
|
<label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info (dimensions, BOM, embedding)</label>
|
||||||
|
<div id="techinfo" class="pageonly" hidden>
|
||||||
|
|
||||||
|
<section class="about">
|
||||||
<h2>PM‑S — Showcase</h2>
|
<h2>PM‑S — Showcase</h2>
|
||||||
<div class="ff-tags"><span class="hw">Hardware</span><span>Display piece</span><span>~$41 one‑off</span></div>
|
<div class="ff-tags"><span class="hw">Hardware</span><span>Display piece</span><span>~$41 one‑off</span></div>
|
||||||
<p>A metronome as an object: the silhouette of a classic pyramid wind‑up unit, but the swinging pendulum is
|
<p>A metronome as an object: the silhouette of a classic pyramid wind‑up unit, but the swinging pendulum is
|
||||||
|
|
@ -110,6 +118,10 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<p class="sub" style="max-width:760px;margin:14px auto 0">Embed this widget elsewhere with one <code><div></code> + a script —
|
||||||
|
see <a href="/embed.html">the embed docs</a>.</p>
|
||||||
|
</div><!-- /#techinfo -->
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
/*@BUILD:include:src/footer.html@*/
|
/*@BUILD:include:src/footer.html@*/
|
||||||
|
|
@ -125,7 +137,7 @@ const SAMPLES = {};
|
||||||
const state = { bpm:120, volume:0.85, running:false };
|
const state = { bpm:120, volume:0.85, running:false };
|
||||||
let meters = [], muteWindows = [];
|
let meters = [], muteWindows = [];
|
||||||
|
|
||||||
function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); }
|
function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); if(window.progRefresh) progRefresh(); }
|
||||||
function scheduler(){
|
function scheduler(){
|
||||||
const ahead = audioCtx.currentTime + SCHEDULE_AHEAD;
|
const ahead = audioCtx.currentTime + SCHEDULE_AHEAD;
|
||||||
for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } }
|
for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } }
|
||||||
|
|
@ -286,6 +298,9 @@ addEventListener("keydown",(e)=>{
|
||||||
{ const ht=tracksFromHash(); if(ht) tracks=ht; }
|
{ const ht=tracksFromHash(); if(ht) tracks=ht; }
|
||||||
loadTrack(0); syncBtns();
|
loadTrack(0); syncBtns();
|
||||||
requestAnimationFrame(draw);
|
requestAnimationFrame(draw);
|
||||||
|
window.currentProgramString = function(){ var t=tracks[trackIdx]||{}; return setupToPatch({bpm:state.bpm, volume:state.volume, lanes:t.lanes||[]}); };
|
||||||
|
window.loadProgramString = function(plain){ var s=patchToSetup(plain); tracks=[{name:"Program", ...s}]; trackIdx=0; loadTrack(0); syncBtns(); };
|
||||||
|
/*@BUILD:include:src/progbox.js@*/
|
||||||
/*@BUILD:include:src/chrome.js@*/
|
/*@BUILD:include:src/chrome.js@*/
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
27
src/base.css
27
src/base.css
|
|
@ -73,3 +73,30 @@ details.spec .spec-body { padding:2px 16px 16px; }
|
||||||
.about p { margin:0 0 10px; max-width:64ch; }
|
.about p { margin:0 0 10px; max-width:64ch; }
|
||||||
/* page-only chrome (description, dimensioned views, loading panel) — hidden when embedded */
|
/* page-only chrome (description, dimensioned views, loading panel) — hidden when embedded */
|
||||||
[data-embed] .pageonly { display:none !important; }
|
[data-embed] .pageonly { display:none !important; }
|
||||||
|
|
||||||
|
/* ---- per-form-factor page: visible title + summary (plain view) ---- */
|
||||||
|
.ff-title { font-size:20px; margin:6px 0 2px; text-align:center; color:var(--txt,#c7d0db); }
|
||||||
|
.ff-sum { max-width:60ch; margin:0 auto; text-align:center; color:var(--muted,#7f8b9a); font-size:13.5px; line-height:1.55; }
|
||||||
|
[data-embed] .ff-title, [data-embed] .ff-sum { display:none !important; }
|
||||||
|
|
||||||
|
/* ---- per-device program I/O box (plain view) ---- */
|
||||||
|
.progbox { width:100%; max-width:620px; margin:14px auto 0; display:flex; align-items:center; gap:9px; flex-wrap:wrap;
|
||||||
|
padding:9px 12px; border:1px solid var(--panel-bd,#2a313c); border-radius:11px; background:var(--panel-bg,#161b22); }
|
||||||
|
.progbox > label { flex:0 0 auto; font-size:10px; text-transform:uppercase; letter-spacing:.09em; color:var(--muted,#7f8b9a); }
|
||||||
|
.progbox input { flex:1; min-width:160px; background:var(--field-bg,#0e1116); color:var(--txt,#c7d0db);
|
||||||
|
border:1px solid var(--field-bd,#2a313c); border-radius:8px; padding:8px 10px; font-family:"Courier New",monospace; font-size:12.5px; }
|
||||||
|
.progbox input.err { border-color:#c0392b; }
|
||||||
|
.progbox button { flex:0 0 auto; background:var(--field-bg,#0e1116); color:var(--txt,#c7d0db); border:1px solid var(--field-bd,#2a313c);
|
||||||
|
border-radius:8px; padding:8px 12px; font-size:13px; cursor:pointer; }
|
||||||
|
.progbox button.primary { background:linear-gradient(180deg,#34c6ff,var(--cyan)); color:#04121b; border-color:transparent; font-weight:600; }
|
||||||
|
.progbox button:hover { border-color:var(--cyan); }
|
||||||
|
.progbox-msg { flex:1 1 100%; font-size:11.5px; color:var(--muted,#7f8b9a); min-height:1em; }
|
||||||
|
.progbox-msg.ok { color:#5fd08a; } .progbox-msg.bad { color:#ff8a7a; }
|
||||||
|
[data-embed] .progbox { display:none !important; }
|
||||||
|
|
||||||
|
/* ---- "Show info" toggle + technical section ---- */
|
||||||
|
.info-toggle { display:flex; align-items:center; justify-content:center; gap:8px; margin:18px auto 0; max-width:620px;
|
||||||
|
font-size:13px; color:var(--muted,#7f8b9a); cursor:pointer; }
|
||||||
|
.info-toggle input { width:15px; height:15px; cursor:pointer; }
|
||||||
|
[data-embed] .info-toggle { display:none !important; }
|
||||||
|
#techinfo[hidden] { display:none; }
|
||||||
|
|
|
||||||
11
src/progbox.html
Normal file
11
src/progbox.html
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!-- Per-device program I/O — assembled into each form-factor page by build.sh.
|
||||||
|
Hidden in embed mode (the landing supplies its own box). The host page defines
|
||||||
|
window.currentProgramString() and window.loadProgramString(plain); progbox.js wires this. -->
|
||||||
|
<div class="progbox" title="The program for what's loaded — paste a patch or a base64 set-list code, then Load">
|
||||||
|
<label for="dProg">program</label>
|
||||||
|
<input id="dProg" spellcheck="false" autocomplete="off" autocapitalize="off"
|
||||||
|
placeholder="v1;t120;kick:4;snare:4=.X.X;hat:4/2 — or a base64 set-list code">
|
||||||
|
<span id="dProgMsg" class="progbox-msg"></span>
|
||||||
|
<button id="dProgLoad" class="primary">Load ▸</button>
|
||||||
|
<button id="dProgCopy">Copy</button>
|
||||||
|
</div>
|
||||||
60
src/progbox.js
Normal file
60
src/progbox.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
/* Per-device program I/O + "Show info" toggle — assembled into each form-factor page.
|
||||||
|
Uses the engine codec (patchToSetup / setupToPatch / codeToSetlist) for decode + lint.
|
||||||
|
Host page provides: window.currentProgramString() and window.loadProgramString(plain).
|
||||||
|
Exposes window.progRefresh() — call it after the device's program changes. When the
|
||||||
|
page is embedded it posts {type:'varasys-prog'} to the parent instead of touching the box. */
|
||||||
|
(function () {
|
||||||
|
var box = document.getElementById("dProg");
|
||||||
|
var msg = document.getElementById("dProgMsg");
|
||||||
|
var embedded = document.documentElement.dataset.embed === "1";
|
||||||
|
function setMsg(t, ok) { if (!msg) return; msg.textContent = t || ""; msg.classList.toggle("ok", !!ok && !!t); msg.classList.toggle("bad", !ok && !!t); }
|
||||||
|
function lint(text) {
|
||||||
|
text = (text || "").trim(); if (!text) return { ok: false, msg: "empty" };
|
||||||
|
var m = text.match(/[#?&](p|sl)=([^&\s]+)/), kind = null, payload = text;
|
||||||
|
if (m) { kind = m[1]; try { payload = decodeURIComponent(m[2]); } catch (e) { payload = m[2]; } }
|
||||||
|
var b64 = /^[A-Za-z0-9_-]{12,}$/.test(payload) && !/[;:]/.test(payload);
|
||||||
|
try {
|
||||||
|
if (kind === "sl" || (kind !== "p" && b64)) {
|
||||||
|
var sl = codeToSetlist(payload); if (!sl.items || !sl.items.length) throw new Error("set-list code has no items");
|
||||||
|
return { ok: true, plain: setupToPatch(sl.items[0]), msg: "decoded set list “" + sl.title + "” — item 1" };
|
||||||
|
}
|
||||||
|
var s = patchToSetup(payload); if (!s.lanes.length) throw new Error("no lanes — try e.g. kick:4");
|
||||||
|
return { ok: true, plain: setupToPatch(s), msg: s.lanes.length + " lane" + (s.lanes.length > 1 ? "s" : "") + " · " + s.bpm + " BPM" };
|
||||||
|
} catch (e) { return { ok: false, msg: "✗ " + e.message }; }
|
||||||
|
}
|
||||||
|
var editing = false;
|
||||||
|
// report the current program: to the parent when embedded, else into the box.
|
||||||
|
// No-op on pages without a program hook (e.g. panel-based Teacher/Player).
|
||||||
|
window.progRefresh = function () {
|
||||||
|
if (!window.currentProgramString) return;
|
||||||
|
var p = ""; try { p = window.currentProgramString(); } catch (e) {}
|
||||||
|
if (embedded) { try { parent.postMessage({ type: "varasys-prog", patch: p }, "*"); } catch (e) {} return; }
|
||||||
|
if (box && !editing) { box.value = p; setMsg("", true); }
|
||||||
|
};
|
||||||
|
if (box) {
|
||||||
|
box.addEventListener("focus", function () { editing = true; });
|
||||||
|
box.addEventListener("blur", function () { editing = false; });
|
||||||
|
box.addEventListener("input", function () { box.classList.remove("err"); });
|
||||||
|
box.addEventListener("keydown", function (e) { if (e.key === "Enter") { e.preventDefault(); doLoad(); } });
|
||||||
|
var loadBtn = document.getElementById("dProgLoad"); if (loadBtn) loadBtn.addEventListener("click", doLoad);
|
||||||
|
var copyBtn = document.getElementById("dProgCopy"); if (copyBtn) copyBtn.addEventListener("click", function () {
|
||||||
|
try { navigator.clipboard.writeText(box.value); copyBtn.textContent = "Copied!"; setTimeout(function () { copyBtn.textContent = "Copy"; }, 1200); } catch (e) { box.select(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function doLoad() {
|
||||||
|
var r = lint(box.value);
|
||||||
|
if (!r.ok) { box.classList.add("err"); setMsg(r.msg, false); return; }
|
||||||
|
box.classList.remove("err"); box.value = r.plain; setMsg("✓ " + r.msg, true);
|
||||||
|
try { if (window.loadProgramString) window.loadProgramString(r.plain); } catch (e) { setMsg("✗ " + e.message, false); }
|
||||||
|
}
|
||||||
|
// "Show info" toggle — reveals the technical section; ?info=1 opens it checked
|
||||||
|
var tog = document.getElementById("infoToggle"), tech = document.getElementById("techinfo");
|
||||||
|
if (tog) {
|
||||||
|
var extra = document.querySelectorAll(".tech"); // e.g. dimensioned views outside #techinfo
|
||||||
|
var apply = function (on) { if (tech) tech.hidden = !on; for (var i = 0; i < extra.length; i++) extra[i].hidden = !on; };
|
||||||
|
var open = /[?&]info=1/.test(location.search);
|
||||||
|
tog.checked = open; apply(open);
|
||||||
|
tog.addEventListener("change", function () { apply(tog.checked); });
|
||||||
|
}
|
||||||
|
window.progRefresh();
|
||||||
|
})();
|
||||||
19
stage.html
19
stage.html
|
|
@ -111,6 +111,9 @@
|
||||||
|
|
||||||
/*@BUILD:include:src/header.html@*/
|
/*@BUILD:include:src/header.html@*/
|
||||||
|
|
||||||
|
<h1 class="ff-title">PM‑1 Stage</h1>
|
||||||
|
<p class="ff-sum">Foot‑pedal stompbox for the stage — hands‑free with two footswitches and an expression pedal, a big floor‑readable RGB beat light, instrument pass‑through with the click mixed in.</p>
|
||||||
|
|
||||||
<div class="device">
|
<div class="device">
|
||||||
<!-- top edge: all jacks, including dual USB-C daisy-chain power -->
|
<!-- top edge: all jacks, including dual USB-C daisy-chain power -->
|
||||||
<div class="edge">
|
<div class="edge">
|
||||||
|
|
@ -154,6 +157,8 @@
|
||||||
<div class="hint">Stomp <b>Tap</b> to set tempo (hold = start/stop) · <b>Next</b> to change item (hold = previous) ·
|
<div class="hint">Stomp <b>Tap</b> to set tempo (hold = start/stop) · <b>Next</b> to change item (hold = previous) ·
|
||||||
an expression pedal sweeps tempo. Instrument passes through with the click mixed in (analog).</div>
|
an expression pedal sweeps tempo. Instrument passes through with the click mixed in (analog).</div>
|
||||||
|
|
||||||
|
/*@BUILD:include:src/progbox.html@*/
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_VERSION = "v0.0.1-dev";
|
const APP_VERSION = "v0.0.1-dev";
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
@ -165,7 +170,7 @@ const SAMPLES = {};
|
||||||
const state = { bpm:120, volume:0.85, running:false };
|
const state = { bpm:120, volume:0.85, running:false };
|
||||||
let meters = [], muteWindows = [];
|
let meters = [], muteWindows = [];
|
||||||
|
|
||||||
function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); }
|
function setBpm(v){ state.bpm = Math.max(30, Math.min(300, Math.round(v))); if(window.progRefresh) progRefresh(); }
|
||||||
function scheduler(){
|
function scheduler(){
|
||||||
const ahead = audioCtx.currentTime + SCHEDULE_AHEAD;
|
const ahead = audioCtx.currentTime + SCHEDULE_AHEAD;
|
||||||
for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } }
|
for(const m of meters){ while(m.nextTime < ahead){ scheduleMeterTick(m, m.nextTime); m.nextTime += laneStepDur(m, m.tick); m.tick++; } }
|
||||||
|
|
@ -296,9 +301,15 @@ addEventListener("keydown",(e)=>{
|
||||||
{ const ht=tracksFromHash(); if(ht) tracks=ht; }
|
{ const ht=tracksFromHash(); if(ht) tracks=ht; }
|
||||||
loadTrack(0);
|
loadTrack(0);
|
||||||
requestAnimationFrame(draw);
|
requestAnimationFrame(draw);
|
||||||
|
window.currentProgramString = function(){ var t=tracks[trackIdx]||{}; return setupToPatch({bpm:state.bpm, volume:state.volume, lanes:t.lanes||[]}); };
|
||||||
|
window.loadProgramString = function(plain){ var s=patchToSetup(plain); tracks=[{name:"Program", ...s}]; trackIdx=0; loadTrack(0); };
|
||||||
|
/*@BUILD:include:src/progbox.js@*/
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="about pageonly">
|
<label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info (dimensions, BOM, embedding)</label>
|
||||||
|
<div id="techinfo" class="pageonly" hidden>
|
||||||
|
|
||||||
|
<section class="about">
|
||||||
<h2>PM‑1 — Stage</h2>
|
<h2>PM‑1 — Stage</h2>
|
||||||
<div class="ff-tags"><span class="hw">Hardware</span><span>Foot‑pedal stompbox</span><span>~$52 one‑off</span></div>
|
<div class="ff-tags"><span class="hw">Hardware</span><span>Foot‑pedal stompbox</span><span>~$52 one‑off</span></div>
|
||||||
<p>A foot‑operated polymeter stompbox for the stage: drive it hands‑free with two heavy footswitches and an
|
<p>A foot‑operated polymeter stompbox for the stage: drive it hands‑free with two heavy footswitches and an
|
||||||
|
|
@ -346,6 +357,10 @@ requestAnimationFrame(draw);
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<p class="sub" style="max-width:760px;margin:14px auto 0">Embed this widget elsewhere with one <code><div></code> + a script —
|
||||||
|
see <a href="/embed.html">the embed docs</a>.</p>
|
||||||
|
</div><!-- /#techinfo -->
|
||||||
|
|
||||||
/*@BUILD:include:src/footer.html@*/
|
/*@BUILD:include:src/footer.html@*/
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
18
teacher.html
18
teacher.html
|
|
@ -204,11 +204,14 @@
|
||||||
|
|
||||||
/*@BUILD:include:src/header.html@*/
|
/*@BUILD:include:src/header.html@*/
|
||||||
|
|
||||||
|
<h1 class="ff-title">PM‑1 Teacher</h1>
|
||||||
|
<p class="ff-sum">Full‑feature studio / lesson desk console — a colour TFT showing every lane, arcade buttons and a thumb‑roller, with your instrument running through and the click mixed in.</p>
|
||||||
|
|
||||||
<div class="cols">
|
<div class="cols">
|
||||||
<div class="col-left">
|
<div class="col-left">
|
||||||
|
|
||||||
<!-- ===================== TOP EDGE (connectors) ===================== -->
|
<!-- ===================== TOP EDGE (connectors) — technical view, hidden until "Show info" ===================== -->
|
||||||
<div class="topview">
|
<div class="topview tech" hidden>
|
||||||
<div class="tv-cap">Top edge — all connectors (cables exit upward; pedalboard-friendly)</div>
|
<div class="tv-cap">Top edge — all connectors (cables exit upward; pedalboard-friendly)</div>
|
||||||
<div class="dim-row">
|
<div class="dim-row">
|
||||||
<div class="dim-y">↕ 1.8 in (45 mm)</div>
|
<div class="dim-y">↕ 1.8 in (45 mm)</div>
|
||||||
|
|
@ -224,7 +227,7 @@
|
||||||
|
|
||||||
<!-- ===================== THE DEVICE (front view) ===================== -->
|
<!-- ===================== THE DEVICE (front view) ===================== -->
|
||||||
<div class="dim-row">
|
<div class="dim-row">
|
||||||
<div class="dim-y">↕ 5.5 in (140 mm)</div>
|
<div class="dim-y tech" hidden>↕ 5.5 in (140 mm)</div>
|
||||||
<div class="device">
|
<div class="device">
|
||||||
|
|
||||||
<div class="brandrow">
|
<div class="brandrow">
|
||||||
|
|
@ -254,7 +257,7 @@
|
||||||
<div class="grille"></div>
|
<div class="grille"></div>
|
||||||
</div><!-- /device -->
|
</div><!-- /device -->
|
||||||
</div><!-- /dim-row -->
|
</div><!-- /dim-row -->
|
||||||
<div class="dim-x">↔ 4.7 in (120 mm) wide</div>
|
<div class="dim-x tech" hidden>↔ 4.7 in (120 mm) wide</div>
|
||||||
|
|
||||||
<!-- ===================== LOAD CONFIG ===================== -->
|
<!-- ===================== LOAD CONFIG ===================== -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
|
@ -275,7 +278,10 @@
|
||||||
</div><!-- /col-left -->
|
</div><!-- /col-left -->
|
||||||
</div><!-- /cols -->
|
</div><!-- /cols -->
|
||||||
|
|
||||||
<section class="about pageonly">
|
<label class="info-toggle pageonly"><input type="checkbox" id="infoToggle"> Show technical info (front/top views, dimensions, BOM, embedding)</label>
|
||||||
|
<div id="techinfo" class="pageonly" hidden>
|
||||||
|
|
||||||
|
<section class="about">
|
||||||
<h2>PM‑1 — Teacher</h2>
|
<h2>PM‑1 — Teacher</h2>
|
||||||
<div class="ff-tags"><span class="hw">Hardware</span><span>Studio / lesson console</span><span>~$59 one‑off</span></div>
|
<div class="ff-tags"><span class="hw">Hardware</span><span>Studio / lesson console</span><span>~$59 one‑off</span></div>
|
||||||
<p>The full‑feature desktop console: a colour readout of every lane, fast set‑list navigation, and your
|
<p>The full‑feature desktop console: a colour readout of every lane, fast set‑list navigation, and your
|
||||||
|
|
@ -324,6 +330,7 @@
|
||||||
monitor amp — so your instrument is never re‑digitised (no added latency).</p>
|
monitor amp — so your instrument is never re‑digitised (no added latency).</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
</div><!-- /#techinfo -->
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const APP_VERSION = "v0.0.1-dev";
|
const APP_VERSION = "v0.0.1-dev";
|
||||||
|
|
@ -553,6 +560,7 @@ if(location.hash && /(p|sl)=/.test(location.hash)) loadConfig(location.hash, tru
|
||||||
if(!setlist){ setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); }
|
if(!setlist){ setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); }
|
||||||
renderAll();
|
renderAll();
|
||||||
requestAnimationFrame(draw);
|
requestAnimationFrame(draw);
|
||||||
|
/*@BUILD:include:src/progbox.js@*/
|
||||||
</script>
|
</script>
|
||||||
/*@BUILD:include:src/footer.html@*/
|
/*@BUILD:include:src/footer.html@*/
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue