/* 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/. */ //! Generic types for CSS values that are related to transformations. use crate::values::computed::length::Length as ComputedLength; use crate::values::computed::length::LengthPercentage as ComputedLengthPercentage; use crate::values::specified::angle::Angle as SpecifiedAngle; use crate::values::specified::length::Length as SpecifiedLength; use crate::values::specified::length::LengthPercentage as SpecifiedLengthPercentage; use crate::values::{computed, CSSFloat}; use crate::{Zero, ZeroNoPercent}; use euclid; use euclid::default::{Rect, Transform3D}; use std::fmt::{self, Write}; use style_traits::{CssWriter, ToCss}; /// A generic 2D transformation matrix. #[allow(missing_docs)] #[derive( Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[css(comma, function = "matrix")] #[repr(C)] pub struct GenericMatrix { pub a: T, pub b: T, pub c: T, pub d: T, pub e: T, pub f: T, } pub use self::GenericMatrix as Matrix; #[allow(missing_docs)] #[cfg_attr(rustfmt, rustfmt_skip)] #[derive( Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[css(comma, function = "matrix3d")] #[repr(C)] pub struct GenericMatrix3D { pub m11: T, pub m12: T, pub m13: T, pub m14: T, pub m21: T, pub m22: T, pub m23: T, pub m24: T, pub m31: T, pub m32: T, pub m33: T, pub m34: T, pub m41: T, pub m42: T, pub m43: T, pub m44: T, } pub use self::GenericMatrix3D as Matrix3D; #[cfg_attr(rustfmt, rustfmt_skip)] impl> From> for Transform3D { #[inline] fn from(m: Matrix) -> Self { Transform3D::new( m.a.into(), m.b.into(), 0.0, 0.0, m.c.into(), m.d.into(), 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, m.e.into(), m.f.into(), 0.0, 1.0, ) } } #[cfg_attr(rustfmt, rustfmt_skip)] impl> From> for Transform3D { #[inline] fn from(m: Matrix3D) -> Self { Transform3D::new( m.m11.into(), m.m12.into(), m.m13.into(), m.m14.into(), m.m21.into(), m.m22.into(), m.m23.into(), m.m24.into(), m.m31.into(), m.m32.into(), m.m33.into(), m.m34.into(), m.m41.into(), m.m42.into(), m.m43.into(), m.m44.into(), ) } } /// A generic transform origin. #[derive( Animate, Clone, ComputeSquaredDistance, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToAnimatedZero, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(C)] pub struct GenericTransformOrigin { /// The horizontal origin. pub horizontal: H, /// The vertical origin. pub vertical: V, /// The depth. pub depth: Depth, } pub use self::GenericTransformOrigin as TransformOrigin; impl TransformOrigin { /// Returns a new transform origin. pub fn new(horizontal: H, vertical: V, depth: D) -> Self { Self { horizontal, vertical, depth, } } } fn is_same(x: &N, y: &N) -> bool { x == y } /// A value for the `perspective()` transform function, which is either a /// non-negative `` or `none`. #[derive( Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(C, u8)] pub enum GenericPerspectiveFunction { /// `none` None, /// A ``. Length(L), } impl GenericPerspectiveFunction { /// Returns `f32::INFINITY` or the result of a function on the length value. pub fn infinity_or(&self, f: impl FnOnce(&L) -> f32) -> f32 { match *self { Self::None => f32::INFINITY, Self::Length(ref l) => f(l), } } } pub use self::GenericPerspectiveFunction as PerspectiveFunction; #[derive( Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(C, u8)] /// A single operation in the list of a `transform` value pub enum GenericTransformOperation where Angle: Zero, LengthPercentage: Zero + ZeroNoPercent, Number: PartialEq, { /// Represents a 2D 2x3 matrix. Matrix(GenericMatrix), /// Represents a 3D 4x4 matrix. Matrix3D(GenericMatrix3D), /// A 2D skew. /// /// If the second angle is not provided it is assumed zero. /// /// Syntax can be skew(angle) or skew(angle, angle) #[css(comma, function)] Skew(Angle, #[css(skip_if = "Zero::is_zero")] Angle), /// skewX(angle) #[css(function = "skewX")] SkewX(Angle), /// skewY(angle) #[css(function = "skewY")] SkewY(Angle), /// translate(x, y) or translate(x) #[css(comma, function)] Translate( LengthPercentage, #[css(skip_if = "ZeroNoPercent::is_zero_no_percent")] LengthPercentage, ), /// translateX(x) #[css(function = "translateX")] TranslateX(LengthPercentage), /// translateY(y) #[css(function = "translateY")] TranslateY(LengthPercentage), /// translateZ(z) #[css(function = "translateZ")] TranslateZ(Length), /// translate3d(x, y, z) #[css(comma, function = "translate3d")] Translate3D(LengthPercentage, LengthPercentage, Length), /// A 2D scaling factor. /// /// Syntax can be scale(factor) or scale(factor, factor) #[css(comma, function)] Scale(Number, #[css(contextual_skip_if = "is_same")] Number), /// scaleX(factor) #[css(function = "scaleX")] ScaleX(Number), /// scaleY(factor) #[css(function = "scaleY")] ScaleY(Number), /// scaleZ(factor) #[css(function = "scaleZ")] ScaleZ(Number), /// scale3D(factorX, factorY, factorZ) #[css(comma, function = "scale3d")] Scale3D(Number, Number, Number), /// Describes a 2D Rotation. /// /// In a 3D scene `rotate(angle)` is equivalent to `rotateZ(angle)`. #[css(function)] Rotate(Angle), /// Rotation in 3D space around the x-axis. #[css(function = "rotateX")] RotateX(Angle), /// Rotation in 3D space around the y-axis. #[css(function = "rotateY")] RotateY(Angle), /// Rotation in 3D space around the z-axis. #[css(function = "rotateZ")] RotateZ(Angle), /// Rotation in 3D space. /// /// Generalization of rotateX, rotateY and rotateZ. #[css(comma, function = "rotate3d")] Rotate3D(Number, Number, Number, Angle), /// Specifies a perspective projection matrix. /// /// Part of CSS Transform Module Level 2 and defined at /// [ยง 13.1. 3D Transform Function](https://drafts.csswg.org/css-transforms-2/#funcdef-perspective). /// /// The value must be greater than or equal to zero. #[css(function)] Perspective(GenericPerspectiveFunction), /// A intermediate type for interpolation of mismatched transform lists. #[allow(missing_docs)] #[css(comma, function = "interpolatematrix")] InterpolateMatrix { from_list: GenericTransform< GenericTransformOperation, >, to_list: GenericTransform< GenericTransformOperation, >, progress: computed::Percentage, }, /// A intermediate type for accumulation of mismatched transform lists. #[allow(missing_docs)] #[css(comma, function = "accumulatematrix")] AccumulateMatrix { from_list: GenericTransform< GenericTransformOperation, >, to_list: GenericTransform< GenericTransformOperation, >, count: Integer, }, } pub use self::GenericTransformOperation as TransformOperation; #[derive( Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(C)] /// A value of the `transform` property pub struct GenericTransform(#[css(if_empty = "none", iterable)] pub crate::OwnedSlice); pub use self::GenericTransform as Transform; impl TransformOperation where Angle: Zero, LengthPercentage: Zero + ZeroNoPercent, Number: PartialEq, { /// Check if it is any rotate function. pub fn is_rotate(&self) -> bool { use self::TransformOperation::*; matches!( *self, Rotate(..) | Rotate3D(..) | RotateX(..) | RotateY(..) | RotateZ(..) ) } /// Check if it is any translate function pub fn is_translate(&self) -> bool { use self::TransformOperation::*; match *self { Translate(..) | Translate3D(..) | TranslateX(..) | TranslateY(..) | TranslateZ(..) => { true }, _ => false, } } /// Check if it is any scale function pub fn is_scale(&self) -> bool { use self::TransformOperation::*; match *self { Scale(..) | Scale3D(..) | ScaleX(..) | ScaleY(..) | ScaleZ(..) => true, _ => false, } } } /// Convert a length type into the absolute lengths. pub trait ToAbsoluteLength { /// Returns the absolute length as pixel value. fn to_pixel_length(&self, containing_len: Option) -> Result; } impl ToAbsoluteLength for SpecifiedLength { // This returns Err(()) if there is any relative length or percentage. We use this when // parsing a transform list of DOMMatrix because we want to return a DOM Exception // if there is relative length. #[inline] fn to_pixel_length(&self, _containing_len: Option) -> Result { match *self { SpecifiedLength::NoCalc(len) => len.to_computed_pixel_length_without_context(), SpecifiedLength::Calc(ref calc) => calc.to_computed_pixel_length_without_context(), } } } impl ToAbsoluteLength for SpecifiedLengthPercentage { // This returns Err(()) if there is any relative length or percentage. We use this when // parsing a transform list of DOMMatrix because we want to return a DOM Exception // if there is relative length. #[inline] fn to_pixel_length(&self, _containing_len: Option) -> Result { use self::SpecifiedLengthPercentage::*; match *self { Length(len) => len.to_computed_pixel_length_without_context(), Calc(ref calc) => calc.to_computed_pixel_length_without_context(), Percentage(..) => Err(()), } } } impl ToAbsoluteLength for ComputedLength { #[inline] fn to_pixel_length(&self, _containing_len: Option) -> Result { Ok(self.px()) } } impl ToAbsoluteLength for ComputedLengthPercentage { #[inline] fn to_pixel_length(&self, containing_len: Option) -> Result { Ok(self .maybe_percentage_relative_to(containing_len) .ok_or(())? .px()) } } /// Support the conversion to a 3d matrix. pub trait ToMatrix { /// Check if it is a 3d transform function. fn is_3d(&self) -> bool; /// Return the equivalent 3d matrix. fn to_3d_matrix( &self, reference_box: Option<&Rect>, ) -> Result, ()>; } /// A little helper to deal with both specified and computed angles. pub trait ToRadians { /// Return the radians value as a 64-bit floating point value. fn radians64(&self) -> f64; } impl ToRadians for computed::angle::Angle { #[inline] fn radians64(&self) -> f64 { computed::angle::Angle::radians64(self) } } impl ToRadians for SpecifiedAngle { #[inline] fn radians64(&self) -> f64 { computed::angle::Angle::from_degrees(self.degrees()).radians64() } } impl ToMatrix for TransformOperation where Angle: Zero + ToRadians + Copy, Number: PartialEq + Copy + Into + Into, Length: ToAbsoluteLength, LoP: Zero + ToAbsoluteLength + ZeroNoPercent, { #[inline] fn is_3d(&self) -> bool { use self::TransformOperation::*; match *self { Translate3D(..) | TranslateZ(..) | Rotate3D(..) | RotateX(..) | RotateY(..) | RotateZ(..) | Scale3D(..) | ScaleZ(..) | Perspective(..) | Matrix3D(..) => true, _ => false, } } /// If |reference_box| is None, we will drop the percent part from translate because /// we cannot resolve it without the layout info, for computed TransformOperation. /// However, for specified TransformOperation, we will return Err(()) if there is any relative /// lengths because the only caller, DOMMatrix, doesn't accept relative lengths. #[inline] fn to_3d_matrix( &self, reference_box: Option<&Rect>, ) -> Result, ()> { use self::TransformOperation::*; let reference_width = reference_box.map(|v| v.size.width); let reference_height = reference_box.map(|v| v.size.height); let matrix = match *self { Rotate3D(ax, ay, az, theta) => { let theta = theta.radians64(); let (ax, ay, az, theta) = get_normalized_vector_and_angle(ax.into(), ay.into(), az.into(), theta); Transform3D::rotation( ax as f64, ay as f64, az as f64, euclid::Angle::radians(theta), ) }, RotateX(theta) => { let theta = euclid::Angle::radians(theta.radians64()); Transform3D::rotation(1., 0., 0., theta) }, RotateY(theta) => { let theta = euclid::Angle::radians(theta.radians64()); Transform3D::rotation(0., 1., 0., theta) }, RotateZ(theta) | Rotate(theta) => { let theta = euclid::Angle::radians(theta.radians64()); Transform3D::rotation(0., 0., 1., theta) }, Perspective(ref p) => { let px = match p { PerspectiveFunction::None => f32::INFINITY, PerspectiveFunction::Length(ref p) => p.to_pixel_length(None)?, }; create_perspective_matrix(px).cast() }, Scale3D(sx, sy, sz) => Transform3D::scale(sx.into(), sy.into(), sz.into()), Scale(sx, sy) => Transform3D::scale(sx.into(), sy.into(), 1.), ScaleX(s) => Transform3D::scale(s.into(), 1., 1.), ScaleY(s) => Transform3D::scale(1., s.into(), 1.), ScaleZ(s) => Transform3D::scale(1., 1., s.into()), Translate3D(ref tx, ref ty, ref tz) => { let tx = tx.to_pixel_length(reference_width)? as f64; let ty = ty.to_pixel_length(reference_height)? as f64; Transform3D::translation(tx, ty, tz.to_pixel_length(None)? as f64) }, Translate(ref tx, ref ty) => { let tx = tx.to_pixel_length(reference_width)? as f64; let ty = ty.to_pixel_length(reference_height)? as f64; Transform3D::translation(tx, ty, 0.) }, TranslateX(ref t) => { let t = t.to_pixel_length(reference_width)? as f64; Transform3D::translation(t, 0., 0.) }, TranslateY(ref t) => { let t = t.to_pixel_length(reference_height)? as f64; Transform3D::translation(0., t, 0.) }, TranslateZ(ref z) => Transform3D::translation(0., 0., z.to_pixel_length(None)? as f64), Skew(theta_x, theta_y) => Transform3D::skew( euclid::Angle::radians(theta_x.radians64()), euclid::Angle::radians(theta_y.radians64()), ), SkewX(theta) => Transform3D::skew( euclid::Angle::radians(theta.radians64()), euclid::Angle::radians(0.), ), SkewY(theta) => Transform3D::skew( euclid::Angle::radians(0.), euclid::Angle::radians(theta.radians64()), ), Matrix3D(m) => m.into(), Matrix(m) => m.into(), InterpolateMatrix { .. } | AccumulateMatrix { .. } => { // TODO: Convert InterpolateMatrix/AccumulateMatrix into a valid Transform3D by // the reference box and do interpolation on these two Transform3D matrices. // Both Gecko and Servo don't support this for computing distance, and Servo // doesn't support animations on InterpolateMatrix/AccumulateMatrix, so // return an identity matrix. // Note: DOMMatrix doesn't go into this arm. Transform3D::identity() }, }; Ok(matrix) } } impl Transform { /// `none` pub fn none() -> Self { Transform(Default::default()) } } impl Transform { /// Return the equivalent 3d matrix of this transform list. /// /// We return a pair: the first one is the transform matrix, and the second one /// indicates if there is any 3d transform function in this transform list. #[cfg_attr(rustfmt, rustfmt_skip)] pub fn to_transform_3d_matrix( &self, reference_box: Option<&Rect> ) -> Result<(Transform3D, bool), ()> { Self::components_to_transform_3d_matrix(&self.0, reference_box) } /// Converts a series of components to a 3d matrix. #[cfg_attr(rustfmt, rustfmt_skip)] pub fn components_to_transform_3d_matrix( ops: &[T], reference_box: Option<&Rect>, ) -> Result<(Transform3D, bool), ()> { let cast_3d_transform = |m: Transform3D| -> Transform3D { use std::{f32, f64}; let cast = |v: f64| v.min(f32::MAX as f64).max(f32::MIN as f64) as f32; Transform3D::new( cast(m.m11), cast(m.m12), cast(m.m13), cast(m.m14), cast(m.m21), cast(m.m22), cast(m.m23), cast(m.m24), cast(m.m31), cast(m.m32), cast(m.m33), cast(m.m34), cast(m.m41), cast(m.m42), cast(m.m43), cast(m.m44), ) }; let (m, is_3d) = Self::components_to_transform_3d_matrix_f64(ops, reference_box)?; Ok((cast_3d_transform(m), is_3d)) } /// Same as Transform::to_transform_3d_matrix but a f64 version. fn components_to_transform_3d_matrix_f64( ops: &[T], reference_box: Option<&Rect>, ) -> Result<(Transform3D, bool), ()> { // We intentionally use Transform3D during computation to avoid // error propagation because using f32 to compute triangle functions // (e.g. in rotation()) is not accurate enough. In Gecko, we also use // "double" to compute the triangle functions. Therefore, let's use // Transform3D during matrix computation and cast it into f32 in // the end. let mut transform = Transform3D::::identity(); let mut contain_3d = false; for operation in ops { let matrix = operation.to_3d_matrix(reference_box)?; contain_3d = contain_3d || operation.is_3d(); transform = matrix.then(&transform); } Ok((transform, contain_3d)) } } /// Return the transform matrix from a perspective length. #[inline] pub fn create_perspective_matrix(d: CSSFloat) -> Transform3D { if d.is_finite() { Transform3D::perspective(d.max(1.)) } else { Transform3D::identity() } } /// Return the normalized direction vector and its angle for Rotate3D. pub fn get_normalized_vector_and_angle( x: CSSFloat, y: CSSFloat, z: CSSFloat, angle: T, ) -> (CSSFloat, CSSFloat, CSSFloat, T) { use crate::values::computed::transform::DirectionVector; use euclid::approxeq::ApproxEq; let vector = DirectionVector::new(x, y, z); if vector.square_length().approx_eq(&f32::zero()) { // https://www.w3.org/TR/css-transforms-1/#funcdef-rotate3d // A direction vector that cannot be normalized, such as [0, 0, 0], will cause the // rotation to not be applied, so we use identity matrix (i.e. rotate3d(0, 0, 1, 0)). (0., 0., 1., T::zero()) } else { let vector = vector.robust_normalize(); (vector.x, vector.y, vector.z, angle) } } #[derive( Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, SpecifiedValueInfo, ToAnimatedZero, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(C, u8)] /// A value of the `Rotate` property /// /// pub enum GenericRotate { /// 'none' None, /// '' Rotate(Angle), /// '{3} ' Rotate3D(Number, Number, Number, Angle), } pub use self::GenericRotate as Rotate; /// A trait to check if the current 3D vector is parallel to the DirectionVector. /// This is especially for serialization on Rotate. pub trait IsParallelTo { /// Returns true if this is parallel to the vector. fn is_parallel_to(&self, vector: &computed::transform::DirectionVector) -> bool; } impl ToCss for Rotate where Number: Copy + ToCss + Zero, Angle: ToCss, (Number, Number, Number): IsParallelTo, { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, { use crate::values::computed::transform::DirectionVector; match *self { Rotate::None => dest.write_str("none"), Rotate::Rotate(ref angle) => angle.to_css(dest), Rotate::Rotate3D(x, y, z, ref angle) => { // If the axis is parallel with the x or y axes, it must serialize as the // appropriate keyword. If a rotation about the z axis (that is, in 2D) is // specified, the property must serialize as just an // // https://drafts.csswg.org/css-transforms-2/#individual-transform-serialization let v = (x, y, z); let axis = if x.is_zero() && y.is_zero() && z.is_zero() { // The zero length vector is parallel to every other vector, so // is_parallel_to() returns true for it. However, it is definitely different // from x axis, y axis, or z axis, and it's meaningless to perform a rotation // using that direction vector. So we *have* to serialize it using that same // vector - we can't simplify to some theoretically parallel axis-aligned // vector. None } else if v.is_parallel_to(&DirectionVector::new(1., 0., 0.)) { Some("x ") } else if v.is_parallel_to(&DirectionVector::new(0., 1., 0.)) { Some("y ") } else if v.is_parallel_to(&DirectionVector::new(0., 0., 1.)) { // When we're parallel to the z-axis, we can just serialize the angle. return angle.to_css(dest); } else { None }; match axis { Some(a) => dest.write_str(a)?, None => { x.to_css(dest)?; dest.write_char(' ')?; y.to_css(dest)?; dest.write_char(' ')?; z.to_css(dest)?; dest.write_char(' ')?; }, } angle.to_css(dest) }, } } } #[derive( Clone, Copy, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, SpecifiedValueInfo, ToAnimatedZero, ToComputedValue, ToResolvedValue, ToShmem, )] #[repr(C, u8)] /// A value of the `Scale` property /// /// pub enum GenericScale { /// 'none' None, /// '{1,3}' Scale(Number, Number, Number), } pub use self::GenericScale as Scale; impl ToCss for Scale where Number: ToCss + PartialEq + Copy, f32: From, { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: fmt::Write, f32: From, { match *self { Scale::None => dest.write_str("none"), Scale::Scale(ref x, ref y, ref z) => { x.to_css(dest)?; let is_3d = f32::from(*z) != 1.0; if is_3d || x != y { dest.write_char(' ')?; y.to_css(dest)?; } if is_3d { dest.write_char(' ')?; z.to_css(dest)?; } Ok(()) }, } } } #[inline] fn y_axis_and_z_axis_are_zero( _: &LengthPercentage, y: &LengthPercentage, z: &Length, ) -> bool { y.is_zero_no_percent() && z.is_zero() } #[derive( Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize, SpecifiedValueInfo, ToAnimatedZero, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(C, u8)] /// A value of the `translate` property /// /// https://drafts.csswg.org/css-transforms-2/#individual-transform-serialization: /// /// If a 2d translation is specified, the property must serialize with only one /// or two values (per usual, if the second value is 0px, the default, it must /// be omitted when serializing; however if 0% is the second value, it is included). /// /// If a 3d translation is specified and the value can be expressed as 2d, we treat as 2d and /// serialize accoringly. Otherwise, we serialize all three values. /// https://github.com/w3c/csswg-drafts/issues/3305 /// /// pub enum GenericTranslate where LengthPercentage: Zero + ZeroNoPercent, Length: Zero, { /// 'none' None, /// [ ? ]? Translate( LengthPercentage, #[css(contextual_skip_if = "y_axis_and_z_axis_are_zero")] LengthPercentage, #[css(skip_if = "Zero::is_zero")] Length, ), } pub use self::GenericTranslate as Translate; #[allow(missing_docs)] #[derive( Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum TransformStyle { Flat, #[css(keyword = "preserve-3d")] Preserve3d, }