| //! CSS properties related to 2D and 3D transforms. |
| |
| use super::{Property, PropertyId}; |
| use crate::context::PropertyHandlerContext; |
| use crate::declaration::DeclarationList; |
| use crate::error::{ParserError, PrinterError}; |
| use crate::macros::enum_property; |
| use crate::prefixes::Feature; |
| use crate::printer::Printer; |
| use crate::stylesheet::PrinterOptions; |
| use crate::traits::{Parse, PropertyHandler, ToCss, Zero}; |
| use crate::values::{ |
| angle::Angle, |
| length::{Length, LengthPercentage}, |
| percentage::NumberOrPercentage, |
| }; |
| use crate::vendor_prefix::VendorPrefix; |
| #[cfg(feature = "visitor")] |
| use crate::visitor::Visit; |
| use cssparser::*; |
| use std::f32::consts::PI; |
| |
| /// A value for the [transform](https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#propdef-transform) property. |
| #[derive(Debug, Clone, PartialEq, Default)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct TransformList(pub Vec<Transform>); |
| |
| impl<'i> Parse<'i> for TransformList { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { |
| return Ok(TransformList(vec![])); |
| } |
| |
| input.skip_whitespace(); |
| let mut results = vec![Transform::parse(input)?]; |
| loop { |
| input.skip_whitespace(); |
| if let Ok(item) = input.try_parse(Transform::parse) { |
| results.push(item); |
| } else { |
| return Ok(TransformList(results)); |
| } |
| } |
| } |
| } |
| |
| impl ToCss for TransformList { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| if self.0.is_empty() { |
| dest.write_str("none")?; |
| return Ok(()); |
| } |
| |
| // TODO: Re-enable with a better solution |
| // See: https://github.com/parcel-bundler/lightningcss/issues/288 |
| if dest.minify { |
| let mut base = String::new(); |
| self.to_css_base(&mut Printer::new( |
| &mut base, |
| PrinterOptions { |
| minify: true, |
| ..PrinterOptions::default() |
| }, |
| ))?; |
| |
| dest.write_str(&base)?; |
| |
| return Ok(()); |
| } |
| // if dest.minify { |
| // // Combine transforms into a single matrix. |
| // if let Some(matrix) = self.to_matrix() { |
| // // Generate based on the original transforms. |
| // let mut base = String::new(); |
| // self.to_css_base(&mut Printer::new( |
| // &mut base, |
| // PrinterOptions { |
| // minify: true, |
| // ..PrinterOptions::default() |
| // }, |
| // ))?; |
| // |
| // // Decompose the matrix into transform functions if possible. |
| // // If the resulting length is shorter than the original, use it. |
| // if let Some(d) = matrix.decompose() { |
| // let mut decomposed = String::new(); |
| // d.to_css_base(&mut Printer::new( |
| // &mut decomposed, |
| // PrinterOptions { |
| // minify: true, |
| // ..PrinterOptions::default() |
| // }, |
| // ))?; |
| // if decomposed.len() < base.len() { |
| // base = decomposed; |
| // } |
| // } |
| // |
| // // Also generate a matrix() or matrix3d() representation and compare that. |
| // let mut mat = String::new(); |
| // if let Some(matrix) = matrix.to_matrix2d() { |
| // Transform::Matrix(matrix).to_css(&mut Printer::new( |
| // &mut mat, |
| // PrinterOptions { |
| // minify: true, |
| // ..PrinterOptions::default() |
| // }, |
| // ))? |
| // } else { |
| // Transform::Matrix3d(matrix).to_css(&mut Printer::new( |
| // &mut mat, |
| // PrinterOptions { |
| // minify: true, |
| // ..PrinterOptions::default() |
| // }, |
| // ))? |
| // } |
| // |
| // if mat.len() < base.len() { |
| // dest.write_str(&mat)?; |
| // } else { |
| // dest.write_str(&base)?; |
| // } |
| // |
| // return Ok(()); |
| // } |
| // } |
| |
| self.to_css_base(dest) |
| } |
| } |
| |
| impl TransformList { |
| fn to_css_base<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| for item in &self.0 { |
| item.to_css(dest)?; |
| } |
| Ok(()) |
| } |
| |
| /// Converts the transform list to a 3D matrix if possible. |
| pub fn to_matrix(&self) -> Option<Matrix3d<f32>> { |
| let mut matrix = Matrix3d::identity(); |
| for transform in &self.0 { |
| if let Some(m) = transform.to_matrix() { |
| matrix = m.multiply(&matrix); |
| } else { |
| return None; |
| } |
| } |
| Some(matrix) |
| } |
| } |
| |
| /// An individual [transform function](https://www.w3.org/TR/2019/CR-css-transforms-1-20190214/#two-d-transform-functions). |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "camelCase") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum Transform { |
| /// A 2D translation. |
| Translate(LengthPercentage, LengthPercentage), |
| /// A translation in the X direction. |
| TranslateX(LengthPercentage), |
| /// A translation in the Y direction. |
| TranslateY(LengthPercentage), |
| /// A translation in the Z direction. |
| TranslateZ(Length), |
| /// A 3D translation. |
| Translate3d(LengthPercentage, LengthPercentage, Length), |
| /// A 2D scale. |
| Scale(NumberOrPercentage, NumberOrPercentage), |
| /// A scale in the X direction. |
| ScaleX(NumberOrPercentage), |
| /// A scale in the Y direction. |
| ScaleY(NumberOrPercentage), |
| /// A scale in the Z direction. |
| ScaleZ(NumberOrPercentage), |
| /// A 3D scale. |
| Scale3d(NumberOrPercentage, NumberOrPercentage, NumberOrPercentage), |
| /// A 2D rotation. |
| Rotate(Angle), |
| /// A rotation around the X axis. |
| RotateX(Angle), |
| /// A rotation around the Y axis. |
| RotateY(Angle), |
| /// A rotation around the Z axis. |
| RotateZ(Angle), |
| /// A 3D rotation. |
| Rotate3d(f32, f32, f32, Angle), |
| /// A 2D skew. |
| Skew(Angle, Angle), |
| /// A skew along the X axis. |
| SkewX(Angle), |
| /// A skew along the Y axis. |
| SkewY(Angle), |
| /// A perspective transform. |
| Perspective(Length), |
| /// A 2D matrix transform. |
| Matrix(Matrix<f32>), |
| /// A 3D matrix transform. |
| Matrix3d(Matrix3d<f32>), |
| } |
| |
| /// A 2D matrix. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[allow(missing_docs)] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct Matrix<T> { |
| pub a: T, |
| pub b: T, |
| pub c: T, |
| pub d: T, |
| pub e: T, |
| pub f: T, |
| } |
| |
| impl Matrix<f32> { |
| /// Converts the matrix to a 3D matrix. |
| pub fn to_matrix3d(&self) -> Matrix3d<f32> { |
| Matrix3d { |
| m11: self.a, |
| m12: self.b, |
| m13: 0.0, |
| m14: 0.0, |
| m21: self.c, |
| m22: self.d, |
| m23: 0.0, |
| m24: 0.0, |
| m31: 0.0, |
| m32: 0.0, |
| m33: 1.0, |
| m34: 0.0, |
| m41: self.e, |
| m42: self.f, |
| m43: 0.0, |
| m44: 1.0, |
| } |
| } |
| } |
| |
| /// A 3D matrix. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[allow(missing_docs)] |
| pub struct Matrix3d<T> { |
| 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, |
| } |
| |
| // https://drafts.csswg.org/css-transforms-2/#mathematical-description |
| impl Matrix3d<f32> { |
| /// Creates an identity matrix. |
| pub fn identity() -> Matrix3d<f32> { |
| Matrix3d { |
| m11: 1.0, |
| m12: 0.0, |
| m13: 0.0, |
| m14: 0.0, |
| m21: 0.0, |
| m22: 1.0, |
| m23: 0.0, |
| m24: 0.0, |
| m31: 0.0, |
| m32: 0.0, |
| m33: 1.0, |
| m34: 0.0, |
| m41: 0.0, |
| m42: 0.0, |
| m43: 0.0, |
| m44: 1.0, |
| } |
| } |
| |
| /// Creates a translation matrix. |
| pub fn translate(x: f32, y: f32, z: f32) -> Matrix3d<f32> { |
| Matrix3d { |
| m11: 1.0, |
| m12: 0.0, |
| m13: 0.0, |
| m14: 0.0, |
| m21: 0.0, |
| m22: 1.0, |
| m23: 0.0, |
| m24: 0.0, |
| m31: 0.0, |
| m32: 0.0, |
| m33: 1.0, |
| m34: 0.0, |
| m41: x, |
| m42: y, |
| m43: z, |
| m44: 1.0, |
| } |
| } |
| |
| /// Creates a scale matrix. |
| pub fn scale(x: f32, y: f32, z: f32) -> Matrix3d<f32> { |
| Matrix3d { |
| m11: x, |
| m12: 0.0, |
| m13: 0.0, |
| m14: 0.0, |
| m21: 0.0, |
| m22: y, |
| m23: 0.0, |
| m24: 0.0, |
| m31: 0.0, |
| m32: 0.0, |
| m33: z, |
| m34: 0.0, |
| m41: 0.0, |
| m42: 0.0, |
| m43: 0.0, |
| m44: 1.0, |
| } |
| } |
| |
| /// Creates a rotation matrix. |
| pub fn rotate(x: f32, y: f32, z: f32, angle: f32) -> Matrix3d<f32> { |
| // Normalize the vector. |
| let length = (x * x + y * y + z * z).sqrt(); |
| if length == 0.0 { |
| // A direction vector that cannot be normalized, such as [0,0,0], will cause the rotation to not be applied. |
| return Matrix3d::identity(); |
| } |
| |
| let x = x / length; |
| let y = y / length; |
| let z = z / length; |
| |
| let half_angle = angle / 2.0; |
| let sin = half_angle.sin(); |
| let sc = sin * half_angle.cos(); |
| let sq = sin * sin; |
| let m11 = 1.0 - 2.0 * (y * y + z * z) * sq; |
| let m12 = 2.0 * (x * y * sq + z * sc); |
| let m13 = 2.0 * (x * z * sq - y * sc); |
| let m21 = 2.0 * (x * y * sq - z * sc); |
| let m22 = 1.0 - 2.0 * (x * x + z * z) * sq; |
| let m23 = 2.0 * (y * z * sq + x * sc); |
| let m31 = 2.0 * (x * z * sq + y * sc); |
| let m32 = 2.0 * (y * z * sq - x * sc); |
| let m33 = 1.0 - 2.0 * (x * x + y * y) * sq; |
| Matrix3d { |
| m11, |
| m12, |
| m13, |
| m14: 0.0, |
| m21, |
| m22, |
| m23, |
| m24: 0.0, |
| m31, |
| m32, |
| m33, |
| m34: 0.0, |
| m41: 0.0, |
| m42: 0.0, |
| m43: 0.0, |
| m44: 1.0, |
| } |
| } |
| |
| /// Creates a skew matrix. |
| pub fn skew(a: f32, b: f32) -> Matrix3d<f32> { |
| Matrix3d { |
| m11: 1.0, |
| m12: b.tan(), |
| m13: 0.0, |
| m14: 0.0, |
| m21: a.tan(), |
| m22: 1.0, |
| m23: 0.0, |
| m24: 0.0, |
| m31: 0.0, |
| m32: 0.0, |
| m33: 1.0, |
| m34: 0.0, |
| m41: 0.0, |
| m42: 0.0, |
| m43: 0.0, |
| m44: 1.0, |
| } |
| } |
| |
| /// Creates a perspective matrix. |
| pub fn perspective(d: f32) -> Matrix3d<f32> { |
| Matrix3d { |
| m11: 1.0, |
| m12: 0.0, |
| m13: 0.0, |
| m14: 0.0, |
| m21: 0.0, |
| m22: 1.0, |
| m23: 0.0, |
| m24: 0.0, |
| m31: 0.0, |
| m32: 0.0, |
| m33: 1.0, |
| m34: -1.0 / d, |
| m41: 0.0, |
| m42: 0.0, |
| m43: 0.0, |
| m44: 1.0, |
| } |
| } |
| |
| /// Multiplies this matrix by another, returning a new matrix. |
| pub fn multiply(&self, other: &Self) -> Self { |
| Matrix3d { |
| m11: self.m11 * other.m11 + self.m12 * other.m21 + self.m13 * other.m31 + self.m14 * other.m41, |
| m12: self.m11 * other.m12 + self.m12 * other.m22 + self.m13 * other.m32 + self.m14 * other.m42, |
| m13: self.m11 * other.m13 + self.m12 * other.m23 + self.m13 * other.m33 + self.m14 * other.m43, |
| m14: self.m11 * other.m14 + self.m12 * other.m24 + self.m13 * other.m34 + self.m14 * other.m44, |
| m21: self.m21 * other.m11 + self.m22 * other.m21 + self.m23 * other.m31 + self.m24 * other.m41, |
| m22: self.m21 * other.m12 + self.m22 * other.m22 + self.m23 * other.m32 + self.m24 * other.m42, |
| m23: self.m21 * other.m13 + self.m22 * other.m23 + self.m23 * other.m33 + self.m24 * other.m43, |
| m24: self.m21 * other.m14 + self.m22 * other.m24 + self.m23 * other.m34 + self.m24 * other.m44, |
| m31: self.m31 * other.m11 + self.m32 * other.m21 + self.m33 * other.m31 + self.m34 * other.m41, |
| m32: self.m31 * other.m12 + self.m32 * other.m22 + self.m33 * other.m32 + self.m34 * other.m42, |
| m33: self.m31 * other.m13 + self.m32 * other.m23 + self.m33 * other.m33 + self.m34 * other.m43, |
| m34: self.m31 * other.m14 + self.m32 * other.m24 + self.m33 * other.m34 + self.m34 * other.m44, |
| m41: self.m41 * other.m11 + self.m42 * other.m21 + self.m43 * other.m31 + self.m44 * other.m41, |
| m42: self.m41 * other.m12 + self.m42 * other.m22 + self.m43 * other.m32 + self.m44 * other.m42, |
| m43: self.m41 * other.m13 + self.m42 * other.m23 + self.m43 * other.m33 + self.m44 * other.m43, |
| m44: self.m41 * other.m14 + self.m42 * other.m24 + self.m43 * other.m34 + self.m44 * other.m44, |
| } |
| } |
| |
| /// Returns whether this matrix could be converted to a 2D matrix. |
| pub fn is_2d(&self) -> bool { |
| self.m31 == 0.0 |
| && self.m32 == 0.0 |
| && self.m13 == 0.0 |
| && self.m23 == 0.0 |
| && self.m43 == 0.0 |
| && self.m14 == 0.0 |
| && self.m24 == 0.0 |
| && self.m34 == 0.0 |
| && self.m33 == 1.0 |
| && self.m44 == 1.0 |
| } |
| |
| /// Attempts to convert the matrix to 2D. |
| /// Returns `None` if the conversion is not possible. |
| pub fn to_matrix2d(&self) -> Option<Matrix<f32>> { |
| if self.is_2d() { |
| return Some(Matrix { |
| a: self.m11, |
| b: self.m12, |
| c: self.m21, |
| d: self.m22, |
| e: self.m41, |
| f: self.m42, |
| }); |
| } |
| None |
| } |
| |
| /// Scales the matrix by the given factor. |
| pub fn scale_by_factor(&mut self, scaling_factor: f32) { |
| self.m11 *= scaling_factor; |
| self.m12 *= scaling_factor; |
| self.m13 *= scaling_factor; |
| self.m14 *= scaling_factor; |
| self.m21 *= scaling_factor; |
| self.m22 *= scaling_factor; |
| self.m23 *= scaling_factor; |
| self.m24 *= scaling_factor; |
| self.m31 *= scaling_factor; |
| self.m32 *= scaling_factor; |
| self.m33 *= scaling_factor; |
| self.m34 *= scaling_factor; |
| self.m41 *= scaling_factor; |
| self.m42 *= scaling_factor; |
| self.m43 *= scaling_factor; |
| self.m44 *= scaling_factor; |
| } |
| |
| /// Returns the determinant of the matrix. |
| pub fn determinant(&self) -> f32 { |
| self.m14 * self.m23 * self.m32 * self.m41 |
| - self.m13 * self.m24 * self.m32 * self.m41 |
| - self.m14 * self.m22 * self.m33 * self.m41 |
| + self.m12 * self.m24 * self.m33 * self.m41 |
| + self.m13 * self.m22 * self.m34 * self.m41 |
| - self.m12 * self.m23 * self.m34 * self.m41 |
| - self.m14 * self.m23 * self.m31 * self.m42 |
| + self.m13 * self.m24 * self.m31 * self.m42 |
| + self.m14 * self.m21 * self.m33 * self.m42 |
| - self.m11 * self.m24 * self.m33 * self.m42 |
| - self.m13 * self.m21 * self.m34 * self.m42 |
| + self.m11 * self.m23 * self.m34 * self.m42 |
| + self.m14 * self.m22 * self.m31 * self.m43 |
| - self.m12 * self.m24 * self.m31 * self.m43 |
| - self.m14 * self.m21 * self.m32 * self.m43 |
| + self.m11 * self.m24 * self.m32 * self.m43 |
| + self.m12 * self.m21 * self.m34 * self.m43 |
| - self.m11 * self.m22 * self.m34 * self.m43 |
| - self.m13 * self.m22 * self.m31 * self.m44 |
| + self.m12 * self.m23 * self.m31 * self.m44 |
| + self.m13 * self.m21 * self.m32 * self.m44 |
| - self.m11 * self.m23 * self.m32 * self.m44 |
| - self.m12 * self.m21 * self.m33 * self.m44 |
| + self.m11 * self.m22 * self.m33 * self.m44 |
| } |
| |
| /// Returns the inverse of the matrix if possible. |
| pub fn inverse(&self) -> Option<Matrix3d<f32>> { |
| let mut det = self.determinant(); |
| if det == 0.0 { |
| return None; |
| } |
| |
| det = 1.0 / det; |
| Some(Matrix3d { |
| m11: det |
| * (self.m23 * self.m34 * self.m42 - self.m24 * self.m33 * self.m42 + self.m24 * self.m32 * self.m43 |
| - self.m22 * self.m34 * self.m43 |
| - self.m23 * self.m32 * self.m44 |
| + self.m22 * self.m33 * self.m44), |
| m12: det |
| * (self.m14 * self.m33 * self.m42 - self.m13 * self.m34 * self.m42 - self.m14 * self.m32 * self.m43 |
| + self.m12 * self.m34 * self.m43 |
| + self.m13 * self.m32 * self.m44 |
| - self.m12 * self.m33 * self.m44), |
| m13: det |
| * (self.m13 * self.m24 * self.m42 - self.m14 * self.m23 * self.m42 + self.m14 * self.m22 * self.m43 |
| - self.m12 * self.m24 * self.m43 |
| - self.m13 * self.m22 * self.m44 |
| + self.m12 * self.m23 * self.m44), |
| m14: det |
| * (self.m14 * self.m23 * self.m32 - self.m13 * self.m24 * self.m32 - self.m14 * self.m22 * self.m33 |
| + self.m12 * self.m24 * self.m33 |
| + self.m13 * self.m22 * self.m34 |
| - self.m12 * self.m23 * self.m34), |
| m21: det |
| * (self.m24 * self.m33 * self.m41 - self.m23 * self.m34 * self.m41 - self.m24 * self.m31 * self.m43 |
| + self.m21 * self.m34 * self.m43 |
| + self.m23 * self.m31 * self.m44 |
| - self.m21 * self.m33 * self.m44), |
| m22: det |
| * (self.m13 * self.m34 * self.m41 - self.m14 * self.m33 * self.m41 + self.m14 * self.m31 * self.m43 |
| - self.m11 * self.m34 * self.m43 |
| - self.m13 * self.m31 * self.m44 |
| + self.m11 * self.m33 * self.m44), |
| m23: det |
| * (self.m14 * self.m23 * self.m41 - self.m13 * self.m24 * self.m41 - self.m14 * self.m21 * self.m43 |
| + self.m11 * self.m24 * self.m43 |
| + self.m13 * self.m21 * self.m44 |
| - self.m11 * self.m23 * self.m44), |
| m24: det |
| * (self.m13 * self.m24 * self.m31 - self.m14 * self.m23 * self.m31 + self.m14 * self.m21 * self.m33 |
| - self.m11 * self.m24 * self.m33 |
| - self.m13 * self.m21 * self.m34 |
| + self.m11 * self.m23 * self.m34), |
| m31: det |
| * (self.m22 * self.m34 * self.m41 - self.m24 * self.m32 * self.m41 + self.m24 * self.m31 * self.m42 |
| - self.m21 * self.m34 * self.m42 |
| - self.m22 * self.m31 * self.m44 |
| + self.m21 * self.m32 * self.m44), |
| m32: det |
| * (self.m14 * self.m32 * self.m41 - self.m12 * self.m34 * self.m41 - self.m14 * self.m31 * self.m42 |
| + self.m11 * self.m34 * self.m42 |
| + self.m12 * self.m31 * self.m44 |
| - self.m11 * self.m32 * self.m44), |
| m33: det |
| * (self.m12 * self.m24 * self.m41 - self.m14 * self.m22 * self.m41 + self.m14 * self.m21 * self.m42 |
| - self.m11 * self.m24 * self.m42 |
| - self.m12 * self.m21 * self.m44 |
| + self.m11 * self.m22 * self.m44), |
| m34: det |
| * (self.m14 * self.m22 * self.m31 - self.m12 * self.m24 * self.m31 - self.m14 * self.m21 * self.m32 |
| + self.m11 * self.m24 * self.m32 |
| + self.m12 * self.m21 * self.m34 |
| - self.m11 * self.m22 * self.m34), |
| m41: det |
| * (self.m23 * self.m32 * self.m41 - self.m22 * self.m33 * self.m41 - self.m23 * self.m31 * self.m42 |
| + self.m21 * self.m33 * self.m42 |
| + self.m22 * self.m31 * self.m43 |
| - self.m21 * self.m32 * self.m43), |
| m42: det |
| * (self.m12 * self.m33 * self.m41 - self.m13 * self.m32 * self.m41 + self.m13 * self.m31 * self.m42 |
| - self.m11 * self.m33 * self.m42 |
| - self.m12 * self.m31 * self.m43 |
| + self.m11 * self.m32 * self.m43), |
| m43: det |
| * (self.m13 * self.m22 * self.m41 - self.m12 * self.m23 * self.m41 - self.m13 * self.m21 * self.m42 |
| + self.m11 * self.m23 * self.m42 |
| + self.m12 * self.m21 * self.m43 |
| - self.m11 * self.m22 * self.m43), |
| m44: det |
| * (self.m12 * self.m23 * self.m31 - self.m13 * self.m22 * self.m31 + self.m13 * self.m21 * self.m32 |
| - self.m11 * self.m23 * self.m32 |
| - self.m12 * self.m21 * self.m33 |
| + self.m11 * self.m22 * self.m33), |
| }) |
| } |
| |
| /// Transposes the matrix. |
| pub fn transpose(&self) -> Self { |
| Self { |
| m11: self.m11, |
| m12: self.m21, |
| m13: self.m31, |
| m14: self.m41, |
| m21: self.m12, |
| m22: self.m22, |
| m23: self.m32, |
| m24: self.m42, |
| m31: self.m13, |
| m32: self.m23, |
| m33: self.m33, |
| m34: self.m43, |
| m41: self.m14, |
| m42: self.m24, |
| m43: self.m34, |
| m44: self.m44, |
| } |
| } |
| |
| /// Multiplies a vector by the matrix. |
| pub fn multiply_vector(&self, pin: &[f32; 4]) -> [f32; 4] { |
| [ |
| pin[0] * self.m11 + pin[1] * self.m21 + pin[2] * self.m31 + pin[3] * self.m41, |
| pin[0] * self.m12 + pin[1] * self.m22 + pin[2] * self.m32 + pin[3] * self.m42, |
| pin[0] * self.m13 + pin[1] * self.m23 + pin[2] * self.m33 + pin[3] * self.m43, |
| pin[0] * self.m14 + pin[1] * self.m24 + pin[2] * self.m34 + pin[3] * self.m44, |
| ] |
| } |
| |
| /// Decomposes the matrix into a list of transform functions if possible. |
| pub fn decompose(&self) -> Option<TransformList> { |
| // https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix |
| // 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 self.m44 == 0.0 { |
| return None; |
| } |
| |
| let scaling_factor = self.m44; |
| |
| // Normalize the matrix. |
| let mut matrix = self.clone(); |
| 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.clone(); |
| 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 None; |
| } |
| |
| let mut transforms = vec![]; |
| |
| // First, isolate 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.multiply_vector(&right_hand_side); |
| if perspective[0] == 0.0 && perspective[1] == 0.0 && perspective[3] == 0.0 { |
| transforms.push(Transform::Perspective(Length::px(-1.0 / perspective[2]))) |
| } else { |
| return None; |
| } |
| } |
| |
| // Next take care of translation (easy). |
| // let translate = Translate3D(matrix.m41, matrix.m42, matrix.m43); |
| if matrix.m41 != 0.0 || matrix.m42 != 0.0 || matrix.m43 != 0.0 { |
| transforms.push(Transform::Translate3d( |
| LengthPercentage::px(matrix.m41), |
| LengthPercentage::px(matrix.m42), |
| Length::px(matrix.m43), |
| )); |
| } |
| |
| // Now get scale and shear. 'row' is a 3 element array of 3 component vectors |
| let mut row = [ |
| [matrix.m11, matrix.m12, matrix.m13], |
| [matrix.m21, matrix.m22, matrix.m23], |
| [matrix.m31, matrix.m32, matrix.m33], |
| ]; |
| |
| // 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_x = row0len; |
| 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_x = dot(row[0], row[1]); |
| row[1] = combine(row[1], row[0], 1.0, -skew_x); |
| |
| // 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(); |
| let mut scale_y = row1len; |
| row[1] = [row[1][0] / row1len, row[1][1] / row1len, row[1][2] / row1len]; |
| skew_x /= scale_y; |
| |
| // Compute XZ and YZ shears, orthogonalize 3rd row |
| let mut skew_y = dot(row[0], row[2]); |
| row[2] = combine(row[2], row[0], 1.0, -skew_y); |
| let mut skew_z = dot(row[1], row[2]); |
| row[2] = combine(row[2], row[1], 1.0, -skew_z); |
| |
| // 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(); |
| let mut scale_z = row2len; |
| row[2] = [row[2][0] / row2len, row[2][1] / row2len, row[2][2] / row2len]; |
| skew_y /= scale_z; |
| skew_z /= scale_z; |
| |
| if skew_z != 0.0 { |
| return None; // ??? |
| } |
| |
| // Round to 5 digits of precision, which is what we print. |
| macro_rules! round { |
| ($var: ident) => { |
| $var = ($var * 100000.0).round() / 100000.0; |
| }; |
| } |
| |
| round!(skew_x); |
| round!(skew_y); |
| round!(skew_z); |
| |
| if skew_x != 0.0 || skew_y != 0.0 || skew_z != 0.0 { |
| transforms.push(Transform::Skew(Angle::Rad(skew_x), Angle::Rad(skew_y))); |
| } |
| |
| // 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_x = -scale_x; |
| scale_y = -scale_y; |
| scale_z = -scale_z; |
| for i in 0..3 { |
| row[i][0] *= -1.0; |
| row[i][1] *= -1.0; |
| row[i][2] *= -1.0; |
| } |
| } |
| |
| round!(scale_x); |
| round!(scale_y); |
| round!(scale_z); |
| |
| if scale_x != 1.0 || scale_y != 1.0 || scale_z != 1.0 { |
| transforms.push(Transform::Scale3d( |
| NumberOrPercentage::Number(scale_x), |
| NumberOrPercentage::Number(scale_y), |
| NumberOrPercentage::Number(scale_z), |
| )) |
| } |
| |
| // Now, get the rotations out. |
| let mut rotate_x = 0.5 * ((1.0 + row[0][0] - row[1][1] - row[2][2]).max(0.0)).sqrt(); |
| let mut rotate_y = 0.5 * ((1.0 - row[0][0] + row[1][1] - row[2][2]).max(0.0)).sqrt(); |
| let mut rotate_z = 0.5 * ((1.0 - row[0][0] - row[1][1] + row[2][2]).max(0.0)).sqrt(); |
| let rotate_w = 0.5 * ((1.0 + row[0][0] + row[1][1] + row[2][2]).max(0.0)).sqrt(); |
| |
| if row[2][1] > row[1][2] { |
| rotate_x = -rotate_x |
| } |
| |
| if row[0][2] > row[2][0] { |
| rotate_y = -rotate_y |
| } |
| |
| if row[1][0] > row[0][1] { |
| rotate_z = -rotate_z |
| } |
| |
| let len = (rotate_x * rotate_x + rotate_y * rotate_y + rotate_z * rotate_z).sqrt(); |
| if len != 0.0 { |
| rotate_x /= len; |
| rotate_y /= len; |
| rotate_z /= len; |
| } |
| let a = 2.0 * len.atan2(rotate_w); |
| |
| // normalize the vector so one of the values is 1 |
| let max = rotate_x.max(rotate_y).max(rotate_z); |
| rotate_x /= max; |
| rotate_y /= max; |
| rotate_z /= max; |
| |
| if a != 0.0 { |
| transforms.push(Transform::Rotate3d(rotate_x, rotate_y, rotate_z, Angle::Rad(a))) |
| } |
| |
| if transforms.is_empty() { |
| return None; |
| } |
| |
| Some(TransformList(transforms)) |
| } |
| } |
| |
| impl<'i> Parse<'i> for Transform { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| let function = input.expect_function()?.clone(); |
| input.parse_nested_block(|input| { |
| let location = input.current_source_location(); |
| match_ignore_ascii_case! { &function, |
| "matrix" => { |
| let a = f32::parse(input)?; |
| input.expect_comma()?; |
| let b = f32::parse(input)?; |
| input.expect_comma()?; |
| let c = f32::parse(input)?; |
| input.expect_comma()?; |
| let d = f32::parse(input)?; |
| input.expect_comma()?; |
| let e = f32::parse(input)?; |
| input.expect_comma()?; |
| let f = f32::parse(input)?; |
| Ok(Transform::Matrix(Matrix { a, b, c, d, e, f })) |
| }, |
| "matrix3d" => { |
| let m11 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m12 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m13 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m14 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m21 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m22 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m23 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m24 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m31 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m32 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m33 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m34 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m41 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m42 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m43 = f32::parse(input)?; |
| input.expect_comma()?; |
| let m44 = f32::parse(input)?; |
| Ok(Transform::Matrix3d(Matrix3d { |
| m11, m12, m13, m14, |
| m21, m22, m23, m24, |
| m31, m32, m33, m34, |
| m41, m42, m43, m44 |
| })) |
| }, |
| "translate" => { |
| let x = LengthPercentage::parse(input)?; |
| if input.try_parse(|input| input.expect_comma()).is_ok() { |
| let y = LengthPercentage::parse(input)?; |
| Ok(Transform::Translate(x, y)) |
| } else { |
| Ok(Transform::Translate(x, LengthPercentage::zero())) |
| } |
| }, |
| "translatex" => { |
| let x = LengthPercentage::parse(input)?; |
| Ok(Transform::TranslateX(x)) |
| }, |
| "translatey" => { |
| let y = LengthPercentage::parse(input)?; |
| Ok(Transform::TranslateY(y)) |
| }, |
| "translatez" => { |
| let z = Length::parse(input)?; |
| Ok(Transform::TranslateZ(z)) |
| }, |
| "translate3d" => { |
| let x = LengthPercentage::parse(input)?; |
| input.expect_comma()?; |
| let y = LengthPercentage::parse(input)?; |
| input.expect_comma()?; |
| let z = Length::parse(input)?; |
| Ok(Transform::Translate3d(x, y, z)) |
| }, |
| "scale" => { |
| let x = NumberOrPercentage::parse(input)?; |
| if input.try_parse(|input| input.expect_comma()).is_ok() { |
| let y = NumberOrPercentage::parse(input)?; |
| Ok(Transform::Scale(x, y)) |
| } else { |
| Ok(Transform::Scale(x.clone(), x)) |
| } |
| }, |
| "scalex" => { |
| let x = NumberOrPercentage::parse(input)?; |
| Ok(Transform::ScaleX(x)) |
| }, |
| "scaley" => { |
| let y = NumberOrPercentage::parse(input)?; |
| Ok(Transform::ScaleY(y)) |
| }, |
| "scalez" => { |
| let z = NumberOrPercentage::parse(input)?; |
| Ok(Transform::ScaleZ(z)) |
| }, |
| "scale3d" => { |
| let x = NumberOrPercentage::parse(input)?; |
| input.expect_comma()?; |
| let y = NumberOrPercentage::parse(input)?; |
| input.expect_comma()?; |
| let z = NumberOrPercentage::parse(input)?; |
| Ok(Transform::Scale3d(x, y, z)) |
| }, |
| "rotate" => { |
| let angle = Angle::parse_with_unitless_zero(input)?; |
| Ok(Transform::Rotate(angle)) |
| }, |
| "rotatex" => { |
| let angle = Angle::parse_with_unitless_zero(input)?; |
| Ok(Transform::RotateX(angle)) |
| }, |
| "rotatey" => { |
| let angle = Angle::parse_with_unitless_zero(input)?; |
| Ok(Transform::RotateY(angle)) |
| }, |
| "rotatez" => { |
| let angle = Angle::parse_with_unitless_zero(input)?; |
| Ok(Transform::RotateZ(angle)) |
| }, |
| "rotate3d" => { |
| let x = f32::parse(input)?; |
| input.expect_comma()?; |
| let y = f32::parse(input)?; |
| input.expect_comma()?; |
| let z = f32::parse(input)?; |
| input.expect_comma()?; |
| let angle = Angle::parse_with_unitless_zero(input)?; |
| Ok(Transform::Rotate3d(x, y, z, angle)) |
| }, |
| "skew" => { |
| let x = Angle::parse_with_unitless_zero(input)?; |
| if input.try_parse(|input| input.expect_comma()).is_ok() { |
| let y = Angle::parse_with_unitless_zero(input)?; |
| Ok(Transform::Skew(x, y)) |
| } else { |
| Ok(Transform::Skew(x, Angle::Deg(0.0))) |
| } |
| }, |
| "skewx" => { |
| let angle = Angle::parse_with_unitless_zero(input)?; |
| Ok(Transform::SkewX(angle)) |
| }, |
| "skewy" => { |
| let angle = Angle::parse_with_unitless_zero(input)?; |
| Ok(Transform::SkewY(angle)) |
| }, |
| "perspective" => { |
| let len = Length::parse(input)?; |
| Ok(Transform::Perspective(len)) |
| }, |
| _ => Err(location.new_unexpected_token_error( |
| cssparser::Token::Ident(function.clone()) |
| )) |
| } |
| }) |
| } |
| } |
| |
| impl ToCss for Transform { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| use Transform::*; |
| match self { |
| Translate(x, y) => { |
| if dest.minify && x.is_zero() && !y.is_zero() { |
| dest.write_str("translateY(")?; |
| y.to_css(dest)? |
| } else { |
| dest.write_str("translate(")?; |
| x.to_css(dest)?; |
| if !y.is_zero() { |
| dest.delim(',', false)?; |
| y.to_css(dest)?; |
| } |
| } |
| dest.write_char(')') |
| } |
| TranslateX(x) => { |
| dest.write_str(if dest.minify { "translate(" } else { "translateX(" })?; |
| x.to_css(dest)?; |
| dest.write_char(')') |
| } |
| TranslateY(y) => { |
| dest.write_str("translateY(")?; |
| y.to_css(dest)?; |
| dest.write_char(')') |
| } |
| TranslateZ(z) => { |
| dest.write_str("translateZ(")?; |
| z.to_css(dest)?; |
| dest.write_char(')') |
| } |
| Translate3d(x, y, z) => { |
| if dest.minify && !x.is_zero() && y.is_zero() && z.is_zero() { |
| dest.write_str("translate(")?; |
| x.to_css(dest)?; |
| } else if dest.minify && x.is_zero() && !y.is_zero() && z.is_zero() { |
| dest.write_str("translateY(")?; |
| y.to_css(dest)?; |
| } else if dest.minify && x.is_zero() && y.is_zero() && !z.is_zero() { |
| dest.write_str("translateZ(")?; |
| z.to_css(dest)?; |
| } else if dest.minify && z.is_zero() { |
| dest.write_str("translate(")?; |
| x.to_css(dest)?; |
| dest.delim(',', false)?; |
| y.to_css(dest)?; |
| } else { |
| dest.write_str("translate3d(")?; |
| x.to_css(dest)?; |
| dest.delim(',', false)?; |
| y.to_css(dest)?; |
| dest.delim(',', false)?; |
| z.to_css(dest)?; |
| } |
| dest.write_char(')') |
| } |
| Scale(x, y) => { |
| let x: f32 = x.into(); |
| let y: f32 = y.into(); |
| if dest.minify && x == 1.0 && y != 1.0 { |
| dest.write_str("scaleY(")?; |
| y.to_css(dest)?; |
| } else if dest.minify && x != 1.0 && y == 1.0 { |
| dest.write_str("scaleX(")?; |
| x.to_css(dest)?; |
| } else { |
| dest.write_str("scale(")?; |
| x.to_css(dest)?; |
| if y != x { |
| dest.delim(',', false)?; |
| y.to_css(dest)?; |
| } |
| } |
| dest.write_char(')') |
| } |
| ScaleX(x) => { |
| dest.write_str("scaleX(")?; |
| x.to_css(dest)?; |
| dest.write_char(')') |
| } |
| ScaleY(y) => { |
| dest.write_str("scaleY(")?; |
| y.to_css(dest)?; |
| dest.write_char(')') |
| } |
| ScaleZ(z) => { |
| dest.write_str("scaleZ(")?; |
| z.to_css(dest)?; |
| dest.write_char(')') |
| } |
| Scale3d(x, y, z) => { |
| let x: f32 = x.into(); |
| let y: f32 = y.into(); |
| let z: f32 = z.into(); |
| if dest.minify && z == 1.0 && x == y { |
| // scale3d(x, x, 1) => scale(x) |
| dest.write_str("scale(")?; |
| x.to_css(dest)?; |
| } else if dest.minify && x != 1.0 && y == 1.0 && z == 1.0 { |
| // scale3d(x, 1, 1) => scaleX(x) |
| dest.write_str("scaleX(")?; |
| x.to_css(dest)?; |
| } else if dest.minify && x == 1.0 && y != 1.0 && z == 1.0 { |
| // scale3d(1, y, 1) => scaleY(y) |
| dest.write_str("scaleY(")?; |
| y.to_css(dest)?; |
| } else if dest.minify && x == 1.0 && y == 1.0 && z != 1.0 { |
| // scale3d(1, 1, z) => scaleZ(z) |
| dest.write_str("scaleZ(")?; |
| z.to_css(dest)?; |
| } else if dest.minify && z == 1.0 { |
| // scale3d(x, y, 1) => scale(x, y) |
| dest.write_str("scale(")?; |
| x.to_css(dest)?; |
| dest.delim(',', false)?; |
| y.to_css(dest)?; |
| } else { |
| dest.write_str("scale3d(")?; |
| x.to_css(dest)?; |
| dest.delim(',', false)?; |
| y.to_css(dest)?; |
| dest.delim(',', false)?; |
| z.to_css(dest)?; |
| } |
| dest.write_char(')') |
| } |
| Rotate(angle) => { |
| dest.write_str("rotate(")?; |
| angle.to_css_with_unitless_zero(dest)?; |
| dest.write_char(')') |
| } |
| RotateX(angle) => { |
| dest.write_str("rotateX(")?; |
| angle.to_css_with_unitless_zero(dest)?; |
| dest.write_char(')') |
| } |
| RotateY(angle) => { |
| dest.write_str("rotateY(")?; |
| angle.to_css_with_unitless_zero(dest)?; |
| dest.write_char(')') |
| } |
| RotateZ(angle) => { |
| dest.write_str(if dest.minify { "rotate(" } else { "rotateZ(" })?; |
| angle.to_css_with_unitless_zero(dest)?; |
| dest.write_char(')') |
| } |
| Rotate3d(x, y, z, angle) => { |
| if dest.minify && *x == 1.0 && *y == 0.0 && *z == 0.0 { |
| // rotate3d(1, 0, 0, a) => rotateX(a) |
| dest.write_str("rotateX(")?; |
| angle.to_css_with_unitless_zero(dest)?; |
| } else if dest.minify && *x == 0.0 && *y == 1.0 && *z == 0.0 { |
| // rotate3d(0, 1, 0, a) => rotateY(a) |
| dest.write_str("rotateY(")?; |
| angle.to_css_with_unitless_zero(dest)?; |
| } else if dest.minify && *x == 0.0 && *y == 0.0 && *z == 1.0 { |
| // rotate3d(0, 0, 1, a) => rotate(a) |
| dest.write_str("rotate(")?; |
| angle.to_css_with_unitless_zero(dest)?; |
| } else { |
| dest.write_str("rotate3d(")?; |
| x.to_css(dest)?; |
| dest.delim(',', false)?; |
| y.to_css(dest)?; |
| dest.delim(',', false)?; |
| z.to_css(dest)?; |
| dest.delim(',', false)?; |
| angle.to_css_with_unitless_zero(dest)?; |
| } |
| dest.write_char(')') |
| } |
| Skew(x, y) => { |
| if dest.minify && x.is_zero() && !y.is_zero() { |
| dest.write_str("skewY(")?; |
| y.to_css_with_unitless_zero(dest)? |
| } else { |
| dest.write_str("skew(")?; |
| x.to_css(dest)?; |
| if !y.is_zero() { |
| dest.delim(',', false)?; |
| y.to_css_with_unitless_zero(dest)?; |
| } |
| } |
| dest.write_char(')') |
| } |
| SkewX(angle) => { |
| dest.write_str(if dest.minify { "skew(" } else { "skewX(" })?; |
| angle.to_css_with_unitless_zero(dest)?; |
| dest.write_char(')') |
| } |
| SkewY(angle) => { |
| dest.write_str("skewY(")?; |
| angle.to_css_with_unitless_zero(dest)?; |
| dest.write_char(')') |
| } |
| Perspective(len) => { |
| dest.write_str("perspective(")?; |
| len.to_css(dest)?; |
| dest.write_char(')') |
| } |
| Matrix(super::transform::Matrix { a, b, c, d, e, f }) => { |
| dest.write_str("matrix(")?; |
| a.to_css(dest)?; |
| dest.delim(',', false)?; |
| b.to_css(dest)?; |
| dest.delim(',', false)?; |
| c.to_css(dest)?; |
| dest.delim(',', false)?; |
| d.to_css(dest)?; |
| dest.delim(',', false)?; |
| e.to_css(dest)?; |
| dest.delim(',', false)?; |
| f.to_css(dest)?; |
| dest.write_char(')') |
| } |
| Matrix3d(super::transform::Matrix3d { |
| m11, |
| m12, |
| m13, |
| m14, |
| m21, |
| m22, |
| m23, |
| m24, |
| m31, |
| m32, |
| m33, |
| m34, |
| m41, |
| m42, |
| m43, |
| m44, |
| }) => { |
| dest.write_str("matrix3d(")?; |
| m11.to_css(dest)?; |
| dest.delim(',', false)?; |
| m12.to_css(dest)?; |
| dest.delim(',', false)?; |
| m13.to_css(dest)?; |
| dest.delim(',', false)?; |
| m14.to_css(dest)?; |
| dest.delim(',', false)?; |
| m21.to_css(dest)?; |
| dest.delim(',', false)?; |
| m22.to_css(dest)?; |
| dest.delim(',', false)?; |
| m23.to_css(dest)?; |
| dest.delim(',', false)?; |
| m24.to_css(dest)?; |
| dest.delim(',', false)?; |
| m31.to_css(dest)?; |
| dest.delim(',', false)?; |
| m32.to_css(dest)?; |
| dest.delim(',', false)?; |
| m33.to_css(dest)?; |
| dest.delim(',', false)?; |
| m34.to_css(dest)?; |
| dest.delim(',', false)?; |
| m41.to_css(dest)?; |
| dest.delim(',', false)?; |
| m42.to_css(dest)?; |
| dest.delim(',', false)?; |
| m43.to_css(dest)?; |
| dest.delim(',', false)?; |
| m44.to_css(dest)?; |
| dest.write_char(')') |
| } |
| } |
| } |
| } |
| |
| impl Transform { |
| /// Converts the transform to a 3D matrix. |
| pub fn to_matrix(&self) -> Option<Matrix3d<f32>> { |
| macro_rules! to_radians { |
| ($angle: ident) => {{ |
| // If the angle is negative or more than a full circle, we cannot |
| // safely convert to a matrix. Transforms are interpolated numerically |
| // when types match, and this will have different results than |
| // when interpolating matrices with large angles. |
| // https://www.w3.org/TR/css-transforms-1/#matrix-interpolation |
| let rad = $angle.to_radians(); |
| if rad < 0.0 || rad >= 2.0 * PI { |
| return None; |
| } |
| |
| rad |
| }}; |
| } |
| |
| match &self { |
| Transform::Translate(LengthPercentage::Dimension(x), LengthPercentage::Dimension(y)) => { |
| if let (Some(x), Some(y)) = (x.to_px(), y.to_px()) { |
| return Some(Matrix3d::translate(x, y, 0.0)); |
| } |
| } |
| Transform::TranslateX(LengthPercentage::Dimension(x)) => { |
| if let Some(x) = x.to_px() { |
| return Some(Matrix3d::translate(x, 0.0, 0.0)); |
| } |
| } |
| Transform::TranslateY(LengthPercentage::Dimension(y)) => { |
| if let Some(y) = y.to_px() { |
| return Some(Matrix3d::translate(0.0, y, 0.0)); |
| } |
| } |
| Transform::TranslateZ(z) => { |
| if let Some(z) = z.to_px() { |
| return Some(Matrix3d::translate(0.0, 0.0, z)); |
| } |
| } |
| Transform::Translate3d(LengthPercentage::Dimension(x), LengthPercentage::Dimension(y), z) => { |
| if let (Some(x), Some(y), Some(z)) = (x.to_px(), y.to_px(), z.to_px()) { |
| return Some(Matrix3d::translate(x, y, z)); |
| } |
| } |
| Transform::Scale(x, y) => return Some(Matrix3d::scale(x.into(), y.into(), 1.0)), |
| Transform::ScaleX(x) => return Some(Matrix3d::scale(x.into(), 1.0, 1.0)), |
| Transform::ScaleY(y) => return Some(Matrix3d::scale(1.0, y.into(), 1.0)), |
| Transform::ScaleZ(z) => return Some(Matrix3d::scale(1.0, 1.0, z.into())), |
| Transform::Scale3d(x, y, z) => return Some(Matrix3d::scale(x.into(), y.into(), z.into())), |
| Transform::Rotate(angle) | Transform::RotateZ(angle) => { |
| return Some(Matrix3d::rotate(0.0, 0.0, 1.0, to_radians!(angle))) |
| } |
| Transform::RotateX(angle) => return Some(Matrix3d::rotate(1.0, 0.0, 0.0, to_radians!(angle))), |
| Transform::RotateY(angle) => return Some(Matrix3d::rotate(0.0, 1.0, 0.0, to_radians!(angle))), |
| Transform::Rotate3d(x, y, z, angle) => return Some(Matrix3d::rotate(*x, *y, *z, to_radians!(angle))), |
| Transform::Skew(x, y) => return Some(Matrix3d::skew(to_radians!(x), to_radians!(y))), |
| Transform::SkewX(x) => return Some(Matrix3d::skew(to_radians!(x), 0.0)), |
| Transform::SkewY(y) => return Some(Matrix3d::skew(0.0, to_radians!(y))), |
| Transform::Perspective(len) => { |
| if let Some(len) = len.to_px() { |
| return Some(Matrix3d::perspective(len)); |
| } |
| } |
| Transform::Matrix(m) => return Some(m.to_matrix3d()), |
| Transform::Matrix3d(m) => return Some(m.clone()), |
| _ => {} |
| } |
| None |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [transform-style](https://drafts.csswg.org/css-transforms-2/#transform-style-property) property. |
| #[allow(missing_docs)] |
| pub enum TransformStyle { |
| Flat, |
| Preserve3d, |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [transform-box](https://drafts.csswg.org/css-transforms-1/#transform-box) property. |
| pub enum TransformBox { |
| /// Uses the content box as reference box. |
| ContentBox, |
| /// Uses the border box as reference box. |
| BorderBox, |
| /// Uses the object bounding box as reference box. |
| FillBox, |
| /// Uses the stroke bounding box as reference box. |
| StrokeBox, |
| /// Uses the nearest SVG viewport as reference box. |
| ViewBox, |
| } |
| } |
| |
| enum_property! { |
| /// A value for the [backface-visibility](https://drafts.csswg.org/css-transforms-2/#backface-visibility-property) property. |
| #[allow(missing_docs)] |
| pub enum BackfaceVisibility { |
| Visible, |
| Hidden, |
| } |
| } |
| |
| /// A value for the [perspective](https://drafts.csswg.org/css-transforms-2/#perspective-property) property. |
| #[derive(Debug, Clone, PartialEq, Parse, ToCss)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(tag = "type", content = "value", rename_all = "kebab-case") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum Perspective { |
| /// No perspective transform is applied. |
| None, |
| /// Distance to the center of projection. |
| Length(Length), |
| } |
| |
| /// A value for the [translate](https://drafts.csswg.org/css-transforms-2/#propdef-translate) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "lowercase") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum Translate { |
| /// The "none" keyword. |
| None, |
| |
| /// The x, y, and z translations. |
| #[cfg_attr(feature = "serde", serde(untagged))] |
| XYZ { |
| /// The x translation. |
| x: LengthPercentage, |
| /// The y translation. |
| y: LengthPercentage, |
| /// The z translation. |
| z: Length, |
| }, |
| } |
| |
| impl<'i> Parse<'i> for Translate { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { |
| return Ok(Translate::None); |
| } |
| |
| let x = LengthPercentage::parse(input)?; |
| let y = input.try_parse(LengthPercentage::parse); |
| let z = if y.is_ok() { |
| input.try_parse(Length::parse).ok() |
| } else { |
| None |
| }; |
| |
| Ok(Translate::XYZ { |
| x, |
| y: y.unwrap_or(LengthPercentage::zero()), |
| z: z.unwrap_or(Length::zero()), |
| }) |
| } |
| } |
| |
| impl ToCss for Translate { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| Translate::None => { |
| dest.write_str("none")?; |
| } |
| Translate::XYZ { x, y, z } => { |
| x.to_css(dest)?; |
| if !y.is_zero() || !z.is_zero() { |
| dest.write_char(' ')?; |
| y.to_css(dest)?; |
| if !z.is_zero() { |
| dest.write_char(' ')?; |
| z.to_css(dest)?; |
| } |
| } |
| } |
| }; |
| |
| Ok(()) |
| } |
| } |
| |
| impl Translate { |
| /// Converts the translation to a transform function. |
| pub fn to_transform(&self) -> Transform { |
| match self { |
| Translate::None => { |
| Transform::Translate3d(LengthPercentage::zero(), LengthPercentage::zero(), Length::zero()) |
| } |
| Translate::XYZ { x, y, z } => Transform::Translate3d(x.clone(), y.clone(), z.clone()), |
| } |
| } |
| } |
| |
| /// A value for the [rotate](https://drafts.csswg.org/css-transforms-2/#propdef-rotate) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub struct Rotate { |
| /// Rotation around the x axis. |
| pub x: f32, |
| /// Rotation around the y axis. |
| pub y: f32, |
| /// Rotation around the z axis. |
| pub z: f32, |
| /// The angle of rotation. |
| pub angle: Angle, |
| } |
| |
| impl<'i> Parse<'i> for Rotate { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { |
| return Ok(Rotate { |
| x: 0.0, |
| y: 0.0, |
| z: 1.0, |
| angle: Angle::Deg(0.0), |
| }); |
| } |
| |
| let angle = input.try_parse(Angle::parse); |
| let (x, y, z) = input |
| .try_parse(|input| { |
| let location = input.current_source_location(); |
| let ident = input.expect_ident()?; |
| match_ignore_ascii_case! { &*ident, |
| "x" => Ok((1.0, 0.0, 0.0)), |
| "y" => Ok((0.0, 1.0, 0.0)), |
| "z" => Ok((0.0, 0.0, 1.0)), |
| _ => Err(location.new_unexpected_token_error( |
| cssparser::Token::Ident(ident.clone()) |
| )) |
| } |
| }) |
| .or_else( |
| |_: ParseError<'i, ParserError<'i>>| -> Result<_, ParseError<'i, ParserError<'i>>> { |
| input.try_parse(|input| Ok((f32::parse(input)?, f32::parse(input)?, f32::parse(input)?))) |
| }, |
| ) |
| .unwrap_or((0.0, 0.0, 1.0)); |
| let angle = angle.or_else(|_| Angle::parse(input))?; |
| Ok(Rotate { x, y, z, angle }) |
| } |
| } |
| |
| impl ToCss for Rotate { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| if self.x == 0.0 && self.y == 0.0 && self.z == 1.0 && self.angle.is_zero() { |
| dest.write_str("none")?; |
| return Ok(()); |
| } |
| |
| if self.x == 1.0 && self.y == 0.0 && self.z == 0.0 { |
| dest.write_str("x ")?; |
| } else if self.x == 0.0 && self.y == 1.0 && self.z == 0.0 { |
| dest.write_str("y ")?; |
| } else if !(self.x == 0.0 && self.y == 0.0 && self.z == 1.0) { |
| self.x.to_css(dest)?; |
| dest.write_char(' ')?; |
| self.y.to_css(dest)?; |
| dest.write_char(' ')?; |
| self.z.to_css(dest)?; |
| dest.write_char(' ')?; |
| } |
| |
| self.angle.to_css(dest) |
| } |
| } |
| |
| impl Rotate { |
| /// Converts the rotation to a transform function. |
| pub fn to_transform(&self) -> Transform { |
| Transform::Rotate3d(self.x, self.y, self.z, self.angle.clone()) |
| } |
| } |
| |
| /// A value for the [scale](https://drafts.csswg.org/css-transforms-2/#propdef-scale) property. |
| #[derive(Debug, Clone, PartialEq)] |
| #[cfg_attr(feature = "visitor", derive(Visit))] |
| #[cfg_attr( |
| feature = "serde", |
| derive(serde::Serialize, serde::Deserialize), |
| serde(rename_all = "lowercase") |
| )] |
| #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] |
| #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] |
| pub enum Scale { |
| /// The "none" keyword. |
| None, |
| |
| /// Scale on the x, y, and z axis. |
| #[cfg_attr(feature = "serde", serde(untagged))] |
| XYZ { |
| /// Scale on the x axis. |
| x: NumberOrPercentage, |
| /// Scale on the y axis. |
| y: NumberOrPercentage, |
| /// Scale on the z axis. |
| z: NumberOrPercentage, |
| }, |
| } |
| |
| impl<'i> Parse<'i> for Scale { |
| fn parse<'t>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i, ParserError<'i>>> { |
| if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { |
| return Ok(Scale::None); |
| } |
| |
| let x = NumberOrPercentage::parse(input)?; |
| let y = input.try_parse(NumberOrPercentage::parse); |
| let z = if y.is_ok() { |
| input.try_parse(NumberOrPercentage::parse).ok() |
| } else { |
| None |
| }; |
| |
| Ok(Scale::XYZ { |
| x: x.clone(), |
| y: y.unwrap_or(x), |
| z: z.unwrap_or(NumberOrPercentage::Number(1.0)), |
| }) |
| } |
| } |
| |
| impl ToCss for Scale { |
| fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError> |
| where |
| W: std::fmt::Write, |
| { |
| match self { |
| Scale::None => { |
| dest.write_str("none")?; |
| } |
| Scale::XYZ { x, y, z } => { |
| x.to_css(dest)?; |
| let zv: f32 = z.into(); |
| if y != x || zv != 1.0 { |
| dest.write_char(' ')?; |
| y.to_css(dest)?; |
| if zv != 1.0 { |
| dest.write_char(' ')?; |
| z.to_css(dest)?; |
| } |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| impl Scale { |
| /// Converts the scale to a transform function. |
| pub fn to_transform(&self) -> Transform { |
| match self { |
| Scale::None => Transform::Scale3d( |
| NumberOrPercentage::Number(1.0), |
| NumberOrPercentage::Number(1.0), |
| NumberOrPercentage::Number(1.0), |
| ), |
| Scale::XYZ { x, y, z } => Transform::Scale3d(x.clone(), y.clone(), z.clone()), |
| } |
| } |
| } |
| |
| #[derive(Default)] |
| pub(crate) struct TransformHandler { |
| transform: Option<(TransformList, VendorPrefix)>, |
| translate: Option<Translate>, |
| rotate: Option<Rotate>, |
| scale: Option<Scale>, |
| has_any: bool, |
| } |
| |
| impl<'i> PropertyHandler<'i> for TransformHandler { |
| fn handle_property( |
| &mut self, |
| property: &Property<'i>, |
| dest: &mut DeclarationList<'i>, |
| context: &mut PropertyHandlerContext<'i, '_>, |
| ) -> bool { |
| use Property::*; |
| |
| macro_rules! individual_property { |
| ($prop: ident, $val: ident) => { |
| if let Some((transform, _)) = &mut self.transform { |
| transform.0.push($val.to_transform()) |
| } else { |
| self.$prop = Some($val.clone()); |
| self.has_any = true; |
| } |
| }; |
| } |
| |
| match property { |
| Transform(val, vp) => { |
| // If two vendor prefixes for the same property have different |
| // values, we need to flush what we have immediately to preserve order. |
| if let Some((cur, prefixes)) = &self.transform { |
| if cur != val && !prefixes.contains(*vp) { |
| self.flush(dest, context); |
| } |
| } |
| |
| // Otherwise, update the value and add the prefix. |
| if let Some((transform, prefixes)) = &mut self.transform { |
| *transform = val.clone(); |
| *prefixes |= *vp; |
| } else { |
| self.transform = Some((val.clone(), *vp)); |
| self.has_any = true; |
| } |
| |
| self.translate = None; |
| self.rotate = None; |
| self.scale = None; |
| } |
| Translate(val) => individual_property!(translate, val), |
| Rotate(val) => individual_property!(rotate, val), |
| Scale(val) => individual_property!(scale, val), |
| Unparsed(val) |
| if matches!( |
| val.property_id, |
| PropertyId::Transform(_) | PropertyId::Translate | PropertyId::Rotate | PropertyId::Scale |
| ) => |
| { |
| self.flush(dest, context); |
| let prop = if matches!(val.property_id, PropertyId::Transform(_)) { |
| Property::Unparsed(val.get_prefixed(context.targets, Feature::Transform)) |
| } else { |
| property.clone() |
| }; |
| dest.push(prop) |
| } |
| _ => return false, |
| } |
| |
| true |
| } |
| |
| fn finalize(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) { |
| self.flush(dest, context); |
| } |
| } |
| |
| impl TransformHandler { |
| fn flush<'i>(&mut self, dest: &mut DeclarationList<'i>, context: &mut PropertyHandlerContext<'i, '_>) { |
| if !self.has_any { |
| return; |
| } |
| |
| self.has_any = false; |
| |
| let transform = std::mem::take(&mut self.transform); |
| let translate = std::mem::take(&mut self.translate); |
| let rotate = std::mem::take(&mut self.rotate); |
| let scale = std::mem::take(&mut self.scale); |
| |
| if let Some((transform, prefix)) = transform { |
| let prefix = context.targets.prefixes(prefix, Feature::Transform); |
| dest.push(Property::Transform(transform, prefix)) |
| } |
| |
| if let Some(translate) = translate { |
| dest.push(Property::Translate(translate)) |
| } |
| |
| if let Some(rotate) = rotate { |
| dest.push(Property::Rotate(rotate)) |
| } |
| |
| if let Some(scale) = scale { |
| dest.push(Property::Scale(scale)) |
| } |
| } |
| } |