瀏覽代碼

LibWeb: Introduce color-function-specific style values

Instead of CSSColorValue holding a Gfx::Color, make it an abstract class
with subclasses for each different color function, to match the Typed-OM
spec. This means moving the color calculations from the parsing code to
the `to_color()` method on the style value.

This lets us have calc() inside a color function, instead of having to
fully resolve the color at parse time. The canvas fillStyle tests have
been updated to reflect this.

The other test change is Screenshot/css-color-functions.html: previously
we produced slightly different colors for an alpha of 0.5 and one of
50%, and this incorrect behavior was baked into the test. So now it's
more correct. :^)
Sam Atkins 10 月之前
父節點
當前提交
3af6a69f1e
共有 24 個文件被更改,包括 1014 次插入402 次删除
  1. 5 0
      Meta/gn/secondary/Userland/Libraries/LibWeb/CSS/StyleValues/BUILD.gn
  2. 0 2
      Tests/LibWeb/Screenshot/canvas-fillstyle-rgb.html
  3. 二進制
      Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png
  4. 二進制
      Tests/LibWeb/Screenshot/images/css-color-functions-ref.png
  5. 1 1
      Tests/LibWeb/Text/expected/canvas/fillstyle.txt
  6. 1 3
      Tests/LibWeb/Text/input/canvas/fillstyle.html
  7. 5 0
      Userland/Libraries/LibWeb/CMakeLists.txt
  8. 344 369
      Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
  9. 8 7
      Userland/Libraries/LibWeb/CSS/Parser/Parser.h
  10. 6 2
      Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
  11. 90 6
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.cpp
  12. 20 11
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h
  13. 41 0
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.cpp
  14. 54 0
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.h
  15. 51 0
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSHWB.cpp
  16. 54 0
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSHWB.h
  17. 45 0
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLCH.cpp
  18. 54 0
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLCH.h
  19. 44 0
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLab.cpp
  20. 54 0
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLab.h
  21. 77 0
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSRGB.cpp
  22. 54 0
      Userland/Libraries/LibWeb/CSS/StyleValues/CSSRGB.h
  23. 5 0
      Userland/Libraries/LibWeb/Forward.h
  24. 1 1
      Userland/Libraries/LibWeb/HTML/HTMLMetaElement.cpp

+ 5 - 0
Meta/gn/secondary/Userland/Libraries/LibWeb/CSS/StyleValues/BUILD.gn

@@ -8,7 +8,12 @@ source_set("StyleValues") {
     "BasicShapeStyleValue.cpp",
     "BorderRadiusStyleValue.cpp",
     "CSSColorValue.cpp",
+    "CSSHSL.cpp",
+    "CSSHWB.cpp",
     "CSSKeywordValue.cpp",
+    "CSSOKLCH.cpp",
+    "CSSOKLab.cpp",
+    "CSSRGB.cpp",
     "CalculatedStyleValue.cpp",
     "ConicGradientStyleValue.cpp",
     "ContentStyleValue.cpp",

+ 0 - 2
Tests/LibWeb/Screenshot/canvas-fillstyle-rgb.html

@@ -30,8 +30,6 @@
     ctx.fillRect(0, 300, 500, 100);
 
     // Calc
-    // FIXME: The CSS parser currently chokes on calc expressions, which will
-    //        leave the fillStyle as it was (green).
     ctx.fillStyle = "rgb(calc(infinity), 0, 0)";
     ctx.fillRect(0, 400, 500, 100);
 </script>

二進制
Tests/LibWeb/Screenshot/images/canvas-fillstyle-rgb.png


二進制
Tests/LibWeb/Screenshot/images/css-color-functions-ref.png


+ 1 - 1
Tests/LibWeb/Text/expected/canvas/fillstyle.txt

@@ -2,4 +2,4 @@
 2. "#ff0000ff"
 3. "#0000ffff"
 4. "#00ff00ff"
-5. "#00ff00ff"
+5. "#ff0000ff"

+ 1 - 3
Tests/LibWeb/Text/input/canvas/fillstyle.html

@@ -33,10 +33,8 @@
             return context.fillStyle;
         });
 
-        // 5. Percentages
+        // 5. Calc, with out-of-range values
         testPart(() => {
-            // FIXME: The CSS parser currently chokes on calc expressions, which will
-            //        leave the fillStyle as it was (green).
             context.fillStyle = "rgb(calc(infinity), 0, 0)";
             return context.fillStyle;
         });

+ 5 - 0
Userland/Libraries/LibWeb/CMakeLists.txt

@@ -113,7 +113,12 @@ set(SOURCES
     CSS/StyleValues/CounterDefinitionsStyleValue.cpp
     CSS/StyleValues/CounterStyleValue.cpp
     CSS/StyleValues/CSSColorValue.cpp
+    CSS/StyleValues/CSSHSL.cpp
+    CSS/StyleValues/CSSHWB.cpp
     CSS/StyleValues/CSSKeywordValue.cpp
+    CSS/StyleValues/CSSOKLab.cpp
+    CSS/StyleValues/CSSOKLCH.cpp
+    CSS/StyleValues/CSSRGB.cpp
     CSS/StyleValues/DisplayStyleValue.cpp
     CSS/StyleValues/EasingStyleValue.cpp
     CSS/StyleValues/EdgeStyleValue.cpp

+ 344 - 369
Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp

@@ -1,7 +1,7 @@
 /*
  * Copyright (c) 2018-2022, Andreas Kling <kling@serenityos.org>
  * Copyright (c) 2020-2021, the SerenityOS developers.
- * Copyright (c) 2021-2024, Sam Atkins <atkinssj@serenityos.org>
+ * Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
  * Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
  * Copyright (c) 2022, MacDue <macdue@dueutil.tech>
  * Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
@@ -43,7 +43,12 @@
 #include <LibWeb/CSS/StyleValues/BasicShapeStyleValue.h>
 #include <LibWeb/CSS/StyleValues/BorderRadiusStyleValue.h>
 #include <LibWeb/CSS/StyleValues/CSSColorValue.h>
+#include <LibWeb/CSS/StyleValues/CSSHSL.h>
+#include <LibWeb/CSS/StyleValues/CSSHWB.h>
 #include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
+#include <LibWeb/CSS/StyleValues/CSSOKLCH.h>
+#include <LibWeb/CSS/StyleValues/CSSOKLab.h>
+#include <LibWeb/CSS/StyleValues/CSSRGB.h>
 #include <LibWeb/CSS/StyleValues/ContentStyleValue.h>
 #include <LibWeb/CSS/StyleValues/CounterDefinitionsStyleValue.h>
 #include <LibWeb/CSS/StyleValues/CounterStyleValue.h>
@@ -2622,474 +2627,454 @@ RefPtr<CSSStyleValue> Parser::parse_rect_value(TokenStream<ComponentValue>& toke
     return RectStyleValue::create(EdgeRect { params[0], params[1], params[2], params[3] });
 }
 
-Optional<Color> Parser::parse_rgb_color(Vector<ComponentValue> const& component_values)
+// https://www.w3.org/TR/css-color-4/#typedef-hue
+RefPtr<CSSStyleValue> Parser::parse_hue_value(TokenStream<ComponentValue>& tokens)
 {
-    u8 r_val = 0;
-    u8 g_val = 0;
-    u8 b_val = 0;
+    // <hue> = <number> | <angle>
+    if (auto number = parse_number_value(tokens))
+        return number;
+    if (auto angle = parse_angle_value(tokens))
+        return angle;
 
-    auto tokens = TokenStream { component_values };
+    return nullptr;
+}
 
+RefPtr<CSSStyleValue> Parser::parse_solidus_and_alpha_value(TokenStream<ComponentValue>& tokens)
+{
+    // [ / [<alpha-value> | none] ]?
+    // Common to the modern-syntax color functions.
+    // TODO: Parse `none`
+
+    auto transaction = tokens.begin_transaction();
+    tokens.skip_whitespace();
+    if (!tokens.next_token().is_delim('/'))
+        return {};
+    tokens.skip_whitespace();
+    auto alpha = parse_number_percentage_value(tokens);
+    if (!alpha)
+        return {};
     tokens.skip_whitespace();
-    auto const& red = tokens.next_token();
 
-    if (!red.is(Token::Type::Number)
-        && !red.is(Token::Type::Percentage))
+    transaction.commit();
+    return alpha;
+}
+
+// https://www.w3.org/TR/css-color-4/#funcdef-rgb
+RefPtr<CSSStyleValue> Parser::parse_rgb_color_value(TokenStream<ComponentValue>& outer_tokens)
+{
+    // rgb() = [ <legacy-rgb-syntax> | <modern-rgb-syntax> ]
+    // rgba() = [ <legacy-rgba-syntax> | <modern-rgba-syntax> ]
+    // <legacy-rgb-syntax> = rgb( <percentage>#{3} , <alpha-value>? ) |
+    //                       rgb( <number>#{3} , <alpha-value>? )
+    // <legacy-rgba-syntax> = rgba( <percentage>#{3} , <alpha-value>? ) |
+    //                        rgba( <number>#{3} , <alpha-value>? )
+    // <modern-rgb-syntax> = rgb(
+    //     [ <number> | <percentage> | none]{3}
+    //     [ / [<alpha-value> | none] ]?  )
+    // <modern-rgba-syntax> = rgba(
+    //     [ <number> | <percentage> | none]{3}
+    //     [ / [<alpha-value> | none] ]?  )
+    // TODO: Handle none values
+
+    auto transaction = outer_tokens.begin_transaction();
+    outer_tokens.skip_whitespace();
+
+    auto& function_token = outer_tokens.next_token();
+    if (!function_token.is_function("rgb"sv) && !function_token.is_function("rgba"sv))
         return {};
 
-    tokens.skip_whitespace();
-    bool legacy_syntax = tokens.peek_token().is(Token::Type::Comma);
+    RefPtr<CSSStyleValue> red;
+    RefPtr<CSSStyleValue> green;
+    RefPtr<CSSStyleValue> blue;
+    RefPtr<CSSStyleValue> alpha;
+
+    auto inner_tokens = TokenStream { function_token.function().values() };
+    inner_tokens.skip_whitespace();
+
+    red = parse_number_percentage_value(inner_tokens);
+    if (!red)
+        return {};
+
+    inner_tokens.skip_whitespace();
+    bool legacy_syntax = inner_tokens.peek_token().is(Token::Type::Comma);
     if (legacy_syntax) {
-        // Legacy syntax.
-        tokens.next_token();
-        tokens.skip_whitespace();
+        // Legacy syntax
+        //   <percentage>#{3} , <alpha-value>?
+        //   | <number>#{3} , <alpha-value>?
+        // So, r/g/b can be numbers or percentages, as long as they're all the same type.
 
-        auto const& green = tokens.next_token();
-        tokens.skip_whitespace();
+        inner_tokens.next_token(); // comma
+        inner_tokens.skip_whitespace();
 
-        tokens.next_token();
-        tokens.skip_whitespace();
+        green = parse_number_percentage_value(inner_tokens);
+        if (!green)
+            return {};
+        inner_tokens.skip_whitespace();
 
-        auto const& blue = tokens.next_token();
+        if (!inner_tokens.next_token().is(Token::Type::Comma))
+            return {};
+        inner_tokens.skip_whitespace();
 
-        if (red.is(Token::Type::Percentage)) {
-            // Percentage components.
-            if (!green.is(Token::Type::Percentage) || !blue.is(Token::Type::Percentage))
-                return {};
+        blue = parse_number_percentage_value(inner_tokens);
+        if (!blue)
+            return {};
+        inner_tokens.skip_whitespace();
 
-            r_val = lround(clamp(red.token().percentage() * 2.55, 0, 255));
-            g_val = lround(clamp(green.token().percentage() * 2.55, 0, 255));
-            b_val = lround(clamp(blue.token().percentage() * 2.55, 0, 255));
-        } else {
-            // Number components.
-            if (!green.is(Token::Type::Number) || !blue.is(Token::Type::Number))
+        if (inner_tokens.has_next_token()) {
+            // Try and read comma and alpha
+            if (!inner_tokens.next_token().is(Token::Type::Comma))
                 return {};
+            inner_tokens.skip_whitespace();
 
-            r_val = clamp(llroundf(red.token().number_value()), 0, 255);
-            g_val = clamp(llroundf(green.token().number_value()), 0, 255);
-            b_val = clamp(llroundf(blue.token().number_value()), 0, 255);
-        }
-    } else {
-        // Modern syntax.
+            alpha = parse_number_percentage_value(inner_tokens);
+            inner_tokens.skip_whitespace();
 
-        if (red.is(Token::Type::Number)) {
-            r_val = clamp(llroundf(red.token().number_value()), 0, 255);
-        } else {
-            r_val = lround(clamp(red.token().percentage() * 2.55, 0, 255));
+            if (inner_tokens.has_next_token())
+                return {};
         }
 
-        auto const& green = tokens.next_token();
-        if (green.is(Token::Type::Number)) {
-            g_val = clamp(llroundf(green.token().number_value()), 0, 255);
-        } else if (green.is(Token::Type::Percentage)) {
-            g_val = lround(clamp(green.token().percentage() * 2.55, 0, 255));
-        } else {
+        // Verify we're all percentages or all numbers
+        auto is_percentage = [](CSSStyleValue const& style_value) {
+            return style_value.is_percentage()
+                || (style_value.is_calculated() && style_value.as_calculated().resolves_to_percentage());
+        };
+        bool red_is_percentage = is_percentage(*red);
+        bool green_is_percentage = is_percentage(*green);
+        bool blue_is_percentage = is_percentage(*blue);
+        if (red_is_percentage != green_is_percentage || red_is_percentage != blue_is_percentage)
             return {};
-        }
 
-        tokens.skip_whitespace();
-        auto const& blue = tokens.next_token();
-        if (blue.is(Token::Type::Number)) {
-            b_val = clamp(llroundf(blue.token().number_value()), 0, 255);
-        } else if (blue.is(Token::Type::Percentage)) {
-            b_val = lround(clamp(blue.token().percentage() * 2.55, 0, 255));
-        } else {
-            return {};
-        }
-    }
+    } else {
+        // Modern syntax
+        //   [ <number> | <percentage> | none]{3}  [ / [<alpha-value> | none] ]?
 
-    u8 alpha_val = 255;
-    tokens.skip_whitespace();
-    if (tokens.has_next_token()) {
-        auto const& separator = tokens.next_token();
-        bool is_comma = separator.is(Token::Type::Comma);
-        bool is_slash = separator.is_delim('/');
-        if (legacy_syntax ? !is_comma : !is_slash)
+        green = parse_number_percentage_value(inner_tokens);
+        if (!green)
             return {};
+        inner_tokens.skip_whitespace();
 
-        tokens.skip_whitespace();
-        auto const& alpha = tokens.next_token();
-
-        if (alpha.is(Token::Type::Number))
-            alpha_val = clamp(lround(alpha.token().number_value() * 255.0), 0, 255);
-        else if (alpha.is(Token::Type::Percentage))
-            alpha_val = clamp(lround(alpha.token().percentage() * 2.55), 0, 255);
-        else
+        blue = parse_number_percentage_value(inner_tokens);
+        if (!blue)
             return {};
+        inner_tokens.skip_whitespace();
 
-        tokens.skip_whitespace();
-        if (tokens.has_next_token())
-            return {}; // should have consumed all arguments.
+        if (inner_tokens.has_next_token()) {
+            alpha = parse_solidus_and_alpha_value(inner_tokens);
+            if (!alpha || inner_tokens.has_next_token())
+                return {};
+        }
     }
 
-    return Color(r_val, g_val, b_val, alpha_val);
-}
-
-Optional<Color> Parser::parse_hsl_color(Vector<ComponentValue> const& component_values)
-{
-    float h_val = 0.0;
-    float s_val = 0.0;
-    float l_val = 0.0;
-
-    auto tokens = TokenStream { component_values };
+    if (!alpha)
+        alpha = NumberStyleValue::create(1);
 
-    tokens.skip_whitespace();
-    auto const& hue = tokens.next_token();
-
-    if (!hue.is(Token::Type::Number)
-        && !hue.is(Token::Type::Dimension))
+    transaction.commit();
+    return CSSRGB::create(red.release_nonnull(), green.release_nonnull(), blue.release_nonnull(), alpha.release_nonnull());
+}
+
+// https://www.w3.org/TR/css-color-4/#funcdef-hsl
+RefPtr<CSSStyleValue> Parser::parse_hsl_color_value(TokenStream<ComponentValue>& outer_tokens)
+{
+    // hsl() = [ <legacy-hsl-syntax> | <modern-hsl-syntax> ]
+    // hsla() = [ <legacy-hsla-syntax> | <modern-hsla-syntax> ]
+    // <modern-hsl-syntax> = hsl(
+    //     [<hue> | none]
+    //     [<percentage> | <number> | none]
+    //     [<percentage> | <number> | none]
+    //     [ / [<alpha-value> | none] ]? )
+    // <modern-hsla-syntax> = hsla(
+    //     [<hue> | none]
+    //     [<percentage> | <number> | none]
+    //     [<percentage> | <number> | none]
+    //     [ / [<alpha-value> | none] ]? )
+    // <legacy-hsl-syntax> = hsl( <hue>, <percentage>, <percentage>, <alpha-value>? )
+    // <legacy-hsla-syntax> = hsla( <hue>, <percentage>, <percentage>, <alpha-value>? )
+    // TODO: Handle none values
+
+    auto transaction = outer_tokens.begin_transaction();
+    outer_tokens.skip_whitespace();
+
+    auto& function_token = outer_tokens.next_token();
+    if (!function_token.is_function("hsl"sv) && !function_token.is_function("hsla"sv))
         return {};
 
-    if (hue.is(Token::Type::Number)) {
-        h_val = fmod(hue.token().number_value(), 360.0);
-    } else {
-        auto numeric_value = hue.token().dimension_value();
-        auto unit_string = hue.token().dimension_unit();
-        auto angle_type = Angle::unit_from_name(unit_string);
+    RefPtr<CSSStyleValue> h;
+    RefPtr<CSSStyleValue> s;
+    RefPtr<CSSStyleValue> l;
+    RefPtr<CSSStyleValue> alpha;
 
-        if (!angle_type.has_value())
-            return {};
-
-        auto angle = Angle { numeric_value, angle_type.release_value() };
+    auto inner_tokens = TokenStream { function_token.function().values() };
+    inner_tokens.skip_whitespace();
 
-        h_val = fmod(angle.to_degrees(), 360.0);
-    }
+    h = parse_hue_value(inner_tokens);
+    if (!h)
+        return {};
 
-    tokens.skip_whitespace();
-    bool legacy_syntax = tokens.peek_token().is(Token::Type::Comma);
+    inner_tokens.skip_whitespace();
+    bool legacy_syntax = inner_tokens.peek_token().is(Token::Type::Comma);
     if (legacy_syntax) {
-        // legacy syntax.
-        tokens.next_token();
-        tokens.skip_whitespace();
+        // Legacy syntax
+        //   <hue>, <percentage>, <percentage>, <alpha-value>?
+        (void)inner_tokens.next_token(); // comma
+        inner_tokens.skip_whitespace();
 
-        auto const& saturation = tokens.next_token();
-        if (!saturation.is(Token::Type::Percentage))
+        s = parse_percentage_value(inner_tokens);
+        if (!s)
             return {};
-        s_val = max(saturation.token().percentage() / 100.0, 0);
-
-        tokens.skip_whitespace();
-        tokens.next_token();
-        tokens.skip_whitespace();
+        inner_tokens.skip_whitespace();
 
-        auto const& lightness = tokens.next_token();
-        if (!lightness.is(Token::Type::Percentage))
+        if (!inner_tokens.next_token().is(Token::Type::Comma))
             return {};
-        l_val = lightness.token().percentage() / 100.0;
-    } else {
-        // Modern syntax.
+        inner_tokens.skip_whitespace();
 
-        auto const& saturation = tokens.next_token();
-        if (saturation.is(Token::Type::Number)) {
-            s_val = saturation.token().number_value() / 100.0;
-        } else if (saturation.is(Token::Type::Percentage)) {
-            s_val = saturation.token().percentage() / 100.0;
-        } else {
+        l = parse_percentage_value(inner_tokens);
+        if (!l)
             return {};
-        }
-        s_val = max(s_val, 0);
+        inner_tokens.skip_whitespace();
 
-        tokens.skip_whitespace();
-        auto const& lightness = tokens.next_token();
-        if (lightness.is(Token::Type::Number)) {
-            l_val = lightness.token().number_value() / 100.0;
-        } else if (lightness.is(Token::Type::Percentage)) {
-            l_val = lightness.token().percentage() / 100.0;
-        } else {
-            return {};
-        }
-    }
+        if (inner_tokens.has_next_token()) {
+            // Try and read comma and alpha
+            if (!inner_tokens.next_token().is(Token::Type::Comma))
+                return {};
+            inner_tokens.skip_whitespace();
 
-    float alpha_val = 1.0;
-    tokens.skip_whitespace();
-    if (tokens.has_next_token()) {
-        auto const& separator = tokens.next_token();
-        bool is_comma = separator.is(Token::Type::Comma);
-        bool is_slash = separator.is_delim('/');
-        if (legacy_syntax ? !is_comma : !is_slash)
-            return {};
+            alpha = parse_number_percentage_value(inner_tokens);
+            inner_tokens.skip_whitespace();
 
-        tokens.skip_whitespace();
-        auto const& alpha = tokens.next_token();
+            if (inner_tokens.has_next_token())
+                return {};
+        }
+    } else {
+        // Modern syntax
+        //   [<hue> | none]
+        //   [<percentage> | <number> | none]
+        //   [<percentage> | <number> | none]
+        //   [ / [<alpha-value> | none] ]?
+
+        s = parse_number_percentage_value(inner_tokens);
+        if (!s)
+            return {};
+        inner_tokens.skip_whitespace();
 
-        if (alpha.is(Token::Type::Number))
-            alpha_val = alpha.token().number_value();
-        else if (alpha.is(Token::Type::Percentage))
-            alpha_val = alpha.token().percentage() / 100.0;
-        else
+        l = parse_number_percentage_value(inner_tokens);
+        if (!l)
             return {};
+        inner_tokens.skip_whitespace();
 
-        tokens.skip_whitespace();
-        if (tokens.has_next_token())
-            return {}; // should have consumed all arguments.
+        if (inner_tokens.has_next_token()) {
+            alpha = parse_solidus_and_alpha_value(inner_tokens);
+            if (!alpha || inner_tokens.has_next_token())
+                return {};
+        }
     }
 
-    return Color::from_hsla(h_val, s_val, l_val, alpha_val);
+    if (!alpha)
+        alpha = NumberStyleValue::create(1);
+
+    transaction.commit();
+    return CSSHSL::create(h.release_nonnull(), s.release_nonnull(), l.release_nonnull(), alpha.release_nonnull());
 }
 
-Optional<Color> Parser::parse_hwb_color(Vector<ComponentValue> const& component_values)
+// https://www.w3.org/TR/css-color-4/#funcdef-hwb
+RefPtr<CSSStyleValue> Parser::parse_hwb_color_value(TokenStream<ComponentValue>& outer_tokens)
 {
-    float h_val = 0.0;
-    float w_val = 0.0;
-    float b_val = 0.0;
-
-    auto tokens = TokenStream { component_values };
+    // hwb() = hwb(
+    //     [<hue> | none]
+    //     [<percentage> | <number> | none]
+    //     [<percentage> | <number> | none]
+    //     [ / [<alpha-value> | none] ]? )
 
-    tokens.skip_whitespace();
-    auto const& hue = tokens.next_token();
+    auto transaction = outer_tokens.begin_transaction();
+    outer_tokens.skip_whitespace();
 
-    if (!hue.is(Token::Type::Number)
-        && !hue.is(Token::Type::Dimension))
+    auto& function_token = outer_tokens.next_token();
+    if (!function_token.is_function("hwb"sv))
         return {};
 
-    if (hue.is(Token::Type::Number)) {
-        h_val = fmod(hue.token().number_value(), 360.0);
-    } else {
-        auto numeric_value = hue.token().dimension_value();
-        auto unit_string = hue.token().dimension_unit();
-        auto angle_type = Angle::unit_from_name(unit_string);
-
-        if (!angle_type.has_value())
-            return {};
-
-        auto angle = Angle { numeric_value, angle_type.release_value() };
+    RefPtr<CSSStyleValue> h;
+    RefPtr<CSSStyleValue> w;
+    RefPtr<CSSStyleValue> b;
+    RefPtr<CSSStyleValue> alpha;
 
-        h_val = fmod(angle.to_degrees(), 360);
-    }
+    auto inner_tokens = TokenStream { function_token.function().values() };
+    inner_tokens.skip_whitespace();
 
-    tokens.skip_whitespace();
-    auto const& whiteness = tokens.next_token();
-    if (whiteness.is(Token::Type::Number)) {
-        w_val = whiteness.token().number_value() / 100.0;
-    } else if (whiteness.is(Token::Type::Percentage)) {
-        w_val = whiteness.token().percentage() / 100.0;
-    } else {
+    h = parse_hue_value(inner_tokens);
+    if (!h)
         return {};
-    }
+    inner_tokens.skip_whitespace();
 
-    tokens.skip_whitespace();
-    auto const& blackness = tokens.next_token();
-    if (blackness.is(Token::Type::Number)) {
-        b_val = blackness.token().number_value() / 100.0;
-    } else if (blackness.is(Token::Type::Percentage)) {
-        b_val = blackness.token().percentage() / 100.0;
-    } else {
+    w = parse_number_percentage_value(inner_tokens);
+    if (!w)
         return {};
-    }
+    inner_tokens.skip_whitespace();
 
-    float alpha_val = 1.0;
-    tokens.skip_whitespace();
-    if (tokens.has_next_token()) {
-        auto const& separator = tokens.next_token();
-        if (!separator.is_delim('/'))
-            return {};
-
-        tokens.skip_whitespace();
-        auto const& alpha = tokens.next_token();
+    b = parse_number_percentage_value(inner_tokens);
+    if (!b)
+        return {};
+    inner_tokens.skip_whitespace();
 
-        if (alpha.is(Token::Type::Number))
-            alpha_val = alpha.token().number_value();
-        else if (alpha.is(Token::Type::Percentage))
-            alpha_val = alpha.token().percentage() / 100.0;
-        else
+    if (inner_tokens.has_next_token()) {
+        alpha = parse_solidus_and_alpha_value(inner_tokens);
+        if (!alpha || inner_tokens.has_next_token())
             return {};
-
-        tokens.skip_whitespace();
-        if (tokens.has_next_token())
-            return {}; // should have consumed all arguments.
     }
 
-    if (w_val + b_val >= 1.0) {
-        u8 gray = clamp(llroundf((w_val / (w_val + b_val)) * 255), 0, 255);
-        return Color(gray, gray, gray, clamp(llroundf(alpha_val * 255), 0, 255));
-    }
+    if (!alpha)
+        alpha = NumberStyleValue::create(1);
 
-    float value = 1 - b_val;
-    float saturation = 1 - (w_val / value);
-    return Color::from_hsv(h_val, saturation, value).with_opacity(alpha_val);
+    transaction.commit();
+    return CSSHWB::create(h.release_nonnull(), w.release_nonnull(), b.release_nonnull(), alpha.release_nonnull());
 }
 
-Optional<Color> Parser::parse_oklab_color(Vector<ComponentValue> const& component_values)
+// https://www.w3.org/TR/css-color-4/#funcdef-oklab
+RefPtr<CSSStyleValue> Parser::parse_oklab_color_value(TokenStream<ComponentValue>& outer_tokens)
 {
-    float L_val = 0.0;
-    float a_val = 0.0;
-    float b_val = 0.0;
+    // oklab() = oklab( [ <percentage> | <number> | none]
+    //     [ <percentage> | <number> | none]
+    //     [ <percentage> | <number> | none]
+    //     [ / [<alpha-value> | none] ]? )
 
-    auto tokens = TokenStream { component_values };
+    auto transaction = outer_tokens.begin_transaction();
+    outer_tokens.skip_whitespace();
 
-    tokens.skip_whitespace();
-    auto const& lightness = tokens.next_token();
-    if (lightness.is(Token::Type::Number)) {
-        L_val = lightness.token().number_value();
-    } else if (lightness.is(Token::Type::Percentage)) {
-        L_val = lightness.token().percentage() / 100.0;
-    } else {
+    auto& function_token = outer_tokens.next_token();
+    if (!function_token.is_function("oklab"sv))
         return {};
-    }
-    L_val = clamp(L_val, 0.0, 1.0);
 
-    tokens.skip_whitespace();
-    auto const& a = tokens.next_token();
-    if (a.is(Token::Type::Number)) {
-        a_val = a.token().number_value();
-    } else if (a.is(Token::Type::Percentage)) {
-        a_val = a.token().percentage() / 100.0 * 0.4;
-    } else {
-        return {};
-    }
+    RefPtr<CSSStyleValue> l;
+    RefPtr<CSSStyleValue> a;
+    RefPtr<CSSStyleValue> b;
+    RefPtr<CSSStyleValue> alpha;
 
-    tokens.skip_whitespace();
-    auto const& b = tokens.next_token();
-    if (b.is(Token::Type::Number)) {
-        b_val = b.token().number_value();
-    } else if (a.is(Token::Type::Percentage)) {
-        b_val = b.token().percentage() / 100.0 * 0.4;
-    } else {
+    auto inner_tokens = TokenStream { function_token.function().values() };
+    inner_tokens.skip_whitespace();
+
+    l = parse_number_percentage_value(inner_tokens);
+    if (!l)
         return {};
-    }
+    inner_tokens.skip_whitespace();
 
-    float alpha_val = 1.0;
-    tokens.skip_whitespace();
-    if (tokens.has_next_token()) {
-        auto const& separator = tokens.next_token();
-        if (!separator.is_delim('/'))
-            return {};
+    a = parse_number_percentage_value(inner_tokens);
+    if (!a)
+        return {};
+    inner_tokens.skip_whitespace();
 
-        tokens.skip_whitespace();
-        auto const& alpha = tokens.next_token();
+    b = parse_number_percentage_value(inner_tokens);
+    if (!b)
+        return {};
+    inner_tokens.skip_whitespace();
 
-        if (alpha.is(Token::Type::Number))
-            alpha_val = alpha.token().number_value();
-        else if (alpha.is(Token::Type::Percentage))
-            alpha_val = alpha.token().percentage() / 100.0;
-        else
+    if (inner_tokens.has_next_token()) {
+        alpha = parse_solidus_and_alpha_value(inner_tokens);
+        if (!alpha || inner_tokens.has_next_token())
             return {};
-
-        tokens.skip_whitespace();
-        if (tokens.has_next_token())
-            return {}; // should have consumed all arguments.
     }
 
-    return Color::from_oklab(L_val, a_val, b_val, alpha_val);
+    if (!alpha)
+        alpha = NumberStyleValue::create(1);
+
+    transaction.commit();
+    return CSSOKLab::create(l.release_nonnull(), a.release_nonnull(), b.release_nonnull(), alpha.release_nonnull());
 }
 
-Optional<Color> Parser::parse_oklch_color(Vector<ComponentValue> const& component_values)
+// https://www.w3.org/TR/css-color-4/#funcdef-oklch
+RefPtr<CSSStyleValue> Parser::parse_oklch_color_value(TokenStream<ComponentValue>& outer_tokens)
 {
-    float L_val = 0.0;
-    float c_val = 0.0;
-    float h_val = 0.0;
+    // oklch() = oklch( [ <percentage> | <number> | none]
+    //     [ <percentage> | <number> | none]
+    //     [ <hue> | none]
+    //     [ / [<alpha-value> | none] ]? )
 
-    auto tokens = TokenStream { component_values };
+    auto transaction = outer_tokens.begin_transaction();
+    outer_tokens.skip_whitespace();
 
-    tokens.skip_whitespace();
-    auto const& lightness = tokens.next_token();
-    if (lightness.is(Token::Type::Number)) {
-        L_val = lightness.token().number_value();
-    } else if (lightness.is(Token::Type::Percentage)) {
-        L_val = lightness.token().percentage() / 100.0;
-    } else {
+    auto& function_token = outer_tokens.next_token();
+    if (!function_token.is_function("oklch"sv))
         return {};
-    }
-    L_val = clamp(L_val, 0.0, 1.0);
 
-    tokens.skip_whitespace();
-    auto const& chroma = tokens.next_token();
-    if (chroma.is(Token::Type::Number)) {
-        c_val = chroma.token().number_value();
-    } else if (chroma.is(Token::Type::Percentage)) {
-        c_val = chroma.token().percentage() / 100.0 * 0.4;
-    } else {
-        return {};
-    }
-    c_val = max(c_val, 0.0);
+    RefPtr<CSSStyleValue> l;
+    RefPtr<CSSStyleValue> c;
+    RefPtr<CSSStyleValue> h;
+    RefPtr<CSSStyleValue> alpha;
 
-    tokens.skip_whitespace();
-    auto const& hue = tokens.next_token();
+    auto inner_tokens = TokenStream { function_token.function().values() };
+    inner_tokens.skip_whitespace();
 
-    if (!hue.is(Token::Type::Number)
-        && !hue.is(Token::Type::Dimension))
+    l = parse_number_percentage_value(inner_tokens);
+    if (!l)
         return {};
+    inner_tokens.skip_whitespace();
 
-    if (hue.is(Token::Type::Number)) {
-        h_val = hue.token().number_value() * AK::Pi<float> / 180;
-    } else {
-        auto numeric_value = hue.token().dimension_value();
-        auto unit_string = hue.token().dimension_unit();
-        auto angle_type = Angle::unit_from_name(unit_string);
-
-        if (!angle_type.has_value())
-            return {};
+    c = parse_number_percentage_value(inner_tokens);
+    if (!c)
+        return {};
+    inner_tokens.skip_whitespace();
 
-        auto angle = Angle { numeric_value, angle_type.release_value() };
+    h = parse_hue_value(inner_tokens);
+    if (!h)
+        return {};
+    inner_tokens.skip_whitespace();
 
-        h_val = angle.to_radians();
+    if (inner_tokens.has_next_token()) {
+        alpha = parse_solidus_and_alpha_value(inner_tokens);
+        if (!alpha || inner_tokens.has_next_token())
+            return {};
     }
 
-    float alpha_val = 1.0;
-    tokens.skip_whitespace();
-    if (tokens.has_next_token()) {
-        auto const& separator = tokens.next_token();
-        if (!separator.is_delim('/'))
-            return {};
+    if (!alpha)
+        alpha = NumberStyleValue::create(1);
 
-        tokens.skip_whitespace();
-        auto const& alpha = tokens.next_token();
+    transaction.commit();
+    return CSSOKLCH::create(l.release_nonnull(), c.release_nonnull(), h.release_nonnull(), alpha.release_nonnull());
+}
 
-        if (alpha.is(Token::Type::Number))
-            alpha_val = alpha.token().number_value();
-        else if (alpha.is(Token::Type::Percentage))
-            alpha_val = alpha.token().percentage() / 100.0;
-        else
-            return {};
+// https://www.w3.org/TR/css-color-4/#color-syntax
+RefPtr<CSSStyleValue> Parser::parse_color_value(TokenStream<ComponentValue>& tokens)
+{
 
-        tokens.skip_whitespace();
-        if (tokens.has_next_token())
-            return {}; // should have consumed all arguments.
+    // Keywords: <system-color> | <deprecated-color> | currentColor
+    {
+        auto transaction = tokens.begin_transaction();
+        if (auto keyword = parse_keyword_value(tokens); keyword && keyword->has_color()) {
+            transaction.commit();
+            return keyword;
+        }
     }
 
-    return Color::from_oklab(L_val, c_val * cos(h_val), c_val * sin(h_val), alpha_val);
-}
+    // Functions
+    if (auto rgb = parse_rgb_color_value(tokens))
+        return rgb;
+    if (auto hsl = parse_hsl_color_value(tokens))
+        return hsl;
+    if (auto hwb = parse_hwb_color_value(tokens))
+        return hwb;
+    if (auto oklab = parse_oklab_color_value(tokens))
+        return oklab;
+    if (auto oklch = parse_oklch_color_value(tokens))
+        return oklch;
 
-Optional<Color> Parser::parse_color(TokenStream<ComponentValue>& tokens)
-{
     auto transaction = tokens.begin_transaction();
-    auto commit_if_valid = [&](Optional<Color> color) {
-        if (color.has_value())
-            transaction.commit();
-        return color;
-    };
-
     tokens.skip_whitespace();
     auto component_value = tokens.next_token();
 
-    // https://www.w3.org/TR/css-color-4/
     if (component_value.is(Token::Type::Ident)) {
         auto ident = component_value.token().ident();
 
         auto color = Color::from_string(ident);
         if (color.has_value()) {
             transaction.commit();
-            return color;
+            return CSSColorValue::create_from_color(color.release_value());
         }
         // Otherwise, fall through to the hashless-hex-color case
-    } else if (component_value.is(Token::Type::Hash)) {
-        auto color = Color::from_string(MUST(String::formatted("#{}", component_value.token().hash_value())));
-        return commit_if_valid(color);
-    } else if (component_value.is_function()) {
-        auto const& function = component_value.function();
-        auto const& values = function.values();
-        auto const function_name = function.name();
-
-        if (function_name.equals_ignoring_ascii_case("rgb"sv) || function_name.equals_ignoring_ascii_case("rgba"sv))
-            return commit_if_valid(parse_rgb_color(values));
-        if (function_name.equals_ignoring_ascii_case("hsl"sv) || function_name.equals_ignoring_ascii_case("hsla"sv))
-            return commit_if_valid(parse_hsl_color(values));
-        if (function_name.equals_ignoring_ascii_case("hwb"sv))
-            return commit_if_valid(parse_hwb_color(values));
-        if (function_name.equals_ignoring_ascii_case("oklab"sv))
-            return commit_if_valid(parse_oklab_color(values));
-        if (function_name.equals_ignoring_ascii_case("oklch"sv))
-            return commit_if_valid(parse_oklch_color(values));
+    }
 
+    if (component_value.is(Token::Type::Hash)) {
+        auto color = Color::from_string(MUST(String::formatted("#{}", component_value.token().hash_value())));
+        if (color.has_value()) {
+            transaction.commit();
+            return CSSColorValue::create_from_color(color.release_value());
+        }
         return {};
     }
 
@@ -3149,26 +3134,16 @@ Optional<Color> Parser::parse_color(TokenStream<ComponentValue>& tokens)
         }
 
         // 6. Return the concatenation of "#" (U+0023) and serialization.
-        return commit_if_valid(Color::from_string(MUST(String::formatted("#{}", serialization))));
+        auto color = Color::from_string(MUST(String::formatted("#{}", serialization)));
+        if (color.has_value()) {
+            transaction.commit();
+            return CSSColorValue::create_from_color(color.release_value());
+        }
     }
 
     return {};
 }
 
-RefPtr<CSSStyleValue> Parser::parse_color_value(TokenStream<ComponentValue>& tokens)
-{
-    if (auto color = parse_color(tokens); color.has_value())
-        return CSSColorValue::create_from_color(color.value());
-
-    auto transaction = tokens.begin_transaction();
-    if (auto keyword = parse_keyword_value(tokens); keyword && keyword->has_color()) {
-        transaction.commit();
-        return keyword;
-    }
-
-    return nullptr;
-}
-
 // https://drafts.csswg.org/css-lists-3/#counter-functions
 RefPtr<CSSStyleValue> Parser::parse_counter_value(TokenStream<ComponentValue>& tokens)
 {

+ 8 - 7
Userland/Libraries/LibWeb/CSS/Parser/Parser.h

@@ -1,6 +1,6 @@
 /*
  * Copyright (c) 2020-2021, the SerenityOS developers.
- * Copyright (c) 2021-2024, Sam Atkins <atkinssj@serenityos.org>
+ * Copyright (c) 2021-2024, Sam Atkins <sam@ladybird.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -194,12 +194,6 @@ private:
     Optional<TimeOrCalculated> parse_time(TokenStream<ComponentValue>&);
     Optional<TimePercentage> parse_time_percentage(TokenStream<ComponentValue>&);
 
-    Optional<Color> parse_rgb_color(Vector<ComponentValue> const&);
-    Optional<Color> parse_hsl_color(Vector<ComponentValue> const&);
-    Optional<Color> parse_hwb_color(Vector<ComponentValue> const&);
-    Optional<Color> parse_oklab_color(Vector<ComponentValue> const&);
-    Optional<Color> parse_oklch_color(Vector<ComponentValue> const&);
-    Optional<Color> parse_color(TokenStream<ComponentValue>&);
     Optional<LengthOrCalculated> parse_source_size_value(TokenStream<ComponentValue>&);
     Optional<Ratio> parse_ratio(TokenStream<ComponentValue>&);
     Optional<Gfx::UnicodeRange> parse_unicode_range(TokenStream<ComponentValue>&);
@@ -239,6 +233,13 @@ private:
     OwnPtr<CalculationNode> parse_math_function(PropertyID, Function const&);
     OwnPtr<CalculationNode> parse_a_calc_function_node(Function const&);
     RefPtr<CSSStyleValue> parse_keyword_value(TokenStream<ComponentValue>&);
+    RefPtr<CSSStyleValue> parse_hue_value(TokenStream<ComponentValue>&);
+    RefPtr<CSSStyleValue> parse_solidus_and_alpha_value(TokenStream<ComponentValue>&);
+    RefPtr<CSSStyleValue> parse_rgb_color_value(TokenStream<ComponentValue>&);
+    RefPtr<CSSStyleValue> parse_hsl_color_value(TokenStream<ComponentValue>&);
+    RefPtr<CSSStyleValue> parse_hwb_color_value(TokenStream<ComponentValue>&);
+    RefPtr<CSSStyleValue> parse_oklab_color_value(TokenStream<ComponentValue>&);
+    RefPtr<CSSStyleValue> parse_oklch_color_value(TokenStream<ComponentValue>&);
     RefPtr<CSSStyleValue> parse_color_value(TokenStream<ComponentValue>&);
     RefPtr<CSSStyleValue> parse_counter_value(TokenStream<ComponentValue>&);
     enum class AllowReversed {

+ 6 - 2
Userland/Libraries/LibWeb/CSS/StyleComputer.cpp

@@ -1431,8 +1431,12 @@ static NonnullRefPtr<CSSStyleValue const> interpolate_value(DOM::Element& elemen
     switch (from.type()) {
     case CSSStyleValue::Type::Angle:
         return AngleStyleValue::create(Angle::make_degrees(interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees(), delta)));
-    case CSSStyleValue::Type::Color:
-        return CSSColorValue::create_from_color(interpolate_color(from.as_color().color(), to.as_color().color(), delta));
+    case CSSStyleValue::Type::Color: {
+        Optional<Layout::NodeWithStyle const&> layout_node;
+        if (auto node = element.layout_node())
+            layout_node = *node;
+        return CSSColorValue::create_from_color(interpolate_color(from.to_color(layout_node), to.to_color(layout_node), delta));
+    }
     case CSSStyleValue::Type::Integer:
         return IntegerStyleValue::create(interpolate_raw(from.as_integer().integer(), to.as_integer().integer(), delta));
     case CSSStyleValue::Type::Length: {

+ 90 - 6
Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.cpp

@@ -9,32 +9,116 @@
 
 #include "CSSColorValue.h"
 #include <LibWeb/CSS/Serialize.h>
+#include <LibWeb/CSS/StyleValues/AngleStyleValue.h>
+#include <LibWeb/CSS/StyleValues/CSSRGB.h>
+#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
+#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
+#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
 
 namespace Web::CSS {
 
 ValueComparingNonnullRefPtr<CSSColorValue> CSSColorValue::create_from_color(Color color)
 {
+    auto make_rgb_color = [](Color const& color) {
+        return CSSRGB::create(
+            NumberStyleValue::create(color.red()),
+            NumberStyleValue::create(color.green()),
+            NumberStyleValue::create(color.blue()),
+            NumberStyleValue::create(color.alpha() / 255.0));
+    };
+
     if (color.value() == 0) {
-        static auto transparent = adopt_ref(*new (nothrow) CSSColorValue(color));
+        static auto transparent = make_rgb_color(color);
         return transparent;
     }
 
     if (color == Color::from_rgb(0x000000)) {
-        static auto black = adopt_ref(*new (nothrow) CSSColorValue(color));
+        static auto black = make_rgb_color(color);
         return black;
     }
 
     if (color == Color::from_rgb(0xffffff)) {
-        static auto white = adopt_ref(*new (nothrow) CSSColorValue(color));
+        static auto white = make_rgb_color(color);
         return white;
     }
 
-    return adopt_ref(*new (nothrow) CSSColorValue(color));
+    return make_rgb_color(color);
+}
+
+Optional<float> CSSColorValue::resolve_hue(CSSStyleValue const& style_value)
+{
+    // <number> | <angle> | none
+    auto normalized = [](double number) {
+        return fmod(number, 360.0);
+    };
+
+    if (style_value.is_number())
+        return normalized(style_value.as_number().number());
+
+    if (style_value.is_angle())
+        return normalized(style_value.as_angle().angle().to_degrees());
+
+    if (style_value.is_calculated() && style_value.as_calculated().resolves_to_angle())
+        return normalized(style_value.as_calculated().resolve_angle().value().to_degrees());
+
+    if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
+        return 0;
+
+    return {};
+}
+
+Optional<float> CSSColorValue::resolve_with_reference_value(CSSStyleValue const& style_value, float one_hundred_percent_value)
+{
+    // <percentage> | <number> | none
+    auto normalize_percentage = [one_hundred_percent_value](Percentage const& percentage) {
+        return percentage.as_fraction() * one_hundred_percent_value;
+    };
+
+    if (style_value.is_percentage())
+        return normalize_percentage(style_value.as_percentage().percentage());
+
+    if (style_value.is_number())
+        return style_value.as_number().number();
+
+    if (style_value.is_calculated()) {
+        auto const& calculated = style_value.as_calculated();
+        if (calculated.resolves_to_number())
+            return calculated.resolve_number().value();
+        if (calculated.resolves_to_percentage())
+            return normalize_percentage(calculated.resolve_percentage().value());
+    }
+
+    if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
+        return 0;
+
+    return {};
 }
 
-String CSSColorValue::to_string() const
+Optional<float> CSSColorValue::resolve_alpha(CSSStyleValue const& style_value)
 {
-    return serialize_a_srgb_value(m_color);
+    // <number> | <percentage> | none
+    auto normalized = [](double number) {
+        return clamp(number, 0.0, 1.0);
+    };
+
+    if (style_value.is_number())
+        return normalized(style_value.as_number().number());
+
+    if (style_value.is_percentage())
+        return normalized(style_value.as_percentage().percentage().as_fraction());
+
+    if (style_value.is_calculated()) {
+        auto const& calculated = style_value.as_calculated();
+        if (calculated.resolves_to_number())
+            return normalized(calculated.resolve_number().value());
+        if (calculated.resolves_to_percentage())
+            return normalized(calculated.resolve_percentage().value().as_fraction());
+    }
+
+    if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
+        return 0;
+
+    return {};
 }
 
 }

+ 20 - 11
Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h

@@ -15,26 +15,35 @@
 namespace Web::CSS {
 
 // https://drafts.css-houdini.org/css-typed-om-1/#csscolorvalue
-class CSSColorValue : public StyleValueWithDefaultOperators<CSSColorValue> {
+class CSSColorValue : public CSSStyleValue {
 public:
     static ValueComparingNonnullRefPtr<CSSColorValue> create_from_color(Color color);
     virtual ~CSSColorValue() override = default;
 
-    Color color() const { return m_color; }
-    virtual String to_string() const override;
     virtual bool has_color() const override { return true; }
-    virtual Color to_color(Optional<Layout::NodeWithStyle const&>) const override { return m_color; }
 
-    bool properties_equal(CSSColorValue const& other) const { return m_color == other.m_color; }
-
-private:
-    explicit CSSColorValue(Color color)
-        : StyleValueWithDefaultOperators(Type::Color)
-        , m_color(color)
+    enum class ColorType {
+        RGB,
+        HSL,
+        HWB,
+        OKLab,
+        OKLCH,
+    };
+    ColorType color_type() const { return m_color_type; }
+
+protected:
+    explicit CSSColorValue(ColorType color_type)
+        : CSSStyleValue(Type::Color)
+        , m_color_type(color_type)
     {
     }
 
-    Color m_color;
+    static Optional<float> resolve_hue(CSSStyleValue const&);
+    static Optional<float> resolve_with_reference_value(CSSStyleValue const&, float one_hundred_percent_value);
+    static Optional<float> resolve_alpha(CSSStyleValue const&);
+
+private:
+    ColorType m_color_type;
 };
 
 }

+ 41 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.cpp

@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CSSHSL.h"
+#include <AK/TypeCasts.h>
+#include <LibWeb/CSS/Serialize.h>
+
+namespace Web::CSS {
+
+Color CSSHSL::to_color(Optional<Layout::NodeWithStyle const&>) const
+{
+    auto const h_val = resolve_hue(m_properties.h).value_or(0);
+    auto const s_val = resolve_with_reference_value(m_properties.s, 100.0).value_or(0);
+    auto const l_val = resolve_with_reference_value(m_properties.l, 100.0).value_or(0);
+    auto const alpha_val = resolve_alpha(m_properties.alpha).value_or(1);
+
+    return Color::from_hsla(h_val, s_val / 100.0f, l_val / 100.0f, alpha_val);
+}
+
+bool CSSHSL::equals(CSSStyleValue const& other) const
+{
+    if (type() != other.type())
+        return false;
+    auto const& other_color = other.as_color();
+    if (color_type() != other_color.color_type())
+        return false;
+    auto const& other_hsl = verify_cast<CSSHSL>(other_color);
+    return m_properties == other_hsl.m_properties;
+}
+
+// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
+String CSSHSL::to_string() const
+{
+    // FIXME: Do this properly, taking unresolved calculated values into account.
+    return serialize_a_srgb_value(to_color({}));
+}
+
+}

+ 54 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CSSHSL.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibWeb/CSS/StyleValues/CSSColorValue.h>
+#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
+
+namespace Web::CSS {
+
+// https://drafts.css-houdini.org/css-typed-om-1/#csshsl
+class CSSHSL final : public CSSColorValue {
+public:
+    static ValueComparingNonnullRefPtr<CSSHSL> create(ValueComparingNonnullRefPtr<CSSStyleValue> h, ValueComparingNonnullRefPtr<CSSStyleValue> s, ValueComparingNonnullRefPtr<CSSStyleValue> l, ValueComparingRefPtr<CSSStyleValue> alpha = {})
+    {
+        // alpha defaults to 1
+        if (!alpha)
+            return adopt_ref(*new (nothrow) CSSHSL(move(h), move(s), move(l), NumberStyleValue::create(1)));
+
+        return adopt_ref(*new (nothrow) CSSHSL(move(h), move(s), move(l), alpha.release_nonnull()));
+    }
+    virtual ~CSSHSL() override = default;
+
+    CSSStyleValue const& h() const { return *m_properties.h; }
+    CSSStyleValue const& s() const { return *m_properties.s; }
+    CSSStyleValue const& l() const { return *m_properties.l; }
+    CSSStyleValue const& alpha() const { return *m_properties.alpha; }
+
+    virtual Color to_color(Optional<Layout::NodeWithStyle const&>) const override;
+
+    String to_string() const override;
+
+    virtual bool equals(CSSStyleValue const& other) const override;
+
+private:
+    CSSHSL(ValueComparingNonnullRefPtr<CSSStyleValue> h, ValueComparingNonnullRefPtr<CSSStyleValue> s, ValueComparingNonnullRefPtr<CSSStyleValue> l, ValueComparingNonnullRefPtr<CSSStyleValue> alpha)
+        : CSSColorValue(ColorType::HSL)
+        , m_properties { .h = move(h), .s = move(s), .l = move(l), .alpha = move(alpha) }
+    {
+    }
+
+    struct Properties {
+        ValueComparingNonnullRefPtr<CSSStyleValue> h;
+        ValueComparingNonnullRefPtr<CSSStyleValue> s;
+        ValueComparingNonnullRefPtr<CSSStyleValue> l;
+        ValueComparingNonnullRefPtr<CSSStyleValue> alpha;
+        bool operator==(Properties const&) const = default;
+    } m_properties;
+};
+
+}

+ 51 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CSSHWB.cpp

@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CSSHWB.h"
+#include <AK/TypeCasts.h>
+#include <LibWeb/CSS/Serialize.h>
+
+namespace Web::CSS {
+
+Color CSSHWB::to_color(Optional<Layout::NodeWithStyle const&>) const
+{
+    auto const h_val = resolve_hue(m_properties.h).value_or(0);
+    auto const w_val = clamp(resolve_with_reference_value(m_properties.w, 100.0).value_or(0), 0, 100) / 100.0;
+    auto const b_val = clamp(resolve_with_reference_value(m_properties.b, 100.0).value_or(0), 0, 100) / 100.0;
+    auto const alpha_val = resolve_alpha(m_properties.alpha).value_or(1);
+
+    if (w_val + b_val >= 1.0) {
+        auto to_byte = [](double value) {
+            return round_to<u8>(clamp(value * 255.0, 0.0, 255.0));
+        };
+        u8 gray = to_byte(w_val / (w_val + b_val));
+        return Color(gray, gray, gray, to_byte(alpha_val));
+    }
+
+    float value = 1 - b_val;
+    float saturation = 1 - (w_val / value);
+    return Color::from_hsv(h_val, saturation, value).with_opacity(alpha_val);
+}
+
+bool CSSHWB::equals(CSSStyleValue const& other) const
+{
+    if (type() != other.type())
+        return false;
+    auto const& other_color = other.as_color();
+    if (color_type() != other_color.color_type())
+        return false;
+    auto const& other_hwb = verify_cast<CSSHWB>(other_color);
+    return m_properties == other_hwb.m_properties;
+}
+
+// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
+String CSSHWB::to_string() const
+{
+    // FIXME: Do this properly, taking unresolved calculated values into account.
+    return serialize_a_srgb_value(to_color({}));
+}
+
+}

+ 54 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CSSHWB.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibWeb/CSS/StyleValues/CSSColorValue.h>
+#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
+
+namespace Web::CSS {
+
+// https://drafts.css-houdini.org/css-typed-om-1/#csshwb
+class CSSHWB final : public CSSColorValue {
+public:
+    static ValueComparingNonnullRefPtr<CSSHWB> create(ValueComparingNonnullRefPtr<CSSStyleValue> h, ValueComparingNonnullRefPtr<CSSStyleValue> w, ValueComparingNonnullRefPtr<CSSStyleValue> b, ValueComparingRefPtr<CSSStyleValue> alpha = {})
+    {
+        // alpha defaults to 1
+        if (!alpha)
+            return adopt_ref(*new (nothrow) CSSHWB(move(h), move(w), move(b), NumberStyleValue::create(1)));
+
+        return adopt_ref(*new (nothrow) CSSHWB(move(h), move(w), move(b), alpha.release_nonnull()));
+    }
+    virtual ~CSSHWB() override = default;
+
+    CSSStyleValue const& h() const { return *m_properties.h; }
+    CSSStyleValue const& w() const { return *m_properties.w; }
+    CSSStyleValue const& b() const { return *m_properties.b; }
+    CSSStyleValue const& alpha() const { return *m_properties.alpha; }
+
+    virtual Color to_color(Optional<Layout::NodeWithStyle const&>) const override;
+
+    String to_string() const override;
+
+    virtual bool equals(CSSStyleValue const& other) const override;
+
+private:
+    CSSHWB(ValueComparingNonnullRefPtr<CSSStyleValue> h, ValueComparingNonnullRefPtr<CSSStyleValue> w, ValueComparingNonnullRefPtr<CSSStyleValue> b, ValueComparingNonnullRefPtr<CSSStyleValue> alpha)
+        : CSSColorValue(ColorType::HWB)
+        , m_properties { .h = move(h), .w = move(w), .b = move(b), .alpha = move(alpha) }
+    {
+    }
+
+    struct Properties {
+        ValueComparingNonnullRefPtr<CSSStyleValue> h;
+        ValueComparingNonnullRefPtr<CSSStyleValue> w;
+        ValueComparingNonnullRefPtr<CSSStyleValue> b;
+        ValueComparingNonnullRefPtr<CSSStyleValue> alpha;
+        bool operator==(Properties const&) const = default;
+    } m_properties;
+};
+
+}

+ 45 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLCH.cpp

@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CSSOKLCH.h"
+#include <AK/Math.h>
+#include <AK/TypeCasts.h>
+#include <LibWeb/CSS/Serialize.h>
+#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
+#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
+#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
+
+namespace Web::CSS {
+
+Color CSSOKLCH::to_color(Optional<Layout::NodeWithStyle const&>) const
+{
+    auto const l_val = clamp(resolve_with_reference_value(m_properties.l, 1.0).value_or(0), 0, 1);
+    auto const c_val = max(resolve_with_reference_value(m_properties.c, 0.4).value_or(0), 0);
+    auto const h_val = AK::to_radians(resolve_hue(m_properties.h).value_or(0));
+    auto const alpha_val = resolve_alpha(m_properties.alpha).value_or(1);
+
+    return Color::from_oklab(l_val, c_val * cos(h_val), c_val * sin(h_val), alpha_val);
+}
+
+bool CSSOKLCH::equals(CSSStyleValue const& other) const
+{
+    if (type() != other.type())
+        return false;
+    auto const& other_color = other.as_color();
+    if (color_type() != other_color.color_type())
+        return false;
+    auto const& other_oklch = verify_cast<CSSOKLCH>(other_color);
+    return m_properties == other_oklch.m_properties;
+}
+
+// https://www.w3.org/TR/css-color-4/#serializing-oklab-oklch
+String CSSOKLCH::to_string() const
+{
+    // FIXME: Do this properly, taking unresolved calculated values into account.
+    return serialize_a_srgb_value(to_color({}));
+}
+
+}

+ 54 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLCH.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibWeb/CSS/StyleValues/CSSColorValue.h>
+#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
+
+namespace Web::CSS {
+
+// https://drafts.css-houdini.org/css-typed-om-1/#cssoklch
+class CSSOKLCH final : public CSSColorValue {
+public:
+    static ValueComparingNonnullRefPtr<CSSOKLCH> create(ValueComparingNonnullRefPtr<CSSStyleValue> l, ValueComparingNonnullRefPtr<CSSStyleValue> c, ValueComparingNonnullRefPtr<CSSStyleValue> h, ValueComparingRefPtr<CSSStyleValue> alpha = {})
+    {
+        // alpha defaults to 1
+        if (!alpha)
+            return adopt_ref(*new (nothrow) CSSOKLCH(move(l), move(c), move(h), NumberStyleValue::create(1)));
+
+        return adopt_ref(*new (nothrow) CSSOKLCH(move(l), move(c), move(h), alpha.release_nonnull()));
+    }
+    virtual ~CSSOKLCH() override = default;
+
+    CSSStyleValue const& l() const { return *m_properties.l; }
+    CSSStyleValue const& c() const { return *m_properties.c; }
+    CSSStyleValue const& h() const { return *m_properties.h; }
+    CSSStyleValue const& alpha() const { return *m_properties.alpha; }
+
+    virtual Color to_color(Optional<Layout::NodeWithStyle const&>) const override;
+
+    String to_string() const override;
+
+    virtual bool equals(CSSStyleValue const& other) const override;
+
+private:
+    CSSOKLCH(ValueComparingNonnullRefPtr<CSSStyleValue> l, ValueComparingNonnullRefPtr<CSSStyleValue> c, ValueComparingNonnullRefPtr<CSSStyleValue> h, ValueComparingNonnullRefPtr<CSSStyleValue> alpha)
+        : CSSColorValue(ColorType::OKLCH)
+        , m_properties { .l = move(l), .c = move(c), .h = move(h), .alpha = move(alpha) }
+    {
+    }
+
+    struct Properties {
+        ValueComparingNonnullRefPtr<CSSStyleValue> l;
+        ValueComparingNonnullRefPtr<CSSStyleValue> c;
+        ValueComparingNonnullRefPtr<CSSStyleValue> h;
+        ValueComparingNonnullRefPtr<CSSStyleValue> alpha;
+        bool operator==(Properties const&) const = default;
+    } m_properties;
+};
+
+}

+ 44 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLab.cpp

@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CSSOKLab.h"
+#include <AK/TypeCasts.h>
+#include <LibWeb/CSS/Serialize.h>
+#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
+#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
+#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
+
+namespace Web::CSS {
+
+Color CSSOKLab::to_color(Optional<Layout::NodeWithStyle const&>) const
+{
+    auto const l_val = clamp(resolve_with_reference_value(m_properties.l, 1.0).value_or(0), 0, 1);
+    auto const a_val = resolve_with_reference_value(m_properties.a, 0.4).value_or(0);
+    auto const b_val = resolve_with_reference_value(m_properties.b, 0.4).value_or(0);
+    auto const alpha_val = resolve_alpha(m_properties.alpha).value_or(1);
+
+    return Color::from_oklab(l_val, a_val, b_val, alpha_val);
+}
+
+bool CSSOKLab::equals(CSSStyleValue const& other) const
+{
+    if (type() != other.type())
+        return false;
+    auto const& other_color = other.as_color();
+    if (color_type() != other_color.color_type())
+        return false;
+    auto const& other_oklab = verify_cast<CSSOKLab>(other_color);
+    return m_properties == other_oklab.m_properties;
+}
+
+// https://www.w3.org/TR/css-color-4/#serializing-oklab-oklch
+String CSSOKLab::to_string() const
+{
+    // FIXME: Do this properly, taking unresolved calculated values into account.
+    return serialize_a_srgb_value(to_color({}));
+}
+
+}

+ 54 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CSSOKLab.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibWeb/CSS/StyleValues/CSSColorValue.h>
+#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
+
+namespace Web::CSS {
+
+// https://drafts.css-houdini.org/css-typed-om-1/#cssoklab
+class CSSOKLab final : public CSSColorValue {
+public:
+    static ValueComparingNonnullRefPtr<CSSOKLab> create(ValueComparingNonnullRefPtr<CSSStyleValue> l, ValueComparingNonnullRefPtr<CSSStyleValue> a, ValueComparingNonnullRefPtr<CSSStyleValue> b, ValueComparingRefPtr<CSSStyleValue> alpha = {})
+    {
+        // alpha defaults to 1
+        if (!alpha)
+            return adopt_ref(*new (nothrow) CSSOKLab(move(l), move(a), move(b), NumberStyleValue::create(1)));
+
+        return adopt_ref(*new (nothrow) CSSOKLab(move(l), move(a), move(b), alpha.release_nonnull()));
+    }
+    virtual ~CSSOKLab() override = default;
+
+    CSSStyleValue const& l() const { return *m_properties.l; }
+    CSSStyleValue const& a() const { return *m_properties.a; }
+    CSSStyleValue const& b() const { return *m_properties.b; }
+    CSSStyleValue const& alpha() const { return *m_properties.alpha; }
+
+    virtual Color to_color(Optional<Layout::NodeWithStyle const&>) const override;
+
+    String to_string() const override;
+
+    virtual bool equals(CSSStyleValue const& other) const override;
+
+private:
+    CSSOKLab(ValueComparingNonnullRefPtr<CSSStyleValue> l, ValueComparingNonnullRefPtr<CSSStyleValue> a, ValueComparingNonnullRefPtr<CSSStyleValue> b, ValueComparingNonnullRefPtr<CSSStyleValue> alpha)
+        : CSSColorValue(ColorType::OKLab)
+        , m_properties { .l = move(l), .a = move(a), .b = move(b), .alpha = move(alpha) }
+    {
+    }
+
+    struct Properties {
+        ValueComparingNonnullRefPtr<CSSStyleValue> l;
+        ValueComparingNonnullRefPtr<CSSStyleValue> a;
+        ValueComparingNonnullRefPtr<CSSStyleValue> b;
+        ValueComparingNonnullRefPtr<CSSStyleValue> alpha;
+        bool operator==(Properties const&) const = default;
+    } m_properties;
+};
+
+}

+ 77 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CSSRGB.cpp

@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CSSRGB.h"
+#include <AK/TypeCasts.h>
+#include <LibWeb/CSS/Serialize.h>
+#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
+#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
+#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
+
+namespace Web::CSS {
+
+Color CSSRGB::to_color(Optional<Layout::NodeWithStyle const&>) const
+{
+    auto resolve_rgb_to_u8 = [](CSSStyleValue const& style_value) -> Optional<u8> {
+        // <number> | <percentage> | none
+        auto normalized = [](double number) {
+            return llround(clamp(number, 0.0, 255.0));
+        };
+
+        if (style_value.is_number())
+            return normalized(style_value.as_number().number());
+
+        if (style_value.is_percentage())
+            return normalized(style_value.as_percentage().value() * 2.55);
+
+        if (style_value.is_calculated()) {
+            auto const& calculated = style_value.as_calculated();
+            if (calculated.resolves_to_number())
+                return normalized(calculated.resolve_number().value());
+            if (calculated.resolves_to_percentage())
+                return normalized(calculated.resolve_percentage().value().value() * 2.55);
+        }
+
+        if (style_value.is_keyword() && style_value.to_keyword() == Keyword::None)
+            return 0;
+
+        return {};
+    };
+
+    auto resolve_alpha_to_u8 = [](CSSStyleValue const& style_value) -> Optional<u8> {
+        auto alpha_0_1 = resolve_alpha(style_value);
+        if (alpha_0_1.has_value())
+            return llround(clamp(alpha_0_1.value() * 255.0, 0.0, 255.0));
+        return {};
+    };
+
+    u8 const r_val = resolve_rgb_to_u8(m_properties.r).value_or(0);
+    u8 const g_val = resolve_rgb_to_u8(m_properties.g).value_or(0);
+    u8 const b_val = resolve_rgb_to_u8(m_properties.b).value_or(0);
+    u8 const alpha_val = resolve_alpha_to_u8(m_properties.alpha).value_or(255);
+
+    return Color(r_val, g_val, b_val, alpha_val);
+}
+
+bool CSSRGB::equals(CSSStyleValue const& other) const
+{
+    if (type() != other.type())
+        return false;
+    auto const& other_color = other.as_color();
+    if (color_type() != other_color.color_type())
+        return false;
+    auto const& other_rgb = verify_cast<CSSRGB>(other_color);
+    return m_properties == other_rgb.m_properties;
+}
+
+// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
+String CSSRGB::to_string() const
+{
+    // FIXME: Do this properly, taking unresolved calculated values into account.
+    return serialize_a_srgb_value(to_color({}));
+}
+
+}

+ 54 - 0
Userland/Libraries/LibWeb/CSS/StyleValues/CSSRGB.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <LibWeb/CSS/StyleValues/CSSColorValue.h>
+#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
+
+namespace Web::CSS {
+
+// https://drafts.css-houdini.org/css-typed-om-1/#cssrgb
+class CSSRGB final : public CSSColorValue {
+public:
+    static ValueComparingNonnullRefPtr<CSSRGB> create(ValueComparingNonnullRefPtr<CSSStyleValue> r, ValueComparingNonnullRefPtr<CSSStyleValue> g, ValueComparingNonnullRefPtr<CSSStyleValue> b, ValueComparingRefPtr<CSSStyleValue> alpha = {})
+    {
+        // alpha defaults to 1
+        if (!alpha)
+            return adopt_ref(*new (nothrow) CSSRGB(move(r), move(g), move(b), NumberStyleValue::create(1)));
+
+        return adopt_ref(*new (nothrow) CSSRGB(move(r), move(g), move(b), alpha.release_nonnull()));
+    }
+    virtual ~CSSRGB() override = default;
+
+    CSSStyleValue const& r() const { return *m_properties.r; }
+    CSSStyleValue const& g() const { return *m_properties.g; }
+    CSSStyleValue const& b() const { return *m_properties.b; }
+    CSSStyleValue const& alpha() const { return *m_properties.alpha; }
+
+    virtual Color to_color(Optional<Layout::NodeWithStyle const&>) const override;
+
+    String to_string() const override;
+
+    virtual bool equals(CSSStyleValue const& other) const override;
+
+private:
+    CSSRGB(ValueComparingNonnullRefPtr<CSSStyleValue> r, ValueComparingNonnullRefPtr<CSSStyleValue> g, ValueComparingNonnullRefPtr<CSSStyleValue> b, ValueComparingNonnullRefPtr<CSSStyleValue> alpha)
+        : CSSColorValue(ColorType::RGB)
+        , m_properties { .r = move(r), .g = move(g), .b = move(b), .alpha = move(alpha) }
+    {
+    }
+
+    struct Properties {
+        ValueComparingNonnullRefPtr<CSSStyleValue> r;
+        ValueComparingNonnullRefPtr<CSSStyleValue> g;
+        ValueComparingNonnullRefPtr<CSSStyleValue> b;
+        ValueComparingNonnullRefPtr<CSSStyleValue> alpha;
+        bool operator==(Properties const&) const = default;
+    } m_properties;
+};
+
+}

+ 5 - 0
Userland/Libraries/LibWeb/Forward.h

@@ -106,11 +106,16 @@ class CSSColorValue;
 class CSSConditionRule;
 class CSSFontFaceRule;
 class CSSGroupingRule;
+class CSSHSL;
+class CSSHWB;
 class CSSImportRule;
 class CSSKeyframeRule;
 class CSSKeyframesRule;
 class CSSKeywordValue;
 class CSSMediaRule;
+class CSSOKLab;
+class CSSOKLCH;
+class CSSRGB;
 class CSSRule;
 class CSSRuleList;
 class CSSStyleDeclaration;

+ 1 - 1
Userland/Libraries/LibWeb/HTML/HTMLMetaElement.cpp

@@ -76,7 +76,7 @@ void HTMLMetaElement::inserted()
         auto css_value = parse_css_value(context, value, CSS::PropertyID::Color);
         if (css_value.is_null() || !css_value->is_color())
             return;
-        auto color = css_value->as_color().color();
+        auto color = css_value->to_color({}); // TODO: Pass a layout node?
 
         // 4. If color is not failure, then return color.
         document().page().client().page_did_change_theme_color(color);