diff --git a/docker/frontend/public/assets/samples/s1015.jpg b/docker/frontend/public/assets/samples/s1015.jpg deleted file mode 100644 index be511e5..0000000 Binary files a/docker/frontend/public/assets/samples/s1015.jpg and /dev/null differ diff --git a/docker/frontend/public/assets/samples/s1025.jpg b/docker/frontend/public/assets/samples/s1025.jpg deleted file mode 100644 index 4b342ff..0000000 Binary files a/docker/frontend/public/assets/samples/s1025.jpg and /dev/null differ diff --git a/docker/frontend/public/assets/samples/s1035.jpg b/docker/frontend/public/assets/samples/s1035.jpg deleted file mode 100644 index 89bfa0e..0000000 Binary files a/docker/frontend/public/assets/samples/s1035.jpg and /dev/null differ diff --git a/docker/frontend/public/assets/samples/s1043.jpg b/docker/frontend/public/assets/samples/s1043.jpg deleted file mode 100644 index ea03952..0000000 Binary files a/docker/frontend/public/assets/samples/s1043.jpg and /dev/null differ diff --git a/docker/frontend/public/assets/samples/s1050.jpg b/docker/frontend/public/assets/samples/s1050.jpg deleted file mode 100644 index b888250..0000000 Binary files a/docker/frontend/public/assets/samples/s1050.jpg and /dev/null differ diff --git a/docker/frontend/public/assets/samples/s1069.jpg b/docker/frontend/public/assets/samples/s1069.jpg deleted file mode 100644 index 262bc0e..0000000 Binary files a/docker/frontend/public/assets/samples/s1069.jpg and /dev/null differ diff --git a/docker/frontend/src/Landing.tsx b/docker/frontend/src/Landing.tsx index 9d08277..bc20244 100644 --- a/docker/frontend/src/Landing.tsx +++ b/docker/frontend/src/Landing.tsx @@ -22,8 +22,6 @@ const STORE_LINKS = { type Txt = { en: string; vi: string }; -const SAMPLE = (n: number) => `/assets/samples/s${n}.jpg`; - // No page can really stop a save — the bytes are already on the machine. What it // can do is refuse to hand over more pixels than it draws: the photo is decoded, // redrawn at the size of its box times the screen's pixel ratio (2x at most) and @@ -90,27 +88,11 @@ interface Look { meta: string; photo: string; filter: string; - // The subject this frame is rated under: `look:` for a built-in look, - // `photo:` for a contribution. Filled in where the reel is assembled. + // The subject this frame is rated under: `photo:`. Stamped when the reel + // is assembled so the literal cannot drift from the key form. rateKey: string; } -// 35mm reel: eight looks over six sample negatives, each with its own CSS -// emulation. ponytail: CSS filters, not the CanvasKit pipeline — a landing page -// does not need a 12MP render; swap in real graded stills when we have them. -// The built-in looks carry no `rateKey` of their own — the reel stamps it from -// the tag when it is assembled, so the literal cannot drift from the key form. -const REEL: Omit[] = [ - { tag: '#KODAK_PORTRA_400', title: { en: 'Golden Hour Portrait', vi: 'Chân dung giờ vàng' }, meta: 'ISO 400 · GRAIN 35 · WARMTH +18', photo: SAMPLE(1015), filter: 'sepia(.16) saturate(1.18) contrast(1.04) brightness(1.04)' }, - { tag: '#FUJI_CLASSIC_CHROME', title: { en: 'Quiet Street, 7am', vi: 'Phố vắng, 7 giờ sáng' }, meta: 'ISO 200 · GRAIN 20 · WARMTH -6', photo: SAMPLE(1025), filter: 'saturate(.78) contrast(1.14) hue-rotate(-6deg)' }, - { tag: '#CYBER_NEON_88', title: { en: 'Neon Crossing', vi: 'Ngã tư neon' }, meta: 'ISO 1600 · GRAIN 45 · WARMTH +4', photo: SAMPLE(1035), filter: 'saturate(1.7) contrast(1.25) hue-rotate(268deg)' }, - { tag: '#CINESTILL_800T', title: { en: 'Harbour Tungsten', vi: 'Cảng đèn vàng' }, meta: 'ISO 800 · GRAIN 40 · WARMTH -14', photo: SAMPLE(1043), filter: 'saturate(1.1) contrast(1.1) hue-rotate(186deg) brightness(.96)' }, - { tag: '#ILFORD_HP5_BW', title: { en: 'Alley Study', vi: 'Hẻm nhỏ' }, meta: 'ISO 400 · GRAIN 55 · MONO', photo: SAMPLE(1050), filter: 'grayscale(1) contrast(1.35) brightness(1.02)' }, - { tag: '#VELVIA_50', title: { en: 'Ridge Line', vi: 'Sống núi' }, meta: 'ISO 50 · GRAIN 12 · WARMTH +6', photo: SAMPLE(1069), filter: 'saturate(1.55) contrast(1.22)' }, - { tag: '#ACROS_100', title: { en: 'Stone Facade', vi: 'Mặt tường đá' }, meta: 'ISO 100 · GRAIN 30 · MONO', photo: SAMPLE(1025), filter: 'grayscale(1) contrast(1.18) brightness(1.06)' }, - { tag: '#ETRNA_VINTAGE', title: { en: 'Faded Postcard', vi: 'Bưu thiếp bạc màu' }, meta: 'ISO 200 · GRAIN 25 · WARMTH +10', photo: SAMPLE(1015), filter: 'sepia(.28) saturate(.9) contrast(.94) brightness(1.08)' }, -]; - type PresetGroup = 'landscape' | 'portrait' | 'streetlife'; interface Preset { @@ -291,13 +273,11 @@ export function Landing() { } }, []); - // Contributed stills join the reel after the six built-in looks. Every frame - // is repeated once, so the marquee's -50% translation still loops seamlessly. + // The reel holds only stills the studio's own uploads put in the strip slot. + // Every frame is repeated once, so the marquee's -50% translation still loops + // seamlessly. const reel = useMemo( () => [ - // A built-in look is rated under its own tag, so the eight samples carry - // scores just like a contributed frame does. - ...REEL.map((l) => ({ ...l, rateKey: `look:${l.tag.replace(/^#/, '')}` })), // The labels the uploader's studio stored — the tagline, the title and // the ISO/grain line. A contribution from before those existed keeps the // plain community defaults. @@ -315,33 +295,31 @@ export function Landing() { // The three live sections. One random photo out of each set, drawn once per // page load so the preview does not change under the visitor's eyes on every - // render; the built-in sample stands in while a section has no curation. A - // photo the curator put in several sections can turn up in each of them. + // render; a section whose slot holds nothing simply shows no frame. A photo + // the curator put in several sections can turn up in each of them. const slotPhoto = useMemo(() => { const pick = (slot: PhotoSlot) => { const list = photos.filter((p) => p.slots.includes(slot)); return list.length ? list[Math.floor(Math.random() * list.length)] : null; }; - const src = (slot: PhotoSlot, fallback: string) => { + const src = (slot: PhotoSlot) => { const p = pick(slot); - return p ? api.photoUrl(p.id) : fallback; + return p ? api.photoUrl(p.id) : null; }; // The QR card's payload is the curated photo's own look, as the link the - // phone app downloads it from. The card's mock address stands in while the - // section holds nothing that has a look to hand out. + // phone app downloads it from. // The tester cycles every still the curator put in its slot, so a recipe - // can be checked against more than one frame. The built-in sample stands - // in on its own while the slot holds nothing. + // can be checked against more than one frame. const curated = photos.filter((p) => p.slots.includes('tester')).map((p) => api.photoUrl(p.id)); - const testers = curated.length ? curated : [SAMPLE(1043)]; + const testers = curated; const qr = pick('qr'); return { testers, // A plain step count on top of one random start keeps the frame random // per load without an effect that would reset it on every re-render. testerStart: Math.floor(Math.random() * testers.length), - creator: src('creator', SAMPLE(1015)), - qr: qr ? api.photoUrl(qr.id) : SAMPLE(1069), + creator: src('creator'), + qr: qr ? api.photoUrl(qr.id) : null, qrLink: qr?.hasPreset ? new URL(api.presetUrl(qr.id), window.location.origin).href : `${window.location.origin}/app`, }; }, [photos]); @@ -724,11 +702,13 @@ export function Landing() {
- + {slotPhoto.testers.length > 0 && ( + + )} {slotPhoto.testers.length > 1 && ( <>