pm-mobile: note-value picker, top-bar reorg, share menu, end rework, scaling layout
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) <noreply@anthropic.com>
This commit is contained in:
parent
1f5fdeaba9
commit
1a66eb962d
1 changed files with 163 additions and 93 deletions
256
mobile.html
256
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 @@
|
|||
|
||||
<div id="app">
|
||||
<div id="top">
|
||||
<div class="sels">
|
||||
<label class="sel"><span>Set list</span><select id="slSel"></select></label>
|
||||
<label class="sel"><span>Track</span><select id="trkSel"></select></label>
|
||||
</div>
|
||||
<div class="trow">
|
||||
<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="icon" id="saveBtn" title="Save & library" aria-label="Save and library">↧</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="helpBtn" title="Help" aria-label="Help">?</div>
|
||||
<div class="icon" id="themeBtn" title="Theme" aria-label="Theme">◐</div>
|
||||
<div class="icon" id="fsBtn" title="Full screen" aria-label="Full screen">⛶</div>
|
||||
</div>
|
||||
<div class="sels">
|
||||
<label class="sel"><span>Set list</span><select id="slSel"></select></label>
|
||||
<label class="sel"><span>Track</span><select id="trkSel"></select></label>
|
||||
<div class="icon" id="saveBtn" title="Save & library" aria-label="Save and library"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"><path d="M5 4 H15 L19 8 V20 H5 Z"/><path d="M8.5 4 V8 H13.5 V4"/><rect x="8" y="13" width="8" height="7"/></svg></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mid">
|
||||
|
|
@ -240,16 +265,17 @@
|
|||
</div>
|
||||
<div id="detail">
|
||||
<div id="trackpanel">
|
||||
<div class="tp-grid">
|
||||
<label>Repeat <input id="ipBars" type="number" inputmode="numeric" min="0" max="999" /> bars</label>
|
||||
<label>End <select id="ipEnd"><option value="loop">Loop</option><option value="stop">Stop</option><option value="next">Next</option></select></label>
|
||||
<label class="tp-chk"><input type="checkbox" id="ipRamp" /> Ramp</label>
|
||||
<label class="tp-chk"><input type="checkbox" id="ipGap" /> Gap</label>
|
||||
<span id="ipMsg" class="tp-msg"></span>
|
||||
<div class="tp-row">
|
||||
<label>Play <input id="ipBars" type="number" inputmode="numeric" min="0" max="999" /> bars,</label>
|
||||
<label id="ipThen">then <select id="ipEnd"><option value="stop">stop</option><option value="next">next track</option><option value="prev">prev track</option></select></label>
|
||||
<span id="ipLoop" class="tp-loop">then loop</span>
|
||||
</div>
|
||||
<div class="tp-sub off" id="ipRampRow"><b>Ramp</b> start <input id="ipRampStart" type="number" min="30" max="300" /> → +<input id="ipRampAmt" type="number" min="1" max="50" /> every <input id="ipRampEvery" type="number" min="1" max="64" /> bars</div>
|
||||
<div class="tp-sub off" id="ipGapRow"><b>Gap</b> <input id="ipGapPlay" type="number" min="1" max="32" /> play / <input id="ipGapMute" type="number" min="1" max="32" /> mute bars</div>
|
||||
<div class="tp-str"><input id="ipStr" type="text" spellcheck="false" autocomplete="off" /><button id="ipCopy" class="tp-btn">Copy</button><button id="ipPaste" class="tp-btn">Apply</button></div>
|
||||
<div class="tp-row">
|
||||
<label class="tp-chk"><input type="checkbox" id="ipRamp" /> Tempo ramp</label>
|
||||
<label class="tp-chk"><input type="checkbox" id="ipGap" /> Practice gaps</label>
|
||||
</div>
|
||||
<div class="tp-sub off" id="ipRampRow"><input id="ipRampStart" type="number" min="30" max="300" /> → +<input id="ipRampAmt" type="number" min="1" max="50" /> bpm / <input id="ipRampEvery" type="number" min="1" max="64" /> bars</div>
|
||||
<div class="tp-sub off" id="ipGapRow"><input id="ipGapPlay" type="number" min="1" max="32" /> play / <input id="ipGapMute" type="number" min="1" max="32" /> mute bars</div>
|
||||
</div>
|
||||
<div id="lanes"></div>
|
||||
</div>
|
||||
|
|
@ -267,7 +293,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<a id="sessbar" href="/mobile-sessions.html"><span class="dotrec"></span><span id="sessText">Practice sessions →</span></a>
|
||||
<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 & 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>
|
||||
|
||||
<!-- lane editor sheet -->
|
||||
|
|
@ -277,10 +306,7 @@
|
|||
<h2>Edit lane</h2>
|
||||
<label for="lsSound">Sound</label><select id="lsSound"></select>
|
||||
<label for="lsGroup">Grouping — beats per bar (e.g. 4, or 2+2+3)</label><input id="lsGroup" type="text" inputmode="text" autocomplete="off" />
|
||||
<div class="lrow">
|
||||
<label class="half">Subdivision<select id="lsSub"><option>1</option><option>2</option><option>3</option><option>4</option></select></label>
|
||||
<label class="chk"><input type="checkbox" id="lsSwing" /> Swing</label>
|
||||
</div>
|
||||
<label>Note value</label><div id="lsNotes" class="noterow"></div>
|
||||
<div class="lrow">
|
||||
<label class="chk"><input type="checkbox" id="lsPoly" /> Polymeter</label>
|
||||
<label class="chk"><input type="checkbox" id="lsMute" /> Mute lane</label>
|
||||
|
|
@ -290,6 +316,26 @@
|
|||
<div class="lfoot"><button id="lsDel" class="lbtn danger">Delete lane</button><button id="lsDone" class="lbtn">Done</button></div>
|
||||
</div>
|
||||
|
||||
<!-- note-value picker (opened by tapping a lane's rhythm icon) -->
|
||||
<div id="noteSheet">
|
||||
<div class="grab"></div>
|
||||
<h2>Note value</h2>
|
||||
<div id="noteOpts" class="noterow"></div>
|
||||
</div>
|
||||
|
||||
<!-- share sheet: share a track or set list as a link, or paste a string to load -->
|
||||
<div id="shareSheet">
|
||||
<div class="grab"></div>
|
||||
<h2>Share</h2>
|
||||
<div class="seg" id="shareSeg"><button data-k="p" class="active">This track</button><button data-k="sl">This set list</button></div>
|
||||
<label>Shareable link</label>
|
||||
<input id="shareLink" type="text" readonly onfocus="this.select()" />
|
||||
<div class="lrow"><button id="shareCopy" class="lbtn">Copy link</button><button id="shareCopyT" class="lbtn">Copy text</button><span id="shareMsg" class="savemsg"></span></div>
|
||||
<label for="sharePaste">Or paste a track string / link to load</label>
|
||||
<input id="sharePaste" type="text" autocomplete="off" placeholder="v1;t120;kick:4;… or a #p=/#sl= link" />
|
||||
<div class="lfoot"><button id="shareLoad" class="lbtn">Load</button><button id="shareDone" class="lbtn">Done</button></div>
|
||||
</div>
|
||||
|
||||
<!-- save & library sheet -->
|
||||
<div id="saveSheet">
|
||||
<div class="grab"></div>
|
||||
|
|
@ -363,9 +409,9 @@ function scheduler(){
|
|||
for(const m of meters){ while(m.nextTime<ahead){ scheduleMeterTick(m,m.nextTime); m.nextTime+=laneStepDur(m,m.tick); m.tick++; } }
|
||||
if(pendingAdvance){ pendingAdvance=false; setTimeout(handleEnd,0); }
|
||||
}
|
||||
function handleEnd(){
|
||||
if(curEnd==="stop"){ if(sessionActive&&state.running) pauseTrack(); else stopAudio(); } // stop at end of the bar count
|
||||
else gotoItem(idx+1,true); // "next" → advance the set list
|
||||
function handleEnd(){ // fires after segBars bars when an end action is set (loop = no action)
|
||||
if(curEnd==="stop"){ if(sessionActive&&state.running) pauseTrack(); else stopAudio(); }
|
||||
else if(typeof curEnd==="number") gotoItem(idx+curEnd,true); // +1 next track, -1 prev track
|
||||
}
|
||||
|
||||
/* ========================= PLAYER ============================================= */
|
||||
|
|
@ -465,21 +511,13 @@ function laneNoteValue(m){
|
|||
if(!any) return 1;
|
||||
return Math.max(1, spb/g);
|
||||
}
|
||||
// Small engraved rhythm figure (1=quarter, 2=8ths, 3=triplet, 4=16ths, 5/6/7=tuplets);
|
||||
// swung eighths render as the dotted-8th + 16th shuffle figure. Drawn as SVG.
|
||||
function rhythmSVG(n, swing){
|
||||
// Small engraved rhythm figure (1=quarter, 2=8ths, 3=triplet, 4=16ths, 5/6/7=tuplets). SVG.
|
||||
function rhythmSVG(n){
|
||||
n=Math.max(1,n|0);
|
||||
const baseY=15, topY=6, stemH=(baseY-topY-0.5).toFixed(1);
|
||||
const head=(cx)=>'<ellipse cx="'+cx.toFixed(1)+'" cy="'+baseY+'" rx="2.4" ry="1.8" transform="rotate(-20 '+cx.toFixed(1)+' '+baseY+')"/>';
|
||||
const stem=(sx)=>'<rect x="'+(sx-0.45).toFixed(2)+'" y="'+topY+'" width="0.9" height="'+stemH+'"/>';
|
||||
const beam=(x0,x1,y)=>'<rect x="'+x0.toFixed(2)+'" y="'+y+'" width="'+(x1-x0).toFixed(2)+'" height="1.7"/>';
|
||||
const wrap=(W,g)=>'<svg class="rhythm" viewBox="0 0 '+W+' 18" width="'+W+'" height="16" fill="currentColor" aria-hidden="true">'+g+'</svg>';
|
||||
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)
|
||||
+'<circle cx="'+(cx0+3.7).toFixed(1)+'" cy="'+(baseY-0.6)+'" r="0.95"/>';
|
||||
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+='<text x="'+(W/2).toFixed(1)+'" y="3.6" font-size="6" text-anchor="middle" font-style="italic" stroke="none">'+tup+'</text>';
|
||||
return wrap(W,g);
|
||||
return '<svg class="rhythm" viewBox="0 0 '+W+' 18" width="'+W+'" height="16" fill="currentColor" aria-hidden="true">'+g+'</svg>';
|
||||
}
|
||||
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?"<span class='polybadge' title='polyrhythm — "+m.beatsPerBar+" over "+ref+"'>↻"+m.beatsPerBar+":"+ref+"</span>":"";
|
||||
return "<span class='ln-name'>"+esc(m.sound)+"</span>"+rhythmSVG(eff,m.swing)+"<span class='lg'>"+esc(m.groupsStr)+(m.swing&&!swGlyph?" sw":"")+"</span>"+poly; }
|
||||
return "<span class='ln-name'>"+esc(m.sound)+"</span><span class='rh-host' title='Note value'>"+rhythmSVG(eff)+"</span><span class='lg'>"+esc(m.groupsStr)+"</span>"+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)+"<small>"+noteName(n)+"</small>"; 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@*/
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue