pm-mobile: track settings dialog, balanced layout, clearer help wording
- Help wording: Practice no longer says "record" (which read like audio
capture) — it now says it times your playing and logs it to the practice log,
not audio. Play step and the session bar reworded too ("Practising…").
- Track settings dialog (mirrors the lane dialog), opened from a "⚙ Track"
summary button under the lanes: bar count, end behavior (loop / stop / next),
tempo ramp, practice gaps, and copy/paste of the track share-string. These
were previously read-only chips; now editable and persisted.
end/loop is now honored at runtime: loop repeats the phrase, stop halts at the
bar count, next advances the set list (the bar readout cycles 1..N when looping).
- Layout: pulse + lanes are centered as one block with the transport pinned at
the bottom — kills the big empty mid-band. Landscape reflows to pulse-left /
lanes-right with a full-width transport. Bigger pulse.
Engine untouched; conformance suite unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8b795d4107
commit
8f5635af52
1 changed files with 130 additions and 60 deletions
190
mobile.html
190
mobile.html
|
|
@ -67,10 +67,11 @@
|
|||
font-size:18px; line-height:1; cursor:pointer; color:var(--txt); background:rgba(127,139,154,.14); border:1px solid var(--panel-bd); }
|
||||
.icon:active{ background:rgba(127,139,154,.30); }
|
||||
|
||||
/* ---- middle: stage (pulse) + right column (lanes/features + transport) ---- */
|
||||
#mid{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; gap:6px; }
|
||||
#stage{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:clamp(8px,2vmin,22px); }
|
||||
#pulse{ position:relative; width:clamp(150px,36vmin,340px); height:clamp(150px,36vmin,340px); border-radius:50%;
|
||||
/* ---- middle: pulse + lanes centered as one block, transport pinned below ---- */
|
||||
#mid{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; gap:10px; }
|
||||
#center{ flex:1 1 auto; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:clamp(14px,3.5vmin,34px); }
|
||||
#stage{ flex:0 0 auto; display:flex; flex-direction:column; align-items:center; gap:clamp(8px,2vmin,18px); }
|
||||
#pulse{ position:relative; width:clamp(186px,46vmin,360px); height:clamp(186px,46vmin,360px); border-radius:50%;
|
||||
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; }
|
||||
|
|
@ -83,9 +84,12 @@
|
|||
#bpmIn::-webkit-outer-spin-button, #bpmIn::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; }
|
||||
#meterline{ font-size:clamp(12px,2.1vmin,16px); color:var(--muted); text-align:center; min-height:1.2em; letter-spacing:.02em; }
|
||||
|
||||
/* ---- editable lanes + feature chips ---- */
|
||||
#detail{ flex:0 0 auto; max-height:30vh; overflow-y:auto; display:flex; flex-direction:column; gap:6px; padding:2px 0; }
|
||||
/* ---- editable lanes + track-settings button ---- */
|
||||
#detail{ flex:0 1 auto; width:100%; max-width:560px; max-height:34vh; overflow-y:auto; display:flex; flex-direction:column; gap:8px; padding:2px 0; }
|
||||
#lanes{ display:flex; flex-direction:column; gap:6px; }
|
||||
#trackBtn{ align-self:center; background:var(--chip-bg); border:1px solid var(--chip-bd); color:var(--muted);
|
||||
border-radius:9px; padding:7px 14px; font-size:clamp(11px,1.9vmin,13px); cursor:pointer; max-width:100%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
||||
#trackBtn:active{ background:rgba(127,139,154,.22); }
|
||||
.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;
|
||||
|
|
@ -120,16 +124,16 @@
|
|||
.tbtn.prac{ background:linear-gradient(180deg,#1c87b8,#136488); border-color:#1f9bd0; color:#eaf8ff; }
|
||||
.tbtn.prac.on{ background:linear-gradient(180deg,#c8922a,#9c6f12); border-color:#e0a93a; color:#fff; }
|
||||
|
||||
/* landscape phones: pulse left, lanes right (centered), transport full-width below */
|
||||
@media (orientation:landscape) and (max-height:600px){
|
||||
#mid{ flex-direction:row; align-items:stretch; gap:3vw; }
|
||||
#stage{ flex:1 1 46%; gap:clamp(6px,1.6vmin,14px); }
|
||||
#rightcol{ flex:1 1 54%; display:flex; flex-direction:column; justify-content:center; gap:6px; min-width:0; }
|
||||
#pulse{ width:clamp(130px,34vmin,260px); height:clamp(130px,34vmin,260px); }
|
||||
#center{ flex-direction:row; align-items:center; justify-content:center; gap:4vw; }
|
||||
#stage{ flex:0 1 44%; gap:clamp(6px,1.6vmin,12px); }
|
||||
#detail{ flex:0 1 52%; max-width:560px; max-height:64vh; }
|
||||
#pulse{ width:clamp(132px,40vmin,300px); height:clamp(132px,40vmin,300px); }
|
||||
#bpmlab{ display:none; }
|
||||
#detail{ max-height:42vh; }
|
||||
.tbtn{ height:clamp(40px,12vmin,60px); }
|
||||
.tbtn{ height:clamp(40px,13vmin,58px); }
|
||||
#transport .tgrid{ max-width:640px; }
|
||||
}
|
||||
#rightcol{ display:contents; }
|
||||
|
||||
/* ---- session bar ---- */
|
||||
#sessbar{ flex:0 0 auto; display:flex; align-items:center; gap:8px; width:100%; margin-top:6px; padding:9px 12px; border-radius:11px;
|
||||
|
|
@ -141,21 +145,24 @@
|
|||
/* ---- bottom sheet (lane editor) ---- */
|
||||
#scrim{ position:fixed; inset:0; background:rgba(0,0,0,.55); opacity:0; pointer-events:none; transition:opacity .2s; z-index:40; }
|
||||
#scrim.open{ opacity:1; pointer-events:auto; }
|
||||
#laneSheet{ position:fixed; left:0; right:0; bottom:0; z-index:50; max-height:84vh; overflow-y:auto;
|
||||
#laneSheet, #trackSheet{ position:fixed; left:0; right:0; bottom:0; z-index:50; max-height:88vh; overflow-y:auto;
|
||||
background:var(--panel-bg); border-top:1px solid var(--panel-bd); border-radius:18px 18px 0 0; transform:translateY(110%);
|
||||
transition:transform .26s cubic-bezier(.2,.8,.2,1);
|
||||
padding:12px max(16px,env(safe-area-inset-right)) max(18px,env(safe-area-inset-bottom)) max(16px,env(safe-area-inset-left)); }
|
||||
#laneSheet.open{ transform:none; }
|
||||
#laneSheet .grab{ width:42px; height:5px; border-radius:3px; background:var(--panel-bd); margin:0 auto 12px; }
|
||||
#laneSheet h2{ margin:0 0 10px; font-size:16px; }
|
||||
#laneSheet label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 5px; }
|
||||
#laneSheet select, #laneSheet input[type=text]{ width:100%; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:10px; padding:11px; font-size:15px; }
|
||||
.lrow{ display:flex; gap:14px; margin-top:10px; flex-wrap:wrap; }
|
||||
.lrow .half{ flex:1 1 120px; margin:0; }
|
||||
.chk{ display:flex; align-items:center; gap:8px; font-size:14px; color:var(--txt); margin-top:18px; }
|
||||
.chk input{ width:20px; height:20px; accent-color:var(--cyan); }
|
||||
.lfoot{ display:flex; justify-content:space-between; margin-top:18px; }
|
||||
.lbtn{ cursor:pointer; color:var(--txt); background:linear-gradient(180deg,var(--btn1),var(--btn2)); border:1px solid var(--btn-bd); border-radius:10px; padding:10px 18px; font-size:14px; }
|
||||
#laneSheet.open, #trackSheet.open{ transform:none; }
|
||||
#laneSheet .grab, #trackSheet .grab{ width:42px; height:5px; border-radius:3px; background:var(--panel-bd); margin:0 auto 12px; }
|
||||
#laneSheet h2, #trackSheet h2{ margin:0 0 10px; font-size:16px; }
|
||||
#laneSheet label, #trackSheet label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 5px; }
|
||||
#laneSheet select, #trackSheet select,
|
||||
#laneSheet input[type=text], #trackSheet input[type=text], #trackSheet input[type=number]{
|
||||
width:100%; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:10px; padding:11px; font-size:15px; }
|
||||
#laneSheet .lrow, #trackSheet .lrow{ display:flex; gap:12px; margin-top:10px; flex-wrap:wrap; }
|
||||
#laneSheet .half, #trackSheet .half{ display:block; flex:1 1 120px; margin:0; }
|
||||
#laneSheet .chk, #trackSheet .chk{ display:flex; align-items:center; gap:8px; font-size:14px; color:var(--txt); margin-top:16px; }
|
||||
#laneSheet .chk input, #trackSheet .chk input{ width:20px; height:20px; accent-color:var(--cyan); flex:0 0 auto; }
|
||||
#trackSheet .lrow.off{ display:none; }
|
||||
.lfoot{ display:flex; justify-content:space-between; align-items:center; margin-top:18px; }
|
||||
.lbtn{ cursor:pointer; color:var(--txt); background:linear-gradient(180deg,var(--btn1),var(--btn2)); border:1px solid var(--btn-bd); border-radius:10px; padding:10px 16px; font-size:14px; }
|
||||
.lbtn.danger{ background:transparent; color:#ff7a7a; border-color:#ff7a7a; }
|
||||
|
||||
/* ---- help tour (coachmarks) ---- */
|
||||
|
|
@ -186,31 +193,30 @@
|
|||
</div>
|
||||
|
||||
<div id="mid">
|
||||
<div id="stage">
|
||||
<div id="pulse">
|
||||
<div id="bpm">120</div>
|
||||
<input id="bpmIn" type="number" inputmode="numeric" min="30" max="300" />
|
||||
<div id="bpmlab">BPM</div>
|
||||
<div id="center">
|
||||
<div id="stage">
|
||||
<div id="pulse">
|
||||
<div id="bpm">120</div>
|
||||
<input id="bpmIn" type="number" inputmode="numeric" min="30" max="300" />
|
||||
<div id="bpmlab">BPM</div>
|
||||
</div>
|
||||
<div id="meterline"></div>
|
||||
</div>
|
||||
<div id="meterline"></div>
|
||||
</div>
|
||||
|
||||
<div id="rightcol">
|
||||
<div id="detail">
|
||||
<div id="lanes"></div>
|
||||
<div class="chips" id="feats"></div>
|
||||
<button id="trackBtn" title="Track settings — bars, loop/end, ramp, gaps, share string"></button>
|
||||
</div>
|
||||
<div id="transport">
|
||||
<div class="tgrid">
|
||||
<button class="tbtn" id="bDn10" title="Tempo −10">−10</button>
|
||||
<button class="tbtn" id="bPrev" title="Previous track">⏮</button>
|
||||
<button class="tbtn" id="bNext" title="Next track">⏭</button>
|
||||
<button class="tbtn" id="bUp10" title="Tempo +10">+10</button>
|
||||
<button class="tbtn" id="bDown" title="Tempo −1">−</button>
|
||||
<button class="tbtn play" id="bPlay" title="Play / Stop">▶<small>PLAY</small></button>
|
||||
<button class="tbtn prac" id="bPrac" title="Practice (start/stop tracks within a session)">⦿<small>PRACTICE</small></button>
|
||||
<button class="tbtn" id="bUp" title="Tempo +1">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="transport">
|
||||
<div class="tgrid">
|
||||
<button class="tbtn" id="bDn10" title="Tempo −10">−10</button>
|
||||
<button class="tbtn" id="bPrev" title="Previous track">⏮</button>
|
||||
<button class="tbtn" id="bNext" title="Next track">⏭</button>
|
||||
<button class="tbtn" id="bUp10" title="Tempo +10">+10</button>
|
||||
<button class="tbtn" id="bDown" title="Tempo −1">−</button>
|
||||
<button class="tbtn play" id="bPlay" title="Play / Stop">▶<small>PLAY</small></button>
|
||||
<button class="tbtn prac" id="bPrac" title="Practice — logs your time to the practice log">⦿<small>PRACTICE</small></button>
|
||||
<button class="tbtn" id="bUp" title="Tempo +1">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -236,6 +242,35 @@
|
|||
<div class="lfoot"><button id="lsDel" class="lbtn danger">Delete lane</button><button id="lsDone" class="lbtn">Done</button></div>
|
||||
</div>
|
||||
|
||||
<!-- track settings sheet -->
|
||||
<div id="trackSheet">
|
||||
<div class="grab"></div>
|
||||
<h2>Track settings</h2>
|
||||
<div class="lrow">
|
||||
<label class="half">Bars per repeat (0 = unlimited)<input id="tsBars" type="number" inputmode="numeric" min="0" max="999" /></label>
|
||||
<label class="half">At end<select id="tsEnd"><option value="loop">Loop</option><option value="stop">Stop</option><option value="next">Next track</option></select></label>
|
||||
</div>
|
||||
<label class="chk"><input type="checkbox" id="tsRamp" /> Tempo ramp (speed up while you play)</label>
|
||||
<div class="lrow" id="tsRampRow">
|
||||
<label class="half">Start BPM<input id="tsRampStart" type="number" inputmode="numeric" min="30" max="300" /></label>
|
||||
<label class="half">+BPM<input id="tsRampAmt" type="number" inputmode="numeric" min="1" max="50" /></label>
|
||||
<label class="half">every N bars<input id="tsRampEvery" type="number" inputmode="numeric" min="1" max="64" /></label>
|
||||
</div>
|
||||
<label class="chk"><input type="checkbox" id="tsGap" /> Practice gaps (mute bars to test your inner clock)</label>
|
||||
<div class="lrow" id="tsGapRow">
|
||||
<label class="half">Play bars<input id="tsGapPlay" type="number" inputmode="numeric" min="1" max="32" /></label>
|
||||
<label class="half">Mute bars<input id="tsGapMute" type="number" inputmode="numeric" min="1" max="32" /></label>
|
||||
</div>
|
||||
<label for="tsStr">Track string (share / copy / paste)</label>
|
||||
<textarea id="tsStr" spellcheck="false" style="width:100%;background:var(--field-bg);color:var(--txt);border:1px solid var(--field-bd);border-radius:10px;padding:10px;font-family:'Courier New',monospace;font-size:12px;resize:vertical;min-height:54px"></textarea>
|
||||
<div class="lrow">
|
||||
<button id="tsCopy" class="lbtn">Copy</button>
|
||||
<button id="tsApply" class="lbtn">Apply pasted string</button>
|
||||
<span id="tsMsg" style="font-size:12px;color:var(--muted);align-self:center"></span>
|
||||
</div>
|
||||
<div class="lfoot"><span></span><button id="tsDone" class="lbtn">Done</button></div>
|
||||
</div>
|
||||
|
||||
<!-- guided help tour -->
|
||||
<div id="tour">
|
||||
<div id="tourHole"></div>
|
||||
|
|
@ -277,7 +312,7 @@ function advanceMaster(ahead){
|
|||
if(barIndex>0&&ramp.on&&(barIndex%ramp.everyBars===0)) setBpm(state.bpm+ramp.amount);
|
||||
if(trainer.on){ const cyc=trainer.playBars+trainer.muteBars; if(cyc>0&&(barIndex%cyc)>=trainer.playBars) muteWindows.push({start:masterBeatTime,end:masterBeatTime+mbpb*(60/state.bpm)}); }
|
||||
segBarCount=barIndex;
|
||||
if(segBars>0&&barIndex>=segBars&&!pendingAdvance){ pendingAdvance=true; }
|
||||
if(segBars>0&&barIndex>=segBars&&!pendingAdvance&&curEnd!=null){ pendingAdvance=true; } // loop (null) keeps playing
|
||||
}
|
||||
masterBeat++; masterBeatTime+=60/state.bpm;
|
||||
}
|
||||
|
|
@ -287,7 +322,11 @@ function scheduler(){
|
|||
const ahead=audioCtx.currentTime+SCHEDULE_AHEAD;
|
||||
advanceMaster(ahead);
|
||||
for(const m of meters){ while(m.nextTime<ahead){ scheduleMeterTick(m,m.nextTime); m.nextTime+=laneStepDur(m,m.tick); m.tick++; } }
|
||||
if(pendingAdvance){ pendingAdvance=false; setTimeout(()=>gotoItem(idx+1,true),0); }
|
||||
if(pendingAdvance){ pendingAdvance=false; setTimeout(handleEnd,0); }
|
||||
}
|
||||
function handleEnd(){
|
||||
if(curEnd==="stop"){ if(sessionActive&&state.running) pauseTrack(); else stopAudio(); } // stop at end of the bar count
|
||||
else gotoItem(idx+1,true); // "next" → advance the set list
|
||||
}
|
||||
|
||||
/* ========================= PLAYER ============================================= */
|
||||
|
|
@ -412,7 +451,8 @@ function addLane(){ meters.push(buildMeters([{groupsStr:"4",stepsPerBeat:1,sound
|
|||
function openLaneSheet(i){ editLaneIdx=i; const m=meters[i]; if(!m) return;
|
||||
$("lsSound").value=m.sound; $("lsGroup").value=m.groupsStr; $("lsSub").value=String(m.stepsPerBeat); $("lsSwing").checked=!!m.swing; $("lsPoly").checked=!!m.poly; $("lsMute").checked=!m.enabled;
|
||||
$("scrim").classList.add("open"); $("laneSheet").classList.add("open"); }
|
||||
function closeLaneSheet(){ $("scrim").classList.remove("open"); $("laneSheet").classList.remove("open"); }
|
||||
function closeSheets(){ $("scrim").classList.remove("open"); $("laneSheet").classList.remove("open"); $("trackSheet").classList.remove("open"); }
|
||||
const closeLaneSheet=closeSheets;
|
||||
function applyLane(){ const m=meters[editLaneIdx]; if(!m) return;
|
||||
let grp=($("lsGroup").value||"").trim()||"4";
|
||||
rebuildLane(editLaneIdx,{groupsStr:grp,stepsPerBeat:Math.max(1,Math.min(4,parseInt($("lsSub").value,10)||1)),swing:$("lsSwing").checked,
|
||||
|
|
@ -422,7 +462,36 @@ function applyLane(){ const m=meters[editLaneIdx]; if(!m) return;
|
|||
$("lsGroup").addEventListener("change",applyLane);
|
||||
$("lsDone").onclick=closeLaneSheet;
|
||||
$("lsDel").onclick=()=>{ if(meters.length<=1){ closeLaneSheet(); return; } meters.splice(editLaneIdx,1); laneSig=null; renderAll(); saveState(); closeLaneSheet(); };
|
||||
$("scrim").onclick=closeLaneSheet;
|
||||
$("scrim").onclick=closeSheets;
|
||||
|
||||
/* ---- track settings sheet: bars, end/loop, ramp, gaps, copy/paste string ---- */
|
||||
function clampInt(v,lo,hi,def){ v=parseInt(v,10); if(isNaN(v)) return def; return Math.max(lo,Math.min(hi,v)); }
|
||||
function openTrackSheet(){
|
||||
$("tsBars").value=segBars||0;
|
||||
$("tsEnd").value = curEnd==null?"loop":(curEnd==="stop"?"stop":"next");
|
||||
$("tsRamp").checked=ramp.on; $("tsRampStart").value=ramp.startBpm||80; $("tsRampAmt").value=ramp.amount||5; $("tsRampEvery").value=ramp.everyBars||4;
|
||||
$("tsGap").checked=trainer.on; $("tsGapPlay").value=trainer.playBars||2; $("tsGapMute").value=trainer.muteBars||2;
|
||||
$("tsRampRow").classList.toggle("off",!ramp.on); $("tsGapRow").classList.toggle("off",!trainer.on);
|
||||
$("tsStr").value=currentPatch(); $("tsMsg").textContent="";
|
||||
$("laneSheet").classList.remove("open"); $("scrim").classList.add("open"); $("trackSheet").classList.add("open");
|
||||
}
|
||||
function applyTrackSettings(){
|
||||
segBars=Math.max(0,parseInt($("tsBars").value,10)||0);
|
||||
const e=$("tsEnd").value; curEnd = e==="loop"?null:(e==="stop"?"stop":1);
|
||||
ramp.on=$("tsRamp").checked; ramp.startBpm=clampInt($("tsRampStart").value,30,300,80); ramp.amount=clampInt($("tsRampAmt").value,1,50,5); ramp.everyBars=clampInt($("tsRampEvery").value,1,64,4);
|
||||
trainer.on=$("tsGap").checked; trainer.playBars=clampInt($("tsGapPlay").value,1,32,2); trainer.muteBars=clampInt($("tsGapMute").value,1,32,2);
|
||||
$("tsRampRow").classList.toggle("off",!ramp.on); $("tsGapRow").classList.toggle("off",!trainer.on);
|
||||
$("tsStr").value=currentPatch(); renderAll(); saveState();
|
||||
}
|
||||
["tsBars","tsEnd","tsRamp","tsRampStart","tsRampAmt","tsRampEvery","tsGap","tsGapPlay","tsGapMute"].forEach(id=>$(id).addEventListener("change",applyTrackSettings));
|
||||
$("tsCopy").onclick=()=>{ const s=currentPatch(); const ok=()=>{ $("tsMsg").textContent="Copied ✓"; setTimeout(()=>$("tsMsg").textContent="",1600); };
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(s).then(ok,()=>{ $("tsStr").select(); }); } else { $("tsStr").select(); try{ document.execCommand("copy"); ok(); }catch(e){} } };
|
||||
$("tsApply").onclick=()=>{ const txt=($("tsStr").value||"").trim().replace(/^[#?&]*p=/,"");
|
||||
try{ const setup=patchToSetup(txt); if(!setup.lanes.length) throw 0; loadSetup(setup); laneSig=null; renderAll(); saveState();
|
||||
$("tsStr").value=currentPatch(); $("tsMsg").textContent="Applied ✓"; setTimeout(()=>$("tsMsg").textContent="",1600); }
|
||||
catch(e){ $("tsMsg").textContent="✗ not a valid track string"; } };
|
||||
$("tsDone").onclick=closeSheets;
|
||||
$("trackBtn").onclick=openTrackSheet;
|
||||
|
||||
/* ========================= SET-LIST / TRACK DROPDOWNS ======================== */
|
||||
function opt(v,t){ const o=document.createElement("option"); o.value=v; o.textContent=t; return o; }
|
||||
|
|
@ -449,18 +518,18 @@ function renderInfo(){
|
|||
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();
|
||||
buildFeats(); updateStatus();
|
||||
buildTrackBtn(); updateStatus();
|
||||
}
|
||||
function endLabel(){ if(curEnd==null) return "loop"; if(curEnd==="stop") return "stop"; if(curEnd===1) return "next";
|
||||
if(typeof curEnd==="number"&&curEnd>0) return "+"+curEnd; return String(curEnd); }
|
||||
function buildFeats(){ const ft=$("feats"); ft.innerHTML="";
|
||||
const add=(t,cls)=>{ const c=document.createElement("span"); c.className="chip feat"+(cls?" "+cls:""); c.textContent=t; ft.appendChild(c); };
|
||||
if(segBars>0) add(segBars+" bars"); add("→ "+endLabel()); if(curRep>1) add("× "+curRep);
|
||||
if(ramp.on) add("ramp "+ramp.startBpm+"→ +"+ramp.amount+"/"+ramp.everyBars+"bar","r");
|
||||
if(trainer.on) add("gap "+trainer.playBars+"/"+trainer.muteBars+" play/mute","g"); }
|
||||
function buildTrackBtn(){ const parts=[];
|
||||
if(segBars>0) parts.push(segBars+" bars"); parts.push("→ "+endLabel());
|
||||
if(ramp.on) parts.push("ramp"); if(trainer.on) parts.push("gaps");
|
||||
$("trackBtn").textContent="⚙ "+parts.join(" · "); }
|
||||
function updateStatus(){ const m=meters[0]; let s="";
|
||||
if(m){ s=m.beatsPerBar+" beats"+(m.groups.length>1?" · "+m.groups.join("+"):"")+(m.swing?" · swing":""); }
|
||||
if(state.running&&m){ s+=" · bar "+((m.currentBar|0)+1)+(segBars>0?(" / "+segBars):"")+" · "+fmt((Date.now()-runStartAt)/1000); }
|
||||
if(state.running&&m){ const bar=segBars>0?((m.currentBar|0)%segBars+1):((m.currentBar|0)+1);
|
||||
s+=" · bar "+bar+(segBars>0?(" / "+segBars):"")+" · "+fmt((Date.now()-runStartAt)/1000); }
|
||||
else if(segBars>0){ s+=" · "+segBars+" bars"; }
|
||||
$("meterline").textContent=s; }
|
||||
function renderTransport(){
|
||||
|
|
@ -470,7 +539,7 @@ function renderTransport(){
|
|||
$("bPrac").innerHTML=(pr?"❚❚<small>PAUSE</small>":"⦿<small>PRACTICE</small>"); $("bPrac").classList.toggle("on",pr); }
|
||||
function renderSessionBar(){ const bar=$("sessbar"), n=lsGet(LS_SESSIONS,[]).length;
|
||||
if(sessionActive){ bar.classList.add("rec"); const segs=session.segments.length+(trackSegStart?1:0);
|
||||
$("sessText").textContent="Recording · session "+fmt((Date.now()-session.at)/1000)+" · "+segs+" track"+(segs===1?"":"s"); }
|
||||
$("sessText").textContent="Practising · session "+fmt((Date.now()-session.at)/1000)+" · "+segs+" track"+(segs===1?"":"s"); }
|
||||
else { bar.classList.remove("rec"); $("sessText").textContent=(lastSaved?"✓ Session saved · ":"")+"Practice sessions"+(n?(" ("+n+")"):"")+" →"; } }
|
||||
function renderAll(){ renderInfo(); renderTransport(); saveState(); }
|
||||
|
||||
|
|
@ -540,8 +609,9 @@ const TOUR=[
|
|||
{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."},
|
||||
{sel:"#bDn10", title:"Nudge tempo", text:"−10 / +10 for big jumps, − / + for fine adjustments."},
|
||||
{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 change its sound, grouping, subdivision, mute or polymeter. “+ Add lane” for more."},
|
||||
{sel:"#bPlay", title:"Play", text:"Runs the metronome without recording anything."},
|
||||
{sel:"#bPrac", title:"Practice = a timed session", text:"Practice starts a session clock and records what you play; Play turns into Stop. Use Practice to start/stop individual tracks, then hit Stop when you're done to save the whole session."},
|
||||
{sel:"#bPlay", title:"Play", text:"Just runs the metronome — nothing is added to your practice log."},
|
||||
{sel:"#bPrac", title:"Practice = a timed session", text:"Practice times your playing and logs it to your practice log (it does NOT record audio). It starts a session clock; Play turns into Stop. Use Practice to start/pause each track, then tap Stop when you're done to save the session."},
|
||||
{sel:"#trackBtn", title:"Track settings", text:"Set this track's bar count, what happens at the end (loop / stop / next), tempo ramp and practice gaps — and copy or paste the track's share string."},
|
||||
{sel:"#sessbar", title:"Your practice log", text:"Review past sessions, add notes, and compare a track across days."},
|
||||
];
|
||||
let tstep=0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue