From bd57dd72b3bd205e42898fec24864b9d665b699f Mon Sep 17 00:00:00 2001 From: 3dtours Date: Tue, 29 Sep 2026 17:09:01 +0700 Subject: [PATCH] EXPOSURE reads in stops: -5..+5, two decimals, on an unchanged store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tone spec draws EXPOSURE as `min="-5" max="5" step="0.01"` with a `0.00` readout, and the app's row was still the original whole-unit knob: -10..+10 in one step, printed as a bare `+2`. The recipe file behind it has always carried units where one unit is EV_PER_UNIT = 0.25 of a stop, which is the same ±2.5 EV travel, so the two are the same range said two ways. The def now talks stops on its face and units in the store: `min: -5, max: 5, step: 0.01`, a `twoStops` readout (`0.00`, `+0.50`, `-0.25`, no unit because the label is the unit), and accessors that translate — `get` multiplies by EV_PER_UNIT, `set` divides and rounds to 1/10000. EV_PER_UNIT is exported for that one use. Because the ×10 of `deepen` is not applied here and the store's field is untouched, a recipe that shipped with EXPOSURE 2 still means the two units it always meant: nothing gets brighter or darker, and half a stop is still 2 units in the file. The knob steps by 0.01, which is 0.04 units — the slider can express a quarter stop where the old one could only express quarter stops, and everything between them. TINT and TEMPERATURE keep the web's ranges (tint ±100 on the ±10 store scale, temperature 2500..10000 K). The spec's ±150 tint and its 2000..50000 K temperature were reviewed and left as they are: the tint at ±100 is already what `deepen` publishes and the wider pair would move stored looks. Verified: - `npx tsc --noEmit` clean; `npm run build` clean. - Repo checks re-run green: `highlight-knee-check`, `auto-tone-check`, `half-check`, `white-level-check`, `preview-match-check` — the last two render real looks through the engine, so a shifted EXPOSURE scale would have shown up as a brightness change in recipes that carry a non-zero one. - Live browser pass on the deployed build: the row reports `min=-5 max=5 step=0.01`, prints `+1.00` at a full stop, `+0.01` at a hundredth, `-0.25` at a quarter stop down and `0.00` at rest, and EXPOSURE +1.00 still lifts the graded frame (mean luma 184.3 -> 203.4). ponytail: the CREATE RECIPES form (RecipeCreatePanel) still edits a sim's raw adjustments in store units, EXPOSURE included. Leave it there until that form is asked for stops: it is a store editor, not the develop panel, and every field on it is in the same units. Co-authored-by: PenguinHarness --- docker/frontend/shared/utils/colorUtils.ts | 2 +- docker/frontend/shared/utils/paramDefs.ts | 27 +++++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) 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',