agx-photo 0.2.0

An open-source photo editing library with a readable, portable preset format
#define_import_path common::color
// Color space conversion and HSL utilities for AgX compute shaders.

// Linear Rec.2020 -> linear sRGB. WGSL mat3x3 is column-major, so the
// vec3 arguments below are columns of the matrix -- equivalent to the
// row-major declaration in crate::color_space.
const LINEAR_REC2020_TO_LINEAR_SRGB: mat3x3<f32> = mat3x3<f32>(
    vec3<f32>( 1.660491, -0.124550, -0.018151),
    vec3<f32>(-0.587641,  1.132899, -0.100579),
    vec3<f32>(-0.072850, -0.008349,  1.118730),
);

// Linear sRGB -> linear Rec.2020 (inverse of the above). Same column-major
// layout caveat applies.
const LINEAR_SRGB_TO_LINEAR_REC2020: mat3x3<f32> = mat3x3<f32>(
    vec3<f32>(0.627404, 0.069097, 0.016391),
    vec3<f32>(0.329283, 0.919541, 0.088013),
    vec3<f32>(0.043313, 0.011362, 0.895595),
);

// Sign-preserving sRGB transfer: sign(x) * srgb_curve(abs(x)). The standard
// sRGB curve is defined only for non-negative inputs; for negative inputs
// (which arise from out-of-gamut math in wide working spaces), apply the
// curve to abs(x) and negate. Matches crate::color_space::srgb_curve_signed.
fn srgb_curve_signed(x: f32) -> f32 {
    let sign_factor = select(1.0, -1.0, x < 0.0);
    let ax = abs(x);
    let curved = select(
        1.055 * pow(ax, 1.0 / 2.4) - 0.055,
        12.92 * ax,
        ax <= 0.0031308
    );
    return sign_factor * curved;
}

// Inverse of srgb_curve_signed: sign(x) * srgb_curve_inverse(abs(x)).
// Matches crate::color_space::srgb_curve_signed_inverse.
fn srgb_curve_signed_inverse(x: f32) -> f32 {
    let sign_factor = select(1.0, -1.0, x < 0.0);
    let ax = abs(x);
    let lin = select(
        pow((ax + 0.055) / 1.055, 2.4),
        ax / 12.92,
        ax <= 0.04045
    );
    return sign_factor * lin;
}

// Sign-preserving sRGB transfer applied per channel.
// CPU equivalent: srgb_curve_signed in crate::color_space, applied to each channel.
fn linear_to_gamma(rgb: vec3f) -> vec3f {
    return vec3f(
        srgb_curve_signed(rgb.x),
        srgb_curve_signed(rgb.y),
        srgb_curve_signed(rgb.z),
    );
}

fn gamma_to_linear(rgb: vec3f) -> vec3f {
    return vec3f(
        srgb_curve_signed_inverse(rgb.x),
        srgb_curve_signed_inverse(rgb.y),
        srgb_curve_signed_inverse(rgb.z),
    );
}

// Convert sRGB to HSL. Returns (hue 0-360, saturation 0-1, lightness 0-1).
fn rgb_to_hsl(rgb: vec3f) -> vec3f {
    let r = rgb.x;
    let g = rgb.y;
    let b = rgb.z;

    let max_c = max(r, max(g, b));
    let min_c = min(r, min(g, b));
    let delta = max_c - min_c;
    let l = (max_c + min_c) * 0.5;

    if delta < 1e-6 {
        return vec3f(0.0, 0.0, l);
    }

    let s = select(
        delta / (2.0 - max_c - min_c),
        delta / (max_c + min_c),
        l < 0.5
    );

    var h: f32;
    if max_c == r {
        h = ((g - b) / delta) % 6.0;
    } else if max_c == g {
        h = (b - r) / delta + 2.0;
    } else {
        h = (r - g) / delta + 4.0;
    }
    h = h * 60.0;
    if h < 0.0 {
        h = h + 360.0;
    }

    return vec3f(h, s, l);
}

// Convert HSL to sRGB. Input: (hue 0-360, saturation 0-1, lightness 0-1).
fn hsl_to_rgb(hsl: vec3f) -> vec3f {
    let h = hsl.x;
    let s = hsl.y;
    let l = hsl.z;

    if s < 1e-6 {
        return vec3f(l, l, l);
    }

    let q = select(l + s - l * s, l * (1.0 + s), l < 0.5);
    let p = 2.0 * l - q;
    let h_norm = h / 360.0;

    return vec3f(
        hue_to_rgb(p, q, h_norm + 1.0 / 3.0),
        hue_to_rgb(p, q, h_norm),
        hue_to_rgb(p, q, h_norm - 1.0 / 3.0),
    );
}

fn hue_to_rgb(p: f32, q: f32, t_in: f32) -> f32 {
    var t = t_in;
    if t < 0.0 { t = t + 1.0; }
    if t > 1.0 { t = t - 1.0; }
    if t < 1.0 / 6.0 { return p + (q - p) * 6.0 * t; }
    if t < 1.0 / 2.0 { return q; }
    if t < 2.0 / 3.0 { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; }
    return p;
}

// Shortest angular distance between two hue angles in degrees. Result in [0, 180].
fn hue_distance(a: f32, b: f32) -> f32 {
    let d = ((a - b) % 360.0 + 360.0) % 360.0;
    return select(360.0 - d, d, d <= 180.0);
}

// Cosine falloff weight: 1.0 at center, 0.0 at half_width.
const PI: f32 = 3.14159265358979323846;

fn cosine_weight(hue_dist: f32, half_width: f32) -> f32 {
    if hue_dist >= half_width {
        return 0.0;
    }
    return cos(hue_dist / half_width * PI) * 0.5 + 0.5;
}