summaryrefslogtreecommitdiffstats
path: root/servo/components/style/values/animated
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--servo/components/style/values/animated/color.rs841
-rw-r--r--servo/components/style/values/animated/effects.rs27
-rw-r--r--servo/components/style/values/animated/font.rs151
-rw-r--r--servo/components/style/values/animated/grid.rs166
-rw-r--r--servo/components/style/values/animated/mod.rs489
-rw-r--r--servo/components/style/values/animated/svg.rs45
-rw-r--r--servo/components/style/values/animated/transform.rs1484
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(&current_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)?)
+ }
+}