Support parsing hwb() functions (#48913)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48913

This brings us to parity with normalize-color, and is mostly similar to hsl, with the notable exception there is not a separate alpha variant, and only modern function syntax is supported. Again, I took the math from normalize-color, and sanity tested it against reference function provided by Color Spec.

I'm going to let that cap off color function support for now, and leave the rest as TODO.

Changelog: [Internal]

Reviewed By: lenaic

Differential Revision: D68594471

fbshipit-source-id: 95702d576e068655d34e52a714d38e4fd718bbc9
This commit is contained in:
Nick Gerleman
2025-01-27 12:07:21 -08:00
committed by Facebook GitHub Bot
parent 676359efd9
commit 1593142648
2 changed files with 161 additions and 13 deletions
@@ -104,6 +104,31 @@ hslToRgb(float h, float s, float l) {
};
}
inline std::tuple<uint8_t, uint8_t, uint8_t>
hwbToRgb(float h, float w, float b) {
w = std::clamp(w / 100.0f, 0.0f, 1.0f);
b = std::clamp(b / 100.0f, 0.0f, 1.0f);
if (w + b >= 1.0f) {
auto gray = w / (w + b);
return {
static_cast<uint8_t>(std::round(gray * 255.0f)),
static_cast<uint8_t>(std::round(gray * 255.0f)),
static_cast<uint8_t>(std::round(gray * 255.0f)),
};
}
auto red = hueToRgb(0.0f, 1.0f, h + 1.0f / 3.0f) * (1.0f - w - b) + w;
auto green = hueToRgb(0.0f, 1.0f, h) * (1.0f - w - b) + w;
auto blue = hueToRgb(0.0f, 1.0f, h - 1.0f / 3.0f) * (1.0f - w - b) + w;
return {
static_cast<uint8_t>(std::round(red * 255.0f)),
static_cast<uint8_t>(std::round(green * 255.0f)),
static_cast<uint8_t>(std::round(blue * 255.0f)),
};
}
template <typename... ComponentT>
requires(
(std::is_same_v<CSSNumber, ComponentT> ||
@@ -269,6 +294,7 @@ inline std::optional<CSSColor> parseLegacyHslFunction(CSSSyntaxParser& parser) {
if (!l.has_value()) {
return {};
}
auto a = normalizeComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(parser, CSSDelimiter::Comma),
1.0f);
@@ -310,6 +336,7 @@ inline std::optional<CSSColor> parseModernHslFunction(CSSSyntaxParser& parser) {
if (!l.has_value()) {
return {};
}
auto a = normalizeComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::SolidusOrWhitespace),
@@ -337,6 +364,50 @@ inline std::optional<CSSColor> parseHslFunction(CSSSyntaxParser& parser) {
return parseModernHslFunction<CSSColor>(parser);
}
}
/**
* Parses an hwb() function and returns a CSSColor if it is valid.
* https://www.w3.org/TR/css-color-4/#funcdef-hwb
*/
template <typename CSSColor>
inline std::optional<CSSColor> parseHwbFunction(CSSSyntaxParser& parser) {
auto h =
normalizeHueComponent(parseNextCSSValue<CSSNumber, CSSAngle>(parser));
if (!h.has_value()) {
return {};
}
auto w = normalizeComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::Whitespace),
100.0f);
if (!w.has_value()) {
return {};
}
auto b = normalizeComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::Whitespace),
100.0f);
if (!b.has_value()) {
return {};
}
auto a = normalizeComponent(
parseNextCSSValue<CSSNumber, CSSPercentage>(
parser, CSSDelimiter::SolidusOrWhitespace),
1.0f);
auto [red, green, blue] = hwbToRgb(*h, *w, *b);
return CSSColor{
.r = red,
.g = green,
.b = blue,
.a = clampAlpha(a),
};
}
} // namespace detail
/**
@@ -360,20 +431,11 @@ constexpr std::optional<CSSColor> parseCSSColorFunction(
return detail::parseHslFunction<CSSColor>(parser);
break;
case fnv1a("hwb"):
case fnv1a("hwba"):
// TODO
break;
case fnv1a("lab"):
break;
case fnv1a("lch"):
break;
case fnv1a("oklab"):
break;
case fnv1a("oklch"):
break;
case fnv1a("color"):
// TODO T213000437: Support `color()` functions and wide-gamut colors.
return detail::parseHwbFunction<CSSColor>(parser);
break;
// TODO T213000437: support lab(), lch(), oklab(), oklch(), color(),
// color-mix()
default:
return {};
}
@@ -394,6 +394,92 @@ TEST(CSSColor, hsl_hsla_values) {
EXPECT_TRUE(std::holds_alternative<std::monostate>(valueEndingWithComma));
}
TEST(CSSColor, hwb_values) {
auto simpleValue = parseCSSProperty<CSSColor>("hwb(208 14% 42%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(simpleValue));
EXPECT_EQ(std::get<CSSColor>(simpleValue).r, 36);
EXPECT_EQ(std::get<CSSColor>(simpleValue).g, 96);
EXPECT_EQ(std::get<CSSColor>(simpleValue).b, 148);
EXPECT_EQ(std::get<CSSColor>(simpleValue).a, 255);
auto grayValue = parseCSSProperty<CSSColor>("hwb(208 100 100)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(grayValue));
EXPECT_EQ(std::get<CSSColor>(grayValue).r, 128);
EXPECT_EQ(std::get<CSSColor>(grayValue).g, 128);
EXPECT_EQ(std::get<CSSColor>(grayValue).b, 128);
EXPECT_EQ(std::get<CSSColor>(grayValue).a, 255);
auto angleValue = parseCSSProperty<CSSColor>("hwb(36.3028E-1rad 14% 42%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(angleValue));
EXPECT_EQ(std::get<CSSColor>(angleValue).r, 36);
EXPECT_EQ(std::get<CSSColor>(angleValue).g, 96);
EXPECT_EQ(std::get<CSSColor>(angleValue).b, 148);
EXPECT_EQ(std::get<CSSColor>(angleValue).a, 255);
auto legacySyntaxValue = parseCSSProperty<CSSColor>("hwb(208, 14%, 42%)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(legacySyntaxValue));
auto alphaValue = parseCSSProperty<CSSColor>("hwb(208 14% 42% 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(alphaValue));
EXPECT_EQ(std::get<CSSColor>(alphaValue).r, 36);
EXPECT_EQ(std::get<CSSColor>(alphaValue).g, 96);
EXPECT_EQ(std::get<CSSColor>(alphaValue).b, 148);
EXPECT_EQ(std::get<CSSColor>(alphaValue).a, 128);
auto alphaPercentageValue =
parseCSSProperty<CSSColor>("hwb(208 14% 42% 50%)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(alphaPercentageValue));
EXPECT_EQ(std::get<CSSColor>(alphaPercentageValue).r, 36);
EXPECT_EQ(std::get<CSSColor>(alphaPercentageValue).g, 96);
EXPECT_EQ(std::get<CSSColor>(alphaPercentageValue).b, 148);
EXPECT_EQ(std::get<CSSColor>(alphaPercentageValue).a, 128);
auto alphaSolidusValue = parseCSSProperty<CSSColor>("hwb(208 14% 42% / 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(alphaSolidusValue));
EXPECT_EQ(std::get<CSSColor>(alphaSolidusValue).r, 36);
EXPECT_EQ(std::get<CSSColor>(alphaSolidusValue).g, 96);
EXPECT_EQ(std::get<CSSColor>(alphaSolidusValue).b, 148);
EXPECT_EQ(std::get<CSSColor>(alphaSolidusValue).a, 128);
auto mixedWhitespaceValue =
parseCSSProperty<CSSColor>(" hwb( 208 14% 42% /0.5 ) ");
EXPECT_TRUE(std::holds_alternative<CSSColor>(mixedWhitespaceValue));
EXPECT_EQ(std::get<CSSColor>(mixedWhitespaceValue).r, 36);
EXPECT_EQ(std::get<CSSColor>(mixedWhitespaceValue).g, 96);
EXPECT_EQ(std::get<CSSColor>(mixedWhitespaceValue).b, 148);
EXPECT_EQ(std::get<CSSColor>(mixedWhitespaceValue).a, 128);
auto extraDegreesValue = parseCSSProperty<CSSColor>("hwb(568 14% 42% / 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(extraDegreesValue));
EXPECT_EQ(std::get<CSSColor>(extraDegreesValue).r, 36);
EXPECT_EQ(std::get<CSSColor>(extraDegreesValue).g, 96);
EXPECT_EQ(std::get<CSSColor>(extraDegreesValue).b, 148);
EXPECT_EQ(std::get<CSSColor>(extraDegreesValue).a, 128);
auto negativeDegreesValue =
parseCSSProperty<CSSColor>("hwb(-152 14% 42% / 0.5)");
EXPECT_TRUE(std::holds_alternative<CSSColor>(negativeDegreesValue));
EXPECT_EQ(std::get<CSSColor>(negativeDegreesValue).r, 36);
EXPECT_EQ(std::get<CSSColor>(negativeDegreesValue).g, 96);
EXPECT_EQ(std::get<CSSColor>(negativeDegreesValue).b, 148);
EXPECT_EQ(std::get<CSSColor>(negativeDegreesValue).a, 128);
auto missingComponentsValue = parseCSSProperty<CSSColor>("hwb(208 14%)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(missingComponentsValue));
auto tooManyComponentsValue =
parseCSSProperty<CSSColor>("hwb(208 14% 42% 0.5 0.5)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(tooManyComponentsValue));
auto valueStartingWithComma =
parseCSSProperty<CSSColor>("hwb(,208 14% 42% / 0.5)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(valueStartingWithComma));
auto valueEndingWithComma =
parseCSSProperty<CSSColor>("hwb(208 14% 42% / 0.5,)");
EXPECT_TRUE(std::holds_alternative<std::monostate>(valueEndingWithComma));
}
TEST(CSSColor, constexpr_values) {
[[maybe_unused]] constexpr auto emptyValue = parseCSSProperty<CSSColor>("");