From b98c37ff6827b4928535af8768420e8a550166f1 Mon Sep 17 00:00:00 2001 From: Me Here Date: Sun, 7 Jun 2026 12:18:48 -0500 Subject: [PATCH] pm-mobile: beat-aligned pads + per-lane subdivision rhythm icons - Pads now group into per-beat cells (one flex cell per beat), so beats line up in columns across lanes regardless of each lane's subdivision; the downbeat pad in each cell is full-height and the sub-beat pads are shorter/smaller. - Each lane label shows a small engraved rhythm figure for its subdivision, drawn as SVG (notehead + stem + beams + tuplet number): quarter, beamed eighths, triplet (3), sixteenths (double beam), sextuplet (6), etc. Engine untouched; conformance passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile.html | 48 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/mobile.html b/mobile.html index 01e9b15..2ed8b28 100644 --- a/mobile.html +++ b/mobile.html @@ -104,12 +104,18 @@ #trackpanel .tp-msg{ color:#5fd08a; margin-left:auto; } .lane{ display:flex; align-items:center; gap:8px; } .lane.off{ opacity:.5; } - .lmeta{ flex:0 0 auto; width:30%; max-width:130px; min-width:64px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; text-align:left; - background:var(--chip-bg); border:1px solid var(--chip-bd); color:var(--txt); border-radius:8px; padding:6px 8px; + .lmeta{ flex:0 0 auto; width:34%; max-width:150px; min-width:92px; display:flex; align-items:center; gap:5px; text-align:left; + background:var(--chip-bg); border:1px solid var(--chip-bd); color:var(--txt); border-radius:8px; padding:5px 8px; font-size:clamp(10px,1.8vmin,13px); font-family:"Courier New",monospace; cursor:pointer; } - .lmeta .lg{ color:var(--muted); } - .pads{ flex:1 1 auto; display:flex; gap:3px; overflow-x:auto; padding-bottom:2px; min-width:0; } - .pad{ flex:1 0 14px; min-width:14px; height:clamp(20px,3.6vmin,28px); border-radius:5px; border:1px solid var(--chip-bd); background:var(--led-off); cursor:pointer; padding:0; } + .lmeta .ln-name{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + .lmeta .lg{ flex:0 0 auto; color:var(--muted); } + .lmeta .rhythm{ flex:0 0 auto; color:var(--txt); display:block; } + /* beats line up in columns across lanes; sub-beats sit inside the beat cell, smaller */ + .pads{ flex:1 1 auto; display:flex; gap:7px; overflow-x:auto; padding-bottom:2px; min-width:0; align-items:center; } + .beatcell{ flex:1 1 0; min-width:0; display:flex; gap:2px; align-items:center; } + .pad{ flex:1 1 0; min-width:5px; border:1px solid var(--chip-bd); background:var(--led-off); cursor:pointer; padding:0; } + .pad.beat{ height:clamp(20px,3.8vmin,28px); border-radius:5px; } + .pad.sub{ height:clamp(11px,2.3vmin,16px); border-radius:3px; } .pad.gs{ border-color:var(--amber); } .pad.on{ background:var(--cyan); } .pad.acc{ background:var(--amber); } @@ -443,25 +449,43 @@ function nudge(d){ ramp.on=false; setBpm(state.bpm+d); renderAll(); } let laneSig=null, editLaneIdx=0; function laneSignature(){ return meters.map(m=>m.sound+":"+m.groupsStr+"/"+m.stepsPerBeat+(m.swing?"s":"")+(m.enabled?"":"!")+(m.poly?"~":"")).join("|"); } function lvlClass(l){ return l===2?"acc":l===3?"ghost":l===1?"on":""; } +function padClass(m,k){ const spb=m.stepsPerBeat, isBeat=(k%spb===0), gs=isBeat&&m.groupStarts.has(k/spb), lvl=m.beatsOn[k]|0; + return "pad "+(isBeat?"beat":"sub")+(gs?" gs":"")+(lvl?(" "+lvlClass(lvl)):""); } +// Small engraved rhythm figure for a beat's subdivision (1=quarter, 2=8ths, 3=triplet, +// 4=16ths, 5/6/7=tuplets). Drawn as SVG so triplets/sextuplets render crisply. +function rhythmSVG(spb){ + spb=Math.max(1,spb|0); + const n=spb, beams = n===1?0 : (n<=3?1:2), tup = (n===3||n>=5)?n:null; + const LEFT=3, GAP = n<=2?8 : n<=4?6.5 : 5.5, baseY=15, topY=6; + const W=Math.round(LEFT*2 + (n-1)*GAP + 4); let g="", first=0, last=0; + for(let i=0;i'; + g+=''; } + if(beams>=1) g+=''; + if(beams>=2) g+=''; + if(tup) g+=''+tup+''; + return ''; +} function buildLanes(){ const box=$("lanes"); box.innerHTML=""; meters.forEach((m,i)=>{ const lane=document.createElement("div"); lane.className="lane"+(m.enabled?"":" off"); const meta=document.createElement("button"); meta.className="lmeta"; - meta.innerHTML=esc(m.sound)+" "+esc(m.groupsStr)+(m.stepsPerBeat>1?"/"+m.stepsPerBeat:"")+(m.poly?"~":"")+""; + meta.innerHTML=""+esc(m.sound)+""+rhythmSVG(m.stepsPerBeat)+""+esc(m.groupsStr)+(m.swing?" sw":"")+(m.poly?"~":"")+""; meta.onclick=()=>openLaneSheet(i); const pads=document.createElement("div"); pads.className="pads"; - const steps=m.beatsPerBar*m.stepsPerBeat; m._padEls=[]; m._lastPad=-1; - for(let k=0;kcyclePad(m,k,p); pads.appendChild(p); m._padEls.push(p); } + const spb=m.stepsPerBeat; m._padEls=new Array(m.beatsPerBar*spb); m._lastPad=-1; + for(let b=0;bcyclePad(m,k,p); cell.appendChild(p); m._padEls[k]=p; } + pads.appendChild(cell); } lane.appendChild(meta); lane.appendChild(pads); box.appendChild(lane); }); const add=document.createElement("button"); add.className="addlane"; add.textContent="+ Add lane"; add.onclick=addLane; box.appendChild(add); renderPadLevels(); } -function renderPadLevels(){ meters.forEach(m=>{ if(!m._padEls) return; const spb=m.stepsPerBeat; - m._padEls.forEach((p,k)=>{ const gs=(k%spb===0)&&m.groupStarts.has(k/spb); const lvl=m.beatsOn[k]|0; p.className="pad"+(gs?" gs":"")+(lvl?(" "+lvlClass(lvl)):""); }); }); } -function cyclePad(m,k,p){ m.beatsOn[k]=((m.beatsOn[k]|0)+1)%4; if(m.orns) m.orns[k]=0; const spb=m.stepsPerBeat; const gs=(k%spb===0)&&m.groupStarts.has(k/spb); const lvl=m.beatsOn[k]; - p.className="pad"+(gs?" gs":"")+(lvl?(" "+lvlClass(lvl)):""); saveState(); } +function renderPadLevels(){ meters.forEach(m=>{ if(!m._padEls) return; + m._padEls.forEach((p,k)=>{ if(p) p.className=padClass(m,k); }); }); } +function cyclePad(m,k,p){ m.beatsOn[k]=((m.beatsOn[k]|0)+1)%4; if(m.orns) m.orns[k]=0; p.className=padClass(m,k); saveState(); } function renderPadPlayheads(){ meters.forEach(m=>{ if(!m._padEls) return; const cur=state.running?m.currentStep:-1; if(m._lastPad!==cur){ if(m._lastPad>=0&&m._padEls[m._lastPad]) m._padEls[m._lastPad].classList.remove("cur"); if(cur>=0&&m._padEls[cur]) m._padEls[cur].classList.add("cur"); m._lastPad=cur; } }); } function rebuildLane(i,cfg){