diff --git a/mobile.html b/mobile.html index 5253df5..6a2d992 100644 --- a/mobile.html +++ b/mobile.html @@ -75,13 +75,14 @@ /* ---- middle: pulse + track panel + lanes + transport, centered as one block ---- */ #mid{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:safe center; gap:clamp(10px,2.4vmin,22px); } - #stage{ flex:0 0 auto; display:flex; flex-direction:column; align-items:center; gap:clamp(6px,1.6vmin,14px); } - #pulse{ position:relative; width:clamp(186px,46vmin,360px); height:clamp(186px,46vmin,360px); border-radius:50%; + #stage{ flex:0 0 auto; display:flex; flex-direction:column; align-items:center; width:100%; } + /* compact tempo "plate" (was a big circle) — flashes on the beat; tap/hold/drag target */ + #pulse{ position:relative; width:100%; padding:clamp(10px,2.4vmin,20px) 16px; border-radius:16px; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; - border:2px solid var(--ring); background:radial-gradient(circle at 50% 40%, rgba(127,139,154,.07), transparent 70%); - 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); } + border:1px solid var(--ring); background:linear-gradient(180deg, rgba(127,139,154,.06), transparent); + transition:box-shadow .12s ease-out, border-color .12s ease-out, background .12s ease-out; touch-action:none; cursor:pointer; } + #pulse.hit{ border-color:var(--cyan); box-shadow:0 0 22px var(--glow); background:rgba(10,179,247,.07); } + #pulse.hit.acc{ border-color:var(--amber); box-shadow:0 0 26px var(--aglow); background:rgba(255,209,102,.08); } /* 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; } @@ -153,12 +154,13 @@ .chip.feat.r{ border-color:var(--cyan); color:var(--cyan); } .chip.feat.g{ border-color:var(--amber); color:var(--amber); } /* ---- transport ---- */ - #transport{ flex:0 0 auto; display:flex; justify-content:center; padding-top:2px; } - .tgrid{ display:grid; width:100%; gap:clamp(6px,1.6vmin,13px); - grid-template-columns:1fr 1.5fr 1.5fr 1fr; grid-template-areas:"dn10 prev next up10" "dn play prac up"; } + /* transport grows to fill the space freed by the compact tempo plate */ + #transport{ flex:1 1 auto; max-height:clamp(150px,42vh,300px); display:flex; align-items:stretch; justify-content:center; width:100%; padding-top:6px; } + .tgrid{ display:grid; width:100%; height:100%; gap:clamp(7px,1.7vmin,14px); + grid-template-columns:1fr 1.5fr 1.5fr 1fr; grid-template-rows:1fr 1fr; grid-template-areas:"dn10 prev next up10" "dn play prac up"; } .tbtn{ background:linear-gradient(180deg,var(--btn1),var(--btn2)); color:var(--txt); border:1px solid var(--btn-bd); - border-radius:14px; height:clamp(46px,11vmin,72px); font-size:clamp(18px,4.4vmin,28px); cursor:pointer; - box-shadow:0 3px 0 rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.06); display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px; } + border-radius:14px; height:auto; min-height:46px; font-size:clamp(18px,4.4vmin,30px); cursor:pointer; + box-shadow:0 3px 0 rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.06); display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px; } .tbtn small{ font-size:clamp(8px,1.5vmin,11px); letter-spacing:.1em; color:inherit; opacity:.85; } .tbtn:active{ transform:translateY(2px); box-shadow:0 1px 0 rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.06); } #bDn10{grid-area:dn10} #bPrev{grid-area:prev} #bNext{grid-area:next} #bUp10{grid-area:up10} @@ -272,20 +274,17 @@
+
- - - then loop -
-
+
+
Play bars, then
→ + bpm / bars
play / mute bars
-
@@ -324,13 +323,6 @@
- -
-
-

Note value

-
-
-
@@ -539,8 +531,7 @@ function laneMetaHTML(m){ const eff=laneNoteValue(m); const ref=(meters[0]?meters[0].beatsPerBar:m.beatsPerBar); const poly=m.poly?"↻"+m.beatsPerBar+":"+ref+"":""; return ""+esc(m.sound)+""+rhythmSVG(eff)+""+esc(m.groupsStr)+""+poly; } -function setLaneMeta(m){ if(!m._meta) return; m._meta.innerHTML=laneMetaHTML(m); - const rh=m._meta.querySelector(".rh-host"); if(rh) rh.onclick=(e)=>{ e.stopPropagation(); openNotePicker(m._idx); }; } +function setLaneMeta(m){ if(!m._meta) return; m._meta.innerHTML=laneMetaHTML(m); } function buildLanes(){ const box=$("lanes"); box.innerHTML=""; meters.forEach((m,i)=>{ @@ -571,9 +562,6 @@ function renderNoteOpts(box, cur, pick){ if(!box) return; box.innerHTML=""; function setLaneSub(i,n){ const m=meters[i]; if(!m) return; rebuildLane(i,{groupsStr:m.groupsStr,stepsPerBeat:n,swing:m.swing,poly:m.poly,enabled:m.enabled,sound:m.sound,gainDb:m.gainDb,beatsOn:m.beatsOn.slice(),orns:(m.orns||[]).slice()}); laneSig=null; renderAll(); saveState(); } -function openNotePicker(i){ const m=meters[i]; if(!m) return; editLaneIdx=i; - renderNoteOpts($("noteOpts"), m.stepsPerBeat, (n)=>{ setLaneSub(i,n); closeSheets(); }); - $("laneSheet").classList.remove("open"); $("saveSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("noteSheet").classList.add("open"); } function refreshLaneSheetNotes(){ const m=meters[editLaneIdx]; if(!m) return; renderNoteOpts($("lsNotes"), m.stepsPerBeat, (n)=>{ setLaneSub(editLaneIdx,n); refreshLaneSheetNotes(); }); } function renderPadPlayheads(){ meters.forEach(m=>{ if(!m._padEls) return; const cur=state.running?m.currentStep:-1; @@ -595,8 +583,8 @@ function gainLabel(db){ return (db>0?"+":"")+db+" dB"; } function openLaneSheet(i){ editLaneIdx=i; const m=meters[i]; if(!m) return; $("lsSound").value=m.sound; $("lsGroup").value=m.groupsStr; $("lsPoly").checked=!!m.poly; $("lsMute").checked=!m.enabled; $("lsGain").value=m.gainDb||0; $("lsGainVal").textContent=gainLabel(m.gainDb||0); refreshLaneSheetNotes(); - $("saveSheet").classList.remove("open"); $("noteSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); } -function closeSheets(){ ["laneSheet","saveSheet","noteSheet","shareSheet","scrim"].forEach(id=>$(id).classList.remove("open")); } + $("saveSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); } +function closeSheets(){ ["laneSheet","saveSheet","shareSheet","scrim"].forEach(id=>$(id).classList.remove("open")); } const closeLaneSheet=closeSheets; function applyLane(){ const m=meters[editLaneIdx]; if(!m) return; let grp=($("lsGroup").value||"").trim()||"4"; @@ -615,9 +603,9 @@ function clampInt(v,lo,hi,def){ v=parseInt(v,10); if(isNaN(v)) return def; retur function flashIp(msg){ $("ipMsg").textContent=msg; setTimeout(()=>{ if($("ipMsg").textContent===msg) $("ipMsg").textContent=""; },1600); } function buildTrackPanel(){ if(document.activeElement&&document.activeElement.closest&&document.activeElement.closest("#trackpanel")) return; // don't fight the user mid-edit - const hasEnd=segBars>0; - $("ipBars").value=segBars||0; - $("ipThen").style.display=hasEnd?"":"none"; $("ipLoop").style.display=hasEnd?"none":""; + const rep=segBars>0; + $("ipRepeat").checked=rep; $("ipRepeatRow").classList.toggle("off",!rep); + $("ipBars").value=segBars||4; $("ipEnd").value = curEnd==="stop"?"stop":(curEnd===-1?"prev":"next"); $("ipRamp").checked=ramp.on; $("ipGap").checked=trainer.on; $("ipRampStart").value=ramp.startBpm||80; $("ipRampAmt").value=ramp.amount||5; $("ipRampEvery").value=ramp.everyBars||4; @@ -625,15 +613,15 @@ function buildTrackPanel(){ $("ipRampRow").classList.toggle("off",!ramp.on); $("ipGapRow").classList.toggle("off",!trainer.on); } function applyTrackPanel(){ - segBars=Math.max(0,parseInt($("ipBars").value,10)||0); - if(segBars>0){ const e=$("ipEnd").value; curEnd = e==="stop"?"stop":(e==="prev"?-1:1); } else { curEnd=null; } // 0 bars = loop forever - $("ipThen").style.display=segBars>0?"":"none"; $("ipLoop").style.display=segBars>0?"none":""; + if($("ipRepeat").checked){ segBars=Math.max(1,parseInt($("ipBars").value,10)||4); const e=$("ipEnd").value; curEnd = e==="stop"?"stop":(e==="prev"?-1:1); } + else { segBars=0; curEnd=null; } // no Repeat = loop forever + $("ipRepeatRow").classList.toggle("off",!$("ipRepeat").checked); ramp.on=$("ipRamp").checked; ramp.startBpm=clampInt($("ipRampStart").value,30,300,80); ramp.amount=clampInt($("ipRampAmt").value,1,50,5); ramp.everyBars=clampInt($("ipRampEvery").value,1,64,4); trainer.on=$("ipGap").checked; trainer.playBars=clampInt($("ipGapPlay").value,1,32,2); trainer.muteBars=clampInt($("ipGapMute").value,1,32,2); $("ipRampRow").classList.toggle("off",!ramp.on); $("ipGapRow").classList.toggle("off",!trainer.on); saveState(); } -["ipBars","ipEnd","ipRamp","ipGap","ipRampStart","ipRampAmt","ipRampEvery","ipGapPlay","ipGapMute"].forEach(id=>$(id).addEventListener("change",applyTrackPanel)); +["ipRepeat","ipBars","ipEnd","ipRamp","ipGap","ipRampStart","ipRampAmt","ipRampEvery","ipGapPlay","ipGapMute"].forEach(id=>$(id).addEventListener("change",applyTrackPanel)); /* ---- save & library: user set lists/tracks (same store + format as the editor) ---- */ function userSetlists(){ return lsGet(LS_SETLISTS,[]); } @@ -708,7 +696,7 @@ function openSaveSheet(){ $("saveName").value=currentName()||"My track"; buildSaveTo(); const upd=$("saveUpd"); if(slKey[0]==="s"){ upd.style.display=""; upd.textContent='Update “'+(currentName()||"track")+'”'; } else upd.style.display="none"; $("saveMsg").textContent=""; renderLibrary(); - $("laneSheet").classList.remove("open"); $("noteSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("saveSheet").classList.add("open"); } + $("laneSheet").classList.remove("open"); $("shareSheet").classList.remove("open"); $("scrim").classList.add("open"); $("saveSheet").classList.add("open"); } $("saveNew").onclick=doSaveAsNew; $("saveUpd").onclick=doUpdate; $("saveDone").onclick=closeSheets; $("saveBtn").onclick=openSaveSheet; /* ---- share: a track or set list as a link, or paste a string to load ---- */ @@ -721,7 +709,7 @@ function refreshShare(){ $("shareLink").value=shareUrl(); $("shareSeg").querySel function flashShare(m){ $("shareMsg").textContent=m; setTimeout(()=>{ if($("shareMsg").textContent===m) $("shareMsg").textContent=""; },1600); } function copyText(s, ok){ if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(s).then(ok,()=>{}); } else { const t=$("shareLink"); t.value=s; t.select(); try{ document.execCommand("copy"); ok(); }catch(e){} refreshShare(); } } function openShareSheet(){ shareKind="p"; refreshShare(); $("sharePaste").value=""; $("shareMsg").textContent=""; - ["laneSheet","saveSheet","noteSheet"].forEach(id=>$(id).classList.remove("open")); $("scrim").classList.add("open"); $("shareSheet").classList.add("open"); } + ["laneSheet","saveSheet"].forEach(id=>$(id).classList.remove("open")); $("scrim").classList.add("open"); $("shareSheet").classList.add("open"); } $("shareSeg").querySelectorAll("button").forEach(b=>b.onclick=()=>{ shareKind=b.dataset.k; refreshShare(); }); $("shareCopy").onclick=()=>copyText(shareUrl(),()=>flashShare("Link copied ✓")); $("shareCopyT").onclick=()=>copyText(shareText(),()=>flashShare("Copied ✓")); @@ -842,8 +830,8 @@ const TOUR=[ {sel:".sels", title:"Pick what to play", text:"Choose a set list and the track within it. Tracks are your practice items — name them for whatever you're working on, even if two share the same beat."}, {sel:"#saveBtn", title:"Save & library", text:"Save the current track — “Save as new”, or “Update” one of yours. The same sheet is your library: make set lists and rename / reorder / delete tracks. It all lives with the full editor too."}, {sel:"#pulse", title:"Tempo", text:"Tap the BPM to tap-tempo, press-and-hold to type an exact value, or drag up/down to scrub. ±10 / ±1 buttons nudge it."}, - {sel:"#lanes", title:"Edit the beat", text:"Each lane is a row of pads that blink on the beat — tap a pad to cycle rest → beat → accent → ghost. Tap the little note icon to pick the note value (eighths, triplets, sixteenths…); tap the lane name for sound, grouping, mute or polymeter. “+ Add lane” for more."}, - {sel:"#trackpanel", title:"Track settings", text:"How many bars to play and what happens then (loop, stop, or jump to the next/previous track), plus a tempo ramp and practice gaps."}, + {sel:"#lanes", title:"Edit the beat", text:"Each lane is a row of pads that blink on the beat — tap a pad to cycle rest → beat → accent → ghost. Tap a lane's label to set its note value (eighths, triplets, sixteenths…), sound, grouping, mute or polymeter. “+ Add lane” for more."}, + {sel:"#trackpanel", title:"Track settings", text:"Optional per-track extras (under the lanes): Repeat for N bars then stop / next / prev track, a tempo ramp, and practice gaps."}, {sel:"#bPrac", title:"Practice = a timed session", text:"Play just runs the metronome. Practice times your playing and logs it (not audio): it starts a session clock and Play becomes Stop — start/pause each track, then Stop to save the session."}, {sel:"#sessbar", title:"Your practice log", text:"Review past sessions, add notes, and compare a track across days."}, ];