pm-mobile: top logo, tap/number/wheel tempo, transport reorder, Journal, no staff

- VARASYS logo (tagline-on-the-side lockup) moved to the top, linking the
  Codeberg repo; deleted the bottom session-bar footer.
- Tempo plate is now [TAP] [big BPM] [thumbwheel]: TAP = tap-tempo, tap the
  number to type, drag the ridged wheel on the right to scrub. Removed the ♩
  note glyph next to the number.
- Repeat is a checkbox (with Tempo ramp / Practice gaps); expanding any of them
  now SHRINKS the transport buttons instead of scrolling the page (lanes scroll
  on their own if there are many).
- Transport reordered: row 1 = −10 / − / + / +10, row 2 = prev / play / practice
  / next. Added a Journal button (reaches the practice-sessions log; doubles as
  the live recording timer while practising).
- Removed the staff lines behind the lanes.
- New build markers @BUILD:logo-side-{dark,light}@ (assets added).

Engine untouched; conformance passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-06-07 14:35:59 -05:00
parent 36b7cacd3f
commit d27bf07069
4 changed files with 69 additions and 63 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -40,6 +40,8 @@ def build(name):
src = src.replace("@BUILD:favicon@", (A / "favicon.b64").read_text().strip()) src = src.replace("@BUILD:favicon@", (A / "favicon.b64").read_text().strip())
src = src.replace("@BUILD:logo-dark@", (A / "logo-dark.b64").read_text().strip()) src = src.replace("@BUILD:logo-dark@", (A / "logo-dark.b64").read_text().strip())
src = src.replace("@BUILD:logo-light@", (A / "logo-light.b64").read_text().strip()) src = src.replace("@BUILD:logo-light@", (A / "logo-light.b64").read_text().strip())
src = src.replace("@BUILD:logo-side-dark@", (A / "logo-side-dark.b64").read_text().strip())
src = src.replace("@BUILD:logo-side-light@", (A / "logo-side-light.b64").read_text().strip())
src = src.replace("@BUILD:bravura@", (A / "bravura.woff2.b64").read_text().strip()) # SMuFL music font subset (PM_E-2 notation) src = src.replace("@BUILD:bravura@", (A / "bravura.woff2.b64").read_text().strip()) # SMuFL music font subset (PM_E-2 notation)
assert "@BUILD:" not in src, f"unresolved build marker(s) remain in {name}" assert "@BUILD:" not in src, f"unresolved build marker(s) remain in {name}"
out = pathlib.Path("dist") / name out = pathlib.Path("dist") / name

View file

@ -55,12 +55,15 @@
#app{ position:fixed; inset:0; overflow:hidden; display:flex; flex-direction:column; align-items:center; --maxw:600px; --pad:14px; #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)) 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)); } max(12px,env(safe-area-inset-bottom)) max(var(--pad),env(safe-area-inset-left)); }
#top, #mid, #sessbar{ width:100%; max-width:var(--maxw); } #top, #mid{ 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)); } :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)); } } @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 ---- */
#top{ flex:0 0 auto; display:flex; flex-direction:column; gap:10px; } #top{ flex:0 0 auto; display:flex; flex-direction:column; gap:9px; }
#brandrow{ display:flex; align-items:center; }
#logoLink{ display:inline-flex; opacity:.9; }
.brandlogo{ height:clamp(17px,3.4vmin,24px); width:auto; display:block; }
.sels{ display:flex; gap:8px; align-items:flex-end; } .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{ 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 > span{ font-size:10px; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); padding-left:3px; }
@ -76,31 +79,36 @@
/* ---- middle: pulse + track panel + lanes + transport, centered as one block ---- */ /* ---- 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); } #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; width:100%; } #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 */ /* tempo plate: [TAP] [big BPM] [thumbwheel] — flashes on the beat */
#pulse{ position:relative; width:100%; padding:clamp(10px,2.4vmin,20px) 16px; border-radius:16px; #pulse{ position:relative; width:100%; padding:clamp(8px,1.8vmin,14px); border-radius:16px;
display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; display:flex; flex-direction:row; align-items:stretch; gap:clamp(8px,2vmin,16px);
border:1px solid var(--ring); background:linear-gradient(180deg, rgba(127,139,154,.06), transparent); 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; } transition:box-shadow .12s ease-out, border-color .12s ease-out, background .12s ease-out; }
#pulse.hit{ border-color:var(--cyan); box-shadow:0 0 22px var(--glow); background:rgba(10,179,247,.07); } #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); } #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 */ .tapbtn{ flex:0 0 auto; align-self:stretch; min-width:clamp(58px,16vmin,100px); border-radius:12px;
#bpm{ display:inline-flex; align-items:center; justify-content:center; gap:.12em; line-height:.82; } background:linear-gradient(180deg,var(--btn1),var(--btn2)); border:1px solid var(--btn-bd); color:var(--txt);
#bpmNum{ font-size:clamp(44px,14vmin,128px); font-weight:800; font-variant-numeric:tabular-nums; letter-spacing:-.01em; } font-size:clamp(13px,2.8vmin,18px); font-weight:600; letter-spacing:.14em; cursor:pointer;
.metmark{ display:inline-flex; align-items:center; gap:.12em; color:var(--muted); font-size:clamp(20px,5.6vmin,50px); font-weight:600; } box-shadow:0 3px 0 rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.06); }
.metmark .rhythm{ height:1.2em; width:auto; } .tapbtn:active{ transform:translateY(2px); box-shadow:0 1px 0 rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.06); }
.metmark::after{ content:"="; } #bpm{ flex:1 1 auto; display:flex; flex-direction:column; align-items:center; justify-content:center; line-height:.82; cursor:pointer; min-width:0; }
#bpmlab{ font-size:clamp(9px,1.8vmin,14px); letter-spacing:.18em; text-transform:uppercase; color:var(--muted); margin-top:.7em; opacity:.8; } #bpmNum{ font-size:clamp(44px,15vmin,120px); font-weight:800; font-variant-numeric:tabular-nums; letter-spacing:-.01em; }
#bpmIn{ display:none; width:64%; text-align:center; font:inherit; font-size:clamp(42px,13vmin,120px); font-weight:800; #bpmlab{ font-size:clamp(9px,1.8vmin,13px); letter-spacing:.2em; text-transform:uppercase; color:var(--muted); margin-top:.5em; opacity:.85; }
#bpmIn{ display:none; flex:1 1 auto; min-width:0; text-align:center; font:inherit; font-size:clamp(40px,13vmin,108px); font-weight:800;
background:transparent; color:var(--txt); border:none; border-bottom:2px solid var(--cyan); outline:none; font-variant-numeric:tabular-nums; -moz-appearance:textfield; } background:transparent; color:var(--txt); border:none; border-bottom:2px solid var(--cyan); outline:none; font-variant-numeric:tabular-nums; -moz-appearance:textfield; }
#bpmIn::-webkit-outer-spin-button, #bpmIn::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; } #bpmIn::-webkit-outer-spin-button, #bpmIn::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; }
/* thumbwheel encoder — drag up/down to scrape the tempo */
#wheel{ flex:0 0 auto; align-self:stretch; width:clamp(30px,7.5vmin,46px); border-radius:11px; cursor:ns-resize; touch-action:none;
border:1px solid var(--btn-bd);
background:repeating-linear-gradient(to bottom, rgba(0,0,0,.16) 0 1px, transparent 1px 6px),
linear-gradient(to right, rgba(0,0,0,.26), rgba(255,255,255,.14) 48%, rgba(0,0,0,.26)), var(--field-bg);
box-shadow:inset 0 0 6px rgba(0,0,0,.25); }
#wheel:active{ border-color:var(--cyan); }
#meterline{ font-size:clamp(12px,2.1vmin,16px); color:var(--muted); text-align:center; min-height:1.2em; letter-spacing:.02em; } #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 ---- */ /* ---- editable lanes (scroll if many) + track panel below ---- */
#detail{ flex:0 1 auto; width:100%; max-height:32vh; overflow-y:auto; display:flex; flex-direction:column; gap:8px; padding:2px 0; } #detail{ flex:0 1 auto; width:100%; display:flex; flex-direction:column; gap:8px; padding:2px 0; min-height:0; }
#lanes{ display:flex; flex-direction:column; gap:6px; position:relative; } #lanes{ display:flex; flex-direction:column; gap:6px; max-height:34vh; overflow-y:auto; }
#lanes::before{ content:""; position:absolute; left:-2px; right:-2px; top:7%; bottom:7%; pointer-events:none; z-index:0;
background:repeating-linear-gradient(to bottom, var(--staff) 0 1.5px, transparent 1.5px 25%); }
#lanes > *{ position:relative; z-index:1; }
#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{ 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 .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 label{ display:flex; align-items:center; gap:6px; white-space:nowrap; }
@ -153,14 +161,20 @@
.chip.feat{ font-size:clamp(10px,1.8vmin,13px); color:var(--muted); background:var(--chip-bg); border:1px solid var(--chip-bd); border-radius:7px; padding:3px 8px; white-space:nowrap; } .chip.feat{ font-size:clamp(10px,1.8vmin,13px); color:var(--muted); background:var(--chip-bg); border:1px solid var(--chip-bd); border-radius:7px; padding:3px 8px; white-space:nowrap; }
.chip.feat.r{ border-color:var(--cyan); color:var(--cyan); } .chip.feat.g{ border-color:var(--amber); color:var(--amber); } .chip.feat.r{ border-color:var(--cyan); color:var(--cyan); } .chip.feat.g{ border-color:var(--amber); color:var(--amber); }
/* ---- transport ---- */ /* ---- transport: tempo row (10//+/+10) then nav+play row (prev/play/practice/next) ---- */
/* transport grows to fill the space freed by the compact tempo plate */ /* grows to fill freed space; SHRINKS (rather than the page scrolling) when the panel expands */
#transport{ flex:1 1 auto; max-height:clamp(150px,42vh,300px); display:flex; align-items:stretch; justify-content:center; width:100%; padding-top:6px; } #transport{ flex:1 1 auto; min-height:0; max-height:clamp(150px,42vh,300px); display:flex; flex-direction:column; align-items:stretch; width:100%; padding-top:6px; gap:clamp(6px,1.4vmin,11px); }
.tgrid{ display:grid; width:100%; height:100%; gap:clamp(7px,1.7vmin,14px); .tgrid{ display:grid; width:100%; flex:1 1 auto; min-height:0; 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"; } grid-template-columns:1fr 1.5fr 1.5fr 1fr; grid-template-rows:1fr 1fr; grid-template-areas:"dn10 dn up up10" "prev play prac next"; }
.tbtn{ background:linear-gradient(180deg,var(--btn1),var(--btn2)); color:var(--txt); border:1px solid var(--btn-bd); .tbtn{ background:linear-gradient(180deg,var(--btn1),var(--btn2)); color:var(--txt); border:1px solid var(--btn-bd);
border-radius:14px; height:auto; min-height:46px; font-size:clamp(18px,4.4vmin,30px); cursor:pointer; border-radius:14px; height:auto; min-height:42px; 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; } 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; }
.journal{ flex:0 0 auto; width:100%; height:clamp(34px,7vmin,46px); border-radius:11px; cursor:pointer;
background:rgba(127,139,154,.10); border:1px solid var(--panel-bd); color:var(--muted); font-size:13px;
display:flex; align-items:center; justify-content:center; gap:8px; }
.journal.rec{ background:rgba(192,57,43,.12); border-color:#c0392b; color:var(--txt); cursor:default; }
.journal .dotrec{ width:9px; height:9px; border-radius:50%; background:#e0493a; box-shadow:0 0 8px #e0493a; display:none; }
.journal.rec .dotrec{ display:inline-block; }
.tbtn small{ font-size:clamp(8px,1.5vmin,11px); letter-spacing:.1em; color:inherit; opacity:.85; } .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); } .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} #bDn10{grid-area:dn10} #bPrev{grid-area:prev} #bNext{grid-area:next} #bUp10{grid-area:up10}
@ -182,22 +196,10 @@
grid-template-areas:"stage detail" "transport detail"; } grid-template-areas:"stage detail" "transport detail"; }
#stage{ grid-area:stage; align-self:center; } #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; } #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; } #transport{ grid-area:transport; align-self:end; max-height:none; }
#pulse{ width:clamp(110px,40vmin,360px); height:clamp(110px,40vmin,360px); } .tbtn{ height:clamp(38px,12vmin,62px); }
.tbtn{ height:clamp(40px,13vmin,68px); }
} }
/* ---- session bar ---- */
#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; 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; } [data-theme="light"] .logo-dark{ display:none; } [data-theme="dark"] .logo-light{ display:none; }
/* ---- bottom sheet (lane editor) ---- */ /* ---- bottom sheet (lane editor) ---- */
@ -250,6 +252,9 @@
<div id="app"> <div id="app">
<div id="top"> <div id="top">
<div id="brandrow">
<a id="logoLink" href="https://codeberg.org/VARASYS/metronome" target="_blank" rel="noopener" title="VARASYS PolyMeter — source on Codeberg"><img class="brandlogo logo-dark" src="data:image/png;base64,@BUILD:logo-side-dark@" alt="VARASYS PolyMeter" /><img class="brandlogo logo-light" src="data:image/png;base64,@BUILD:logo-side-light@" alt="VARASYS PolyMeter" /></a>
</div>
<div class="trow" id="utilrow"> <div class="trow" id="utilrow">
<div class="vol"><span class="dyn" aria-hidden="true">p</span><input id="vol" type="range" min="0" max="100" value="85" aria-label="Volume" /><span class="dyn" aria-hidden="true">f</span></div> <div class="vol"><span class="dyn" aria-hidden="true">p</span><input id="vol" type="range" min="0" max="100" value="85" aria-label="Volume" /><span class="dyn" aria-hidden="true">f</span></div>
<div class="icon" id="shareBtn" title="Share / paste" aria-label="Share or paste"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="M8 7l4-4 4 4"/><path d="M5 12v8h14v-8"/></svg></div> <div class="icon" id="shareBtn" title="Share / paste" aria-label="Share or paste"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="M8 7l4-4 4 4"/><path d="M5 12v8h14v-8"/></svg></div>
@ -267,9 +272,10 @@
<div id="mid"> <div id="mid">
<div id="stage"> <div id="stage">
<div id="pulse"> <div id="pulse">
<div id="bpm"><span id="bpmMark" class="metmark" aria-hidden="true"></span><span id="bpmNum">120</span></div> <button id="bTapBtn" class="tapbtn" title="Tap tempo">TAP</button>
<div id="bpm"><span id="bpmNum">120</span><span id="bpmlab">BPM</span></div>
<input id="bpmIn" type="number" inputmode="numeric" min="30" max="300" /> <input id="bpmIn" type="number" inputmode="numeric" min="30" max="300" />
<div id="bpmlab">per minute</div> <div id="wheel" title="Drag to set tempo" aria-label="Tempo wheel"></div>
</div> </div>
<div id="meterline"></div> <div id="meterline"></div>
</div> </div>
@ -289,21 +295,17 @@
<div id="transport"> <div id="transport">
<div class="tgrid"> <div class="tgrid">
<button class="tbtn" id="bDn10" title="Tempo 10">10</button> <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" id="bDown" title="Tempo 1"></button>
<button class="tbtn" id="bUp" title="Tempo +1">+</button>
<button class="tbtn" id="bUp10" title="Tempo +10">+10</button>
<button class="tbtn" id="bPrev" title="Previous track"></button>
<button class="tbtn play" id="bPlay" title="Play / Stop"><small>PLAY</small></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 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> <button class="tbtn" id="bNext" title="Next track"></button>
</div> </div>
<button id="bJournal" class="journal"><span class="dotrec"></span><span id="jText">Journal →</span></button>
</div> </div>
</div> </div>
<div id="sessbar">
<a id="sessLink" href="/mobile-sessions.html"><span class="dotrec"></span><span id="sessText">Practice sessions →</span></a>
<a id="repoLink" href="https://codeberg.org/VARASYS/metronome" target="_blank" rel="noopener" title="Source &amp; docs on Codeberg — VARASYS PolyMeter"><img class="rlogo logo-dark" src="data:image/png;base64,@BUILD:logo-dark@" alt="VARASYS" /><img class="rlogo logo-light" src="data:image/png;base64,@BUILD:logo-light@" alt="VARASYS" /></a>
</div>
</div> </div>
<!-- lane editor sheet --> <!-- lane editor sheet -->
@ -758,10 +760,10 @@ function renderTransport(){
$("bPlay").innerHTML=(onAny?"■<small>STOP</small>":"▶<small>PLAY</small>"); $("bPlay").classList.toggle("on",onAny); $("bPlay").innerHTML=(onAny?"■<small>STOP</small>":"▶<small>PLAY</small>"); $("bPlay").classList.toggle("on",onAny);
const pr=state.running&&sessionActive; const pr=state.running&&sessionActive;
$("bPrac").innerHTML=(pr?"❚❚<small>PAUSE</small>":"⦿<small>PRACTICE</small>"); $("bPrac").classList.toggle("on",pr); } $("bPrac").innerHTML=(pr?"❚❚<small>PAUSE</small>":"⦿<small>PRACTICE</small>"); $("bPrac").classList.toggle("on",pr); }
function renderSessionBar(){ const bar=$("sessbar"), n=lsGet(LS_SESSIONS,[]).length; function renderSessionBar(){ const bar=$("bJournal"), n=lsGet(LS_SESSIONS,[]).length; // the Journal button doubles as live session status
if(sessionActive){ bar.classList.add("rec"); const segs=session.segments.length+(trackSegStart?1:0); if(sessionActive){ bar.classList.add("rec"); const segs=session.segments.length+(trackSegStart?1:0);
$("sessText").textContent="Practising · session "+fmt((Date.now()-session.at)/1000)+" · "+segs+" track"+(segs===1?"":"s"); } $("jText").textContent="Recording "+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+")"):"")+" →"; } } else { bar.classList.remove("rec"); $("jText").textContent=(lastSaved?"✓ saved · ":"")+"Journal"+(n?(" ("+n+")"):"")+" →"; } }
function renderAll(){ renderInfo(); renderTransport(); saveState(); } function renderAll(){ renderInfo(); renderTransport(); saveState(); }
let lastBeatKey=-1, pulseTimer=null; let lastBeatKey=-1, pulseTimer=null;
@ -786,12 +788,13 @@ function openBpmEdit(){ editingBpm=true; const i=$("bpmIn"); $("bpm").style.disp
function closeBpmEdit(commit){ const i=$("bpmIn"); if(commit){ const v=parseInt(i.value,10); if(v){ ramp.on=false; setBpm(v); } } editingBpm=false; i.style.display="none"; $("bpm").style.display=""; renderAll(); } function closeBpmEdit(commit){ const i=$("bpmIn"); if(commit){ const v=parseInt(i.value,10); if(v){ ramp.on=false; setBpm(v); } } editingBpm=false; i.style.display="none"; $("bpm").style.display=""; renderAll(); }
$("bpmIn").addEventListener("keydown",(e)=>{ if(e.key==="Enter"){ closeBpmEdit(true); } else if(e.key==="Escape"){ closeBpmEdit(false); } }); $("bpmIn").addEventListener("keydown",(e)=>{ if(e.key==="Enter"){ closeBpmEdit(true); } else if(e.key==="Escape"){ closeBpmEdit(false); } });
$("bpmIn").addEventListener("blur",()=>{ if(editingBpm) closeBpmEdit(true); }); $("bpmIn").addEventListener("blur",()=>{ if(editingBpm) closeBpmEdit(true); });
(function(){ const p=$("pulse"); let dragging=false, moved=false, lpFired=false, startY=0, startBpm=120, lpTimer=null; $("bTapBtn").onclick=tapTempo; // TAP button (left of the BPM) — tap to set
p.addEventListener("pointerdown",(e)=>{ if(editingBpm) return; dragging=true; moved=false; lpFired=false; startY=e.clientY; startBpm=state.bpm; $("bpm").onclick=()=>{ if(!editingBpm) openBpmEdit(); }; // tap the number to type
p.setPointerCapture(e.pointerId); lpTimer=setTimeout(()=>{ lpFired=true; openBpmEdit(); },450); }); (function(){ const w=$("wheel"); let dragging=false, startY=0, startBpm=120; // thumbwheel (right) — drag to scrub
p.addEventListener("pointermove",(e)=>{ if(!dragging) return; const dy=startY-e.clientY; if(Math.abs(dy)>6){ moved=true; clearTimeout(lpTimer); ramp.on=false; setBpm(startBpm+dy*0.5); renderAll(); } }); w.addEventListener("pointerdown",(e)=>{ dragging=true; startY=e.clientY; startBpm=state.bpm; w.setPointerCapture(e.pointerId); e.preventDefault(); });
p.addEventListener("pointerup",(e)=>{ if(!dragging) return; dragging=false; clearTimeout(lpTimer); try{p.releasePointerCapture(e.pointerId);}catch(_){} if(!moved && !lpFired) tapTempo(); }); w.addEventListener("pointermove",(e)=>{ if(!dragging) return; ramp.on=false; setBpm(startBpm+(startY-e.clientY)*0.5); renderAll(); });
p.addEventListener("pointercancel",()=>{ dragging=false; clearTimeout(lpTimer); }); w.addEventListener("pointerup",(e)=>{ dragging=false; try{w.releasePointerCapture(e.pointerId);}catch(_){} });
w.addEventListener("pointercancel",()=>{ dragging=false; });
})(); })();
/* ========================= PERSIST / RESTORE STATE =========================== */ /* ========================= PERSIST / RESTORE STATE =========================== */
@ -833,7 +836,7 @@ const TOUR=[
{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:"#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:"#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:"#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."}, {sel:"#bJournal", title:"Practice journal", text:"Opens your saved practice sessions — notes and a per-track breakdown across days. While Practice is recording, this shows the live session timer."},
]; ];
let tstep=0; let tstep=0;
function startTour(){ tstep=0; $("tour").classList.add("open"); showTour(); } function startTour(){ tstep=0; $("tour").classList.add("open"); showTour(); }
@ -863,7 +866,7 @@ $("bPrev").onclick=()=>gotoItem(idx-1,state.running); $("bNext").onclick=()=>got
$("bUp").onclick=()=>nudge(+1); $("bDown").onclick=()=>nudge(-1); $("bUp").onclick=()=>nudge(+1); $("bDown").onclick=()=>nudge(-1);
$("bUp10").onclick=()=>nudge(+10); $("bDn10").onclick=()=>nudge(-10); $("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(); }; $("vol").oninput=(e)=>{ state.volume=(+e.target.value)/100; if(masterGain) masterGain.gain.value=state.volume; saveState(); };
$("sessLink").addEventListener("click",(e)=>{ if(sessionActive) e.preventDefault(); }); // don't lose an in-progress session $("bJournal").addEventListener("click",()=>{ if(!sessionActive) location.href="/mobile-sessions.html"; }); // mid-session: just shows the live timer
/*@BUILD:include:src/chrome.js@*/ /*@BUILD:include:src/chrome.js@*/
@ -902,7 +905,6 @@ if(location.hash && /(p|sl)=/.test(location.hash)) loadFromHash(location.hash);
else restoreState(); else restoreState();
if(!setlist){ slKey="b0"; setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); } if(!setlist){ slKey="b0"; setlist=BUILTIN[0]; idx=0; loadSetup(setlist.items[0]); }
buildSetlistOptions(); buildTrackOptions(); buildSetlistOptions(); buildTrackOptions();
$("bpmMark").innerHTML=rhythmSVG(1); // ♩ in the "♩ = N" tempo marking
$("vol").value=Math.round(state.volume*100); if(masterGain) masterGain.gain.value=state.volume; $("vol").value=Math.round(state.volume*100); if(masterGain) masterGain.gain.value=state.volume;
renderAll(); renderSessionBar(); renderAll(); renderSessionBar();
requestAnimationFrame(draw); requestAnimationFrame(draw);