web: a film sim is colour and tone only
The ten PHOTO STYLE sims now carry nothing but their stock's own grade, and each is named for the stock it stands for: PROVIA, VELVIA, CLASSIC CHROME, CLASSIC VIVID (Velvia spliced with Classic Chrome at the blue row), CLASSIC NEGATIVE, ASTIA, ETERNA, ACROS, LC STREETLIFE CLASSIC, LC STREETLIFE VIVID. Grain, clarity, saturation and light moves were dropped from their `adjustments`, so a sim is a clean starting point and the general knobs read their defaults while the look still lands on the pixels. LC STREETLIFE VIVID keeps the one brightness step its stock needs, but as SIM_EXPOSURE_BIAS in colorUtils rather than as an adjustment: it is folded in where the Exposure slider applies, so the picture gets the lift and the parameter stays at 0. Also in this checkpoint: the watermark/GPS boxes and their colour pickers, the WATERMARK chip column, the real admin stats, and the fix that stopped presets from doubling and a frame from refusing to come off when a photo was reopened (/file is the finished render, /base the editable pixels).
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// Thin wrapper over the API container. Same origin in production (nginx proxies
|
||||
// /api), the Vite dev server proxies it too — so no base URL, no CORS.
|
||||
import type { Recipe } from '../shared/types';
|
||||
import { shrinkForUpload } from './engine/imageOps';
|
||||
import { shrinkForUpload, resizedJpeg } from './engine/imageOps';
|
||||
|
||||
// Upload caps. The API + nginx both refuse oversized bodies, so shrink in the
|
||||
// browser first; the server then sniffs the bytes and requires the declared
|
||||
@@ -216,6 +216,9 @@ export const api = {
|
||||
// labels ride the query string, since the body is the image itself.
|
||||
listPhotos: () => call<{ photos: Photo[] }>('/photos'),
|
||||
listMyPhotos: () => call<{ photos: Photo[] }>('/photos/mine'),
|
||||
// The name of a coordinate, for the stamp. Resolved server-side (the browser
|
||||
// has no OS geocoder), so a hit is a network round trip; null = no name.
|
||||
place: (lat: number, lng: number) => call<{ place: string | null }>(`/place?lat=${lat}&lng=${lng}`),
|
||||
// Film-strip ratings, keyed by subject (`photo:<id>` or `look:<TAG>`). Public:
|
||||
// any visitor may read the tallies and cast one vote per frame.
|
||||
ratings: () => call<{ ratings: Record<string, Rating> }>('/ratings'),
|
||||
@@ -242,6 +245,25 @@ export const api = {
|
||||
// The owner's own delete; an admin may pass any id here too.
|
||||
deletePhoto: (id: number) => call<void>(`/photos/${id}`, { method: 'DELETE' }),
|
||||
photoUrl: (id: number) => `/api/photos/${id}/file`,
|
||||
// The editable base beside a saved render — the pixels the look was applied
|
||||
// to, written right after the photo itself. Opening a saved photo loads this,
|
||||
// so its look lands on the original instead of a second time on its own
|
||||
// output. 404 for a photo saved before the base existed.
|
||||
photoBaseUrl: (id: number) => `/api/photos/${id}/base`,
|
||||
putPhotoBase: async (id: number, bytes: Uint8Array) => {
|
||||
// Always JPEG, always inside an upload's caps: the server compares the
|
||||
// declared type against the bytes' magic number, and a base is only ever
|
||||
// read back by the engine (`0` = no byte budget, so a copy already inside
|
||||
// `MAX_PHOTO_DIM` is still re-encoded rather than passed through).
|
||||
const out = await shrinkForUpload(bytes, MAX_PHOTO_DIM, 0);
|
||||
const res = await fetch(`/api/photos/${id}/base`, {
|
||||
method: 'PUT',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'content-type': 'image/jpeg' },
|
||||
body: out as unknown as BodyInit,
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
},
|
||||
// The look a photo was uploaded with, as the `.recipe` file the app imports on
|
||||
// a scan. Only answers for a photo the curator ticked into the `qr` section.
|
||||
presetUrl: (id: number) => `/api/photos/${id}/preset.recipe`,
|
||||
|
||||
Reference in New Issue
Block a user