web: the three mixer knobs move the whole image, a frame chip toggles itself, ROTATE lets go when STRAIGHTEN steers
HUE, SAT and LUM leave the colour row: behind an IMAGE divider they are hslHue/hslSat/hslLum, seeded into the shader's band accumulator at full weight for every hue, while the eight band chips keep picking which colour the panel on the photo edits. The image lightness term stays ungated so a frame drained to grey by -SAT still answers +LUM. FRAME loses its NO FRAME chip: pressing the frame already on the photo takes it off. ROTATE's quarter turns stop lighting the moment the fine angle leaves 0, so the strip shows which of the two is steering the photo.
This commit is contained in:
@@ -105,6 +105,12 @@ export interface ColorAdjustments {
|
||||
// Selective colour by hue band. Only the bands the user actually moved are
|
||||
// present, so an untouched mixer costs nothing and an old recipe stays valid.
|
||||
hslBands?: Partial<Record<HslBandId, HslBand>>;
|
||||
// The mixer's overall move: one hue turn, one saturation scale and one
|
||||
// lightness offset for the WHOLE image, applied to every hue before the band
|
||||
// edits above. -10..+10 like every other knob; absent/0 = no move at all.
|
||||
hslHue?: number;
|
||||
hslSat?: number;
|
||||
hslLum?: number;
|
||||
exposureCompensation: number; // -3 to +3 EV. Camera: AE bias (hardware). Library: 2^EV matrix gain.
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ export const DEFAULT_ADJUSTMENTS: ColorAdjustments = {
|
||||
colorChromeBlue: 'none',
|
||||
dynamicRange: 'auto',
|
||||
sharpening: 0,
|
||||
hslHue: 0,
|
||||
hslSat: 0,
|
||||
hslLum: 0,
|
||||
exposureCompensation: 0,
|
||||
};
|
||||
|
||||
|
||||
@@ -48,6 +48,11 @@ import { HSL_BANDS, hslBandGaps } from './colorUtils';
|
||||
// hue — so unlike everything else above it, the eight bands are not a
|
||||
// global move and cannot live in the colour matrix. See the band block at
|
||||
// the foot of TONE_SKSL.
|
||||
// gh/gs/gl - the mixer's overall move: the same three quantities for the WHOLE
|
||||
// image, so they are simply the starting value of the per-band
|
||||
// accumulator and every hue gets them at full weight. The lightness one
|
||||
// is not gated by saturation (unlike the bands'), so a frame drained to
|
||||
// grey by -SAT still answers +LUM.
|
||||
// The eight band lines of TONE_SKSL's mixer, generated from HSL_BANDS so the
|
||||
// anchors and the gaps in the shader are the same numbers the chips are built
|
||||
// from. Each line reads its own band's three values with a CONSTANT index —
|
||||
@@ -80,6 +85,9 @@ uniform float hslOn;
|
||||
uniform float hslH[8];
|
||||
uniform float hslS[8];
|
||||
uniform float hslL[8];
|
||||
uniform float gh;
|
||||
uniform float gs;
|
||||
uniform float gl;
|
||||
// sRGB <-> HSL. The mixer works in HSL because that is the space the knobs are
|
||||
// named after: a hue shift must not change how light a colour is, and a
|
||||
// lightness move must not change its hue, which is exactly what scaling RGB
|
||||
@@ -202,10 +210,14 @@ vec4 main(vec2 xy) {
|
||||
vec3 hsl = rgb2hsl(rgb);
|
||||
float gate = smoothstep(0.0, 0.08, hsl.y);
|
||||
float hd = hsl.x * 360.0;
|
||||
vec3 acc = vec3(0.0);
|
||||
// The overall move is the seed: every hue gets its turn and its saturation
|
||||
// scale at full weight, and the bands add their own share on top. The
|
||||
// lightness term is added below UNGATED, so it still lifts a colour that a
|
||||
// -SAT has already drained to grey.
|
||||
vec3 acc = vec3(gh, gs, 0.0) * gate;
|
||||
${BAND_BLOCK} hsl.x = fract(hsl.x + acc.x * (30.0 / 360.0));
|
||||
hsl.y = clamp(hsl.y * (1.0 + acc.y), 0.0, 1.0);
|
||||
hsl.z = clamp(hsl.z + acc.z * 0.25, 0.0, 1.0);
|
||||
hsl.z = clamp(hsl.z + (acc.z + gl) * 0.25, 0.0, 1.0);
|
||||
rgb = hsl2rgb(hsl);
|
||||
}
|
||||
return vec4(clamp(rgb, 0.0, 1.0), c.a);
|
||||
@@ -289,10 +301,13 @@ export interface ToneUniforms {
|
||||
hlT: [number, number, number]; // highlight split-tone RGB bias, -1..1
|
||||
cc: number; // 0..1 Color Chrome depth (0 = 'none')
|
||||
ccb: number; // 0..1 Color Chrome FX Blue depth (0 = 'none')
|
||||
hslOn: number; // 1 when any band is moved (0 skips the whole mixer)
|
||||
hslOn: number; // 1 when any band or the overall move is set (0 skips the mixer)
|
||||
hslH: number[]; // 8 × -1..1 per band, in HSL_BANDS order (±30° of hue at full)
|
||||
hslS: number[]; // 8 × -1..1 per band (saturation scale, -1 = grey)
|
||||
hslL: number[]; // 8 × -1..1 per band (additive lightness, ±0.25 at full)
|
||||
gh: number; // -1..1 whole-image hue turn (±30° at full)
|
||||
gs: number; // -1..1 whole-image saturation scale
|
||||
gl: number; // -1..1 whole-image lightness offset (±0.25 at full, ungated)
|
||||
}
|
||||
|
||||
// Per-stock tone pass. Fuji's Classic stocks are not a plain colour matrix:
|
||||
@@ -357,6 +372,11 @@ export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter):
|
||||
}
|
||||
// A monochrome stock has no hue to be selective about.
|
||||
if (!colour) hslOn = 0;
|
||||
// The mixer's overall move, which every hue receives at full weight.
|
||||
const gh = tenth(adj.hslHue);
|
||||
const gs = tenth(adj.hslSat);
|
||||
const gl = tenth(adj.hslLum);
|
||||
if (colour && (gh || gs || gl)) hslOn = 1;
|
||||
return {
|
||||
dr,
|
||||
hl: hl + (film.hl ?? 0),
|
||||
@@ -372,6 +392,9 @@ export function getToneUniforms(adj: ColorAdjustments, baseFilter?: BaseFilter):
|
||||
hslH,
|
||||
hslS,
|
||||
hslL,
|
||||
gh,
|
||||
gs,
|
||||
gl,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -381,7 +404,7 @@ export function toneUniformArray(u: ToneUniforms): number[] {
|
||||
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,
|
||||
u.hslOn, ...u.hslH, ...u.hslS, ...u.hslL,
|
||||
u.hslOn, ...u.hslH, ...u.hslS, ...u.hslL, u.gh, u.gs, u.gl,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user