From 8308b3e65f514aea152cc4e564274db42143b1fc Mon Sep 17 00:00:00 2001 From: 3dtours Date: Fri, 18 Sep 2026 14:35:48 +0700 Subject: [PATCH] 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). --- docker/frontend/shared/types/index.ts | 4 +- docker/frontend/shared/utils/paramDefs.ts | 50 +++++++++++----------- docker/frontend/shared/utils/toneShader.ts | 28 ++++++------ 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/docker/frontend/shared/types/index.ts b/docker/frontend/shared/types/index.ts index 9dd5fdc..9cffb4a 100644 --- a/docker/frontend/shared/types/index.ts +++ b/docker/frontend/shared/types/index.ts @@ -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) diff --git a/docker/frontend/shared/utils/paramDefs.ts b/docker/frontend/shared/utils/paramDefs.ts index 133cad8..4c4c67d 100644 --- a/docker/frontend/shared/utils/paramDefs.ts +++ b/docker/frontend/shared/utils/paramDefs.ts @@ -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: [ { diff --git a/docker/frontend/shared/utils/toneShader.ts b/docker/frontend/shared/utils/toneShader.ts index 28ed06f..e1cf48c 100644 --- a/docker/frontend/shared/utils/toneShader.ts +++ b/docker/frontend/shared/utils/toneShader.ts @@ -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