web: move WHITE/BLACK onto the WB tab as per-channel points

LIGHT already spends its slope budget on the tone knees, so the two end
points were doing nothing a tone knob could not. On WB they act on each
channel's own distance from the end: the darker channel of a shadow and
the brighter channel of a highlight move most, which neutralises a cast
at the toe and the shoulder. Both shuffles stay cubic in the channel
value, so every channel's curve is still monotonic (>= 0.46).
This commit is contained in:
2026-09-18 14:35:48 +07:00
parent 89beb5f160
commit 8308b3e65f
3 changed files with 43 additions and 39 deletions
+2 -2
View File
@@ -80,8 +80,8 @@ export interface ColorAdjustments {
wbBlue?: number; // Fuji WB fine-tune, Blue axis (-9..9) — extra blue-channel gain
highlight: number; // -10 to +10 (- pulls highlights down / roll, + brightens)
shadow: number; // -10 to +10 (+ lifts shadows, - deepens)
whites?: number; // -10 to +10 (white point: the very top of the tone curve)
blacks?: number; // -10 to +10 (black point: the very toe of the tone curve)
whites?: number; // -10 to +10 (WB white point: per channel, the shoulder)
blacks?: number; // -10 to +10 (WB black point: per channel, the toe)
denoise: number; // -10 to +10 (+ = blur sigma; - = add film grain back)
clarity: number; // -10 to +10 (mapped to matrix convolution / bloom)
grain: number; // 0 to 10 (mapped to noise turbulence opacity/scale)
+26 -24
View File
@@ -105,30 +105,6 @@ export const PARAM_DEFS: {
get: (a) => a.shadow ?? 0,
set: (v) => ({ shadow: v }),
},
{
// The end points of the tone curve: WHITE lifts/crushes the top, BLACK the
// toe. Same range as the CREATE form's rows, so a look round-trips.
key: 'whites',
label: 'WHITE',
min: -10,
max: 10,
step: 1,
defaultValue: 0,
display: sign,
get: (a) => a.whites ?? 0,
set: (v) => ({ whites: v }),
},
{
key: 'blacks',
label: 'BLACK',
min: -10,
max: 10,
step: 1,
defaultValue: 0,
display: sign,
get: (a) => a.blacks ?? 0,
set: (v) => ({ blacks: v }),
},
],
wb: [
{
@@ -153,6 +129,32 @@ export const PARAM_DEFS: {
get: (a) => a.tint ?? 0,
set: (v) => ({ tint: v }),
},
{
// The two points live on WB, not on LIGHT: they shift each channel's own
// end of the ramp, which balances a cast at the toe and the shoulder
// rather than adding another tone knob. Same range as the CREATE form's
// rows, so a look round-trips.
key: 'whites',
label: 'WHITE',
min: -10,
max: 10,
step: 1,
defaultValue: 0,
display: sign,
get: (a) => a.whites ?? 0,
set: (v) => ({ whites: v }),
},
{
key: 'blacks',
label: 'BLACK',
min: -10,
max: 10,
step: 1,
defaultValue: 0,
display: sign,
get: (a) => a.blacks ?? 0,
set: (v) => ({ blacks: v }),
},
],
filters: [
{
+15 -13
View File
@@ -25,9 +25,11 @@ 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.
// wh - white point -1..1: per channel, from the WB tab. + lifts the shoulder,
// - rolls it down. Cubic weight in the channel's own value, so the
// brighter channel of a highlight moves most.
// bl - black point -1..1: per channel. + lifts the toe (faded black), -
// crushes it; the darker channel of a shadow moves most.
// 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
@@ -66,20 +68,20 @@ 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
// cannot blow a dark saturated colour out to white.
float cg = clamp(o / max(t, 0.0004), 0.55, 1.35);
rgb = clamp(vec3(o) + (rgb - vec3(t)) * cg, 0.0, 1.0);
// WHITE/BLACK points, per channel. The weight is cubic in the channel's own
// distance from the end, so in the shadows the darker channels move most and
// in the highlights the brighter ones do: the two points pull R, G and B
// toward a common toe and shoulder, which is a white-balance move (it
// neutralises a cast) and not another tone slider. Each channel's curve is
// still monotonic — 1 - 3*0.18 = 0.46 at worst — so no value can invert.
vec3 dk = 1.0 - rgb;
rgb = clamp(rgb + bl * 0.18 * dk * dk * dk + wh * 0.18 * rgb * rgb * rgb, 0.0, 1.0);
// Split tone (stock look): the shadows and the highlights may each carry
// their own tint, so the two ends of the curve can drift opposite ways
// (Classic Neg: green-cyan darks, warm brights) without touching mid-greys.
@@ -189,8 +191,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)
wh: number; // -1..1 (adjustments.whites / 10 — WB white point, per channel)
bl: number; // -1..1 (adjustments.blacks / 10 — WB black point, per channel)
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