Timers: show stopwatch + countdown in the BPM display; countdown blank-default, hⓂ️s input

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-24 18:35:58 -05:00
parent 72147f5b32
commit 285d78b499

View file

@ -55,6 +55,7 @@
.card h2 { font-size:11px; text-transform:uppercase; letter-spacing:1.4px; color:var(--muted); margin:0 0 14px; } .card h2 { font-size:11px; text-transform:uppercase; letter-spacing:1.4px; color:var(--muted); margin:0 0 14px; }
.display { background:#0a0d11; border:1px solid #000; border-radius:8px; padding:8px 14px; text-align:center; box-shadow:inset 0 2px 10px rgba(0,0,0,.7); } .display { background:#0a0d11; border:1px solid #000; border-radius:8px; padding:8px 14px; text-align:center; box-shadow:inset 0 2px 10px rgba(0,0,0,.7); }
.display .big { font-family:"Courier New",monospace; font-weight:700; font-size:40px; color:#ffd166; letter-spacing:3px; text-shadow:0 0 12px rgba(255,209,102,.5); } .display .big { font-family:"Courier New",monospace; font-weight:700; font-size:40px; color:#ffd166; letter-spacing:3px; text-shadow:0 0 12px rgba(255,209,102,.5); }
.display .dtimers { font-family:"Courier New",monospace; font-size:13px; color:#4dd0e1; margin:3px 0; display:flex; gap:14px; justify-content:center; flex-wrap:wrap; }
.display .ctx { font-family:"Courier New",monospace; font-size:12px; color:#4dd0e1; min-height:15px; line-height:1.3; } .display .ctx { font-family:"Courier New",monospace; font-size:12px; color:#4dd0e1; min-height:15px; line-height:1.3; }
.display .ctx.muted-cue { color:#ffb454; } .display .ctx.muted-cue { color:#ffb454; }
.knob { margin-bottom:10px; } .knob { margin-bottom:10px; }
@ -183,6 +184,10 @@
<div style="flex:0 0 190px; min-width:170px"> <div style="flex:0 0 190px; min-width:170px">
<div class="display"> <div class="display">
<div class="big" id="bpmDisplay">120</div> <div class="big" id="bpmDisplay">120</div>
<div class="dtimers">
<span title="elapsed (stopwatch)"><span id="elapsedVal">0:00</span></span>
<span id="countWrap" title="countdown" hidden><span id="countVal" class="tval">0:00</span></span>
</div>
<div class="ctx" id="ctxDisplay">&nbsp;</div> <div class="ctx" id="ctxDisplay">&nbsp;</div>
</div> </div>
<div class="btnrow" style="margin-top:10px"><button class="primary" id="startBtn">▶ Start</button><button id="tapBtn">Tap</button></div> <div class="btnrow" style="margin-top:10px"><button class="primary" id="startBtn">▶ Start</button><button id="tapBtn">Tap</button></div>
@ -213,13 +218,11 @@
</div> </div>
<div class="checkrow" style="margin:12px 0 6px; gap:8px"><b style="font-size:11px; text-transform:uppercase; letter-spacing:1px; color:var(--muted)">Timers</b><span class="hint" style="margin:0">run while playing</span></div> <div class="checkrow" style="margin:12px 0 6px; gap:8px"><b style="font-size:11px; text-transform:uppercase; letter-spacing:1px; color:var(--muted)">Timers</b><span class="hint" style="margin:0">run while playing</span></div>
<div class="row" style="gap:10px; align-items:center"> <div class="row" style="gap:10px; align-items:center">
<label style="font-size:12px">Elapsed</label> <label style="font-size:12px">Elapsed (stopwatch)</label>
<span class="tval" id="elapsedVal">0:00</span> <button class="iconbtn" id="elapsedReset" title="reset elapsed"></button>
<button class="iconbtn" id="elapsedReset" title="reset elapsed timer"></button>
</div> </div>
<div class="row" style="gap:10px; align-items:center; margin-top:6px"> <div class="row" style="gap:10px; align-items:center; margin-top:6px">
<label style="font-size:12px" title="0 = no countdown">Countdown <input type="number" class="num" id="countMin" min="0" max="120" value="5"> min</label> <label style="font-size:12px">Countdown <input type="text" class="txt" id="countTime" placeholder="m:ss" title="blank = off · h:mm:ss, m:ss, or plain minutes" style="width:80px; text-align:center"></label>
<span class="tval" id="countVal">5:00</span>
<button class="iconbtn" id="countReset" title="reset countdown"></button> <button class="iconbtn" id="countReset" title="reset countdown"></button>
</div> </div>
</div> </div>
@ -951,8 +954,18 @@ function drawLoop() {
/* ========================================================================= /* =========================================================================
PRACTICE TIMERS — advance only while the metronome is running PRACTICE TIMERS — advance only while the metronome is running
========================================================================= */ ========================================================================= */
const timers = { elapsedMs: 0, totalMs: 5 * 60000, remainingMs: 5 * 60000, last: 0 }; const timers = { elapsedMs: 0, totalMs: 0, remainingMs: 0, last: 0 }; // countdown off by default
function fmtClock(ms) { const neg = ms < 0; const s = Math.round(Math.abs(ms) / 1000); return (neg ? "-" : "") + Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); } function fmtClock(ms) { const neg = ms < 0; const s = Math.round(Math.abs(ms) / 1000); return (neg ? "-" : "") + Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); }
// Parse a countdown duration: blank = off; "h:mm:ss" / "m:ss" (seconds-last); a plain number = minutes.
function parseTime(str) {
str = (str || "").trim(); if (!str) return 0;
if (!str.includes(":")) { const m = parseFloat(str); return isFinite(m) && m > 0 ? Math.round(m * 60000) : 0; }
const p = str.split(":").map((x) => parseInt(x, 10) || 0);
let h = 0, m = 0, s = 0;
if (p.length >= 3) { h = p[0]; m = p[1]; s = p[2]; } else { m = p[0]; s = p[1]; }
const ms = ((h * 60 + m) * 60 + s) * 1000;
return ms > 0 ? ms : 0;
}
function tickTimers() { function tickTimers() {
const now = Date.now(); const now = Date.now();
const dt = timers.last ? Math.min(now - timers.last, 1000) : 0; // clamp so backgrounded gaps don't jump const dt = timers.last ? Math.min(now - timers.last, 1000) : 0; // clamp so backgrounded gaps don't jump
@ -965,8 +978,10 @@ function tickTimers() {
} }
function renderTimers() { function renderTimers() {
$("elapsedVal").textContent = fmtClock(timers.elapsedMs); $("elapsedVal").textContent = fmtClock(timers.elapsedMs);
const off = timers.totalMs <= 0;
$("countWrap").hidden = off; // hide countdown when off
if (off) return;
const cd = $("countVal"); const cd = $("countVal");
if (timers.totalMs <= 0) { cd.textContent = "off"; cd.className = "tval"; return; }
cd.textContent = fmtClock(timers.remainingMs); cd.textContent = fmtClock(timers.remainingMs);
cd.classList.toggle("over", timers.remainingMs <= 0); // overtime cd.classList.toggle("over", timers.remainingMs <= 0); // overtime
cd.classList.toggle("low", timers.remainingMs > 0 && timers.remainingMs <= 10000); // almost up cd.classList.toggle("low", timers.remainingMs > 0 && timers.remainingMs <= 10000); // almost up
@ -1037,7 +1052,7 @@ $("rampStart").addEventListener("input", (e) => ramp.startBpm = +e.target.value)
$("rampAmt").addEventListener("input", (e) => ramp.amount = +e.target.value); $("rampAmt").addEventListener("input", (e) => ramp.amount = +e.target.value);
$("rampEvery").addEventListener("input", (e) => ramp.everyBars = +e.target.value); $("rampEvery").addEventListener("input", (e) => ramp.everyBars = +e.target.value);
$("addMeterBtn").addEventListener("click", () => addMeter("4", 1, "claves")); $("addMeterBtn").addEventListener("click", () => addMeter("4", 1, "claves"));
$("countMin").addEventListener("input", (e) => { timers.totalMs = (+e.target.value || 0) * 60000; timers.remainingMs = timers.totalMs; renderTimers(); }); $("countTime").addEventListener("input", (e) => { timers.totalMs = parseTime(e.target.value); timers.remainingMs = timers.totalMs; renderTimers(); });
$("elapsedReset").addEventListener("click", () => { timers.elapsedMs = 0; renderTimers(); }); $("elapsedReset").addEventListener("click", () => { timers.elapsedMs = 0; renderTimers(); });
$("countReset").addEventListener("click", () => { timers.remainingMs = timers.totalMs; renderTimers(); }); $("countReset").addEventListener("click", () => { timers.remainingMs = timers.totalMs; renderTimers(); });
$("trayMenuBtn").addEventListener("click", (e) => { e.stopPropagation(); $("trayMenu").hidden = !$("trayMenu").hidden; }); $("trayMenuBtn").addEventListener("click", (e) => { e.stopPropagation(); $("trayMenu").hidden = !$("trayMenu").hidden; });