Embed version dropdown; Display weight static/behind lights; Practice beat flash; move Philosophy up

- Embed page: a form-factor dropdown that rewrites every snippet (drop-in +
  plain iframe), the live demo, and the name for the chosen version; variant
  table completed to all six.
- Display (Showcase): the tempo weight no longer flashes and is drawn BEHIND the
  pendulum lights so it never hides a beat flash.
- Practice (Micro): a beat/sub-beat flash — the whole 14-seg display washes amber
  on each step (subtle on sub-beats, brighter on the beat, full on the "1"),
  latency-compensated like the other devices.
- Landing: Philosophy section moved above "Pick a form factor".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-28 11:55:12 -05:00
parent 36cff77219
commit 1faf9cad41
4 changed files with 106 additions and 41 deletions

View file

@ -33,6 +33,10 @@
th{ color:var(--muted); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.04em; }
td.k{ white-space:nowrap; color:var(--cyan); font-family:"Courier New",monospace; }
.demo{ background:var(--panel-bg); border:1px solid var(--panel-bd); border-radius:14px; padding:14px; margin-top:8px; }
.pick{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-top:14px; }
.pick label{ font-size:13px; color:var(--txt); }
.pick select{ background:var(--field-bg); color:var(--txt); border:1px solid var(--panel-bd); border-radius:8px; padding:7px 10px; font-size:13px; }
.ff-name{ color:var(--cyan); font-weight:600; font-size:13px; }
.site-foot{ max-width:760px; margin:40px auto 0; font-size:12px; color:var(--muted); }
</style>
</head>
@ -46,27 +50,35 @@
a placeholder + one script tag — no build step, no dependencies. It loads in an iframe and is preloaded
with whatever <b>program / settings string</b> you give it.</p>
<p class="pick"><label for="ffSel">Show snippets for:</label>
<select id="ffSel">
<option value="editor">PM_E1 Editor</option>
<option value="teacher">PM_T1 Teacher</option>
<option value="stage">PM_S1 Stage</option>
<option value="micro" selected>PM_P1 Practice</option>
<option value="showcase">PM_D1 Display</option>
<option value="initial">PM_C1 Concept</option>
</select>
<span class="ff-name"></span></p>
<h2>Drop-in (recommended)</h2>
<pre>&lt;div data-varasys-metronome="micro"
data-patch="v1;t120;kick:4;snare:4=.X.X;hatClosed:4/2"&gt;&lt;/div&gt;
&lt;script src="https://metronome.varasys.io/embed.js"&gt;&lt;/script&gt;</pre>
<p>The script replaces the <code>&lt;div&gt;</code> with an auto-sizing iframe. Here it is, live on this page:</p>
<div class="demo">
<div data-varasys-metronome="micro" data-patch="v1;t120;kick:4;snare:4=.X.X;hatClosed:4/2"></div>
</div>
<pre id="snipDrop"></pre>
<p>The script replaces the <code>&lt;div&gt;</code> with an auto-sizing iframe. Here's the <span class="ff-name"></span>, live on this page:</p>
<div class="demo"><iframe id="demoFrame" title="live embed preview" allow="autoplay" style="border:0;display:block;width:100%;height:300px"></iframe></div>
<h2>Or a plain iframe</h2>
<pre>&lt;iframe src="https://metronome.varasys.io/micro.html?embed=1#p=v1;t120;kick:4"
width="360" height="300" style="border:0"&gt;&lt;/iframe&gt;</pre>
<pre id="snipIframe"></pre>
<h2>Form factors — <code>data-varasys-metronome</code></h2>
<table>
<thead><tr><th>value</th><th>widget</th></tr></thead>
<tbody>
<tr><td class="k">editor</td><td>PM_E1 PolyMeter Editor (full app)</td></tr>
<tr><td class="k">initial</td><td>PM_C1 Concept (idealized device)</td></tr>
<tr><td class="k">editor</td><td>PM_E1 PolyMeter Editor (full web app)</td></tr>
<tr><td class="k">teacher</td><td>PM_T1 Teacher (studio / lesson console)</td></tr>
<tr><td class="k">stage</td><td>PM_S1 Stage (footpedal stompbox)</td></tr>
<tr><td class="k">micro</td><td>PM_P1 Practice (inline practice bar)</td></tr>
<tr><td class="k">showcase</td><td>PM_D1 Display (RGB pendulum showpiece)</td></tr>
<tr><td class="k">initial</td><td>PM_C1 Concept (idealized render)</td></tr>
</tbody>
</table>
@ -86,9 +98,37 @@
<script>
const APP_VERSION = "v0.0.1-dev";
const $ = (id)=>document.getElementById(id);
/* Form-factor picker: updates every snippet + the live demo for the chosen version. */
const ORIGIN = "https://metronome.varasys.io";
const DEMO_PATCH = "v1;t120;kick:4;snare:4=.X.X;hatClosed:4/2";
const FF = [
{ k:"editor", name:"PM_E1 Editor", file:"editor.html", h:560 },
{ k:"teacher", name:"PM_T1 Teacher", file:"teacher.html", h:440 },
{ k:"stage", name:"PM_S1 Stage", file:"stage.html", h:430 },
{ k:"micro", name:"PM_P1 Practice", file:"micro.html", h:240 },
{ k:"showcase", name:"PM_D1 Display", file:"showcase.html",h:540 },
{ k:"initial", name:"PM_C1 Concept", file:"player.html", h:440 },
];
function updateFF(k){
const v = FF.find(x => x.k === k) || FF[0];
$("snipDrop").textContent =
'<div data-varasys-metronome="' + v.k + '"\n data-patch="' + DEMO_PATCH + '"></div>\n' +
'<script src="' + ORIGIN + '/embed.js"><\/script>';
$("snipIframe").textContent =
'<iframe src="' + ORIGIN + '/' + v.file + '?embed=1#p=' + DEMO_PATCH + '"\n' +
' width="360" height="' + v.h + '" style="border:0"><\/iframe>';
const f = $("demoFrame"); f.style.height = v.h + "px"; f.src = "/" + v.file + "?embed=1#p=" + encodeURIComponent(DEMO_PATCH);
document.querySelectorAll(".ff-name").forEach(el => el.textContent = v.name);
}
$("ffSel").addEventListener("change", (e) => updateFF(e.target.value));
addEventListener("message", (e) => {
if (e.data && e.data.type === "varasys-h" && typeof e.data.h === "number" && e.source === $("demoFrame").contentWindow)
$("demoFrame").style.height = e.data.h + "px";
});
updateFF($("ffSel").value || "micro");
/*@BUILD:include:src/chrome.js@*/
</script>
<script src="/embed.js"></script>
/*@BUILD:include:src/footer.html@*/
</body>
</html>

View file

@ -89,6 +89,22 @@
below — or pick any form factor to load and play the same groove on it.</p>
</section>
<section class="philosophy">
<div class="section-label">Philosophy</div>
<div class="phil-grid">
<div class="phil">
<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
<b>program string</b> loads into whichever form factor fits the moment. One engine, one language.</p>
</div>
<div class="phil">
<h3>🔌 USBC power everywhere — no batteries</h3>
<p>Every device runs over a single <b>USBC</b> port (the larger ones add a passthrough to daisychain).
No internal battery to wear out; bring a power bank. One connector keeps it all <b>futureproof</b>.</p>
</div>
</div>
</section>
<div class="section-label">Pick a form factor — it loads live below</div>
<div class="panes" id="panes"></div>
@ -107,22 +123,6 @@
<div class="prog-hint">The current program, decoded (not base64). Paste a patch <i>or</i> a base64 setlist code; it's checked, then loaded.
Conventions: GM names or numbers (<code>kick</code> / <code>36</code>), <code>=X.x-</code> steps, <code>/2</code> subdivision, <code>(3,8)</code> euclidean, <code>@-3</code> dB, <code>~</code> polymeter.</div>
</div>
<section class="philosophy">
<div class="section-label">Philosophy</div>
<div class="phil-grid">
<div class="phil">
<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
<b>program string</b> loads into whichever form factor fits the moment. One engine, one language.</p>
</div>
<div class="phil">
<h3>🔌 USBC power everywhere — no batteries</h3>
<p>Every device runs over a single <b>USBC</b> port (the larger ones add a passthrough to daisychain).
No internal battery to wear out; bring a power bank. One connector keeps it all <b>futureproof</b>.</p>
</div>
</div>
</section>
</main>
/*@BUILD:include:src/footer.html@*/

View file

@ -201,7 +201,7 @@ function startAudio(){
ensureAudio(); audioCtx.resume(); state.running=true;
const t0=audioCtx.currentTime+0.08;
for(const m of meters){ m.tick=0; m.nextTime=t0; m.vq=[]; m.vqPtr=0; }
muteWindows=[]; schedulerTimer=setInterval(scheduler,LOOKAHEAD_MS); scheduler(); render();
muteWindows=[]; beatFlash=0; schedulerTimer=setInterval(scheduler,LOOKAHEAD_MS); scheduler(); render();
}
function stopAudio(){ state.running=false; clearInterval(schedulerTimer); schedulerTimer=null; render(); }
function toggle(){ state.running ? stopAudio() : startAudio(); }
@ -262,11 +262,36 @@ function drawChar(dx,dy,w,h,ch){
diag(11, dx+t+1, dy+h-t-1, cx-t*0.6, midY+t*0.6); // K bottom-left
diag(13, dx+w-t-1, dy+h-t-1, cx+t*0.6, midY+t*0.6); // M bottom-right
}
function drawLED(){
function drawLED(flash, level){
flash = flash || 0; level = level || 0;
lc.fillStyle=LED_BG; lc.fillRect(0,0,LW,LH);
// whole-display beat flash: a wash over the window — subtle on sub-beats, bright on the "1"
if(flash>0){ const a = flash * (level>=3 ? 0.55 : level>=2 ? 0.30 : 0.13);
lc.fillStyle = (level>=3 ? "rgba(255,184,74," : "rgba(255,138,30,") + a.toFixed(3) + ")"; lc.fillRect(0,0,LW,LH); }
const txt=ledText(), pad=10, gap=9, n=NCH, dw=(LW-2*pad-(n-1)*gap)/n, dh=LH-2*pad;
for(let i=0;i<n;i++) drawChar(pad+i*(dw+gap), pad, dw, dh, txt[i]||" ");
}
// beat / sub-beat flash, driven off the master lane (latency-compensated, like the other devices)
let beatFlash=0, beatLevel=0;
function audioLatency(){ return audioCtx ? (audioCtx.outputLatency || audioCtx.baseLatency || 0) : 0; }
function frame(){
if(audioCtx && state.running){
const now = audioCtx.currentTime - audioLatency();
for(const m of meters){
while(m.vqPtr<m.vq.length && m.vq[m.vqPtr].time<=now){
const e=m.vq[m.vqPtr]; m.currentStep=e.step;
if(m===meters[0]){ const spb=m.stepsPerBeat;
beatLevel = (e.step===0) ? 3 : (e.step % spb === 0) ? 2 : 1; // downbeat ("1") · beat · sub-beat
if((m.beatsOn[e.step]|0)!==0 || e.step % spb === 0) beatFlash=1; }
m.vqPtr++;
}
if(m.vqPtr>512){ m.vq=m.vq.slice(m.vqPtr); m.vqPtr=0; }
}
beatFlash = Math.max(0, beatFlash - 0.10);
drawLED(beatFlash, beatLevel);
}
requestAnimationFrame(frame);
}
function render(){
drawLED();
$("indBpm").classList.toggle("on", displayMode==="bpm");
@ -315,6 +340,7 @@ addEventListener("keydown",(e)=>{
{ const ht=tracksFromHash(); if(ht) tracks=ht; } // a #p=/#sl= link (or embed config) overrides the built-ins
loadTrack(0);
render();
requestAnimationFrame(frame);
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@*/

View file

@ -240,7 +240,16 @@ function drawPendulum(){
[40,60,80,100,120,160,200,240].forEach(function(b){ const y=-bpmToFrac(b)*ROD;
g.strokeStyle="rgba(180,190,205,.5)"; g.lineWidth=1; g.beginPath(); g.moveTo(4,y); g.lineTo(10,y); g.stroke();
g.fillStyle="rgba(180,190,205,.6)"; g.fillText(String(b), 2, y+3); });
// combined-meter LEDs: each lane is a moving point of light along the bar (its current step position)
// fixed bob (drives the swing) near the bottom of the rod
g.fillStyle="#2a2f37"; roundRectP(-9,-58,18,30,4); g.fill();
g.fillStyle="rgba(255,255,255,.06)"; roundRectP(-9,-58,18,5,2); g.fill();
// sliding WEIGHT = tempo — STATIC (no flash) and drawn BEHIND the lights so it never hides a beat flash
const wy=-bpmToFrac(state.bpm)*ROD;
g.fillStyle="#3a4049"; roundRectP(-15,wy-11,30,22,4); g.fill();
g.fillStyle="rgba(150,160,176,.5)"; roundRectP(-13,wy-3.5,26,7,2); g.fill(); // index mark (static)
g.fillStyle="rgba(255,255,255,.10)"; roundRectP(-15,wy-11,30,5,2); g.fill(); // top sheen
// combined-meter LEDs: each lane is a moving point of light along the bar (its current step position).
// Drawn LAST so the flashes sit on top of the weight.
for(const m of meters){ if(m.currentStep<0 || !state.running) continue;
const steps=m.beatsPerBar*m.stepsPerBeat, fr=steps?((m.currentStep%steps)/steps):0;
const y=-(0.16 + fr*(0.96-0.16))*ROD, lvl=m.beatsOn[m.currentStep]|0; if(lvl===0) continue;
@ -248,16 +257,6 @@ function drawPendulum(){
g.shadowColor=rgb; g.shadowBlur=14; g.fillStyle=rgb;
g.beginPath(); g.arc(0,y, lvl>=2?6:4.5, 0,7); g.fill(); g.shadowBlur=0;
}
// fixed bob (drives the swing) near the bottom of the rod
g.fillStyle="#2a2f37"; roundRectP(-9,-58,18,30,4); g.fill();
g.fillStyle="rgba(255,255,255,.06)"; roundRectP(-9,-58,18,5,2); g.fill();
// sliding WEIGHT = tempo; glows on the beat
const wy=-bpmToFrac(state.bpm)*ROD, lit=Math.max(0,flash);
const wc = flashAccent ? "rgb(255,155,46)" : "rgb(51,208,255)";
g.shadowColor=wc; g.shadowBlur=8+22*lit;
g.fillStyle="#3a4049"; roundRectP(-15,wy-11,30,22,4); g.fill(); g.shadowBlur=0;
g.fillStyle=wc; g.globalAlpha=.30+0.7*lit; roundRectP(-13,wy-3.5,26,7,2); g.fill(); g.globalAlpha=1;
g.fillStyle="rgba(255,255,255,.10)"; roundRectP(-15,wy-11,30,5,2); g.fill();
// pivot hub
g.restore();
g.beginPath(); g.arc(PIVX,PIVY,6,0,7); g.fillStyle="#2a2f37"; g.fill();