From ba34f66c91ba535fc0c2f6e906004f5dcb339c36 Mon Sep 17 00:00:00 2001 From: b1rdmania <102524336+b1rdmania@users.noreply.github.com> Date: Fri, 26 Dec 2025 18:36:40 +0000 Subject: [PATCH] Add progress slider to share player. Updates /play with a seekable progress bar, time display, 70% default volume, and Wario engine copy. --- play.html | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++-- src/play.ts | 50 ++++++++++++++++++++++++ 2 files changed, 154 insertions(+), 3 deletions(-) diff --git a/play.html b/play.html index 2d8981e..ccdfc51 100644 --- a/play.html +++ b/play.html @@ -249,6 +249,96 @@ font-size: 13px; color: rgba(255, 255, 255, 0.65); } + + /* Progress (matches main page style) */ + .progress-container { + margin-top: 8px; + padding-top: 16px; + border-top: 1px solid rgba(255, 170, 68, 0.15); + display: none; + } + + .progress-time { + display: flex; + justify-content: space-between; + margin-bottom: 8px; + font-size: 0.85em; + color: #8f8f8f; + } + + .progress-bar-wrapper { + position: relative; + height: 44px; + display: flex; + align-items: center; + padding: calc(var(--spacing-unit) * 2) 0; + } + + .progress-bar { + width: 100%; + height: 6px; + -webkit-appearance: none; + appearance: none; + background: transparent; + border-radius: 3px; + outline: none; + cursor: pointer; + position: relative; + z-index: 2; + } + + .progress-bar::-webkit-slider-runnable-track { + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + } + + .progress-bar::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--color-accent-secondary); + cursor: pointer; + box-shadow: 0 0 8px rgba(255, 170, 68, 0.6), var(--shadow-sm); + margin-top: -7px; + transition: transform 100ms ease; + } + + .progress-bar:active::-webkit-slider-thumb { + transform: scale(1.15); + } + + .progress-bar::-moz-range-track { + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + } + + .progress-bar::-moz-range-thumb { + width: 20px; + height: 20px; + border-radius: 50%; + background: var(--color-accent-secondary); + cursor: pointer; + border: none; + box-shadow: 0 0 8px rgba(255, 170, 68, 0.6), var(--shadow-sm); + } + + .progress-fill { + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + height: 6px; + background: linear-gradient(90deg, var(--color-accent-secondary), rgba(255, 170, 68, 0.5)); + border-radius: 3px; + pointer-events: none; + z-index: 1; + width: 0%; + transition: width 0.1s linear; + } @@ -258,7 +348,7 @@
-

Synthesis Engine

+

Wario synthesis engine

The same Game Boy/NES render you hear on the main page.

@@ -267,7 +357,7 @@

Loading…

-

Tap play to run the shared MIDI through MOTIF’s synth engine.

+

Tap play to run the shared MIDI through the Wario engine.

@@ -275,13 +365,24 @@
- +
/play
Loading…
+
+
+ 0:00 + 0:00 +
+
+ +
+
+
+
Generate your own Home diff --git a/src/play.ts b/src/play.ts index 4343ae9..8b3e737 100644 --- a/src/play.ts +++ b/src/play.ts @@ -56,6 +56,11 @@ async function main(): Promise { const volumeEl = qs('volume') as HTMLInputElement; const generateOwn = qs('generateOwn') as HTMLAnchorElement; const shareHint = qs('shareHint'); + const progressContainer = qs('playProgressContainer') as HTMLElement; + const progressBar = qs('playProgressBar') as HTMLInputElement; + const progressFill = qs('playProgressFill') as HTMLElement; + const currentTimeEl = qs('playCurrentTime') as HTMLElement; + const durationEl = qs('playDuration') as HTMLElement; const { midiUrl: u, title } = buildMidiUrlFromParams(); @@ -76,6 +81,8 @@ async function main(): Promise { const motifEngine = new MotifEngine(); let events: NoteEvent[] | null = null; let isPlaying = false; + let progressInterval: number | null = null; + let durationSec = 0; statusEl.textContent = 'Loading MIDI…'; try { @@ -97,6 +104,32 @@ async function main(): Promise { stopBtn.disabled = !playing; } + function formatTime(seconds: number): string { + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; + } + + function updateProgress(): void { + const progress = motifEngine.getProgress(); + const current = motifEngine.getCurrentTime(); + progressBar.value = (progress * 100).toString(); + progressFill.style.width = `${progress * 100}%`; + currentTimeEl.textContent = formatTime(current); + } + + function startProgressUpdates(): void { + stopProgressUpdates(); + progressInterval = window.setInterval(updateProgress, 100); + } + + function stopProgressUpdates(): void { + if (progressInterval !== null) { + window.clearInterval(progressInterval); + progressInterval = null; + } + } + volumeEl.addEventListener('input', () => { const vol = Number.parseFloat(volumeEl.value); motifEngine.setVolume(vol); @@ -105,9 +138,18 @@ async function main(): Promise { stopBtn.addEventListener('click', () => { motifEngine.stop(); setUiPlaying(false); + stopProgressUpdates(); statusEl.textContent = 'Stopped.'; }); + const seekHandler = (e: Event) => { + const p = Number.parseFloat((e.target as HTMLInputElement).value) / 100; + motifEngine.seek(p); + updateProgress(); + }; + progressBar.addEventListener('input', seekHandler); + progressBar.addEventListener('change', seekHandler); + playBtn.addEventListener('click', async () => { if (!events || isPlaying) return; try { @@ -117,11 +159,19 @@ async function main(): Promise { // Match main page "Generate & Play": procedural role-mapping → existing SynthesisEngine. await motifEngine.generateFromMIDI(events, 'procedural'); motifEngine.setVolume(Number.parseFloat(volumeEl.value)); + durationSec = motifEngine.getDuration(); + durationEl.textContent = formatTime(durationSec); + currentTimeEl.textContent = '0:00'; + progressBar.value = '0'; + progressFill.style.width = '0%'; + progressContainer.style.display = 'block'; statusEl.textContent = 'Playing…'; await motifEngine.play(); + startProgressUpdates(); } catch (e) { setUiPlaying(false); + stopProgressUpdates(); statusEl.textContent = `Play error: ${e instanceof Error ? e.message : 'Unknown error'}`; } });