pm-mobile: engraved tempo marking (♩ = N) + subtle staff behind the lanes

- The BPM now reads as a sheet-music tempo marking: a quarter-note glyph
  (reusing the lane rhythm-figure SVG, so it matches) + "= N", with a small
  "per minute" beneath. Tap/hold/drag editing unchanged.
- A faint 5-line staff sits behind the lane rows (--staff, theme-aware) for a
  subtle engraved feel; pads/labels render above it.

Engine untouched; conformance passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-06-07 13:14:33 -05:00
parent 1a66eb962d
commit 8402b4f92c

View file

@ -34,14 +34,14 @@
--txt:#e7edf5; --muted:#8b96a5; --link:#6cb6ff;
--panel-bg:#161b22; --panel-bd:#2a313c; --field-bg:#0e1116; --field-bd:#222a36;
--cyan:#0AB3F7; --amber:#ffd166;
--led-off:#1b2330; --ring:#2a3340; --glow:rgba(10,179,247,.55); --aglow:rgba(255,209,102,.55); --poly:#bb8cff;
--led-off:#1b2330; --ring:#2a3340; --glow:rgba(10,179,247,.55); --aglow:rgba(255,209,102,.55); --poly:#bb8cff; --staff:rgba(199,208,219,.17);
--btn1:#2b323d; --btn2:#1b212a; --btn-bd:#39424f; --chip-bg:#1b2230; --chip-bd:#2c3545;
}
:root[data-theme="light"]{
--bg1:#eef3f9; --bg2:#cfd9e6;
--txt:#10202f; --muted:#5c6776; --link:#1769c4;
--panel-bg:#ffffff; --panel-bd:#d2dae4; --field-bg:#f1f4f8; --field-bd:#cdd6e0;
--led-off:#c4cedb; --ring:#c9d4e1; --glow:rgba(10,179,247,.40); --aglow:rgba(230,160,30,.45); --poly:#7a3df0;
--led-off:#c4cedb; --ring:#c9d4e1; --glow:rgba(10,179,247,.40); --aglow:rgba(230,160,30,.45); --poly:#7a3df0; --staff:rgba(28,40,63,.15);
--btn1:#ffffff; --btn2:#e7edf4; --btn-bd:#c8d2de; --chip-bg:#eef2f7; --chip-bd:#d3dbe5;
}
html,body{ height:100%; }
@ -82,8 +82,13 @@
transition:transform .12s ease-out, box-shadow .12s ease-out, border-color .12s ease-out; touch-action:none; cursor:pointer; }
#pulse.hit{ transform:scale(1.045); border-color:var(--cyan); box-shadow:0 0 60px var(--glow); }
#pulse.hit.acc{ border-color:var(--amber); box-shadow:0 0 72px var(--aglow); }
#bpm{ font-size:clamp(46px,15vmin,140px); font-weight:800; line-height:.85; font-variant-numeric:tabular-nums; letter-spacing:-.01em; }
#bpmlab{ font-size:clamp(10px,2vmin,16px); letter-spacing:.3em; color:var(--muted); margin-top:.6em; }
/* tempo as an engraved marking: ♩ = N */
#bpm{ display:inline-flex; align-items:center; justify-content:center; gap:.12em; line-height:.82; }
#bpmNum{ font-size:clamp(44px,14vmin,128px); font-weight:800; font-variant-numeric:tabular-nums; letter-spacing:-.01em; }
.metmark{ display:inline-flex; align-items:center; gap:.12em; color:var(--muted); font-size:clamp(20px,5.6vmin,50px); font-weight:600; }
.metmark .rhythm{ height:1.2em; width:auto; }
.metmark::after{ content:"="; }
#bpmlab{ font-size:clamp(9px,1.8vmin,14px); letter-spacing:.18em; text-transform:uppercase; color:var(--muted); margin-top:.7em; opacity:.8; }
#bpmIn{ display:none; width:64%; text-align:center; font:inherit; font-size:clamp(42px,13vmin,120px); font-weight:800;
background:transparent; color:var(--txt); border:none; border-bottom:2px solid var(--cyan); outline:none; font-variant-numeric:tabular-nums; -moz-appearance:textfield; }
#bpmIn::-webkit-outer-spin-button, #bpmIn::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; }
@ -91,7 +96,10 @@
/* ---- track panel (repeat/end/ramp/gap/string) + editable lanes ---- */
#detail{ flex:0 1 auto; width:100%; max-height:32vh; overflow-y:auto; display:flex; flex-direction:column; gap:8px; padding:2px 0; }
#lanes{ display:flex; flex-direction:column; gap:6px; }
#lanes{ display:flex; flex-direction:column; gap:6px; position:relative; }
#lanes::before{ content:""; position:absolute; left:-2px; right:-2px; top:7%; bottom:7%; pointer-events:none; z-index:0;
background:repeating-linear-gradient(to bottom, var(--staff) 0 1.5px, transparent 1.5px 25%); }
#lanes > *{ position:relative; z-index:1; }
#trackpanel{ width:100%; background:var(--chip-bg); border:1px solid var(--chip-bd); border-radius:10px; padding:8px 11px; display:flex; flex-direction:column; gap:8px; font-size:12px; color:var(--muted); }
#trackpanel .tp-row{ display:flex; align-items:center; gap:8px 18px; flex-wrap:nowrap; }
#trackpanel label{ display:flex; align-items:center; gap:6px; white-space:nowrap; }
@ -257,9 +265,9 @@
<div id="mid">
<div id="stage">
<div id="pulse">
<div id="bpm">120</div>
<div id="bpm"><span id="bpmMark" class="metmark" aria-hidden="true"></span><span id="bpmNum">120</span></div>
<input id="bpmIn" type="number" inputmode="numeric" min="30" max="300" />
<div id="bpmlab">BPM</div>
<div id="bpmlab">per minute</div>
</div>
<div id="meterline"></div>
</div>
@ -743,7 +751,7 @@ $("trkSel").onchange=(e)=>{ gotoItem(+e.target.value, state.running); };
/* ========================= RENDER ============================================ */
let lastCur=null;
function renderInfo(){
if(!editingBpm) $("bpm").textContent=state.bpm;
if(!editingBpm) $("bpmNum").textContent=state.bpm;
const ts=$("trkSel"); if(ts && ts.value!==String(idx)) ts.value=String(idx);
const nm=currentName(); if(nm!==lastCur){ lastCur=nm; lsSet(LS_CURTRACK,nm); }
const sig=laneSignature(); if(sig!==laneSig){ laneSig=sig; buildLanes(); } else renderPadLevels();
@ -787,7 +795,7 @@ function draw(){
/* ========================= BPM: tap=tap-tempo · hold=type · drag=scrub ======== */
let editingBpm=false;
function openBpmEdit(){ editingBpm=true; const i=$("bpmIn"); $("bpm").style.display="none"; i.style.display="block"; i.value=state.bpm; i.focus(); i.select(); }
function closeBpmEdit(commit){ const i=$("bpmIn"); if(commit){ const v=parseInt(i.value,10); if(v){ ramp.on=false; setBpm(v); } } editingBpm=false; i.style.display="none"; $("bpm").style.display="block"; renderAll(); }
function closeBpmEdit(commit){ const i=$("bpmIn"); if(commit){ const v=parseInt(i.value,10); if(v){ ramp.on=false; setBpm(v); } } editingBpm=false; i.style.display="none"; $("bpm").style.display=""; renderAll(); }
$("bpmIn").addEventListener("keydown",(e)=>{ if(e.key==="Enter"){ closeBpmEdit(true); } else if(e.key==="Escape"){ closeBpmEdit(false); } });
$("bpmIn").addEventListener("blur",()=>{ if(editingBpm) closeBpmEdit(true); });
(function(){ const p=$("pulse"); let dragging=false, moved=false, lpFired=false, startY=0, startBpm=120, lpTimer=null;
@ -906,6 +914,7 @@ if(location.hash && /(p|sl)=/.test(location.hash)) loadFromHash(location.hash);
else restoreState();
if(!setlist){ slKey="b0"; setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); }
buildSetlistOptions(); buildTrackOptions();
$("bpmMark").innerHTML=rhythmSVG(1); // ♩ in the "♩ = N" tempo marking
$("vol").value=Math.round(state.volume*100); if(masterGain) masterGain.gain.value=state.volume;
renderAll(); renderSessionBar();
requestAnimationFrame(draw);