Skip to content
Permalink
Browse files
Hue interpolation "specified" is not correctly implemented for hues <…
… 0 or > 360

https://bugs.webkit.org/show_bug.cgi?id=245552
<rdar://100344615>

Reviewed by Darin Adler.

Defers normalization of the hue component of lch(), oklch(), hsl() and hwb()
colors until necessary, such as due to a color conversion, non-"specified" hue
interpolation method use or serialization (though that one is unclear and is
with the spec editors to decide on via w3c/csswg-drafts#7782).

By deferring this normalization, we now implement the "specified" hue interpolation
method correctly, which explicitly allows for a hue to rotate around the spectrum
multiple times if the difference between angles requires it.

To support this for hsl() and hwb(), which usually are stored as 8-bit sRGB, we use
the same technique as was employed for "none" support, and conditionally use their
extended form if an angle < 0 or > 360 is provided. This means that in the common
cases there will be no change in memory usage.

Additionally, I update the "longer" hue interpolation method to match the updated
spec which changed a "< 0" to a "<= 0".

Tests were added to show that gradients that prior to this change would render the
same due to angle normalization, now render differently.

* LayoutTests/fast/gradients/gradient-using-specified-hue-interpolation-method-hsl-expected-mismatch.html: Added.
* LayoutTests/fast/gradients/gradient-using-specified-hue-interpolation-method-hsl.html: Added.
* LayoutTests/fast/gradients/gradient-using-specified-hue-interpolation-method-hwb-expected-mismatch.html: Added.
* LayoutTests/fast/gradients/gradient-using-specified-hue-interpolation-method-hwb.html: Added.
* LayoutTests/fast/gradients/gradient-using-specified-hue-interpolation-method-lch-expected-mismatch.html: Added.
* LayoutTests/fast/gradients/gradient-using-specified-hue-interpolation-method-lch.html: Added.
* LayoutTests/fast/gradients/gradient-using-specified-hue-interpolation-method-oklch-expected-mismatch.html: Added.
* LayoutTests/fast/gradients/gradient-using-specified-hue-interpolation-method-oklch.html: Added.
* Source/WebCore/css/parser/CSSPropertyParserHelpers.cpp:
(WebCore::CSSPropertyParserHelpers::colorByNormalizingHSLComponents):
(WebCore::CSSPropertyParserHelpers::parseHWBParameters):
(WebCore::CSSPropertyParserHelpers::parseLCHParameters):
* Source/WebCore/platform/graphics/ColorConversion.cpp:
(WebCore::HSLA<float>>::convert):
(WebCore::HWBA<float>>::convert):
* Source/WebCore/platform/graphics/ColorInterpolation.cpp:
(WebCore::fixupHueComponentsPriorToInterpolation):
* Source/WebCore/platform/graphics/ColorModels.h:
* Source/WebCore/platform/graphics/ColorNormalization.h:
(WebCore::makeColorTypeByNormalizingComponents<HWBA<float>>):
(WebCore::makeColorTypeByNormalizingComponents<HSLA<float>>): Deleted.
(WebCore::makeColorTypeByNormalizingComponents<LCHA<float>>): Deleted.
(WebCore::makeColorTypeByNormalizingComponents<OKLCHA<float>>): Deleted.
* Source/WebCore/platform/graphics/ColorSerialization.cpp:
(WebCore::serializationOfLabLikeColorsForCSS):
(WebCore::serializationOfLCHLikeColorsForCSS):
(WebCore::serializationForCSS):
(WebCore::serializationOfLabFamilyForCSS): Deleted.

Canonical link: https://commits.webkit.org/254833@main
  • Loading branch information
weinig authored and Sam Weinig committed Sep 25, 2022
1 parent 28e457f commit cf0675f3297a9890c3a7bb0226896b0904caa958
Show file tree
Hide file tree
Showing 15 changed files with 258 additions and 73 deletions.
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<style>
div {
width: 400px;
height: 100px;
display: inline-block;
}

/* Ensures that when using the "specified" hue interpolation method that values < 0 and greater than 360 are respected and not normalized to the [0, 360] range. */

#gradient {
background-image: linear-gradient(in hsl specified hue to right, hsl(120deg 100% 50%), hsl(300deg 100% 25.1%));
}
</style>
</head>
<body>
<div id="gradient"></div>
</body>
</html>
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<style>
div {
width: 400px;
height: 100px;
display: inline-block;
}

/* Ensures that when using the "specified" hue interpolation method that values < 0 and greater than 360 are respected and not normalized to the [0, 360] range. */

#gradient {
background-image: linear-gradient(in hsl specified hue to right, hsl(-240deg 100% 50%), hsl(660deg 100% 25.1%));
}
</style>
</head>
<body>
<div id="gradient"></div>
</body>
</html>
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<style>
div {
width: 400px;
height: 100px;
display: inline-block;
}

/* Ensures that when using the "specified" hue interpolation method that values < 0 and greater than 360 are respected and not normalized to the [0, 360] range. */

#gradient {
background-image: linear-gradient(in hwb specified hue to right, hwb(120deg 0% 0%), hwb(300deg 0% 50%));
}
</style>
</head>
<body>
<div id="gradient"></div>
</body>
</html>
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<style>
div {
width: 400px;
height: 100px;
display: inline-block;
}

/* Ensures that when using the "specified" hue interpolation method that values < 0 and greater than 360 are respected and not normalized to the [0, 360] range. */

#gradient {
background-image: linear-gradient(in hwb specified hue to right, hwb(-240deg 0% 0%), hwb(660deg 0% 50%));
}
</style>
</head>
<body>
<div id="gradient"></div>
</body>
</html>
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<style>
div {
width: 400px;
height: 100px;
display: inline-block;
}

/* Ensures that when using the "specified" hue interpolation method that values < 0 and greater than 360 are respected and not normalized to the [0, 360] range. */

#gradient {
background-image: linear-gradient(in lch specified hue to right, lch(70.55% 92.66 134.7deg), lch(31.32% 129.1 302.4deg));
}
</style>
</head>
<body>
<div id="gradient"></div>
</body>
</html>
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<style>
div {
width: 400px;
height: 100px;
display: inline-block;
}

/* Ensures that when using the "specified" hue interpolation method that values < 0 and greater than 360 are respected and not normalized to the [0, 360] range. */

#gradient {
background-image: linear-gradient(in lch specified hue to right, lch(70.55% 92.66 -225.3deg), lch(31.32% 129.1 662.4deg));
}
</style>
</head>
<body>
<div id="gradient"></div>
</body>
</html>
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<style>
div {
width: 400px;
height: 100px;
display: inline-block;
}

/* Ensures that when using the "specified" hue interpolation method that values < 0 and greater than 360 are respected and not normalized to the [0, 360] range. */

#gradient {
background-image: linear-gradient(in oklch specified hue to right, oklch(45.2% 0.313 264.1deg), oklch(72.26% 0.242 142.6deg));
}
</style>
</head>
<body>
<div id="gradient"></div>
</body>
</html>
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<style>
div {
width: 400px;
height: 100px;
display: inline-block;
}

/* Ensures that when using the "specified" hue interpolation method that values < 0 and greater than 360 are respected and not normalized to the [0, 360] range. */

#gradient {
background-image: linear-gradient(in oklch specified hue to right, oklch(45.2% 0.313 -95.9deg), oklch(72.26% 0.242 502.6deg));
}
</style>
</head>
<body>
<div id="gradient"></div>
</body>
</html>
@@ -574,6 +574,10 @@ webkit.org/b/234606 fast/gradients/conic-gradient-alpha-unpremultiplied.html [ I
webkit.org/b/234606 fast/gradients/conic-gradient-alpha.html [ ImageOnlyFailure ]
webkit.org/b/234606 fast/gradients/alpha-premultiplied-representable-by-unpremultiplied.html [ ImageOnlyFailure ]
webkit.org/b/234606 fast/gradients/alpha-premultiplied.html [ ImageOnlyFailure ]
webkit.org/b/245622 fast/gradients/gradient-using-specified-hue-interpolation-method-hsl.html [ ImageOnlyFailure ]
webkit.org/b/245622 fast/gradients/gradient-using-specified-hue-interpolation-method-hwb.html [ ImageOnlyFailure ]
webkit.org/b/245622 fast/gradients/gradient-using-specified-hue-interpolation-method-lch.html [ ImageOnlyFailure ]
webkit.org/b/245622 fast/gradients/gradient-using-specified-hue-interpolation-method-oklch.html [ ImageOnlyFailure ]

webkit.org/b/169988 css3/filters/backdrop/backdrop-filter-with-border-radius-and-reflection-add.html [ ImageOnlyFailure ]
webkit.org/b/169988 css3/filters/backdrop/backdrop-filter-with-border-radius-and-reflection.html [ ImageOnlyFailure ]
@@ -1975,8 +1975,8 @@ template<RGBFunctionMode Mode> static Color parseRGBParameters(CSSParserTokenRan
static Color colorByNormalizingHSLComponents(AngleOrNumberOrNoneRaw hue, PercentOrNoneRaw saturation, PercentOrNoneRaw lightness, double alpha, RGBOrHSLSeparatorSyntax syntax)
{
auto normalizedHue = WTF::switchOn(hue,
[] (AngleRaw angle) { return normalizeHue(CSSPrimitiveValue::computeDegrees(angle.type, angle.value)); },
[] (NumberRaw number) { return normalizeHue(number.value); },
[] (AngleRaw angle) { return CSSPrimitiveValue::computeDegrees(angle.type, angle.value); },
[] (NumberRaw number) { return number.value; },
[] (NoneRaw) { return std::numeric_limits<double>::quiet_NaN(); }
);
auto normalizedSaturation = WTF::switchOn(saturation,
@@ -1997,8 +1997,16 @@ static Color colorByNormalizingHSLComponents(AngleOrNumberOrNoneRaw hue, Percent
return HSLA<float> { static_cast<float>(normalizedHue), static_cast<float>(normalizedSaturation), static_cast<float>(normalizedLightness), static_cast<float>(alpha) };
}

// NOTE: The explicit conversion to SRGBA<uint8_t> is intentional for performance (no extra allocation for
// the extended color) and compatability, forcing serialiazation to use the rgb()/rgba() form.
if (normalizedHue < 0.0 || normalizedHue > 360.0) {
// If hue is not in the [0, 360] range, we store the value as a HSLA<float> to allow for correct interpolation
// using the "specified" hue interpolation method.
return HSLA<float> { static_cast<float>(normalizedHue), static_cast<float>(normalizedSaturation), static_cast<float>(normalizedLightness), static_cast<float>(alpha) };
}

// NOTE: The explicit conversion to SRGBA<uint8_t> is an intentional performance optimization that allows storing the
// color with no extra allocation for an extended color object. This is permissible due to the historical requirement
// that HSLA colors serialize using the legacy color syntax (rgb()/rgba()) and historically have used the 8-bit rgba
// internal representation in engines.
return convertColor<SRGBA<uint8_t>>(HSLA<float> { static_cast<float>(normalizedHue), static_cast<float>(normalizedSaturation), static_cast<float>(normalizedLightness), static_cast<float>(alpha) });
}

@@ -2109,8 +2117,8 @@ static Color parseHWBParameters(CSSParserTokenRange& args, ConsumerForHue&& hueC
return { };

auto normalizedHue = WTF::switchOn(*hue,
[] (AngleRaw angle) { return normalizeHue(CSSPrimitiveValue::computeDegrees(angle.type, angle.value)); },
[] (NumberRaw number) { return normalizeHue(number.value); },
[] (AngleRaw angle) { return CSSPrimitiveValue::computeDegrees(angle.type, angle.value); },
[] (NumberRaw number) { return number.value; },
[] (NoneRaw) { return std::numeric_limits<double>::quiet_NaN(); }
);
auto clampedWhiteness = WTF::switchOn(*whiteness,
@@ -2125,14 +2133,22 @@ static Color parseHWBParameters(CSSParserTokenRange& args, ConsumerForHue&& hueC
if (std::isnan(normalizedHue) || std::isnan(clampedWhiteness) || std::isnan(clampedBlackness) || std::isnan(*alpha)) {
auto [normalizedWhitness, normalizedBlackness] = normalizeClampedWhitenessBlacknessAllowingNone(clampedWhiteness, clampedBlackness);

// If any component uses "none", we store the value as a HSLA<float> to allow for storage of the special value as NaN.
// If any component uses "none", we store the value as a HWBA<float> to allow for storage of the special value as NaN.
return HWBA<float> { static_cast<float>(normalizedHue), static_cast<float>(normalizedWhitness), static_cast<float>(normalizedBlackness), static_cast<float>(*alpha) };
}

auto [normalizedWhitness, normalizedBlackness] = normalizeClampedWhitenessBlacknessDisallowingNone(clampedWhiteness, clampedBlackness);

// NOTE: The explicit conversion to SRGBA<uint8_t> is intentional for performance (no extra allocation for
// the extended color) and compatability, forcing serialiazation to use the rgb()/rgba() form.
if (normalizedHue < 0.0 || normalizedHue > 360.0) {
// If 'hue' is not in the [0, 360] range, we store the value as a HWBA<float> to allow for correct interpolation
// using the "specified" hue interpolation method.
return HWBA<float> { static_cast<float>(normalizedHue), static_cast<float>(normalizedWhitness), static_cast<float>(normalizedBlackness), static_cast<float>(*alpha) };
}

// NOTE: The explicit conversion to SRGBA<uint8_t> is an intentional performance optimization that allows storing the
// color with no extra allocation for an extended color object. This is permissible due to the historical requirement
// that HWBA colors serialize using the legacy color syntax (rgb()/rgba()) and historically have used the 8-bit rgba
// internal representation in engines.
return convertColor<SRGBA<uint8_t>>(HWBA<float> { static_cast<float>(normalizedHue), static_cast<float>(normalizedWhitness), static_cast<float>(normalizedBlackness), static_cast<float>(*alpha) });
}

@@ -2426,8 +2442,8 @@ static Color parseLCHParameters(CSSParserTokenRange& args, ConsumerForLightness&
[] (NoneRaw) { return std::numeric_limits<double>::quiet_NaN(); }
);
auto normalizedHue = WTF::switchOn(*hue,
[] (AngleRaw angle) { return normalizeHue(CSSPrimitiveValue::computeDegrees(angle.type, angle.value)); },
[] (NumberRaw number) { return normalizeHue(number.value); },
[] (AngleRaw angle) { return CSSPrimitiveValue::computeDegrees(angle.type, angle.value); },
[] (NumberRaw number) { return number.value; },
[] (NoneRaw) { return std::numeric_limits<double>::quiet_NaN(); }
);

@@ -27,6 +27,7 @@
#include "ColorConversion.h"

#include "Color.h"
#include "ColorNormalization.h"
#include "ColorSpace.h"
#include "DestinationColorSpace.h"
#include <wtf/MathExtras.h>
@@ -119,20 +120,20 @@ SRGBA<float> ColorConversion<SRGBA<float>, HSLA<float>>::convert(const HSLA<floa
}

// hueToRGB() wants hue in the 0-6 range.
auto normalizedHue = (hue / 360.0f) * 6.0f;
auto normalizedLightness = lightness / 100.0f;
auto normalizedSaturation = saturation / 100.0f;
auto scaledHue = (normalizeHue(hue) / 360.0f) * 6.0f;
auto scaledLightness = lightness / 100.0f;
auto scaledSaturation = saturation / 100.0f;

auto hueForRed = normalizedHue + 2.0f;
auto hueForGreen = normalizedHue;
auto hueForBlue = normalizedHue - 2.0f;
auto hueForRed = scaledHue + 2.0f;
auto hueForGreen = scaledHue;
auto hueForBlue = scaledHue - 2.0f;
if (hueForRed > 6.0f)
hueForRed -= 6.0f;
else if (hueForBlue < 0.0f)
hueForBlue += 6.0f;

float temp2 = normalizedLightness <= 0.5f ? normalizedLightness * (1.0f + normalizedSaturation) : normalizedLightness + normalizedSaturation - normalizedLightness * normalizedSaturation;
float temp1 = 2.0f * normalizedLightness - temp2;
float temp2 = scaledLightness <= 0.5f ? scaledLightness * (1.0f + scaledSaturation) : scaledLightness + scaledSaturation - scaledLightness * scaledSaturation;
float temp1 = 2.0f * scaledLightness - temp2;

// Hue is in the range 0-6, other args in 0-1.
auto hueToRGB = [](float temp1, float temp2, float hue) {
@@ -177,18 +178,18 @@ SRGBA<float> ColorConversion<SRGBA<float>, HWBA<float>>::convert(const HWBA<floa
}

// hueToRGB() wants hue in the 0-6 range.
auto normalizedHue = (hue / 360.0f) * 6.0f;
auto scaledHue = (normalizeHue(hue) / 360.0f) * 6.0f;

auto hueForRed = normalizedHue + 2.0f;
auto hueForGreen = normalizedHue;
auto hueForBlue = normalizedHue - 2.0f;
auto hueForRed = scaledHue + 2.0f;
auto hueForGreen = scaledHue;
auto hueForBlue = scaledHue - 2.0f;
if (hueForRed > 6.0f)
hueForRed -= 6.0f;
else if (hueForBlue < 0.0f)
hueForBlue += 6.0f;

auto normalizedWhiteness = whiteness / 100.0f;
auto normalizedBlackness = blackness / 100.0f;
auto scaledWhiteness = whiteness / 100.0f;
auto scaledBlackness = blackness / 100.0f;

// This is the hueToRGB function in convertColor<SRGBA<float>>(const HSLA&) with temp1 == 0
// and temp2 == 1 strength reduced through it.
@@ -207,9 +208,9 @@ SRGBA<float> ColorConversion<SRGBA<float>, HWBA<float>>::convert(const HWBA<floa
};

return {
applyWhitenessBlackness(hueToRGB(hueForRed), normalizedWhiteness, normalizedBlackness),
applyWhitenessBlackness(hueToRGB(hueForGreen), normalizedWhiteness, normalizedBlackness),
applyWhitenessBlackness(hueToRGB(hueForBlue), normalizedWhiteness, normalizedBlackness),
applyWhitenessBlackness(hueToRGB(hueForRed), scaledWhiteness, scaledBlackness),
applyWhitenessBlackness(hueToRGB(hueForGreen), scaledWhiteness, scaledBlackness),
applyWhitenessBlackness(hueToRGB(hueForBlue), scaledWhiteness, scaledBlackness),
alpha
};
}
@@ -47,7 +47,7 @@ std::pair<float, float> fixupHueComponentsPriorToInterpolation(HueInterpolationM
auto difference = theta2 - theta1;
if (difference > 0.0 && difference < 180.0)
return { theta1 + 360.0, theta2 };
if (difference > -180.0 && difference < 0)
if (difference > -180.0 && difference <= 0)
return { theta1, theta2 + 360.0 };
return { theta1, theta2 };
};
@@ -71,15 +71,20 @@ std::pair<float, float> fixupHueComponentsPriorToInterpolation(HueInterpolationM
return { theta1, theta2 };
};

// https://www.w3.org/TR/css-color-4/#hue-interpolation
// "Unless the type of hue interpolation is specified, both angles need to
// be constrained to [0, 360) prior to interpolation. One way to do this
// is θ = ((θ % 360) + 360) % 360."

switch (method) {
case HueInterpolationMethod::Shorter:
return normalizeAnglesUsingShorterAlgorithm(component1, component2);
return normalizeAnglesUsingShorterAlgorithm(normalizeHue(component1), normalizeHue(component2));
case HueInterpolationMethod::Longer:
return normalizeAnglesUsingLongerAlgorithm(component1, component2);
return normalizeAnglesUsingLongerAlgorithm(normalizeHue(component1), normalizeHue(component2));
case HueInterpolationMethod::Increasing:
return normalizeAnglesUsingIncreasingAlgorithm(component1, component2);
return normalizeAnglesUsingIncreasingAlgorithm(normalizeHue(component1), normalizeHue(component2));
case HueInterpolationMethod::Decreasing:
return normalizeAnglesUsingDecreasingAlgorithm(component1, component2);
return normalizeAnglesUsingDecreasingAlgorithm(normalizeHue(component1), normalizeHue(component2));
case HueInterpolationMethod::Specified:
return normalizeAnglesUsingSpecifiedAlgorithm(component1, component2);
}

0 comments on commit cf0675f

Please sign in to comment.