From a5433ad68a26bcf410a5c6784f0a38652e0d677b Mon Sep 17 00:00:00 2001 From: Boris Chiou Date: Wed, 9 Aug 2017 19:15:03 +0800 Subject: [PATCH 1/4] Tweak CalcLengthOrPercentage to use pixel value. We compute the distance for eCSSUnit_Calc by pixel value in Gecko, so let's follow the same rules. --- components/style/values/computed/length.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/style/values/computed/length.rs b/components/style/values/computed/length.rs index fbe3b56e724..402ec36552a 100644 --- a/components/style/values/computed/length.rs +++ b/components/style/values/computed/length.rs @@ -78,7 +78,8 @@ impl ComputeSquaredDistance for CalcLengthOrPercentage { // FIXME(nox): This looks incorrect to me, to add a distance between lengths // with a distance between percentages. Ok( - self.unclamped_length().compute_squared_distance(&other.unclamped_length())? + + self.unclamped_length().to_f64_px().compute_squared_distance( + &other.unclamped_length().to_f64_px())? + self.percentage().compute_squared_distance(&other.percentage())?, ) } From 03e1794c12f1e98cd859e4655826da2ed9d53dac Mon Sep 17 00:00:00 2001 From: Boris Chiou Date: Thu, 10 Aug 2017 11:38:11 +0800 Subject: [PATCH 2/4] Use f64 for Quaternion. The unit of gfxQuaternion in Gecko is gfxFloat, which is "double", so it's better to use f64 to match the precision of Gecko. --- .../helpers/animated_properties.mako.rs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/components/style/properties/helpers/animated_properties.mako.rs b/components/style/properties/helpers/animated_properties.mako.rs index 38ac83a941d..8e6a2802203 100644 --- a/components/style/properties/helpers/animated_properties.mako.rs +++ b/components/style/properties/helpers/animated_properties.mako.rs @@ -1776,7 +1776,7 @@ pub struct Perspective(f32, f32, f32, f32); /// A quaternion used to represent a rotation. #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] -pub struct Quaternion(f32, f32, f32, f32); +pub struct Quaternion(f64, f64, f64, f64); /// A decomposed 3d matrix. #[derive(Clone, Copy, Debug)] @@ -1914,10 +1914,10 @@ fn decompose_3d_matrix(mut matrix: ComputedMatrix) -> Result row[1][2] { @@ -2034,7 +2034,7 @@ impl Animatable for MatrixDecomposed3D { // 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 *= self_portion as f32; + theta *= self_portion; scale *= theta.sin(); // Scale the self matrix by self_portion. @@ -2068,12 +2068,12 @@ impl Animatable for MatrixDecomposed3D { } let theta = product.acos(); - let w = (other_portion as f32 * theta).sin() * 1.0 / (1.0 - product * product).sqrt(); + let w = (other_portion * theta).sin() * 1.0 / (1.0 - product * product).sqrt(); let mut a = *self; let mut b = *other; % for i in range(4): - a.quaternion.${i} *= (other_portion as f32 * theta).cos() - product * w; + a.quaternion.${i} *= (other_portion * theta).cos() - product * w; b.quaternion.${i} *= w; sum.quaternion.${i} = a.quaternion.${i} + b.quaternion.${i}; % endfor @@ -2110,15 +2110,15 @@ impl From for ComputedMatrix { // Construct a composite rotation matrix from the quaternion values // rotationMatrix is a identity 4x4 matrix initially let mut rotation_matrix = ComputedMatrix::identity(); - rotation_matrix.m11 = 1.0 - 2.0 * (y * y + z * z); - rotation_matrix.m12 = 2.0 * (x * y + z * w); - rotation_matrix.m13 = 2.0 * (x * z - y * w); - rotation_matrix.m21 = 2.0 * (x * y - z * w); - rotation_matrix.m22 = 1.0 - 2.0 * (x * x + z * z); - rotation_matrix.m23 = 2.0 * (y * z + x * w); - rotation_matrix.m31 = 2.0 * (x * z + y * w); - rotation_matrix.m32 = 2.0 * (y * z - x * w); - rotation_matrix.m33 = 1.0 - 2.0 * (x * x + y * y); + 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 = multiply(rotation_matrix, matrix); From 3a5cbfb76935eaa05057d3ac07620c3ada3a6782 Mon Sep 17 00:00:00 2001 From: Boris Chiou Date: Wed, 9 Aug 2017 11:40:27 +0800 Subject: [PATCH 3/4] Implement ComputeSquaredDistance for TransformList. --- .../helpers/animated_properties.mako.rs | 178 +++++++++++++++++- 1 file changed, 174 insertions(+), 4 deletions(-) diff --git a/components/style/properties/helpers/animated_properties.mako.rs b/components/style/properties/helpers/animated_properties.mako.rs index 8e6a2802203..9d9f261496a 100644 --- a/components/style/properties/helpers/animated_properties.mako.rs +++ b/components/style/properties/helpers/animated_properties.mako.rs @@ -8,7 +8,7 @@ use app_units::Au; use cssparser::Parser; -use euclid::{Point2D, Size2D}; +use euclid::{Point2D, Point3D, Size2D}; #[cfg(feature = "gecko")] use gecko_bindings::bindings::RawServoAnimationValueMap; #[cfg(feature = "gecko")] use gecko_bindings::structs::RawGeckoGfxMatrix4x4; #[cfg(feature = "gecko")] use gecko_bindings::structs::nsCSSPropertyID; @@ -1794,6 +1794,67 @@ pub struct MatrixDecomposed3D { pub quaternion: Quaternion, } +/// A wrapper of Point3D to represent the direction vector (rotate axis) for Rotate3D. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "servo", derive(HeapSizeOf))] +pub struct DirectionVector(Point3D); + +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.0001f64, + "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.0.x * (angle / 2.).sin(), + vector.0.y * (angle / 2.).sin(), + vector.0.z * (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 + } +} + +impl DirectionVector { + /// Create a DirectionVector. + #[inline] + fn new(x: f64, y: f64, z: f64) -> Self { + DirectionVector(Point3D::new(x, y, z)) + } + + /// Return the normalized direction vector. + #[inline] + fn normalize(&mut self) -> bool { + let len = self.length(); + if len > 0. { + self.0.x = self.0.x / len; + self.0.y = self.0.y / len; + self.0.z = self.0.z / len; + true + } else { + false + } + } + + /// Get the length of this vector. + #[inline] + fn length(&self) -> f64 { + self.0.to_array().iter().fold(0f64, |sum, v| sum + v * v).sqrt() + } +} + /// Decompose a 3D matrix. /// https://drafts.csswg.org/css-transforms/#decomposing-a-3d-matrix fn decompose_3d_matrix(mut matrix: ComputedMatrix) -> Result { @@ -2362,11 +2423,120 @@ impl Animatable for TransformList { } } +/// A helper function to retrieve the pixel length and percentage value. +fn extract_pixel_calc_value(lop: &LengthOrPercentage) -> (f64, CSSFloat) { + match lop { + &LengthOrPercentage::Length(au) => (au.to_f64_px(), 0.), + &LengthOrPercentage::Percentage(percent) => (0., percent.0), + &LengthOrPercentage::Calc(calc) => (calc.length().to_f64_px(), calc.percentage()) + } +} + +/// Compute the squared distance of two transform lists. +// 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. +fn compute_transform_lists_squared_distance(from_list: &[TransformOperation], + to_list: &[TransformOperation]) + -> Result { + let zero_distance = SquaredDistance::Value(0.); + let squared_distance = from_list.iter().zip(to_list.iter()).map(|(from, to)| { + match (from, to) { + (&TransformOperation::Matrix(_from), + &TransformOperation::Matrix(_to)) => { + // TODO: decompose matrix. + zero_distance + } + (&TransformOperation::Skew(fx, fy), + &TransformOperation::Skew(tx, ty)) => { + fx.compute_squared_distance(&tx).unwrap_or(zero_distance) + + fy.compute_squared_distance(&ty).unwrap_or(zero_distance) + } + (&TransformOperation::Translate(fx, fy, fz), + &TransformOperation::Translate(tx, ty, tz)) => { + // 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. + // Note: We use pixel value to compute the distance for translate, so we have to + // convert Au into px. + let diff_x = fx.add_weighted(&tx, 1., -1.).unwrap_or(LengthOrPercentage::zero()); + let diff_y = fy.add_weighted(&ty, 1., -1.).unwrap_or(LengthOrPercentage::zero()); + let (diff_x_length, _) = extract_pixel_calc_value(&diff_x); + let (diff_y_length, _) = extract_pixel_calc_value(&diff_y); + SquaredDistance::Value(diff_x_length * diff_x_length) + + SquaredDistance::Value(diff_y_length * diff_y_length) + + fz.to_f64_px().compute_squared_distance(&tz.to_f64_px()).unwrap_or(zero_distance) + } + (&TransformOperation::Scale(fx, fy, fz), + &TransformOperation::Scale(tx, ty, tz)) => { + fx.compute_squared_distance(&tx).unwrap_or(zero_distance) + + fy.compute_squared_distance(&ty).unwrap_or(zero_distance) + + fz.compute_squared_distance(&tz).unwrap_or(zero_distance) + } + (&TransformOperation::Rotate(fx, fy, fz, fa), + &TransformOperation::Rotate(tx, ty, tz, ta)) => { + // A direction vector that cannot be normalized, such as [0,0,0], will cause the + // rotation to not be applied. i.e. Use an identity matrix or rotate3d(0, 0, 1, 0). + let get_normalized_vector_and_angle = |x: f32, y: f32, z: f32, angle: Angle| + -> (DirectionVector, Angle) { + let mut vector = DirectionVector::new(x as f64, y as f64, z as f64); + if vector.normalize() { + (vector, angle) + } else { + (DirectionVector::new(0., 0., 1.), Angle::zero()) + } + }; + + let (vector1, angle1) = get_normalized_vector_and_angle(fx, fy, fz, fa); + let (vector2, angle2) = get_normalized_vector_and_angle(tx, ty, tz, ta); + if vector1 == vector2 { + angle1.compute_squared_distance(&angle2).unwrap_or(zero_distance) + } else { + // 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 q1 = Quaternion::from_direction_and_angle(&vector1, angle1.radians64()); + let q2 = Quaternion::from_direction_and_angle(&vector2, angle2.radians64()); + let dist = q1.dot(&q2).max(-1.).min(1.).acos() * 2.0; + SquaredDistance::Value(dist * dist) + } + } + (&TransformOperation::Perspective(_fd), + &TransformOperation::Perspective(_td)) => { + // TODO: decompose matrix. + zero_distance + } + _ => { + // We don't support computation of distance for InterpolateMatrix and + // AccumulateMatrix. + zero_distance + } + } + }).sum(); + + Ok(squared_distance) +} + impl ComputeSquaredDistance for TransformList { #[inline] - fn compute_squared_distance(&self, _other: &Self) -> Result { - // FIXME: This should be implemented. - Err(()) + fn compute_squared_distance(&self, other: &Self) -> Result { + match (self.0.as_ref(), other.0.as_ref()) { + (Some(from_list), Some(to_list)) => { + if can_interpolate_list(from_list, to_list) { + compute_transform_lists_squared_distance(from_list, to_list) + } else { + // Bug 1390039: we don't handle mismatch transform lists for now. + Err(()) + } + }, + (Some(list), None) | (None, Some(list)) => { + let none = build_identity_transform_list(list); + compute_transform_lists_squared_distance(list, &none) + } + _ => Ok(SquaredDistance::Value(0.)) + } } } From e8fad236effdfd3f7b7662f000d766560a8186c6 Mon Sep 17 00:00:00 2001 From: Boris Chiou Date: Thu, 10 Aug 2017 17:24:22 +0800 Subject: [PATCH 4/4] Implement ComputeSquaredDistance for Matrix and Perspective. --- .../helpers/animated_properties.mako.rs | 106 ++++++++++++++---- 1 file changed, 86 insertions(+), 20 deletions(-) diff --git a/components/style/properties/helpers/animated_properties.mako.rs b/components/style/properties/helpers/animated_properties.mako.rs index 9d9f261496a..cd0d01a5d42 100644 --- a/components/style/properties/helpers/animated_properties.mako.rs +++ b/components/style/properties/helpers/animated_properties.mako.rs @@ -1497,21 +1497,25 @@ fn rotate_to_matrix(x: f32, y: f32, z: f32, a: Angle) -> ComputedMatrix { } /// A 2d matrix for interpolation. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] #[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 ComputedMatrix. pub struct InnerMatrix2D { pub m11: CSSFloat, pub m12: CSSFloat, pub m21: CSSFloat, pub m22: CSSFloat, } /// A 2d translation function. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub struct Translate2D(f32, f32); /// A 2d scale function. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub struct Scale2D(f32, f32); @@ -1606,6 +1610,20 @@ impl Animatable for MatrixDecomposed2D { } } +impl ComputeSquaredDistance for MatrixDecomposed2D { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result { + // 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 Animatable for ComputedMatrix { fn add_weighted(&self, other: &Self, self_portion: f64, other_portion: f64) -> Result { if self.is_3d() || other.is_3d() { @@ -1630,6 +1648,21 @@ impl Animatable for ComputedMatrix { } } +impl ComputeSquaredDistance for ComputedMatrix { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result { + 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) + } + } +} + impl From for MatrixDecomposed2D { /// Decompose a 2D matrix. /// https://drafts.csswg.org/css-transforms/#decomposing-a-2d-matrix @@ -1754,12 +1787,12 @@ impl From for RawGeckoGfxMatrix4x4 { } /// A 3d translation. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub struct Translate3D(f32, f32, f32); /// A 3d scale function. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub struct Scale3D(f32, f32, f32); @@ -1769,7 +1802,7 @@ pub struct Scale3D(f32, f32, f32); pub struct Skew(f32, f32, f32); /// A 3d perspective transformation. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub struct Perspective(f32, f32, f32, f32); @@ -1779,7 +1812,7 @@ pub struct Perspective(f32, f32, f32, f32); pub struct Quaternion(f64, f64, f64, f64); /// A decomposed 3d matrix. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] #[cfg_attr(feature = "servo", derive(HeapSizeOf))] pub struct MatrixDecomposed3D { /// A translation function. @@ -1827,6 +1860,17 @@ impl Quaternion { } } +impl ComputeSquaredDistance for Quaternion { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result { + // 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::Value(distance * distance)) + } +} + impl DirectionVector { /// Create a DirectionVector. #[inline] @@ -2053,6 +2097,17 @@ impl Animatable for Skew { } } +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 { + 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())?) + } +} + impl Animatable for Perspective { fn add_weighted(&self, other: &Self, self_portion: f64, other_portion: f64) -> Result { Ok(Perspective( @@ -2443,10 +2498,9 @@ fn compute_transform_lists_squared_distance(from_list: &[TransformOperation], let zero_distance = SquaredDistance::Value(0.); let squared_distance = from_list.iter().zip(to_list.iter()).map(|(from, to)| { match (from, to) { - (&TransformOperation::Matrix(_from), - &TransformOperation::Matrix(_to)) => { - // TODO: decompose matrix. - zero_distance + (&TransformOperation::Matrix(from), + &TransformOperation::Matrix(to)) => { + from.compute_squared_distance(&to).unwrap_or(zero_distance) } (&TransformOperation::Skew(fx, fy), &TransformOperation::Skew(tx, ty)) => { @@ -2494,19 +2548,31 @@ fn compute_transform_lists_squared_distance(from_list: &[TransformOperation], if vector1 == vector2 { angle1.compute_squared_distance(&angle2).unwrap_or(zero_distance) } else { - // 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 q1 = Quaternion::from_direction_and_angle(&vector1, angle1.radians64()); let q2 = Quaternion::from_direction_and_angle(&vector2, angle2.radians64()); - let dist = q1.dot(&q2).max(-1.).min(1.).acos() * 2.0; - SquaredDistance::Value(dist * dist) + q1.compute_squared_distance(&q2).unwrap_or(zero_distance) } } - (&TransformOperation::Perspective(_fd), - &TransformOperation::Perspective(_td)) => { - // TODO: decompose matrix. - zero_distance + (&TransformOperation::Perspective(fd), + &TransformOperation::Perspective(td)) => { + let mut fd_matrix = ComputedMatrix::identity(); + let mut td_matrix = ComputedMatrix::identity(); + if fd.0 > 0 { + fd_matrix.m34 = -1. / fd.to_f32_px(); + } + + if td.0 > 0 { + td_matrix.m34 = -1. / td.to_f32_px(); + } + fd_matrix.compute_squared_distance(&td_matrix).unwrap_or(zero_distance) + } + (&TransformOperation::Perspective(p), &TransformOperation::Matrix(m)) | + (&TransformOperation::Matrix(m), &TransformOperation::Perspective(p)) => { + let mut p_matrix = ComputedMatrix::identity(); + if p.0 > 0 { + p_matrix.m34 = -1. / p.to_f32_px(); + } + p_matrix.compute_squared_distance(&m).unwrap_or(zero_distance) } _ => { // We don't support computation of distance for InterpolateMatrix and