diff options
Diffstat (limited to '')
-rw-r--r-- | servo/components/style/values/animated/color.rs | 841 | ||||
-rw-r--r-- | servo/components/style/values/animated/effects.rs | 27 | ||||
-rw-r--r-- | servo/components/style/values/animated/font.rs | 151 | ||||
-rw-r--r-- | servo/components/style/values/animated/grid.rs | 166 | ||||
-rw-r--r-- | servo/components/style/values/animated/mod.rs | 489 | ||||
-rw-r--r-- | servo/components/style/values/animated/svg.rs | 45 | ||||
-rw-r--r-- | servo/components/style/values/animated/transform.rs | 1484 |
7 files changed, 3203 insertions, 0 deletions
diff --git a/servo/components/style/values/animated/color.rs b/servo/components/style/values/animated/color.rs new file mode 100644 index 0000000000..ac3a2ad2d3 --- /dev/null +++ b/servo/components/style/values/animated/color.rs @@ -0,0 +1,841 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated types for CSS colors. + +use crate::values::animated::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::Percentage; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::color::{ + ColorInterpolationMethod, ColorSpace, GenericColor, GenericColorMix, HueInterpolationMethod, +}; +use euclid::default::{Transform3D, Vector3D}; +use std::f32::consts::PI; + +/// An animated RGBA color. +/// +/// Unlike in computed values, each component value may exceed the +/// range `[0.0, 1.0]`. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedZero, ToAnimatedValue)] +#[repr(C)] +pub struct AnimatedRGBA { + /// The red component. + pub red: f32, + /// The green component. + pub green: f32, + /// The blue component. + pub blue: f32, + /// The alpha component. + pub alpha: f32, +} + +use self::AnimatedRGBA as RGBA; + +const RAD_PER_DEG: f32 = PI / 180.0; +const DEG_PER_RAD: f32 = 180.0 / PI; + +impl RGBA { + /// Returns a transparent color. + #[inline] + pub fn transparent() -> Self { + Self::new(0., 0., 0., 0.) + } + + /// Returns a new color. + #[inline] + pub fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self { + RGBA { + red, + green, + blue, + alpha, + } + } +} + +impl Animate for RGBA { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let (left_weight, right_weight) = procedure.weights(); + Ok(Color::mix( + &ColorInterpolationMethod::srgb(), + self, + left_weight as f32, + other, + right_weight as f32, + /* normalize_weights = */ false, + )) + } +} + +impl ComputeSquaredDistance for RGBA { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let start = [ + self.alpha, + self.red * self.alpha, + self.green * self.alpha, + self.blue * self.alpha, + ]; + let end = [ + other.alpha, + other.red * other.alpha, + other.green * other.alpha, + other.blue * other.alpha, + ]; + start + .iter() + .zip(&end) + .map(|(this, other)| this.compute_squared_distance(other)) + .sum() + } +} + +/// An animated value for `<color>`. +pub type Color = GenericColor<RGBA, Percentage>; + +/// An animated value for `<color-mix>`. +pub type ColorMix = GenericColorMix<Color, Percentage>; + +impl Color { + fn to_rgba(&self, current_color: RGBA) -> RGBA { + let mut clone = self.clone(); + clone.simplify(Some(¤t_color)); + *clone.as_numeric().unwrap() + } + + /// Mix two colors into one. + pub fn mix( + interpolation: &ColorInterpolationMethod, + left_color: &RGBA, + mut left_weight: f32, + right_color: &RGBA, + mut right_weight: f32, + normalize_weights: bool, + ) -> RGBA { + // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm + let mut alpha_multiplier = 1.0; + if normalize_weights { + let sum = left_weight + right_weight; + if sum != 1.0 { + let scale = 1.0 / sum; + left_weight *= scale; + right_weight *= scale; + if sum < 1.0 { + alpha_multiplier = sum; + } + } + } + + let mix_function = match interpolation.space { + ColorSpace::Srgb => Self::mix_in::<RGBA>, + ColorSpace::LinearSrgb => Self::mix_in::<LinearRGBA>, + ColorSpace::Xyz => Self::mix_in::<XYZD65A>, + ColorSpace::XyzD50 => Self::mix_in::<XYZD50A>, + ColorSpace::Lab => Self::mix_in::<LABA>, + ColorSpace::Hwb => Self::mix_in::<HWBA>, + ColorSpace::Hsl => Self::mix_in::<HSLA>, + ColorSpace::Lch => Self::mix_in::<LCHA>, + }; + mix_function( + left_color, + left_weight, + right_color, + right_weight, + interpolation.hue, + alpha_multiplier, + ) + } + + fn mix_in<S>( + left_color: &RGBA, + left_weight: f32, + right_color: &RGBA, + right_weight: f32, + hue_interpolation: HueInterpolationMethod, + alpha_multiplier: f32, + ) -> RGBA + where + S: ModelledColor, + { + let left = S::from(*left_color); + let right = S::from(*right_color); + + let color = S::lerp(&left, left_weight, &right, right_weight, hue_interpolation); + let mut rgba = RGBA::from(color.into()); + if alpha_multiplier != 1.0 { + rgba.alpha *= alpha_multiplier; + } + + rgba + } +} + +impl Animate for Color { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let (left_weight, right_weight) = procedure.weights(); + let mut color = Color::ColorMix(Box::new(ColorMix { + interpolation: ColorInterpolationMethod::srgb(), + left: self.clone(), + left_percentage: Percentage(left_weight as f32), + right: other.clone(), + right_percentage: Percentage(right_weight as f32), + // See https://github.com/w3c/csswg-drafts/issues/7324 + normalize_weights: false, + })); + color.simplify(None); + Ok(color) + } +} + +impl ComputeSquaredDistance for Color { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let current_color = RGBA::transparent(); + self.to_rgba(current_color) + .compute_squared_distance(&other.to_rgba(current_color)) + } +} + +impl ToAnimatedZero for Color { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(Color::rgba(RGBA::transparent())) + } +} + +/// A color modelled in a specific color space (such as sRGB or CIE XYZ). +/// +/// For now, colors modelled in other spaces need to be convertible to and from +/// `RGBA` because we use sRGB for displaying colors. +trait ModelledColor: Clone + Copy + From<RGBA> + Into<RGBA> { + /// Linearly interpolate between the left and right colors. + /// + /// The HueInterpolationMethod parameter is only for color spaces where the hue is + /// represented as an angle (e.g., CIE LCH). + fn lerp( + left_bg: &Self, + left_weight: f32, + right_bg: &Self, + right_weight: f32, + hue_interpolation: HueInterpolationMethod, + ) -> Self; +} + +fn interpolate_premultiplied_component( + left: f32, + left_weight: f32, + left_alpha: f32, + right: f32, + right_weight: f32, + right_alpha: f32, + inverse_of_result_alpha: f32, +) -> f32 { + (left * left_weight * left_alpha + right * right_weight * right_alpha) * inverse_of_result_alpha +} + +// Normalize hue into [0, 360) +fn normalize_hue(v: f32) -> f32 { + v - 360. * (v / 360.).floor() +} + +fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) { + // Adjust the hue angle as per + // https://drafts.csswg.org/css-color/#hue-interpolation. + // + // If both hue angles are NAN, they should be set to 0. Otherwise, if a + // single hue angle is NAN, it should use the other hue angle. + if left.is_nan() { + if right.is_nan() { + *left = 0.; + *right = 0.; + } else { + *left = *right; + } + } else if right.is_nan() { + *right = *left; + } + + if hue_interpolation == HueInterpolationMethod::Specified { + // Angles are not adjusted. They are interpolated like any other + // component. + return; + } + + *left = normalize_hue(*left); + *right = normalize_hue(*right); + + match hue_interpolation { + // https://drafts.csswg.org/css-color/#shorter + HueInterpolationMethod::Shorter => { + let delta = *right - *left; + + if delta > 180. { + *left += 360.; + } else if delta < -180. { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#longer + HueInterpolationMethod::Longer => { + let delta = *right - *left; + if 0. < delta && delta < 180. { + *left += 360.; + } else if -180. < delta && delta < 0. { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#increasing + HueInterpolationMethod::Increasing => { + if *right < *left { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#decreasing + HueInterpolationMethod::Decreasing => { + if *left < *right { + *left += 360.; + } + }, + HueInterpolationMethod::Specified => unreachable!("Handled above"), + } +} + +fn interpolate_hue( + mut left: f32, + left_weight: f32, + mut right: f32, + right_weight: f32, + hue_interpolation: HueInterpolationMethod, +) -> f32 { + adjust_hue(&mut left, &mut right, hue_interpolation); + left * left_weight + right * right_weight +} + +fn interpolate_premultiplied( + left: &[f32; 4], + left_weight: f32, + right: &[f32; 4], + right_weight: f32, + hue_index: Option<usize>, + hue_interpolation: HueInterpolationMethod, +) -> [f32; 4] { + let left_alpha = left[3]; + let right_alpha = right[3]; + let result_alpha = (left_alpha * left_weight + right_alpha * right_weight).min(1.); + let mut result = [0.; 4]; + if result_alpha <= 0. { + return result; + } + + let inverse_of_result_alpha = 1. / result_alpha; + for i in 0..3 { + let is_hue = hue_index == Some(i); + result[i] = if is_hue { + interpolate_hue( + left[i], + left_weight, + right[i], + right_weight, + hue_interpolation, + ) + } else { + interpolate_premultiplied_component( + left[i], + left_weight, + left_alpha, + right[i], + right_weight, + right_alpha, + inverse_of_result_alpha, + ) + }; + } + result[3] = result_alpha; + + result +} + +macro_rules! impl_lerp { + ($ty:ident, $hue_index:expr) => { + // These ensure the transmutes below are sound. + const_assert_eq!(std::mem::size_of::<$ty>(), std::mem::size_of::<f32>() * 4); + const_assert_eq!(std::mem::align_of::<$ty>(), std::mem::align_of::<f32>()); + impl ModelledColor for $ty { + fn lerp( + left: &Self, + left_weight: f32, + right: &Self, + right_weight: f32, + hue_interpolation: HueInterpolationMethod, + ) -> Self { + use std::mem::transmute; + unsafe { + transmute::<[f32; 4], Self>(interpolate_premultiplied( + transmute::<&Self, &[f32; 4]>(left), + left_weight, + transmute::<&Self, &[f32; 4]>(right), + right_weight, + $hue_index, + hue_interpolation, + )) + } + } + } + }; +} + +impl_lerp!(RGBA, None); + +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct LinearRGBA { + red: f32, + green: f32, + blue: f32, + alpha: f32, +} + +impl_lerp!(LinearRGBA, None); + +/// An animated XYZ D65 colour. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct XYZD65A { + x: f32, + y: f32, + z: f32, + alpha: f32, +} + +impl_lerp!(XYZD65A, None); + +/// An animated XYZ D50 colour. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct XYZD50A { + x: f32, + y: f32, + z: f32, + alpha: f32, +} + +impl_lerp!(XYZD50A, None); + +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct LABA { + lightness: f32, + a: f32, + b: f32, + alpha: f32, +} + +impl_lerp!(LABA, None); + +/// An animated LCHA colour. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct LCHA { + lightness: f32, + chroma: f32, + hue: f32, + alpha: f32, +} + +impl_lerp!(LCHA, Some(2)); + +/// An animated hwb() color. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct HWBA { + hue: f32, + white: f32, + black: f32, + alpha: f32, +} + +impl_lerp!(HWBA, Some(0)); + +#[derive(Clone, Copy, Debug)] +#[repr(C)] +struct HSLA { + hue: f32, + sat: f32, + light: f32, + alpha: f32, +} + +impl_lerp!(HSLA, Some(0)); + +// https://drafts.csswg.org/css-color/#rgb-to-hsl +// +// We also return min/max for the hwb conversion. +fn rgb_to_hsl(rgba: RGBA) -> (HSLA, f32, f32) { + let RGBA { + red, + green, + blue, + alpha, + } = rgba; + let max = red.max(green).max(blue); + let min = red.min(green).min(blue); + let mut hue = std::f32::NAN; + let mut sat = 0.; + let light = (min + max) / 2.; + let d = max - min; + + if d != 0. { + sat = if light == 0.0 || light == 1.0 { + 0. + } else { + (max - light) / light.min(1. - light) + }; + + if max == red { + hue = (green - blue) / d + if green < blue { 6. } else { 0. } + } else if max == green { + hue = (blue - red) / d + 2.; + } else { + hue = (red - green) / d + 4.; + } + + hue *= 60.; + } + + ( + HSLA { + hue, + sat, + light, + alpha, + }, + min, + max, + ) +} + +impl From<RGBA> for HSLA { + fn from(rgba: RGBA) -> Self { + rgb_to_hsl(rgba).0 + } +} + +impl From<HSLA> for RGBA { + fn from(hsla: HSLA) -> Self { + // cssparser expects hue in the 0..1 range. + let hue_normalized = normalize_hue(hsla.hue) / 360.; + let (r, g, b) = cssparser::hsl_to_rgb(hue_normalized, hsla.sat, hsla.light); + RGBA::new(r, g, b, hsla.alpha) + } +} + +impl From<RGBA> for HWBA { + // https://drafts.csswg.org/css-color/#rgb-to-hwb + fn from(rgba: RGBA) -> Self { + let (hsl, min, max) = rgb_to_hsl(rgba); + Self { + hue: hsl.hue, + white: min, + black: 1. - max, + alpha: rgba.alpha, + } + } +} + +impl From<HWBA> for RGBA { + fn from(hwba: HWBA) -> Self { + let hue_normalized = normalize_hue(hwba.hue) / 360.; + let (r, g, b) = cssparser::hwb_to_rgb(hue_normalized, hwba.white, hwba.black); + RGBA::new(r, g, b, hwba.alpha) + } +} + +impl From<RGBA> for LinearRGBA { + fn from(rgba: RGBA) -> Self { + fn linearize(value: f32) -> f32 { + let sign = if value < 0. { -1. } else { 1. }; + let abs = value.abs(); + if abs < 0.04045 { + return value / 12.92; + } + + sign * ((abs + 0.055) / 1.055).powf(2.4) + } + Self { + red: linearize(rgba.red), + green: linearize(rgba.green), + blue: linearize(rgba.blue), + alpha: rgba.alpha, + } + } +} + +impl From<LinearRGBA> for RGBA { + fn from(lrgba: LinearRGBA) -> Self { + fn delinearize(value: f32) -> f32 { + let sign = if value < 0. { -1. } else { 1. }; + let abs = value.abs(); + + if abs > 0.0031308 { + sign * (1.055 * abs.powf(1. / 2.4) - 0.055) + } else { + 12.92 * value + } + } + Self { + red: delinearize(lrgba.red), + green: delinearize(lrgba.green), + blue: delinearize(lrgba.blue), + alpha: lrgba.alpha, + } + } +} + +impl From<XYZD65A> for XYZD50A { + fn from(d65: XYZD65A) -> Self { + // https://drafts.csswg.org/css-color-4/#color-conversion-code + #[cfg_attr(rustfmt, rustfmt_skip)] + const BRADFORD: Transform3D<f32> = Transform3D::new( + 1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0., + 0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0., + -0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0., + 0., 0., 0., 1., + ); + let d50 = BRADFORD.transform_vector3d(Vector3D::new(d65.x, d65.y, d65.z)); + Self { + x: d50.x, + y: d50.y, + z: d50.z, + alpha: d65.alpha, + } + } +} + +impl From<XYZD50A> for XYZD65A { + fn from(d50: XYZD50A) -> Self { + // https://drafts.csswg.org/css-color-4/#color-conversion-code + #[cfg_attr(rustfmt, rustfmt_skip)] + const BRADFORD_INVERSE: Transform3D<f32> = Transform3D::new( + 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0., + -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0., + 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0., + 0., 0., 0., 1., + ); + let d65 = BRADFORD_INVERSE.transform_vector3d(Vector3D::new(d50.x, d50.y, d50.z)); + Self { + x: d65.x, + y: d65.y, + z: d65.z, + alpha: d50.alpha, + } + } +} + +impl From<LinearRGBA> for XYZD65A { + fn from(lrgba: LinearRGBA) -> Self { + // https://drafts.csswg.org/css-color-4/#color-conversion-code + #[cfg_attr(rustfmt, rustfmt_skip)] + const LSRGB_TO_XYZ: Transform3D<f32> = Transform3D::new( + 0.41239079926595934, 0.21263900587151027, 0.01933081871559182, 0., + 0.357584339383878, 0.715168678767756, 0.11919477979462598, 0., + 0.1804807884018343, 0.07219231536073371, 0.9505321522496607, 0., + 0., 0., 0., 1., + ); + let linear_rgb = Vector3D::new(lrgba.red, lrgba.green, lrgba.blue); + let xyz = LSRGB_TO_XYZ.transform_vector3d(linear_rgb); + Self { + x: xyz.x, + y: xyz.y, + z: xyz.z, + alpha: lrgba.alpha, + } + } +} + +impl From<XYZD65A> for LinearRGBA { + fn from(d65: XYZD65A) -> Self { + // https://drafts.csswg.org/css-color-4/#color-conversion-code + #[cfg_attr(rustfmt, rustfmt_skip)] + const XYZ_TO_LSRGB: Transform3D<f32> = Transform3D::new( + 3.2409699419045226, -0.9692436362808796, 0.05563007969699366, 0., + -1.537383177570094, 1.8759675015077202, -0.20397695888897652, 0., + -0.4986107602930034, 0.04155505740717559, 1.0569715142428786, 0., + 0., 0., 0., 1., + ); + + let xyz = Vector3D::new(d65.x, d65.y, d65.z); + let rgb = XYZ_TO_LSRGB.transform_vector3d(xyz); + Self { + red: rgb.x, + green: rgb.y, + blue: rgb.z, + alpha: d65.alpha, + } + } +} + +impl From<XYZD65A> for RGBA { + fn from(d65: XYZD65A) -> Self { + Self::from(LinearRGBA::from(d65)) + } +} + +impl From<RGBA> for XYZD65A { + /// Convert an RGBA colour to XYZ as specified in [1]. + /// + /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab + fn from(rgba: RGBA) -> Self { + Self::from(LinearRGBA::from(rgba)) + } +} + +impl From<XYZD50A> for LABA { + /// Convert an XYZ colour to LAB as specified in [1] and [2]. + /// + /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab + /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code + fn from(xyza: XYZD50A) -> Self { + const WHITE: [f32; 3] = [0.96422, 1., 0.82521]; + + fn compute_f(value: f32) -> f32 { + const EPSILON: f32 = 216. / 24389.; + const KAPPA: f32 = 24389. / 27.; + + if value > EPSILON { + value.cbrt() + } else { + (KAPPA * value + 16.) / 116. + } + } + + // 4. Convert D50-adapted XYZ to Lab. + let f = [ + compute_f(xyza.x / WHITE[0]), + compute_f(xyza.y / WHITE[1]), + compute_f(xyza.z / WHITE[2]), + ]; + + let lightness = 116. * f[1] - 16.; + let a = 500. * (f[0] - f[1]); + let b = 200. * (f[1] - f[2]); + + LABA { + lightness, + a, + b, + alpha: xyza.alpha, + } + } +} + +impl From<LABA> for LCHA { + /// Convert a LAB color to LCH as specified in [1]. + /// + /// [1]: https://drafts.csswg.org/css-color/#color-conversion-code + fn from(laba: LABA) -> Self { + let hue = laba.b.atan2(laba.a) * DEG_PER_RAD; + let chroma = (laba.a * laba.a + laba.b * laba.b).sqrt(); + LCHA { + lightness: laba.lightness, + chroma, + hue, + alpha: laba.alpha, + } + } +} + +impl From<LCHA> for LABA { + /// Convert a LCH color to LAB as specified in [1]. + /// + /// [1]: https://drafts.csswg.org/css-color/#color-conversion-code + fn from(lcha: LCHA) -> Self { + let hue_radians = lcha.hue * RAD_PER_DEG; + let a = lcha.chroma * hue_radians.cos(); + let b = lcha.chroma * hue_radians.sin(); + LABA { + lightness: lcha.lightness, + a, + b, + alpha: lcha.alpha, + } + } +} + +impl From<LABA> for XYZD50A { + /// Convert a CIELAB color to XYZ as specified in [1] and [2]. + /// + /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined + /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code + fn from(laba: LABA) -> Self { + // 1. Convert LAB to (D50-adapated) XYZ. + const KAPPA: f32 = 24389. / 27.; + const EPSILON: f32 = 216. / 24389.; + const WHITE: [f32; 3] = [0.96422, 1., 0.82521]; + + let f1 = (laba.lightness + 16f32) / 116f32; + let f0 = (laba.a / 500.) + f1; + let f2 = f1 - laba.b / 200.; + + let x = if f0.powf(3.) > EPSILON { + f0.powf(3.) + } else { + (116. * f0 - 16.) / KAPPA + }; + let y = if laba.lightness > KAPPA * EPSILON { + ((laba.lightness + 16.) / 116.).powf(3.) + } else { + laba.lightness / KAPPA + }; + let z = if f2.powf(3.) > EPSILON { + f2.powf(3.) + } else { + (116. * f2 - 16.) / KAPPA + }; + + Self { + x: x * WHITE[0], + y: y * WHITE[1], + z: z * WHITE[2], + alpha: laba.alpha, + } + } +} + +impl From<XYZD50A> for RGBA { + fn from(d50: XYZD50A) -> Self { + Self::from(XYZD65A::from(d50)) + } +} + +impl From<RGBA> for XYZD50A { + fn from(rgba: RGBA) -> Self { + Self::from(XYZD65A::from(rgba)) + } +} + +impl From<RGBA> for LABA { + fn from(rgba: RGBA) -> Self { + Self::from(XYZD50A::from(rgba)) + } +} + +impl From<LABA> for RGBA { + fn from(laba: LABA) -> Self { + Self::from(XYZD50A::from(laba)) + } +} + +impl From<RGBA> for LCHA { + fn from(rgba: RGBA) -> Self { + Self::from(LABA::from(rgba)) + } +} + +impl From<LCHA> for RGBA { + fn from(lcha: LCHA) -> Self { + Self::from(LABA::from(lcha)) + } +} diff --git a/servo/components/style/values/animated/effects.rs b/servo/components/style/values/animated/effects.rs new file mode 100644 index 0000000000..67557e54b7 --- /dev/null +++ b/servo/components/style/values/animated/effects.rs @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated types for CSS values related to effects. + +use crate::values::animated::color::Color; +use crate::values::computed::length::Length; +#[cfg(feature = "gecko")] +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{Angle, Number}; +use crate::values::generics::effects::Filter as GenericFilter; +use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow; +#[cfg(not(feature = "gecko"))] +use crate::values::Impossible; + +/// An animated value for the `drop-shadow()` filter. +pub type AnimatedSimpleShadow = GenericSimpleShadow<Color, Length, Length>; + +/// An animated value for a single `filter`. +#[cfg(feature = "gecko")] +pub type AnimatedFilter = + GenericFilter<Angle, Number, Number, Length, AnimatedSimpleShadow, ComputedUrl>; + +/// An animated value for a single `filter`. +#[cfg(not(feature = "gecko"))] +pub type AnimatedFilter = GenericFilter<Angle, Number, Number, Length, Impossible, Impossible>; diff --git a/servo/components/style/values/animated/font.rs b/servo/components/style/values/animated/font.rs new file mode 100644 index 0000000000..f890a3b2bd --- /dev/null +++ b/servo/components/style/values/animated/font.rs @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animation implementation for various font-related types. + +use super::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::font::FontVariationSettings; +use crate::values::computed::Number; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::font::{FontSettings as GenericFontSettings, FontTag, VariationValue}; + +/// <https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def> +impl Animate for FontVariationSettings { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + FontSettingTagIter::new(self, other)? + .map(|r| r.and_then(|(st, ot)| st.animate(&ot, procedure))) + .collect::<Result<Vec<ComputedVariationValue>, ()>>() + .map(|v| GenericFontSettings(v.into_boxed_slice())) + } +} + +impl ComputeSquaredDistance for FontVariationSettings { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + FontSettingTagIter::new(self, other)? + .map(|r| r.and_then(|(st, ot)| st.compute_squared_distance(&ot))) + .sum() + } +} + +impl ToAnimatedZero for FontVariationSettings { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} + +type ComputedVariationValue = VariationValue<Number>; + +// FIXME: Could do a rename, this is only used for font variations. +struct FontSettingTagIterState<'a> { + tags: Vec<&'a ComputedVariationValue>, + index: usize, + prev_tag: FontTag, +} + +impl<'a> FontSettingTagIterState<'a> { + fn new(tags: Vec<&'a ComputedVariationValue>) -> FontSettingTagIterState<'a> { + FontSettingTagIterState { + index: tags.len(), + tags, + prev_tag: FontTag(0), + } + } +} + +/// Iterator for font-variation-settings tag lists +/// +/// [CSS fonts level 4](https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-variation-settings) +/// defines the animation of font-variation-settings as follows: +/// +/// Two declarations of font-feature-settings[sic] can be animated between if +/// they are "like". "Like" declarations are ones where the same set of +/// properties appear (in any order). Because succesive[sic] duplicate +/// properties are applied instead of prior duplicate properties, two +/// declarations can be "like" even if they have differing number of +/// properties. If two declarations are "like" then animation occurs pairwise +/// between corresponding values in the declarations. +/// +/// In other words if we have the following lists: +/// +/// "wght" 1.4, "wdth" 5, "wght" 2 +/// "wdth" 8, "wght" 4, "wdth" 10 +/// +/// We should animate between: +/// +/// "wdth" 5, "wght" 2 +/// "wght" 4, "wdth" 10 +/// +/// This iterator supports this by sorting the two lists, then iterating them in +/// reverse, and skipping entries with repeated tag names. It will return +/// Some(Err()) if it reaches the end of one list before the other, or if the +/// tag names do not match. +/// +/// For the above example, this iterator would return: +/// +/// Some(Ok("wght" 2, "wght" 4)) +/// Some(Ok("wdth" 5, "wdth" 10)) +/// None +/// +struct FontSettingTagIter<'a> { + a_state: FontSettingTagIterState<'a>, + b_state: FontSettingTagIterState<'a>, +} + +impl<'a> FontSettingTagIter<'a> { + fn new( + a_settings: &'a FontVariationSettings, + b_settings: &'a FontVariationSettings, + ) -> Result<FontSettingTagIter<'a>, ()> { + if a_settings.0.is_empty() || b_settings.0.is_empty() { + return Err(()); + } + + fn as_new_sorted_tags(tags: &[ComputedVariationValue]) -> Vec<&ComputedVariationValue> { + use std::iter::FromIterator; + let mut sorted_tags = Vec::from_iter(tags.iter()); + sorted_tags.sort_by_key(|k| k.tag.0); + sorted_tags + } + + Ok(FontSettingTagIter { + a_state: FontSettingTagIterState::new(as_new_sorted_tags(&a_settings.0)), + b_state: FontSettingTagIterState::new(as_new_sorted_tags(&b_settings.0)), + }) + } + + fn next_tag(state: &mut FontSettingTagIterState<'a>) -> Option<&'a ComputedVariationValue> { + if state.index == 0 { + return None; + } + + state.index -= 1; + let tag = state.tags[state.index]; + if tag.tag == state.prev_tag { + FontSettingTagIter::next_tag(state) + } else { + state.prev_tag = tag.tag; + Some(tag) + } + } +} + +impl<'a> Iterator for FontSettingTagIter<'a> { + type Item = Result<(&'a ComputedVariationValue, &'a ComputedVariationValue), ()>; + + fn next( + &mut self, + ) -> Option<Result<(&'a ComputedVariationValue, &'a ComputedVariationValue), ()>> { + match ( + FontSettingTagIter::next_tag(&mut self.a_state), + FontSettingTagIter::next_tag(&mut self.b_state), + ) { + (Some(at), Some(bt)) if at.tag == bt.tag => Some(Ok((at, bt))), + (None, None) => None, + _ => Some(Err(())), // Mismatch number of unique tags or tag names. + } + } +} diff --git a/servo/components/style/values/animated/grid.rs b/servo/components/style/values/animated/grid.rs new file mode 100644 index 0000000000..7b9417a08c --- /dev/null +++ b/servo/components/style/values/animated/grid.rs @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animation implementation for various grid-related types. + +// Note: we can implement Animate on their generic types directly, but in this case we need to +// make sure two trait bounds, L: Clone and I: PartialEq, are satisfied on almost all the +// grid-related types and their other trait implementations because Animate needs them. So in +// order to avoid adding these two trait bounds (or maybe more..) everywhere, we implement +// Animate for the computed types, instead of the generic types. + +use super::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::Integer; +use crate::values::computed::LengthPercentage; +use crate::values::computed::{GridTemplateComponent, TrackList, TrackSize}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::grid as generics; + +fn discrete<T: Clone>(from: &T, to: &T, procedure: Procedure) -> Result<T, ()> { + if let Procedure::Interpolate { progress } = procedure { + Ok(if progress < 0.5 { + from.clone() + } else { + to.clone() + }) + } else { + Err(()) + } +} + +fn animate_with_discrete_fallback<T: Animate + Clone>( + from: &T, + to: &T, + procedure: Procedure, +) -> Result<T, ()> { + from.animate(to, procedure) + .or_else(|_| discrete(from, to, procedure)) +} + +impl Animate for TrackSize { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self, other) { + (&generics::TrackSize::Breadth(ref from), &generics::TrackSize::Breadth(ref to)) => { + animate_with_discrete_fallback(from, to, procedure) + .map(generics::TrackSize::Breadth) + }, + ( + &generics::TrackSize::Minmax(ref from_min, ref from_max), + &generics::TrackSize::Minmax(ref to_min, ref to_max), + ) => Ok(generics::TrackSize::Minmax( + animate_with_discrete_fallback(from_min, to_min, procedure)?, + animate_with_discrete_fallback(from_max, to_max, procedure)?, + )), + ( + &generics::TrackSize::FitContent(ref from), + &generics::TrackSize::FitContent(ref to), + ) => animate_with_discrete_fallback(from, to, procedure) + .map(generics::TrackSize::FitContent), + (_, _) => discrete(self, other, procedure), + } + } +} + +impl Animate for generics::TrackRepeat<LengthPercentage, Integer> { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + // If the keyword, auto-fit/fill, is the same it can result in different + // number of tracks. For both auto-fit/fill, the number of columns isn't + // known until you do layout since it depends on the container size, item + // placement and other factors, so we cannot do the correct interpolation + // by computed values. Therefore, return Err(()) if it's keywords. If it + // is Number, we support animation only if the count is the same and the + // length of track_sizes is the same. + // https://github.com/w3c/csswg-drafts/issues/3503 + match (&self.count, &other.count) { + (&generics::RepeatCount::Number(from), &generics::RepeatCount::Number(to)) + if from == to => + { + () + }, + (_, _) => return Err(()), + } + + // The length of track_sizes should be matched. + if self.track_sizes.len() != other.track_sizes.len() { + return Err(()); + } + + let count = self.count; + let track_sizes = self + .track_sizes + .iter() + .zip(other.track_sizes.iter()) + .map(|(a, b)| a.animate(b, procedure)) + .collect::<Result<Vec<_>, _>>()?; + + // The length of |line_names| is always 0 or N+1, where N is the length + // of |track_sizes|. Besides, <line-names> is always discrete. + let line_names = discrete(&self.line_names, &other.line_names, procedure)?; + + Ok(generics::TrackRepeat { + count, + line_names, + track_sizes: track_sizes.into(), + }) + } +} + +impl Animate for TrackList { + // Based on https://github.com/w3c/csswg-drafts/issues/3201: + // 1. Check interpolation type per track, so we need to handle discrete animations + // in TrackSize, so any Err(()) returned from TrackSize doesn't make all TrackSize + // fallback to discrete animation. + // 2. line-names is always discrete. + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.values.len() != other.values.len() { + return Err(()); + } + + if self.is_explicit() != other.is_explicit() { + return Err(()); + } + + // For now, repeat(auto-fill/auto-fit, ...) is not animatable. + // TrackRepeat will return Err(()) if we use keywords. Therefore, we can + // early return here to avoid traversing |values| in <auto-track-list>. + // This may be updated in the future. + // https://github.com/w3c/csswg-drafts/issues/3503 + if self.has_auto_repeat() || other.has_auto_repeat() { + return Err(()); + } + + let values = self + .values + .iter() + .zip(other.values.iter()) + .map(|(a, b)| a.animate(b, procedure)) + .collect::<Result<Vec<_>, _>>()?; + // The length of |line_names| is always 0 or N+1, where N is the length + // of |track_sizes|. Besides, <line-names> is always discrete. + let line_names = discrete(&self.line_names, &other.line_names, procedure)?; + + Ok(TrackList { + values: values.into(), + line_names, + auto_repeat_index: self.auto_repeat_index, + }) + } +} + +impl ComputeSquaredDistance for GridTemplateComponent { + #[inline] + fn compute_squared_distance(&self, _other: &Self) -> Result<SquaredDistance, ()> { + // TODO: Bug 1518585, we should implement ComputeSquaredDistance. + Err(()) + } +} + +impl ToAnimatedZero for GridTemplateComponent { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + // It's not clear to get a zero grid track list based on the current definition + // of spec, so we return Err(()) directly. + Err(()) + } +} diff --git a/servo/components/style/values/animated/mod.rs b/servo/components/style/values/animated/mod.rs new file mode 100644 index 0000000000..b36946c492 --- /dev/null +++ b/servo/components/style/values/animated/mod.rs @@ -0,0 +1,489 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated values. +//! +//! Some values, notably colors, cannot be interpolated directly with their +//! computed values and need yet another intermediate representation. This +//! module's raison d'être is to ultimately contain all these types. + +use crate::properties::PropertyId; +use crate::values::computed::length::LengthPercentage; +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::Angle as ComputedAngle; +use crate::values::computed::Image; +use crate::values::specified::SVGPathData; +use crate::values::CSSFloat; +use app_units::Au; +use smallvec::SmallVec; +use std::cmp; + +pub mod color; +pub mod effects; +mod font; +mod grid; +mod svg; +pub mod transform; + +/// The category a property falls into for ordering purposes. +/// +/// https://drafts.csswg.org/web-animations/#calculating-computed-keyframes +#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +enum PropertyCategory { + Custom, + PhysicalLonghand, + LogicalLonghand, + Shorthand, +} + +impl PropertyCategory { + fn of(id: &PropertyId) -> Self { + match *id { + PropertyId::Shorthand(..) | PropertyId::ShorthandAlias(..) => { + PropertyCategory::Shorthand + }, + PropertyId::Longhand(id) | PropertyId::LonghandAlias(id, ..) => { + if id.is_logical() { + PropertyCategory::LogicalLonghand + } else { + PropertyCategory::PhysicalLonghand + } + }, + PropertyId::Custom(..) => PropertyCategory::Custom, + } + } +} + +/// A comparator to sort PropertyIds such that physical longhands are sorted +/// before logical longhands and shorthands, shorthands with fewer components +/// are sorted before shorthands with more components, and otherwise shorthands +/// are sorted by IDL name as defined by [Web Animations][property-order]. +/// +/// Using this allows us to prioritize values specified by longhands (or smaller +/// shorthand subsets) when longhands and shorthands are both specified on the +/// one keyframe. +/// +/// [property-order] https://drafts.csswg.org/web-animations/#calculating-computed-keyframes +pub fn compare_property_priority(a: &PropertyId, b: &PropertyId) -> cmp::Ordering { + let a_category = PropertyCategory::of(a); + let b_category = PropertyCategory::of(b); + + if a_category != b_category { + return a_category.cmp(&b_category); + } + + if a_category != PropertyCategory::Shorthand { + return cmp::Ordering::Equal; + } + + let a = a.as_shorthand().unwrap(); + let b = b.as_shorthand().unwrap(); + // Within shorthands, sort by the number of subproperties, then by IDL + // name. + let subprop_count_a = a.longhands().count(); + let subprop_count_b = b.longhands().count(); + subprop_count_a + .cmp(&subprop_count_b) + .then_with(|| a.idl_name_sort_order().cmp(&b.idl_name_sort_order())) +} + +/// A helper function to animate two multiplicative factor. +pub fn animate_multiplicative_factor( + this: CSSFloat, + other: CSSFloat, + procedure: Procedure, +) -> Result<CSSFloat, ()> { + Ok((this - 1.).animate(&(other - 1.), procedure)? + 1.) +} + +/// Animate from one value to another. +/// +/// This trait is derivable with `#[derive(Animate)]`. The derived +/// implementation uses a `match` expression with identical patterns for both +/// `self` and `other`, calling `Animate::animate` on each fields of the values. +/// If a field is annotated with `#[animation(constant)]`, the two values should +/// be equal or an error is returned. +/// +/// If a variant is annotated with `#[animation(error)]`, the corresponding +/// `match` arm returns an error. +/// +/// Trait bounds for type parameter `Foo` can be opted out of with +/// `#[animation(no_bound(Foo))]` on the type definition, trait bounds for +/// fields can be opted into with `#[animation(field_bound)]` on the field. +pub trait Animate: Sized { + /// Animate a value towards another one, given an animation procedure. + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()>; +} + +/// An animation procedure. +/// +/// <https://drafts.csswg.org/web-animations/#procedures-for-animating-properties> +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Procedure { + /// <https://drafts.csswg.org/web-animations/#animation-interpolation> + Interpolate { progress: f64 }, + /// <https://drafts.csswg.org/web-animations/#animation-addition> + Add, + /// <https://drafts.csswg.org/web-animations/#animation-accumulation> + Accumulate { count: u64 }, +} + +/// Conversion between computed values and intermediate values for animations. +/// +/// Notably, colors are represented as four floats during animations. +/// +/// This trait is derivable with `#[derive(ToAnimatedValue)]`. +pub trait ToAnimatedValue { + /// The type of the animated value. + type AnimatedValue; + + /// Converts this value to an animated value. + fn to_animated_value(self) -> Self::AnimatedValue; + + /// Converts back an animated value into a computed value. + fn from_animated_value(animated: Self::AnimatedValue) -> Self; +} + +/// Returns a value similar to `self` that represents zero. +/// +/// This trait is derivable with `#[derive(ToAnimatedValue)]`. If a field is +/// annotated with `#[animation(constant)]`, a clone of its value will be used +/// instead of calling `ToAnimatedZero::to_animated_zero` on it. +/// +/// If a variant is annotated with `#[animation(error)]`, the corresponding +/// `match` arm is not generated. +/// +/// Trait bounds for type parameter `Foo` can be opted out of with +/// `#[animation(no_bound(Foo))]` on the type definition. +pub trait ToAnimatedZero: Sized { + /// Returns a value that, when added with an underlying value, will produce the underlying + /// value. This is used for SMIL animation's "by-animation" where SMIL first interpolates from + /// the zero value to the 'by' value, and then adds the result to the underlying value. + /// + /// This is not the necessarily the same as the initial value of a property. For example, the + /// initial value of 'stroke-width' is 1, but the zero value is 0, since adding 1 to the + /// underlying value will not produce the underlying value. + fn to_animated_zero(&self) -> Result<Self, ()>; +} + +impl Procedure { + /// Returns this procedure as a pair of weights. + /// + /// This is useful for animations that don't animate differently + /// depending on the used procedure. + #[inline] + pub fn weights(self) -> (f64, f64) { + match self { + Procedure::Interpolate { progress } => (1. - progress, progress), + Procedure::Add => (1., 1.), + Procedure::Accumulate { count } => (count as f64, 1.), + } + } +} + +/// <https://drafts.csswg.org/css-transitions/#animtype-number> +impl Animate for i32 { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(((*self as f64).animate(&(*other as f64), procedure)? + 0.5).floor() as i32) + } +} + +/// <https://drafts.csswg.org/css-transitions/#animtype-number> +impl Animate for f32 { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + use std::f32; + + let ret = (*self as f64).animate(&(*other as f64), procedure)?; + Ok(ret.min(f32::MAX as f64).max(f32::MIN as f64) as f32) + } +} + +/// <https://drafts.csswg.org/css-transitions/#animtype-number> +impl Animate for f64 { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + use std::f64; + + let (self_weight, other_weight) = procedure.weights(); + + let ret = *self * self_weight + *other * other_weight; + Ok(ret.min(f64::MAX).max(f64::MIN)) + } +} + +impl<T> Animate for Option<T> +where + T: Animate, +{ + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self.as_ref(), other.as_ref()) { + (Some(ref this), Some(ref other)) => Ok(Some(this.animate(other, procedure)?)), + (None, None) => Ok(None), + _ => Err(()), + } + } +} + +impl Animate for Au { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(Au::new(self.0.animate(&other.0, procedure)?)) + } +} + +impl<T: Animate> Animate for Box<T> { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(Box::new((**self).animate(&other, procedure)?)) + } +} + +impl<T> ToAnimatedValue for Option<T> +where + T: ToAnimatedValue, +{ + type AnimatedValue = Option<<T as ToAnimatedValue>::AnimatedValue>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.map(T::to_animated_value) + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated.map(T::from_animated_value) + } +} + +impl<T> ToAnimatedValue for Vec<T> +where + T: ToAnimatedValue, +{ + type AnimatedValue = Vec<<T as ToAnimatedValue>::AnimatedValue>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.into_iter().map(T::to_animated_value).collect() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated.into_iter().map(T::from_animated_value).collect() + } +} + +impl<T> ToAnimatedValue for Box<T> +where + T: ToAnimatedValue, +{ + type AnimatedValue = Box<<T as ToAnimatedValue>::AnimatedValue>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + Box::new((*self).to_animated_value()) + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + Box::new(T::from_animated_value(*animated)) + } +} + +impl<T> ToAnimatedValue for Box<[T]> +where + T: ToAnimatedValue, +{ + type AnimatedValue = Box<[<T as ToAnimatedValue>::AnimatedValue]>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.into_vec() + .into_iter() + .map(T::to_animated_value) + .collect::<Vec<_>>() + .into_boxed_slice() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated + .into_vec() + .into_iter() + .map(T::from_animated_value) + .collect::<Vec<_>>() + .into_boxed_slice() + } +} + +impl<T> ToAnimatedValue for crate::OwnedSlice<T> +where + T: ToAnimatedValue, +{ + type AnimatedValue = crate::OwnedSlice<<T as ToAnimatedValue>::AnimatedValue>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.into_box().to_animated_value().into() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + Self::from(Box::from_animated_value(animated.into_box())) + } +} + +impl<T> ToAnimatedValue for SmallVec<[T; 1]> +where + T: ToAnimatedValue, +{ + type AnimatedValue = SmallVec<[T::AnimatedValue; 1]>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.into_iter().map(T::to_animated_value).collect() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated.into_iter().map(T::from_animated_value).collect() + } +} + +macro_rules! trivial_to_animated_value { + ($ty:ty) => { + impl $crate::values::animated::ToAnimatedValue for $ty { + type AnimatedValue = Self; + + #[inline] + fn to_animated_value(self) -> Self { + self + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated + } + } + }; +} + +trivial_to_animated_value!(Au); +trivial_to_animated_value!(LengthPercentage); +trivial_to_animated_value!(ComputedAngle); +trivial_to_animated_value!(ComputedUrl); +trivial_to_animated_value!(bool); +trivial_to_animated_value!(f32); +trivial_to_animated_value!(i32); +// Note: This implementation is for ToAnimatedValue of ShapeSource. +// +// SVGPathData uses Box<[T]>. If we want to derive ToAnimatedValue for all the +// types, we have to do "impl ToAnimatedValue for Box<[T]>" first. +// However, the general version of "impl ToAnimatedValue for Box<[T]>" needs to +// clone |T| and convert it into |T::AnimatedValue|. However, for SVGPathData +// that is unnecessary--moving |T| is sufficient. So here, we implement this +// trait manually. +trivial_to_animated_value!(SVGPathData); +// FIXME: Bug 1514342, Image is not animatable, but we still need to implement +// this to avoid adding this derive to generic::Image and all its arms. We can +// drop this after landing Bug 1514342. +trivial_to_animated_value!(Image); + +impl ToAnimatedZero for Au { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(Au(0)) + } +} + +impl ToAnimatedZero for f32 { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(0.) + } +} + +impl ToAnimatedZero for f64 { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(0.) + } +} + +impl ToAnimatedZero for i32 { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(0) + } +} + +impl<T> ToAnimatedZero for Box<T> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(Box::new((**self).to_animated_zero()?)) + } +} + +impl<T> ToAnimatedZero for Option<T> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + match *self { + Some(ref value) => Ok(Some(value.to_animated_zero()?)), + None => Ok(None), + } + } +} + +impl<T> ToAnimatedZero for Vec<T> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + self.iter().map(|v| v.to_animated_zero()).collect() + } +} + +impl<T> ToAnimatedZero for Box<[T]> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + self.iter().map(|v| v.to_animated_zero()).collect() + } +} + +impl<T> ToAnimatedZero for crate::OwnedSlice<T> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + self.iter().map(|v| v.to_animated_zero()).collect() + } +} + +impl<T> ToAnimatedZero for crate::ArcSlice<T> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + let v = self + .iter() + .map(|v| v.to_animated_zero()) + .collect::<Result<Vec<_>, _>>()?; + Ok(crate::ArcSlice::from_iter(v.into_iter())) + } +} diff --git a/servo/components/style/values/animated/svg.rs b/servo/components/style/values/animated/svg.rs new file mode 100644 index 0000000000..13ad10174b --- /dev/null +++ b/servo/components/style/values/animated/svg.rs @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animation implementations for various SVG-related types. + +use super::{Animate, Procedure}; +use crate::properties::animated_properties::ListAnimation; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::svg::SVGStrokeDashArray; + +/// <https://www.w3.org/TR/SVG11/painting.html#StrokeDasharrayProperty> +impl<L> Animate for SVGStrokeDashArray<L> +where + L: Clone + Animate, +{ + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if matches!(procedure, Procedure::Add | Procedure::Accumulate { .. }) { + // Non-additive. + return Err(()); + } + match (self, other) { + (&SVGStrokeDashArray::Values(ref this), &SVGStrokeDashArray::Values(ref other)) => Ok( + SVGStrokeDashArray::Values(this.animate_repeatable_list(other, procedure)?), + ), + _ => Err(()), + } + } +} + +impl<L> ComputeSquaredDistance for SVGStrokeDashArray<L> +where + L: ComputeSquaredDistance, +{ + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + match (self, other) { + (&SVGStrokeDashArray::Values(ref this), &SVGStrokeDashArray::Values(ref other)) => { + this.squared_distance_repeatable_list(other) + }, + _ => Err(()), + } + } +} diff --git a/servo/components/style/values/animated/transform.rs b/servo/components/style/values/animated/transform.rs new file mode 100644 index 0000000000..bb1b4e910c --- /dev/null +++ b/servo/components/style/values/animated/transform.rs @@ -0,0 +1,1484 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated types for transform. +// There are still some implementation on Matrix3D in animated_properties.mako.rs +// because they still need mako to generate the code. + +use super::animate_multiplicative_factor; +use super::{Animate, Procedure, ToAnimatedZero}; +use crate::properties::animated_properties::ListAnimation; +use crate::values::computed::transform::Rotate as ComputedRotate; +use crate::values::computed::transform::Scale as ComputedScale; +use crate::values::computed::transform::Transform as ComputedTransform; +use crate::values::computed::transform::TransformOperation as ComputedTransformOperation; +use crate::values::computed::transform::Translate as ComputedTranslate; +use crate::values::computed::transform::{DirectionVector, Matrix, Matrix3D}; +use crate::values::computed::Angle; +use crate::values::computed::{Length, LengthPercentage}; +use crate::values::computed::{Number, Percentage}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::transform::{self, Transform, TransformOperation}; +use crate::values::generics::transform::{Rotate, Scale, Translate}; +use crate::values::CSSFloat; +use crate::Zero; +use std::cmp; + +// ------------------------------------ +// Animations for Matrix/Matrix3D. +// ------------------------------------ +/// A 2d matrix for interpolation. +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[allow(missing_docs)] +// FIXME: We use custom derive for ComputeSquaredDistance. However, If possible, we should convert +// the InnerMatrix2D into types with physical meaning. This custom derive computes the squared +// distance from each matrix item, and this makes the result different from that in Gecko if we +// have skew factor in the Matrix3D. +pub struct InnerMatrix2D { + pub m11: CSSFloat, + pub m12: CSSFloat, + pub m21: CSSFloat, + pub m22: CSSFloat, +} + +impl Animate for InnerMatrix2D { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(InnerMatrix2D { + m11: animate_multiplicative_factor(self.m11, other.m11, procedure)?, + m12: self.m12.animate(&other.m12, procedure)?, + m21: self.m21.animate(&other.m21, procedure)?, + m22: animate_multiplicative_factor(self.m22, other.m22, procedure)?, + }) + } +} + +/// A 2d translation function. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)] +pub struct Translate2D(f32, f32); + +/// A 2d scale function. +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Scale2D(f32, f32); + +impl Animate for Scale2D { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(Scale2D( + animate_multiplicative_factor(self.0, other.0, procedure)?, + animate_multiplicative_factor(self.1, other.1, procedure)?, + )) + } +} + +/// A decomposed 2d matrix. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct MatrixDecomposed2D { + /// The translation function. + pub translate: Translate2D, + /// The scale function. + pub scale: Scale2D, + /// The rotation angle. + pub angle: f32, + /// The inner matrix. + pub matrix: InnerMatrix2D, +} + +impl Animate for MatrixDecomposed2D { + /// <https://drafts.csswg.org/css-transforms/#interpolation-of-decomposed-2d-matrix-values> + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + // If x-axis of one is flipped, and y-axis of the other, + // convert to an unflipped rotation. + let mut scale = self.scale; + let mut angle = self.angle; + let mut other_angle = other.angle; + if (scale.0 < 0.0 && other.scale.1 < 0.0) || (scale.1 < 0.0 && other.scale.0 < 0.0) { + scale.0 = -scale.0; + scale.1 = -scale.1; + angle += if angle < 0.0 { 180. } else { -180. }; + } + + // Don't rotate the long way around. + if angle == 0.0 { + angle = 360. + } + if other_angle == 0.0 { + other_angle = 360. + } + + if (angle - other_angle).abs() > 180. { + if angle > other_angle { + angle -= 360. + } else { + other_angle -= 360. + } + } + + // Interpolate all values. + let translate = self.translate.animate(&other.translate, procedure)?; + let scale = scale.animate(&other.scale, procedure)?; + let angle = angle.animate(&other_angle, procedure)?; + let matrix = self.matrix.animate(&other.matrix, procedure)?; + + Ok(MatrixDecomposed2D { + translate, + scale, + angle, + matrix, + }) + } +} + +impl ComputeSquaredDistance for MatrixDecomposed2D { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + // Use Radian to compute the distance. + const RAD_PER_DEG: f64 = std::f64::consts::PI / 180.0; + let angle1 = self.angle as f64 * RAD_PER_DEG; + let angle2 = other.angle as f64 * RAD_PER_DEG; + Ok(self.translate.compute_squared_distance(&other.translate)? + + self.scale.compute_squared_distance(&other.scale)? + + angle1.compute_squared_distance(&angle2)? + + self.matrix.compute_squared_distance(&other.matrix)?) + } +} + +impl From<Matrix3D> for MatrixDecomposed2D { + /// Decompose a 2D matrix. + /// <https://drafts.csswg.org/css-transforms/#decomposing-a-2d-matrix> + fn from(matrix: Matrix3D) -> MatrixDecomposed2D { + let mut row0x = matrix.m11; + let mut row0y = matrix.m12; + let mut row1x = matrix.m21; + let mut row1y = matrix.m22; + + let translate = Translate2D(matrix.m41, matrix.m42); + let mut scale = Scale2D( + (row0x * row0x + row0y * row0y).sqrt(), + (row1x * row1x + row1y * row1y).sqrt(), + ); + + // If determinant is negative, one axis was flipped. + let determinant = row0x * row1y - row0y * row1x; + if determinant < 0. { + if row0x < row1y { + scale.0 = -scale.0; + } else { + scale.1 = -scale.1; + } + } + + // Renormalize matrix to remove scale. + if scale.0 != 0.0 { + row0x *= 1. / scale.0; + row0y *= 1. / scale.0; + } + if scale.1 != 0.0 { + row1x *= 1. / scale.1; + row1y *= 1. / scale.1; + } + + // Compute rotation and renormalize matrix. + let mut angle = row0y.atan2(row0x); + if angle != 0.0 { + let sn = -row0y; + let cs = row0x; + let m11 = row0x; + let m12 = row0y; + let m21 = row1x; + let m22 = row1y; + row0x = cs * m11 + sn * m21; + row0y = cs * m12 + sn * m22; + row1x = -sn * m11 + cs * m21; + row1y = -sn * m12 + cs * m22; + } + + let m = InnerMatrix2D { + m11: row0x, + m12: row0y, + m21: row1x, + m22: row1y, + }; + + // Convert into degrees because our rotation functions expect it. + angle = angle.to_degrees(); + MatrixDecomposed2D { + translate: translate, + scale: scale, + angle: angle, + matrix: m, + } + } +} + +impl From<MatrixDecomposed2D> for Matrix3D { + /// Recompose a 2D matrix. + /// <https://drafts.csswg.org/css-transforms/#recomposing-to-a-2d-matrix> + fn from(decomposed: MatrixDecomposed2D) -> Matrix3D { + let mut computed_matrix = Matrix3D::identity(); + computed_matrix.m11 = decomposed.matrix.m11; + computed_matrix.m12 = decomposed.matrix.m12; + computed_matrix.m21 = decomposed.matrix.m21; + computed_matrix.m22 = decomposed.matrix.m22; + + // Translate matrix. + computed_matrix.m41 = decomposed.translate.0; + computed_matrix.m42 = decomposed.translate.1; + + // Rotate matrix. + let angle = decomposed.angle.to_radians(); + let cos_angle = angle.cos(); + let sin_angle = angle.sin(); + + let mut rotate_matrix = Matrix3D::identity(); + rotate_matrix.m11 = cos_angle; + rotate_matrix.m12 = sin_angle; + rotate_matrix.m21 = -sin_angle; + rotate_matrix.m22 = cos_angle; + + // Multiplication of computed_matrix and rotate_matrix + computed_matrix = rotate_matrix.multiply(&computed_matrix); + + // Scale matrix. + computed_matrix.m11 *= decomposed.scale.0; + computed_matrix.m12 *= decomposed.scale.0; + computed_matrix.m21 *= decomposed.scale.1; + computed_matrix.m22 *= decomposed.scale.1; + computed_matrix + } +} + +impl Animate for Matrix { + #[cfg(feature = "servo")] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let this = Matrix3D::from(*self); + let other = Matrix3D::from(*other); + let this = MatrixDecomposed2D::from(this); + let other = MatrixDecomposed2D::from(other); + Ok(Matrix3D::from(this.animate(&other, procedure)?).into_2d()?) + } + + #[cfg(feature = "gecko")] + // Gecko doesn't exactly follow the spec here; we use a different procedure + // to match it + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let from = decompose_2d_matrix(&(*self).into()); + let to = decompose_2d_matrix(&(*other).into()); + match (from, to) { + (Ok(from), Ok(to)) => Matrix3D::from(from.animate(&to, procedure)?).into_2d(), + // Matrices can be undecomposable due to couple reasons, e.g., + // non-invertible matrices. In this case, we should report Err here, + // and let the caller do the fallback procedure. + _ => Err(()), + } + } +} + +/// A 3d translation. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)] +pub struct Translate3D(pub f32, pub f32, pub f32); + +/// A 3d scale function. +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Scale3D(pub f32, pub f32, pub f32); + +impl Scale3D { + /// Negate self. + fn negate(&mut self) { + self.0 *= -1.0; + self.1 *= -1.0; + self.2 *= -1.0; + } +} + +impl Animate for Scale3D { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(Scale3D( + animate_multiplicative_factor(self.0, other.0, procedure)?, + animate_multiplicative_factor(self.1, other.1, procedure)?, + animate_multiplicative_factor(self.2, other.2, procedure)?, + )) + } +} + +/// A 3d skew function. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[derive(Animate, Clone, Copy, Debug)] +pub struct Skew(f32, f32, f32); + +impl ComputeSquaredDistance for Skew { + // We have to use atan() to convert the skew factors into skew angles, so implement + // ComputeSquaredDistance manually. + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + Ok(self.0.atan().compute_squared_distance(&other.0.atan())? + + self.1.atan().compute_squared_distance(&other.1.atan())? + + self.2.atan().compute_squared_distance(&other.2.atan())?) + } +} + +/// A 3d perspective transformation. +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Perspective(pub f32, pub f32, pub f32, pub f32); + +impl Animate for Perspective { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(Perspective( + self.0.animate(&other.0, procedure)?, + self.1.animate(&other.1, procedure)?, + self.2.animate(&other.2, procedure)?, + animate_multiplicative_factor(self.3, other.3, procedure)?, + )) + } +} + +/// A quaternion used to represent a rotation. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Quaternion(f64, f64, f64, f64); + +impl Quaternion { + /// Return a quaternion from a unit direction vector and angle (unit: radian). + #[inline] + fn from_direction_and_angle(vector: &DirectionVector, angle: f64) -> Self { + debug_assert!( + (vector.length() - 1.).abs() < 0.0001, + "Only accept an unit direction vector to create a quaternion" + ); + // Reference: + // https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation + // + // if the direction axis is (x, y, z) = xi + yj + zk, + // and the angle is |theta|, this formula can be done using + // an extension of Euler's formula: + // q = cos(theta/2) + (xi + yj + zk)(sin(theta/2)) + // = cos(theta/2) + + // x*sin(theta/2)i + y*sin(theta/2)j + z*sin(theta/2)k + Quaternion( + vector.x as f64 * (angle / 2.).sin(), + vector.y as f64 * (angle / 2.).sin(), + vector.z as f64 * (angle / 2.).sin(), + (angle / 2.).cos(), + ) + } + + /// Calculate the dot product. + #[inline] + fn dot(&self, other: &Self) -> f64 { + self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3 + } + + /// Return the scaled quaternion by a factor. + #[inline] + fn scale(&self, factor: f64) -> Self { + Quaternion( + self.0 * factor, + self.1 * factor, + self.2 * factor, + self.3 * factor, + ) + } +} + +impl Animate for Quaternion { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + use std::f64; + + let (this_weight, other_weight) = procedure.weights(); + debug_assert!( + // Doule EPSILON since both this_weight and other_weght have calculation errors + // which are approximately equal to EPSILON. + (this_weight + other_weight - 1.0f64).abs() <= f64::EPSILON * 2.0 || + other_weight == 1.0f64 || + other_weight == 0.0f64, + "animate should only be used for interpolating or accumulating transforms" + ); + + // We take a specialized code path for accumulation (where other_weight + // is 1). + if let Procedure::Accumulate { .. } = procedure { + debug_assert_eq!(other_weight, 1.0); + if this_weight == 0.0 { + return Ok(*other); + } + + let clamped_w = self.3.min(1.0).max(-1.0); + + // Determine the scale factor. + let mut theta = clamped_w.acos(); + let mut scale = if theta == 0.0 { 0.0 } else { 1.0 / theta.sin() }; + theta *= this_weight; + scale *= theta.sin(); + + // Scale the self matrix by this_weight. + let mut scaled_self = *self; + scaled_self.0 *= scale; + scaled_self.1 *= scale; + scaled_self.2 *= scale; + scaled_self.3 = theta.cos(); + + // Multiply scaled-self by other. + let a = &scaled_self; + let b = other; + return Ok(Quaternion( + a.3 * b.0 + a.0 * b.3 + a.1 * b.2 - a.2 * b.1, + a.3 * b.1 - a.0 * b.2 + a.1 * b.3 + a.2 * b.0, + a.3 * b.2 + a.0 * b.1 - a.1 * b.0 + a.2 * b.3, + a.3 * b.3 - a.0 * b.0 - a.1 * b.1 - a.2 * b.2, + )); + } + + // Straight from gfxQuaternion::Slerp. + // + // Dot product, clamped between -1 and 1. + let dot = (self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3) + .min(1.0) + .max(-1.0); + + if dot.abs() == 1.0 { + return Ok(*self); + } + + let theta = dot.acos(); + let rsintheta = 1.0 / (1.0 - dot * dot).sqrt(); + + let right_weight = (other_weight * theta).sin() * rsintheta; + let left_weight = (other_weight * theta).cos() - dot * right_weight; + + let left = self.scale(left_weight); + let right = other.scale(right_weight); + + Ok(Quaternion( + left.0 + right.0, + left.1 + right.1, + left.2 + right.2, + left.3 + right.3, + )) + } +} + +impl ComputeSquaredDistance for Quaternion { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + // Use quaternion vectors to get the angle difference. Both q1 and q2 are unit vectors, + // so we can get their angle difference by: + // cos(theta/2) = (q1 dot q2) / (|q1| * |q2|) = q1 dot q2. + let distance = self.dot(other).max(-1.0).min(1.0).acos() * 2.0; + Ok(SquaredDistance::from_sqrt(distance)) + } +} + +/// A decomposed 3d matrix. +#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct MatrixDecomposed3D { + /// A translation function. + pub translate: Translate3D, + /// A scale function. + pub scale: Scale3D, + /// The skew component of the transformation. + pub skew: Skew, + /// The perspective component of the transformation. + pub perspective: Perspective, + /// The quaternion used to represent the rotation. + pub quaternion: Quaternion, +} + +impl From<MatrixDecomposed3D> for Matrix3D { + /// Recompose a 3D matrix. + /// <https://drafts.csswg.org/css-transforms/#recomposing-to-a-3d-matrix> + fn from(decomposed: MatrixDecomposed3D) -> Matrix3D { + let mut matrix = Matrix3D::identity(); + + // Apply perspective + matrix.set_perspective(&decomposed.perspective); + + // Apply translation + matrix.apply_translate(&decomposed.translate); + + // Apply rotation + { + let x = decomposed.quaternion.0; + let y = decomposed.quaternion.1; + let z = decomposed.quaternion.2; + let w = decomposed.quaternion.3; + + // Construct a composite rotation matrix from the quaternion values + // rotationMatrix is a identity 4x4 matrix initially + let mut rotation_matrix = Matrix3D::identity(); + rotation_matrix.m11 = 1.0 - 2.0 * (y * y + z * z) as f32; + rotation_matrix.m12 = 2.0 * (x * y + z * w) as f32; + rotation_matrix.m13 = 2.0 * (x * z - y * w) as f32; + rotation_matrix.m21 = 2.0 * (x * y - z * w) as f32; + rotation_matrix.m22 = 1.0 - 2.0 * (x * x + z * z) as f32; + rotation_matrix.m23 = 2.0 * (y * z + x * w) as f32; + rotation_matrix.m31 = 2.0 * (x * z + y * w) as f32; + rotation_matrix.m32 = 2.0 * (y * z - x * w) as f32; + rotation_matrix.m33 = 1.0 - 2.0 * (x * x + y * y) as f32; + + matrix = rotation_matrix.multiply(&matrix); + } + + // Apply skew + { + let mut temp = Matrix3D::identity(); + if decomposed.skew.2 != 0.0 { + temp.m32 = decomposed.skew.2; + matrix = temp.multiply(&matrix); + temp.m32 = 0.0; + } + + if decomposed.skew.1 != 0.0 { + temp.m31 = decomposed.skew.1; + matrix = temp.multiply(&matrix); + temp.m31 = 0.0; + } + + if decomposed.skew.0 != 0.0 { + temp.m21 = decomposed.skew.0; + matrix = temp.multiply(&matrix); + } + } + + // Apply scale + matrix.apply_scale(&decomposed.scale); + + matrix + } +} + +/// Decompose a 3D matrix. +/// https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix +/// http://www.realtimerendering.com/resources/GraphicsGems/gemsii/unmatrix.c +fn decompose_3d_matrix(mut matrix: Matrix3D) -> Result<MatrixDecomposed3D, ()> { + // Combine 2 point. + let combine = |a: [f32; 3], b: [f32; 3], ascl: f32, bscl: f32| { + [ + (ascl * a[0]) + (bscl * b[0]), + (ascl * a[1]) + (bscl * b[1]), + (ascl * a[2]) + (bscl * b[2]), + ] + }; + // Dot product. + let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + // Cross product. + let cross = |row1: [f32; 3], row2: [f32; 3]| { + [ + row1[1] * row2[2] - row1[2] * row2[1], + row1[2] * row2[0] - row1[0] * row2[2], + row1[0] * row2[1] - row1[1] * row2[0], + ] + }; + + if matrix.m44 == 0.0 { + return Err(()); + } + + let scaling_factor = matrix.m44; + + // Normalize the matrix. + matrix.scale_by_factor(1.0 / scaling_factor); + + // perspective_matrix is used to solve for perspective, but it also provides + // an easy way to test for singularity of the upper 3x3 component. + let mut perspective_matrix = matrix; + + perspective_matrix.m14 = 0.0; + perspective_matrix.m24 = 0.0; + perspective_matrix.m34 = 0.0; + perspective_matrix.m44 = 1.0; + + if perspective_matrix.determinant() == 0.0 { + return Err(()); + } + + // First, isolate perspective. + let perspective = if matrix.m14 != 0.0 || matrix.m24 != 0.0 || matrix.m34 != 0.0 { + let right_hand_side: [f32; 4] = [matrix.m14, matrix.m24, matrix.m34, matrix.m44]; + + perspective_matrix = perspective_matrix.inverse().unwrap().transpose(); + let perspective = perspective_matrix.pre_mul_point4(&right_hand_side); + // NOTE(emilio): Even though the reference algorithm clears the + // fourth column here (matrix.m14..matrix.m44), they're not used below + // so it's not really needed. + Perspective( + perspective[0], + perspective[1], + perspective[2], + perspective[3], + ) + } else { + Perspective(0.0, 0.0, 0.0, 1.0) + }; + + // Next take care of translation (easy). + let translate = Translate3D(matrix.m41, matrix.m42, matrix.m43); + + // Now get scale and shear. 'row' is a 3 element array of 3 component vectors + let mut row = matrix.get_matrix_3x3_part(); + + // Compute X scale factor and normalize first row. + let row0len = (row[0][0] * row[0][0] + row[0][1] * row[0][1] + row[0][2] * row[0][2]).sqrt(); + let mut scale = Scale3D(row0len, 0.0, 0.0); + row[0] = [ + row[0][0] / row0len, + row[0][1] / row0len, + row[0][2] / row0len, + ]; + + // Compute XY shear factor and make 2nd row orthogonal to 1st. + let mut skew = Skew(dot(row[0], row[1]), 0.0, 0.0); + row[1] = combine(row[1], row[0], 1.0, -skew.0); + + // Now, compute Y scale and normalize 2nd row. + let row1len = (row[1][0] * row[1][0] + row[1][1] * row[1][1] + row[1][2] * row[1][2]).sqrt(); + scale.1 = row1len; + row[1] = [ + row[1][0] / row1len, + row[1][1] / row1len, + row[1][2] / row1len, + ]; + skew.0 /= scale.1; + + // Compute XZ and YZ shears, orthogonalize 3rd row + skew.1 = dot(row[0], row[2]); + row[2] = combine(row[2], row[0], 1.0, -skew.1); + skew.2 = dot(row[1], row[2]); + row[2] = combine(row[2], row[1], 1.0, -skew.2); + + // Next, get Z scale and normalize 3rd row. + let row2len = (row[2][0] * row[2][0] + row[2][1] * row[2][1] + row[2][2] * row[2][2]).sqrt(); + scale.2 = row2len; + row[2] = [ + row[2][0] / row2len, + row[2][1] / row2len, + row[2][2] / row2len, + ]; + skew.1 /= scale.2; + skew.2 /= scale.2; + + // At this point, the matrix (in rows) is orthonormal. + // Check for a coordinate system flip. If the determinant + // is -1, then negate the matrix and the scaling factors. + if dot(row[0], cross(row[1], row[2])) < 0.0 { + scale.negate(); + for i in 0..3 { + row[i][0] *= -1.0; + row[i][1] *= -1.0; + row[i][2] *= -1.0; + } + } + + // Now, get the rotations out. + let mut quaternion = Quaternion( + 0.5 * ((1.0 + row[0][0] - row[1][1] - row[2][2]).max(0.0) as f64).sqrt(), + 0.5 * ((1.0 - row[0][0] + row[1][1] - row[2][2]).max(0.0) as f64).sqrt(), + 0.5 * ((1.0 - row[0][0] - row[1][1] + row[2][2]).max(0.0) as f64).sqrt(), + 0.5 * ((1.0 + row[0][0] + row[1][1] + row[2][2]).max(0.0) as f64).sqrt(), + ); + + if row[2][1] > row[1][2] { + quaternion.0 = -quaternion.0 + } + if row[0][2] > row[2][0] { + quaternion.1 = -quaternion.1 + } + if row[1][0] > row[0][1] { + quaternion.2 = -quaternion.2 + } + + Ok(MatrixDecomposed3D { + translate, + scale, + skew, + perspective, + quaternion, + }) +} + +/// Decompose a 2D matrix for Gecko. +// Use the algorithm from nsStyleTransformMatrix::Decompose2DMatrix() in Gecko. +#[cfg(feature = "gecko")] +fn decompose_2d_matrix(matrix: &Matrix3D) -> Result<MatrixDecomposed3D, ()> { + // The index is column-major, so the equivalent transform matrix is: + // | m11 m21 0 m41 | => | m11 m21 | and translate(m41, m42) + // | m12 m22 0 m42 | | m12 m22 | + // | 0 0 1 0 | + // | 0 0 0 1 | + let (mut m11, mut m12) = (matrix.m11, matrix.m12); + let (mut m21, mut m22) = (matrix.m21, matrix.m22); + // Check if this is a singular matrix. + if m11 * m22 == m12 * m21 { + return Err(()); + } + + let mut scale_x = (m11 * m11 + m12 * m12).sqrt(); + m11 /= scale_x; + m12 /= scale_x; + + let mut shear_xy = m11 * m21 + m12 * m22; + m21 -= m11 * shear_xy; + m22 -= m12 * shear_xy; + + let scale_y = (m21 * m21 + m22 * m22).sqrt(); + m21 /= scale_y; + m22 /= scale_y; + shear_xy /= scale_y; + + let determinant = m11 * m22 - m12 * m21; + // Determinant should now be 1 or -1. + if 0.99 > determinant.abs() || determinant.abs() > 1.01 { + return Err(()); + } + + if determinant < 0. { + m11 = -m11; + m12 = -m12; + shear_xy = -shear_xy; + scale_x = -scale_x; + } + + Ok(MatrixDecomposed3D { + translate: Translate3D(matrix.m41, matrix.m42, 0.), + scale: Scale3D(scale_x, scale_y, 1.), + skew: Skew(shear_xy, 0., 0.), + perspective: Perspective(0., 0., 0., 1.), + quaternion: Quaternion::from_direction_and_angle( + &DirectionVector::new(0., 0., 1.), + m12.atan2(m11) as f64, + ), + }) +} + +impl Animate for Matrix3D { + #[cfg(feature = "servo")] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.is_3d() || other.is_3d() { + let decomposed_from = decompose_3d_matrix(*self); + let decomposed_to = decompose_3d_matrix(*other); + match (decomposed_from, decomposed_to) { + (Ok(this), Ok(other)) => Ok(Matrix3D::from(this.animate(&other, procedure)?)), + // Matrices can be undecomposable due to couple reasons, e.g., + // non-invertible matrices. In this case, we should report Err + // here, and let the caller do the fallback procedure. + _ => Err(()), + } + } else { + let this = MatrixDecomposed2D::from(*self); + let other = MatrixDecomposed2D::from(*other); + Ok(Matrix3D::from(this.animate(&other, procedure)?)) + } + } + + #[cfg(feature = "gecko")] + // Gecko doesn't exactly follow the spec here; we use a different procedure + // to match it + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let (from, to) = if self.is_3d() || other.is_3d() { + (decompose_3d_matrix(*self), decompose_3d_matrix(*other)) + } else { + (decompose_2d_matrix(self), decompose_2d_matrix(other)) + }; + match (from, to) { + (Ok(from), Ok(to)) => Ok(Matrix3D::from(from.animate(&to, procedure)?)), + // Matrices can be undecomposable due to couple reasons, e.g., + // non-invertible matrices. In this case, we should report Err here, + // and let the caller do the fallback procedure. + _ => Err(()), + } + } +} + +impl ComputeSquaredDistance for Matrix3D { + #[inline] + #[cfg(feature = "servo")] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + if self.is_3d() || other.is_3d() { + let from = decompose_3d_matrix(*self)?; + let to = decompose_3d_matrix(*other)?; + from.compute_squared_distance(&to) + } else { + let from = MatrixDecomposed2D::from(*self); + let to = MatrixDecomposed2D::from(*other); + from.compute_squared_distance(&to) + } + } + + #[inline] + #[cfg(feature = "gecko")] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let (from, to) = if self.is_3d() || other.is_3d() { + (decompose_3d_matrix(*self)?, decompose_3d_matrix(*other)?) + } else { + (decompose_2d_matrix(self)?, decompose_2d_matrix(other)?) + }; + from.compute_squared_distance(&to) + } +} + +// ------------------------------------ +// Animation for Transform list. +// ------------------------------------ +fn is_matched_operation( + first: &ComputedTransformOperation, + second: &ComputedTransformOperation, +) -> bool { + match (first, second) { + (&TransformOperation::Matrix(..), &TransformOperation::Matrix(..)) | + (&TransformOperation::Matrix3D(..), &TransformOperation::Matrix3D(..)) | + (&TransformOperation::Skew(..), &TransformOperation::Skew(..)) | + (&TransformOperation::SkewX(..), &TransformOperation::SkewX(..)) | + (&TransformOperation::SkewY(..), &TransformOperation::SkewY(..)) | + (&TransformOperation::Rotate(..), &TransformOperation::Rotate(..)) | + (&TransformOperation::Rotate3D(..), &TransformOperation::Rotate3D(..)) | + (&TransformOperation::RotateX(..), &TransformOperation::RotateX(..)) | + (&TransformOperation::RotateY(..), &TransformOperation::RotateY(..)) | + (&TransformOperation::RotateZ(..), &TransformOperation::RotateZ(..)) | + (&TransformOperation::Perspective(..), &TransformOperation::Perspective(..)) => true, + // Match functions that have the same primitive transform function + (a, b) if a.is_translate() && b.is_translate() => true, + (a, b) if a.is_scale() && b.is_scale() => true, + (a, b) if a.is_rotate() && b.is_rotate() => true, + // InterpolateMatrix and AccumulateMatrix are for mismatched transforms + _ => false, + } +} + +/// <https://drafts.csswg.org/css-transforms/#interpolation-of-transforms> +impl Animate for ComputedTransform { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + use std::borrow::Cow; + + // Addition for transforms simply means appending to the list of + // transform functions. This is different to how we handle the other + // animation procedures so we treat it separately here rather than + // handling it in TransformOperation. + if procedure == Procedure::Add { + let result = self.0.iter().chain(&*other.0).cloned().collect(); + return Ok(Transform(result)); + } + + let this = Cow::Borrowed(&self.0); + let other = Cow::Borrowed(&other.0); + + // Interpolate the common prefix + let mut result = this + .iter() + .zip(other.iter()) + .take_while(|(this, other)| is_matched_operation(this, other)) + .map(|(this, other)| this.animate(other, procedure)) + .collect::<Result<Vec<_>, _>>()?; + + // Deal with the remainders + let this_remainder = if this.len() > result.len() { + Some(&this[result.len()..]) + } else { + None + }; + let other_remainder = if other.len() > result.len() { + Some(&other[result.len()..]) + } else { + None + }; + + match (this_remainder, other_remainder) { + // If there is a remainder from *both* lists we must have had mismatched functions. + // => Add the remainders to a suitable ___Matrix function. + (Some(this_remainder), Some(other_remainder)) => { + result.push(TransformOperation::animate_mismatched_transforms( + this_remainder, + other_remainder, + procedure, + )?); + }, + // If there is a remainder from just one list, then one list must be shorter but + // completely match the type of the corresponding functions in the longer list. + // => Interpolate the remainder with identity transforms. + (Some(remainder), None) | (None, Some(remainder)) => { + let fill_right = this_remainder.is_some(); + result.append( + &mut remainder + .iter() + .map(|transform| { + let identity = transform.to_animated_zero().unwrap(); + + match transform { + TransformOperation::AccumulateMatrix { .. } | + TransformOperation::InterpolateMatrix { .. } => { + let (from, to) = if fill_right { + (transform, &identity) + } else { + (&identity, transform) + }; + + TransformOperation::animate_mismatched_transforms( + &[from.clone()], + &[to.clone()], + procedure, + ) + }, + _ => { + let (lhs, rhs) = if fill_right { + (transform, &identity) + } else { + (&identity, transform) + }; + lhs.animate(rhs, procedure) + }, + } + }) + .collect::<Result<Vec<_>, _>>()?, + ); + }, + (None, None) => {}, + } + + Ok(Transform(result.into())) + } +} + +impl ComputeSquaredDistance for ComputedTransform { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let squared_dist = self.0.squared_distance_with_zero(&other.0); + + // Roll back to matrix interpolation if there is any Err(()) in the + // transform lists, such as mismatched transform functions. + // + // FIXME: Using a zero size here seems a bit sketchy but matches the + // previous behavior. + if squared_dist.is_err() { + let rect = euclid::Rect::zero(); + let matrix1: Matrix3D = self.to_transform_3d_matrix(Some(&rect))?.0.into(); + let matrix2: Matrix3D = other.to_transform_3d_matrix(Some(&rect))?.0.into(); + return matrix1.compute_squared_distance(&matrix2); + } + + squared_dist + } +} + +/// <http://dev.w3.org/csswg/css-transforms/#interpolation-of-transforms> +impl Animate for ComputedTransformOperation { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self, other) { + (&TransformOperation::Matrix3D(ref this), &TransformOperation::Matrix3D(ref other)) => { + Ok(TransformOperation::Matrix3D( + this.animate(other, procedure)?, + )) + }, + (&TransformOperation::Matrix(ref this), &TransformOperation::Matrix(ref other)) => { + Ok(TransformOperation::Matrix(this.animate(other, procedure)?)) + }, + ( + &TransformOperation::Skew(ref fx, ref fy), + &TransformOperation::Skew(ref tx, ref ty), + ) => Ok(TransformOperation::Skew( + fx.animate(tx, procedure)?, + fy.animate(ty, procedure)?, + )), + (&TransformOperation::SkewX(ref f), &TransformOperation::SkewX(ref t)) => { + Ok(TransformOperation::SkewX(f.animate(t, procedure)?)) + }, + (&TransformOperation::SkewY(ref f), &TransformOperation::SkewY(ref t)) => { + Ok(TransformOperation::SkewY(f.animate(t, procedure)?)) + }, + ( + &TransformOperation::Translate3D(ref fx, ref fy, ref fz), + &TransformOperation::Translate3D(ref tx, ref ty, ref tz), + ) => Ok(TransformOperation::Translate3D( + fx.animate(tx, procedure)?, + fy.animate(ty, procedure)?, + fz.animate(tz, procedure)?, + )), + ( + &TransformOperation::Translate(ref fx, ref fy), + &TransformOperation::Translate(ref tx, ref ty), + ) => Ok(TransformOperation::Translate( + fx.animate(tx, procedure)?, + fy.animate(ty, procedure)?, + )), + (&TransformOperation::TranslateX(ref f), &TransformOperation::TranslateX(ref t)) => { + Ok(TransformOperation::TranslateX(f.animate(t, procedure)?)) + }, + (&TransformOperation::TranslateY(ref f), &TransformOperation::TranslateY(ref t)) => { + Ok(TransformOperation::TranslateY(f.animate(t, procedure)?)) + }, + (&TransformOperation::TranslateZ(ref f), &TransformOperation::TranslateZ(ref t)) => { + Ok(TransformOperation::TranslateZ(f.animate(t, procedure)?)) + }, + ( + &TransformOperation::Scale3D(ref fx, ref fy, ref fz), + &TransformOperation::Scale3D(ref tx, ref ty, ref tz), + ) => Ok(TransformOperation::Scale3D( + animate_multiplicative_factor(*fx, *tx, procedure)?, + animate_multiplicative_factor(*fy, *ty, procedure)?, + animate_multiplicative_factor(*fz, *tz, procedure)?, + )), + (&TransformOperation::ScaleX(ref f), &TransformOperation::ScaleX(ref t)) => Ok( + TransformOperation::ScaleX(animate_multiplicative_factor(*f, *t, procedure)?), + ), + (&TransformOperation::ScaleY(ref f), &TransformOperation::ScaleY(ref t)) => Ok( + TransformOperation::ScaleY(animate_multiplicative_factor(*f, *t, procedure)?), + ), + (&TransformOperation::ScaleZ(ref f), &TransformOperation::ScaleZ(ref t)) => Ok( + TransformOperation::ScaleZ(animate_multiplicative_factor(*f, *t, procedure)?), + ), + ( + &TransformOperation::Scale(ref fx, ref fy), + &TransformOperation::Scale(ref tx, ref ty), + ) => Ok(TransformOperation::Scale( + animate_multiplicative_factor(*fx, *tx, procedure)?, + animate_multiplicative_factor(*fy, *ty, procedure)?, + )), + ( + &TransformOperation::Rotate3D(fx, fy, fz, fa), + &TransformOperation::Rotate3D(tx, ty, tz, ta), + ) => { + let animated = Rotate::Rotate3D(fx, fy, fz, fa) + .animate(&Rotate::Rotate3D(tx, ty, tz, ta), procedure)?; + let (fx, fy, fz, fa) = ComputedRotate::resolve(&animated); + Ok(TransformOperation::Rotate3D(fx, fy, fz, fa)) + }, + (&TransformOperation::RotateX(fa), &TransformOperation::RotateX(ta)) => { + Ok(TransformOperation::RotateX(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::RotateY(fa), &TransformOperation::RotateY(ta)) => { + Ok(TransformOperation::RotateY(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::RotateZ(fa), &TransformOperation::RotateZ(ta)) => { + Ok(TransformOperation::RotateZ(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::Rotate(fa), &TransformOperation::Rotate(ta)) => { + Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::Rotate(fa), &TransformOperation::RotateZ(ta)) => { + Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::RotateZ(fa), &TransformOperation::Rotate(ta)) => { + Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?)) + }, + ( + &TransformOperation::Perspective(ref fd), + &TransformOperation::Perspective(ref td), + ) => { + use crate::values::computed::CSSPixelLength; + use crate::values::generics::transform::create_perspective_matrix; + + // From https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions: + // + // The transform functions matrix(), matrix3d() and + // perspective() get converted into 4x4 matrices first and + // interpolated as defined in section Interpolation of + // Matrices afterwards. + // + let from = create_perspective_matrix(fd.infinity_or(|l| l.px())); + let to = create_perspective_matrix(td.infinity_or(|l| l.px())); + + let interpolated = Matrix3D::from(from).animate(&Matrix3D::from(to), procedure)?; + + let decomposed = decompose_3d_matrix(interpolated)?; + let perspective_z = decomposed.perspective.2; + // Clamp results outside of the -1 to 0 range so that we get perspective + // function values between 1 and infinity. + let used_value = if perspective_z >= 0. { + transform::PerspectiveFunction::None + } else { + transform::PerspectiveFunction::Length(CSSPixelLength::new( + if perspective_z <= -1. { + 1. + } else { + -1. / perspective_z + }, + )) + }; + Ok(TransformOperation::Perspective(used_value)) + }, + _ if self.is_translate() && other.is_translate() => self + .to_translate_3d() + .animate(&other.to_translate_3d(), procedure), + _ if self.is_scale() && other.is_scale() => { + self.to_scale_3d().animate(&other.to_scale_3d(), procedure) + }, + _ if self.is_rotate() && other.is_rotate() => self + .to_rotate_3d() + .animate(&other.to_rotate_3d(), procedure), + _ => Err(()), + } + } +} + +impl ComputedTransformOperation { + /// If there are no size dependencies, we try to animate in-place, to avoid + /// creating deeply nested Interpolate* operations. + fn try_animate_mismatched_transforms_in_place( + left: &[Self], + right: &[Self], + procedure: Procedure, + ) -> Result<Self, ()> { + let (left, _left_3d) = Transform::components_to_transform_3d_matrix(left, None)?; + let (right, _right_3d) = Transform::components_to_transform_3d_matrix(right, None)?; + ComputedTransformOperation::Matrix3D(left.into()).animate( + &ComputedTransformOperation::Matrix3D(right.into()), + procedure, + ) + } + + fn animate_mismatched_transforms( + left: &[Self], + right: &[Self], + procedure: Procedure, + ) -> Result<Self, ()> { + if let Ok(op) = Self::try_animate_mismatched_transforms_in_place(left, right, procedure) { + return Ok(op); + } + let from_list = Transform(left.to_vec().into()); + let to_list = Transform(right.to_vec().into()); + Ok(match procedure { + Procedure::Add => { + debug_assert!(false, "Addition should've been handled earlier"); + return Err(()); + }, + Procedure::Interpolate { progress } => Self::InterpolateMatrix { + from_list, + to_list, + progress: Percentage(progress as f32), + }, + Procedure::Accumulate { count } => Self::AccumulateMatrix { + from_list, + to_list, + count: cmp::min(count, i32::max_value() as u64) as i32, + }, + }) + } +} + +// This might not be the most useful definition of distance. It might be better, for example, +// to trace the distance travelled by a point as its transform is interpolated between the two +// lists. That, however, proves to be quite complicated so we take a simple approach for now. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1318591#c0. +impl ComputeSquaredDistance for ComputedTransformOperation { + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + match (self, other) { + (&TransformOperation::Matrix3D(ref this), &TransformOperation::Matrix3D(ref other)) => { + this.compute_squared_distance(other) + }, + (&TransformOperation::Matrix(ref this), &TransformOperation::Matrix(ref other)) => { + let this: Matrix3D = (*this).into(); + let other: Matrix3D = (*other).into(); + this.compute_squared_distance(&other) + }, + ( + &TransformOperation::Skew(ref fx, ref fy), + &TransformOperation::Skew(ref tx, ref ty), + ) => Ok(fx.compute_squared_distance(&tx)? + fy.compute_squared_distance(&ty)?), + (&TransformOperation::SkewX(ref f), &TransformOperation::SkewX(ref t)) | + (&TransformOperation::SkewY(ref f), &TransformOperation::SkewY(ref t)) => { + f.compute_squared_distance(&t) + }, + ( + &TransformOperation::Translate3D(ref fx, ref fy, ref fz), + &TransformOperation::Translate3D(ref tx, ref ty, ref tz), + ) => { + // For translate, We don't want to require doing layout in order + // to calculate the result, so drop the percentage part. + // + // However, dropping percentage makes us impossible to compute + // the distance for the percentage-percentage case, but Gecko + // uses the same formula, so it's fine for now. + let basis = Length::new(0.); + let fx = fx.resolve(basis).px(); + let fy = fy.resolve(basis).px(); + let tx = tx.resolve(basis).px(); + let ty = ty.resolve(basis).px(); + + Ok(fx.compute_squared_distance(&tx)? + + fy.compute_squared_distance(&ty)? + + fz.compute_squared_distance(&tz)?) + }, + ( + &TransformOperation::Scale3D(ref fx, ref fy, ref fz), + &TransformOperation::Scale3D(ref tx, ref ty, ref tz), + ) => Ok(fx.compute_squared_distance(&tx)? + + fy.compute_squared_distance(&ty)? + + fz.compute_squared_distance(&tz)?), + ( + &TransformOperation::Rotate3D(fx, fy, fz, fa), + &TransformOperation::Rotate3D(tx, ty, tz, ta), + ) => Rotate::Rotate3D(fx, fy, fz, fa) + .compute_squared_distance(&Rotate::Rotate3D(tx, ty, tz, ta)), + (&TransformOperation::RotateX(fa), &TransformOperation::RotateX(ta)) | + (&TransformOperation::RotateY(fa), &TransformOperation::RotateY(ta)) | + (&TransformOperation::RotateZ(fa), &TransformOperation::RotateZ(ta)) | + (&TransformOperation::Rotate(fa), &TransformOperation::Rotate(ta)) => { + fa.compute_squared_distance(&ta) + }, + ( + &TransformOperation::Perspective(ref fd), + &TransformOperation::Perspective(ref td), + ) => fd + .infinity_or(|l| l.px()) + .compute_squared_distance(&td.infinity_or(|l| l.px())), + (&TransformOperation::Perspective(ref p), &TransformOperation::Matrix3D(ref m)) | + (&TransformOperation::Matrix3D(ref m), &TransformOperation::Perspective(ref p)) => { + // FIXME(emilio): Is this right? Why interpolating this with + // Perspective but not with anything else? + let mut p_matrix = Matrix3D::identity(); + let p = p.infinity_or(|p| p.px()); + if p >= 0. { + p_matrix.m34 = -1. / p.max(1.); + } + p_matrix.compute_squared_distance(&m) + }, + // Gecko cross-interpolates amongst all translate and all scale + // functions (See ToPrimitive in layout/style/StyleAnimationValue.cpp) + // without falling back to InterpolateMatrix + _ if self.is_translate() && other.is_translate() => self + .to_translate_3d() + .compute_squared_distance(&other.to_translate_3d()), + _ if self.is_scale() && other.is_scale() => self + .to_scale_3d() + .compute_squared_distance(&other.to_scale_3d()), + _ if self.is_rotate() && other.is_rotate() => self + .to_rotate_3d() + .compute_squared_distance(&other.to_rotate_3d()), + _ => Err(()), + } + } +} + +// ------------------------------------ +// Individual transforms. +// ------------------------------------ +/// <https://drafts.csswg.org/css-transforms-2/#propdef-rotate> +impl ComputedRotate { + fn resolve(&self) -> (Number, Number, Number, Angle) { + // According to the spec: + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + // + // If the axis is unspecified, it defaults to "0 0 1" + match *self { + Rotate::None => (0., 0., 1., Angle::zero()), + Rotate::Rotate3D(rx, ry, rz, angle) => (rx, ry, rz, angle), + Rotate::Rotate(angle) => (0., 0., 1., angle), + } + } +} + +impl Animate for ComputedRotate { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self, other) { + (&Rotate::None, &Rotate::None) => Ok(Rotate::None), + (&Rotate::Rotate3D(fx, fy, fz, fa), &Rotate::None) => { + // We always normalize direction vector for rotate3d() first, so we should also + // apply the same rule for rotate property. In other words, we promote none into + // a 3d rotate, and normalize both direction vector first, and then do + // interpolation. + let (fx, fy, fz, fa) = transform::get_normalized_vector_and_angle(fx, fy, fz, fa); + Ok(Rotate::Rotate3D( + fx, + fy, + fz, + fa.animate(&Angle::zero(), procedure)?, + )) + }, + (&Rotate::None, &Rotate::Rotate3D(tx, ty, tz, ta)) => { + // Normalize direction vector first. + let (tx, ty, tz, ta) = transform::get_normalized_vector_and_angle(tx, ty, tz, ta); + Ok(Rotate::Rotate3D( + tx, + ty, + tz, + Angle::zero().animate(&ta, procedure)?, + )) + }, + (&Rotate::Rotate3D(_, ..), _) | (_, &Rotate::Rotate3D(_, ..)) => { + let (from, to) = (self.resolve(), other.resolve()); + let (mut fx, mut fy, mut fz, fa) = + transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3); + let (mut tx, mut ty, mut tz, ta) = + transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3); + + if fa == Angle::from_degrees(0.) { + fx = tx; + fy = ty; + fz = tz; + } else if ta == Angle::from_degrees(0.) { + tx = fx; + ty = fy; + tz = fz; + } + + if (fx, fy, fz) == (tx, ty, tz) { + return Ok(Rotate::Rotate3D(fx, fy, fz, fa.animate(&ta, procedure)?)); + } + + let fv = DirectionVector::new(fx, fy, fz); + let tv = DirectionVector::new(tx, ty, tz); + let fq = Quaternion::from_direction_and_angle(&fv, fa.radians64()); + let tq = Quaternion::from_direction_and_angle(&tv, ta.radians64()); + + let rq = Quaternion::animate(&fq, &tq, procedure)?; + let (x, y, z, angle) = transform::get_normalized_vector_and_angle( + rq.0 as f32, + rq.1 as f32, + rq.2 as f32, + rq.3.acos() as f32 * 2.0, + ); + + Ok(Rotate::Rotate3D(x, y, z, Angle::from_radians(angle))) + }, + (&Rotate::Rotate(_), _) | (_, &Rotate::Rotate(_)) => { + // If this is a 2D rotation, we just animate the <angle> + let (from, to) = (self.resolve().3, other.resolve().3); + Ok(Rotate::Rotate(from.animate(&to, procedure)?)) + }, + } + } +} + +impl ComputeSquaredDistance for ComputedRotate { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + match (self, other) { + (&Rotate::None, &Rotate::None) => Ok(SquaredDistance::from_sqrt(0.)), + (&Rotate::Rotate3D(_, _, _, a), &Rotate::None) | + (&Rotate::None, &Rotate::Rotate3D(_, _, _, a)) => { + a.compute_squared_distance(&Angle::zero()) + }, + (&Rotate::Rotate3D(_, ..), _) | (_, &Rotate::Rotate3D(_, ..)) => { + let (from, to) = (self.resolve(), other.resolve()); + let (mut fx, mut fy, mut fz, angle1) = + transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3); + let (mut tx, mut ty, mut tz, angle2) = + transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3); + + if angle1 == Angle::zero() { + fx = tx; + fy = ty; + fz = tz; + } else if angle2 == Angle::zero() { + tx = fx; + ty = fy; + tz = fz; + } + + if (fx, fy, fz) == (tx, ty, tz) { + angle1.compute_squared_distance(&angle2) + } else { + let v1 = DirectionVector::new(fx, fy, fz); + let v2 = DirectionVector::new(tx, ty, tz); + let q1 = Quaternion::from_direction_and_angle(&v1, angle1.radians64()); + let q2 = Quaternion::from_direction_and_angle(&v2, angle2.radians64()); + q1.compute_squared_distance(&q2) + } + }, + (&Rotate::Rotate(_), _) | (_, &Rotate::Rotate(_)) => self + .resolve() + .3 + .compute_squared_distance(&other.resolve().3), + } + } +} + +/// <https://drafts.csswg.org/css-transforms-2/#propdef-translate> +impl ComputedTranslate { + fn resolve(&self) -> (LengthPercentage, LengthPercentage, Length) { + // According to the spec: + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + // + // Unspecified translations default to 0px + match *self { + Translate::None => ( + LengthPercentage::zero(), + LengthPercentage::zero(), + Length::zero(), + ), + Translate::Translate(ref tx, ref ty, ref tz) => (tx.clone(), ty.clone(), tz.clone()), + } + } +} + +impl Animate for ComputedTranslate { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self, other) { + (&Translate::None, &Translate::None) => Ok(Translate::None), + (&Translate::Translate(_, ..), _) | (_, &Translate::Translate(_, ..)) => { + let (from, to) = (self.resolve(), other.resolve()); + Ok(Translate::Translate( + from.0.animate(&to.0, procedure)?, + from.1.animate(&to.1, procedure)?, + from.2.animate(&to.2, procedure)?, + )) + }, + } + } +} + +impl ComputeSquaredDistance for ComputedTranslate { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let (from, to) = (self.resolve(), other.resolve()); + Ok(from.0.compute_squared_distance(&to.0)? + + from.1.compute_squared_distance(&to.1)? + + from.2.compute_squared_distance(&to.2)?) + } +} + +/// <https://drafts.csswg.org/css-transforms-2/#propdef-scale> +impl ComputedScale { + fn resolve(&self) -> (Number, Number, Number) { + // According to the spec: + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + // + // Unspecified scales default to 1 + match *self { + Scale::None => (1.0, 1.0, 1.0), + Scale::Scale(sx, sy, sz) => (sx, sy, sz), + } + } +} + +impl Animate for ComputedScale { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self, other) { + (&Scale::None, &Scale::None) => Ok(Scale::None), + (&Scale::Scale(_, ..), _) | (_, &Scale::Scale(_, ..)) => { + let (from, to) = (self.resolve(), other.resolve()); + // For transform lists, we add by appending to the list of + // transform functions. However, ComputedScale cannot be + // simply concatenated, so we have to calculate the additive + // result here. + if procedure == Procedure::Add { + // scale(x1,y1,z1)*scale(x2,y2,z2) = scale(x1*x2, y1*y2, z1*z2) + return Ok(Scale::Scale(from.0 * to.0, from.1 * to.1, from.2 * to.2)); + } + Ok(Scale::Scale( + animate_multiplicative_factor(from.0, to.0, procedure)?, + animate_multiplicative_factor(from.1, to.1, procedure)?, + animate_multiplicative_factor(from.2, to.2, procedure)?, + )) + }, + } + } +} + +impl ComputeSquaredDistance for ComputedScale { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let (from, to) = (self.resolve(), other.resolve()); + Ok(from.0.compute_squared_distance(&to.0)? + + from.1.compute_squared_distance(&to.1)? + + from.2.compute_squared_distance(&to.2)?) + } +} |