Add progress slider to share player.
Updates /play with a seekable progress bar, time display, 70% default volume, and Wario engine copy.
This commit is contained in:
@@ -249,6 +249,96 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: rgba(255, 255, 255, 0.65);
|
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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -258,7 +348,7 @@
|
|||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h3 class="section-title">Synthesis Engine</h3>
|
<h3 class="section-title">Wario synthesis engine</h3>
|
||||||
<p class="section-subtitle">The same Game Boy/NES render you hear on the main page.</p>
|
<p class="section-subtitle">The same Game Boy/NES render you hear on the main page.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
@@ -267,7 +357,7 @@
|
|||||||
<div class="motif-grid">
|
<div class="motif-grid">
|
||||||
<div class="motif-header">
|
<div class="motif-header">
|
||||||
<h2 class="motif-title" id="title">Loading…</h2>
|
<h2 class="motif-title" id="title">Loading…</h2>
|
||||||
<p class="motif-kicker">Tap play to run the shared MIDI through MOTIF’s synth engine.</p>
|
<p class="motif-kicker">Tap play to run the shared MIDI through the Wario engine.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="motif-controls">
|
<div class="motif-controls">
|
||||||
@@ -275,13 +365,24 @@
|
|||||||
<button id="stopBtn" disabled>Stop</button>
|
<button id="stopBtn" disabled>Stop</button>
|
||||||
<div class="volume-control">
|
<div class="volume-control">
|
||||||
<label for="volume">Volume</label>
|
<label for="volume">Volume</label>
|
||||||
<input id="volume" type="range" min="0" max="1" step="0.01" value="0.35" />
|
<input id="volume" type="range" min="0" max="1" step="0.01" value="0.7" />
|
||||||
</div>
|
</div>
|
||||||
<span class="backpill" id="shareHint">/play</span>
|
<span class="backpill" id="shareHint">/play</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="status">Loading…</div>
|
<div id="status">Loading…</div>
|
||||||
|
|
||||||
|
<div class="progress-container" id="playProgressContainer">
|
||||||
|
<div class="progress-time">
|
||||||
|
<span id="playCurrentTime">0:00</span>
|
||||||
|
<span id="playDuration">0:00</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar-wrapper">
|
||||||
|
<input id="playProgressBar" class="progress-bar" type="range" min="0" max="100" value="0" step="0.1" />
|
||||||
|
<div id="playProgressFill" class="progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<a id="generateOwn" href="/">Generate your own</a>
|
<a id="generateOwn" href="/">Generate your own</a>
|
||||||
<a href="/" aria-label="Back to MOTIF home">Home</a>
|
<a href="/" aria-label="Back to MOTIF home">Home</a>
|
||||||
|
|||||||
+50
@@ -56,6 +56,11 @@ async function main(): Promise<void> {
|
|||||||
const volumeEl = qs('volume') as HTMLInputElement;
|
const volumeEl = qs('volume') as HTMLInputElement;
|
||||||
const generateOwn = qs('generateOwn') as HTMLAnchorElement;
|
const generateOwn = qs('generateOwn') as HTMLAnchorElement;
|
||||||
const shareHint = qs('shareHint');
|
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();
|
const { midiUrl: u, title } = buildMidiUrlFromParams();
|
||||||
|
|
||||||
@@ -76,6 +81,8 @@ async function main(): Promise<void> {
|
|||||||
const motifEngine = new MotifEngine();
|
const motifEngine = new MotifEngine();
|
||||||
let events: NoteEvent[] | null = null;
|
let events: NoteEvent[] | null = null;
|
||||||
let isPlaying = false;
|
let isPlaying = false;
|
||||||
|
let progressInterval: number | null = null;
|
||||||
|
let durationSec = 0;
|
||||||
|
|
||||||
statusEl.textContent = 'Loading MIDI…';
|
statusEl.textContent = 'Loading MIDI…';
|
||||||
try {
|
try {
|
||||||
@@ -97,6 +104,32 @@ async function main(): Promise<void> {
|
|||||||
stopBtn.disabled = !playing;
|
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', () => {
|
volumeEl.addEventListener('input', () => {
|
||||||
const vol = Number.parseFloat(volumeEl.value);
|
const vol = Number.parseFloat(volumeEl.value);
|
||||||
motifEngine.setVolume(vol);
|
motifEngine.setVolume(vol);
|
||||||
@@ -105,9 +138,18 @@ async function main(): Promise<void> {
|
|||||||
stopBtn.addEventListener('click', () => {
|
stopBtn.addEventListener('click', () => {
|
||||||
motifEngine.stop();
|
motifEngine.stop();
|
||||||
setUiPlaying(false);
|
setUiPlaying(false);
|
||||||
|
stopProgressUpdates();
|
||||||
statusEl.textContent = 'Stopped.';
|
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 () => {
|
playBtn.addEventListener('click', async () => {
|
||||||
if (!events || isPlaying) return;
|
if (!events || isPlaying) return;
|
||||||
try {
|
try {
|
||||||
@@ -117,11 +159,19 @@ async function main(): Promise<void> {
|
|||||||
// Match main page "Generate & Play": procedural role-mapping → existing SynthesisEngine.
|
// Match main page "Generate & Play": procedural role-mapping → existing SynthesisEngine.
|
||||||
await motifEngine.generateFromMIDI(events, 'procedural');
|
await motifEngine.generateFromMIDI(events, 'procedural');
|
||||||
motifEngine.setVolume(Number.parseFloat(volumeEl.value));
|
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…';
|
statusEl.textContent = 'Playing…';
|
||||||
await motifEngine.play();
|
await motifEngine.play();
|
||||||
|
startProgressUpdates();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setUiPlaying(false);
|
setUiPlaying(false);
|
||||||
|
stopProgressUpdates();
|
||||||
statusEl.textContent = `Play error: ${e instanceof Error ? e.message : 'Unknown error'}`;
|
statusEl.textContent = `Play error: ${e instanceof Error ? e.message : 'Unknown error'}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user