rustmotion_core/css/
animation.rs1use crate::css::style::CssStyle;
11use crate::css::style::{FilterFn, Size, TransformFn};
12use crate::css::units::{Length, LengthPercentage};
13use crate::engine::animator::AnimatedProperties;
14
15pub fn apply_animated_props(css: &mut CssStyle, props: &AnimatedProperties) {
23 let mut tx: Vec<TransformFn> = Vec::new();
25 if props.translate_x != 0.0 || props.translate_y != 0.0 {
26 tx.push(TransformFn::Translate {
27 x: LengthPercentage::Px(props.translate_x),
28 y: LengthPercentage::Px(props.translate_y),
29 });
30 }
31 let sx = props.scale_x;
32 let sy = props.scale_y;
33 if (sx - 1.0).abs() > 1e-4 || (sy - 1.0).abs() > 1e-4 {
34 tx.push(TransformFn::Scale { x: sx, y: sy });
35 }
36 if props.rotation.abs() > 1e-3 {
37 tx.push(TransformFn::Rotate {
38 deg: props.rotation,
39 });
40 }
41 if props.rotate_x.abs() > 1e-3 {
42 tx.push(TransformFn::RotateX {
43 deg: props.rotate_x,
44 });
45 }
46 if props.rotate_y.abs() > 1e-3 {
47 tx.push(TransformFn::RotateY {
48 deg: props.rotate_y,
49 });
50 }
51 if !tx.is_empty() {
52 match css.transform.as_mut() {
55 Some(existing) => existing.extend(tx),
56 None => css.transform = Some(tx),
57 }
58 }
59
60 if (props.opacity - 1.0).abs() > 1e-4 {
64 let base = css.opacity.unwrap_or(1.0);
65 css.opacity = Some(base * props.opacity);
66 }
67
68 let mut filters: Vec<FilterFn> = Vec::new();
70 if props.blur > 0.0 {
71 filters.push(FilterFn::Blur {
72 radius: Length::Px(props.blur),
73 });
74 }
75 if props.glow_radius > 0.0 && props.glow_intensity > 0.0 {
76 filters.push(FilterFn::DropShadow {
77 offset_x: Length::Px(0.0),
78 offset_y: Length::Px(0.0),
79 blur: Some(Length::Px(props.glow_radius)),
80 color: None,
81 });
82 }
83 if !filters.is_empty() {
84 match css.filter.as_mut() {
85 Some(existing) => existing.extend(filters),
86 None => css.filter = Some(filters),
87 }
88 }
89
90 if props.perspective > 0.0 {
92 css.perspective = Some(Length::Px(props.perspective));
93 }
94
95 if props.width >= 0.0 {
103 css.width = Some(Size::Length(LengthPercentage::Px(props.width)));
104 }
105 if props.height >= 0.0 {
106 css.height = Some(Size::Length(LengthPercentage::Px(props.height)));
107 }
108
109 }
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn translate_props_become_transform_translate() {
123 let mut css = CssStyle::default();
124 let props = AnimatedProperties {
125 translate_x: 10.0,
126 translate_y: -5.0,
127 ..AnimatedProperties::default()
128 };
129 apply_animated_props(&mut css, &props);
130
131 let tx = css.transform.expect("transform list created");
132 assert_eq!(tx.len(), 1);
133 match &tx[0] {
134 TransformFn::Translate { x, y } => {
135 assert!(matches!(x, LengthPercentage::Px(v) if (*v - 10.0).abs() < 1e-6));
136 assert!(matches!(y, LengthPercentage::Px(v) if (*v + 5.0).abs() < 1e-6));
137 }
138 other => panic!("expected Translate, got {:?}", other),
139 }
140 }
141
142 #[test]
143 fn opacity_is_multiplied_with_existing_css_opacity() {
144 let mut css = CssStyle {
145 opacity: Some(0.5),
146 ..CssStyle::default()
147 };
148 let props = AnimatedProperties {
149 opacity: 0.5,
150 ..AnimatedProperties::default()
151 };
152 apply_animated_props(&mut css, &props);
153 assert!((css.opacity.unwrap() - 0.25).abs() < 1e-6);
154 }
155
156 #[test]
157 fn no_op_props_leave_css_untouched() {
158 let mut css = CssStyle::default();
159 let props = AnimatedProperties::default();
160 apply_animated_props(&mut css, &props);
161 assert!(css.transform.is_none());
162 assert!(css.opacity.is_none());
163 assert!(css.filter.is_none());
164 assert!(css.perspective.is_none());
165 }
166
167 #[test]
178 fn motion_path_shaped_translate_and_rotation_compose_into_one_transform_list() {
179 let mut css = CssStyle::default();
180 let props = AnimatedProperties {
181 translate_x: 120.0,
182 translate_y: -40.0,
183 rotation: 33.5,
184 ..AnimatedProperties::default()
185 };
186 apply_animated_props(&mut css, &props);
187
188 let tx = css.transform.expect("transform list created");
189 assert_eq!(tx.len(), 2, "expected translate + rotate, got {:?}", tx);
190 match &tx[0] {
191 TransformFn::Translate { x, y } => {
192 assert!(matches!(x, LengthPercentage::Px(v) if (*v - 120.0).abs() < 1e-6));
193 assert!(matches!(y, LengthPercentage::Px(v) if (*v + 40.0).abs() < 1e-6));
194 }
195 other => panic!("expected Translate first, got {:?}", other),
196 }
197 match &tx[1] {
198 TransformFn::Rotate { deg } => assert!((*deg - 33.5).abs() < 1e-6),
199 other => panic!("expected Rotate second, got {:?}", other),
200 }
201 }
202
203 #[test]
204 fn blur_and_glow_compose_into_filter_list() {
205 let mut css = CssStyle::default();
206 let props = AnimatedProperties {
207 blur: 4.0,
208 glow_radius: 8.0,
209 glow_intensity: 1.0,
210 ..AnimatedProperties::default()
211 };
212 apply_animated_props(&mut css, &props);
213
214 let filters = css.filter.expect("filter list created");
215 assert_eq!(filters.len(), 2);
216 assert!(matches!(filters[0], FilterFn::Blur { .. }));
217 assert!(matches!(filters[1], FilterFn::DropShadow { .. }));
218 }
219}