Skip to main content

ferroui_base/media/transformation/
transform_operations.rs

1use crate::media::transformation::{
2    InterpolationUtilities, OperationType, TransformOperation, TransformParser,
3};
4use crate::media::ITransform;
5use crate::utilities::FormatError;
6use crate::{Matrix, MatrixDecomposed};
7use std::any::Any;
8use std::rc::Rc;
9
10/// Contains a list of [`TransformOperation`] that represent primitive
11/// transforms that will be applied in declared order.
12#[derive(Debug)]
13pub struct TransformOperations {
14    operations: Vec<TransformOperation>,
15    is_identity: bool,
16    value: Matrix,
17}
18
19
20/// Compares by identity (reference equality), as the reference type this
21/// mirrors.
22impl PartialEq for TransformOperations {
23    #[inline]
24    fn eq(&self, other: &Self) -> bool {
25        std::ptr::eq(self, other)
26    }
27}
28
29impl TransformOperations {
30    /// The shared empty operation list.
31    pub fn identity() -> Rc<TransformOperations> {
32        crate::ferro_thread_local! {
33            static IDENTITY: Rc<TransformOperations> = Rc::new(TransformOperations::new(Vec::new()));
34        }
35        IDENTITY.with(Rc::clone)
36    }
37
38    fn new(operations: Vec<TransformOperation>) -> Self {
39        let is_identity = operations.iter().all(TransformOperation::is_identity);
40        let value = Self::apply_transforms(&operations, 0);
41        Self { operations, is_identity, value }
42    }
43
44    /// Returns whether all operations combined together produce the identity
45    /// matrix.
46    #[inline]
47    pub fn is_identity(&self) -> bool {
48        self.is_identity
49    }
50
51    /// The operations, in declared order.
52    #[inline]
53    pub fn operations(&self) -> &[TransformOperation] {
54        &self.operations
55    }
56
57    /// The combined matrix of all operations.
58    #[inline]
59    pub fn value(&self) -> Matrix {
60        self.value
61    }
62
63    /// Parses a transform string such as `scale(1.5) rotate(45deg)`.
64    pub fn parse(s: &str) -> Result<Rc<TransformOperations>, FormatError> {
65        TransformParser::parse(s)
66    }
67
68    /// Creates a builder with the given initial capacity.
69    pub fn create_builder(capacity: usize) -> TransformOperationsBuilder {
70        TransformOperationsBuilder::new(capacity)
71    }
72
73    /// Interpolates between two operation lists.
74    pub fn interpolate(
75        from: &Rc<TransformOperations>,
76        to: &Rc<TransformOperations>,
77        progress: f64,
78    ) -> Rc<TransformOperations> {
79        match Self::try_interpolate(from, to, progress) {
80            Some(result) => result,
81            // If the matrices cannot be interpolated, fallback to discrete animation logic.
82            // See https://drafts.csswg.org/css-transforms/#matrix-interpolation
83            None => {
84                if progress < 0.5 {
85                    from.clone()
86                } else {
87                    to.clone()
88                }
89            }
90        }
91    }
92
93    fn apply_transforms(operations: &[TransformOperation], start_offset: usize) -> Matrix {
94        let mut matrix = Matrix::IDENTITY;
95        for operation in operations.iter().skip(start_offset) {
96            matrix *= operation.matrix;
97        }
98        matrix
99    }
100
101    fn try_interpolate(
102        from: &TransformOperations,
103        to: &TransformOperations,
104        progress: f64,
105    ) -> Option<Rc<TransformOperations>> {
106        let from_identity = from.is_identity;
107        let to_identity = to.is_identity;
108
109        if from_identity && to_identity {
110            return Some(Self::identity());
111        }
112
113        let matching_prefix_length = Self::compute_matching_prefix_length(from, to);
114        let from_size = if from_identity { 0 } else { from.operations.len() };
115        let to_size = if to_identity { 0 } else { to.operations.len() };
116        let num_operations = from_size.max(to_size);
117
118        let mut builder = TransformOperationsBuilder::new(matching_prefix_length);
119
120        for i in 0..matching_prefix_length {
121            let mut interpolated =
122                TransformOperation { type_: OperationType::Identity, ..TransformOperation::default() };
123
124            if !TransformOperation::try_interpolate(
125                if i >= from_size { None } else { Some(from.operations[i]) },
126                if i >= to_size { None } else { Some(to.operations[i]) },
127                progress,
128                &mut interpolated,
129            ) {
130                return None;
131            }
132
133            builder.append(interpolated);
134        }
135
136        if matching_prefix_length < num_operations {
137            let from_decomposed = Self::compute_decomposed_transform(from, matching_prefix_length)?;
138            let to_decomposed = Self::compute_decomposed_transform(to, matching_prefix_length)?;
139
140            let transform =
141                InterpolationUtilities::interpolate_decomposed_transforms(&from_decomposed, &to_decomposed, progress);
142
143            builder.append_matrix(InterpolationUtilities::compose_transform(transform));
144        }
145
146        Some(builder.build())
147    }
148
149    fn compute_decomposed_transform(operations: &TransformOperations, start_offset: usize) -> Option<MatrixDecomposed> {
150        let transform = Self::apply_transforms(&operations.operations, start_offset);
151        Matrix::try_decompose_transform(transform)
152    }
153
154    fn compute_matching_prefix_length(from: &TransformOperations, to: &TransformOperations) -> usize {
155        let num_operations = from.operations.len().min(to.operations.len());
156
157        for i in 0..num_operations {
158            if from.operations[i].type_ != to.operations[i].type_ {
159                return i;
160            }
161        }
162
163        // If the operations match to the length of the shorter list, then pad its
164        // length with the matching identity operations.
165        // https://drafts.csswg.org/css-transforms/#transform-function-lists
166        from.operations.len().max(to.operations.len())
167    }
168}
169
170impl ITransform for TransformOperations {
171    #[inline]
172    fn value(&self) -> Matrix {
173        self.value
174    }
175
176    fn as_any(&self) -> &dyn Any {
177        self
178    }
179}
180
181/// Builds a [`TransformOperations`] list.
182#[derive(Clone, Debug, Default, PartialEq)]
183pub struct TransformOperationsBuilder {
184    operations: Vec<TransformOperation>,
185}
186
187impl TransformOperationsBuilder {
188    pub fn new(capacity: usize) -> Self {
189        Self { operations: Vec::with_capacity(capacity) }
190    }
191
192    pub fn append_translate(&mut self, x: f64, y: f64) {
193        let mut to_add = TransformOperation { type_: OperationType::Translate, ..TransformOperation::default() };
194        to_add.data.set_translate(x, y);
195        to_add.bake();
196        self.operations.push(to_add);
197    }
198
199    pub fn append_rotate(&mut self, angle: f64) {
200        let mut to_add = TransformOperation { type_: OperationType::Rotate, ..TransformOperation::default() };
201        to_add.data.set_rotate(angle);
202        to_add.bake();
203        self.operations.push(to_add);
204    }
205
206    pub fn append_scale(&mut self, x: f64, y: f64) {
207        let mut to_add = TransformOperation { type_: OperationType::Scale, ..TransformOperation::default() };
208        to_add.data.set_scale(x, y);
209        to_add.bake();
210        self.operations.push(to_add);
211    }
212
213    pub fn append_skew(&mut self, x: f64, y: f64) {
214        let mut to_add = TransformOperation { type_: OperationType::Skew, ..TransformOperation::default() };
215        to_add.data.set_skew(x, y);
216        to_add.bake();
217        self.operations.push(to_add);
218    }
219
220    pub fn append_matrix(&mut self, matrix: Matrix) {
221        self.operations.push(TransformOperation {
222            type_: OperationType::Matrix,
223            matrix,
224            ..TransformOperation::default()
225        });
226    }
227
228    /// Appends an identity-typed operation. As in the reference
229    /// implementation its matrix is left unbaked (all zeros).
230    pub fn append_identity(&mut self) {
231        self.operations.push(TransformOperation { type_: OperationType::Identity, ..TransformOperation::default() });
232    }
233
234    pub fn append(&mut self, to_add: TransformOperation) {
235        self.operations.push(to_add);
236    }
237
238    pub fn build(self) -> Rc<TransformOperations> {
239        Rc::new(TransformOperations::new(self.operations))
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::media::{TransformGroup, TranslateTransform};
247    use crate::utilities::MathUtilities;
248
249    fn assert_close(expected: f64, actual: f64, precision: i32) {
250        let scale = 10f64.powi(precision);
251        assert_eq!((expected * scale).round(), (actual * scale).round(), "{expected} != {actual}");
252    }
253
254    #[test]
255    fn can_parse_translation() {
256        for (data, x, y) in [
257            ("translate(10px)", 10.0, 0.0),
258            ("translate(10px, 10px)", 10.0, 10.0),
259            ("translate(0px, 10px)", 0.0, 10.0),
260            ("translate(10px, 0px)", 10.0, 0.0),
261            ("translateX(10px)", 10.0, 0.0),
262            ("translateY(10px)", 0.0, 10.0),
263        ] {
264            let transform = TransformOperations::parse(data).unwrap();
265            let operations = transform.operations();
266            assert_eq!(1, operations.len());
267            assert_eq!(OperationType::Translate, operations[0].type_);
268            assert_eq!(x, operations[0].data.translate().x);
269            assert_eq!(y, operations[0].data.translate().y);
270        }
271    }
272
273    #[test]
274    fn can_parse_rotation() {
275        for (data, angle_deg) in [
276            ("rotate(90deg)", 90.0),
277            ("rotate(0.5turn)", 180.0),
278            ("rotate(200grad)", 180.0),
279            ("rotate(3.14159265rad)", 180.0),
280        ] {
281            let transform = TransformOperations::parse(data).unwrap();
282            let operations = transform.operations();
283            assert_eq!(1, operations.len());
284            assert_eq!(OperationType::Rotate, operations[0].type_);
285            assert_close(MathUtilities::deg2rad(angle_deg), operations[0].data.rotate().angle, 4);
286        }
287    }
288
289    #[test]
290    fn can_parse_scale() {
291        for (data, x, y) in [
292            ("scale(10)", 10.0, 10.0),
293            ("scale(10, 10)", 10.0, 10.0),
294            ("scale(0, 10)", 0.0, 10.0),
295            ("scale(10, 0)", 10.0, 0.0),
296            ("scaleX(10)", 10.0, 1.0),
297            ("scaleY(10)", 1.0, 10.0),
298        ] {
299            let transform = TransformOperations::parse(data).unwrap();
300            let operations = transform.operations();
301            assert_eq!(1, operations.len());
302            assert_eq!(OperationType::Scale, operations[0].type_);
303            assert_eq!(x, operations[0].data.scale().x);
304            assert_eq!(y, operations[0].data.scale().y);
305        }
306    }
307
308    #[test]
309    fn can_parse_skew() {
310        for (data, x, y) in [
311            ("skew(90deg)", 90.0, 0.0),
312            ("skew(0.5turn)", 180.0, 0.0),
313            ("skew(200grad)", 180.0, 0.0),
314            ("skew(3.14159265rad)", 180.0, 0.0),
315            ("skewX(90deg)", 90.0, 0.0),
316            ("skewX(0.5turn)", 180.0, 0.0),
317            ("skewX(200grad)", 180.0, 0.0),
318            ("skewX(3.14159265rad)", 180.0, 0.0),
319            ("skew(0, 90deg)", 0.0, 90.0),
320            ("skew(0, 0.5turn)", 0.0, 180.0),
321            ("skew(0, 200grad)", 0.0, 180.0),
322            ("skew(0, 3.14159265rad)", 0.0, 180.0),
323            ("skewY(90deg)", 0.0, 90.0),
324            ("skewY(0.5turn)", 0.0, 180.0),
325            ("skewY(200grad)", 0.0, 180.0),
326            ("skewY(3.14159265rad)", 0.0, 180.0),
327            ("skew(90deg, 90deg)", 90.0, 90.0),
328            ("skew(0.5turn, 0.5turn)", 180.0, 180.0),
329            ("skew(200grad, 200grad)", 180.0, 180.0),
330            ("skew(3.14159265rad, 3.14159265rad)", 180.0, 180.0),
331        ] {
332            let transform = TransformOperations::parse(data).unwrap();
333            let operations = transform.operations();
334            assert_eq!(1, operations.len());
335            assert_eq!(OperationType::Skew, operations[0].type_);
336            assert_close(MathUtilities::deg2rad(x), operations[0].data.skew().x, 4);
337            assert_close(MathUtilities::deg2rad(y), operations[0].data.skew().y, 4);
338        }
339    }
340
341    #[test]
342    fn can_parse_compound_operations() {
343        let data = "scale(1,2) translate(3px,4px) rotate(5deg) skew(6deg,7deg)";
344        let transform = TransformOperations::parse(data).unwrap();
345        let operations = transform.operations();
346
347        assert_eq!(OperationType::Scale, operations[0].type_);
348        assert_eq!(1.0, operations[0].data.scale().x);
349        assert_eq!(2.0, operations[0].data.scale().y);
350
351        assert_eq!(OperationType::Translate, operations[1].type_);
352        assert_eq!(3.0, operations[1].data.translate().x);
353        assert_eq!(4.0, operations[1].data.translate().y);
354
355        assert_eq!(OperationType::Rotate, operations[2].type_);
356        assert_eq!(MathUtilities::deg2rad(5.0), operations[2].data.rotate().angle);
357
358        assert_eq!(OperationType::Skew, operations[3].type_);
359        assert_eq!(MathUtilities::deg2rad(6.0), operations[3].data.skew().x);
360        assert_eq!(MathUtilities::deg2rad(7.0), operations[3].data.skew().y);
361    }
362
363    #[test]
364    fn can_parse_matrix_operation() {
365        let transform = TransformOperations::parse("matrix(1,2,3,4,5,6)").unwrap();
366        let operations = transform.operations();
367        assert_eq!(1, operations.len());
368        assert_eq!(OperationType::Matrix, operations[0].type_);
369        assert_eq!(Matrix::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), operations[0].matrix);
370    }
371
372    #[test]
373    fn can_interpolate_translation() {
374        for (progress, x, y) in [(0.0, 10.0, 0.0), (0.5, 5.0, 10.0), (1.0, 0.0, 20.0)] {
375            let from = TransformOperations::parse("translateX(10px)").unwrap();
376            let to = TransformOperations::parse("translateY(20px)").unwrap();
377            let interpolated = TransformOperations::interpolate(&from, &to, progress);
378            let operations = interpolated.operations();
379            assert_eq!(1, operations.len());
380            assert_eq!(OperationType::Translate, operations[0].type_);
381            assert_eq!(x, operations[0].data.translate().x);
382            assert_eq!(y, operations[0].data.translate().y);
383        }
384    }
385
386    #[test]
387    fn can_interpolate_scale() {
388        for (progress, x, y) in [(0.0, 10.0, 1.0), (0.5, 5.5, 10.5), (1.0, 1.0, 20.0)] {
389            let from = TransformOperations::parse("scaleX(10)").unwrap();
390            let to = TransformOperations::parse("scaleY(20)").unwrap();
391            let interpolated = TransformOperations::interpolate(&from, &to, progress);
392            let operations = interpolated.operations();
393            assert_eq!(1, operations.len());
394            assert_eq!(OperationType::Scale, operations[0].type_);
395            assert_eq!(x, operations[0].data.scale().x);
396            assert_eq!(y, operations[0].data.scale().y);
397        }
398    }
399
400    #[test]
401    fn can_interpolate_skew() {
402        for (progress, x, y) in [(0.0, 10.0, 0.0), (0.5, 5.0, 10.0), (1.0, 0.0, 20.0)] {
403            let from = TransformOperations::parse("skewX(10deg)").unwrap();
404            let to = TransformOperations::parse("skewY(20deg)").unwrap();
405            let interpolated = TransformOperations::interpolate(&from, &to, progress);
406            let operations = interpolated.operations();
407            assert_eq!(1, operations.len());
408            assert_eq!(OperationType::Skew, operations[0].type_);
409            assert_eq!(MathUtilities::deg2rad(x), operations[0].data.skew().x);
410            assert_eq!(MathUtilities::deg2rad(y), operations[0].data.skew().y);
411        }
412    }
413
414    #[test]
415    fn can_interpolate_rotation() {
416        for (progress, angle) in [(0.0, 10.0), (0.5, 15.0), (1.0, 20.0)] {
417            let from = TransformOperations::parse("rotate(10deg)").unwrap();
418            let to = TransformOperations::parse("rotate(20deg)").unwrap();
419            let interpolated = TransformOperations::interpolate(&from, &to, progress);
420            let operations = interpolated.operations();
421            assert_eq!(1, operations.len());
422            assert_eq!(OperationType::Rotate, operations[0].type_);
423            assert_eq!(MathUtilities::deg2rad(angle), operations[0].data.rotate().angle);
424        }
425    }
426
427    #[test]
428    fn interpolation_fallback_to_matrix() {
429        let from = TransformOperations::parse("rotate(45deg)").unwrap();
430        let to = TransformOperations::parse("translate(100px, 100px) rotate(1215deg)").unwrap();
431        let interpolated = TransformOperations::interpolate(&from, &to, 0.5);
432        let operations = interpolated.operations();
433        assert_eq!(1, operations.len());
434        assert_eq!(OperationType::Matrix, operations[0].type_);
435    }
436
437    fn assert_matrix(matrix: Matrix, scale_x: f64, scale_y: f64, translate_x: f64, translate_y: f64) {
438        let composed = Matrix::try_decompose_transform(matrix).unwrap();
439        assert_eq!(scale_x, composed.scale.x);
440        assert_eq!(scale_y, composed.scale.y);
441        assert_eq!(translate_x, composed.translate.x);
442        assert_eq!(translate_y, composed.translate.y);
443    }
444
445    #[test]
446    fn order_of_operations_is_preserved_no_prefix() {
447        let from = TransformOperations::parse("scale(1)").unwrap();
448        let to = TransformOperations::parse("translate(50px,50px) scale(0.5,0.5)").unwrap();
449
450        let interpolated_0 = TransformOperations::interpolate(&from, &to, 0.0);
451        assert!(interpolated_0.is_identity());
452
453        let interpolated_50 = TransformOperations::interpolate(&from, &to, 0.5);
454        assert_matrix(interpolated_50.value(), 0.75, 0.75, 12.5, 12.5);
455
456        let interpolated_100 = TransformOperations::interpolate(&from, &to, 1.0);
457        assert_matrix(interpolated_100.value(), 0.5, 0.5, 25.0, 25.0);
458    }
459
460    #[test]
461    fn order_of_operations_is_preserved_one_prefix() {
462        let from = TransformOperations::parse("scale(1)").unwrap();
463        let to = TransformOperations::parse("scale(0.5,0.5) translate(50px,50px)").unwrap();
464
465        let interpolated_0 = TransformOperations::interpolate(&from, &to, 0.0);
466        assert!(interpolated_0.is_identity());
467
468        let interpolated_50 = TransformOperations::interpolate(&from, &to, 0.5);
469        assert_matrix(interpolated_50.value(), 0.75, 0.75, 25.0, 25.0);
470
471        let interpolated_100 = TransformOperations::interpolate(&from, &to, 1.0);
472        assert_matrix(interpolated_100.value(), 0.5, 0.5, 50.0, 50.0);
473    }
474
475    #[test]
476    fn transform_group_invalidates_when_child_collection_changes() {
477        let group = TransformGroup::new();
478        let transform = TranslateTransform::with_offset(10.0, 0.0);
479        assert_eq!(Matrix::IDENTITY, group.value());
480        group.children().add(transform.clone().upcast());
481        assert_ne!(Matrix::IDENTITY, group.value());
482        group.children().clear();
483        assert_eq!(Matrix::IDENTITY, group.value());
484        group.set_children(crate::media::Transforms::from_items([transform.upcast()]));
485        assert_ne!(Matrix::IDENTITY, group.value());
486    }
487
488    #[test]
489    fn parse_rejects_invalid_input() {
490        for data in ["", "foo(1)", "rotate(10)", "rotate(10px)", "translate(10deg)", "scaleX(1,2)", "matrix(1,2,3)",
491            "matrix(1,2,3,4,5,6,7)", "scale(1", "scale 1)", "scale(abc)", "rotate(1foo)"]
492        {
493            assert!(TransformOperations::parse(data).is_err(), "{data}");
494        }
495        assert!(TransformOperations::parse(" NONE ").unwrap().is_identity());
496    }
497}