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:
+34
-15
@@ -1026,17 +1026,17 @@
|
||||
<div id="selectedCard" class="selected-card">
|
||||
<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>
|
||||
|
||||
<!-- selected_pre_generate only -->
|
||||
<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.
|
||||
</div>
|
||||
|
||||
<div class="selected-actions">
|
||||
<div class="selected-actions" id="preGenActions">
|
||||
<button id="motifBtn" class="primary" disabled>Generate music</button>
|
||||
</div>
|
||||
|
||||
<div class="selected-support">
|
||||
<button id="motifStopBtn" type="button" disabled>Stop</button>
|
||||
<button id="copyLinkBtn" type="button" disabled style="display:none;">Copy link</button>
|
||||
<div class="selected-support" id="preGenSupport">
|
||||
<div class="preview-inline">
|
||||
<button id="previewBtn" type="button" class="linklike" disabled>Preview original MIDI</button>
|
||||
<span id="previewState" class="preview-state">Previewing…</span>
|
||||
@@ -1045,30 +1045,49 @@
|
||||
<button id="chooseDifferentBtn" type="button" class="linklike">Choose a different source</button>
|
||||
</div>
|
||||
|
||||
<div class="engine-line">Reimagined as classic game-console music using Motif’s Wario engine.</div>
|
||||
|
||||
<div id="iosAudioBanner" class="ios-audio-banner" aria-live="polite">
|
||||
<div class="row">
|
||||
<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>
|
||||
<button id="enableAudioBtn" type="button">Enable Audio</button>
|
||||
</div>
|
||||
<div id="iosAudioState" class="state">Audio: …</div>
|
||||
</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>
|
||||
<!-- generated only: artifact playback + sharing -->
|
||||
<div id="generatedBlock" style="display:none;">
|
||||
<div class="engine-line" id="generatedTransform">
|
||||
Reimagined as classic game-console music<br />
|
||||
using Motif’s Wario engine.
|
||||
</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 style="display:flex; justify-content:center; margin-top: 14px; margin-bottom: 14px;">
|
||||
<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>
|
||||
|
||||
<details class="details-toggle">
|
||||
<details class="details-toggle" id="detailsToggle">
|
||||
<summary>Details</summary>
|
||||
<div id="selectedDetails" class="selected-details"></div>
|
||||
</details>
|
||||
|
||||
+113
-15
@@ -16,6 +16,7 @@ class MotifApp {
|
||||
private searchBtn!: HTMLButtonElement;
|
||||
private songInput!: HTMLInputElement;
|
||||
private status!: HTMLElement;
|
||||
private controlsEl!: HTMLElement;
|
||||
|
||||
private resultsSection!: HTMLElement;
|
||||
private resultsBody!: HTMLElement;
|
||||
@@ -25,6 +26,13 @@ class MotifApp {
|
||||
private selectedTitle!: HTMLElement;
|
||||
private selectedMeta!: 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 previewStopBtn!: HTMLButtonElement;
|
||||
private previewState!: HTMLElement;
|
||||
@@ -32,12 +40,13 @@ class MotifApp {
|
||||
|
||||
private isPreviewPlaying = false;
|
||||
private hasGenerated = false;
|
||||
private isMotifPlaying = false;
|
||||
private motifResumeProgress = 0;
|
||||
|
||||
// Preview player (no UI volume)
|
||||
|
||||
// Motif controls
|
||||
private motifBtn!: HTMLButtonElement;
|
||||
private motifStopBtn!: HTMLButtonElement;
|
||||
private motifProgressContainer!: HTMLElement;
|
||||
private motifProgressBar!: HTMLInputElement;
|
||||
private motifProgressFill!: HTMLElement;
|
||||
@@ -97,6 +106,7 @@ class MotifApp {
|
||||
this.searchBtn = document.getElementById('searchBtn') as HTMLButtonElement;
|
||||
this.songInput = document.getElementById('songInput') as HTMLInputElement;
|
||||
this.status = document.getElementById('status')!;
|
||||
this.controlsEl = document.querySelector('.controls') as HTMLElement;
|
||||
|
||||
this.resultsSection = document.getElementById('resultsSection')!;
|
||||
this.resultsBody = document.getElementById('resultsBody')!;
|
||||
@@ -106,6 +116,13 @@ class MotifApp {
|
||||
this.selectedTitle = document.getElementById('selectedTitle')!;
|
||||
this.selectedMeta = document.getElementById('selectedMeta')!;
|
||||
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.previewStopBtn = document.getElementById('previewStopBtn') as HTMLButtonElement;
|
||||
this.previewState = document.getElementById('previewState')!;
|
||||
@@ -113,7 +130,6 @@ class MotifApp {
|
||||
|
||||
// Motif controls
|
||||
this.motifBtn = document.getElementById('motifBtn') as HTMLButtonElement;
|
||||
this.motifStopBtn = document.getElementById('motifStopBtn') as HTMLButtonElement;
|
||||
this.motifProgressContainer = document.getElementById('motifProgressContainer')!;
|
||||
this.motifProgressBar = document.getElementById('motifProgressBar') as HTMLInputElement;
|
||||
this.motifProgressFill = document.getElementById('motifProgressFill')!;
|
||||
@@ -153,7 +169,10 @@ class MotifApp {
|
||||
|
||||
// Motif
|
||||
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
|
||||
this.previewBtn.addEventListener('click', () => void this.handlePreviewToggle());
|
||||
@@ -433,7 +452,8 @@ class MotifApp {
|
||||
this.previewBtn.disabled = true;
|
||||
const player = await this.ensureAudioReady();
|
||||
await player.load(this.currentMIDI.events);
|
||||
player.setVolume(1);
|
||||
// conservative fixed preview level
|
||||
player.setVolume(0.8);
|
||||
await player.play();
|
||||
|
||||
this.isPreviewPlaying = true;
|
||||
@@ -464,16 +484,21 @@ class MotifApp {
|
||||
await unlockAudio();
|
||||
this.updateIOSAudioBanner();
|
||||
|
||||
// Once generation starts: lock into generated experience mode.
|
||||
this.setState('generated');
|
||||
this.updateStatus('Generating…');
|
||||
this.motifBtn.disabled = true;
|
||||
this.playPauseBtn.disabled = true;
|
||||
|
||||
// Generate a variation using the procedural role-mapping mode
|
||||
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();
|
||||
|
||||
this.motifStopBtn.disabled = false;
|
||||
this.isMotifPlaying = true;
|
||||
this.playPauseBtn.disabled = false;
|
||||
this.playPauseBtn.textContent = 'Pause';
|
||||
|
||||
// Show progress bar and set duration
|
||||
this.motifProgressContainer.style.display = 'block';
|
||||
@@ -487,24 +512,35 @@ class MotifApp {
|
||||
|
||||
this.updateStatus('');
|
||||
this.hasGenerated = true;
|
||||
this.setState('generated');
|
||||
// keep generated state
|
||||
} catch (error) {
|
||||
this.updateStatus(`Motif error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
this.motifBtn.disabled = false;
|
||||
this.playPauseBtn.disabled = false;
|
||||
// If generation fails, return to selected state
|
||||
this.setState('selected');
|
||||
}
|
||||
}
|
||||
|
||||
private handleMotifStop(): void {
|
||||
this.motifEngine.stop();
|
||||
this.motifBtn.disabled = false;
|
||||
this.motifStopBtn.disabled = true;
|
||||
this.isMotifPlaying = false;
|
||||
this.playPauseBtn.textContent = 'Play';
|
||||
this.stopMotifProgressUpdates();
|
||||
this.updateStatus('');
|
||||
}
|
||||
|
||||
private handleMotifSeek(progress: number): void {
|
||||
this.motifEngine.seek(progress);
|
||||
this.updateMotifProgress();
|
||||
this.motifResumeProgress = progress;
|
||||
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 {
|
||||
@@ -528,6 +564,16 @@ class MotifApp {
|
||||
this.motifProgressBar.value = (progress * 100).toString();
|
||||
this.motifProgressFill.style.width = `${progress * 100}%`;
|
||||
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 {
|
||||
@@ -588,7 +634,6 @@ class MotifApp {
|
||||
|
||||
private disablePlayerControls(): void {
|
||||
this.motifBtn.disabled = true;
|
||||
this.motifStopBtn.disabled = true;
|
||||
this.copyLinkBtn.disabled = true;
|
||||
this.previewBtn.disabled = true;
|
||||
}
|
||||
@@ -683,7 +728,7 @@ class MotifApp {
|
||||
|
||||
private setState(state: 'idle' | 'results' | 'selected' | 'generated'): void {
|
||||
// results
|
||||
const hasResults = state === 'results' || state === 'selected' || state === 'generated';
|
||||
const hasResults = state === 'results' || state === 'selected';
|
||||
this.resultsSection.classList.toggle('visible', hasResults);
|
||||
this.chooseHelper.style.display = state === 'results' ? 'block' : 'none';
|
||||
|
||||
@@ -693,10 +738,35 @@ class MotifApp {
|
||||
|
||||
// share link only after generation
|
||||
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
|
||||
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 {
|
||||
@@ -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 {
|
||||
if (!this.embedSection || !this.embedCodeEl) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user