diff --git a/docker/frontend/shared/utils/colorUtils.ts b/docker/frontend/shared/utils/colorUtils.ts index d84b870..a2d4aa0 100644 --- a/docker/frontend/shared/utils/colorUtils.ts +++ b/docker/frontend/shared/utils/colorUtils.ts @@ -178,7 +178,7 @@ const SIM_EXPOSURE_BIAS_EV: Partial> = { // button decides. Raise it toward 0.5 for Lightroom's full travel, and accept // that every recipe already saved with a non-zero EXPOSURE gets brighter with // it. -const EV_PER_UNIT = 0.25; +export const EV_PER_UNIT = 0.25; // The EXPOSURE knob and the sim bias, in stops, for the linear-light pass in // exportEngine. Separate from the colour matrix on purpose: the matrix works on diff --git a/docker/frontend/shared/utils/paramDefs.ts b/docker/frontend/shared/utils/paramDefs.ts index f62d550..51d69af 100644 --- a/docker/frontend/shared/utils/paramDefs.ts +++ b/docker/frontend/shared/utils/paramDefs.ts @@ -1,4 +1,5 @@ import { ColorAdjustments } from '../types'; +import { EV_PER_UNIT } from './colorUtils'; // Static metadata for the continuous (slider) adjustments of the IQ/WB/FILTERS // tabs — shared between AdjustmentPanel (chips + slider rows) and App (which @@ -21,6 +22,9 @@ export interface ContinuousParamDef { const sign = (v: number) => (v > 0 ? `+${v}` : String(v)); const oneEv = (v: number) => `${v > 0 ? '+' : ''}${v.toFixed(1)} EV`; +// The spec reads EXPOSURE to two decimals, so "+0.50" and "-0.25" and a plain +// "0.00" — the EV unit is already on the row's own label, not in its readout. +const twoStops = (v: number) => `${v > 0 ? '+' : ''}${v.toFixed(2)}`; // The bipolar develop knobs read -100..+100: the spec's own numbers (the panel // draws every TONE and PRESENCE slider from -100 to 100, and the tone-mapping doc @@ -28,8 +32,11 @@ const oneEv = (v: number) => `${v > 0 ? '+' : ''}${v.toFixed(1)} EV`; // every look has always been filed in, so the ×10 belongs to the slider and not // to the store: a look saved, shared or shipped in DEFAULT_RECIPES before this // still means exactly what it meant. A knob that is not bipolar — a temperature -// in kelvin, an exposure in stops, a grain size in percent, an amount that only -// goes up — keeps its own scale, because -100 of a percentage is nothing. +// in kelvin, an amount that only goes up — keeps its own scale, because -100 of +// a percentage is nothing. EXPOSURE is the odd one out: its slider is in stops +// (-5..+5, two decimals, the spec's own numbers) while the store still holds the +// ±10 units EV_PER_UNIT converts, so its accessors translate instead of scaling +// and every recipe's brightness is untouched. const HUNDRED = new Set(['contrast', 'color', 'vibrance', 'highlight', 'shadow', 'whites', 'blacks', 'tint', 'denoise', 'clarity', 'dehaze', 'sharpening']); const deepen = (def: ContinuousParamDef): ContinuousParamDef => HUNDRED.has(def.key) @@ -45,13 +52,17 @@ export const PARAM_DEFS: { { key: 'exposure', label: 'EXPOSURE', - min: -10, - max: 10, - step: 1, + min: -5, + max: 5, + step: 0.01, defaultValue: 0, - display: sign, - get: (a) => a.exposure, - set: (v) => ({ exposure: v }), + display: twoStops, + // Stops out, units in: the store has carried ±10 units since the first + // recipe, so the knob translates rather than replacing the scale. The + // /4 lands on a whole 0.04 for every step the slider can emit, which keeps + // the stored number readable in a recipe file. + get: (a) => (a.exposure ?? 0) * EV_PER_UNIT, + set: (v) => ({ exposure: Math.round((v / EV_PER_UNIT) * 1e4) / 1e4 }), }, { key: 'ev',