Saturon LogoSaturon

🚧 This documentation covers a pre-1.0 release. Expect breaking changes.

RecipesRegister Color Spaces

JzAzBz

A recipe to register the JzAzBz color function in Saturon.

function inverseMatrix(m: number[][]) {
    const a = m;
    const A = a[0][0],
        B = a[0][1],
        C = a[0][2];
    const D = a[1][0],
        E = a[1][1],
        F = a[1][2];
    const G = a[2][0],
        H = a[2][1],
        I = a[2][2];
    const det = A * (E * I - F * H) - B * (D * I - F * G) + C * (D * H - E * G);
    if (Math.abs(det) < 1e-12) throw new Error("Matrix singular");
    const invDet = 1 / det;
    const inv = [
        [(E * I - F * H) * invDet, (C * H - B * I) * invDet, (B * F - C * E) * invDet],
        [(F * G - D * I) * invDet, (A * I - C * G) * invDet, (C * D - A * F) * invDet],
        [(D * H - E * G) * invDet, (B * G - A * H) * invDet, (A * E - B * D) * invDet],
    ];
    return inv;
}

const M_XYZ_TO_LMS = [
    [0.41478972, 0.579999, 0.014648],
    [-0.20151, 1.120649, 0.0531008],
    [-0.0166008, 0.2648, 0.6684799],
];

const M_LMS_P_TO_IZAZBZ = [
    [0.5, 0.5, 0.0],
    [3.524, -4.066708, 0.542708],
    [0.199076, 1.096799, -1.295875],
];

const N = 4096;

const m1 = 2610 / 16384;
const m2 = (2523 / N) * 128;
const c1 = 3424 / N;
const c2 = (2413 / N) * 32;
const c3 = (2392 / N) * 32;

const b = 1.15;
const g = 0.66;
const d = -0.56;
const d_0 = 1.6295499532821566e-11;

/**
 * @see {@link https://www.w3.org/TR/css-color-hdr-1/|CSS Color HDR Module Level 1}
 * @see {@link https://opg.optica.org/oe/fulltext.cfm?uri=oe-25-13-15131|Perceptually uniform color space for image signals including high dynamic range and wide gamut}
 */
registerColorFunction("jzazbz", {
    components: {
        jz: { index: 0, value: [0, 1], precision: 5 },
        az: { index: 1, value: [-1, 1], precision: 5 },
        bz: { index: 2, value: [-1, 1], precision: 5 },
    },
    bridge: "xyz-d65",
    toBridge: ([Jz, az, bz]: number[]): number[] => {
        const st2084_forward = (E: number) => {
            if (E <= 0) return 0;
            const Em1 = Math.pow(E, 1 / m2);
            const top = Math.max(Em1 - c1, 0);
            const den = c2 - c3 * Em1;
            if (den <= 0) return 0;
            return Math.pow(top / den, 1 / m1);
        };
        const M_LMS_TO_XYZ = inverseMatrix(M_XYZ_TO_LMS);
        const M_IZAZBZ_TO_LMS_P = inverseMatrix(M_LMS_P_TO_IZAZBZ);
        const Jz_plus_d0 = Jz + d_0;
        const Iz = Jz_plus_d0 / (1 + d - d * Jz_plus_d0);
        const LMS_p = multiplyMatrices(M_IZAZBZ_TO_LMS_P, [Iz, az, bz]);
        const scale = 10000;
        const LMS = LMS_p.map((e) => scale * st2084_forward(e));
        const XYZp = multiplyMatrices(M_LMS_TO_XYZ, LMS);
        const Xp = XYZp[0],
            Yp = XYZp[1],
            Zp = XYZp[2];
        const Z = Zp;
        const X = (Xp + (b - 1) * Z) / b;
        const Y = (Yp + (g - 1) * X) / g;
        return [X, Y, Z];
    },
    fromBridge: ([X, Y, Z]: number[]): number[] => {
        const st2084_inverse = (Y: number) => {
            if (Y <= 0) return 0;
            const Ym1 = Math.pow(Y, m1);
            const num = c1 + c2 * Ym1;
            const den = 1 + c3 * Ym1;
            return Math.pow(num / den, m2);
        };
        const Xp = b * X - (b - 1) * Z;
        const Yp = g * Y - (g - 1) * X;
        const Zp = Z;
        const LMS = multiplyMatrices(M_XYZ_TO_LMS, [Xp, Yp, Zp]);
        const scale = 10000;
        const LMS_p = LMS.map((v) => st2084_inverse(Math.max(v / scale, 0)));
        const IzAzBz = multiplyMatrices(M_LMS_P_TO_IZAZBZ, LMS_p);
        const Iz = IzAzBz[0];
        const az = IzAzBz[1];
        const bz = IzAzBz[2];
        const Jz = ((1 + d) * Iz) / (1 + d * Iz) - d_0;
        return [Jz, az, bz];
    },
});