From 1a66eb962d22363d95896fd26f869bf19dabcd86 Mon Sep 17 00:00:00 2001 From: Me Here Date: Sun, 7 Jun 2026 13:05:31 -0500 Subject: [PATCH] pm-mobile: note-value picker, top-bar reorg, share menu, end rework, scaling layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lanes / note values: - Removed the swing toggle + shuffle glyph (swing == triplet in this engine, so it's redundant; `swing` stays in the DSL). Pick the note value by GRAPHIC: tap a lane's rhythm icon (or use the lane sheet) to choose quarter/eighth/triplet/ sixteenth/sextuplet — replaces the number dropdown; adds per-lane gain earlier. Top bar: - Utilities grouped on one row (volume p→f, share, theme, full screen, help); Set list + Track + a disk Save button on the row below. - Save icon is now a disk; help "?" replays the tour. Track end: - Dropped the nonsensical "repeat + loop". Now "Play N bars, then [stop / next track / prev track]"; 0 bars = loops forever. Honored at runtime. Share: - Removed inline Copy/Apply. New Share sheet (↑): toggle This track / This set list → shareable link (+ copy link / copy text), and paste a string/link to load. (setlistToCode added; multi-select tree is a follow-up.) Layout (rock-solid, pure phone↔tablet scaling): - Content capped to --maxw and centered; fixed (non-wrapping) track panel and rows so nothing re-flows as the screen grows — phone and tablet are the same layout, just scaled. Landscape now uses one 2-column layout at ALL heights (was falling back to portrait on tall tablets). Bigger margins in full screen. Inconspicuous VARASYS logo in the bottom bar links to the Codeberg repo. Engine untouched; conformance passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile.html | 256 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 163 insertions(+), 93 deletions(-) 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 → +
@@ -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@*/