Clickable previews: exact app deep-links everywhere

Every rendered still links to the app frame that produced it (song + seed + frame + venue/viz pins + probe warm-up, new window), via shared src/ui/preview.js. Supporting work, each measured: app honors ?frame/?venueSel/?viz/?warm= with pre-build pins (Show.applyPins) and primes deep-link loads; shared pinViz/pinVenueShell paths for gallery, filmstrip and app; probes prime and carry their warm-up; bank probes analyse the bank PCM and file decode is fixed-rate with bit-exact WAV parsing so app and probes see identical samples; materials built mid-update compile synchronously (needsCompile). Round trip verified bit-identical gallery-versus-app.
This commit is contained in:
Dejvino
2026-09-20 18:42:53 +02:00
parent cc8888077f
commit 4d05250294
18 changed files with 377 additions and 96 deletions
+21 -17
View File
@@ -16,7 +16,6 @@ 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 = {
@@ -101,7 +100,7 @@ const paramPanel = new ParamPanel(document.createElement('div'), onParamChange);
// ---------------------------------------------------------------- loading
async function loadFile(file, { seed = null, venueSel = null, frame = null } = {}) {
async function loadFile(file, { seed = null, pins = null, frame = null, warm = null } = {}) {
if (state.busy) return;
state.busy = true;
stopPlayback();
@@ -121,7 +120,7 @@ async function loadFile(file, { seed = null, venueSel = null, frame = null } = {
await state.show.load(file, (stage, fraction) => {
dom.thStep.textContent = stage;
dom.thFill.style.width = `${Math.round((fraction || 0) * 100)}%`;
}, { seed });
}, { seed, pins });
dom.audio.src = URL.createObjectURL(file);
dom.thLabel.textContent = 'loaded track';
@@ -135,18 +134,18 @@ async function loadFile(file, { seed = null, venueSel = null, frame = null } = {
if (dom.osd) dom.osd.classList.toggle('on', state.osdVisible);
resize();
// 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);
// pins ride into load() so the arc builds pinned layers once, then
// the exact frame with the probe's warm-up so stateful layers land
// converged like the probe render. Dropped files never carry pins —
// only `?song=` loads pass them.
const deepLink = !!(pins && (pins.venueSel || pins.viz)) || Number.isFinite(frame);
// Prime before the landing seek on deep-link loads: programs link
// asynchronously and a draw against an unlinked program is wrong
// output — the probe pages prime for the same reason, and both sides
// must be linked for the frames to agree. Normal loads skip it.
if (deepLink) state.show.prime();
seekTo(Number.isFinite(frame) ? frame : 0,
Number.isFinite(warm) ? { warmup: warm } : {});
renderPanel();
document.title = `flow-state · ${state.show.fileName}`;
console.info('[flow-state]', describeLook(state.show.look));
@@ -184,7 +183,10 @@ dom.fileInput.addEventListener('change', (e) => {
* 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.
* preview deep-link reproduce its image. `&viz=` pins the subject the same
* way, and `&warm=` replays the probe's own warm-up length so feedback
* lands identically converged; deep-link loads prime first so neither side
* renders against unlinked programs. 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
@@ -201,10 +203,12 @@ async function loadBankSong(name, seed) {
// on the exact image the preview showed. See src/ui/preview.js.
const q = new URLSearchParams(location.search);
const frame = q.get('frame');
const warm = q.get('warm');
await loadFile(new File([blob], `${name}.wav`, { type: 'audio/wav' }), {
seed,
venueSel: q.get('venueSel'),
pins: { venueSel: q.get('venueSel'), viz: q.get('viz') },
frame: frame === null ? null : Number(frame),
warm: warm === null ? null : Number(warm),
});
} catch (err) {
dom.thLabel.textContent = 'no such song';