8c6e7930db
`docker/` now holds the whole web build — frontend (Vite + React + CanvasKit),
backend (Fastify + SQLite) and the compose file — so the folder can be moved to
another machine and run without the React Native project:
cd docker && cp .env.example .env && docker compose up -d --build
Only `${WEB_PORT:-8090}` is published; nginx serves the SPA and proxies /api to
the `api` container over Docker's DNS. Photos never reach the server.
The shared render code is vendored into `docker/frontend/shared/` and aliased to
a CanvasKit shim, so the app's own frameUtils/toneShader/jpegDpi run unchanged.
Fix the all-black render on GPU surfaces: `MakeWebGLCanvasSurface` creates a
separate WebGL context per call, and a texture from one context cannot be
sampled by a surface on another — so any pass that drew a snapshot onto a second
surface (output sharpen, screen sharpen, polaroid/wallframe cards) came out
solid black, while the raster fallback was correct. Use one shared
GrDirectContext + MakeRenderTarget instead.
Verified in headless Chromium against the running stack: 12MP JPEG in, preview
mean=120.5 sd=60.5, export 2048x1536 mean=107.2 sd=62.1, JFIF density 300/300,
EXIF present, no console errors; health/signup/login/me/recipes all 2xx through
the nginx proxy.
203 lines
4.3 KiB
TypeScript
203 lines
4.3 KiB
TypeScript
import { ColorAdjustments } from '../types';
|
|
|
|
// Static metadata for the continuous (slider) adjustments of the IQ/WB/FILTERS
|
|
// tabs — shared between AdjustmentPanel (chips + slider rows) and App (which
|
|
// needs the open row's bounds to drive drag-on-the-image adjustment). The get
|
|
// /set accessors are stateless so one table serves every render.
|
|
export interface ContinuousParamDef {
|
|
key: string;
|
|
label: string;
|
|
min: number;
|
|
max: number;
|
|
step: number;
|
|
defaultValue: number;
|
|
display: (v: number) => string;
|
|
get: (a: ColorAdjustments) => number;
|
|
set: (v: number) => Partial<ColorAdjustments>;
|
|
// PRO-only knob: the LITE build greys the chip out (PRO corner tag) and
|
|
// cannot open its row, so the value stays at its default there.
|
|
pro?: boolean;
|
|
}
|
|
|
|
const sign = (v: number) => (v > 0 ? `+${v}` : String(v));
|
|
const oneEv = (v: number) => `${v > 0 ? '+' : ''}${v.toFixed(1)} EV`;
|
|
|
|
export const PARAM_DEFS: {
|
|
iq: ContinuousParamDef[];
|
|
wb: ContinuousParamDef[];
|
|
filters: ContinuousParamDef[];
|
|
} = {
|
|
iq: [
|
|
{
|
|
key: 'exposure',
|
|
label: 'EXPOSURE',
|
|
min: -10,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: sign,
|
|
get: (a) => a.exposure,
|
|
set: (v) => ({ exposure: v }),
|
|
},
|
|
{
|
|
key: 'ev',
|
|
label: 'EV',
|
|
min: -3,
|
|
max: 3,
|
|
step: 0.5,
|
|
defaultValue: 0,
|
|
display: oneEv,
|
|
get: (a) => a.exposureCompensation ?? 0,
|
|
set: (v) => ({ exposureCompensation: v }),
|
|
},
|
|
{
|
|
key: 'contrast',
|
|
label: 'CONTRAST',
|
|
min: -10,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: sign,
|
|
get: (a) => a.contrast,
|
|
set: (v) => ({ contrast: v }),
|
|
},
|
|
{
|
|
key: 'color',
|
|
label: 'COLOR',
|
|
min: -10,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: sign,
|
|
get: (a) => a.saturation,
|
|
set: (v) => ({ saturation: v }),
|
|
},
|
|
{
|
|
key: 'vibrance',
|
|
label: 'VIBRANCE',
|
|
min: -10,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: sign,
|
|
get: (a) => a.vibrance ?? 0,
|
|
set: (v) => ({ vibrance: v }),
|
|
},
|
|
{
|
|
key: 'highlight',
|
|
label: 'HIGHLIGHT',
|
|
min: -10,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: sign,
|
|
get: (a) => a.highlight ?? 0,
|
|
set: (v) => ({ highlight: v }),
|
|
},
|
|
{
|
|
key: 'shadow',
|
|
label: 'SHADOW',
|
|
min: -10,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: sign,
|
|
get: (a) => a.shadow ?? 0,
|
|
set: (v) => ({ shadow: v }),
|
|
},
|
|
],
|
|
wb: [
|
|
{
|
|
key: 'temperature',
|
|
label: 'COLOR TEMP',
|
|
min: 2500,
|
|
max: 10000,
|
|
step: 100,
|
|
defaultValue: 5500,
|
|
display: (v) => `${v}K`,
|
|
get: (a) => a.temperature,
|
|
set: (v) => ({ temperature: v }),
|
|
},
|
|
{
|
|
key: 'tint',
|
|
label: 'TINT',
|
|
min: -10,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: sign,
|
|
get: (a) => a.tint ?? 0,
|
|
set: (v) => ({ tint: v }),
|
|
},
|
|
],
|
|
filters: [
|
|
{
|
|
key: 'denoise',
|
|
label: 'NOISE REDUCTION',
|
|
min: -10,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: String,
|
|
get: (a) => a.denoise,
|
|
set: (v) => ({ denoise: v }),
|
|
},
|
|
{
|
|
key: 'clarity',
|
|
label: 'CLARITY',
|
|
min: -10,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: sign,
|
|
get: (a) => a.clarity,
|
|
set: (v) => ({ clarity: v }),
|
|
},
|
|
{
|
|
key: 'sharpening',
|
|
label: 'SHARPENING',
|
|
min: -10,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: String,
|
|
get: (a) => a.sharpening ?? 0,
|
|
set: (v) => ({ sharpening: v }),
|
|
},
|
|
{
|
|
key: 'grain',
|
|
label: 'MONOCHROME GRAIN',
|
|
min: 0,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: String,
|
|
get: (a) => a.grain,
|
|
set: (v) => ({ grain: v }),
|
|
},
|
|
{
|
|
key: 'hdf',
|
|
label: 'HDF EFFECT',
|
|
pro: true,
|
|
min: 0,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: String,
|
|
get: (a) => a.hdf ?? 0,
|
|
set: (v) => ({ hdf: v }),
|
|
},
|
|
{
|
|
key: 'vignette',
|
|
label: 'VIGNETTING',
|
|
min: 0,
|
|
max: 10,
|
|
step: 1,
|
|
defaultValue: 0,
|
|
display: String,
|
|
get: (a) => a.vignette ?? 0,
|
|
set: (v) => ({ vignette: v }),
|
|
},
|
|
],
|
|
};
|