style: Update color-mix() syntax to match the current spec

Test expectation updates for this in the latest patch of the bug.

Differential Revision: https://phabricator.services.mozilla.com/D147002
This commit is contained in:
Emilio Cobos Álvarez 2023-08-15 00:40:45 +02:00 committed by Martin Robinson
parent 3723a7b18d
commit f4ede10441
4 changed files with 141 additions and 85 deletions

View file

@ -7,7 +7,7 @@
use crate::values::animated::{Animate, Procedure, ToAnimatedZero}; use crate::values::animated::{Animate, Procedure, ToAnimatedZero};
use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
use crate::values::generics::color::{Color as GenericColor, ComplexColorRatios}; use crate::values::generics::color::{Color as GenericColor, ComplexColorRatios};
use crate::values::specified::color::{ColorSpaceKind, HueAdjuster}; use crate::values::specified::color::{ColorInterpolationMethod, ColorSpace, HueInterpolationMethod};
use euclid::default::{Transform3D, Vector3D}; use euclid::default::{Transform3D, Vector3D};
/// An animated RGBA color. /// An animated RGBA color.
@ -128,41 +128,40 @@ impl Color {
/// Mix two colors into one. /// Mix two colors into one.
pub fn mix( pub fn mix(
color_space: ColorSpaceKind, interpolation: &ColorInterpolationMethod,
left_color: &Color, left_color: &Color,
left_weight: f32, left_weight: f32,
right_color: &Color, right_color: &Color,
right_weight: f32, right_weight: f32,
hue_adjuster: HueAdjuster,
) -> Self { ) -> Self {
match color_space { match interpolation.space {
ColorSpaceKind::Srgb => Self::mix_in::<RGBA>( ColorSpace::Srgb => Self::mix_in::<RGBA>(
left_color, left_color,
left_weight, left_weight,
right_color, right_color,
right_weight, right_weight,
hue_adjuster, interpolation.hue,
), ),
ColorSpaceKind::Xyz => Self::mix_in::<XYZA>( ColorSpace::Xyz => Self::mix_in::<XYZA>(
left_color, left_color,
left_weight, left_weight,
right_color, right_color,
right_weight, right_weight,
hue_adjuster, interpolation.hue,
), ),
ColorSpaceKind::Lab => Self::mix_in::<LABA>( ColorSpace::Lab => Self::mix_in::<LABA>(
left_color, left_color,
left_weight, left_weight,
right_color, right_color,
right_weight, right_weight,
hue_adjuster, interpolation.hue,
), ),
ColorSpaceKind::Lch => Self::mix_in::<LCHA>( ColorSpace::Lch => Self::mix_in::<LCHA>(
left_color, left_color,
left_weight, left_weight,
right_color, right_color,
right_weight, right_weight,
hue_adjuster, interpolation.hue,
), ),
} }
} }
@ -172,7 +171,7 @@ impl Color {
left_weight: f32, left_weight: f32,
right_color: &Color, right_color: &Color,
right_weight: f32, right_weight: f32,
hue_adjuster: HueAdjuster, hue_interpolation: HueInterpolationMethod,
) -> Self ) -> Self
where where
S: ModelledColor, S: ModelledColor,
@ -180,7 +179,7 @@ impl Color {
let left_bg = S::from(left_color.scaled_rgba()); let left_bg = S::from(left_color.scaled_rgba());
let right_bg = S::from(right_color.scaled_rgba()); let right_bg = S::from(right_color.scaled_rgba());
let color = S::lerp(left_bg, left_weight, right_bg, right_weight, hue_adjuster); let color = S::lerp(left_bg, left_weight, right_bg, right_weight, hue_interpolation);
let rgba: RGBA = color.into(); let rgba: RGBA = color.into();
let rgba = if !rgba.in_gamut() { let rgba = if !rgba.in_gamut() {
// TODO: Better gamut mapping. // TODO: Better gamut mapping.
@ -365,14 +364,14 @@ impl ToAnimatedZero for Color {
trait ModelledColor: Clone + Copy + From<RGBA> + Into<RGBA> { trait ModelledColor: Clone + Copy + From<RGBA> + Into<RGBA> {
/// Linearly interpolate between the left and right colors. /// Linearly interpolate between the left and right colors.
/// ///
/// The HueAdjuster parameter is only for color spaces where the hue is /// The HueInterpolationMethod parameter is only for color spaces where the hue is
/// represented as an angle (e.g., CIE LCH). /// represented as an angle (e.g., CIE LCH).
fn lerp( fn lerp(
left_bg: Self, left_bg: Self,
left_weight: f32, left_weight: f32,
right_bg: Self, right_bg: Self,
right_weight: f32, right_weight: f32,
hue_adjuster: HueAdjuster, hue_interpolation: HueInterpolationMethod,
) -> Self; ) -> Self;
} }
@ -382,7 +381,7 @@ impl ModelledColor for RGBA {
left_weight: f32, left_weight: f32,
right_bg: Self, right_bg: Self,
right_weight: f32, right_weight: f32,
_: HueAdjuster, _: HueInterpolationMethod,
) -> Self { ) -> Self {
// Interpolation with alpha, as per // Interpolation with alpha, as per
// https://drafts.csswg.org/css-color/#interpolation-alpha. // https://drafts.csswg.org/css-color/#interpolation-alpha.
@ -440,7 +439,7 @@ impl ModelledColor for XYZA {
left_weight: f32, left_weight: f32,
right_bg: Self, right_bg: Self,
right_weight: f32, right_weight: f32,
_: HueAdjuster, _: HueInterpolationMethod,
) -> Self { ) -> Self {
// Interpolation with alpha, as per // Interpolation with alpha, as per
// https://drafts.csswg.org/css-color/#interpolation-alpha. // https://drafts.csswg.org/css-color/#interpolation-alpha.
@ -503,7 +502,7 @@ impl ModelledColor for LABA {
left_weight: f32, left_weight: f32,
right_bg: Self, right_bg: Self,
right_weight: f32, right_weight: f32,
_: HueAdjuster, _: HueInterpolationMethod,
) -> Self { ) -> Self {
// Interpolation with alpha, as per // Interpolation with alpha, as per
// https://drafts.csswg.org/css-color/#interpolation-alpha. // https://drafts.csswg.org/css-color/#interpolation-alpha.
@ -561,7 +560,7 @@ impl LCHA {
} }
impl LCHA { impl LCHA {
fn adjust(left_bg: Self, right_bg: Self, hue_adjuster: HueAdjuster) -> (Self, Self) { fn adjust(left_bg: Self, right_bg: Self, hue_interpolation: HueInterpolationMethod) -> (Self, Self) {
use std::f32::consts::{PI, TAU}; use std::f32::consts::{PI, TAU};
let mut left_bg = left_bg; let mut left_bg = left_bg;
@ -583,7 +582,7 @@ impl LCHA {
} }
} }
if hue_adjuster != HueAdjuster::Specified { if hue_interpolation != HueInterpolationMethod::Specified {
// Normalize hue into [0, 2 * PI) // Normalize hue into [0, 2 * PI)
while left_bg.hue < 0. { while left_bg.hue < 0. {
left_bg.hue += TAU; left_bg.hue += TAU;
@ -600,8 +599,8 @@ impl LCHA {
} }
} }
match hue_adjuster { match hue_interpolation {
HueAdjuster::Shorter => { HueInterpolationMethod::Shorter => {
let delta = right_bg.hue - left_bg.hue; let delta = right_bg.hue - left_bg.hue;
if delta > PI { if delta > PI {
@ -611,7 +610,7 @@ impl LCHA {
} }
}, },
HueAdjuster::Longer => { HueInterpolationMethod::Longer => {
let delta = right_bg.hue - left_bg.hue; let delta = right_bg.hue - left_bg.hue;
if 0. < delta && delta < PI { if 0. < delta && delta < PI {
left_bg.hue += TAU; left_bg.hue += TAU;
@ -620,13 +619,13 @@ impl LCHA {
} }
}, },
HueAdjuster::Increasing => { HueInterpolationMethod::Increasing => {
if right_bg.hue < left_bg.hue { if right_bg.hue < left_bg.hue {
right_bg.hue += TAU; right_bg.hue += TAU;
} }
}, },
HueAdjuster::Decreasing => { HueInterpolationMethod::Decreasing => {
if left_bg.hue < right_bg.hue { if left_bg.hue < right_bg.hue {
left_bg.hue += TAU; left_bg.hue += TAU;
} }
@ -634,7 +633,7 @@ impl LCHA {
//Angles are not adjusted. They are interpolated like any other //Angles are not adjusted. They are interpolated like any other
//component. //component.
HueAdjuster::Specified => {}, HueInterpolationMethod::Specified => {},
} }
(left_bg, right_bg) (left_bg, right_bg)
@ -647,11 +646,11 @@ impl ModelledColor for LCHA {
left_weight: f32, left_weight: f32,
right_bg: Self, right_bg: Self,
right_weight: f32, right_weight: f32,
hue_adjuster: HueAdjuster, hue_interpolation: HueInterpolationMethod,
) -> Self { ) -> Self {
// Interpolation with alpha, as per // Interpolation with alpha, as per
// https://drafts.csswg.org/css-color/#interpolation-alpha. // https://drafts.csswg.org/css-color/#interpolation-alpha.
let (left_bg, right_bg) = Self::adjust(left_bg, right_bg, hue_adjuster); let (left_bg, right_bg) = Self::adjust(left_bg, right_bg, hue_interpolation);
let mut lightness = 0.; let mut lightness = 0.;
let mut chroma = 0.; let mut chroma = 0.;

View file

@ -21,9 +21,9 @@ use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind};
/// A color space as defined in [1]. /// A color space as defined in [1].
/// ///
/// [1]: https://drafts.csswg.org/css-color-5/#typedef-colorspace /// [1]: https://drafts.csswg.org/css-color-4/#typedef-color-space
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
pub enum ColorSpaceKind { pub enum ColorSpace {
/// The sRGB color space. /// The sRGB color space.
Srgb, Srgb,
/// The CIEXYZ color space. /// The CIEXYZ color space.
@ -34,23 +34,81 @@ pub enum ColorSpaceKind {
Lch, Lch,
} }
/// A hue adjuster as defined in [1]. impl ColorSpace {
/// Returns whether this is a `<polar-color-space>`.
pub fn is_polar(self) -> bool {
match self {
Self::Srgb | Self::Xyz | Self::Lab => false,
Self::Lch => true,
}
}
}
/// A hue-interpolation-method as defined in [1].
/// ///
/// [1]: https://drafts.csswg.org/css-color-5/#typedef-hue-adjuster /// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
pub enum HueAdjuster { pub enum HueInterpolationMethod {
/// The "shorter" angle adjustment. /// https://drafts.csswg.org/css-color-4/#shorter
Shorter, Shorter,
/// The "longer" angle adjustment. /// https://drafts.csswg.org/css-color-4/#longer
Longer, Longer,
/// The "increasing" angle adjustment. /// https://drafts.csswg.org/css-color-4/#increasing
Increasing, Increasing,
/// The "decreasing" angle adjustment. /// https://drafts.csswg.org/css-color-4/#decreasing
Decreasing, Decreasing,
/// The "specified" angle adjustment. /// https://drafts.csswg.org/css-color-4/#specified
Specified, Specified,
} }
/// https://drafts.csswg.org/css-color-4/#color-interpolation-method
#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
pub struct ColorInterpolationMethod {
/// The color-space the interpolation should be done in.
pub space: ColorSpace,
/// The hue interpolation method.
pub hue: HueInterpolationMethod,
}
impl Parse for ColorInterpolationMethod {
fn parse<'i, 't>(
_: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
input.expect_ident_matching("in")?;
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.
let hue = if space.is_polar() {
input.try_parse(|input| -> Result<_, ParseError<'i>> {
let hue = HueInterpolationMethod::parse(input)?;
input.expect_ident_matching("hue")?;
Ok(hue)
}).unwrap_or(HueInterpolationMethod::Shorter)
} else {
HueInterpolationMethod::Shorter
};
Ok(Self { space, hue })
}
}
impl ToCss for ColorInterpolationMethod {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: Write,
{
dest.write_str("in ")?;
self.space.to_css(dest)?;
if self.hue != HueInterpolationMethod::Shorter {
dest.write_char(' ')?;
self.hue.to_css(dest)?;
dest.write_str(" hue")?;
}
Ok(())
}
}
/// A restricted version of the css `color-mix()` function, which only supports /// A restricted version of the css `color-mix()` function, which only supports
/// percentages. /// percentages.
/// ///
@ -58,12 +116,11 @@ pub enum HueAdjuster {
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
#[allow(missing_docs)] #[allow(missing_docs)]
pub struct ColorMix { pub struct ColorMix {
pub color_space: ColorSpaceKind, pub interpolation: ColorInterpolationMethod,
pub left: Color, pub left: Color,
pub left_percentage: Percentage, pub left_percentage: Percentage,
pub right: Color, pub right: Color,
pub right_percentage: Percentage, pub right_percentage: Percentage,
pub hue_adjuster: HueAdjuster,
} }
#[inline] #[inline]
@ -82,10 +139,6 @@ fn allow_color_mix_color_spaces() -> bool {
return false; return false;
} }
// NOTE(emilio): Syntax is still a bit in-flux, since [1] doesn't seem
// particularly complete, and disagrees with the examples.
//
// [1]: https://github.com/w3c/csswg-drafts/commit/a4316446112f9e814668c2caff7f826f512f8fed
impl Parse for ColorMix { impl Parse for ColorMix {
fn parse<'i, 't>( fn parse<'i, 't>(
context: &ParserContext, context: &ParserContext,
@ -98,53 +151,51 @@ impl Parse for ColorMix {
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
} }
let color_spaces_enabled = context.chrome_rules_enabled() ||
allow_color_mix_color_spaces();
input.expect_function_matching("color-mix")?; input.expect_function_matching("color-mix")?;
// NOTE(emilio): This implements the syntax described here for now,
// might need to get updated in the future.
//
// https://github.com/w3c/csswg-drafts/issues/6066#issuecomment-789836765
input.parse_nested_block(|input| { input.parse_nested_block(|input| {
input.expect_ident_matching("in")?; let interpolation = ColorInterpolationMethod::parse(context, input)?;
let color_space = if color_spaces_enabled {
ColorSpaceKind::parse(input)?
} else {
input.expect_ident_matching("srgb")?;
ColorSpaceKind::Srgb
};
input.expect_comma()?; input.expect_comma()?;
let try_parse_percentage = |input: &mut Parser| -> Option<Percentage> {
input.try_parse(|input| Percentage::parse_zero_to_a_hundred(context, input)).ok()
};
let mut left_percentage = try_parse_percentage(input);
let left = Color::parse(context, input)?; let left = Color::parse(context, input)?;
let left_percentage = input if left_percentage.is_none() {
.try_parse(|input| Percentage::parse(context, input)) left_percentage = try_parse_percentage(input);
.ok(); }
input.expect_comma()?; input.expect_comma()?;
let mut right_percentage = try_parse_percentage(input);
let right = Color::parse(context, input)?; let right = Color::parse(context, input)?;
let right_percentage = input
.try_parse(|input| Percentage::parse(context, input)) if right_percentage.is_none() {
.unwrap_or_else(|_| { right_percentage = try_parse_percentage(input);
Percentage::new(1.0 - left_percentage.map_or(0.5, |p| p.get())) }
});
let right_percentage = right_percentage.unwrap_or_else(|| {
Percentage::new(1.0 - left_percentage.map_or(0.5, |p| p.get()))
});
let left_percentage = let left_percentage =
left_percentage.unwrap_or_else(|| Percentage::new(1.0 - right_percentage.get())); left_percentage.unwrap_or_else(|| Percentage::new(1.0 - right_percentage.get()));
let hue_adjuster = input if left_percentage.get() + right_percentage.get() <= 0.0 {
.try_parse(|input| HueAdjuster::parse(input)) // If the percentages sum to zero, the function is invalid.
.unwrap_or(HueAdjuster::Shorter); return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
Ok(ColorMix { Ok(ColorMix {
color_space, interpolation,
left, left,
left_percentage, left_percentage,
right, right,
right_percentage, right_percentage,
hue_adjuster,
}) })
}) })
} }
@ -168,8 +219,8 @@ impl ToCss for ColorMix {
(1.0 - percent.get() - other.get()).abs() <= f32::EPSILON (1.0 - percent.get() - other.get()).abs() <= f32::EPSILON
} }
dest.write_str("color-mix(in ")?; dest.write_str("color-mix(")?;
self.color_space.to_css(dest)?; self.interpolation.to_css(dest)?;
dest.write_str(", ")?; dest.write_str(", ")?;
self.left.to_css(dest)?; self.left.to_css(dest)?;
if !can_omit(&self.left_percentage, &self.right_percentage, true) { if !can_omit(&self.left_percentage, &self.right_percentage, true) {
@ -182,12 +233,6 @@ impl ToCss for ColorMix {
dest.write_str(" ")?; dest.write_str(" ")?;
self.right_percentage.to_css(dest)?; self.right_percentage.to_css(dest)?;
} }
if self.hue_adjuster != HueAdjuster::Shorter {
dest.write_str(" ")?;
self.hue_adjuster.to_css(dest)?;
}
dest.write_str(")") dest.write_str(")")
} }
} }
@ -754,12 +799,11 @@ impl Color {
let left = mix.left.to_computed_color(context)?.to_animated_value(); let left = mix.left.to_computed_color(context)?.to_animated_value();
let right = mix.right.to_computed_color(context)?.to_animated_value(); let right = mix.right.to_computed_color(context)?.to_animated_value();
ToAnimatedValue::from_animated_value(AnimatedColor::mix( ToAnimatedValue::from_animated_value(AnimatedColor::mix(
mix.color_space, &mix.interpolation,
&left, &left,
mix.left_percentage.get(), mix.left_percentage.get(),
&right, &right,
mix.right_percentage.get(), mix.right_percentage.get(),
mix.hue_adjuster,
)) ))
}, },
#[cfg(feature = "gecko")] #[cfg(feature = "gecko")]

View file

@ -138,6 +138,15 @@ impl Percentage {
Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative) Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
} }
/// Parses a percentage token, but rejects it if it's negative or more than
/// 100%.
pub fn parse_zero_to_a_hundred<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
Self::parse_with_clamping_mode(context, input, AllowedNumericType::ZeroToOne)
}
/// Clamp to 100% if the value is over 100%. /// Clamp to 100% if the value is over 100%.
#[inline] #[inline]
pub fn clamp_to_hundred(self) -> Self { pub fn clamp_to_hundred(self) -> Self {

View file

@ -587,6 +587,8 @@ pub mod specified {
NonNegative, NonNegative,
/// Allow only numeric values greater or equal to 1.0. /// Allow only numeric values greater or equal to 1.0.
AtLeastOne, AtLeastOne,
/// Allow only numeric values from 0 to 1.0.
ZeroToOne,
} }
impl Default for AllowedNumericType { impl Default for AllowedNumericType {
@ -607,6 +609,7 @@ pub mod specified {
AllowedNumericType::All => true, AllowedNumericType::All => true,
AllowedNumericType::NonNegative => val >= 0.0, AllowedNumericType::NonNegative => val >= 0.0,
AllowedNumericType::AtLeastOne => val >= 1.0, AllowedNumericType::AtLeastOne => val >= 1.0,
AllowedNumericType::ZeroToOne => val >= 0.0 && val <= 1.0,
} }
} }
@ -614,9 +617,10 @@ pub mod specified {
#[inline] #[inline]
pub fn clamp(&self, val: f32) -> f32 { pub fn clamp(&self, val: f32) -> f32 {
match *self { match *self {
AllowedNumericType::NonNegative if val < 0. => 0., AllowedNumericType::All => val,
AllowedNumericType::AtLeastOne if val < 1. => 1., AllowedNumericType::NonNegative => val.max(0.),
_ => val, AllowedNumericType::AtLeastOne => val.max(1.),
AllowedNumericType::ZeroToOne => val.max(0.).min(1.),
} }
} }
} }