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:
@@ -63,6 +63,8 @@
|
||||
.strip { display: flex; gap: 4px; overflow-x: auto; padding-bottom: 6px; }
|
||||
.strip figure { margin: 0; flex: 0 0 auto; width: 168px; }
|
||||
.strip canvas { width: 100%; display: block; background: #05070a; border-radius: 2px; aspect-ratio: 16 / 9; }
|
||||
a.preview-link canvas { cursor: zoom-in; }
|
||||
a.preview-link:hover canvas { outline: 1px solid #7dd3fc; outline-offset: -1px; }
|
||||
.strip figcaption { font-size: 10px; color: #565f70; margin-top: 3px; line-height: 1.35; }
|
||||
.strip .t { color: #8b94a7; }
|
||||
.strip .sc { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
@@ -107,6 +109,7 @@ import { buildFilmstrip, songNames, STRIP, DEFAULT_EVERY, DEFAULT_DURATION } fro
|
||||
import { venues } from '/src/venues/registry.js';
|
||||
import { blit } from '/src/checks/blit.js';
|
||||
import { hashString } from '/src/engine/rng.js';
|
||||
import { previewLink } from '/src/ui/preview.js';
|
||||
import {
|
||||
sourceFingerprint, loadGallery, saveGallery,
|
||||
pixelsToBlob, blobToCanvas,
|
||||
@@ -156,8 +159,26 @@ const grade = (row) => {
|
||||
// video. The app takes the bank's wav straight from `?song=`, and the seed is
|
||||
// the one renderSong() uses — hashed off the name — so what plays is the take
|
||||
// the strip measured rather than a fresh draw that happens to share a title.
|
||||
const watchLink = (name) =>
|
||||
`/?song=${encodeURIComponent(name)}&seed=${hashString(name)}`;
|
||||
// The venue flag and pin ride along too, so a venue strip replays its room.
|
||||
const watchLink = (name) => {
|
||||
const p = new URLSearchParams();
|
||||
p.set('song', name);
|
||||
p.set('seed', String(hashString(name)));
|
||||
if (venueSel.value || venueEnabled) p.set('venue', '1');
|
||||
if (venueSel.value) p.set('venueSel', venueSel.value);
|
||||
return `/?${p}`;
|
||||
};
|
||||
|
||||
// Deep link for one figure: song + seed + the frame it shows, so clicking a
|
||||
// still opens the exact image in the app. Built by the shared preview
|
||||
// component (src/ui/preview.js) — every preview page links the same way.
|
||||
const shotSpec = (rowName, shot) => ({
|
||||
song: rowName,
|
||||
seed: hashString(rowName),
|
||||
frame: shot.frame,
|
||||
venue: venueEnabled || !!venueSel.value,
|
||||
venueSel: venueSel.value || null,
|
||||
});
|
||||
|
||||
/**
|
||||
* One song's row.
|
||||
@@ -192,6 +213,8 @@ function renderRow(row) {
|
||||
if (shot.moment === 'climax') fig.className = 'climax';
|
||||
const canvas = document.createElement('canvas');
|
||||
fig.appendChild(canvas);
|
||||
// Clicking a still opens the exact frame in the app.
|
||||
if (Number.isFinite(shot.frame)) previewLink(canvas, shotSpec(row.name, shot));
|
||||
const cap = document.createElement('figcaption');
|
||||
const mm = Math.floor(shot.time / 60);
|
||||
const ss = String(Math.round(shot.time % 60)).padStart(2, '0');
|
||||
@@ -282,7 +305,7 @@ async function build() {
|
||||
...row,
|
||||
frames: undefined,
|
||||
shots: await Promise.all((row.frames || []).map(async (f) => ({
|
||||
time: f.time, kind: f.kind, scene: f.scene, act: f.act,
|
||||
time: f.time, frame: f.frame, kind: f.kind, scene: f.scene, act: f.act,
|
||||
tension: f.tension, journey: f.journey, moment: f.moment,
|
||||
blob: await pixelsToBlob(f.pixels, STRIP.width, STRIP.height),
|
||||
}))),
|
||||
|
||||
Reference in New Issue
Block a user