diff --git a/components/style/color/convert.rs b/components/style/color/convert.rs index 0aaf7a83b9c..5c44079a7c1 100644 --- a/components/style/color/convert.rs +++ b/components/style/color/convert.rs @@ -21,6 +21,123 @@ type Vector = euclid::default::Vector3D; const RAD_PER_DEG: f32 = PI / 180.0; const DEG_PER_RAD: f32 = 180.0 / PI; +/// Normalize hue into [0, 360). +#[inline] +fn normalize_hue(hue: f32) -> f32 { + hue - 360. * (hue / 360.).floor() +} + +/// Calculate the hue from RGB components and return it along with the min and +/// max RGB values. +#[inline] +fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) { + let max = red.max(green).max(blue); + let min = red.min(green).min(blue); + + let delta = max - min; + + let hue = if delta != 0.0 { + 60.0 * if max == red { + (green - blue) / delta + if green < blue { 6.0 } else { 0.0 } + } else if max == green { + (blue - red) / delta + 2.0 + } else { + (red - green) / delta + 4.0 + } + } else { + std::f32::NAN + }; + + (hue, min, max) +} + +/// Convert a hue value into red, green, blue components. +#[inline] +fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 { + let hue = normalize_hue(hue); + + if hue * 6.0 < 360.0 { + t1 + (t2 - t1) * hue / 60.0 + } else if hue * 2.0 < 360.0 { + t2 + } else if hue * 3.0 < 720.0 { + t1 + (t2 - t1) * (240.0 - hue) / 60.0 + } else { + t1 + } +} + +/// Convert from HSL notation to RGB notation. +#[inline] +pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents { + let ColorComponents(hue, saturation, lightness) = *from; + + let t2 = if lightness <= 0.5 { + lightness * (saturation + 1.0) + } else { + lightness + saturation - lightness * saturation + }; + let t1 = lightness * 2.0 - t2; + + ColorComponents( + hue_to_rgb(t1, t2, hue + 120.0), + hue_to_rgb(t1, t2, hue), + hue_to_rgb(t1, t2, hue - 120.0), + ) +} + +/// Convert from RGB notation to HSL notation. +/// https://drafts.csswg.org/css-color/#rgb-to-hsl +pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents { + let ColorComponents(red, green, blue) = *from; + + let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); + + let light = (min + max) / 2.0; + let delta = max - min; + + let sat = if delta != 0.0 { + if light == 0.0 || light == 1.0 { + 0.0 + } else { + (max - light) / light.min(1.0 - light) + } + } else { + 0.0 + }; + + ColorComponents(hue, sat, light) +} + +/// Convert from HWB notation to RGB notation. +/// https://drafts.csswg.org/css-color-4/#hwb-to-rgb +#[inline] +pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents { + let ColorComponents(hue, whiteness, blackness) = *from; + + if whiteness + blackness > 1.0 { + let gray = whiteness / (whiteness + blackness); + return ColorComponents(gray, gray, gray); + } + + let x = 1.0 - whiteness - blackness; + hsl_to_rgb(&ColorComponents(hue, 1.0, 0.5)).map(|v| v * x + whiteness) +} + +/// Convert from RGB notation to HWB notation. +/// https://drafts.csswg.org/css-color-4/#rgb-to-hwb +#[inline] +pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents { + let ColorComponents(red, green, blue) = *from; + + let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); + + let whiteness = min; + let blackness = 1.0 - max; + + ColorComponents(hue, whiteness, blackness) +} + #[inline] fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents { let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2)); diff --git a/components/style/color/mix.rs b/components/style/color/mix.rs index ac56cb59ef3..90755bbd7f2 100644 --- a/components/style/color/mix.rs +++ b/components/style/color/mix.rs @@ -4,6 +4,7 @@ //! Color mixing/interpolation. +use super::ColorSpace; use crate::parser::{Parse, ParserContext}; use crate::values::animated::color::AnimatedRGBA as RGBA; use cssparser::Parser; @@ -15,55 +16,6 @@ use style_traits::{CssWriter, ParseError, ToCss}; const RAD_PER_DEG: f32 = PI / 180.0; const DEG_PER_RAD: f32 = 180.0 / PI; -/// A color space as defined in [1]. -/// -/// [1]: https://drafts.csswg.org/css-color-4/#typedef-color-space -#[derive( - Clone, - Copy, - Debug, - Eq, - MallocSizeOf, - Parse, - PartialEq, - ToAnimatedValue, - ToComputedValue, - ToCss, - ToResolvedValue, - ToShmem, -)] -#[repr(u8)] -pub enum InterpolationColorSpace { - /// The sRGB color space. - Srgb, - /// The linear-sRGB color space. - LinearSrgb, - /// The CIEXYZ color space. - #[parse(aliases = "xyz-d65")] - Xyz, - /// https://drafts.csswg.org/css-color-4/#valdef-color-xyz - XyzD50, - /// The CIELAB color space. - Lab, - /// https://drafts.csswg.org/css-color-4/#valdef-hsl-hsl - Hsl, - /// https://drafts.csswg.org/css-color-4/#valdef-hwb-hwb - Hwb, - /// The CIELAB color space, expressed in cylindrical coordinates. - Lch, - // TODO: Oklab, Lch -} - -impl InterpolationColorSpace { - /// Returns whether this is a ``. - pub fn is_polar(self) -> bool { - match self { - Self::Srgb | Self::LinearSrgb | Self::Xyz | Self::XyzD50 | Self::Lab => false, - Self::Hsl | Self::Hwb | Self::Lch => true, - } - } -} - /// A hue-interpolation-method as defined in [1]. /// /// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method @@ -111,7 +63,7 @@ pub enum HueInterpolationMethod { #[repr(C)] pub struct ColorInterpolationMethod { /// The color-space the interpolation should be done in. - pub space: InterpolationColorSpace, + pub space: ColorSpace, /// The hue interpolation method. pub hue: HueInterpolationMethod, } @@ -120,7 +72,7 @@ impl ColorInterpolationMethod { /// Returns the srgb interpolation method. pub fn srgb() -> Self { Self { - space: InterpolationColorSpace::Srgb, + space: ColorSpace::Srgb, hue: HueInterpolationMethod::Shorter, } } @@ -132,7 +84,7 @@ impl Parse for ColorInterpolationMethod { input: &mut Parser<'i, 't>, ) -> Result> { input.expect_ident_matching("in")?; - let space = InterpolationColorSpace::parse(input)?; + let space = ColorSpace::parse(input)?; // https://drafts.csswg.org/css-color-4/#hue-interpolation // Unless otherwise specified, if no specific hue interpolation // algorithm is selected by the host syntax, the default is shorter. @@ -209,14 +161,23 @@ pub fn mix( } let mix_function = match interpolation.space { - InterpolationColorSpace::Srgb => mix_in::, - InterpolationColorSpace::LinearSrgb => mix_in::, - InterpolationColorSpace::Xyz => mix_in::, - InterpolationColorSpace::XyzD50 => mix_in::, - InterpolationColorSpace::Lab => mix_in::, - InterpolationColorSpace::Hwb => mix_in::, - InterpolationColorSpace::Hsl => mix_in::, - InterpolationColorSpace::Lch => mix_in::, + ColorSpace::Srgb => mix_in::, + ColorSpace::SrgbLinear => mix_in::, + ColorSpace::XyzD65 => mix_in::, + ColorSpace::XyzD50 => mix_in::, + ColorSpace::Lab => mix_in::, + ColorSpace::Hwb => mix_in::, + ColorSpace::Hsl => mix_in::, + ColorSpace::Lch => mix_in::, + + ColorSpace::Oklab | + ColorSpace::Oklch | + ColorSpace::DisplayP3 | + ColorSpace::A98Rgb | + ColorSpace::ProphotoRgb | + ColorSpace::Rec2020 => { + todo!() + }, }; mix_function( left_color, diff --git a/components/style/color/mod.rs b/components/style/color/mod.rs index 029da81014f..6d2de49ada7 100644 --- a/components/style/color/mod.rs +++ b/components/style/color/mod.rs @@ -24,10 +24,36 @@ impl ColorComponents { /// A color space representation in the CSS specification. /// -/// https://w3c.github.io/csswg-drafts/css-color-4/#color-type -#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] +/// https://drafts.csswg.org/css-color-4/#typedef-color-space +/// +/// NOTE: Right now HSL and HWB colors can not be constructed by the user. They +/// are converted to RGB in the parser. The parser should return the +/// HSL/HWB values as is to avoid unnescessary conversions to/from RGB. +/// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1817035 +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] #[repr(u8)] pub enum ColorSpace { + /// A color specified in the Hsl notation in the sRGB color space, e.g. + /// "hsl(289.18 93.136% 65.531%)" + /// https://drafts.csswg.org/css-color-4/#the-hsl-notation + Hsl, + /// A color specified in the Hwb notation in the sRGB color space, e.g. + /// "hwb(740deg 20% 30%)" + /// https://drafts.csswg.org/css-color-4/#the-hwb-notation + Hwb, /// A color specified in the Lab color format, e.g. /// "lab(29.2345% 39.3825 20.0664)". /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors @@ -67,9 +93,24 @@ pub enum ColorSpace { XyzD50, /// A color specified with the color(..) function and the "xyz-d65" or "xyz" /// color space, e.g. "color(xyz-d65 0.21661 0.14602 0.59452)". + #[parse(aliases = "xyz")] XyzD65, } +impl ColorSpace { + /// Returns whether this is a ``. + #[inline] + pub fn is_rectangular(&self) -> bool { + !self.is_polar() + } + + /// Returns whether this is a ``. + #[inline] + pub fn is_polar(&self) -> bool { + matches!(self, Self::Hsl | Self::Hwb | Self::Lch | Self::Oklch) + } +} + bitflags! { /// Flags used when serializing colors. #[derive(Default, MallocSizeOf, ToShmem)] @@ -96,6 +137,14 @@ pub struct AbsoluteColor { pub flags: SerializationFlags, } +/// Given an [`AbsoluteColor`], return the 4 float components as the type given, +/// e.g.: +/// +/// ```rust +/// let srgb = AbsoluteColor::new(ColorSpace::Srgb, 1.0, 0.0, 0.0, 0.0); +/// let floats = color_components_as!(&srgb, [f32; 4]); // [1.0, 0.0, 0.0, 0.0] +/// ``` +#[macro_export] macro_rules! color_components_as { ($c:expr, $t:ty) => {{ // This macro is not an inline function, because we can't use the @@ -139,6 +188,14 @@ impl AbsoluteColor { } let (xyz, white_point) = match self.color_space { + Hsl => { + let rgb = convert::hsl_to_rgb(&self.components); + convert::to_xyz::(&rgb) + }, + Hwb => { + let rgb = convert::hwb_to_rgb(&self.components); + convert::to_xyz::(&rgb) + }, Lab => convert::to_xyz::(&self.components), Lch => convert::to_xyz::(&self.components), Oklab => convert::to_xyz::(&self.components), @@ -154,6 +211,14 @@ impl AbsoluteColor { }; let result = match color_space { + Hsl => { + let rgb = convert::from_xyz::(&xyz, white_point); + convert::rgb_to_hsl(&rgb) + }, + Hwb => { + let rgb = convert::from_xyz::(&xyz, white_point); + convert::rgb_to_hwb(&rgb) + }, Lab => convert::from_xyz::(&xyz, white_point), Lch => convert::from_xyz::(&xyz, white_point), Oklab => convert::from_xyz::(&xyz, white_point), @@ -239,6 +304,17 @@ impl ToCss for AbsoluteColor { W: Write, { match self.color_space { + ColorSpace::Hsl => { + let rgb = convert::hsl_to_rgb(&self.components); + Self::new(ColorSpace::Srgb, rgb, self.alpha).to_css(dest) + }, + + ColorSpace::Hwb => { + let rgb = convert::hwb_to_rgb(&self.components); + + Self::new(ColorSpace::Srgb, rgb, self.alpha).to_css(dest) + }, + ColorSpace::Srgb if !self.flags.contains(SerializationFlags::AS_COLOR_FUNCTION) => { cssparser::ToCss::to_css( &cssparser::RGBA::from_floats( @@ -268,9 +344,6 @@ impl ToCss for AbsoluteColor { ), _ => { let color_space = match self.color_space { - ColorSpace::Lab | ColorSpace::Lch | ColorSpace::Oklab | ColorSpace::Oklch => { - unreachable!("Handle these in the wrapping match case!!") - }, ColorSpace::Srgb => { debug_assert!( self.flags.contains(SerializationFlags::AS_COLOR_FUNCTION), @@ -286,6 +359,10 @@ impl ToCss for AbsoluteColor { ColorSpace::Rec2020 => cssparser::PredefinedColorSpace::Rec2020, ColorSpace::XyzD50 => cssparser::PredefinedColorSpace::XyzD50, ColorSpace::XyzD65 => cssparser::PredefinedColorSpace::XyzD65, + + _ => { + unreachable!("other color spaces do not support color() syntax") + }, }; let color_function = cssparser::ColorFunction {