From 255fe05d404a216fe6f2e9d0142d321c9eaf49c1 Mon Sep 17 00:00:00 2001 From: Dan Glastonbury Date: Wed, 23 May 2018 15:23:26 +1000 Subject: [PATCH] style: Extend StyleComplexColor to support additive blending. Refactored StyleComplexColor to support "complex" blending between background (numeric) color and foreground color (currentColor). Made explicit the distinction between numeric, currentColor and a complex blend in Gecko and Stylo. This is to support SMIL animation, for example, of the form: Bug: 1465307 Reviewed-by: hiro,xidorn MozReview-Commit-ID: IUAK8P07gtm --- .../sugar/style_complex_color.rs | 81 +++++--- components/style/values/animated/color.rs | 191 ++++++++++-------- components/style/values/computed/color.rs | 146 +++++++------ components/style/values/computed/mod.rs | 2 +- components/style/values/specified/color.rs | 32 ++- 5 files changed, 248 insertions(+), 204 deletions(-) diff --git a/components/style/gecko_bindings/sugar/style_complex_color.rs b/components/style/gecko_bindings/sugar/style_complex_color.rs index 696eb6968b6..3a37f9cb0fd 100644 --- a/components/style/gecko_bindings/sugar/style_complex_color.rs +++ b/components/style/gecko_bindings/sugar/style_complex_color.rs @@ -5,28 +5,21 @@ //! Rust helpers to interact with Gecko's StyleComplexColor. use gecko::values::{convert_nscolor_to_rgba, convert_rgba_to_nscolor}; -use gecko_bindings::structs::{nscolor, StyleComplexColor}; +use gecko_bindings::structs::StyleComplexColor; +use gecko_bindings::structs::StyleComplexColor_Tag as Tag; use values::{Auto, Either}; -use values::computed::Color as ComputedColor; +use values::computed::{Color as ComputedColor, RGBAColor as ComputedRGBA}; +use values::computed::ComplexColorRatios; use values::computed::ui::ColorOrAuto; -impl From for StyleComplexColor { - fn from(other: nscolor) -> Self { - StyleComplexColor { - mColor: other, - mForegroundRatio: 0, - mIsAuto: false, - } - } -} - impl StyleComplexColor { /// Create a `StyleComplexColor` value that represents `currentColor`. pub fn current_color() -> Self { StyleComplexColor { mColor: 0, - mForegroundRatio: 255, - mIsAuto: false, + mBgRatio: 0., + mFgRatio: 1., + mTag: Tag::eForeground, } } @@ -34,28 +27,66 @@ impl StyleComplexColor { pub fn auto() -> Self { StyleComplexColor { mColor: 0, - mForegroundRatio: 255, - mIsAuto: true, + mBgRatio: 0., + mFgRatio: 1., + mTag: Tag::eAuto, + } + } +} + +impl From for StyleComplexColor { + fn from(other: ComputedRGBA) -> Self { + StyleComplexColor { + mColor: convert_rgba_to_nscolor(&other), + mBgRatio: 1., + mFgRatio: 0., + mTag: Tag::eNumeric, } } } impl From for StyleComplexColor { fn from(other: ComputedColor) -> Self { - StyleComplexColor { - mColor: convert_rgba_to_nscolor(&other.color).into(), - mForegroundRatio: other.foreground_ratio, - mIsAuto: false, + match other { + ComputedColor::Numeric(color) => color.into(), + ComputedColor::Foreground => Self::current_color(), + ComputedColor::Complex(color, ratios) => { + debug_assert!(ratios != ComplexColorRatios::NUMERIC); + debug_assert!(ratios != ComplexColorRatios::FOREGROUND); + StyleComplexColor { + mColor: convert_rgba_to_nscolor(&color).into(), + mBgRatio: ratios.bg, + mFgRatio: ratios.fg, + mTag: Tag::eComplex, + } + } } } } impl From for ComputedColor { fn from(other: StyleComplexColor) -> Self { - debug_assert!(!other.mIsAuto); - ComputedColor { - color: convert_nscolor_to_rgba(other.mColor), - foreground_ratio: other.mForegroundRatio, + match other.mTag { + Tag::eNumeric => { + debug_assert!(other.mBgRatio == 1. && other.mFgRatio == 0.); + ComputedColor::Numeric(convert_nscolor_to_rgba(other.mColor)) + } + Tag::eForeground => { + debug_assert!(other.mBgRatio == 0. && other.mFgRatio == 1.); + ComputedColor::Foreground + } + Tag::eComplex => { + debug_assert!(other.mBgRatio != 1. || other.mFgRatio != 0.); + debug_assert!(other.mBgRatio != 0. || other.mFgRatio != 1.); + ComputedColor::Complex( + convert_nscolor_to_rgba(other.mColor), + ComplexColorRatios { + bg: other.mBgRatio, + fg: other.mFgRatio, + }, + ) + } + Tag::eAuto => unreachable!("Unsupport StyleComplexColor with tag eAuto"), } } } @@ -71,7 +102,7 @@ impl From for StyleComplexColor { impl From for ColorOrAuto { fn from(other: StyleComplexColor) -> Self { - if !other.mIsAuto { + if other.mTag != Tag::eAuto { Either::First(other.into()) } else { Either::Second(Auto) diff --git a/components/style/values/animated/color.rs b/components/style/values/animated/color.rs index 614c17df00c..98f3f425d22 100644 --- a/components/style/values/animated/color.rs +++ b/components/style/values/animated/color.rs @@ -6,6 +6,7 @@ use values::animated::{Animate, Procedure, ToAnimatedZero}; use values::distance::{ComputeSquaredDistance, SquaredDistance}; +use values::computed::ComplexColorRatios; /// An animated RGBA color. /// @@ -91,42 +92,51 @@ impl ComputeSquaredDistance for RGBA { } } +impl Animate for ComplexColorRatios { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + let bg = self.bg.animate(&other.bg, procedure)?; + let fg = self.fg.animate(&other.fg, procedure)?; + + Ok(ComplexColorRatios { bg, fg }) + } +} + #[allow(missing_docs)] #[cfg_attr(feature = "servo", derive(MallocSizeOf))] #[derive(Clone, Copy, Debug, PartialEq)] -pub struct Color { - pub color: RGBA, - pub foreground_ratio: f32, +pub enum Color { + Numeric(RGBA), + Foreground, + Complex(RGBA, ComplexColorRatios), } impl Color { fn currentcolor() -> Self { - Color { - color: RGBA::transparent(), - foreground_ratio: 1., - } + Color::Foreground } /// Returns a transparent intermediate color. pub fn transparent() -> Self { - Color { - color: RGBA::transparent(), - foreground_ratio: 0., - } - } - - fn is_currentcolor(&self) -> bool { - self.foreground_ratio >= 1. - } - - fn is_numeric(&self) -> bool { - self.foreground_ratio <= 0. + Color::Numeric(RGBA::transparent()) } fn effective_intermediate_rgba(&self) -> RGBA { - RGBA { - alpha: self.color.alpha * (1. - self.foreground_ratio), - ..self.color + match *self { + Color::Numeric(color) => color, + Color::Foreground => RGBA::transparent(), + Color::Complex(color, ratios) => RGBA { + alpha: color.alpha * ratios.bg, + ..color.clone() + }, + } + } + + fn effective_ratios(&self) -> ComplexColorRatios { + match *self { + Color::Numeric(..) => ComplexColorRatios::NUMERIC, + Color::Foreground => ComplexColorRatios::FOREGROUND, + Color::Complex(.., ratios) => ratios, } } } @@ -136,50 +146,66 @@ impl Animate for Color { fn animate(&self, other: &Self, procedure: Procedure) -> Result { // Common cases are interpolating between two numeric colors, // two currentcolors, and a numeric color and a currentcolor. - // - // Note: this algorithm assumes self_portion + other_portion - // equals to one, so it may be broken for additive operation. - // To properly support additive color interpolation, we would - // need two ratio fields in computed color types. let (this_weight, other_weight) = procedure.weights(); - if self.foreground_ratio == other.foreground_ratio { - if self.is_currentcolor() { - Ok(Color::currentcolor()) - } else { - Ok(Color { - color: self.color.animate(&other.color, procedure)?, - foreground_ratio: self.foreground_ratio, - }) + + Ok(match (*self, *other, procedure) { + // Any interpolation of currentColor with currentColor returns currentColor. + (Color::Foreground, Color::Foreground, Procedure::Interpolate { .. }) => { + Color::currentcolor() } - } else if self.is_currentcolor() && other.is_numeric() { - Ok(Color { - color: other.color, - foreground_ratio: this_weight as f32, - }) - } else if self.is_numeric() && other.is_currentcolor() { - Ok(Color { - color: self.color, - foreground_ratio: other_weight as f32, - }) - } else { - // For interpolating between two complex colors, we need to - // generate colors with effective alpha value. - let self_color = self.effective_intermediate_rgba(); - let other_color = other.effective_intermediate_rgba(); - let color = self_color.animate(&other_color, procedure)?; - // Then we compute the final foreground ratio, and derive - // the final alpha value from the effective alpha value. - let foreground_ratio = self.foreground_ratio - .animate(&other.foreground_ratio, procedure)?; - let alpha = color.alpha / (1. - foreground_ratio); - Ok(Color { - color: RGBA { - alpha: alpha, - ..color + // Animating two numeric colors. + (Color::Numeric(c1), Color::Numeric(c2), _) => { + Color::Numeric(c1.animate(&c2, procedure)?) + } + // Combinations of numeric color and currentColor + (Color::Foreground, Color::Numeric(color), _) => Color::Complex( + color, + ComplexColorRatios { + bg: other_weight as f32, + fg: this_weight as f32, }, - foreground_ratio: foreground_ratio, - }) - } + ), + (Color::Numeric(color), Color::Foreground, _) => Color::Complex( + color, + ComplexColorRatios { + bg: this_weight as f32, + fg: other_weight as f32, + }, + ), + + // Any other animation of currentColor with currentColor is complex. + (Color::Foreground, Color::Foreground, _) => Color::Complex( + RGBA::transparent(), + ComplexColorRatios { + bg: 0., + fg: (this_weight + other_weight) as f32, + }, + ), + + // Defer to complex calculations + _ => { + // For interpolating between two complex colors, we need to + // generate colors with effective alpha value. + let self_color = self.effective_intermediate_rgba(); + let other_color = other.effective_intermediate_rgba(); + let color = self_color.animate(&other_color, procedure)?; + // Then we compute the final background ratio, and derive + // the final alpha value from the effective alpha value. + let self_ratios = self.effective_ratios(); + let other_ratios = other.effective_ratios(); + let ratios = self_ratios.animate(&other_ratios, procedure)?; + let alpha = color.alpha / ratios.bg; + let color = RGBA { alpha, ..color }; + + if ratios == ComplexColorRatios::NUMERIC { + Color::Numeric(color) + } else if ratios == ComplexColorRatios::FOREGROUND { + Color::Foreground + } else { + Color::Complex(color, ratios) + } + } + }) } } @@ -187,27 +213,26 @@ impl ComputeSquaredDistance for Color { #[inline] fn compute_squared_distance(&self, other: &Self) -> Result { // All comments from the Animate impl also applies here. - if self.foreground_ratio == other.foreground_ratio { - if self.is_currentcolor() { - Ok(SquaredDistance::from_sqrt(0.)) - } else { - self.color.compute_squared_distance(&other.color) + Ok(match (*self, *other) { + (Color::Foreground, Color::Foreground) => SquaredDistance::from_sqrt(0.), + (Color::Numeric(c1), Color::Numeric(c2)) => c1.compute_squared_distance(&c2)?, + (Color::Foreground, Color::Numeric(color)) + | (Color::Numeric(color), Color::Foreground) => { + // `computed_squared_distance` is symmetic. + color.compute_squared_distance(&RGBA::transparent())? + + SquaredDistance::from_sqrt(1.) } - } else if self.is_currentcolor() && other.is_numeric() { - Ok( - RGBA::transparent().compute_squared_distance(&other.color)? + - SquaredDistance::from_sqrt(1.), - ) - } else if self.is_numeric() && other.is_currentcolor() { - Ok(self.color.compute_squared_distance(&RGBA::transparent())? + - SquaredDistance::from_sqrt(1.)) - } else { - let self_color = self.effective_intermediate_rgba(); - let other_color = other.effective_intermediate_rgba(); - Ok(self_color.compute_squared_distance(&other_color)? + - self.foreground_ratio - .compute_squared_distance(&other.foreground_ratio)?) - } + (_, _) => { + let self_color = self.effective_intermediate_rgba(); + let other_color = other.effective_intermediate_rgba(); + let self_ratios = self.effective_ratios(); + let other_ratios = other.effective_ratios(); + + self_color.compute_squared_distance(&other_color)? + + self_ratios.bg.compute_squared_distance(&other_ratios.bg)? + + self_ratios.fg.compute_squared_distance(&other_ratios.fg)? + } + }) } } diff --git a/components/style/values/computed/color.rs b/components/style/values/computed/color.rs index d37aef00b9d..d5fff086759 100644 --- a/components/style/values/computed/color.rs +++ b/components/style/values/computed/color.rs @@ -10,17 +10,36 @@ use style_traits::{CssWriter, ToCss}; use values::animated::ToAnimatedValue; use values::animated::color::{Color as AnimatedColor, RGBA as AnimatedRGBA}; -/// This struct represents a combined color from a numeric color and -/// the current foreground color (currentcolor keyword). -/// Conceptually, the formula is "color * (1 - p) + currentcolor * p" -/// where p is foreground_ratio. -#[derive(Clone, Copy, Debug, MallocSizeOf)] -pub struct Color { - /// RGBA color. - pub color: RGBA, +/// Ratios representing the contribution of color and currentcolor to +/// the final color value. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)] +pub struct ComplexColorRatios { + /// Numeric color contribution. + pub bg: f32, + /// Foreground color, aka currentcolor, contribution. + pub fg: f32, +} - /// The ratio of currentcolor in complex color. - pub foreground_ratio: u8, +impl ComplexColorRatios { + /// Ratios representing pure numeric color. + pub const NUMERIC: ComplexColorRatios = ComplexColorRatios { bg: 1., fg: 0. }; + /// Ratios representing pure foreground color. + pub const FOREGROUND: ComplexColorRatios = ComplexColorRatios { bg: 0., fg: 1. }; +} + +/// This enum represents a combined color from a numeric color and +/// the current foreground color (currentColor keyword). +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)] +pub enum Color { + /// Numeric RGBA color. + Numeric(RGBA), + + /// The current foreground color. + Foreground, + + /// A linear combination of numeric color and currentColor. + /// The formula is: `color * bg_ratio + currentColor * fg_ratio`. + Complex(RGBA, ComplexColorRatios), } /// Computed value type for the specified RGBAColor. @@ -31,11 +50,8 @@ pub type ColorPropertyValue = RGBA; impl Color { /// Returns a numeric color representing the given RGBA value. - pub fn rgba(rgba: RGBA) -> Color { - Color { - color: rgba, - foreground_ratio: 0, - } + pub fn rgba(color: RGBA) -> Color { + Color::Numeric(color) } /// Returns a complex color value representing transparent. @@ -45,73 +61,53 @@ impl Color { /// Returns a complex color value representing currentcolor. pub fn currentcolor() -> Color { - Color { - color: RGBA::transparent(), - foreground_ratio: u8::max_value(), - } + Color::Foreground } /// Whether it is a numeric color (no currentcolor component). pub fn is_numeric(&self) -> bool { - self.foreground_ratio == 0 + matches!(*self, Color::Numeric { .. }) } /// Whether it is a currentcolor value (no numeric color component). pub fn is_currentcolor(&self) -> bool { - self.foreground_ratio == u8::max_value() + matches!(*self, Color::Foreground) } /// Combine this complex color with the given foreground color into /// a numeric RGBA color. It currently uses linear blending. pub fn to_rgba(&self, fg_color: RGBA) -> RGBA { - // Common cases that the complex color is either pure numeric - // color or pure currentcolor. - if self.is_numeric() { - return self.color; - } - if self.is_currentcolor() { - return fg_color.clone(); - } - - fn blend_color_component(bg: u8, fg: u8, fg_alpha: u8) -> u8 { - let bg_ratio = (u8::max_value() - fg_alpha) as u32; - let fg_ratio = fg_alpha as u32; - let color = bg as u32 * bg_ratio + fg as u32 * fg_ratio; - // Rounding divide the number by 255 - ((color + 127) / 255) as u8 - } - - // Common case that alpha channel is equal (usually both are opaque). - let fg_ratio = self.foreground_ratio; - if self.color.alpha == fg_color.alpha { - let r = blend_color_component(self.color.red, fg_color.red, fg_ratio); - let g = blend_color_component(self.color.green, fg_color.green, fg_ratio); - let b = blend_color_component(self.color.blue, fg_color.blue, fg_ratio); - return RGBA::new(r, g, b, fg_color.alpha); - } + let (color, ratios) = match *self { + // Common cases that the complex color is either pure numeric + // color or pure currentcolor. + Color::Numeric(color) => return color, + Color::Foreground => return fg_color, + Color::Complex(color, ratios) => (color, ratios), + }; // For the more complicated case that the alpha value differs, // we use the following formula to compute the components: - // alpha = self_alpha * (1 - fg_ratio) + fg_alpha * fg_ratio - // color = (self_color * self_alpha * (1 - fg_ratio) + + // alpha = self_alpha * bg_ratio + fg_alpha * fg_ratio + // color = (self_color * self_alpha * bg_ratio + // fg_color * fg_alpha * fg_ratio) / alpha - let p1 = (1. / 255.) * (255 - fg_ratio) as f32; - let a1 = self.color.alpha_f32(); - let r1 = a1 * self.color.red_f32(); - let g1 = a1 * self.color.green_f32(); - let b1 = a1 * self.color.blue_f32(); + let p1 = ratios.bg; + let a1 = color.alpha_f32(); + let r1 = a1 * color.red_f32(); + let g1 = a1 * color.green_f32(); + let b1 = a1 * color.blue_f32(); - let p2 = 1. - p1; + let p2 = ratios.fg; let a2 = fg_color.alpha_f32(); let r2 = a2 * fg_color.red_f32(); let g2 = a2 * fg_color.green_f32(); let b2 = a2 * fg_color.blue_f32(); let a = p1 * a1 + p2 * a2; - if a == 0.0 { + if a <= 0. { return RGBA::transparent(); } + let a = f32::min(a, 1.); let inverse_a = 1. / a; let r = (p1 * r1 + p2 * r2) * inverse_a; @@ -121,19 +117,9 @@ impl Color { } } -impl PartialEq for Color { - fn eq(&self, other: &Color) -> bool { - self.foreground_ratio == other.foreground_ratio && - (self.is_currentcolor() || self.color == other.color) - } -} - impl From for Color { fn from(color: RGBA) -> Color { - Color { - color: color, - foreground_ratio: 0, - } + Color::Numeric(color) } } @@ -142,12 +128,10 @@ impl ToCss for Color { where W: fmt::Write, { - if self.is_numeric() { - self.color.to_css(dest) - } else if self.is_currentcolor() { - CSSParserColor::CurrentColor.to_css(dest) - } else { - Ok(()) + match *self { + Color::Numeric(color) => color.to_css(dest), + Color::Foreground => CSSParserColor::CurrentColor.to_css(dest), + _ => Ok(()), } } } @@ -157,17 +141,23 @@ impl ToAnimatedValue for Color { #[inline] fn to_animated_value(self) -> Self::AnimatedValue { - AnimatedColor { - color: self.color.to_animated_value(), - foreground_ratio: self.foreground_ratio as f32 * (1. / 255.), + match self { + Color::Numeric(color) => AnimatedColor::Numeric(color.to_animated_value()), + Color::Foreground => AnimatedColor::Foreground, + Color::Complex(color, ratios) => { + AnimatedColor::Complex(color.to_animated_value(), ratios) + } } } #[inline] fn from_animated_value(animated: Self::AnimatedValue) -> Self { - Color { - color: RGBA::from_animated_value(animated.color), - foreground_ratio: (animated.foreground_ratio * 255.).round() as u8, + match animated { + AnimatedColor::Numeric(color) => Color::Numeric(RGBA::from_animated_value(color)), + AnimatedColor::Foreground => Color::Foreground, + AnimatedColor::Complex(color, ratios) => { + Color::Complex(RGBA::from_animated_value(color), ratios) + } } } } diff --git a/components/style/values/computed/mod.rs b/components/style/values/computed/mod.rs index e742c4e49f1..fc1d617a02e 100644 --- a/components/style/values/computed/mod.rs +++ b/components/style/values/computed/mod.rs @@ -45,7 +45,7 @@ pub use self::font::{MozScriptLevel, MozScriptMinSize, MozScriptSizeMultiplier, pub use self::box_::{AnimationIterationCount, AnimationName, Contain, Display, TransitionProperty}; pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective}; pub use self::box_::{ScrollSnapType, TouchAction, VerticalAlign, WillChange}; -pub use self::color::{Color, ColorPropertyValue, RGBAColor}; +pub use self::color::{Color, ColorPropertyValue, ComplexColorRatios, RGBAColor}; pub use self::column::ColumnCount; pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset}; pub use self::effects::{BoxShadow, Filter, SimpleShadow}; diff --git a/components/style/values/specified/color.rs b/components/style/values/specified/color.rs index 3066c5b8c2a..0670e8126ec 100644 --- a/components/style/values/specified/color.rs +++ b/components/style/values/specified/color.rs @@ -88,11 +88,11 @@ impl<'a, 'b: 'a, 'i: 'a> ::cssparser::ColorComponentParser<'i> for ColorComponen }; Ok(AngleOrNumber::Angle { degrees }) - }, + } Token::Number { value, .. } => Ok(AngleOrNumber::Number { value }), Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => { input.parse_nested_block(|i| CalcNode::parse_angle_or_number(self.0, i)) - }, + } t => return Err(location.new_unexpected_token_error(t)), } } @@ -119,10 +119,10 @@ impl<'a, 'b: 'a, 'i: 'a> ::cssparser::ColorComponentParser<'i> for ColorComponen Token::Number { value, .. } => Ok(NumberOrPercentage::Number { value }), Token::Percentage { unit_value, .. } => { Ok(NumberOrPercentage::Percentage { unit_value }) - }, + } Token::Function(ref name) if name.eq_ignore_ascii_case("calc") => { input.parse_nested_block(|i| CalcNode::parse_number_or_percentage(self.0, i)) - }, + } t => return Err(location.new_unexpected_token_error(t)), } } @@ -168,10 +168,10 @@ impl Parse for Color { Err(e.location.new_custom_error(StyleParseErrorKind::ValueError( ValueParseErrorKind::InvalidColor(t), ))) - }, + } _ => Err(e), } - }, + } } } } @@ -275,10 +275,10 @@ impl Color { } return parse_hash_color(ident.as_bytes()) .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); - }, + } ref t => { return Err(location.new_unexpected_token_error(t.clone())); - }, + } }; if value < 0 { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); @@ -358,11 +358,11 @@ impl Color { Keyword::MozVisitedhyperlinktext => pres_context.mVisitedLinkColor, }) }) - }, + } #[cfg(feature = "gecko")] Color::InheritFromBodyQuirk => { _context.map(|context| ComputedColor::rgba(context.device().body_text_color())) - }, + } } } } @@ -372,7 +372,7 @@ impl ToComputedValue for Color { fn to_computed_value(&self, context: &Context) -> ComputedColor { let result = self.to_computed_color(Some(context)).unwrap(); - if result.foreground_ratio != 0 { + if !result.is_numeric() { if let Some(longhand) = context.for_non_inherited_property { if longhand.stores_complex_colors_lossily() { context.rule_cache_conditions.borrow_mut().set_uncacheable(); @@ -383,12 +383,10 @@ impl ToComputedValue for Color { } fn from_computed_value(computed: &ComputedColor) -> Self { - if computed.is_numeric() { - Color::rgba(computed.color) - } else if computed.is_currentcolor() { - Color::currentcolor() - } else { - Color::Complex(*computed) + match *computed { + ComputedColor::Numeric(color) => Color::rgba(color), + ComputedColor::Foreground => Color::currentcolor(), + ComputedColor::Complex(..) => Color::Complex(*computed), } } }