From 954c38cccb6ff3b5feed8f4c2c74922e90544fce Mon Sep 17 00:00:00 2001 From: Tiaan Louw Date: Thu, 16 Feb 2023 15:19:08 +0000 Subject: [PATCH] style: Part 2 - Use new color space for color mixing Mixing is now using the new color space for specifying the interpolation color space. For that reason hsl and hwb is added to the color space and also converting to/from them. Differential Revision: https://phabricator.services.mozilla.com/D169929 --- components/style/color/convert.rs | 117 ++++++++++++++++++++++++++++++ components/style/color/mix.rs | 81 ++++++--------------- components/style/color/mod.rs | 87 ++++++++++++++++++++-- 3 files changed, 220 insertions(+), 65 deletions(-) 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 {