feat(studio & library): sync Light & Color tab parameters (vibrance, highlight, shadow, white, black) to Gradient Mask & add multi-field sorting options

This commit is contained in:
2026-10-07 12:36:55 +07:00
parent 1c7be6dfd4
commit eb47afc63e
6 changed files with 82 additions and 29 deletions
+5
View File
@@ -166,8 +166,13 @@ export interface GradientMask {
shadows?: number;
whites?: number;
blacks?: number;
highlight?: number;
shadow?: number;
white?: number;
black?: number;
clarity?: number;
dehaze?: number;
vibrance?: number;
// The LIGHT column's WB pair on the mask's own pixels: the same two rulers,
// the same units (kelvin, and the store's ±10 tint), the same gain — a local
// white balance, which is what a mask that sits under a mixed light is for.
+16 -16
View File
@@ -82,6 +82,7 @@ export type ReadMask = GradientMask & {
blacks: number;
clarity: number;
dehaze: number;
vibrance: number;
temperature: number;
tint: number;
};
@@ -100,18 +101,19 @@ export function readMasks(masks: GradientMask[] | undefined): ReadMask[] {
ry: Math.max(0, num(m.ry, 0)),
angle: num(m.angle, 0),
feather: clamp01(num(m.feather, MASK_DEFAULT_FEATHER)),
exposure: clampEV(num(m.exposure, 0)),
exposure: clampEV(num(m.exposure ?? (m as any).ev, 0)),
contrast: clampA(num(m.contrast, 0)),
saturation: clampA(num(m.saturation, 0)),
saturation: clampA(num(m.saturation ?? (m as any).color, 0)),
// The spec's section 4 knobs: the two tone soft masks, the two ends, and
// the two spatial moves. All -10..+10 like every other knob here, all 0 on
// a mask stored before they existed.
highlights: clampA(num(m.highlights, 0)),
shadows: clampA(num(m.shadows, 0)),
whites: clampA(num(m.whites, 0)),
blacks: clampA(num(m.blacks, 0)),
highlights: clampA(num(m.highlights ?? m.highlight, 0)),
shadows: clampA(num(m.shadows ?? m.shadow, 0)),
whites: clampA(num(m.whites ?? m.white, 0)),
blacks: clampA(num(m.blacks ?? m.black, 0)),
clarity: clampA(num(m.clarity, 0)),
dehaze: clampA(num(m.dehaze, 0)),
vibrance: clampA(num(m.vibrance, 0)),
// The WB pair, the two rulers the LIGHT column already carries, read on the
// mask's own pixels: kelvin on its own scale, tint in the store's ±10. Both
// absent on a mask stored before they existed, and then the gain is 1.
@@ -158,9 +160,9 @@ export function maskUniforms(
[m.exposure, m.contrast / 10, m.saturation / 10, MASK_KIND[m.kind]],
(2 * n + i) * 4
);
// The tone soft masks and the two ends, then the spatial pair.
// The tone soft masks and the two ends, then the spatial pair & vibrance.
u.set([m.highlights / 10, m.shadows / 10, m.whites / 10, m.blacks / 10], (3 * n + i) * 4);
u.set([m.clarity / 10, m.dehaze / 10, 0, 0], (4 * n + i) * 4);
u.set([m.clarity / 10, m.dehaze / 10, m.vibrance / 10, 0], (4 * n + i) * 4);
// The mask's own white balance, resolved here and not in the shader: the
// Kelvin fit is a curve, and a curve in SkSL would be a second copy of it
// (colorUtils.whiteBalanceGain is the one). Three gains and a zero pad, so
@@ -212,17 +214,15 @@ half3 maskAdjust(half3 c, half3 wb, float4 a, float4 tone, float4 fx, half dark$
// ordering clamp, the 0.50 midpoint no knob reaches, and the luma-preserving
// rebuild are all the frame's. DR is the one knob the ramp also carries that a
// mask does not have, so it is spent as 0 here.
//
// The BASE of that ramp is the mask's own pixel, handed in twice: a mask runs
// on the shape the user drew and has no neighbourhood of its own, and the
// degenerate call is the one the shared maths is written to take — a base equal
// to t is a ratio of exactly 1, so the mask keeps the global move it has always
// made. Give it a base of its own when a mask's SHADOW is reported the way the
// frame's was.
float lf = clamp(float(l), 0.0, 1.0);
c = half3(toneRamp(vec3(c), lf, lf, tone.w, tone.y, tone.x, tone.z, 0.0));
half nl = dot(clamp(c, half3(0.0), half3(1.0)), half3(0.2126, 0.7152, 0.0722));
c = mix(half3(nl), c, half(1.0 + a.z));
// Saturation & Vibrance matching toneShader's exact chroma-masked formula
half mx = max(max(c.r, c.g), c.b);
half mn = min(min(c.r, c.g), c.b);
half chroma = mx > half(0.0001) ? (mx - mn) / mx : half(0.0);
half kv = half(1.0) + half(fx.z) * half(0.75) * (half(1.0) - chroma);
c = clamp(mix(half3(nl), c, half(1.0 + a.z) * kv), half3(0.0), half3(1.0));
${spatial ? ` // DEHAZE before CLARITY, the frame-wide order and for the frame-wide reason:
// sharpening haze only makes it read as detail.
if (fx.y != 0.0) {