EXPOSURE reads in stops: -5..+5, two decimals, on an unchanged store
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 <noreply@penguin.local>
This commit is contained in:
@@ -178,7 +178,7 @@ const SIM_EXPOSURE_BIAS_EV: Partial<Record<BaseFilter, number>> = {
|
|||||||
// button decides. Raise it toward 0.5 for Lightroom's full travel, and accept
|
// 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
|
// that every recipe already saved with a non-zero EXPOSURE gets brighter with
|
||||||
// it.
|
// 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
|
// 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
|
// exportEngine. Separate from the colour matrix on purpose: the matrix works on
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ColorAdjustments } from '../types';
|
import { ColorAdjustments } from '../types';
|
||||||
|
import { EV_PER_UNIT } from './colorUtils';
|
||||||
|
|
||||||
// Static metadata for the continuous (slider) adjustments of the IQ/WB/FILTERS
|
// Static metadata for the continuous (slider) adjustments of the IQ/WB/FILTERS
|
||||||
// tabs — shared between AdjustmentPanel (chips + slider rows) and App (which
|
// 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 sign = (v: number) => (v > 0 ? `+${v}` : String(v));
|
||||||
const oneEv = (v: number) => `${v > 0 ? '+' : ''}${v.toFixed(1)} EV`;
|
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
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// in kelvin, an amount that only goes up — keeps its own scale, because -100 of
|
||||||
// goes up — keeps its own scale, because -100 of a percentage is nothing.
|
// 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 HUNDRED = new Set(['contrast', 'color', 'vibrance', 'highlight', 'shadow', 'whites', 'blacks', 'tint', 'denoise', 'clarity', 'dehaze', 'sharpening']);
|
||||||
const deepen = (def: ContinuousParamDef): ContinuousParamDef =>
|
const deepen = (def: ContinuousParamDef): ContinuousParamDef =>
|
||||||
HUNDRED.has(def.key)
|
HUNDRED.has(def.key)
|
||||||
@@ -45,13 +52,17 @@ export const PARAM_DEFS: {
|
|||||||
{
|
{
|
||||||
key: 'exposure',
|
key: 'exposure',
|
||||||
label: 'EXPOSURE',
|
label: 'EXPOSURE',
|
||||||
min: -10,
|
min: -5,
|
||||||
max: 10,
|
max: 5,
|
||||||
step: 1,
|
step: 0.01,
|
||||||
defaultValue: 0,
|
defaultValue: 0,
|
||||||
display: sign,
|
display: twoStops,
|
||||||
get: (a) => a.exposure,
|
// Stops out, units in: the store has carried ±10 units since the first
|
||||||
set: (v) => ({ exposure: v }),
|
// 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',
|
key: 'ev',
|
||||||
|
|||||||
Reference in New Issue
Block a user