diff --git a/Libraries/LibGfx/Color.cpp b/Libraries/LibGfx/Color.cpp index deb858fd3d..0e5dc2abbd 100644 --- a/Libraries/LibGfx/Color.cpp +++ b/Libraries/LibGfx/Color.cpp @@ -67,6 +67,52 @@ Array format_to_8bit_compatible(u8 value) } +// https://w3c.github.io/wcag/guidelines/22/#dfn-relative-luminance +double Color::relative_luminance() const +{ + // the relative brightness of any point in a colorspace, normalized to 0 for darkest black and 1 for lightest + // white. + + // Note 1: + // For the sRGB colorspace, the relative luminance of a color is defined as L = 0.2126 * R + 0.7152 * G + + // 0.0722 * B where R, G and B are defined as: + // * if RsRGB <= 0.04045 then R = RsRGB/12.92 else R = ((RsRGB+0.055)/1.055) ^ 2.4 + // * if GsRGB <= 0.04045 then G = GsRGB/12.92 else G = ((GsRGB+0.055)/1.055) ^ 2.4 + // * if BsRGB <= 0.04045 then B = BsRGB/12.92 else B = ((BsRGB+0.055)/1.055) ^ 2.4 + // and RsRGB, GsRGB, and BsRGB are defined as: + // * RsRGB = R8bit/255 + // * GsRGB = G8bit/255 + // * BsRGB = B8bit/255 + // The "^" character is the exponentiation operator. (Formula taken from [SRGB].) + auto linearized_srgb_component = [](u8 component) { + auto srgb_component = component / 255.0; + if (srgb_component <= 0.04045) + return srgb_component / 12.92; + return AK::pow((srgb_component + 0.055) / 1.055, 2.4); + }; + return 0.2126 * linearized_srgb_component(red()) + + 0.7152 * linearized_srgb_component(green()) + + 0.0722 * linearized_srgb_component(blue()); +} + +// https://w3c.github.io/wcag/guidelines/22/#dfn-contrast-ratio +double Color::contrast_ratio(Color const other) const +{ + // (L1 + 0.05) / (L2 + 0.05), where + // * L1 is the relative luminance of the lighter of the colors, and + // * L2 is the relative luminance of the darker of the colors. + auto l1 = relative_luminance(); + auto l2 = other.relative_luminance(); + auto darkest = min(l1, l2); + auto brightest = max(l1, l2); + return (brightest + 0.05) / (darkest + 0.05); +} + +Color Color::suggested_foreground_color() const +{ + return contrast_ratio(Black) >= contrast_ratio(White) ? Black : White; +} + // https://www.w3.org/TR/css-color-4/#serializing-sRGB-values void Color::serialize_a_srgb_value(StringBuilder& builder) const { diff --git a/Libraries/LibGfx/Color.h b/Libraries/LibGfx/Color.h index 0624b76d56..62a35cb095 100644 --- a/Libraries/LibGfx/Color.h +++ b/Libraries/LibGfx/Color.h @@ -324,19 +324,8 @@ public: return delta_alpha * delta_alpha / (2.0f * 255 * 255) + rgb_distance * alpha() * other.alpha() / (255 * 255); } - constexpr u8 luminosity() const - { - return round_to(red() * 0.2126f + green() * 0.7152f + blue() * 0.0722f); - } - - constexpr float contrast_ratio(Color other) - { - auto l1 = luminosity(); - auto l2 = other.luminosity(); - auto darkest = min(l1, l2) / 255.; - auto brightest = max(l1, l2) / 255.; - return (brightest + 0.05) / (darkest + 0.05); - } + double relative_luminance() const; + double contrast_ratio(Color other) const; constexpr Color sepia(float amount = 1.0f) const { @@ -457,10 +446,7 @@ public: return Color(out_r, out_g, out_b); } - constexpr Color suggested_foreground_color() const - { - return luminosity() < 128 ? Color::White : Color::Black; - } + Color suggested_foreground_color() const; private: constexpr explicit Color(BGRA8888 argb) diff --git a/Tests/LibGfx/TestColor.cpp b/Tests/LibGfx/TestColor.cpp index 5acd5ca11d..9e2dd4f14c 100644 --- a/Tests/LibGfx/TestColor.cpp +++ b/Tests/LibGfx/TestColor.cpp @@ -7,6 +7,34 @@ #include #include +TEST_CASE(relative_luminance) +{ + auto const orange = Color(247, 143, 0); + + EXPECT_APPROXIMATE(Color(Color::NamedColor::Black).relative_luminance(), 0.0); + EXPECT_APPROXIMATE(Color(Color::NamedColor::White).relative_luminance(), 1.0); + EXPECT_APPROXIMATE_WITH_ERROR(orange.relative_luminance(), 0.394190782, 0.0000005); +} + +TEST_CASE(contrast_ratio) +{ + auto const orange = Color(247, 143, 0); + + EXPECT_APPROXIMATE(Color(Color::NamedColor::Black).contrast_ratio(Color::NamedColor::White), 21.0); + EXPECT_APPROXIMATE_WITH_ERROR( + orange.contrast_ratio(Color::NamedColor::Black), 8.883815642, 0.0000005); + EXPECT_APPROXIMATE_WITH_ERROR( + orange.contrast_ratio(Color::NamedColor::White), 2.363849144, 0.0000005); + EXPECT(orange.contrast_ratio(Color::NamedColor::Black) > orange.contrast_ratio(Color::NamedColor::White)); +} + +TEST_CASE(suggested_foreground_color) +{ + EXPECT_EQ(Color(Color::NamedColor::White), Color(Color::NamedColor::Black).suggested_foreground_color()); + EXPECT_EQ(Color(Color::NamedColor::Black), Color(Color::NamedColor::White).suggested_foreground_color()); + EXPECT_EQ(Color(Color::NamedColor::Black), Color(247, 143, 0).suggested_foreground_color()); +} + TEST_CASE(from_bgrx) { EXPECT_EQ(Color(0x00, 0x00, 0xff), Color::from_bgrx(0x000000ff)); diff --git a/Tests/LibWeb/Screenshot/expected-macos/color-scheme.png b/Tests/LibWeb/Screenshot/expected-macos/color-scheme.png index 84ae71802d..4e898d2db4 100644 Binary files a/Tests/LibWeb/Screenshot/expected-macos/color-scheme.png and b/Tests/LibWeb/Screenshot/expected-macos/color-scheme.png differ diff --git a/Tests/LibWeb/Screenshot/expected/color-scheme.png b/Tests/LibWeb/Screenshot/expected/color-scheme.png index 84ae71802d..d57b453d5e 100644 Binary files a/Tests/LibWeb/Screenshot/expected/color-scheme.png and b/Tests/LibWeb/Screenshot/expected/color-scheme.png differ diff --git a/Tests/LibWeb/Screenshot/input/color-scheme.html b/Tests/LibWeb/Screenshot/input/color-scheme.html index 50bfe0a1fa..4e9d1d6edb 100644 --- a/Tests/LibWeb/Screenshot/input/color-scheme.html +++ b/Tests/LibWeb/Screenshot/input/color-scheme.html @@ -1,5 +1,4 @@ -