Files
RecipesCam/src/utils/defaultRecipes.ts
T
admin cbd0178cda android: port the web studio's HSL selective colour mixer
The rail gains an HSL tab between FX and FRAME, and the tab opens the mixer
ported from the web studio: eight hue bands, each with hue/sat/lum knobs on a
-10..+10 ruler, plus three whole-image moves behind an IMAGE divider. Band
weights are linear tents anchored on HSL_BANDS and summing to 1 at every hue, so
neighbouring bands hand over at exactly half strength and no hue falls in a gap.
Pixels under 8% saturation are gated out of the weights (rgb2hsl calls a neutral
red, so an ungated one would ride the RED band), while the whole-image LUM move
is added after the gate and still lifts a picture that -SAT has pulled to grey.
The mixer runs last in TONE_SKSL, so a band edit is judged on the colour the user
actually sampled.

The eyedropper is library-only: PICK arms a layer over the picture, a tap reads
the rendered pixel off the canvas, points the ruler at the nearest band and
hangs a panel carrying that band's three knobs. A live preview has no pixel to
read, so in camera mode the chip greys out - the mixer itself still runs there,
which is why the camera worklet's tone buffer had to grow from 10 slots to 40: a
missed slot would have pushed NaN into every uniform and taken the whole tone
pass down, not just HSL.

HSL is a LITE feature (the web studio builds it for guests too), and MONOCHROME
switches the mixer off outright - a mono stock has no hue to select. Checked on a
Xiaomi thor against an 8-band chart: +10 whole-image HUE rotates all eight bands
by +30 deg, -10 on one band's SAT flattens that band and leaves the other seven
alone, and a whole-image -SAT drops the camera preview's mean chroma 13.7 -> 0.4
with +LUM still lifting it 116 -> 166. Cut as 1.2.4 / versionCode 6.
2026-09-21 18:28:32 +07:00

311 lines
8.8 KiB
TypeScript

import { Recipe, ColorAdjustments } from '../types';
// Fallback values for every adjustable knob. Old recipes/sessions persisted in
// AsyncStorage lack the newer fields — merge this under stored JSON to keep
// every adjustment well-defined (see storageUtils.getAllRecipes / App session).
export const DEFAULT_ADJUSTMENTS: ColorAdjustments = {
exposure: 0,
contrast: 0,
saturation: 0,
temperature: 5500,
tint: 0,
wbRed: 0,
wbBlue: 0,
highlight: 0,
shadow: 0,
denoise: 0,
clarity: 0,
grain: 0,
colorChrome: 'none',
colorChromeBlue: 'none',
dynamicRange: 'auto',
sharpening: 0,
// The mixer's three whole-image moves. hslBands is deliberately absent: it is
// optional and `undefined` is what "no band moved" means — an empty object
// would make differs(adjustments, DEFAULT_ADJUSTMENTS) report a dirty recipe.
hslHue: 0,
hslSat: 0,
hslLum: 0,
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.
export interface FilmSim {
id: string; // Recipe-like id used only while the sim is the current selection
name: string;
baseFilter: Recipe['baseFilter'];
adjustments: ColorAdjustments;
}
export const FILM_SIMS: FilmSim[] = [
{ id: 'sim-provia', name: 'PROVIPES', baseFilter: 'provia', adjustments: { ...DEFAULT_ADJUSTMENTS } },
{ id: 'sim-velvia', name: 'VELVIPES', baseFilter: 'velvia', adjustments: { ...DEFAULT_ADJUSTMENTS } },
{ id: 'sim-classic-chrome', name: 'CLASSIC CHRIPES', baseFilter: 'classic-chrome', adjustments: { ...DEFAULT_ADJUSTMENTS } },
{ id: 'sim-classic-vivid', name: 'CLASSIC VIVIDIPES', baseFilter: 'classic-vivid', adjustments: { ...DEFAULT_ADJUSTMENTS } },
{ 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 } },
// 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:
// 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 } },
];
// Film sim → full Recipe (never persisted; sims are applied on the fly).
export function filmSimToRecipe(sim: FilmSim): Recipe {
return {
id: sim.id,
name: sim.name,
baseFilter: sim.baseFilter,
adjustments: { ...sim.adjustments },
frameId: 'none',
useGeotag: true,
};
}
export const DEFAULT_RECIPES: Recipe[] = [
{
id: 'classic-neg-default',
name: 'CLASSIC NEGIPES.',
baseFilter: 'classic-neg',
adjustments: {
exposure: 2,
contrast: 1,
saturation: -1,
temperature: 6300,
tint: 2,
highlight: -2,
shadow: 1,
denoise: 1,
clarity: 2,
grain: 4,
colorChrome: 'weak',
colorChromeBlue: 'none',
dynamicRange: 'auto',
sharpening: 0,
exposureCompensation: 0,
},
frameId: 'none',
useGeotag: true,
},
{
id: 'velvia-default',
name: 'VELVIPES VIVID',
baseFilter: 'velvia',
adjustments: {
exposure: 0,
contrast: 2,
saturation: 3,
temperature: 5500,
tint: 0,
highlight: 1,
shadow: -1,
denoise: 2,
clarity: 4,
grain: 1,
colorChrome: 'strong',
colorChromeBlue: 'none',
dynamicRange: 'auto',
sharpening: 0,
exposureCompensation: 0,
},
frameId: 'none',
useGeotag: true,
},
{
id: 'provia-default',
name: 'PROVIPES STD',
baseFilter: 'provia',
adjustments: {
exposure: 1,
contrast: 0,
saturation: 0,
temperature: 5000,
tint: 0,
highlight: 0,
shadow: 0,
denoise: 2,
clarity: 1,
grain: 2,
colorChrome: 'none',
colorChromeBlue: 'none',
dynamicRange: 'auto',
sharpening: 0,
exposureCompensation: 0,
},
frameId: 'none',
useGeotag: true,
},
{
id: 'acros-default',
name: 'ACRIPES MONO',
baseFilter: 'monochrome',
adjustments: {
exposure: 0,
// Low: the deep black comes from FILM_TONE's toe and the micro-contrast
// from CLARITY, both of which add separation WITHOUT flattening the grey
// ramp. The old 4 / highlight 3 / shadow -2 stack was three global
// contrast moves on top of a matrix that was already clipping — it
// bought its deep black by throwing away every step below 9% and every
// one above 91%.
contrast: 2,
saturation: 0,
temperature: 5000,
tint: 0,
highlight: 0,
shadow: 0,
denoise: 0,
clarity: 3,
grain: 5,
colorChrome: 'none',
colorChromeBlue: 'none',
dynamicRange: 'auto',
sharpening: 0,
exposureCompensation: 0,
},
frameId: 'none',
useGeotag: true,
},
{
id: 'retro-amber-default',
name: 'RETRO AMBER 🏮',
baseFilter: 'classic-neg',
adjustments: {
exposure: 3,
contrast: 2,
saturation: 1,
temperature: 7500,
tint: -2,
highlight: -3,
shadow: 2,
denoise: 3,
clarity: -2,
grain: 5,
colorChrome: 'strong',
colorChromeBlue: 'none',
dynamicRange: 'auto',
sharpening: 0,
exposureCompensation: 0,
},
frameId: 'classic-white',
useGeotag: true,
},
{
id: 'cinema-spring-default',
name: 'CINEMA SPRING',
baseFilter: 'none',
adjustments: {
exposure: 0,
contrast: 1,
saturation: 1,
temperature: 5800,
tint: 0,
highlight: -1,
shadow: 1,
denoise: 2,
clarity: 1,
grain: 3,
colorChrome: 'none',
colorChromeBlue: 'none',
dynamicRange: 'auto',
sharpening: 0,
exposureCompensation: 0,
},
frameId: 'none',
cinema: 'spring',
useGeotag: true,
},
{
id: 'cinema-summer-default',
name: 'CINEMA SUMMER',
baseFilter: 'none',
adjustments: {
exposure: 0,
contrast: 2,
saturation: 2,
temperature: 6000,
tint: -1,
highlight: 1,
shadow: -1,
denoise: 1,
clarity: 2,
grain: 2,
colorChrome: 'weak',
colorChromeBlue: 'none',
dynamicRange: 'auto',
sharpening: 0,
exposureCompensation: 0,
},
frameId: 'none',
cinema: 'summer',
useGeotag: true,
},
{
id: 'cinema-autumn-default',
name: 'CINEMA AUTUMN',
baseFilter: 'none',
adjustments: {
exposure: 0,
contrast: 1,
saturation: 2,
temperature: 7200,
tint: -2,
highlight: -2,
shadow: 1,
denoise: 2,
clarity: 0,
grain: 4,
colorChrome: 'weak',
colorChromeBlue: 'none',
dynamicRange: 'auto',
sharpening: 0,
exposureCompensation: 0,
},
frameId: 'none',
cinema: 'autumn',
useGeotag: true,
},
{
id: 'cinema-winter-default',
name: 'CINEMA WINTER',
baseFilter: 'none',
adjustments: {
exposure: 1,
contrast: -1,
saturation: -2,
temperature: 4500,
tint: 2,
highlight: -2,
shadow: 2,
denoise: 3,
clarity: -2,
grain: 3,
colorChrome: 'none',
colorChromeBlue: 'weak',
dynamicRange: 'auto',
sharpening: 0,
exposureCompensation: 0,
},
frameId: 'none',
cinema: 'winter',
useGeotag: true,
},
];