1use crate::css::style::CssStyle;
2use crate::css::style::{BorderRadius, ClipPath, Edges, FilterFn, Gap, Size, TransformFn};
3use crate::css::units::{Length, LengthPercentage};
4use crate::engine::animator::AnimatedProperties;
5
6pub fn apply_animated_props(css: &mut CssStyle, props: &AnimatedProperties) {
7 let mut tx: Vec<TransformFn> = Vec::new();
8 if props.translate_x != 0.0 || props.translate_y != 0.0 {
9 tx.push(TransformFn::Translate {
10 x: LengthPercentage::Px(props.translate_x),
11 y: LengthPercentage::Px(props.translate_y),
12 });
13 }
14 let sx = props.scale_x;
15 let sy = props.scale_y;
16 if (sx - 1.0).abs() > 1e-4 || (sy - 1.0).abs() > 1e-4 {
17 tx.push(TransformFn::Scale { x: sx, y: sy });
18 }
19 if props.rotation.abs() > 1e-3 {
20 tx.push(TransformFn::Rotate {
21 deg: props.rotation,
22 });
23 }
24 if props.rotate_x.abs() > 1e-3 {
25 tx.push(TransformFn::RotateX {
26 deg: props.rotate_x,
27 });
28 }
29 if props.rotate_y.abs() > 1e-3 {
30 tx.push(TransformFn::RotateY {
31 deg: props.rotate_y,
32 });
33 }
34 if !tx.is_empty() {
35 match css.transform.as_mut() {
36 Some(existing) => existing.extend(tx),
37 None => css.transform = Some(tx),
38 }
39 }
40
41 if (props.opacity - 1.0).abs() > 1e-4 {
42 css.opacity = Some(props.opacity);
43 }
44
45 let mut filters: Vec<FilterFn> = Vec::new();
46 if props.blur > 0.0 {
47 filters.push(FilterFn::Blur {
48 radius: Some(Length::Px(props.blur)),
49 radius_x: None,
50 radius_y: None,
51 });
52 }
53 if props.glow_radius > 0.0 && props.glow_intensity > 0.0 {
54 filters.push(FilterFn::DropShadow {
55 offset_x: Length::Px(0.0),
56 offset_y: Length::Px(0.0),
57 blur: Some(Length::Px(props.glow_radius)),
58 color: None,
59 });
60 }
61 if !filters.is_empty() {
62 match css.filter.as_mut() {
63 Some(existing) => existing.extend(filters),
64 None => css.filter = Some(filters),
65 }
66 }
67
68 if props.perspective > 0.0 {
69 css.perspective = Some(Length::Px(props.perspective));
70 }
71
72 if props.width >= 0.0 {
73 css.width = Some(Size::Length(LengthPercentage::Px(props.width)));
74 }
75 if props.height >= 0.0 {
76 css.height = Some(Size::Length(LengthPercentage::Px(props.height)));
77 }
78
79 if props.border_radius >= 0.0 {
80 css.border_radius = Some(BorderRadius::Uniform(LengthPercentage::Px(
81 props.border_radius,
82 )));
83 }
84
85 if props.clip_path_progress >= 0.0 {
86 if let Some(ClipPath::Morph { progress, .. }) = css.clip_path.as_mut() {
87 *progress = props.clip_path_progress;
88 }
89 }
90
91 if props.font_size >= 0.0 {
92 css.font_size = Some(Length::Px(props.font_size));
93 }
94
95 if props.letter_spacing.is_finite() {
96 css.letter_spacing = Some(Length::Px(props.letter_spacing));
97 }
98
99 if props.gap >= 0.0 {
100 css.gap = Some(Gap::Uniform(LengthPercentage::Px(props.gap)));
101 }
102
103 if props.padding >= 0.0 {
104 css.padding = Some(Edges::Uniform(LengthPercentage::Px(props.padding)));
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn translate_props_become_transform_translate() {
114 let mut css = CssStyle::default();
115 let props = AnimatedProperties {
116 translate_x: 10.0,
117 translate_y: -5.0,
118 ..AnimatedProperties::default()
119 };
120 apply_animated_props(&mut css, &props);
121
122 let tx = css.transform.expect("transform list created");
123 assert_eq!(tx.len(), 1);
124 match &tx[0] {
125 TransformFn::Translate { x, y } => {
126 assert!(matches!(x, LengthPercentage::Px(v) if (*v - 10.0).abs() < 1e-6));
127 assert!(matches!(y, LengthPercentage::Px(v) if (*v + 5.0).abs() < 1e-6));
128 }
129 other => panic!("expected Translate, got {:?}", other),
130 }
131 }
132
133 #[test]
134 fn an_active_opacity_animation_replaces_the_declared_static_opacity() {
135 let mut css = CssStyle {
136 opacity: Some(0.5),
137 ..CssStyle::default()
138 };
139 let props = AnimatedProperties {
140 opacity: 0.5,
141 ..AnimatedProperties::default()
142 };
143 apply_animated_props(&mut css, &props);
144 assert!(
145 (css.opacity.unwrap() - 0.5).abs() < 1e-6,
146 "an animation touching opacity must overwrite the static declaration outright, the \
147 same last-value-written rule every other animated property in this function \
148 follows — not multiply against it, got {:?}",
149 css.opacity
150 );
151 }
152
153 #[test]
154 fn a_static_opacity_of_zero_is_overridden_by_an_active_fade_in_animation() {
155 let mut css = CssStyle {
156 opacity: Some(0.0),
157 ..CssStyle::default()
158 };
159 let props = AnimatedProperties {
160 opacity: 0.8,
161 ..AnimatedProperties::default()
162 };
163 apply_animated_props(&mut css, &props);
164 assert!(
165 (css.opacity.unwrap() - 0.8).abs() < 1e-6,
166 "a declared opacity: 0 authored as the fade-in's own starting point must not cancel \
167 the animation for the rest of its run (issue #430); got {:?}",
168 css.opacity
169 );
170 }
171
172 #[test]
173 fn no_op_props_leave_css_untouched() {
174 let mut css = CssStyle::default();
175 let props = AnimatedProperties::default();
176 apply_animated_props(&mut css, &props);
177 assert!(css.transform.is_none());
178 assert!(css.opacity.is_none());
179 assert!(css.filter.is_none());
180 assert!(css.perspective.is_none());
181 }
182
183 #[test]
184 fn motion_path_shaped_translate_and_rotation_compose_into_one_transform_list() {
185 let mut css = CssStyle::default();
186 let props = AnimatedProperties {
187 translate_x: 120.0,
188 translate_y: -40.0,
189 rotation: 33.5,
190 ..AnimatedProperties::default()
191 };
192 apply_animated_props(&mut css, &props);
193
194 let tx = css.transform.expect("transform list created");
195 assert_eq!(tx.len(), 2, "expected translate + rotate, got {:?}", tx);
196 match &tx[0] {
197 TransformFn::Translate { x, y } => {
198 assert!(matches!(x, LengthPercentage::Px(v) if (*v - 120.0).abs() < 1e-6));
199 assert!(matches!(y, LengthPercentage::Px(v) if (*v + 40.0).abs() < 1e-6));
200 }
201 other => panic!("expected Translate first, got {:?}", other),
202 }
203 match &tx[1] {
204 TransformFn::Rotate { deg } => assert!((*deg - 33.5).abs() < 1e-6),
205 other => panic!("expected Rotate second, got {:?}", other),
206 }
207 }
208
209 #[test]
210 fn animated_border_radius_reaches_the_css_style() {
211 let mut css = CssStyle::default();
212 let props = AnimatedProperties {
213 border_radius: 100.0,
214 ..AnimatedProperties::default()
215 };
216 apply_animated_props(&mut css, &props);
217
218 match css.border_radius {
219 Some(BorderRadius::Uniform(LengthPercentage::Px(v))) => {
220 assert!((v - 100.0).abs() < 1e-6)
221 }
222 other => panic!("expected a uniform 100px border-radius, got {other:?}"),
223 }
224 }
225
226 #[test]
227 fn animated_clip_path_progress_writes_onto_the_morph_variant() {
228 let mut css = CssStyle {
229 clip_path: Some(ClipPath::Morph {
230 from: Box::new(ClipPath::Circle {
231 radius: LengthPercentage::Px(10.0),
232 origin: None,
233 }),
234 to: Box::new(ClipPath::Circle {
235 radius: LengthPercentage::Px(50.0),
236 origin: None,
237 }),
238 via: Vec::new(),
239 progress: 0.0,
240 }),
241 ..CssStyle::default()
242 };
243 let props = AnimatedProperties {
244 clip_path_progress: 0.75,
245 ..AnimatedProperties::default()
246 };
247 apply_animated_props(&mut css, &props);
248
249 match css.clip_path {
250 Some(ClipPath::Morph { progress, .. }) => {
251 assert!((progress - 0.75).abs() < 1e-6)
252 }
253 other => panic!("expected the morph's progress to be updated, got {other:?}"),
254 }
255 }
256
257 #[test]
258 fn no_clip_path_progress_leaves_a_non_morph_clip_path_untouched() {
259 let mut css = CssStyle {
260 clip_path: Some(ClipPath::Circle {
261 radius: LengthPercentage::Px(10.0),
262 origin: None,
263 }),
264 ..CssStyle::default()
265 };
266 let props = AnimatedProperties {
267 clip_path_progress: 0.5,
268 ..AnimatedProperties::default()
269 };
270 apply_animated_props(&mut css, &props);
271
272 assert_eq!(
273 css.clip_path,
274 Some(ClipPath::Circle {
275 radius: LengthPercentage::Px(10.0),
276 origin: None,
277 }),
278 "a static (non-morph) clip-path must not be mutated by clip_path_progress"
279 );
280 }
281
282 #[test]
283 fn blur_and_glow_compose_into_filter_list() {
284 let mut css = CssStyle::default();
285 let props = AnimatedProperties {
286 blur: 4.0,
287 glow_radius: 8.0,
288 glow_intensity: 1.0,
289 ..AnimatedProperties::default()
290 };
291 apply_animated_props(&mut css, &props);
292
293 let filters = css.filter.expect("filter list created");
294 assert_eq!(filters.len(), 2);
295 assert!(matches!(filters[0], FilterFn::Blur { .. }));
296 assert!(matches!(filters[1], FilterFn::DropShadow { .. }));
297 }
298
299 #[test]
300 fn an_animation_touching_only_font_size_reaches_the_css_style() {
301 let mut css = CssStyle::default();
302 let props = AnimatedProperties {
303 font_size: 120.0,
304 ..AnimatedProperties::default()
305 };
306 apply_animated_props(&mut css, &props);
307 assert_eq!(css.font_size, Some(Length::Px(120.0)));
308 }
309
310 #[test]
311 fn an_animation_touching_only_letter_spacing_reaches_the_css_style() {
312 let mut css = CssStyle::default();
313 let props = AnimatedProperties {
314 letter_spacing: 6.0,
315 ..AnimatedProperties::default()
316 };
317 apply_animated_props(&mut css, &props);
318 assert_eq!(css.letter_spacing, Some(Length::Px(6.0)));
319 }
320
321 #[test]
322 fn an_untouched_letter_spacing_nan_resting_value_leaves_css_untouched() {
323 let mut css = CssStyle::default();
324 let props = AnimatedProperties::default();
325 apply_animated_props(&mut css, &props);
326 assert!(
327 css.letter_spacing.is_none(),
328 "letter_spacing's resting value is NaN precisely so 'not animated' can be told apart \
329 from 'animated to zero'; it must not be written as a css value"
330 );
331 }
332
333 #[test]
334 fn an_animation_touching_only_gap_reaches_the_css_style() {
335 let mut css = CssStyle::default();
336 let props = AnimatedProperties {
337 gap: 24.0,
338 ..AnimatedProperties::default()
339 };
340 apply_animated_props(&mut css, &props);
341 assert_eq!(css.gap, Some(Gap::Uniform(LengthPercentage::Px(24.0))));
342 }
343
344 #[test]
345 fn an_animation_touching_only_padding_reaches_the_css_style() {
346 let mut css = CssStyle::default();
347 let props = AnimatedProperties {
348 padding: 32.0,
349 ..AnimatedProperties::default()
350 };
351 apply_animated_props(&mut css, &props);
352 assert_eq!(
353 css.padding,
354 Some(Edges::Uniform(LengthPercentage::Px(32.0)))
355 );
356 }
357}