/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* 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.
usesuper::animate_multiplicative_factor; usesuper::{Animate, Procedure, ToAnimatedZero}; usecrate::values::computed::transform::Rotate as ComputedRotate; usecrate::values::computed::transform::Scale as ComputedScale; usecrate::values::computed::transform::Transform as ComputedTransform; usecrate::values::computed::transform::TransformOperation as ComputedTransformOperation; usecrate::values::computed::transform::Translate as ComputedTranslate; usecrate::values::computed::transform::{DirectionVector, Matrix, Matrix3D}; usecrate::values::computed::Angle; usecrate::values::computed::{Length, LengthPercentage}; usecrate::values::computed::{Number, Percentage}; usecrate::values::distance::{ComputeSquaredDistance, SquaredDistance}; usecrate::values::generics::transform::{self, Transform, TransformOperation}; usecrate::values::generics::transform::{Rotate, Scale, Translate}; usecrate::values::CSSFloat; usecrate::Zero; use std::cmp; use std::ops::Add;
// ------------------------------------ // 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. pubstruct InnerMatrix2D { pub m11: CSSFloat, pub m12: CSSFloat, pub m21: CSSFloat, pub m22: CSSFloat,
}
// 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)?;
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);
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 this = Matrix3D::from(*self); let other = Matrix3D::from(*other); let from = decompose_2d_matrix(&this)?; let to = decompose_2d_matrix(&other)?;
Matrix3D::from(from.animate(&to, procedure)?).into_2d()
}
}
/// A 3d translation. #[cfg_attr(feature = "servo", derive(MallocSizeOf))] #[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)] pubstruct Translate3D(pub f32, pub f32, pub f32);
/// A 3d scale function. #[derive(Clone, ComputeSquaredDistance, Copy, Debug)] #[cfg_attr(feature = "servo", derive(MallocSizeOf))] pubstruct Scale3D(pub f32, pub f32, pub f32);
/// A 3d skew function. #[cfg_attr(feature = "servo", derive(MallocSizeOf))] #[derive(Animate, Clone, Copy, Debug)] pubstruct 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 quaternion used to represent a rotation. #[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "servo", derive(MallocSizeOf))] pubstruct 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"
);
// Quaternions between the range [360, 720] will treated as rotations at the other // direction: [-360, 0]. And quaternions between the range [720*k, 720*(k+1)] will be // treated as rotations [0, 720]. So it does not make sense to use quaternions to rotate // the element more than ±360deg. Therefore, we have to make sure its range is (-360, 360). let half_angle = angle
.abs()
.rem_euclid(std::f64::consts::TAU)
.copysign(angle) / 2.;
// 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 * half_angle.sin(),
vector.y as f64 * half_angle.sin(),
vector.z as f64 * half_angle.sin(),
half_angle.cos(),
)
}
impl Animate for Quaternion { fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { 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). iflet Procedure::Accumulate { .. } = procedure {
debug_assert_eq!(other_weight, 1.0); if this_weight == 0.0 { return Ok(*other);
}
if cos_half_theta.abs() == 1.0 { return Ok(*self);
}
let half_theta = cos_half_theta.acos(); let sin_half_theta = (1.0 - cos_half_theta * cos_half_theta).sqrt();
let right_weight = (other_weight * half_theta).sin() / sin_half_theta; // The spec would like to use // "(other_weight * half_theta).cos() - cos_half_theta * right_weight". However, this // formula may produce some precision issues of floating-point number calculation, e.g. // when the progress is 100% (i.e. |other_weight| is 1), the |left_weight| may not be // perfectly equal to 0. It could be something like -2.22e-16, which is approximately equal // to zero, in the test. And after we recompose the Matrix3D, these approximated zeros // make us failed to treat this Matrix3D as a Matrix2D, when serializating it. // // Therefore, we use another formula to calculate |left_weight| here. Blink and WebKit also // use this formula, which is defined in: // https://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/index.htm // https://github.com/w3c/csswg-drafts/issues/9338 let left_weight = (this_weight * half_theta).sin() / sin_half_theta;
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))] pubstruct 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,
}
// 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 letmut 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;
// 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. letmut perspective_matrix = matrix;
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 letmut row = matrix.get_matrix_3x3_part();
// 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 in0..3 {
row[i][0] *= -1.0;
row[i][1] *= -1.0;
row[i][2] *= -1.0;
}
}
// Now, get the rotations out. letmut 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
}
impl Animate for Matrix3D { #[cfg(feature = "servo")] fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { ifself.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) = ifself.is_3d() || other.is_3d() {
(decompose_3d_matrix(*self)?, decompose_3d_matrix(*other)?)
} else {
(decompose_2d_matrix(self)?, decompose_2d_matrix(other)?)
}; // 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.
Ok(Matrix3D::from(from.animate(&to, procedure)?))
}
}
impl ComputeSquaredDistance for Matrix3D { #[inline] #[cfg(feature = "servo")] fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { ifself.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)
}
}
// 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 letmut 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)
};
impl ComputeSquaredDistance for ComputedTransform { #[inline] fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { let squared_dist = super::lists::with_zero::squared_distance(&>self.0, &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);
}
// 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))
},
_ ifself.is_translate() && other.is_translate() => self
.to_translate_3d()
.animate(&other.to_translate_3d(), procedure),
_ ifself.is_scale() && other.is_scale() => { self.to_scale_3d().animate(&other.to_scale_3d(), procedure)
},
_ ifself.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)?;
Ok(Self::Matrix3D(
Matrix3D::from(left).animate(&Matrix3D::from(right), procedure)?,
))
}
// 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? letmut 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
_ ifself.is_translate() && other.is_translate() => self
.to_translate_3d()
.compute_squared_distance(&other.to_translate_3d()),
_ ifself.is_scale() && other.is_scale() => self
.to_scale_3d()
.compute_squared_distance(&other.to_scale_3d()),
_ ifself.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, ()> { use euclid::approxeq::ApproxEq; 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(_, ..)) => { // https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions
let (from, to) = (self.resolve(), other.resolve()); // For interpolations with the primitive rotate3d(), the direction vectors of the // transform functions get normalized first. let (fx, fy, fz, fa) =
transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3); let (tx, ty, tz, ta) =
transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3);
// The rotation angle gets interpolated numerically and the rotation vector of the // non-zero angle is used or (0, 0, 1) if both angles are zero. // // Note: the normalization may get two different vectors because of the // floating-point precision, so we have to use approx_eq to compare two // vectors. let fv = DirectionVector::new(fx, fy, fz); let tv = DirectionVector::new(tx, ty, tz); if fa.is_zero() || ta.is_zero() || fv.approx_eq(&tv) { let (x, y, z) = if fa.is_zero() && ta.is_zero() {
(0., 0., 1.)
} elseif fa.is_zero() {
(tx, ty, tz)
} else { // ta.is_zero() or both vectors are equal.
(fx, fy, fz)
}; return Ok(Rotate::Rotate3D(x, y, z, fa.animate(&ta, procedure)?));
}
// Slerp algorithm doesn't work well for Procedure::Add, which makes both // |this_weight| and |other_weight| be 1.0, and this may make the cosine value of // the angle be out of the range (i.e. the 4th component of the quaternion vector). // (See Quaternion::animate() for more details about the Slerp formula.) // Therefore, if the cosine value is out of range, we get an NaN after applying // acos() on it, and so the result is invalid. // Note: This is specialized for `rotate` property. The addition of `transform` // property has been handled in `ComputedTransform::animate()` by merging two list // directly. let rq = if procedure == Procedure::Add { // In Transform::animate(), it converts two rotations into transform matrices, // and do matrix multiplication. This match the spec definition for the // addition. // https://drafts.csswg.org/css-transforms-2/#combining-transform-lists let f = ComputedTransformOperation::Rotate3D(fx, fy, fz, fa); let t = ComputedTransformOperation::Rotate3D(tx, ty, tz, ta); let v =
Transform(vec![f].into()).animate(&Transform(vec![t].into()), procedure)?; let (m, _) = v.to_transform_3d_matrix(None)?; // Decompose the matrix and retrive the quaternion vector.
decompose_3d_matrix(Matrix3D::from(m))?.quaternion
} else { // If the normalized vectors are not equal and both rotation angles are // non-zero the transform functions get converted into 4x4 matrices first and // interpolated as defined in section Interpolation of Matrices afterwards. // However, per the spec issue [1], we prefer to converting the rotate3D into // quaternion vectors directly, and then apply Slerp algorithm. // // Both ways should be identical, and converting rotate3D into quaternion // vectors directly can avoid redundant math operations, e.g. the generation of // the equivalent matrix3D and the unnecessary decomposition parts of // translation, scale, skew, and persepctive in the matrix3D. // // [1] https://github.com/w3c/csswg-drafts/issues/9278 let fq = Quaternion::from_direction_and_angle(&fv, fa.radians64()); let tq = Quaternion::from_direction_and_angle(&tv, ta.radians64());
Quaternion::animate(&fq, &tq, procedure)?
};
debug_assert!(rq.3 <= 1.0 && rq.3 >= -1.0, "Invalid cosine value"); let (x, y, z, angle) = transform::get_normalized_vector_and_angle(
rq.0as f32,
rq.1as f32,
rq.2as 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)?))
},
}
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.