// Hue / saturation / lightness adjustment. Converts the stored RGB to HSL,
// applies a hue rotation, a saturation multiplier, and a lightness offset, then
// converts back. Operates on the stored (non-linearised) values, matching the
// other colour nodes.
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOutput {
var positions = array<vec2<f32>, 6>(
vec2<f32>(-1.0, 1.0), vec2<f32>( 1.0, 1.0), vec2<f32>(-1.0, -1.0),
vec2<f32>(-1.0, -1.0), vec2<f32>( 1.0, 1.0), vec2<f32>( 1.0, -1.0),
);
var uvs = array<vec2<f32>, 6>(
vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(0.0, 1.0),
vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0),
);
var out: VertexOutput;
out.position = vec4<f32>(positions[vertex_idx], 0.0, 1.0);
out.uv = uvs[vertex_idx];
return out;
}
@group(0) @binding(0) var input_tex: texture_2d<f32>;
@group(0) @binding(1) var<uniform> u: HslUniforms;
// Matches Rust HslUniforms (field order/size kept in sync).
struct HslUniforms {
hue_shift: f32, // degrees, -180..180
saturation: f32, // multiplier (0 = grey, 1 = unchanged)
lightness: f32, // additive offset, -1..1
_pad: f32,
}
fn rgb_to_hsl(c: vec3<f32>) -> vec3<f32> {
let mx = max(c.r, max(c.g, c.b));
let mn = min(c.r, min(c.g, c.b));
let l = (mx + mn) * 0.5;
let d = mx - mn;
if (d < 1e-6) {
return vec3<f32>(0.0, 0.0, l);
}
let s = d / (1.0 - abs(2.0 * l - 1.0));
var h = 0.0;
if (mx == c.r) {
h = (c.g - c.b) / d;
h = h - 6.0 * floor(h / 6.0); // modulo 6
} else if (mx == c.g) {
h = (c.b - c.r) / d + 2.0;
} else {
h = (c.r - c.g) / d + 4.0;
}
return vec3<f32>(h / 6.0, s, l); // h in [0,1)
}
fn hue_to_rgb(p: f32, q: f32, t_in: f32) -> f32 {
var t = t_in - floor(t_in); // wrap to [0,1)
if (t < 1.0 / 6.0) { return p + (q - p) * 6.0 * t; }
if (t < 0.5) { return q; }
if (t < 2.0 / 3.0) { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; }
return p;
}
fn hsl_to_rgb(hsl: vec3<f32>) -> vec3<f32> {
let h = hsl.x;
let s = hsl.y;
let l = hsl.z;
if (s < 1e-6) {
return vec3<f32>(l, l, l);
}
var q = l + s - l * s;
if (l < 0.5) {
q = l * (1.0 + s);
}
let p = 2.0 * l - q;
return vec3<f32>(
hue_to_rgb(p, q, h + 1.0 / 3.0),
hue_to_rgb(p, q, h),
hue_to_rgb(p, q, h - 1.0 / 3.0),
);
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let color = textureLoad(input_tex, vec2<i32>(in.position.xy), 0);
var hsl = rgb_to_hsl(color.rgb);
hsl.x = hsl.x + u.hue_shift / 360.0;
hsl.y = clamp(hsl.y * u.saturation, 0.0, 1.0);
hsl.z = clamp(hsl.z + u.lightness, 0.0, 1.0);
let rgb = hsl_to_rgb(hsl);
return vec4<f32>(rgb, color.a);
}