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
This commit is contained in:
Tiaan Louw 2023-02-16 15:19:08 +00:00 committed by Martin Robinson
parent e963abe2e9
commit 954c38cccb
3 changed files with 220 additions and 65 deletions

View file

@ -21,6 +21,123 @@ type Vector = euclid::default::Vector3D<f32>;
const RAD_PER_DEG: f32 = PI / 180.0; const RAD_PER_DEG: f32 = PI / 180.0;
const DEG_PER_RAD: f32 = 180.0 / PI; 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] #[inline]
fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents { fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents {
let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2)); let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2));

View file

@ -4,6 +4,7 @@
//! Color mixing/interpolation. //! Color mixing/interpolation.
use super::ColorSpace;
use crate::parser::{Parse, ParserContext}; use crate::parser::{Parse, ParserContext};
use crate::values::animated::color::AnimatedRGBA as RGBA; use crate::values::animated::color::AnimatedRGBA as RGBA;
use cssparser::Parser; use cssparser::Parser;
@ -15,55 +16,6 @@ use style_traits::{CssWriter, ParseError, ToCss};
const RAD_PER_DEG: f32 = PI / 180.0; const RAD_PER_DEG: f32 = PI / 180.0;
const DEG_PER_RAD: f32 = 180.0 / PI; 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 `<polar-color-space>`.
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]. /// A hue-interpolation-method as defined in [1].
/// ///
/// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method /// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method
@ -111,7 +63,7 @@ pub enum HueInterpolationMethod {
#[repr(C)] #[repr(C)]
pub struct ColorInterpolationMethod { pub struct ColorInterpolationMethod {
/// The color-space the interpolation should be done in. /// The color-space the interpolation should be done in.
pub space: InterpolationColorSpace, pub space: ColorSpace,
/// The hue interpolation method. /// The hue interpolation method.
pub hue: HueInterpolationMethod, pub hue: HueInterpolationMethod,
} }
@ -120,7 +72,7 @@ impl ColorInterpolationMethod {
/// Returns the srgb interpolation method. /// Returns the srgb interpolation method.
pub fn srgb() -> Self { pub fn srgb() -> Self {
Self { Self {
space: InterpolationColorSpace::Srgb, space: ColorSpace::Srgb,
hue: HueInterpolationMethod::Shorter, hue: HueInterpolationMethod::Shorter,
} }
} }
@ -132,7 +84,7 @@ impl Parse for ColorInterpolationMethod {
input: &mut Parser<'i, 't>, input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> { ) -> Result<Self, ParseError<'i>> {
input.expect_ident_matching("in")?; input.expect_ident_matching("in")?;
let space = InterpolationColorSpace::parse(input)?; let space = ColorSpace::parse(input)?;
// https://drafts.csswg.org/css-color-4/#hue-interpolation // https://drafts.csswg.org/css-color-4/#hue-interpolation
// Unless otherwise specified, if no specific hue interpolation // Unless otherwise specified, if no specific hue interpolation
// algorithm is selected by the host syntax, the default is shorter. // algorithm is selected by the host syntax, the default is shorter.
@ -209,14 +161,23 @@ pub fn mix(
} }
let mix_function = match interpolation.space { let mix_function = match interpolation.space {
InterpolationColorSpace::Srgb => mix_in::<RGBA>, ColorSpace::Srgb => mix_in::<RGBA>,
InterpolationColorSpace::LinearSrgb => mix_in::<LinearRGBA>, ColorSpace::SrgbLinear => mix_in::<LinearRGBA>,
InterpolationColorSpace::Xyz => mix_in::<XYZD65A>, ColorSpace::XyzD65 => mix_in::<XYZD65A>,
InterpolationColorSpace::XyzD50 => mix_in::<XYZD50A>, ColorSpace::XyzD50 => mix_in::<XYZD50A>,
InterpolationColorSpace::Lab => mix_in::<LABA>, ColorSpace::Lab => mix_in::<LABA>,
InterpolationColorSpace::Hwb => mix_in::<HWBA>, ColorSpace::Hwb => mix_in::<HWBA>,
InterpolationColorSpace::Hsl => mix_in::<HSLA>, ColorSpace::Hsl => mix_in::<HSLA>,
InterpolationColorSpace::Lch => mix_in::<LCHA>, ColorSpace::Lch => mix_in::<LCHA>,
ColorSpace::Oklab |
ColorSpace::Oklch |
ColorSpace::DisplayP3 |
ColorSpace::A98Rgb |
ColorSpace::ProphotoRgb |
ColorSpace::Rec2020 => {
todo!()
},
}; };
mix_function( mix_function(
left_color, left_color,

View file

@ -24,10 +24,36 @@ impl ColorComponents {
/// A color space representation in the CSS specification. /// A color space representation in the CSS specification.
/// ///
/// https://w3c.github.io/csswg-drafts/css-color-4/#color-type /// https://drafts.csswg.org/css-color-4/#typedef-color-space
#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] ///
/// 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)] #[repr(u8)]
pub enum ColorSpace { 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. /// A color specified in the Lab color format, e.g.
/// "lab(29.2345% 39.3825 20.0664)". /// "lab(29.2345% 39.3825 20.0664)".
/// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors
@ -67,9 +93,24 @@ pub enum ColorSpace {
XyzD50, XyzD50,
/// A color specified with the color(..) function and the "xyz-d65" or "xyz" /// 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)". /// color space, e.g. "color(xyz-d65 0.21661 0.14602 0.59452)".
#[parse(aliases = "xyz")]
XyzD65, XyzD65,
} }
impl ColorSpace {
/// Returns whether this is a `<rectangular-color-space>`.
#[inline]
pub fn is_rectangular(&self) -> bool {
!self.is_polar()
}
/// Returns whether this is a `<polar-color-space>`.
#[inline]
pub fn is_polar(&self) -> bool {
matches!(self, Self::Hsl | Self::Hwb | Self::Lch | Self::Oklch)
}
}
bitflags! { bitflags! {
/// Flags used when serializing colors. /// Flags used when serializing colors.
#[derive(Default, MallocSizeOf, ToShmem)] #[derive(Default, MallocSizeOf, ToShmem)]
@ -96,6 +137,14 @@ pub struct AbsoluteColor {
pub flags: SerializationFlags, 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 { macro_rules! color_components_as {
($c:expr, $t:ty) => {{ ($c:expr, $t:ty) => {{
// This macro is not an inline function, because we can't use the // 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 { let (xyz, white_point) = match self.color_space {
Hsl => {
let rgb = convert::hsl_to_rgb(&self.components);
convert::to_xyz::<convert::Srgb>(&rgb)
},
Hwb => {
let rgb = convert::hwb_to_rgb(&self.components);
convert::to_xyz::<convert::Srgb>(&rgb)
},
Lab => convert::to_xyz::<convert::Lab>(&self.components), Lab => convert::to_xyz::<convert::Lab>(&self.components),
Lch => convert::to_xyz::<convert::Lch>(&self.components), Lch => convert::to_xyz::<convert::Lch>(&self.components),
Oklab => convert::to_xyz::<convert::Oklab>(&self.components), Oklab => convert::to_xyz::<convert::Oklab>(&self.components),
@ -154,6 +211,14 @@ impl AbsoluteColor {
}; };
let result = match color_space { let result = match color_space {
Hsl => {
let rgb = convert::from_xyz::<convert::Srgb>(&xyz, white_point);
convert::rgb_to_hsl(&rgb)
},
Hwb => {
let rgb = convert::from_xyz::<convert::Srgb>(&xyz, white_point);
convert::rgb_to_hwb(&rgb)
},
Lab => convert::from_xyz::<convert::Lab>(&xyz, white_point), Lab => convert::from_xyz::<convert::Lab>(&xyz, white_point),
Lch => convert::from_xyz::<convert::Lch>(&xyz, white_point), Lch => convert::from_xyz::<convert::Lch>(&xyz, white_point),
Oklab => convert::from_xyz::<convert::Oklab>(&xyz, white_point), Oklab => convert::from_xyz::<convert::Oklab>(&xyz, white_point),
@ -239,6 +304,17 @@ impl ToCss for AbsoluteColor {
W: Write, W: Write,
{ {
match self.color_space { 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) => { ColorSpace::Srgb if !self.flags.contains(SerializationFlags::AS_COLOR_FUNCTION) => {
cssparser::ToCss::to_css( cssparser::ToCss::to_css(
&cssparser::RGBA::from_floats( &cssparser::RGBA::from_floats(
@ -268,9 +344,6 @@ impl ToCss for AbsoluteColor {
), ),
_ => { _ => {
let color_space = match self.color_space { 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 => { ColorSpace::Srgb => {
debug_assert!( debug_assert!(
self.flags.contains(SerializationFlags::AS_COLOR_FUNCTION), self.flags.contains(SerializationFlags::AS_COLOR_FUNCTION),
@ -286,6 +359,10 @@ impl ToCss for AbsoluteColor {
ColorSpace::Rec2020 => cssparser::PredefinedColorSpace::Rec2020, ColorSpace::Rec2020 => cssparser::PredefinedColorSpace::Rec2020,
ColorSpace::XyzD50 => cssparser::PredefinedColorSpace::XyzD50, ColorSpace::XyzD50 => cssparser::PredefinedColorSpace::XyzD50,
ColorSpace::XyzD65 => cssparser::PredefinedColorSpace::XyzD65, ColorSpace::XyzD65 => cssparser::PredefinedColorSpace::XyzD65,
_ => {
unreachable!("other color spaces do not support color() syntax")
},
}; };
let color_function = cssparser::ColorFunction { let color_function = cssparser::ColorFunction {