Clickable previews: every still links to its exact app frame

New shared component src/ui/preview.js (appFrameUrl + previewLink): song + seed + frame + venue/venueSel deep-links. App honors ?frame= (warm seek on bank-song load) and ?venueSel= via shared pinVenueShell helper (src/venues/pin.js), which filmstrip and venue-gallery now also use. Filmstrip figures and venue-gallery cards wrap canvases; gallery cells stay unlinked (isolated scene probes, not reproducible in-app).
This commit is contained in:
Dejvino
2026-09-20 17:08:03 +02:00
parent cd7dfcd63d
commit 6c7e314a3e
6 changed files with 182 additions and 31 deletions
+27 -3
View File
@@ -16,6 +16,7 @@ import { synthesizeSong } from './audio/synth.js';
import { FeatureTrack } from './audio/FeatureTrack.js';
import { generateLook } from './look/LookGenerator.js';
import { SONGS } from './audio/songbank.js';
import { pinVenueShell } from './venues/pin.js';
import { captureThumbnails } from './llm/thumbs.js';
const QUALITY = {
@@ -100,7 +101,7 @@ const paramPanel = new ParamPanel(document.createElement('div'), onParamChange);
// ---------------------------------------------------------------- loading
async function loadFile(file, { seed = null } = {}) {
async function loadFile(file, { seed = null, venueSel = null, frame = null } = {}) {
if (state.busy) return;
state.busy = true;
stopPlayback();
@@ -133,7 +134,19 @@ async function loadFile(file, { seed = null } = {}) {
strip.setShow(state.show);
if (dom.osd) dom.osd.classList.toggle('on', state.osdVisible);
resize();
seekTo(0);
// Deep link from a preview (filmstrip figure, venue-gallery card):
// the same shell-swap the probe pages do, then the exact frame with
// warm-up so stateful layers land converged like the probe render.
// Dropped files never carry these — only `?song=` loads pass them.
if (venueSel && state.show.look.venue) {
try {
if (pinVenueShell(state.show.look, venueSel,
{ seed: state.show.look.seed, buildIfMissing: true })) {
state.show.prewarm();
}
} catch (err) { console.warn('[flow-state]', err.message); }
}
seekTo(Number.isFinite(frame) ? frame : 0);
renderPanel();
document.title = `flow-state · ${state.show.fileName}`;
console.info('[flow-state]', describeLook(state.show.look));
@@ -169,6 +182,9 @@ dom.fileInput.addEventListener('change', (e) => {
* reading is to watch the thing move, and until now that meant remembering
* which of seventeen near-identically-named wavs to drag in. `&seed=` carries
* the strip's seed across so the video you watch is the one it measured.
* `&frame=` seeks to the exact frame after load (with warm-up), and
* `&venueSel=` re-applies the pinned venue shell — together they make a
* preview deep-link reproduce its image. Built by src/ui/preview.js.
*
* Dev-only in practice: `test/songs/` is served by vite from the project root
* and is not part of a build. The name is matched against a bare word rather
@@ -181,7 +197,15 @@ async function loadBankSong(name, seed) {
const res = await fetch(url);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const blob = await res.blob();
await loadFile(new File([blob], `${name}.wav`, { type: 'audio/wav' }), { seed });
// Preview deep-links (?frame=, ?venueSel=) ride along: the app lands
// on the exact image the preview showed. See src/ui/preview.js.
const q = new URLSearchParams(location.search);
const frame = q.get('frame');
await loadFile(new File([blob], `${name}.wav`, { type: 'audio/wav' }), {
seed,
venueSel: q.get('venueSel'),
frame: frame === null ? null : Number(frame),
});
} catch (err) {
dom.thLabel.textContent = 'no such song';
dom.thName.hidden = false;