Skip to main content

takumi_css/style/properties/
transform.rs

1use crate::style::properties::filter::interpolate_field;
2use crate::style::{ToCss, unexpected_token};
3use std::{
4  fmt,
5  ops::{Mul, MulAssign},
6};
7
8use cssparser::{Parser, Token, match_ignore_ascii_case};
9use taffy::{Point, Size};
10use tiny_skia::Transform as TinyTransform;
11
12use crate::style::{
13  Angle, Animatable, Color, CssSyntaxKind, CssToken, FromCss, Length, ListInterpolationStrategy,
14  MakeComputed, ParseResult, PercentageNumber, SizingContext, lerp,
15};
16
17const DEFAULT_SCALE: f32 = 1.0;
18
19/// Represents a single CSS transform operation
20#[derive(Debug, Clone, Copy, PartialEq)]
21#[non_exhaustive]
22pub enum Transform {
23  /// Translates an element along the X-axis and Y-axis by the specified lengths
24  Translate(Length, Length),
25  /// Scales an element by the specified factors
26  Scale(f32, f32),
27  /// Rotates an element (2D rotation) by angle in degrees
28  Rotate(Angle),
29  /// Skews an element by the specified angles
30  Skew(Angle, Angle),
31  /// Applies raw affine matrix values
32  Matrix(Affine),
33}
34
35impl MakeComputed for Transform {
36  fn make_computed(&mut self, sizing: &SizingContext) {
37    if let Transform::Translate(x, y) = self {
38      x.make_computed(sizing);
39      y.make_computed(sizing);
40    }
41  }
42}
43
44impl Animatable for Transform {
45  fn list_interpolation_strategy() -> ListInterpolationStrategy {
46    ListInterpolationStrategy::PadToLongestWithNeutral
47  }
48
49  fn neutral_value_like(other: &Self) -> Option<Self> {
50    Some(match *other {
51      Transform::Translate(_, _) => Transform::Translate(Length::zero(), Length::zero()),
52      Transform::Scale(_, _) => Transform::Scale(1.0, 1.0),
53      Transform::Rotate(_) => Transform::Rotate(Angle::zero()),
54      Transform::Skew(_, _) => Transform::Skew(Angle::zero(), Angle::zero()),
55      Transform::Matrix(_) => Transform::Matrix(Affine::IDENTITY),
56    })
57  }
58
59  fn interpolate(
60    &mut self,
61    from: &Self,
62    to: &Self,
63    progress: f32,
64    sizing: &SizingContext,
65    current_color: Color,
66  ) {
67    *self = match (*from, *to) {
68      (Transform::Translate(from_x, from_y), Transform::Translate(to_x, to_y)) => {
69        interpolate_field!(
70          Transform::Translate,
71          from_x,
72          to_x,
73          from_y,
74          to_y,
75          progress,
76          sizing,
77          current_color
78        )
79      }
80      (Transform::Scale(from_x, from_y), Transform::Scale(to_x, to_y)) => {
81        Transform::Scale(lerp(from_x, to_x, progress), lerp(from_y, to_y, progress))
82      }
83      (Transform::Rotate(from_angle), Transform::Rotate(to_angle)) => {
84        interpolate_field!(
85          Transform::Rotate,
86          from_angle,
87          to_angle,
88          progress,
89          sizing,
90          current_color
91        )
92      }
93      (Transform::Skew(from_x, from_y), Transform::Skew(to_x, to_y)) => {
94        interpolate_field!(
95          Transform::Skew,
96          from_x,
97          to_x,
98          from_y,
99          to_y,
100          progress,
101          sizing,
102          current_color
103        )
104      }
105      (Transform::Matrix(from_affine), Transform::Matrix(to_affine)) => Transform::Matrix(Affine {
106        a: lerp(from_affine.a, to_affine.a, progress),
107        b: lerp(from_affine.b, to_affine.b, progress),
108        c: lerp(from_affine.c, to_affine.c, progress),
109        d: lerp(from_affine.d, to_affine.d, progress),
110        x: lerp(from_affine.x, to_affine.x, progress),
111        y: lerp(from_affine.y, to_affine.y, progress),
112      }),
113      _ => {
114        if progress >= 0.5 {
115          *to
116        } else {
117          *from
118        }
119      }
120    };
121  }
122}
123
124/// | a c x |
125/// | b d y |
126/// | 0 0 1 |
127#[derive(Debug, Clone, Copy, Default, PartialEq)]
128pub struct Affine {
129  /// Horizontal scaling / cosine of rotation
130  pub a: f32,
131  /// Horizontal shear / sine of rotation
132  pub b: f32,
133  /// Vertical shear / negative sine of rotation
134  pub c: f32,
135  /// Vertical scaling / cosine of rotation
136  pub d: f32,
137  /// Horizontal translation (always orthogonal regardless of rotation)
138  pub x: f32,
139  /// Vertical translation (always orthogonal regardless of rotation)
140  pub y: f32,
141}
142
143impl From<Affine> for TinyTransform {
144  fn from(transform: Affine) -> Self {
145    TinyTransform::from_row(
146      transform.a,
147      transform.b,
148      transform.c,
149      transform.d,
150      transform.x,
151      transform.y,
152    )
153  }
154}
155
156impl Mul<Affine> for Affine {
157  type Output = Affine;
158
159  fn mul(self, rhs: Affine) -> Self::Output {
160    if self.is_identity() {
161      return rhs;
162    }
163
164    if rhs.is_identity() {
165      return self;
166    }
167
168    Affine {
169      a: self.a * rhs.a + self.c * rhs.b,
170      b: self.b * rhs.a + self.d * rhs.b,
171      c: self.a * rhs.c + self.c * rhs.d,
172      d: self.b * rhs.c + self.d * rhs.d,
173      x: self.a * rhs.x + self.c * rhs.y + self.x,
174      y: self.b * rhs.x + self.d * rhs.y + self.y,
175    }
176  }
177}
178
179impl MulAssign<Affine> for Affine {
180  fn mul_assign(&mut self, rhs: Affine) {
181    *self = *self * rhs;
182  }
183}
184
185impl Affine {
186  /// Converts the affine transform to a column-major array.
187  pub fn to_cols_array(&self) -> [f32; 6] {
188    [self.a, self.b, self.c, self.d, self.x, self.y]
189  }
190
191  /// Returns the identity transform
192  pub const IDENTITY: Self = Self {
193    a: 1.0,
194    b: 0.0,
195    c: 0.0,
196    d: 1.0,
197    x: 0.0,
198    y: 0.0,
199  };
200
201  /// Returns true if the transform is the identity transform
202  pub fn is_identity(self) -> bool {
203    (self.a - 1.0).abs() < 1e-6
204      && self.b.abs() < 1e-6
205      && self.c.abs() < 1e-6
206      && (self.d - 1.0).abs() < 1e-6
207      && self.x.abs() < 1e-6
208      && self.y.abs() < 1e-6
209  }
210
211  /// Decomposes the translation part of the transform
212  pub fn decompose_translation(self) -> Point<f32> {
213    Point {
214      x: self.x,
215      y: self.y,
216    }
217  }
218
219  /// Geometric mean of the X and Y axis scales.
220  pub fn uniform_scale(self) -> f32 {
221    let sx = (self.a * self.a + self.b * self.b).sqrt();
222    let sy = (self.c * self.c + self.d * self.d).sqrt();
223    (sx * sy).sqrt()
224  }
225
226  /// Returns true if the transform is only a translation
227  pub fn only_translation(self) -> bool {
228    (self.a - 1.0).abs() < 1e-8
229      && self.b.abs() < 1e-8
230      && self.c.abs() < 1e-8
231      && (self.d - 1.0).abs() < 1e-8
232  }
233
234  /// Creates a new rotation transform
235  pub fn rotation(angle: Angle) -> Self {
236    let (sin, cos) = angle.to_radians().sin_cos();
237
238    Self {
239      a: cos,
240      b: sin,
241      c: -sin,
242      d: cos,
243      x: 0.0,
244      y: 0.0,
245    }
246  }
247
248  /// Creates a new translation transform
249  pub const fn translation(x: f32, y: f32) -> Self {
250    Self {
251      x,
252      y,
253      ..Self::IDENTITY
254    }
255  }
256
257  /// Creates a new scale transform
258  pub const fn scale(x: f32, y: f32) -> Self {
259    Self {
260      a: x,
261      b: 0.0,
262      c: 0.0,
263      d: y,
264      x: 0.0,
265      y: 0.0,
266    }
267  }
268
269  /// Transforms a point by the transform
270  #[inline(always)]
271  pub fn transform_point(self, point: Point<f32>) -> Point<f32> {
272    // Fast path: If the transform is only a translation, we can just add the translation to the point
273    if self.only_translation() {
274      return Point {
275        x: point.x + self.x,
276        y: point.y + self.y,
277      };
278    }
279
280    Point {
281      x: self.a * point.x + self.c * point.y + self.x,
282      y: self.b * point.x + self.d * point.y + self.y,
283    }
284  }
285
286  /// Creates a new skew transform
287  pub fn skew(x: Angle, y: Angle) -> Self {
288    let tanx = x.to_radians().tan();
289    let tany = y.to_radians().tan();
290
291    Self {
292      a: 1.0,
293      b: tany,
294      c: tanx,
295      d: 1.0,
296      x: 0.0,
297      y: 0.0,
298    }
299  }
300
301  /// Calculates the determinant of the transform
302  #[inline(always)]
303  pub fn determinant(self) -> f32 {
304    self.a * self.d - self.b * self.c
305  }
306
307  /// Returns true if the transform is invertible
308  #[inline(always)]
309  pub fn is_invertible(self) -> bool {
310    self.determinant().abs() > f32::EPSILON
311  }
312
313  /// Inverts the transform, returns `None` if the transform is not invertible
314  pub fn invert(self) -> Option<Self> {
315    let det = self.determinant();
316    if det.abs() < f32::EPSILON {
317      return None;
318    }
319
320    let inv_det = 1.0 / det;
321
322    Some(Self {
323      a: self.d * inv_det,
324      b: self.b * -inv_det,
325      c: self.c * -inv_det,
326      d: self.a * inv_det,
327      x: (self.d * self.x - self.c * self.y) * -inv_det,
328      y: (self.b * self.x - self.a * self.y) * inv_det,
329    })
330  }
331
332  /// Converts the transforms to a [`Affine`] instance
333  ///
334  /// CSS transform property applies transformations from left to right.
335  /// For `transform: translate() rotate()`, the resulting matrix is translate * rotate.
336  /// When applied to point p: translate * rotate * p, rotate is applied first.
337  pub fn from_transforms<'a, I: Iterator<Item = &'a Transform>>(
338    transforms: I,
339    sizing: &SizingContext,
340    border_box: Size<f32>,
341  ) -> Affine {
342    let mut instance = Affine::IDENTITY;
343
344    for transform in transforms {
345      instance *= match *transform {
346        Transform::Translate(x_length, y_length) => Affine::translation(
347          x_length.to_px(sizing, border_box.width),
348          y_length.to_px(sizing, border_box.height),
349        ),
350        Transform::Scale(x_scale, y_scale) => Affine::scale(x_scale, y_scale),
351        Transform::Rotate(angle) => Affine::rotation(angle),
352        Transform::Skew(x_angle, y_angle) => Affine::skew(x_angle, y_angle),
353        Transform::Matrix(affine) => affine,
354      };
355    }
356
357    instance
358  }
359}
360
361impl<'i> FromCss<'i> for Affine {
362  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
363    let a = input.expect_number()?;
364    input.expect_comma()?;
365    let b = input.expect_number()?;
366    input.expect_comma()?;
367    let c = input.expect_number()?;
368    input.expect_comma()?;
369    let d = input.expect_number()?;
370    input.expect_comma()?;
371    let x = input.expect_number()?;
372    input.expect_comma()?;
373    let y = input.expect_number()?;
374
375    Ok(Affine { a, b, c, d, x, y })
376  }
377
378  const VALID_TOKENS: &'static [CssToken] = &[CssToken::Syntax(CssSyntaxKind::Number)];
379}
380
381/// A collection of transform operations that can be applied together
382#[derive(Debug, Clone, PartialEq)]
383pub struct Transforms(pub Box<[Transform]>);
384
385impl<'i> FromCss<'i> for Transforms {
386  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
387    let mut transforms = Vec::new();
388
389    while !input.is_exhausted() {
390      let transform = Transform::from_css(input)?;
391      transforms.push(transform);
392    }
393
394    Ok(Transforms(transforms.into_boxed_slice()))
395  }
396
397  const VALID_TOKENS: &'static [CssToken] = Transform::VALID_TOKENS;
398}
399
400impl MakeComputed for Transforms {
401  fn make_computed(&mut self, sizing: &SizingContext) {
402    for transform in self.0.iter_mut() {
403      transform.make_computed(sizing);
404    }
405  }
406}
407
408impl Animatable for Transforms {
409  fn missing_value() -> Option<Self> {
410    <Box<[Transform]>>::missing_value().map(Self)
411  }
412
413  fn interpolate(
414    &mut self,
415    from: &Self,
416    to: &Self,
417    progress: f32,
418    sizing: &SizingContext,
419    current_color: Color,
420  ) {
421    self
422      .0
423      .interpolate(&from.0, &to.0, progress, sizing, current_color);
424  }
425}
426
427impl std::ops::Deref for Transforms {
428  type Target = Box<[Transform]>;
429  fn deref(&self) -> &Self::Target {
430    &self.0
431  }
432}
433
434impl std::ops::DerefMut for Transforms {
435  fn deref_mut(&mut self) -> &mut Self::Target {
436    &mut self.0
437  }
438}
439
440impl From<Box<[Transform]>> for Transforms {
441  fn from(box_slice: Box<[Transform]>) -> Self {
442    Self(box_slice)
443  }
444}
445
446impl From<Vec<Transform>> for Transforms {
447  fn from(vec: Vec<Transform>) -> Self {
448    Self(vec.into_boxed_slice())
449  }
450}
451
452impl<const N: usize> From<[Transform; N]> for Transforms {
453  fn from(arr: [Transform; N]) -> Self {
454    Self(Box::from(arr))
455  }
456}
457
458impl ToCss for Transform {
459  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
460    match self {
461      Self::Translate(x, y) => {
462        dest.write_str("translate(")?;
463        x.to_css(dest)?;
464        dest.write_str(", ")?;
465        y.to_css(dest)?;
466        dest.write_char(')')
467      }
468      Self::Scale(x, y) => write!(dest, "scale({x}, {y})"),
469      Self::Rotate(a) => {
470        dest.write_str("rotate(")?;
471        a.to_css(dest)?;
472        dest.write_char(')')
473      }
474      Self::Skew(x, y) => {
475        dest.write_str("skew(")?;
476        x.to_css(dest)?;
477        dest.write_str(", ")?;
478        y.to_css(dest)?;
479        dest.write_char(')')
480      }
481      Self::Matrix(Affine { a, b, c, d, x, y }) => {
482        write!(dest, "matrix({a}, {b}, {c}, {d}, {x}, {y})")
483      }
484    }
485  }
486}
487
488impl ToCss for Affine {
489  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
490    let Self { a, b, c, d, x, y } = self;
491    write!(dest, "matrix({a}, {b}, {c}, {d}, {x}, {y})")
492  }
493}
494
495impl ToCss for Transforms {
496  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
497    let mut first = true;
498    for transform in self.iter() {
499      if !first {
500        dest.write_char(' ')?;
501      }
502      transform.to_css(dest)?;
503      first = false;
504    }
505    Ok(())
506  }
507}
508
509impl<'i> FromCss<'i> for Transform {
510  fn from_css(parser: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
511    let location = parser.current_source_location();
512    let token = parser.next()?;
513
514    let Token::Function(function) = token else {
515      return Err(
516        location
517          .new_basic_unexpected_token_error(token.clone())
518          .into(),
519      );
520    };
521
522    match_ignore_ascii_case! {function,
523      "translate" => parser.parse_nested_block(|input| {
524        let x = Length::from_css(input)?;
525        input.expect_comma()?;
526        let y = Length::from_css(input)?;
527
528        Ok(Transform::Translate(x, y))
529      }),
530      "translatex" => parser.parse_nested_block(|input| Ok(Transform::Translate(
531        Length::from_css(input)?,
532        Length::zero(),
533      ))),
534      "translatey" => parser.parse_nested_block(|input| Ok(Transform::Translate(
535        Length::zero(),
536        Length::from_css(input)?,
537      ))),
538      "scale" => parser.parse_nested_block(|input| {
539        let PercentageNumber(x) = PercentageNumber::from_css(input)?;
540        if input.try_parse(Parser::expect_comma).is_ok() {
541          let PercentageNumber(y) = PercentageNumber::from_css(input)?;
542          Ok(Transform::Scale(x, y))
543        } else {
544          Ok(Transform::Scale(x, x))
545        }
546      }),
547      "scalex" => parser.parse_nested_block(|input| Ok(Transform::Scale(
548        PercentageNumber::from_css(input)?.0,
549        DEFAULT_SCALE,
550      ))),
551      "scaley" => parser.parse_nested_block(|input| Ok(Transform::Scale(
552        DEFAULT_SCALE,
553        PercentageNumber::from_css(input)?.0,
554      ))),
555      "skew" => parser.parse_nested_block(|input| {
556        let x = Angle::from_css(input)?;
557        input.expect_comma()?;
558        let y = Angle::from_css(input)?;
559
560        Ok(Transform::Skew(x, y))
561      }),
562      "skewx" => parser.parse_nested_block(|input| Ok(Transform::Skew(
563        Angle::from_css(input)?,
564        Angle::default(),
565      ))),
566      "skewy" => parser.parse_nested_block(|input| Ok(Transform::Skew(
567        Angle::default(),
568        Angle::from_css(input)?,
569      ))),
570      "rotate" => parser.parse_nested_block(|input| Ok(Transform::Rotate(
571        Angle::from_css(input)?,
572      ))),
573      "matrix" => parser.parse_nested_block(|input| Ok(Transform::Matrix(
574        Affine::from_css(input)?,
575      ))),
576      _ => Err(unexpected_token!(location, token)),
577    }
578  }
579
580  const VALID_TOKENS: &'static [CssToken] = &[CssToken::Syntax(CssSyntaxKind::TransformFunction)];
581}
582
583#[cfg(test)]
584mod tests {
585  use super::*;
586
587  #[test]
588  fn test_transform_from_str() {
589    assert_eq!(
590      Transform::from_str("translate(10, 20px)"),
591      Ok(Transform::Translate(Length::Px(10.0), Length::Px(20.0)))
592    );
593  }
594
595  #[test]
596  fn test_transform_scale_from_str() {
597    assert_eq!(
598      Transform::from_str("scale(10)"),
599      Ok(Transform::Scale(10.0, 10.0))
600    );
601  }
602
603  #[test]
604  fn test_transform_invert() {
605    let transform = Affine::rotation(Angle::new(45.0));
606
607    assert!(transform.invert().is_some_and(|inverse| {
608      let random_point = Point {
609        x: 1234.0,
610        y: -5678.0,
611      };
612
613      let processed_point = inverse.transform_point(transform.transform_point(random_point));
614
615      (random_point.x - processed_point.x).abs() < 1.0
616        && (random_point.y - processed_point.y).abs() < 1.0
617    }));
618  }
619}