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:
+27
-8
@@ -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,18 +1045,27 @@
|
|||||||
<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 Motif’s 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>
|
||||||
|
|
||||||
|
<!-- 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 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="motif-progress-container" id="motifProgressContainer" style="display: none;">
|
||||||
<div class="progress-time">
|
<div class="progress-time">
|
||||||
<span id="motifCurrentTime">0:00</span>
|
<span id="motifCurrentTime">0:00</span>
|
||||||
@@ -1068,7 +1077,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="details-toggle">
|
<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" id="detailsToggle">
|
||||||
<summary>Details</summary>
|
<summary>Details</summary>
|
||||||
<div id="selectedDetails" class="selected-details"></div>
|
<div id="selectedDetails" class="selected-details"></div>
|
||||||
</details>
|
</details>
|
||||||
|
|||||||
+111
-13
@@ -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.motifResumeProgress = progress;
|
||||||
|
if (this.isMotifPlaying) {
|
||||||
this.motifEngine.seek(progress);
|
this.motifEngine.seek(progress);
|
||||||
this.updateMotifProgress();
|
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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user