diff --git a/layout/style/nsCSSNonSRGBColorSpace.h b/layout/style/nsCSSNonSRGBColorSpace.h index 4ce2e56a1..e565649b7 100644 --- a/layout/style/nsCSSNonSRGBColorSpace.h +++ b/layout/style/nsCSSNonSRGBColorSpace.h @@ -6,6 +6,7 @@ #ifndef nsCSSNonSRGBColorSpace_h___ #define nsCSSNonSRGBColorSpace_h___ +#include #include #include "mozilla/MathAlgorithms.h" @@ -18,7 +19,46 @@ namespace css { static constexpr float kLabLightnessMax = 100.0f; static constexpr float kLchPercentScaleC = 150.0f; static constexpr float kOklabPercentScaleAB = 0.4f; +static constexpr float kOklchPowerlessChromaEpsilon = 0.000004f; static constexpr double kRadiansPerDegree = 0.01745329251994329576923690768489; +static constexpr double kDegreesPerRadian = 57.295779513082320876798154814105; + +struct OklabColor { + float mL; + float mA; + float mB; +}; + +struct OklchColor { + float mL; + float mChroma; + float mHue; +}; + +struct LinearSRGBColor { + float mR; + float mG; + float mB; +}; + +inline float +NormalizeHue(float aHue) +{ + float hue = std::fmod(aHue, 360.0f); + if (hue < 0.0f) { + hue += 360.0f; + } + return hue; +} + +inline float +EncodedSRGBToLinear(float aValue) +{ + if (aValue <= 0.04045f) { + return aValue / 12.92f; + } + return std::pow((aValue + 0.055f) / 1.055f, 2.4f); +} inline float LinearSRGBToEncoded(float aValue) @@ -44,43 +84,159 @@ LinearSRGBToColor(float aLinearR, float aLinearG, float aLinearB, aAlpha); } -inline nscolor -OklabToSRGBColor(float aL, float aA, float aB, float aAlpha) +inline LinearSRGBColor +OklabToLinearSRGB(float aL, float aA, float aB) { - // Per CSS Color, the lightness component for Oklab/Oklch is clamped. - float lightness = mozilla::clamped(aL, 0.0f, 1.0f); - uint8_t alpha = - nsStyleUtil::FloatToColorComponent(mozilla::clamped(aAlpha, 0.0f, 1.0f)); - - // Treat values extremely close to zero as zero to avoid tiny floating-point - // representation differences for percentage inputs. - static constexpr float kLightnessEndpointEpsilon = 0.000002f; - - if (lightness <= kLightnessEndpointEpsilon) { - return NS_RGBA(0, 0, 0, alpha); - } - - float lRoot = lightness + 0.3963377774f * aA + 0.2158037573f * aB; - float mRoot = lightness - 0.1055613458f * aA - 0.0638541728f * aB; - float sRoot = lightness - 0.0894841775f * aA - 1.2914855480f * aB; + float lRoot = aL + 0.3963377774f * aA + 0.2158037573f * aB; + float mRoot = aL - 0.1055613458f * aA - 0.0638541728f * aB; + float sRoot = aL - 0.0894841775f * aA - 1.2914855480f * aB; float l = lRoot * lRoot * lRoot; float m = mRoot * mRoot * mRoot; float s = sRoot * sRoot * sRoot; - float linearR = 4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s; - float linearG = -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s; - float linearB = -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s; + return { + 4.0767416361f * l - 3.3077115393f * m + 0.2309699032f * s, + -1.2684379733f * l + 2.6097573493f * m - 0.3413193760f * s, + -0.0041960761f * l - 0.7034186179f * m + 1.7076146941f * s + }; +} - return LinearSRGBToColor(linearR, linearG, linearB, alpha); +inline OklabColor +LinearSRGBToOklabColor(float aLinearR, float aLinearG, float aLinearB) +{ + float l = std::cbrt(0.4122214695f * aLinearR + + 0.5363325373f * aLinearG + + 0.0514459933f * aLinearB); + float m = std::cbrt(0.2119034958f * aLinearR + + 0.6806995506f * aLinearG + + 0.1073969535f * aLinearB); + float s = std::cbrt(0.0883024592f * aLinearR + + 0.2817188391f * aLinearG + + 0.6299787017f * aLinearB); + + return { + 0.2104542683f * l + 0.7936177747f * m - 0.0040720430f * s, + 1.9779985324f * l - 2.4285922420f * m + 0.4505937096f * s, + 0.0259040425f * l + 0.7827717125f * m - 0.8086757549f * s + }; +} + +inline OklabColor +SRGBToOklabColor(nscolor aColor) +{ + float linearR = EncodedSRGBToLinear(NS_GET_R(aColor) / 255.0f); + float linearG = EncodedSRGBToLinear(NS_GET_G(aColor) / 255.0f); + float linearB = EncodedSRGBToLinear(NS_GET_B(aColor) / 255.0f); + + return LinearSRGBToOklabColor(linearR, linearG, linearB); +} + +inline OklchColor +OklabToOklchColor(const OklabColor& aColor) +{ + float chroma = std::sqrt(aColor.mA * aColor.mA + aColor.mB * aColor.mB); + float hue = std::atan2(aColor.mB, aColor.mA) * kDegreesPerRadian; + if (hue < 0.0f) { + hue += 360.0f; + } + + return { aColor.mL, chroma, hue }; +} + +inline nscolor +OklabToSRGBColor(float aL, float aA, float aB, float aAlpha) +{ + // Per CSS Color, the lightness component for Oklab/Oklch is clamped at + // parsed-value time. + float lightness = mozilla::clamped(aL, 0.0f, 1.0f); + uint8_t alpha = + nsStyleUtil::FloatToColorComponent(mozilla::clamped(aAlpha, 0.0f, 1.0f)); + + OklabColor mapped = { lightness, aA, aB }; + auto isInSRGBGamut = [](const OklabColor& aColor) { + LinearSRGBColor linear = + OklabToLinearSRGB(aColor.mL, aColor.mA, aColor.mB); + return linear.mR >= 0.0f && linear.mR <= 1.0f && + linear.mG >= 0.0f && linear.mG <= 1.0f && + linear.mB >= 0.0f && linear.mB <= 1.0f; + }; + auto clippedOklab = [](const OklabColor& aColor) { + LinearSRGBColor linear = + OklabToLinearSRGB(aColor.mL, aColor.mA, aColor.mB); + return LinearSRGBToOklabColor( + mozilla::clamped(linear.mR, 0.0f, 1.0f), + mozilla::clamped(linear.mG, 0.0f, 1.0f), + mozilla::clamped(linear.mB, 0.0f, 1.0f)); + }; + auto deltaEOK = [](const OklabColor& aColor1, const OklabColor& aColor2) { + float deltaL = aColor1.mL - aColor2.mL; + float deltaA = aColor1.mA - aColor2.mA; + float deltaB = aColor1.mB - aColor2.mB; + return std::sqrt(deltaL * deltaL + deltaA * deltaA + deltaB * deltaB); + }; + + if (lightness <= 0.0f) { + mapped = { 0.0f, 0.0f, 0.0f }; + } else if (lightness >= 1.0f) { + mapped = { 1.0f, 0.0f, 0.0f }; + } else if (!isInSRGBGamut(mapped)) { + // CSS Color 4 binary search gamut mapping with local MINDE, targeting sRGB. + static constexpr float kJND = 0.02f; + static constexpr float kGamutMapEpsilon = 0.0001f; + + OklchColor origin = OklabToOklchColor(mapped); + OklabColor clipped = clippedOklab(mapped); + float delta = deltaEOK(clipped, mapped); + + if (delta < kJND) { + mapped = clipped; + } else { + float min = 0.0f; + float max = origin.mChroma; + bool minInGamut = true; + OklabColor current = mapped; + + while (max - min > kGamutMapEpsilon) { + float chroma = (min + max) / 2.0f; + current.mL = origin.mL; + current.mA = chroma * std::cos(origin.mHue * kRadiansPerDegree); + current.mB = chroma * std::sin(origin.mHue * kRadiansPerDegree); + + if (minInGamut && isInSRGBGamut(current)) { + min = chroma; + continue; + } + + clipped = clippedOklab(current); + delta = deltaEOK(clipped, current); + if (delta < kJND) { + if (kJND - delta < kGamutMapEpsilon) { + mapped = clipped; + break; + } + minInGamut = false; + min = chroma; + } else { + max = chroma; + } + mapped = clipped; + } + } + } + + LinearSRGBColor linear = + OklabToLinearSRGB(mapped.mL, mapped.mA, mapped.mB); + return LinearSRGBToColor(linear.mR, linear.mG, linear.mB, alpha); } inline nscolor OklchToSRGBColor(float aL, float aChroma, float aHue, float aAlpha) { - double hueRadians = aHue * kRadiansPerDegree; - float a = aChroma * std::cos(hueRadians); - float b = aChroma * std::sin(hueRadians); + float chroma = std::max(aChroma, 0.0f); + double hueRadians = NormalizeHue(aHue) * kRadiansPerDegree; + float a = chroma * std::cos(hueRadians); + float b = chroma * std::sin(hueRadians); return OklabToSRGBColor(aL, a, b, aAlpha); } diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index c9c38faed..4f9c32325 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -1487,8 +1487,9 @@ protected: bool ParseHSLColor(float& aHue, float& aSaturation, float& aLightness, float& aOpacity); bool ParseLCHColor(nscolor& aColor); - bool ParseOKLabColor(nscolor& aColor); - bool ParseOKLCHColor(nscolor& aColor); + bool ParseOKLabColor(float& aL, float& aA, float& aB, float& aAlpha); + bool ParseOKLCHColor(float& aL, float& aChroma, float& aHue, + float& aAlpha); bool ParseOKLabComponent(float& aComponent, float aPercentScale, Maybe aSeparator); @@ -7763,7 +7764,7 @@ CSSParserImpl::ParseColor(nsCSSValue& aValue) return CSSParseResult::Error; } - // Check for supported color spaces: srgb or hsl + // Check for supported color spaces. if (!GetToken(true) || mToken.mType != eCSSToken_Ident) { SkipUntil(')'); return CSSParseResult::Error; @@ -7774,6 +7775,10 @@ CSSParserImpl::ParseColor(nsCSSValue& aValue) colorSpace = mozilla::css::ColorMixColorSpace::sRGB; } else if (mToken.mIdent.LowerCaseEqualsLiteral("hsl")) { colorSpace = mozilla::css::ColorMixColorSpace::HSL; + } else if (mToken.mIdent.LowerCaseEqualsLiteral("oklab")) { + colorSpace = mozilla::css::ColorMixColorSpace::Oklab; + } else if (mToken.mIdent.LowerCaseEqualsLiteral("oklch")) { + colorSpace = mozilla::css::ColorMixColorSpace::Oklch; } else { SkipUntil(')'); return CSSParseResult::Error; @@ -7922,16 +7927,19 @@ CSSParserImpl::ParseColor(nsCSSValue& aValue) return CSSParseResult::Error; } else if (mToken.mIdent.LowerCaseEqualsLiteral("oklab")) { - if (ParseOKLabColor(rgba)) { - aValue.SetColorValue(rgba); + float l, a, b, alpha; + if (ParseOKLabColor(l, a, b, alpha)) { + aValue.SetFloatColorValue(l, a, b, alpha, eCSSUnit_OklabColor); return CSSParseResult::Ok; } SkipUntil(')'); return CSSParseResult::Error; } else if (mToken.mIdent.LowerCaseEqualsLiteral("oklch")) { - if (ParseOKLCHColor(rgba)) { - aValue.SetColorValue(rgba); + float l, chroma, hue, alpha; + if (ParseOKLCHColor(l, chroma, hue, alpha)) { + aValue.SetFloatColorValue(l, chroma, hue, alpha, + eCSSUnit_OklchColor); return CSSParseResult::Ok; } SkipUntil(')'); @@ -8183,7 +8191,8 @@ CSSParserImpl::ParseLCHColor(nscolor& aColor) } bool -CSSParserImpl::ParseOKLabColor(nscolor& aColor) +CSSParserImpl::ParseOKLabColor(float& aL, float& aA, float& aB, + float& aAlpha) { const char commaSeparator = ','; float l, a, b, alpha; @@ -8201,12 +8210,16 @@ CSSParserImpl::ParseOKLabColor(nscolor& aColor) return false; } - aColor = OklabToSRGBColor(l, a, b, alpha); + aL = mozilla::clamped(l, 0.0f, 1.0f); + aA = a; + aB = b; + aAlpha = alpha; return true; } bool -CSSParserImpl::ParseOKLCHColor(nscolor& aColor) +CSSParserImpl::ParseOKLCHColor(float& aL, float& aChroma, float& aHue, + float& aAlpha) { const char commaSeparator = ','; float l, chroma, hue, alpha; @@ -8224,7 +8237,10 @@ CSSParserImpl::ParseOKLCHColor(nscolor& aColor) return false; } - aColor = OklchToSRGBColor(l, chroma, hue, alpha); + aL = mozilla::clamped(l, 0.0f, 1.0f); + aChroma = std::max(chroma, 0.0f); + aHue = NormalizeHue(hue); + aAlpha = alpha; return true; } diff --git a/layout/style/nsCSSValue.cpp b/layout/style/nsCSSValue.cpp index 93dc3f009..c79602f2f 100644 --- a/layout/style/nsCSSValue.cpp +++ b/layout/style/nsCSSValue.cpp @@ -19,6 +19,7 @@ #include "gfxFontConstants.h" #include "imgIRequest.h" #include "imgRequestProxy.h" +#include "nsCSSNonSRGBColorSpace.h" #include "nsIDocument.h" #include "nsIPrincipal.h" #include "nsCSSProps.h" @@ -1973,6 +1974,12 @@ nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult, case mozilla::css::ColorMixColorSpace::HSL: aResult.AppendLiteral("hsl"); break; + case mozilla::css::ColorMixColorSpace::Oklab: + aResult.AppendLiteral("oklab"); + break; + case mozilla::css::ColorMixColorSpace::Oklch: + aResult.AppendLiteral("oklch"); + break; } // append color1 and color2 @@ -2280,6 +2287,8 @@ nsCSSValue::AppendToString(nsCSSPropertyID aProperty, nsAString& aResult, case eCSSUnit_PercentageRGBAColor: break; case eCSSUnit_HSLColor: break; case eCSSUnit_HSLAColor: break; + case eCSSUnit_OklabColor: break; + case eCSSUnit_OklchColor: break; case eCSSUnit_ComplexColor: break; case eCSSUnit_ColorMix: break; case eCSSUnit_Percent: aResult.Append(char16_t('%')); break; @@ -2488,6 +2497,8 @@ nsCSSValue::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const case eCSSUnit_PercentageRGBAColor: case eCSSUnit_HSLColor: case eCSSUnit_HSLAColor: + case eCSSUnit_OklabColor: + case eCSSUnit_OklchColor: n += mValue.mFloatColor->SizeOfIncludingThis(aMallocSizeOf); break; @@ -3479,6 +3490,16 @@ nsCSSValueFloatColor::GetColorValue(nsCSSUnit aUnit) const NSToIntRound(mozilla::clamped(mAlpha, 0.0f, 1.0f) * 255.0f)); } + if (aUnit == eCSSUnit_OklabColor) { + return css::OklabToSRGBColor(mComponent1, mComponent2, mComponent3, + mAlpha); + } + + if (aUnit == eCSSUnit_OklchColor) { + return css::OklchToSRGBColor(mComponent1, mComponent2, mComponent3, + mAlpha); + } + // HSL color MOZ_ASSERT(aUnit == eCSSUnit_HSLColor || aUnit == eCSSUnit_HSLAColor); @@ -3507,8 +3528,14 @@ nsCSSValueFloatColor::AppendToString(nsCSSUnit aUnit, nsAString& aResult) const bool showAlpha = (mAlpha != 1.0f); bool isHSL = (aUnit == eCSSUnit_HSLColor || aUnit == eCSSUnit_HSLAColor); + bool isOklab = aUnit == eCSSUnit_OklabColor; + bool isOklch = aUnit == eCSSUnit_OklchColor; - if (isHSL) { + if (isOklab) { + aResult.AppendLiteral("oklab"); + } else if (isOklch) { + aResult.AppendLiteral("oklch"); + } else if (isHSL) { aResult.AppendLiteral("hsl"); } else { aResult.AppendLiteral("rgb"); @@ -3518,22 +3545,38 @@ nsCSSValueFloatColor::AppendToString(nsCSSUnit aUnit, nsAString& aResult) const } else { aResult.Append('('); } - if (isHSL) { + if (isOklab || isOklch) { + aResult.AppendFloat(mComponent1); + aResult.Append(' '); + aResult.AppendFloat(mComponent2); + aResult.Append(' '); + aResult.AppendFloat(mComponent3); + } else if (isHSL) { aResult.AppendFloat(mComponent1 * 360.0f); aResult.AppendLiteral(", "); } else { aResult.AppendFloat(mComponent1 * 100.0f); aResult.AppendLiteral("%, "); } - aResult.AppendFloat(mComponent2 * 100.0f); - aResult.AppendLiteral("%, "); - aResult.AppendFloat(mComponent3 * 100.0f); - if (showAlpha) { + if (!isOklab && !isOklch) { + aResult.AppendFloat(mComponent2 * 100.0f); aResult.AppendLiteral("%, "); + aResult.AppendFloat(mComponent3 * 100.0f); + } + if (showAlpha) { + if (isOklab || isOklch) { + aResult.AppendLiteral(" / "); + } else { + aResult.AppendLiteral("%, "); + } aResult.AppendFloat(mAlpha); aResult.Append(')'); } else { - aResult.AppendLiteral("%)"); + if (isOklab || isOklch) { + aResult.Append(')'); + } else { + aResult.AppendLiteral("%)"); + } } } diff --git a/layout/style/nsCSSValue.h b/layout/style/nsCSSValue.h index c68664a18..38d09b468 100644 --- a/layout/style/nsCSSValue.h +++ b/layout/style/nsCSSValue.h @@ -510,8 +510,10 @@ enum nsCSSUnit { // allowed. eCSSUnit_HSLColor = 89, // (nsCSSValueFloatColor*) eCSSUnit_HSLAColor = 90, // (nsCSSValueFloatColor*) - eCSSUnit_ComplexColor = 91, // (ComplexColorValue*) - eCSSUnit_ColorMix = 92, // (ColorMixValue*) + eCSSUnit_OklabColor = 91, // (nsCSSValueFloatColor*) + eCSSUnit_OklchColor = 92, // (nsCSSValueFloatColor*) + eCSSUnit_ComplexColor = 93, // (ComplexColorValue*) + eCSSUnit_ColorMix = 94, // (ColorMixValue*) eCSSUnit_Percent = 100, // (float) 1.0 == 100%) value is percentage of something eCSSUnit_Number = 101, // (float) value is numeric (usually multiplier, different behavior than percent) @@ -723,6 +725,8 @@ public: // eCSSUnit_PercentageRGBAColor -- rgba(%,%,%,float) // eCSSUnit_HSLColor -- hsl(float,%,%) // eCSSUnit_HSLAColor -- hsla(float,%,%,float) + // eCSSUnit_OklabColor -- oklab(float,float,float,float) + // eCSSUnit_OklchColor -- oklch(float,float,float,float) // // - IsNumericColorUnit returns true for any of the above units. // @@ -735,7 +739,7 @@ public: { return eCSSUnit_RGBColor <= aUnit && aUnit <= eCSSUnit_ShortHexColorAlpha; } static bool IsFloatColorUnit(nsCSSUnit aUnit) { return eCSSUnit_PercentageRGBColor <= aUnit && - aUnit <= eCSSUnit_HSLAColor; } + aUnit <= eCSSUnit_OklchColor; } static bool IsNumericColorUnit(nsCSSUnit aUnit) { return IsIntegerColorUnit(aUnit) || IsFloatColorUnit(aUnit); } @@ -1909,6 +1913,9 @@ private: // [0, 1] for hue represents // [0deg, 360deg]. // + // OklabColor stores L in mComponent1, a in mComponent2, b in mComponent3. + // OklchColor stores L in mComponent1, C in mComponent2, H in mComponent3. + // // [-float::max(), float::max()] for PercentageRGBColor, PercentageRGBAColor. // 1.0 means 100%. float mComponent1; @@ -1975,7 +1982,9 @@ namespace css { enum class ColorMixColorSpace { sRGB, - HSL + HSL, + Oklab, + Oklch }; struct ColorMixValue final diff --git a/layout/style/nsRuleNode.cpp b/layout/style/nsRuleNode.cpp index 290057436..f1fc2f23f 100644 --- a/layout/style/nsRuleNode.cpp +++ b/layout/style/nsRuleNode.cpp @@ -45,6 +45,7 @@ #include "nsIStyleRule.h" #include "nsBidiUtils.h" #include "nsStyleStructInlines.h" +#include "nsCSSNonSRGBColorSpace.h" #include "nsCSSProps.h" #include "nsTArray.h" #include "nsContentUtils.h" @@ -1143,6 +1144,137 @@ SetPairCoords(const nsCSSValue& aValue, return cX; } +static void +GetColorMixPremultipliedWeights(float aAlpha1, float aAlpha2, + float aWeight1, float aWeight2, + float& aPremultipliedWeight1, + float& aPremultipliedWeight2, + float& aAlpha) +{ + float alphaWeight1 = aWeight1 * aAlpha1; + float alphaWeight2 = aWeight2 * aAlpha2; + + aAlpha = alphaWeight1 + alphaWeight2; + if (aAlpha <= 0.0f) { + aPremultipliedWeight1 = 0.0f; + aPremultipliedWeight2 = 0.0f; + return; + } + + aPremultipliedWeight1 = alphaWeight1 / aAlpha; + aPremultipliedWeight2 = alphaWeight2 / aAlpha; +} + +static css::OklabColor +OklchToOklabColor(const css::OklchColor& aColor) +{ + float hueRadians = aColor.mHue * css::kRadiansPerDegree; + return { + aColor.mL, + aColor.mChroma * std::cos(hueRadians), + aColor.mChroma * std::sin(hueRadians) + }; +} + +static void +GetColorMixColorComponents(const nsCSSValue& aValue, nscolor aResolvedColor, + css::OklabColor& aOklab, + css::OklchColor& aOklch, + float& aAlpha) +{ + nsCSSUnit unit = aValue.GetUnit(); + if (unit == eCSSUnit_OklabColor) { + const nsCSSValueFloatColor* color = aValue.GetFloatColorValue(); + aOklab = { color->Comp1(), color->Comp2(), color->Comp3() }; + aOklch = css::OklabToOklchColor(aOklab); + aAlpha = mozilla::clamped(color->Alpha(), 0.0f, 1.0f); + return; + } + + if (unit == eCSSUnit_OklchColor) { + const nsCSSValueFloatColor* color = aValue.GetFloatColorValue(); + aOklch = { color->Comp1(), color->Comp2(), color->Comp3() }; + aOklab = OklchToOklabColor(aOklch); + aAlpha = mozilla::clamped(color->Alpha(), 0.0f, 1.0f); + return; + } + + aOklab = css::SRGBToOklabColor(aResolvedColor); + aOklch = css::OklabToOklchColor(aOklab); + aAlpha = NS_GET_A(aResolvedColor) / 255.0f; +} + +static float +InterpolateColorMixHue(float aHue1, float aHue2, float aWeight1, + float aWeight2) +{ + float hueDiff = aHue2 - aHue1; + if (hueDiff > 180.0f) { + aHue1 += 360.0f; + } else if (hueDiff < -180.0f) { + aHue2 += 360.0f; + } + + float hue = aHue1 * aWeight1 + aHue2 * aWeight2; + hue = std::fmod(hue, 360.0f); + if (hue < 0.0f) { + hue += 360.0f; + } + return hue; +} + +static nscolor +MixColorsInOklab(const css::OklabColor& aColor1, float aAlpha1, + const css::OklabColor& aColor2, float aAlpha2, + float aWeight1, float aWeight2, float aAlphaMultiplier) +{ + float premultipliedWeight1, premultipliedWeight2, alpha; + GetColorMixPremultipliedWeights(aAlpha1, aAlpha2, aWeight1, aWeight2, + premultipliedWeight1, premultipliedWeight2, + alpha); + alpha *= aAlphaMultiplier; + if (alpha <= 0.0f) { + return NS_RGBA(0, 0, 0, 0); + } + + return css::OklabToSRGBColor( + aColor1.mL * premultipliedWeight1 + aColor2.mL * premultipliedWeight2, + aColor1.mA * premultipliedWeight1 + aColor2.mA * premultipliedWeight2, + aColor1.mB * premultipliedWeight1 + aColor2.mB * premultipliedWeight2, + alpha); +} + +static nscolor +MixColorsInOklch(css::OklchColor aColor1, float aAlpha1, + css::OklchColor aColor2, float aAlpha2, + float aWeight1, float aWeight2, float aAlphaMultiplier) +{ + if (aColor1.mChroma <= css::kOklchPowerlessChromaEpsilon) { + aColor1.mHue = aColor2.mHue; + } + if (aColor2.mChroma <= css::kOklchPowerlessChromaEpsilon) { + aColor2.mHue = aColor1.mHue; + } + + float premultipliedWeight1, premultipliedWeight2, alpha; + GetColorMixPremultipliedWeights(aAlpha1, aAlpha2, aWeight1, aWeight2, + premultipliedWeight1, premultipliedWeight2, + alpha); + alpha *= aAlphaMultiplier; + if (alpha <= 0.0f) { + return NS_RGBA(0, 0, 0, 0); + } + + float hue = InterpolateColorMixHue(aColor1.mHue, aColor2.mHue, + aWeight1, aWeight2); + return css::OklchToSRGBColor( + aColor1.mL * premultipliedWeight1 + aColor2.mL * premultipliedWeight2, + aColor1.mChroma * premultipliedWeight1 + + aColor2.mChroma * premultipliedWeight2, + hue, + alpha); +} + static bool SetColor(const nsCSSValue& aValue, const nscolor aParentColor, @@ -1232,22 +1364,29 @@ SetColor(const nsCSSValue& aValue, const mozilla::css::ColorMixValue* colorMix = aValue.GetColorMixValue(); if (colorMix) { nscolor color1, color2; + bool resolvedColor1, resolvedColor2; // XXX: This is a hack to avoid recursive calls to SetColor when either color resolves // to NS_COLOR_CURRENTCOLOR, as it would result in re-evaluation of the color. // Instead of recursing, we reach up to set either color to the parent color, instead. if (colorMix->mColor1.GetUnit() == eCSSUnit_EnumColor && colorMix->mColor1.GetIntValue() == NS_COLOR_CURRENTCOLOR) { color1 = aParentColor; + resolvedColor1 = true; } else { - SetColor(colorMix->mColor1, aParentColor, aPresContext, aContext, color1, aConditions); + resolvedColor1 = + SetColor(colorMix->mColor1, aParentColor, aPresContext, aContext, + color1, aConditions); } if (colorMix->mColor2.GetUnit() == eCSSUnit_EnumColor && colorMix->mColor2.GetIntValue() == NS_COLOR_CURRENTCOLOR) { color2 = aParentColor; + resolvedColor2 = true; } else { - SetColor(colorMix->mColor2, aParentColor, aPresContext, aContext, color2, aConditions); + resolvedColor2 = + SetColor(colorMix->mColor2, aParentColor, aPresContext, aContext, + color2, aConditions); } - if (color1 && color2) { + if (resolvedColor1 && resolvedColor2) { // interpolate each RGBA component with proper percentage handling float w1 = colorMix->mWeight1; float w2 = colorMix->mWeight2; @@ -1264,9 +1403,18 @@ SetColor(const nsCSSValue& aValue, w1 = w2 = 0.5f; sum = 1.0f; } + float alphaMultiplier = std::min(sum, 1.0f); float norm1 = w1 / sum; float norm2 = w2 / sum; + + css::OklabColor oklab1, oklab2; + css::OklchColor oklch1, oklch2; + float oklabAlpha1, oklabAlpha2; + GetColorMixColorComponents(colorMix->mColor1, color1, oklab1, + oklch1, oklabAlpha1); + GetColorMixColorComponents(colorMix->mColor2, color2, oklab2, + oklch2, oklabAlpha2); if (colorMix->mColorSpace == mozilla::css::ColorMixColorSpace::HSL) { // HSL color space mixing @@ -1339,10 +1487,23 @@ SetColor(const nsCSSValue& aValue, // Convert back to RGB nscolor hslResult = NS_HSL2RGB(h, s, l); - uint8_t aInt = (uint8_t)mozilla::clamped(a * 255.0f + 0.5f, 0.0f, 255.0f); + uint8_t aInt = (uint8_t)mozilla::clamped( + a * alphaMultiplier * 255.0f + 0.5f, 0.0f, 255.0f); aResult = NS_RGBA(NS_GET_R(hslResult), NS_GET_G(hslResult), NS_GET_B(hslResult), aInt); result = true; + } else if (colorMix->mColorSpace == + mozilla::css::ColorMixColorSpace::Oklab) { + aResult = MixColorsInOklab(oklab1, oklabAlpha1, + oklab2, oklabAlpha2, + norm1, norm2, alphaMultiplier); + result = true; + } else if (colorMix->mColorSpace == + mozilla::css::ColorMixColorSpace::Oklch) { + aResult = MixColorsInOklch(oklch1, oklabAlpha1, + oklch2, oklabAlpha2, + norm1, norm2, alphaMultiplier); + result = true; } else { // sRGB color space mixing with proper alpha premultiplication float r1 = NS_GET_R(color1); @@ -1392,7 +1553,8 @@ SetColor(const nsCSSValue& aValue, uint8_t rInt = (uint8_t)mozilla::clamped(r + 0.5f, 0.0f, 255.0f); uint8_t gInt = (uint8_t)mozilla::clamped(g + 0.5f, 0.0f, 255.0f); uint8_t bInt = (uint8_t)mozilla::clamped(b + 0.5f, 0.0f, 255.0f); - uint8_t aInt = (uint8_t)mozilla::clamped(a * 255.0f + 0.5f, 0.0f, 255.0f); + uint8_t aInt = (uint8_t)mozilla::clamped( + a * alphaMultiplier * 255.0f + 0.5f, 0.0f, 255.0f); aResult = NS_RGBA(rInt, gInt, bInt, aInt); result = true; diff --git a/layout/style/test/property_database.js b/layout/style/test/property_database.js index ef2e39d79..d895cabda 100644 --- a/layout/style/test/property_database.js +++ b/layout/style/test/property_database.js @@ -4332,6 +4332,10 @@ var gCSSProperties = { "oklab(100% 0 0)", "oklab(60% 0.1 -0.1 / 75%)", "oklch(70% 0.2 180deg / 40%)", + "color-mix(in oklab, red, blue)", + "color-mix(in oklch, red, blue)", + "color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))", + "color-mix(in oklch, oklch(0.1 0.2 30), oklch(0.5 0.6 70))", ], invalid_values: [ "#f", diff --git a/layout/style/test/test_computed_style.html b/layout/style/test/test_computed_style.html index 55ba9edb3..5363e1dac 100644 --- a/layout/style/test/test_computed_style.html +++ b/layout/style/test/test_computed_style.html @@ -406,14 +406,19 @@ var noframe_container = document.getElementById("content"); ["lch(150% 50 60 / 1)", "rgb(255, 255, 255)"], ["oklab(0 0 0 / 1)", "rgb(0, 0, 0)"], ["oklab(0 0 0 / 0.5)", "rgba(0, 0, 0, 0.5)"], - ["oklab(150% 0.5 0.2 / 1)", "rgb(255, 0, 0)"], + ["oklab(150% 0.5 0.2 / 1)", "rgb(255, 255, 255)"], ["oklch(0 0 0 / 1)", "rgb(0, 0, 0)"], ["oklch(0 0 0 / 0.5)", "rgba(0, 0, 0, 0.5)"], ["oklch(0.0001% 0.2 45 / 1)", "rgb(0, 0, 0)"], - ["oklch(99.9999% 0.2 45 / 1)", "rgb(255, 207, 132)"], + ["oklch(99.9999% 0.2 45 / 1)", "rgb(255, 251, 242)"], ["oklch(0% 1.1 60 / 1)", "rgb(0, 0, 0)"], - ["oklch(100% 110 60 / 1)", "rgb(0, 255, 0)"], - ["oklch(150% 0.5 50 / 1)", "rgb(255, 0, 0)"], + ["oklch(100% 110 60 / 1)", "rgb(255, 255, 255)"], + ["oklch(150% 0.5 50 / 1)", "rgb(255, 255, 255)"], + ["color-mix(in oklab, red, blue)", "rgb(140, 83, 162)"], + ["color-mix(in oklch, red, blue)", "rgb(183, 0, 190)"], + ["color-mix(in oklab, red, transparent)", "rgba(255, 0, 0, 0.5)"], + ["color-mix(in oklab, oklab(0.1 0.2 0.3), oklab(0.5 0.6 0.7))", "rgb(83, 24, 0)"], + ["color-mix(in oklch, oklch(0.1 0.2 30), oklch(0.5 0.6 70))", "rgb(84, 23, 0)"], ]; var p = document.createElement("p");