web: fold the create form's groups and add the white/black point rows
The CREATE RECIPES form had grown into one long scroll. The six categorical groups (SIMULATION, DYNAMIC RANGE, GRAIN EFFECT, COLOR CHROME EFFECT, COLOR CHROME EFFECT BLUE, WHITE BALANCE) are now native <details> folds — the browser keeps the open flag, so no state and no library. The two tone-curve ENDS join the numeric grid: EXPOSURE (the matrix gain the IQ tab already drives), EV (the old EXPOSURE COMP. row, renamed to the phone's word), WHITE and BLACK. WHITE/BLACK are new ColorAdjustments fields, applied in TONE_SKSL as cubic end-weights rather than another smoothstep knee — the HL/SH knees already spend the slope budget, and the cubic keeps the curve monotonic for every combination (derivative >= 0.46), so a brighter input can still never come out darker. Both are optional, so stored recipes keep working.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { ChipRow, type ChipDef } from './ChipColumn';
|
||||
import { useI18n } from '../i18n/I18nProvider';
|
||||
import { DEFAULT_ADJUSTMENTS, FILM_SIMS } from '../../shared/utils/defaultRecipes';
|
||||
@@ -51,6 +51,17 @@ const wbKeyOf = (s: ColorAdjustments) =>
|
||||
return p.temperature === s.temperature && p.tint === s.tint;
|
||||
}) ?? 'custom';
|
||||
|
||||
// One collapsible group. A native <details> — no state, no library, and the
|
||||
// browser keeps the open/closed flag itself (the `open` prop only seeds it).
|
||||
function Fold({ label, children }: { label: string; children: ReactNode }) {
|
||||
return (
|
||||
<details className="create-sec" open>
|
||||
<summary className="create-label">{label}</summary>
|
||||
{children}
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
export function RecipeCreatePanel({
|
||||
currentBase,
|
||||
current,
|
||||
@@ -89,6 +100,9 @@ export function RecipeCreatePanel({
|
||||
wbKey: wbKeyOf(s),
|
||||
highlight: String(s.highlight),
|
||||
shadow: String(s.shadow),
|
||||
whites: String(s.whites ?? 0),
|
||||
blacks: String(s.blacks ?? 0),
|
||||
exposure: String(s.exposure),
|
||||
color: String(s.saturation),
|
||||
nr: String(s.denoise),
|
||||
sharp: String(s.sharpening),
|
||||
@@ -108,6 +122,9 @@ export function RecipeCreatePanel({
|
||||
const [wbBlue, setWbBlue] = useState(String(seedAdj.wbBlue ?? 0));
|
||||
const [fHigh, setFHigh] = useState(String(seedAdj.highlight));
|
||||
const [fShadow, setFShadow] = useState(String(seedAdj.shadow));
|
||||
const [fWhites, setFWhites] = useState(String(seedAdj.whites ?? 0));
|
||||
const [fBlacks, setFBlacks] = useState(String(seedAdj.blacks ?? 0));
|
||||
const [fExposure, setFExposure] = useState(String(seedAdj.exposure));
|
||||
const [fColor, setFColor] = useState(String(seedAdj.saturation));
|
||||
const [fNr, setFNr] = useState(String(seedAdj.denoise));
|
||||
const [fSharp, setFSharp] = useState(String(seedAdj.sharpening));
|
||||
@@ -125,6 +142,9 @@ export function RecipeCreatePanel({
|
||||
setWbKey(v.wbKey);
|
||||
setFHigh(v.highlight);
|
||||
setFShadow(v.shadow);
|
||||
setFWhites(v.whites);
|
||||
setFBlacks(v.blacks);
|
||||
setFExposure(v.exposure);
|
||||
setFColor(v.color);
|
||||
setFNr(v.nr);
|
||||
setFSharp(v.sharp);
|
||||
@@ -146,6 +166,9 @@ export function RecipeCreatePanel({
|
||||
wbBlue: int(wbBlue, 0, -9, 9),
|
||||
highlight: int(fHigh, simAdj.highlight, -10, 10),
|
||||
shadow: int(fShadow, simAdj.shadow, -10, 10),
|
||||
whites: int(fWhites, simAdj.whites ?? 0, -10, 10),
|
||||
blacks: int(fBlacks, simAdj.blacks ?? 0, -10, 10),
|
||||
exposure: int(fExposure, simAdj.exposure, -10, 10),
|
||||
saturation: int(fColor, simAdj.saturation, -10, 10),
|
||||
denoise: int(fNr, simAdj.denoise, -10, 10),
|
||||
sharpening: int(fSharp, simAdj.sharpening, -10, 10),
|
||||
@@ -182,7 +205,7 @@ export function RecipeCreatePanel({
|
||||
const { baseFilter, adjustments } = build();
|
||||
onPreview({ baseFilter, adjustments });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [sim, dr, grain, cc, ccb, wbKey, wbRed, wbBlue, fHigh, fShadow, fColor, fNr, fSharp, fClarity, fEv]);
|
||||
}, [sim, dr, grain, cc, ccb, wbKey, wbRed, wbBlue, fHigh, fShadow, fWhites, fBlacks, fColor, fNr, fSharp, fClarity, fExposure, fEv]);
|
||||
|
||||
const opts = (
|
||||
prefix: string,
|
||||
@@ -232,71 +255,80 @@ export function RecipeCreatePanel({
|
||||
/>
|
||||
</label>
|
||||
|
||||
<span className="create-label">SIMULATION</span>
|
||||
<ChipRow
|
||||
chips={FILM_SIMS.map((s) => ({
|
||||
key: s.id,
|
||||
label: s.name,
|
||||
active: sim === s.baseFilter,
|
||||
onClick: () => selectSim(s.baseFilter),
|
||||
}))}
|
||||
/>
|
||||
<Fold label="SIMULATION">
|
||||
<ChipRow
|
||||
chips={FILM_SIMS.map((s) => ({
|
||||
key: s.id,
|
||||
label: s.name,
|
||||
active: sim === s.baseFilter,
|
||||
onClick: () => selectSim(s.baseFilter),
|
||||
}))}
|
||||
/>
|
||||
</Fold>
|
||||
|
||||
<span className="create-label">DYNAMIC RANGE</span>
|
||||
<ChipRow chips={opts('dr', DR_OPTS, dr, setDr)} />
|
||||
<Fold label="DYNAMIC RANGE">
|
||||
<ChipRow chips={opts('dr', DR_OPTS, dr, setDr)} />
|
||||
</Fold>
|
||||
|
||||
<div className="create-grid">
|
||||
{[
|
||||
{ label: 'HIGHLIGHT', value: fHigh, set: setFHigh },
|
||||
{ label: 'SHADOW', value: fShadow, set: setFShadow },
|
||||
{ label: 'WHITE', value: fWhites, set: setFWhites },
|
||||
{ label: 'BLACK', value: fBlacks, set: setFBlacks },
|
||||
{ label: 'COLOR', value: fColor, set: setFColor },
|
||||
{ label: 'NOISE RED.', value: fNr, set: setFNr, ph: '-10..10' },
|
||||
{ label: 'SHARPENING', value: fSharp, set: setFSharp, ph: '-10..10' },
|
||||
{ label: 'CLARITY', value: fClarity, set: setFClarity },
|
||||
{ label: 'EXPOSURE COMP.', value: fEv, set: setFEv, ph: 'EV' },
|
||||
{ label: 'EXPOSURE', value: fExposure, set: setFExposure },
|
||||
{ label: 'EV', value: fEv, set: setFEv, ph: '±3 EV' },
|
||||
].map((f) => num(f.label, f.value, f.set, f.ph))}
|
||||
</div>
|
||||
|
||||
<span className="create-label">GRAIN EFFECT</span>
|
||||
<ChipRow chips={opts('grain', GRAIN_OPTS, grain, setGrain)} />
|
||||
<Fold label="GRAIN EFFECT">
|
||||
<ChipRow chips={opts('grain', GRAIN_OPTS, grain, setGrain)} />
|
||||
</Fold>
|
||||
|
||||
<span className="create-label">COLOR CHROME EFFECT</span>
|
||||
<ChipRow chips={opts('cc', CC_OPTS, cc, setCc)} />
|
||||
<Fold label="COLOR CHROME EFFECT">
|
||||
<ChipRow chips={opts('cc', CC_OPTS, cc, setCc)} />
|
||||
</Fold>
|
||||
|
||||
<span className="create-label">COLOR CHROME EFFECT BLUE</span>
|
||||
<ChipRow chips={opts('ccb', CC_OPTS, ccb, setCcb)} />
|
||||
<Fold label="COLOR CHROME EFFECT BLUE">
|
||||
<ChipRow chips={opts('ccb', CC_OPTS, ccb, setCcb)} />
|
||||
</Fold>
|
||||
|
||||
<span className="create-label">WHITE BALANCE</span>
|
||||
<ChipRow
|
||||
chips={[
|
||||
...opts('wb', WB_PRESETS.map((w) => ({ v: w.key, d: w.label })), wbKey, setWbKey),
|
||||
...(wbKey === 'custom'
|
||||
? [{ key: 'wb:custom', label: `${seedAdj.temperature}K`, active: true, onClick: () => setWbKey('custom') }]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="create-shift">
|
||||
<span className="create-label">WB SHIFT</span>
|
||||
<span className="create-label">RED</span>
|
||||
<input
|
||||
className="field"
|
||||
data-key="create-wbRed"
|
||||
value={wbRed}
|
||||
placeholder="-9..9"
|
||||
inputMode="numeric"
|
||||
onChange={(e) => setWbRed(e.target.value)}
|
||||
<Fold label="WHITE BALANCE">
|
||||
<ChipRow
|
||||
chips={[
|
||||
...opts('wb', WB_PRESETS.map((w) => ({ v: w.key, d: w.label })), wbKey, setWbKey),
|
||||
...(wbKey === 'custom'
|
||||
? [{ key: 'wb:custom', label: `${seedAdj.temperature}K`, active: true, onClick: () => setWbKey('custom') }]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
<span className="create-label">BLUE</span>
|
||||
<input
|
||||
className="field"
|
||||
data-key="create-wbBlue"
|
||||
value={wbBlue}
|
||||
placeholder="-9..9"
|
||||
inputMode="numeric"
|
||||
onChange={(e) => setWbBlue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="create-shift">
|
||||
<span className="create-label">WB SHIFT</span>
|
||||
<span className="create-label">RED</span>
|
||||
<input
|
||||
className="field"
|
||||
data-key="create-wbRed"
|
||||
value={wbRed}
|
||||
placeholder="-9..9"
|
||||
inputMode="numeric"
|
||||
onChange={(e) => setWbRed(e.target.value)}
|
||||
/>
|
||||
<span className="create-label">BLUE</span>
|
||||
<input
|
||||
className="field"
|
||||
data-key="create-wbBlue"
|
||||
value={wbBlue}
|
||||
placeholder="-9..9"
|
||||
inputMode="numeric"
|
||||
onChange={(e) => setWbBlue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Fold>
|
||||
|
||||
<div className="create-shift">
|
||||
<span className="create-label">ISO</span>
|
||||
|
||||
Reference in New Issue
Block a user