diff --git a/mobile.html b/mobile.html index a5e7fe4..ba563df 100644 --- a/mobile.html +++ b/mobile.html @@ -50,13 +50,18 @@ font-family:"Segoe UI", Roboto, Helvetica, Arial, sans-serif; -webkit-user-select:none; user-select:none; -webkit-tap-highlight-color:transparent; touch-action:manipulation; overscroll-behavior:none; } - #app{ position:fixed; inset:0; overflow:hidden; display:flex; flex-direction:column; - padding:max(8px,env(safe-area-inset-top)) max(12px,env(safe-area-inset-right)) - max(8px,env(safe-area-inset-bottom)) max(12px,env(safe-area-inset-left)); } + /* Content is capped to --maxw and centered, so phone→tablet is the SAME layout, + just larger (no flex re-flow). Generous margins; even more in full-screen. */ + #app{ position:fixed; inset:0; overflow:hidden; display:flex; flex-direction:column; align-items:center; --maxw:600px; --pad:14px; + padding:max(12px,env(safe-area-inset-top)) max(var(--pad),env(safe-area-inset-right)) + max(12px,env(safe-area-inset-bottom)) max(var(--pad),env(safe-area-inset-left)); } + #top, #mid, #sessbar{ width:100%; max-width:var(--maxw); } + :fullscreen #app, html:fullscreen #app{ --pad:30px; padding-top:max(24px,env(safe-area-inset-top)); padding-bottom:max(18px,env(safe-area-inset-bottom)); } + @media (display-mode:standalone){ #app{ --pad:26px; padding-top:max(20px,env(safe-area-inset-top)); padding-bottom:max(16px,env(safe-area-inset-bottom)); } } /* ---- top ---- */ - #top{ flex:0 0 auto; display:flex; flex-direction:column; gap:8px; } - .sels{ display:flex; gap:8px; } + #top{ flex:0 0 auto; display:flex; flex-direction:column; gap:10px; } + .sels{ display:flex; gap:8px; align-items:flex-end; } .sel{ flex:1 1 0; min-width:0; display:flex; flex-direction:column; gap:3px; } .sel > span{ font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); padding-left:3px; } .sel select{ width:100%; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:10px; padding:10px 8px; font-size:15px; } @@ -85,19 +90,19 @@ #meterline{ font-size:clamp(12px,2.1vmin,16px); color:var(--muted); text-align:center; min-height:1.2em; letter-spacing:.02em; } /* ---- track panel (repeat/end/ramp/gap/string) + editable lanes ---- */ - #detail{ flex:0 1 auto; width:100%; max-width:560px; max-height:32vh; overflow-y:auto; display:flex; flex-direction:column; gap:8px; padding:2px 0; } + #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; } - #trackpanel{ width:100%; background:var(--chip-bg); border:1px solid var(--chip-bd); border-radius:10px; padding:8px 10px; display:flex; flex-direction:column; gap:7px; font-size:12px; color:var(--muted); } - #trackpanel .tp-grid{ display:flex; flex-wrap:wrap; align-items:center; gap:8px 14px; } - #trackpanel label{ display:flex; align-items:center; gap:6px; } + #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; } #trackpanel .tp-chk{ color:var(--txt); } - #trackpanel .tp-chk input{ width:18px; height:18px; accent-color:var(--cyan); } - #trackpanel input[type=number]{ width:48px; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:7px; padding:5px 6px; font-size:13px; -moz-appearance:textfield; } + #trackpanel .tp-chk input{ width:18px; height:18px; accent-color:var(--cyan); flex:0 0 auto; } + #trackpanel .tp-loop{ color:var(--muted); } + #trackpanel input[type=number]{ width:46px; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:7px; padding:5px 6px; font-size:13px; -moz-appearance:textfield; } #trackpanel input[type=number]::-webkit-outer-spin-button, #trackpanel input[type=number]::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; } #trackpanel select{ background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:7px; padding:5px 6px; font-size:13px; } - #trackpanel .tp-sub{ display:flex; align-items:center; gap:6px; flex-wrap:wrap; } + #trackpanel .tp-sub{ display:flex; align-items:center; gap:6px; flex-wrap:nowrap; white-space:nowrap; color:var(--muted); } #trackpanel .tp-sub.off{ display:none; } - #trackpanel .tp-sub b{ color:var(--txt); font-weight:600; } #trackpanel .tp-str{ display:flex; gap:6px; } #trackpanel .tp-str input{ flex:1 1 auto; min-width:0; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:7px; padding:6px 8px; font-family:"Courier New",monospace; font-size:11px; } #trackpanel .tp-btn{ flex:0 0 auto; background:linear-gradient(180deg,var(--btn1),var(--btn2)); color:var(--txt); border:1px solid var(--btn-bd); border-radius:7px; padding:0 12px; font-size:12px; cursor:pointer; } @@ -112,6 +117,15 @@ .lmeta .rhythm{ flex:0 0 auto; color:var(--txt); display:block; } .lmeta .polybadge{ flex:0 0 auto; color:var(--poly); font-weight:700; letter-spacing:.01em; } .lane.poly .lmeta{ border-left:3px solid var(--poly); padding-left:6px; } + .lmeta .rh-host{ flex:0 0 auto; display:flex; align-items:center; padding:2px 3px; margin:-2px -1px; border-radius:5px; cursor:pointer; } + .lmeta .rh-host:active{ background:rgba(127,139,154,.22); } + /* graphic note-value picker */ + .noterow{ display:flex; gap:8px; flex-wrap:wrap; } + .noterow .notebtn{ flex:0 0 auto; display:flex; flex-direction:column; align-items:center; gap:4px; min-width:56px; padding:9px 6px; + background:var(--field-bg); border:1px solid var(--field-bd); border-radius:10px; color:var(--txt); cursor:pointer; } + .noterow .notebtn.active{ border-color:var(--cyan); box-shadow:0 0 0 1px var(--cyan); } + .noterow .notebtn .rhythm{ height:22px; width:auto; } + .noterow .notebtn small{ font-size:10px; color:var(--muted); letter-spacing:.02em; } /* 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; } @@ -132,7 +146,7 @@ /* ---- transport ---- */ #transport{ flex:0 0 auto; display:flex; justify-content:center; padding-top:2px; } - .tgrid{ display:grid; width:100%; max-width:560px; gap:clamp(6px,1.6vmin,13px); + .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"; } .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; @@ -146,9 +160,11 @@ .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: 2-column grid — pulse + transport on the left, panel + lanes on the right */ - @media (orientation:landscape) and (max-height:600px){ - #top{ flex-direction:row; align-items:flex-end; gap:12px; } + /* landscape (phone AND tablet — same layout, just scaled): 2-column grid — + pulse + transport on the left, panel + lanes on the right */ + @media (orientation:landscape){ + #app{ --maxw:1060px; } + #top{ flex-direction:row-reverse; align-items:flex-end; gap:14px; } #top .sels{ flex:3 1 0; min-width:0; } #top .trow{ flex:2 1 0; min-width:0; } #mid{ display:grid; align-items:center; gap:8px 4vw; @@ -157,33 +173,38 @@ #stage{ grid-area:stage; align-self:center; } #detail{ grid-area:detail; align-self:stretch; width:auto; max-width:none; max-height:none; min-height:0; overflow-y:auto; } #transport{ grid-area:transport; align-self:end; } - #pulse{ width:clamp(100px,30vmin,240px); height:clamp(100px,30vmin,240px); } - #bpmlab{ display:none; } - .tbtn{ height:clamp(34px,11vmin,54px); } + #pulse{ width:clamp(110px,40vmin,360px); height:clamp(110px,40vmin,360px); } + .tbtn{ height:clamp(40px,13vmin,68px); } } /* ---- 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; - cursor:pointer; text-decoration:none; background:rgba(127,139,154,.10); border:1px solid var(--panel-bd); color:var(--muted); font-size:13px; } - #sessbar.rec{ background:rgba(192,57,43,.12); border-color:#c0392b; color:var(--txt); cursor:default; } + #sessbar{ flex:0 0 auto; display:flex; align-items:center; gap:10px; width:100%; margin-top:6px; padding:9px 12px; border-radius:11px; + background:rgba(127,139,154,.10); border:1px solid var(--panel-bd); color:var(--muted); font-size:13px; } + #sessbar.rec{ background:rgba(192,57,43,.12); border-color:#c0392b; color:var(--txt); } + #sessLink{ flex:1 1 auto; min-width:0; display:flex; align-items:center; gap:8px; text-decoration:none; color:inherit; cursor:pointer; } #sessbar .dotrec{ width:9px; height:9px; border-radius:50%; background:#e0493a; box-shadow:0 0 8px #e0493a; flex:0 0 auto; display:none; } - #sessbar.rec .dotrec{ display:block; } #sessText{ flex:1 1 auto; } + #sessbar.rec .dotrec{ display:block; } + #sessText{ flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } + #repoLink{ flex:0 0 auto; display:inline-flex; align-items:center; opacity:.38; } + #repoLink:hover{ opacity:.85; } + #repoLink .rlogo{ height:13px; width:auto; display:block; } + [data-theme="light"] .logo-dark{ display:none; } [data-theme="dark"] .logo-light{ display:none; } /* ---- 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, #trackSheet, #saveSheet{ position:fixed; left:0; right:0; bottom:0; z-index:50; max-height:88vh; overflow-y:auto; + #laneSheet, #trackSheet, #saveSheet, #noteSheet, #shareSheet{ 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, #trackSheet.open, #saveSheet.open{ transform:none; } - #laneSheet .grab, #trackSheet .grab, #saveSheet .grab{ width:42px; height:5px; border-radius:3px; background:var(--panel-bd); margin:0 auto 12px; } - #laneSheet h2, #trackSheet h2, #saveSheet h2{ margin:0 0 10px; font-size:16px; } - #laneSheet label, #trackSheet label, #saveSheet label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 5px; } + #laneSheet.open, #trackSheet.open, #saveSheet.open, #noteSheet.open, #shareSheet.open{ transform:none; } + #laneSheet .grab, #trackSheet .grab, #saveSheet .grab, #noteSheet .grab, #shareSheet .grab{ width:42px; height:5px; border-radius:3px; background:var(--panel-bd); margin:0 auto 12px; } + #laneSheet h2, #trackSheet h2, #saveSheet h2, #noteSheet h2, #shareSheet h2{ margin:0 0 10px; font-size:16px; } + #laneSheet label, #trackSheet label, #saveSheet label, #shareSheet label{ display:block; font-size:12px; color:var(--muted); margin:10px 0 5px; } #laneSheet select, #trackSheet select, #saveSheet select, - #laneSheet input[type=text], #trackSheet input[type=text], #trackSheet input[type=number], #saveSheet input[type=text]{ + #laneSheet input[type=text], #trackSheet input[type=text], #trackSheet input[type=number], #saveSheet input[type=text], #shareSheet 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; } - #laneSheet .lrow, #trackSheet .lrow, #saveSheet .lrow{ display:flex; gap:12px; margin-top:10px; flex-wrap:wrap; align-items:center; } + #laneSheet .lrow, #trackSheet .lrow, #saveSheet .lrow, #shareSheet .lrow{ display:flex; gap:12px; margin-top:10px; flex-wrap:wrap; align-items:center; } #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; } @@ -191,6 +212,9 @@ .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; } + .seg{ display:flex; gap:8px; margin-bottom:6px; } + .seg button{ flex:1 1 0; padding:9px; border:1px solid var(--field-bd); background:var(--field-bg); color:var(--muted); border-radius:9px; font-size:14px; cursor:pointer; } + .seg button.active{ border-color:var(--cyan); color:var(--txt); } .seclbl{ font-size:11px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); margin:4px 0 8px; } .savemsg{ font-size:12px; color:#5fd08a; align-self:center; } .liblbl{ font-size:12px; color:var(--muted); margin:14px 0 4px; } @@ -216,17 +240,18 @@
-
- - -
-
+
-
+
?
+
+ + +
+
@@ -240,16 +265,17 @@
-
- - - - - +
+ + + then loop
-
Ramp start → + every bars
-
Gap play / mute bars
-
+
+ + +
+
→ + bpm / bars
+
play / mute bars
@@ -267,7 +293,10 @@
- Practice sessions → +
+ Practice sessions → + +
@@ -277,10 +306,7 @@

Edit lane

-
- - -
+
@@ -290,6 +316,26 @@
+ +
+
+

Note value

+
+
+ + +
+
+

Share

+
+ + +
+ + +
+
+
@@ -363,9 +409,9 @@ function scheduler(){ for(const m of meters){ while(m.nextTime''; const stem=(sx)=>''; const beam=(x0,x1,y)=>''; - const wrap=(W,g)=>''; - if(swing && n===2){ // dotted-eighth + sixteenth (shuffle) - const LEFT=3, U=4.6, cx0=LEFT, cx1=LEFT+2*U, s0=cx0+2, s1=cx1+2, W=Math.round(cx1+8); - let g=head(cx0)+head(cx1)+stem(s0)+stem(s1)+beam(s0-0.45,s1+0.45,topY)+beam(s1-3.4,s1+0.45,topY+2.6) - +''; - return wrap(W,g); - } const 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, W=Math.round(LEFT*2 + (n-1)*GAP + 4); let g="", first=0, last=0; @@ -487,17 +525,19 @@ function rhythmSVG(n, swing){ if(beams>=1) g+=beam(first-0.45,last+0.45,topY); if(beams>=2) g+=beam(first-0.45,last+0.45,topY+2.6); if(tup) g+=''+tup+''; - return wrap(W,g); + return ''; } -function laneMetaHTML(m){ const eff=laneNoteValue(m), swGlyph=m.swing&&eff===2; +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,m.swing)+""+esc(m.groupsStr)+(m.swing&&!swGlyph?" sw":"")+""+poly; } + 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 buildLanes(){ const box=$("lanes"); box.innerHTML=""; meters.forEach((m,i)=>{ const lane=document.createElement("div"); lane.className="lane"+(m.enabled?"":" off")+(m.poly?" poly":""); - const meta=document.createElement("button"); meta.className="lmeta"; meta.innerHTML=laneMetaHTML(m); m._meta=meta; + const meta=document.createElement("button"); meta.className="lmeta"; m._meta=meta; m._idx=i; setLaneMeta(m); meta.onclick=()=>openLaneSheet(i); const pads=document.createElement("div"); pads.className="pads"; const spb=m.stepsPerBeat; m._padEls=new Array(m.beatsPerBar*spb); m._lastPad=-1; @@ -512,8 +552,22 @@ function buildLanes(){ 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); - if(m._meta) m._meta.innerHTML=laneMetaHTML(m); // note value can change as hits are added/removed + setLaneMeta(m); // note value can change as hits are added/removed saveState(); } + +/* ---- graphic note-value picker (tap a lane's rhythm icon, or use it in the lane sheet) ---- */ +const NOTE_OPTS=[1,2,3,4,6]; +function noteName(n){ return {1:"quarter",2:"eighth",3:"triplet",4:"sixteenth",6:"sextuplet"}[n]||(n+"-tuplet"); } +function renderNoteOpts(box, cur, pick){ if(!box) return; box.innerHTML=""; + NOTE_OPTS.forEach(n=>{ const b=el("button","notebtn"+(n===cur?" active":"")); b.innerHTML=rhythmSVG(n)+""+noteName(n)+""; b.onclick=()=>pick(n); box.appendChild(b); }); } +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; 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){ @@ -531,17 +585,17 @@ function addLane(){ meters.push(buildMeters([{groupsStr:"4",stepsPerBeat:1,sound (function(){ const sel=$("lsSound"); VOICES.forEach(([k,lab])=>{ const o=document.createElement("option"); o.value=k; o.textContent=lab; sel.appendChild(o); }); })(); 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; $("lsSub").value=String(m.stepsPerBeat); $("lsSwing").checked=!!m.swing; $("lsPoly").checked=!!m.poly; $("lsMute").checked=!m.enabled; - $("lsGain").value=m.gainDb||0; $("lsGainVal").textContent=gainLabel(m.gainDb||0); - $("saveSheet").classList.remove("open"); $("scrim").classList.add("open"); $("laneSheet").classList.add("open"); } -function closeSheets(){ $("scrim").classList.remove("open"); $("laneSheet").classList.remove("open"); $("saveSheet").classList.remove("open"); } + $("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")); } 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, + rebuildLane(editLaneIdx,{groupsStr:grp,stepsPerBeat:m.stepsPerBeat,swing:m.swing, poly:$("lsPoly").checked,enabled:!$("lsMute").checked,sound:$("lsSound").value,gainDb:parseInt($("lsGain").value,10)||0,beatsOn:m.beatsOn.slice(),orns:(m.orns||[]).slice()}); - laneSig=null; renderAll(); saveState(); } -["lsSound","lsSub","lsSwing","lsPoly","lsMute"].forEach(id=>$(id).addEventListener("change",applyLane)); + laneSig=null; renderAll(); saveState(); refreshLaneSheetNotes(); } +["lsSound","lsPoly","lsMute"].forEach(id=>$(id).addEventListener("change",applyLane)); $("lsGain").addEventListener("input",()=>{ const m=meters[editLaneIdx]; if(!m) return; m.gainDb=parseInt($("lsGain").value,10)||0; $("lsGainVal").textContent=gainLabel(m.gainDb); saveState(); }); // live, no rebuild $("lsGroup").addEventListener("change",applyLane); $("lsDone").onclick=closeLaneSheet; @@ -553,28 +607,25 @@ 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; - $("ipEnd").value = curEnd==null?"loop":(curEnd==="stop"?"stop":"next"); + $("ipThen").style.display=hasEnd?"":"none"; $("ipLoop").style.display=hasEnd?"none":""; + $("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; $("ipGapPlay").value=trainer.playBars||2; $("ipGapMute").value=trainer.muteBars||2; $("ipRampRow").classList.toggle("off",!ramp.on); $("ipGapRow").classList.toggle("off",!trainer.on); - $("ipStr").value=currentPatch(); } function applyTrackPanel(){ segBars=Math.max(0,parseInt($("ipBars").value,10)||0); - const e=$("ipEnd").value; curEnd = e==="loop"?null:(e==="stop"?"stop":1); + 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":""; 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); - $("ipStr").value=currentPatch(); saveState(); + saveState(); } ["ipBars","ipEnd","ipRamp","ipGap","ipRampStart","ipRampAmt","ipRampEvery","ipGapPlay","ipGapMute"].forEach(id=>$(id).addEventListener("change",applyTrackPanel)); -$("ipCopy").onclick=()=>{ const s=currentPatch(); - if(navigator.clipboard&&navigator.clipboard.writeText){ navigator.clipboard.writeText(s).then(()=>flashIp("Copied ✓"),()=>{ $("ipStr").select(); }); } else { $("ipStr").select(); try{ document.execCommand("copy"); flashIp("Copied ✓"); }catch(e){} } }; -$("ipPaste").onclick=()=>{ const txt=($("ipStr").value||"").trim().replace(/^[#?&]*p=/,""); - try{ const setup=patchToSetup(txt); if(!setup.lanes.length) throw 0; loadSetup(setup); laneSig=null; renderAll(); saveState(); flashIp("Applied ✓"); } - catch(e){ flashIp("✗ invalid string"); } }; /* ---- save & library: user set lists/tracks (same store + format as the editor) ---- */ function userSetlists(){ return lsGet(LS_SETLISTS,[]); } @@ -649,9 +700,28 @@ 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"); $("scrim").classList.add("open"); $("saveSheet").classList.add("open"); } + $("laneSheet").classList.remove("open"); $("noteSheet").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 ---- */ +function setlistToCode(sl){ return b64u(JSON.stringify({t:sl.title||"",d:sl.description||"",i:(sl.items||[]).map(it=>({n:it.name,p:setupToPatch(it)}))})); } +function shareSetlistObj(){ return {title:setlist?setlist.title:"Set list", description:"", items:(setlist?setlist.items:[]).map((it,i)=> i===idx?{name:it.name, ...curSetupObj()}:it)}; } +let shareKind="p"; +function shareText(){ return shareKind==="p" ? currentPatch() : setlistToCode(shareSetlistObj()); } +function shareUrl(){ return location.origin+location.pathname+"#"+shareKind+"="+(shareKind==="p"?encodeURIComponent(currentPatch()):setlistToCode(shareSetlistObj())); } +function refreshShare(){ $("shareLink").value=shareUrl(); $("shareSeg").querySelectorAll("button").forEach(b=>b.classList.toggle("active",b.dataset.k===shareKind)); } +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"); } +$("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 ✓")); +$("shareLoad").onclick=()=>{ const v=($("sharePaste").value||"").trim(); if(!v){ flashShare("Paste a string or link"); return; } + if(loadFromHash(/[#?&](p|sl)=/.test(v)?v:("#p="+v))){ closeSheets(); } else flashShare("✗ not valid"); }; +$("shareDone").onclick=closeSheets; +$("shareBtn").onclick=openShareSheet; + /* ========================= SET-LIST / TRACK DROPDOWNS ======================== */ function opt(v,t){ const o=document.createElement("option"); o.value=v; o.textContent=t; return o; } function og(label){ const g=document.createElement("optgroup"); g.label=label; return g; } @@ -760,13 +830,13 @@ function loadFromHash(text){ /* ========================= HELP TOUR ========================================= */ const TOUR=[ + {sel:"#utilrow", title:"Controls", text:"Volume (soft p → loud f), ◐ light/dark theme, ⛶ full screen, the ↑ share menu, and ? to replay this tour anytime."}, {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:"#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:"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:"#trackpanel", title:"Track settings", text:"Right here: bar count, what happens at the end (loop / stop / next), a tempo ramp and practice gaps — plus copy or paste this track's share string."}, + {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:"#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."}, ]; let tstep=0; @@ -797,7 +867,7 @@ $("bPrev").onclick=()=>gotoItem(idx-1,state.running); $("bNext").onclick=()=>got $("bUp").onclick=()=>nudge(+1); $("bDown").onclick=()=>nudge(-1); $("bUp10").onclick=()=>nudge(+10); $("bDn10").onclick=()=>nudge(-10); $("vol").oninput=(e)=>{ state.volume=(+e.target.value)/100; if(masterGain) masterGain.gain.value=state.volume; saveState(); }; -$("sessbar").addEventListener("click",(e)=>{ if(sessionActive) e.preventDefault(); }); +$("sessLink").addEventListener("click",(e)=>{ if(sessionActive) e.preventDefault(); }); // don't lose an in-progress session /*@BUILD:include:src/chrome.js@*/