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:
@@ -59,17 +59,19 @@
|
||||
<script type="module">
|
||||
import { Show } from '/src/Show.js';
|
||||
import { SONGS, song } from '/src/audio/songbank.js';
|
||||
import { Rng, hashString } from '/src/engine/rng.js';
|
||||
import { hashString } from '/src/engine/rng.js';
|
||||
import { generateLook } from '/src/look/LookGenerator.js';
|
||||
import { pinVenueShell } from '/src/venues/pin.js';
|
||||
import { sampleValues } from '/src/params/schema.js';
|
||||
import { subjectIndexOf } from '/src/look/stack.js';
|
||||
import { pinViz } from '/src/look/pinViz.js';
|
||||
import { venues } from '/src/venues/registry.js';
|
||||
import { scenes } from '/src/scenes/registry.js';
|
||||
import { blit } from '/src/checks/blit.js';
|
||||
import { previewLink } from '/src/ui/preview.js';
|
||||
|
||||
const THUMB = { width: 320, height: 180 };
|
||||
// Frames of warm-up behind every probe. Carried in preview links (?warm=)
|
||||
// so the app lands on identically converged state. See src/ui/preview.js.
|
||||
const WARMUP = 24;
|
||||
const DEFAULT_ROWS = 3;
|
||||
const DEFAULT_COLS = 3;
|
||||
const ROW_OPTIONS = [2, 3, 4, 5, 6, 8, 10];
|
||||
@@ -177,36 +179,6 @@ async function ensureSongs(names) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pin one viz as the subject of every section's stacks, keeping the venue
|
||||
// ground (and overlays) untouched. Mutates the look before Show.useTrack, so
|
||||
// the ArcDriver builds its layer cache from the pinned stacks directly.
|
||||
function pinViz(look, viz, seedBase) {
|
||||
const temper = look.personality && look.personality.temperament;
|
||||
for (const sec of look.sections) {
|
||||
const rng = new Rng(hashString(`${viz.name}:sec:${sec.index}:${seedBase >>> 0}`));
|
||||
for (let v = 0; v < (sec.variants || []).length; v++) {
|
||||
const stack = sec.variants[v];
|
||||
if (!stack) continue;
|
||||
const at = subjectIndexOf(stack);
|
||||
const cur = stack[at];
|
||||
if (!cur) continue;
|
||||
const params = sampleValues(viz, rng.fork(`variant:${v}`), sec.bias, temper);
|
||||
stack[at] = {
|
||||
module: viz,
|
||||
params,
|
||||
seed: rng.int(0, 0x7fffffff),
|
||||
// A model viz composites by alpha; a fragment viz needs the
|
||||
// key/screen path over the venue ground. Reuse the old blend
|
||||
// when it already fits, else fall back to the compositor-safe one.
|
||||
blend: viz.kind === 'model' ? 'normal'
|
||||
: (cur.blend === 'normal' ? 'lumakey' : cur.blend),
|
||||
opacity: cur.opacity ?? 1,
|
||||
blaze: cur.blaze ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function errorCard(title, detail) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card error';
|
||||
@@ -239,7 +211,17 @@ async function renderCell(show, entry, venueName, vizMod, colIdx) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (vizMod) pinViz(look, vizMod, seedBase);
|
||||
// Pin the viz through the shared path with the app's ?viz=
|
||||
// (src/look/pinViz.js): the card and its deep-link pin the same subject
|
||||
// with the same seeds.
|
||||
if (vizMod) {
|
||||
try {
|
||||
pinViz(look, vizMod.name);
|
||||
} catch (err) {
|
||||
errorCard(`${entry.name} · col ${colIdx + 1}`, err.message || String(err));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const fps = entry.track.fps || 60;
|
||||
const frame = Math.min(
|
||||
@@ -247,12 +229,25 @@ async function renderCell(show, entry, venueName, vizMod, colIdx) {
|
||||
Math.floor(entry.track.duration * 0.35 * fps),
|
||||
);
|
||||
try {
|
||||
// The file name the app shows in its title plate (OSD): without it
|
||||
// the card renders a blank plate and its deep-link can't match.
|
||||
// See src/ui/preview.js.
|
||||
show.fileName = entry.name;
|
||||
show.useTrack(entry.track, look);
|
||||
} catch (err) {
|
||||
errorCard(`${entry.name} · col ${colIdx + 1}`, `useTrack failed: ${err.message || err}`);
|
||||
return;
|
||||
}
|
||||
show.warmUp(frame, 24);
|
||||
// Prime so the probe never renders against unlinked programs — the app
|
||||
// primes deep-link loads for the same reason; both sides must be linked
|
||||
// for a card and its link to agree. See src/ui/preview.js.
|
||||
try {
|
||||
show.prime();
|
||||
} catch (err) {
|
||||
errorCard(`${entry.name} · col ${colIdx + 1}`, `prime failed: ${err.message || err}`);
|
||||
return;
|
||||
}
|
||||
show.warmUp(frame, WARMUP);
|
||||
let target;
|
||||
try {
|
||||
target = show.renderFrame(frame);
|
||||
@@ -277,16 +272,19 @@ async function renderCell(show, entry, venueName, vizMod, colIdx) {
|
||||
card.appendChild(cv);
|
||||
card.appendChild(meta);
|
||||
out.appendChild(card);
|
||||
// Clicking opens the exact frame in the app — but only when the cell is
|
||||
// reproducible there: a pinned viz re-samples the stacks outside any
|
||||
// look, so those cards stay unlinked (same reason gallery cells aren't).
|
||||
if (!vizMod && look.venue) {
|
||||
// Clicking opens the exact frame in the app (song + seed + frame +
|
||||
// venue pin + viz pin + the probe's warm-up length — the app rebuilds
|
||||
// the same look through the same shared pin paths). Every card links:
|
||||
// the gallery holds nothing back.
|
||||
if (look.venue) {
|
||||
previewLink(cv, {
|
||||
song: entry.name,
|
||||
seed: seedBase,
|
||||
frame,
|
||||
venue: true,
|
||||
venueSel: venueName || look.venue.name,
|
||||
viz: vizMod ? vizMod.name : null,
|
||||
warm: WARMUP,
|
||||
});
|
||||
}
|
||||
blit(cv, pix, THUMB.width, THUMB.height);
|
||||
|
||||
Reference in New Issue
Block a user