Split selected vs generated UI on main page.

After generation starts, hide preview/source-changing/search affordances and switch to an artifact view: title, Wario transformation line, Play/Pause, timeline, volume, and Copy link.
This commit is contained in:
b1rdmania
2025-12-26 21:14:30 +00:00
parent fd422aa084
commit 63618ec4fa
2 changed files with 147 additions and 30 deletions
+34 -15
View File
@@ -1026,17 +1026,17 @@
<div id="selectedCard" class="selected-card"> <div id="selectedCard" class="selected-card">
<h2 id="selectedTitle" class="selected-title">No source selected</h2> <h2 id="selectedTitle" class="selected-title">No source selected</h2>
<div id="selectedMeta" class="selected-meta">Search for a song, then choose a source to use.</div> <div id="selectedMeta" class="selected-meta">Search for a song, then choose a source to use.</div>
<!-- selected_pre_generate only -->
<div id="selectedExpectation" class="selected-meta" style="margin-top: 8px; color: rgba(255,255,255,0.62);"> <div id="selectedExpectation" class="selected-meta" style="margin-top: 8px; color: rgba(255,255,255,0.62);">
Some MIDI files work better than others. If the result sounds off, try another source. Some MIDI files work better than others. If the result sounds off, try another source.
</div> </div>
<div class="selected-actions"> <div class="selected-actions" id="preGenActions">
<button id="motifBtn" class="primary" disabled>Generate music</button> <button id="motifBtn" class="primary" disabled>Generate music</button>
</div> </div>
<div class="selected-support"> <div class="selected-support" id="preGenSupport">
<button id="motifStopBtn" type="button" disabled>Stop</button>
<button id="copyLinkBtn" type="button" disabled style="display:none;">Copy link</button>
<div class="preview-inline"> <div class="preview-inline">
<button id="previewBtn" type="button" class="linklike" disabled>Preview original MIDI</button> <button id="previewBtn" type="button" class="linklike" disabled>Preview original MIDI</button>
<span id="previewState" class="preview-state">Previewing…</span> <span id="previewState" class="preview-state">Previewing…</span>
@@ -1045,30 +1045,49 @@
<button id="chooseDifferentBtn" type="button" class="linklike">Choose a different source</button> <button id="chooseDifferentBtn" type="button" class="linklike">Choose a different source</button>
</div> </div>
<div class="engine-line">Reimagined as classic game-console music using Motifs Wario engine.</div>
<div id="iosAudioBanner" class="ios-audio-banner" aria-live="polite"> <div id="iosAudioBanner" class="ios-audio-banner" aria-live="polite">
<div class="row"> <div class="row">
<div class="copy"> <div class="copy">
<strong>iOS Safari:</strong> tap once to enable audio, then press <strong>Generate</strong>. <strong>iOS Safari:</strong> tap once to enable audio, then press <strong>Generate music</strong>.
</div> </div>
<button id="enableAudioBtn" type="button">Enable Audio</button> <button id="enableAudioBtn" type="button">Enable Audio</button>
</div> </div>
<div id="iosAudioState" class="state">Audio: …</div> <div id="iosAudioState" class="state">Audio: …</div>
</div> </div>
<div class="motif-progress-container" id="motifProgressContainer" style="display: none;"> <!-- generated only: artifact playback + sharing -->
<div class="progress-time"> <div id="generatedBlock" style="display:none;">
<span id="motifCurrentTime">0:00</span> <div class="engine-line" id="generatedTransform">
<span id="motifDuration">0:00</span> Reimagined as classic game-console music<br />
using Motifs Wario engine.
</div> </div>
<div class="progress-bar-wrapper">
<input type="range" id="motifProgressBar" class="progress-bar" min="0" max="100" value="0" step="0.1" /> <div style="display:flex; justify-content:center; margin-top: 14px; margin-bottom: 14px;">
<div class="progress-fill" id="motifProgressFill"></div> <button id="playPauseBtn" class="primary" type="button">Play</button>
</div>
<div class="motif-progress-container" id="motifProgressContainer" style="display: none;">
<div class="progress-time">
<span id="motifCurrentTime">0:00</span>
<span id="motifDuration">0:00</span>
</div>
<div class="progress-bar-wrapper">
<input type="range" id="motifProgressBar" class="progress-bar" min="0" max="100" value="0" step="0.1" />
<div class="progress-fill" id="motifProgressFill"></div>
</div>
</div>
<div class="volume-control" id="generatedVolumeRow" style="margin-top: 14px; opacity: 0.75;">
<label for="motifVolume" style="min-width: auto;">Volume</label>
<input type="range" id="motifVolume" min="0" max="1" step="0.01" value="0.8" />
</div>
<div style="margin-top: 14px;">
<button id="copyLinkBtn" type="button" disabled>Copy link</button>
</div> </div>
</div> </div>
<details class="details-toggle"> <details class="details-toggle" id="detailsToggle">
<summary>Details</summary> <summary>Details</summary>
<div id="selectedDetails" class="selected-details"></div> <div id="selectedDetails" class="selected-details"></div>
</details> </details>
+113 -15
View File
@@ -16,6 +16,7 @@ class MotifApp {
private searchBtn!: HTMLButtonElement; private searchBtn!: HTMLButtonElement;
private songInput!: HTMLInputElement; private songInput!: HTMLInputElement;
private status!: HTMLElement; private status!: HTMLElement;
private controlsEl!: HTMLElement;
private resultsSection!: HTMLElement; private resultsSection!: HTMLElement;
private resultsBody!: HTMLElement; private resultsBody!: HTMLElement;
@@ -25,6 +26,13 @@ class MotifApp {
private selectedTitle!: HTMLElement; private selectedTitle!: HTMLElement;
private selectedMeta!: HTMLElement; private selectedMeta!: HTMLElement;
private selectedDetails!: HTMLElement; private selectedDetails!: HTMLElement;
private selectedExpectation!: HTMLElement;
private preGenActions!: HTMLElement;
private preGenSupport!: HTMLElement;
private generatedBlock!: HTMLElement;
private playPauseBtn!: HTMLButtonElement;
private motifVolumeSlider!: HTMLInputElement;
private detailsToggle!: HTMLElement;
private previewBtn!: HTMLButtonElement; private previewBtn!: HTMLButtonElement;
private previewStopBtn!: HTMLButtonElement; private previewStopBtn!: HTMLButtonElement;
private previewState!: HTMLElement; private previewState!: HTMLElement;
@@ -32,12 +40,13 @@ class MotifApp {
private isPreviewPlaying = false; private isPreviewPlaying = false;
private hasGenerated = false; private hasGenerated = false;
private isMotifPlaying = false;
private motifResumeProgress = 0;
// Preview player (no UI volume) // Preview player (no UI volume)
// Motif controls // Motif controls
private motifBtn!: HTMLButtonElement; private motifBtn!: HTMLButtonElement;
private motifStopBtn!: HTMLButtonElement;
private motifProgressContainer!: HTMLElement; private motifProgressContainer!: HTMLElement;
private motifProgressBar!: HTMLInputElement; private motifProgressBar!: HTMLInputElement;
private motifProgressFill!: HTMLElement; private motifProgressFill!: HTMLElement;
@@ -97,6 +106,7 @@ class MotifApp {
this.searchBtn = document.getElementById('searchBtn') as HTMLButtonElement; this.searchBtn = document.getElementById('searchBtn') as HTMLButtonElement;
this.songInput = document.getElementById('songInput') as HTMLInputElement; this.songInput = document.getElementById('songInput') as HTMLInputElement;
this.status = document.getElementById('status')!; this.status = document.getElementById('status')!;
this.controlsEl = document.querySelector('.controls') as HTMLElement;
this.resultsSection = document.getElementById('resultsSection')!; this.resultsSection = document.getElementById('resultsSection')!;
this.resultsBody = document.getElementById('resultsBody')!; this.resultsBody = document.getElementById('resultsBody')!;
@@ -106,6 +116,13 @@ class MotifApp {
this.selectedTitle = document.getElementById('selectedTitle')!; this.selectedTitle = document.getElementById('selectedTitle')!;
this.selectedMeta = document.getElementById('selectedMeta')!; this.selectedMeta = document.getElementById('selectedMeta')!;
this.selectedDetails = document.getElementById('selectedDetails')!; this.selectedDetails = document.getElementById('selectedDetails')!;
this.selectedExpectation = document.getElementById('selectedExpectation')!;
this.preGenActions = document.getElementById('preGenActions')!;
this.preGenSupport = document.getElementById('preGenSupport')!;
this.generatedBlock = document.getElementById('generatedBlock')!;
this.playPauseBtn = document.getElementById('playPauseBtn') as HTMLButtonElement;
this.motifVolumeSlider = document.getElementById('motifVolume') as HTMLInputElement;
this.detailsToggle = document.getElementById('detailsToggle')!;
this.previewBtn = document.getElementById('previewBtn') as HTMLButtonElement; this.previewBtn = document.getElementById('previewBtn') as HTMLButtonElement;
this.previewStopBtn = document.getElementById('previewStopBtn') as HTMLButtonElement; this.previewStopBtn = document.getElementById('previewStopBtn') as HTMLButtonElement;
this.previewState = document.getElementById('previewState')!; this.previewState = document.getElementById('previewState')!;
@@ -113,7 +130,6 @@ class MotifApp {
// Motif controls // Motif controls
this.motifBtn = document.getElementById('motifBtn') as HTMLButtonElement; this.motifBtn = document.getElementById('motifBtn') as HTMLButtonElement;
this.motifStopBtn = document.getElementById('motifStopBtn') as HTMLButtonElement;
this.motifProgressContainer = document.getElementById('motifProgressContainer')!; this.motifProgressContainer = document.getElementById('motifProgressContainer')!;
this.motifProgressBar = document.getElementById('motifProgressBar') as HTMLInputElement; this.motifProgressBar = document.getElementById('motifProgressBar') as HTMLInputElement;
this.motifProgressFill = document.getElementById('motifProgressFill')!; this.motifProgressFill = document.getElementById('motifProgressFill')!;
@@ -153,7 +169,10 @@ class MotifApp {
// Motif // Motif
this.motifBtn.addEventListener('click', () => this.handleMotif()); this.motifBtn.addEventListener('click', () => this.handleMotif());
this.motifStopBtn.addEventListener('click', () => this.handleMotifStop()); this.playPauseBtn.addEventListener('click', () => void this.handlePlayPause());
this.motifVolumeSlider.addEventListener('input', () => {
this.motifEngine.setVolume(parseFloat(this.motifVolumeSlider.value));
});
// Preview inside selected source only // Preview inside selected source only
this.previewBtn.addEventListener('click', () => void this.handlePreviewToggle()); this.previewBtn.addEventListener('click', () => void this.handlePreviewToggle());
@@ -433,7 +452,8 @@ class MotifApp {
this.previewBtn.disabled = true; this.previewBtn.disabled = true;
const player = await this.ensureAudioReady(); const player = await this.ensureAudioReady();
await player.load(this.currentMIDI.events); await player.load(this.currentMIDI.events);
player.setVolume(1); // conservative fixed preview level
player.setVolume(0.8);
await player.play(); await player.play();
this.isPreviewPlaying = true; this.isPreviewPlaying = true;
@@ -464,16 +484,21 @@ class MotifApp {
await unlockAudio(); await unlockAudio();
this.updateIOSAudioBanner(); this.updateIOSAudioBanner();
// Once generation starts: lock into generated experience mode.
this.setState('generated');
this.updateStatus('Generating…'); this.updateStatus('Generating…');
this.motifBtn.disabled = true; this.motifBtn.disabled = true;
this.playPauseBtn.disabled = true;
// Generate a variation using the procedural role-mapping mode // Generate a variation using the procedural role-mapping mode
await this.motifEngine.generateFromMIDI(this.currentMIDI.events, 'procedural'); await this.motifEngine.generateFromMIDI(this.currentMIDI.events, 'procedural');
this.motifEngine.setVolume(1); this.motifEngine.setVolume(parseFloat(this.motifVolumeSlider.value) || 0.8);
await this.motifEngine.play(); await this.motifEngine.play();
this.motifStopBtn.disabled = false; this.isMotifPlaying = true;
this.playPauseBtn.disabled = false;
this.playPauseBtn.textContent = 'Pause';
// Show progress bar and set duration // Show progress bar and set duration
this.motifProgressContainer.style.display = 'block'; this.motifProgressContainer.style.display = 'block';
@@ -487,24 +512,35 @@ class MotifApp {
this.updateStatus(''); this.updateStatus('');
this.hasGenerated = true; this.hasGenerated = true;
this.setState('generated'); // keep generated state
} catch (error) { } catch (error) {
this.updateStatus(`Motif error: ${error instanceof Error ? error.message : 'Unknown error'}`); this.updateStatus(`Motif error: ${error instanceof Error ? error.message : 'Unknown error'}`);
this.motifBtn.disabled = false; this.motifBtn.disabled = false;
this.playPauseBtn.disabled = false;
// If generation fails, return to selected state
this.setState('selected');
} }
} }
private handleMotifStop(): void { private handleMotifStop(): void {
this.motifEngine.stop(); this.motifEngine.stop();
this.motifBtn.disabled = false; this.isMotifPlaying = false;
this.motifStopBtn.disabled = true; this.playPauseBtn.textContent = 'Play';
this.stopMotifProgressUpdates(); this.stopMotifProgressUpdates();
this.updateStatus(''); this.updateStatus('');
} }
private handleMotifSeek(progress: number): void { private handleMotifSeek(progress: number): void {
this.motifEngine.seek(progress); this.motifResumeProgress = progress;
this.updateMotifProgress(); if (this.isMotifPlaying) {
this.motifEngine.seek(progress);
this.updateMotifProgress();
return;
}
const duration = this.motifEngine.getDuration();
this.motifProgressBar.value = (progress * 100).toString();
this.motifProgressFill.style.width = `${progress * 100}%`;
this.motifCurrentTime.textContent = this.formatTime(progress * duration);
} }
private startMotifProgressUpdates(): void { private startMotifProgressUpdates(): void {
@@ -528,6 +564,16 @@ class MotifApp {
this.motifProgressBar.value = (progress * 100).toString(); this.motifProgressBar.value = (progress * 100).toString();
this.motifProgressFill.style.width = `${progress * 100}%`; this.motifProgressFill.style.width = `${progress * 100}%`;
this.motifCurrentTime.textContent = this.formatTime(currentTime); this.motifCurrentTime.textContent = this.formatTime(currentTime);
// Auto-pause at end
const duration = this.motifEngine.getDuration();
if (duration > 0 && progress >= 0.999) {
this.motifResumeProgress = 0;
this.handleMotifStop();
this.motifCurrentTime.textContent = this.formatTime(duration);
this.motifProgressBar.value = '100';
this.motifProgressFill.style.width = '100%';
}
} }
private formatTime(seconds: number): string { private formatTime(seconds: number): string {
@@ -588,7 +634,6 @@ class MotifApp {
private disablePlayerControls(): void { private disablePlayerControls(): void {
this.motifBtn.disabled = true; this.motifBtn.disabled = true;
this.motifStopBtn.disabled = true;
this.copyLinkBtn.disabled = true; this.copyLinkBtn.disabled = true;
this.previewBtn.disabled = true; this.previewBtn.disabled = true;
} }
@@ -683,7 +728,7 @@ class MotifApp {
private setState(state: 'idle' | 'results' | 'selected' | 'generated'): void { private setState(state: 'idle' | 'results' | 'selected' | 'generated'): void {
// results // results
const hasResults = state === 'results' || state === 'selected' || state === 'generated'; const hasResults = state === 'results' || state === 'selected';
this.resultsSection.classList.toggle('visible', hasResults); this.resultsSection.classList.toggle('visible', hasResults);
this.chooseHelper.style.display = state === 'results' ? 'block' : 'none'; this.chooseHelper.style.display = state === 'results' ? 'block' : 'none';
@@ -693,10 +738,35 @@ class MotifApp {
// share link only after generation // share link only after generation
this.copyLinkBtn.style.display = state === 'generated' ? 'inline-block' : 'none'; this.copyLinkBtn.style.display = state === 'generated' ? 'inline-block' : 'none';
this.copyLinkBtn.disabled = state !== 'generated'; this.copyLinkBtn.disabled = !(state === 'generated' && this.hasGenerated);
// collapse results when selected/generated // collapse results when selected/generated
this.resultsSection.classList.toggle('collapsed', state === 'selected' || state === 'generated'); this.resultsSection.classList.toggle('collapsed', state === 'selected');
// Search affordances: hidden after generation (no choice)
const hideSearch = state === 'generated';
this.controlsEl.style.display = hideSearch ? 'none' : '';
this.status.style.display = hideSearch ? 'none' : '';
// Selected pre-generation content
const showPreGen = state === 'selected';
this.selectedExpectation.style.display = showPreGen ? 'block' : 'none';
this.preGenActions.style.display = showPreGen ? 'block' : 'none';
this.preGenSupport.style.display = showPreGen ? 'flex' : 'none';
this.detailsToggle.style.display = showPreGen ? 'block' : 'none';
// Generated artifact content
const showGenerated = state === 'generated';
this.generatedBlock.style.display = showGenerated ? 'block' : 'none';
this.playPauseBtn.disabled = !showGenerated || !this.hasGenerated;
this.playPauseBtn.textContent = this.isMotifPlaying ? 'Pause' : 'Play';
// iOS banner only before generation
if (showPreGen) {
this.updateIOSAudioBanner();
} else {
this.iosAudioBanner.style.display = 'none';
}
} }
private goToResults(): void { private goToResults(): void {
@@ -708,6 +778,34 @@ class MotifApp {
} }
} }
private async handlePlayPause(): Promise<void> {
if (!this.hasGenerated) return;
if (this.isMotifPlaying) {
this.motifResumeProgress = this.motifEngine.getProgress();
this.motifEngine.stop();
this.isMotifPlaying = false;
this.stopMotifProgressUpdates();
this.playPauseBtn.textContent = 'Play';
return;
}
try {
// Audio exclusivity
this.stopPreview();
await unlockAudio();
this.motifEngine.setVolume(parseFloat(this.motifVolumeSlider.value) || 0.8);
await this.motifEngine.play();
if (this.motifResumeProgress > 0) this.motifEngine.seek(this.motifResumeProgress);
this.isMotifPlaying = true;
this.playPauseBtn.textContent = 'Pause';
this.startMotifProgressUpdates();
} catch {
// ignore
}
}
private updateEmbedSnippet(songTitle: string): void { private updateEmbedSnippet(songTitle: string): void {
if (!this.embedSection || !this.embedCodeEl) return; if (!this.embedSection || !this.embedCodeEl) return;