android: keep film sims colour-only, and fix the Vivid exposure in config
A sim is a stock, not an edit: its whole look must come out of the baseFilter colour matrix and the FILM_TONE curve, so every entry in FILM_SIMS now carries the neutral DEFAULT_ADJUSTMENTS verbatim. Dropped the effects that had been riding along — grain on ACRIPES, clarity+grain on LC STREETLIFE CLASSIC, and exposure/saturation/clarity/shadow/highlight on LC STREETLIFE VIVID. The Vivid brightness is real, though, so it moves into STOCK_EXPOSURE in colorUtils, keyed by baseFilter: a fixed exposure move of the look (2), the same maths the EXPOSURE slider runs (1.04x + 0.03). Keyed by stock because that is what the preview and both exporters know — baseFilter travels with every recipe, an armed sim id does not. A sim's own adjustments stay neutral, so the EXPOSURE slider reads 0 on it. Also records the naming rule in the FILM_SIMS header: COLOUR AND TONE ONLY, and the real stock each sim stands for (reference only, the UI keeps the sim names).
This commit is contained in:
+19
-4
@@ -150,6 +150,18 @@ function normalizeGainLuma(gain: number[]): { r: number; g: number; b: number }
|
||||
return { r: gain[0] * s, g: gain[1] * s, b: gain[2] * s };
|
||||
}
|
||||
|
||||
// Brightness a stock carries BY ITSELF, in the EXPOSURE slider's own units.
|
||||
// It is part of the look, not a knob the user set, so it never reaches the
|
||||
// panel (a sim's adjustments stay neutral — see FILM_SIMS in defaultRecipes)
|
||||
// and the slider reads 0 while the render is already lifted. Keyed by stock
|
||||
// because that is what the renderers know: `baseFilter` travels with every
|
||||
// recipe, an armed sim id does not. ponytail: one entry, one stock; if two
|
||||
// sims ever need to share a matrix with different brightness, the key has to
|
||||
// move to the sim id.
|
||||
const STOCK_EXPOSURE: Partial<Record<BaseFilter, number>> = {
|
||||
'leica-vivid': 2, // LC STREETLIFE VIVID reads bright straight off the shutter
|
||||
};
|
||||
|
||||
// Generate a 4x5 ColorMatrix (array of 20 floats) based on base style and
|
||||
// adjustments. Tone-domain knobs (dynamicRange/highlight/shadow) and the
|
||||
// exposure-compensation EV gain are intentionally NOT part of the matrix —
|
||||
@@ -157,7 +169,9 @@ function normalizeGainLuma(gain: number[]): { r: number; g: number; b: number }
|
||||
// and EV is applied either as hardware AE bias (camera) or by the caller via
|
||||
// applyExposureGain (library), never doubled.
|
||||
export function getSkiaColorMatrix(baseFilter: BaseFilter, adj: ColorAdjustments): number[] {
|
||||
const exposure = adj.exposure;
|
||||
// The stock's own brightness rides with the slider: same maths, same band
|
||||
// (it is a property of the look, not of this photo's edit).
|
||||
const exposure = adj.exposure + (STOCK_EXPOSURE[baseFilter] ?? 0);
|
||||
const contrast = adj.contrast;
|
||||
const saturation = adj.saturation;
|
||||
const temperature = adj.temperature;
|
||||
@@ -270,9 +284,10 @@ export function getSkiaColorMatrix(baseFilter: BaseFilter, adj: ColorAdjustments
|
||||
// digital era: ~20% of chroma gain (still well under Velvia's 39%, so it
|
||||
// sparkles instead of shouting) on the same warm R>G>B bias CLASSIC has, so
|
||||
// VIVID is a brighter read of one look rather than a second, neutral one.
|
||||
// No positive lift here: the brightness is the sim's own exposure value
|
||||
// (defaultRecipes), which keeps the matrix inside 0..1 and pure white out of
|
||||
// the clip — white lands at most at 1.000 on the red row.
|
||||
// No positive lift here either, so the matrix stays inside 0..1 and pure
|
||||
// white stays out of the clip — white lands at most at 1.000 on the red row.
|
||||
// The brightness this look is known for is STOCK_EXPOSURE below: a fixed
|
||||
// exposure move of the stock itself, never a chroma trick.
|
||||
matrix = [
|
||||
1.125, -0.100, -0.010, 0, -0.015,
|
||||
-0.055, 1.110, -0.055, 0, -0.005,
|
||||
|
||||
+39
-19
@@ -29,13 +29,37 @@ export const DEFAULT_ADJUSTMENTS: ColorAdjustments = {
|
||||
exposureCompensation: 0,
|
||||
};
|
||||
|
||||
// Standard Fuji film simulations. These are the fixed "look" presets shown at
|
||||
// the front of the PRESETS chip row: tapping one applies the simulation — its
|
||||
// baseFilter colour matrix plus whatever the stock itself carries outside the
|
||||
// matrix (its FILM_TONE curve, and a grain amount when the grain IS the look,
|
||||
// as on Acros). Every other knob still resets to its neutral default (0 /
|
||||
// auto), so a sim is a clean starting point, never a baked recipe; any further
|
||||
// look (DR, CC, contrast, more grain) is added by the user afterwards.
|
||||
// Standard film simulations. These are the fixed "look" presets shown at the
|
||||
// front of the PRESETS chip row.
|
||||
//
|
||||
// RULE — a sim carries COLOUR AND TONE ONLY, never an effect. Its whole look
|
||||
// must come from its baseFilter colour matrix plus its FILM_TONE curve
|
||||
// (toneShader.ts): the two things that *define* the stock. No grain, no
|
||||
// clarity, no exposure/saturation/shadow/highlight move — those are edits the
|
||||
// user makes afterwards, never something a sim bakes in. That is why every sim
|
||||
// below shares the neutral DEFAULT_ADJUSTMENTS verbatim; if a look ever needs
|
||||
// more, put it in the matrix or the tone curve instead.
|
||||
//
|
||||
// The one exception is a stock that is simply BRIGHTER: that is a fixed
|
||||
// exposure move of the look itself, so it lives in STOCK_EXPOSURE
|
||||
// (colorUtils.ts), keyed by baseFilter, where the preview and both exporters
|
||||
// already pick it up. It never lands here and never reaches the panel, so the
|
||||
// EXPOSURE slider still reads 0 on such a sim — LC STREETLIFE VIVID (+2).
|
||||
//
|
||||
// Reference only — what each sim stands for, so the colour choices can be
|
||||
// checked against the real stock. The UI keeps the sim names below.
|
||||
//
|
||||
// PROVIPES → PROVIA
|
||||
// VELVIPES → VELVIA
|
||||
// CLASSIC CHRIPES → CLASSIC CHROME
|
||||
// CLASSIC VIVIDIPES → VELVIA + CLASSIC CHROME (Chrome's blue row over
|
||||
// Velvia's red and green rows — see colorUtils.ts)
|
||||
// CLASSIC NEGIPES → Classic Negative
|
||||
// ASTIPES → Astia
|
||||
// ETERNIPES → Eterna
|
||||
// ACRIPES → Acros
|
||||
// LC STREETLIFE CLASSIC → Leica Authentic Classic
|
||||
// LC STREETLIFE VIVID → Leica Vivid
|
||||
export interface FilmSim {
|
||||
id: string; // Recipe-like id used only while the sim is the current selection
|
||||
name: string;
|
||||
@@ -51,20 +75,16 @@ export const FILM_SIMS: FilmSim[] = [
|
||||
{ id: 'sim-classic-neg', name: 'CLASSIC NEGIPES', baseFilter: 'classic-neg', adjustments: { ...DEFAULT_ADJUSTMENTS } },
|
||||
{ id: 'sim-astia', name: 'ASTIPES', baseFilter: 'astia', adjustments: { ...DEFAULT_ADJUSTMENTS } },
|
||||
{ id: 'sim-eterna', name: 'ETERNIPES', baseFilter: 'eterna', adjustments: { ...DEFAULT_ADJUSTMENTS } },
|
||||
{ id: 'sim-acros', name: 'ACRIPES', baseFilter: 'monochrome', adjustments: { ...DEFAULT_ADJUSTMENTS, grain: 3 } },
|
||||
{ id: 'sim-acros', name: 'ACRIPES', baseFilter: 'monochrome', adjustments: { ...DEFAULT_ADJUSTMENTS } },
|
||||
// The rangefinder digital look (M/Q/SL) is not a film stock, so it ships as its own
|
||||
// pair instead of one look that has to compromise:
|
||||
// pair instead of one look that has to compromise. Both read off the
|
||||
// 'leica'/'leica-vivid' colour matrices alone (neither has a FILM_TONE
|
||||
// entry):
|
||||
// CLASSIC — the stock character: deep solid darks, natural shadows, rich but
|
||||
// quiet colour. Its micro-contrast is CLARITY (a local unsharp mask in both
|
||||
// renderers), never a global saturation boost, and a whisker of grain
|
||||
// keeps it feeling like film rather than digital sharpness.
|
||||
// VIVID — the same channel separation opened up for the digital era: lifted
|
||||
// exposure, lifted shadows, a little colour. It reads bright and flattering
|
||||
// straight off the shutter, no editing pass needed.
|
||||
// Both share the 'leica'/'leica-vivid' matrices; the tonal half is the sim's
|
||||
// own adjustments, which every apply path now carries into the panel.
|
||||
{ id: 'sim-leica', name: 'LC STREETLIFE CLASSIC', baseFilter: 'leica', adjustments: { ...DEFAULT_ADJUSTMENTS, clarity: 2, grain: 1 } },
|
||||
{ id: 'sim-leica-vivid', name: 'LC STREETLIFE VIVID', baseFilter: 'leica-vivid', adjustments: { ...DEFAULT_ADJUSTMENTS, exposure: 2, saturation: 2, clarity: 2, shadow: 1, highlight: -1 } },
|
||||
// quiet colour.
|
||||
// VIVID — the same channel separation opened up for the digital era.
|
||||
{ id: 'sim-leica', name: 'LC STREETLIFE CLASSIC', baseFilter: 'leica', adjustments: { ...DEFAULT_ADJUSTMENTS } },
|
||||
{ id: 'sim-leica-vivid', name: 'LC STREETLIFE VIVID', baseFilter: 'leica-vivid', adjustments: { ...DEFAULT_ADJUSTMENTS } },
|
||||
];
|
||||
|
||||
// Film sim → full Recipe (never persisted; sims are applied on the fly).
|
||||
|
||||
Reference in New Issue
Block a user