From 1a2ee46efea3170f42b138127676130b82444621 Mon Sep 17 00:00:00 2001 From: Tim Ledbetter Date: Tue, 14 Apr 2026 22:47:26 +0100 Subject: [PATCH] LibGfx: Add color space conversions needed for CSS color interpolation --- Libraries/LibGfx/ColorConversion.cpp | 468 +++++++++++++++++++++++++++ Libraries/LibGfx/ColorConversion.h | 44 +++ 2 files changed, 512 insertions(+) diff --git a/Libraries/LibGfx/ColorConversion.cpp b/Libraries/LibGfx/ColorConversion.cpp index 1ed52772e6..beff7bcdc0 100644 --- a/Libraries/LibGfx/ColorConversion.cpp +++ b/Libraries/LibGfx/ColorConversion.cpp @@ -4,11 +4,32 @@ * 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) @@ -199,4 +220,451 @@ ColorComponents lab_to_xyz50(ColorComponents const& lab) 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(), + }; +} + } diff --git a/Libraries/LibGfx/ColorConversion.h b/Libraries/LibGfx/ColorConversion.h index 86dae882d1..374031cb92 100644 --- a/Libraries/LibGfx/ColorConversion.h +++ b/Libraries/LibGfx/ColorConversion.h @@ -12,6 +12,8 @@ namespace Gfx { +class Color; + class ColorComponents { public: constexpr ColorComponents() = default; @@ -223,4 +225,46 @@ ColorComponents xyz50_to_linear_srgb(ColorComponents const&); ColorComponents xyz65_to_linear_srgb(ColorComponents const&); ColorComponents lab_to_xyz50(ColorComponents const&); +ColorComponents color_to_srgb(Color); +Color srgb_to_color(ColorComponents const&); + +ColorComponents linear_srgb_to_xyz65(ColorComponents const&); + +ColorComponents xyz65_to_xyz50(ColorComponents const&); +ColorComponents xyz50_to_xyz65(ColorComponents const&); + +ColorComponents xyz65_to_oklab(ColorComponents const&); +ColorComponents oklab_to_xyz65(ColorComponents const&); + +ColorComponents oklab_to_oklch(ColorComponents const&); +ColorComponents oklch_to_oklab(ColorComponents const&); + +ColorComponents xyz50_to_lab(ColorComponents const&); + +ColorComponents lab_to_lch(ColorComponents const&); +ColorComponents lch_to_lab(ColorComponents const&); + +ColorComponents srgb_to_hsl(ColorComponents const&); + +ColorComponents srgb_to_hwb(ColorComponents const&); +ColorComponents hwb_to_srgb(ColorComponents const&); + +ColorComponents linear_display_p3_to_display_p3(ColorComponents const&); +ColorComponents xyz65_to_linear_display_p3(ColorComponents const&); + +ColorComponents a98_rgb_to_linear_a98_rgb(ColorComponents const&); +ColorComponents linear_a98_rgb_to_a98_rgb(ColorComponents const&); +ColorComponents linear_a98_rgb_to_xyz65(ColorComponents const&); +ColorComponents xyz65_to_linear_a98_rgb(ColorComponents const&); + +ColorComponents prophoto_rgb_to_linear_prophoto_rgb(ColorComponents const&); +ColorComponents linear_prophoto_rgb_to_prophoto_rgb(ColorComponents const&); +ColorComponents linear_prophoto_rgb_to_xyz50(ColorComponents const&); +ColorComponents xyz50_to_linear_prophoto_rgb(ColorComponents const&); + +ColorComponents rec2020_to_linear_rec2020(ColorComponents const&); +ColorComponents linear_rec2020_to_rec2020(ColorComponents const&); +ColorComponents linear_rec2020_to_xyz65(ColorComponents const&); +ColorComponents xyz65_to_linear_rec2020(ColorComponents const&); + }