/* * Copyright (c) 2026, Tim Ledbetter * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include namespace Gfx { ColorComponents color_to_srgb(Color color) { return { color.red() / 255.0f, color.green() / 255.0f, color.blue() / 255.0f, color.alpha() / 255.0f, }; } Color srgb_to_color(ColorComponents const& components) { return Color( clamp(lroundf(components[0] * 255.0f), 0L, 255L), clamp(lroundf(components[1] * 255.0f), 0L, 255L), clamp(lroundf(components[2] * 255.0f), 0L, 255L), clamp(lroundf(components.alpha() * 255.0f), 0L, 255L)); } // https://drafts.csswg.org/css-color-4/#predefined-sRGB // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents srgb_to_linear_srgb(ColorComponents const& srgb) { auto to_linear = [](float c) { float sign = c < 0 ? -1.0f : 1.0f; float absolute = abs(c); if (absolute <= 0.04045f) return c / 12.92f; return sign * static_cast(pow((absolute + 0.055f) / 1.055f, 2.4)); }; return { to_linear(srgb[0]), to_linear(srgb[1]), to_linear(srgb[2]), srgb.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-sRGB // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents linear_srgb_to_srgb(ColorComponents const& linear) { auto to_srgb = [](float c) { float sign = c < 0 ? -1.0f : 1.0f; float absolute = abs(c); if (absolute > 0.0031308f) return sign * static_cast(1.055 * pow(absolute, 1.0 / 2.4) - 0.055); return 12.92f * c; }; return { to_srgb(linear[0]), to_srgb(linear[1]), to_srgb(linear[2]), linear.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-display-p3 // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents linear_display_p3_to_xyz65(ColorComponents const& p3) { float x = 0.48657095f * p3[0] + 0.26566769f * p3[1] + 0.19821729f * p3[2]; float y = 0.22897456f * p3[0] + 0.69173852f * p3[1] + 0.07928691f * p3[2]; float z = 0.00000000f * p3[0] + 0.04511338f * p3[1] + 1.04394437f * p3[2]; return { x, y, z, p3.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-display-p3 // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents display_p3_to_linear_display_p3(ColorComponents const& p3) { auto to_linear = [](float c) { float sign = c < 0 ? -1.0f : 1.0f; float absolute = abs(c); if (absolute <= 0.04045f) return c / 12.92f; return sign * static_cast(pow((absolute + 0.055f) / 1.055f, 2.4)); }; return { to_linear(p3[0]), to_linear(p3[1]), to_linear(p3[2]), p3.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-a98-rgb // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents a98rgb_to_xyz65(ColorComponents const& a98) { auto to_linear = [](float c) { float sign = c < 0 ? -1.0f : 1.0f; float absolute = abs(c); return sign * static_cast(pow(absolute, 563.0 / 256.0)); }; auto linear_r = to_linear(a98[0]); auto linear_g = to_linear(a98[1]); auto linear_b = to_linear(a98[2]); float x = 0.57666904f * linear_r + 0.18555824f * linear_g + 0.18822865f * linear_b; float y = 0.29734498f * linear_r + 0.62736357f * linear_g + 0.07529146f * linear_b; float z = 0.02703136f * linear_r + 0.07068885f * linear_g + 0.99133754f * linear_b; return { x, y, z, a98.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents pro_photo_rgb_to_xyz50(ColorComponents const& prophoto) { auto to_linear = [](float c) -> float { float sign = c < 0 ? -1.0f : 1.0f; float absolute = abs(c); if (absolute <= 16.0f / 512.0f) return c / 16.0f; return sign * static_cast(pow(absolute, 1.8)); }; auto linear_r = to_linear(prophoto[0]); auto linear_g = to_linear(prophoto[1]); auto linear_b = to_linear(prophoto[2]); float x = 0.79776664f * linear_r + 0.13518130f * linear_g + 0.03134773f * linear_b; float y = 0.28807483f * linear_r + 0.71183523f * linear_g + 0.00008994f * linear_b; float z = 0.00000000f * linear_r + 0.00000000f * linear_g + 0.82510460f * linear_b; return { x, y, z, prophoto.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-rec2020 // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents rec2020_to_xyz65(ColorComponents const& rec2020) { auto to_linear = [](float c) -> float { constexpr auto alpha = 1.09929682680944; constexpr auto beta = 0.018053968510807; float sign = c < 0 ? -1.0f : 1.0f; float absolute = abs(c); if (absolute < beta * 4.5) return c / 4.5f; return sign * static_cast(pow((absolute + alpha - 1) / alpha, 1 / 0.45)); }; auto linear_r = to_linear(rec2020[0]); auto linear_g = to_linear(rec2020[1]); auto linear_b = to_linear(rec2020[2]); float x = 0.63695805f * linear_r + 0.14461690f * linear_g + 0.16888098f * linear_b; float y = 0.26270021f * linear_r + 0.67799807f * linear_g + 0.05930172f * linear_b; float z = 0.00000000f * linear_r + 0.02807269f * linear_g + 1.06098506f * linear_b; return { x, y, z, rec2020.alpha() }; } ColorComponents xyz50_to_linear_srgb(ColorComponents const& xyz) { // See commit description for these values. float r = +3.134136f * xyz[0] - 1.617386f * xyz[1] - 0.490662f * xyz[2]; float g = -0.978795f * xyz[0] + 1.916254f * xyz[1] + 0.033443f * xyz[2]; float b = +0.071955f * xyz[0] - 0.228977f * xyz[1] + 1.405386f * xyz[2]; return { r, g, b, xyz.alpha() }; } ColorComponents xyz65_to_linear_srgb(ColorComponents const& xyz) { // See commit description for these values. float r = +3.240970f * xyz[0] - 1.537383f * xyz[1] - 0.498611f * xyz[2]; float g = -0.969244f * xyz[0] + 1.875968f * xyz[1] + 0.041555f * xyz[2]; float b = +0.055630f * xyz[0] - 0.203977f * xyz[1] + 1.056972f * xyz[2]; return { r, g, b, xyz.alpha() }; } // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents lab_to_xyz50(ColorComponents const& lab) { constexpr auto kappa = 24389.0 / 27.0; constexpr auto epsilon = 216.0 / 24389.0; float L = lab[0]; float a = lab[1]; float b = lab[2]; float f1 = (L + 16.0f) / 116.0f; float f0 = a / 500.0f + f1; float f2 = f1 - b / 200.0f; auto compute = [](float f) -> float { float cubed = f * f * f; if (cubed > epsilon) return cubed; return static_cast((116.0 * f - 16.0) / kappa); }; float x = compute(f0); float y = L > kappa * epsilon ? static_cast(pow((L + 16.0) / 116.0, 3)) : static_cast(L / kappa); float z = compute(f2); // D50 constexpr float x_n = 0.3457f / 0.3585f; constexpr float y_n = 1.0f; constexpr float z_n = (1.0f - 0.3457f - 0.3585f) / 0.3585f; return { x_n * x, y_n * y, z_n * z, lab.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents linear_srgb_to_xyz65(ColorComponents const& components) { float red = components[0]; float green = components[1]; float blue = components[2]; return { 0.4123907993f * red + 0.3575843394f * green + 0.1804807884f * blue, 0.2126390059f * red + 0.7151686788f * green + 0.0721923154f * blue, 0.0193308187f * red + 0.1191947798f * green + 0.9505321522f * blue, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents xyz65_to_xyz50(ColorComponents const& components) { float x = components[0]; float y = components[1]; float z = components[2]; return { +1.0479298208f * x + 0.0229468746f * y - 0.0501922295f * z, +0.0296278156f * x + 0.9904344482f * y - 0.0170738250f * z, -0.0092430581f * x + 0.0150551448f * y + 0.7518742814f * z, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents xyz50_to_xyz65(ColorComponents const& components) { float x = components[0]; float y = components[1]; float z = components[2]; return { +0.9554734528f * x - 0.0230985368f * y + 0.0632593086f * z, -0.0283697094f * x + 1.0099954580f * y + 0.0210415381f * z, +0.0123140016f * x - 0.0205076964f * y + 1.3303659366f * z, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents xyz65_to_oklab(ColorComponents const& components) { float x = components[0]; float y = components[1]; float z = components[2]; float long_cone = cbrtf(0.8190224379967030f * x + 0.3619062600528904f * y - 0.1288737815209879f * z); float medium_cone = cbrtf(0.0329836539323885f * x + 0.9292868615863434f * y + 0.0361446663506424f * z); float short_cone = cbrtf(0.0481771893596242f * x + 0.2642395317527308f * y + 0.6335478284694309f * z); return { 0.2104542683093140f * long_cone + 0.7936177747023054f * medium_cone - 0.0040720430116193f * short_cone, 1.9779985324311684f * long_cone - 2.4285922420485799f * medium_cone + 0.4505937096174110f * short_cone, 0.0259040424655478f * long_cone + 0.7827717124575296f * medium_cone - 0.8086757549230774f * short_cone, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents oklab_to_xyz65(ColorComponents const& components) { float lightness = components[0]; float a = components[1]; float b = components[2]; float long_cone = lightness + 0.3963377774f * a + 0.2158037573f * b; float medium_cone = lightness - 0.1055613458f * a - 0.0638541728f * b; float short_cone = lightness - 0.0894841775f * a - 1.2914855480f * b; long_cone = long_cone * long_cone * long_cone; medium_cone = medium_cone * medium_cone * medium_cone; short_cone = short_cone * short_cone * short_cone; return { +1.2268798758459243f * long_cone - 0.5578149944602171f * medium_cone + 0.2813910456659647f * short_cone, -0.0405757452148008f * long_cone + 1.1122868032803170f * medium_cone - 0.0717110580655164f * short_cone, -0.0763729366746601f * long_cone - 0.4214933324022432f * medium_cone + 1.5869240198367816f * short_cone, components.alpha(), }; } static ColorComponents rectangular_to_polar(ColorComponents const& components) { float a = components[1]; float b = components[2]; float chroma = sqrtf(a * a + b * b); float hue = atan2f(b, a) * 180.0f / AK::Pi; if (hue < 0.0f) hue += 360.0f; return { components[0], chroma, hue, components.alpha() }; } static ColorComponents polar_to_rectangular(ColorComponents const& components) { float chroma = components[1]; float hue_radians = components[2] * AK::Pi / 180.0f; return { components[0], chroma * cosf(hue_radians), chroma * sinf(hue_radians), components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#lab-to-lch ColorComponents oklab_to_oklch(ColorComponents const& components) { return rectangular_to_polar(components); } // https://drafts.csswg.org/css-color-4/#lch-to-lab ColorComponents oklch_to_oklab(ColorComponents const& components) { return polar_to_rectangular(components); } // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents xyz50_to_lab(ColorComponents const& components) { constexpr auto kappa = 24389.0 / 27.0; constexpr auto epsilon = 216.0 / 24389.0; // D50 constexpr float x_n = 0.3457f / 0.3585f; constexpr float y_n = 1.0f; constexpr float z_n = (1.0f - 0.3457f - 0.3585f) / 0.3585f; auto f = [](float value) -> float { if (value > epsilon) return cbrtf(value); return static_cast((kappa * value + 16.0) / 116.0); }; float fx = f(components[0] / x_n); float fy = f(components[1] / y_n); float fz = f(components[2] / z_n); return { 116.0f * fy - 16.0f, 500.0f * (fx - fy), 200.0f * (fy - fz), components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#lab-to-lch ColorComponents lab_to_lch(ColorComponents const& components) { return rectangular_to_polar(components); } // https://drafts.csswg.org/css-color-4/#lch-to-lab ColorComponents lch_to_lab(ColorComponents const& components) { return polar_to_rectangular(components); } // https://drafts.csswg.org/css-color-4/#rgb-to-hsl ColorComponents srgb_to_hsl(ColorComponents const& components) { float red = components[0]; float green = components[1]; float blue = components[2]; float maximum = max(max(red, green), blue); float minimum = min(min(red, green), blue); float chroma = maximum - minimum; float lightness = (minimum + maximum) / 2.0f; float hue = 0.0f; float saturation = 0.0f; if (chroma != 0.0f) { if (lightness == 0.0f || lightness == 1.0f) saturation = 0.0f; else saturation = (maximum - lightness) / min(lightness, 1.0f - lightness); if (maximum == red) hue = (green - blue) / chroma + (green < blue ? 6.0f : 0.0f); else if (maximum == green) hue = (blue - red) / chroma + 2.0f; else hue = (red - green) / chroma + 4.0f; hue *= 60.0f; if (saturation < 0.0f) { hue += 180.0f; saturation = fabsf(saturation); } if (hue >= 360.0f) hue -= 360.0f; } return { hue, saturation, lightness, components.alpha() }; } // https://drafts.csswg.org/css-color-4/#hwb-to-rgb ColorComponents hwb_to_srgb(ColorComponents const& components) { float hue = components[0]; float whiteness = components[1]; float blackness = components[2]; if (whiteness + blackness >= 1.0f) { float gray = whiteness / (whiteness + blackness); return { gray, gray, gray, components.alpha() }; } auto rgb = hsl_to_srgb({ hue, 1.0f, 0.5f, components.alpha() }); float scale = 1.0f - whiteness - blackness; return { rgb[0] * scale + whiteness, rgb[1] * scale + whiteness, rgb[2] * scale + whiteness, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#rgb-to-hwb ColorComponents srgb_to_hwb(ColorComponents const& components) { float red = components[0]; float green = components[1]; float blue = components[2]; float maximum = max(max(red, green), blue); float minimum = min(min(red, green), blue); float chroma = maximum - minimum; float hue = 0.0f; if (chroma != 0.0f) { if (maximum == red) hue = ((green - blue) / chroma) + (green < blue ? 6.0f : 0.0f); else if (maximum == green) hue = (blue - red) / chroma + 2.0f; else hue = (red - green) / chroma + 4.0f; hue *= 60.0f; if (hue >= 360.0f) hue -= 360.0f; } return { hue, minimum, 1.0f - maximum, components.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-display-p3 // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents linear_display_p3_to_display_p3(ColorComponents const& components) { auto to_gamma = [](float c) { float sign = c < 0 ? -1.0f : 1.0f; float absolute = abs(c); if (absolute > 0.0031308f) return sign * static_cast(1.055 * pow(absolute, 1.0 / 2.4) - 0.055); return 12.92f * c; }; return { to_gamma(components[0]), to_gamma(components[1]), to_gamma(components[2]), components.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-display-p3-linear // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents xyz65_to_linear_display_p3(ColorComponents const& components) { float x = components[0]; float y = components[1]; float z = components[2]; return { +2.4934969119f * x - 0.9313836179f * y - 0.4027107845f * z, -0.8294889696f * x + 1.7626640603f * y + 0.0236246858f * z, +0.0358458302f * x - 0.0761723893f * y + 0.9568845240f * z, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#predefined-a98-rgb // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents a98_rgb_to_linear_a98_rgb(ColorComponents const& components) { auto to_linear = [](float c) { float sign = c < 0.0f ? -1.0f : 1.0f; return sign * static_cast(pow(abs(c), 563.0 / 256.0)); }; return { to_linear(components[0]), to_linear(components[1]), to_linear(components[2]), components.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-a98-rgb // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents linear_a98_rgb_to_a98_rgb(ColorComponents const& components) { auto to_gamma = [](float c) { float sign = c < 0.0f ? -1.0f : 1.0f; return sign * static_cast(pow(abs(c), 256.0 / 563.0)); }; return { to_gamma(components[0]), to_gamma(components[1]), to_gamma(components[2]), components.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-a98-rgb // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents linear_a98_rgb_to_xyz65(ColorComponents const& components) { float red = components[0]; float green = components[1]; float blue = components[2]; return { 0.57666904f * red + 0.18555824f * green + 0.18822865f * blue, 0.29734498f * red + 0.62736357f * green + 0.07529146f * blue, 0.02703136f * red + 0.07068885f * green + 0.99133754f * blue, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#predefined-a98-rgb // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents xyz65_to_linear_a98_rgb(ColorComponents const& components) { float x = components[0]; float y = components[1]; float z = components[2]; return { +2.0415879038f * x - 0.5650069743f * y - 0.3473784579f * z, -0.9692436363f * x + 1.8759675015f * y + 0.0415550574f * z, +0.0134442806f * x - 0.1183623922f * y + 1.0151749944f * z, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents prophoto_rgb_to_linear_prophoto_rgb(ColorComponents const& components) { auto to_linear = [](float c) -> float { float sign = c < 0.0f ? -1.0f : 1.0f; float absolute = abs(c); if (absolute <= 16.0f / 512.0f) return c / 16.0f; return sign * static_cast(pow(absolute, 1.8)); }; return { to_linear(components[0]), to_linear(components[1]), to_linear(components[2]), components.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents linear_prophoto_rgb_to_prophoto_rgb(ColorComponents const& components) { auto to_gamma = [](float c) -> float { float sign = c < 0.0f ? -1.0f : 1.0f; float absolute = abs(c); if (absolute <= 1.0f / 512.0f) return c * 16.0f; return sign * static_cast(pow(absolute, 1.0 / 1.8)); }; return { to_gamma(components[0]), to_gamma(components[1]), to_gamma(components[2]), components.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents linear_prophoto_rgb_to_xyz50(ColorComponents const& components) { float red = components[0]; float green = components[1]; float blue = components[2]; return { 0.79776664f * red + 0.13518130f * green + 0.03134773f * blue, 0.28807483f * red + 0.71183523f * green + 0.00008994f * blue, 0.00000000f * red + 0.00000000f * green + 0.82510460f * blue, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents xyz50_to_linear_prophoto_rgb(ColorComponents const& components) { float x = components[0]; float y = components[1]; float z = components[2]; return { +1.3457989731f * x - 0.2555802200f * y - 0.0511188540f * z, -0.5446224939f * x + 1.5082327413f * y + 0.0205274474f * z, +0.0000000000f * x + 0.0000000000f * y + 1.2119675456f * z, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#predefined-rec2020 // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents rec2020_to_linear_rec2020(ColorComponents const& components) { auto to_linear = [](float c) -> float { float sign = c < 0.0f ? -1.0f : 1.0f; float absolute = abs(c); return sign * static_cast(pow(absolute, 2.4)); }; return { to_linear(components[0]), to_linear(components[1]), to_linear(components[2]), components.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-rec2020 // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents linear_rec2020_to_rec2020(ColorComponents const& components) { auto to_gamma = [](float c) -> float { float sign = c < 0.0f ? -1.0f : 1.0f; float absolute = abs(c); return sign * static_cast(pow(absolute, 1.0 / 2.4)); }; return { to_gamma(components[0]), to_gamma(components[1]), to_gamma(components[2]), components.alpha() }; } // https://drafts.csswg.org/css-color-4/#predefined-rec2020 // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents linear_rec2020_to_xyz65(ColorComponents const& components) { float red = components[0]; float green = components[1]; float blue = components[2]; return { 0.63695805f * red + 0.14461690f * green + 0.16888098f * blue, 0.26270021f * red + 0.67799807f * green + 0.05930172f * blue, 0.00000000f * red + 0.02807269f * green + 1.06098506f * blue, components.alpha(), }; } // https://drafts.csswg.org/css-color-4/#predefined-rec2020 // https://drafts.csswg.org/css-color-4/#color-conversion-code ColorComponents xyz65_to_linear_rec2020(ColorComponents const& components) { float x = components[0]; float y = components[1]; float z = components[2]; return { +1.7166511880f * x - 0.3556707838f * y - 0.2533662814f * z, -0.6666843518f * x + 1.6164812366f * y + 0.0157685458f * z, +0.0176398574f * x - 0.0427706133f * y + 0.9421031212f * z, components.alpha(), }; } }