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 @@
@@ -267,7 +357,7 @@
Loading…
+
+
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'}`;
}
});