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:
2026-09-18 12:30:36 +07:00
parent 567f650bbb
commit f17cb08a63
4 changed files with 112 additions and 51 deletions
+22 -1
View File
@@ -25,6 +25,9 @@ import { BaseFilter, ColorAdjustments } from '../types';
// (Fuji extended DR); 0/auto/DR100 = no extra curve.
// hl - highlight -1..1: + lifts toward white, - rolls the bright side down.
// sh - shadow -1..1: + lifts the dark side, - deepens it.
// wh - white point -1..1: + pushes the top of the ramp toward white, - rolls
// it down. Cubic end-weight, so it never fights the HL knee.
// bl - black point -1..1: + lifts the toe (faded black), - crushes it.
// vib - vibrance -1..1: chroma-masked saturation. It rides along in this shader
// (rather than the colour matrix) because it needs per-pixel chroma:
// already-vivid pixels move least, so skins/skies deepen without the neon
@@ -41,6 +44,8 @@ uniform shader src;
uniform float dr;
uniform float hl;
uniform float sh;
uniform float wh;
uniform float bl;
uniform float vib;
uniform float shTr;
uniform float shTg;
@@ -61,6 +66,14 @@ vec4 main(vec2 xy) {
// Dynamic range: gentle shadow lift + highlight roll (protect brights).
o += dr * 0.12 * shMask * (1.0 - t);
o -= dr * 0.18 * hlMask * t;
// White/black point. The HL/SH knees above already spend the slope budget
// (each just under 1.0), so these two use cubic end-weights instead of
// another smoothstep: only the last/first stretch of the ramp moves, and the
// derivative stays >= 1 - 3*0.18 = 0.46 for every combination, so the curve
// can never invert. + lifts that end (faded blacks / brighter whites),
// - digs it in (true black / rolled white).
o += bl * 0.18 * (1.0 - t) * (1.0 - t) * (1.0 - t);
o += wh * 0.18 * t * t * t;
o = clamp(o, 0.0, 1.0);
// Lightness takes the curve, hue stays: the colour difference is gained
// only part-way so darkening cannot collapse a colour to black and lifting
@@ -176,6 +189,8 @@ export interface ToneUniforms {
dr: number; // 0..1
hl: number; // -1..1 (adjustments.highlight / 10)
sh: number; // -1..1 (adjustments.shadow / 10)
wh: number; // -1..1 (adjustments.whites / 10 — white point)
bl: number; // -1..1 (adjustments.blacks / 10 — black point)
vib: number; // -1..1 (adjustments.vibrance / 10)
shT: [number, number, number]; // shadow split-tone RGB bias, -1..1
hlT: [number, number, number]; // highlight split-tone RGB bias, -1..1
@@ -208,6 +223,8 @@ export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter):
const dr = drRaw === 'auto' || drRaw === 100 ? 0 : (drRaw - 100) / 300;
const hl = Math.max(-1, Math.min(1, (adj.highlight ?? 0) / 10));
const sh = Math.max(-1, Math.min(1, (adj.shadow ?? 0) / 10));
const wh = Math.max(-1, Math.min(1, (adj.whites ?? 0) / 10));
const bl = Math.max(-1, Math.min(1, (adj.blacks ?? 0) / 10));
const vib = Math.max(-1, Math.min(1, (adj.vibrance ?? 0) / 10));
const film = (baseFilter && FILM_TONE[baseFilter]) || {};
const shT: [number, number, number] = film.shT ?? [0, 0, 0];
@@ -224,6 +241,8 @@ export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter):
dr,
hl: hl + (film.hl ?? 0),
sh: sh + (film.sh ?? 0),
wh,
bl,
vib,
shT,
hlT,
@@ -235,7 +254,7 @@ export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter):
// Flat uniform buffer for `makeShaderWithChildren` / `<Shader uniforms>` — the
// order must match TONE_SKSL's declarations.
export function toneUniformArray(u: ToneUniforms): number[] {
return [u.dr, u.hl, u.sh, u.vib, u.shT[0], u.shT[1], u.shT[2], u.hlT[0], u.hlT[1], u.hlT[2], u.cc, u.ccb];
return [u.dr, u.hl, u.sh, u.wh, u.bl, u.vib, u.shT[0], u.shT[1], u.shT[2], u.hlT[0], u.hlT[1], u.hlT[2], u.cc, u.ccb];
}
export function toneIsActive(u: ToneUniforms): boolean {
@@ -243,6 +262,8 @@ export function toneIsActive(u: ToneUniforms): boolean {
u.dr !== 0 ||
u.hl !== 0 ||
u.sh !== 0 ||
u.wh !== 0 ||
u.bl !== 0 ||
u.vib !== 0 ||
u.shT[0] !== 0 ||
u.shT[1] !== 0 ||