1use std::collections::HashMap;
4
5use crate::engine::animation::easing::{apply_easing, interpolate_value};
6use crate::engine::animation::types::{
7 AnimationDirection, AnimationFillMode, AnimationPlayState, AnimationSpec, Easing,
8 IterationCount, KeyframesRule, TransitionSpec,
9};
10use crate::engine::css::values::CssValue;
11use crate::engine::layouter::css_resolver::KeyframesRegistry;
12use crate::engine::layouter::dom_snapshot::NodeId;
13use crate::engine::layouter::types::ColorScheme;
14
15#[derive(Debug, Clone, Default)]
17pub struct ElementAnimationState {
18 pub transitions: Vec<TransitionSpec>,
19 pub animations: Vec<AnimationSpec>,
20 pub base_values: HashMap<String, CssValue>,
22}
23
24pub type AnimationInputs = HashMap<NodeId, ElementAnimationState>;
25
26#[derive(Debug, Clone)]
27struct TransitionInstance {
28 _property: String,
29 from: CssValue,
30 to: CssValue,
31 start_ms: f64,
32 duration_ms: f64,
33 delay_ms: f64,
34 easing: Easing,
35}
36
37#[derive(Debug, Clone)]
38struct AnimationInstance {
39 spec: AnimationSpec,
40 start_ms: f64,
41 paused_progress: Option<f32>,
43}
44
45#[derive(Debug, Clone, Default)]
46struct ElementRuntimeState {
47 transitions: HashMap<String, TransitionInstance>,
48 animations: Vec<AnimationInstance>,
49 prev_base: HashMap<String, CssValue>,
50 prev_animation_specs: Vec<AnimationSpec>,
51}
52
53#[derive(Debug, Clone, Default)]
54pub struct AnimationRuntime {
55 elements: HashMap<NodeId, ElementRuntimeState>,
56}
57
58#[derive(Debug, Clone, Default)]
59pub struct SampleResult {
60 pub overrides: HashMap<NodeId, HashMap<String, CssValue>>,
61 pub active: bool,
62 pub dirty: bool,
63}
64
65impl AnimationRuntime {
66 pub fn new() -> Self {
67 Self::default()
68 }
69
70 pub fn clear(&mut self) {
71 *self = Self::default();
72 }
73
74 pub fn sample(
75 &mut self,
76 now_ms: f64,
77 visible: bool,
78 inputs: &AnimationInputs,
79 keyframes: &KeyframesRegistry,
80 color_scheme: ColorScheme,
81 ) -> SampleResult {
82 if !visible {
83 return SampleResult::default();
86 }
87
88 let mut overrides: HashMap<NodeId, HashMap<String, CssValue>> = HashMap::new();
89 let mut active = false;
90
91 self.elements.retain(|id, _| inputs.contains_key(id));
93
94 for (node_id, input) in inputs {
95 let state = self.elements.entry(*node_id).or_default();
96 let mut node_overrides: HashMap<String, CssValue> = HashMap::new();
97 let first_seen = state.prev_base.is_empty()
98 && state.transitions.is_empty()
99 && state.animations.is_empty()
100 && state.prev_animation_specs.is_empty();
101
102 for spec in &input.transitions {
104 if spec.duration_ms <= 0.0 && spec.delay_ms <= 0.0 {
105 continue;
106 }
107 let raw_prop = spec.property.as_str();
108 if raw_prop == "none" {
109 continue;
110 }
111
112 let props: Vec<String> = if raw_prop == "all" {
113 let mut keys: Vec<String> = input.base_values.keys().cloned().collect();
114 keys.sort();
115 keys
116 } else {
117 vec![raw_prop.to_string()]
118 };
119
120 for prop in props {
121 let Some(to) = input.base_values.get(&prop) else {
122 continue;
123 };
124 let prev = state
125 .prev_base
126 .get(&prop)
127 .or_else(|| state.transitions.get(&prop).map(|t| &t.to));
128 if let Some(from) = prev {
129 if !first_seen && from != to {
132 let from_val = state
133 .transitions
134 .get(&prop)
135 .and_then(|inst| {
136 sample_transition_value(inst, now_ms, color_scheme)
137 })
138 .unwrap_or_else(|| from.clone());
139 state.transitions.insert(
140 prop.clone(),
141 TransitionInstance {
142 _property: prop.clone(),
143 from: from_val,
144 to: to.clone(),
145 start_ms: now_ms,
146 duration_ms: spec.duration_ms.max(0.0),
147 delay_ms: spec.delay_ms,
148 easing: spec.easing,
149 },
150 );
151 active = true;
152 }
153 }
154 }
155 }
156
157 let mut finished_transitions = Vec::new();
158 for (prop, inst) in &state.transitions {
159 match sample_transition_value(inst, now_ms, color_scheme) {
160 Some(val) => {
161 let end = inst.start_ms + inst.delay_ms + inst.duration_ms.max(0.0);
163 if now_ms < end {
164 active = true;
165 }
166 if now_ms >= end {
168 finished_transitions.push(prop.clone());
169 } else {
170 node_overrides.insert(prop.clone(), val);
171 }
172 }
173 None => {
174 let end = inst.start_ms + inst.delay_ms + inst.duration_ms.max(0.0);
176 if now_ms < end {
177 active = true;
178 }
179 }
180 }
181 }
182 for prop in finished_transitions {
183 state.transitions.remove(&prop);
184 }
185
186 if state.prev_animation_specs != input.animations {
188 state.animations = input
189 .animations
190 .iter()
191 .filter(|spec| spec.name != "none" && !spec.name.is_empty())
192 .map(|spec| AnimationInstance {
193 spec: spec.clone(),
194 start_ms: now_ms,
195 paused_progress: None,
196 })
197 .collect();
198 state.prev_animation_specs.clone_from(&input.animations);
199 }
200
201 for inst in state.animations.iter_mut() {
202 let Some(rule) = keyframes.get(&inst.spec.name) else {
203 continue;
204 };
205 if let Some((anim_active, anim_vals)) =
206 sample_animation(inst, rule, now_ms, color_scheme)
207 {
208 if anim_active {
209 active = true;
210 }
211 for (prop, val) in anim_vals {
212 node_overrides.insert(prop, val);
214 }
215 }
216 }
217
218 if !node_overrides.is_empty() {
219 overrides.insert(*node_id, node_overrides);
220 }
221
222 state.prev_base.clone_from(&input.base_values);
223 }
224
225 let dirty = active || !overrides.is_empty();
228
229 SampleResult {
230 overrides,
231 active,
232 dirty,
233 }
234 }
235}
236
237fn sample_transition_value(
238 inst: &TransitionInstance,
239 now_ms: f64,
240 color_scheme: ColorScheme,
241) -> Option<CssValue> {
242 let start = inst.start_ms + inst.delay_ms;
243 if now_ms < start {
244 return None;
245 }
246 let dur = inst.duration_ms;
247 if dur <= 0.0 {
248 return Some(inst.to.clone());
249 }
250 let t = ((now_ms - start) / dur).clamp(0.0, 1.0) as f32;
251 if t >= 1.0 {
252 return Some(inst.to.clone());
253 }
254 let eased = apply_easing(t, inst.easing);
255 interpolate_value(&inst.from, &inst.to, eased, color_scheme).or_else(|| {
256 if eased >= 0.5 {
257 Some(inst.to.clone())
258 } else {
259 Some(inst.from.clone())
260 }
261 })
262}
263
264fn sample_animation(
265 inst: &mut AnimationInstance,
266 rule: &KeyframesRule,
267 now_ms: f64,
268 color_scheme: ColorScheme,
269) -> Option<(bool, HashMap<String, CssValue>)> {
270 let spec = &inst.spec;
271 if spec.play_state == AnimationPlayState::Paused {
272 let progress = match inst.paused_progress {
275 Some(p) => p,
276 None => {
277 let duration = spec.duration_ms.max(0.0);
278 let delay = spec.delay_ms;
279 let local = now_ms - inst.start_ms;
280 if local < delay {
281 if matches!(
283 spec.fill_mode,
284 AnimationFillMode::Backwards | AnimationFillMode::Both
285 ) {
286 let p = iteration_progress(0.0, spec.direction, 0);
287 let vals =
288 sample_keyframes(rule, p, spec.direction, 0, spec.easing, color_scheme);
289 return Some((false, vals));
290 }
291 return Some((true, HashMap::new()));
292 }
293 let active_time = local - delay;
294 let iter_progress_total = active_time / duration;
295 let within = match spec.iteration_count {
296 IterationCount::Infinite => {
297 (iter_progress_total - iter_progress_total.floor()) as f32
298 }
299 IterationCount::Finite(count) => {
300 if count <= 0.0 {
301 0.0
302 } else if iter_progress_total >= count as f64 {
303 if count.fract() > 0.0 {
305 count.fract()
306 } else {
307 1.0
308 }
309 } else {
310 (iter_progress_total - iter_progress_total.floor()) as f32
311 }
312 }
313 };
314 inst.paused_progress = Some(within);
315 within
316 }
317 };
318 let vals = sample_keyframes(rule, progress, spec.direction, 0, spec.easing, color_scheme);
319 return Some((false, vals));
320 }
321
322 inst.paused_progress = None;
324
325 let duration = spec.duration_ms.max(0.0);
326 let delay = spec.delay_ms;
327 let local = now_ms - inst.start_ms;
328
329 if duration <= 0.0 {
330 if local < delay {
332 if matches!(
333 spec.fill_mode,
334 AnimationFillMode::Backwards | AnimationFillMode::Both
335 ) {
336 let vals =
337 sample_keyframes(rule, 0.0, spec.direction, 0, spec.easing, color_scheme);
338 return Some((false, vals));
339 }
340 return None;
341 } else {
342 if matches!(
343 spec.fill_mode,
344 AnimationFillMode::Forwards | AnimationFillMode::Both
345 ) {
346 let vals =
347 sample_keyframes(rule, 1.0, spec.direction, 0, spec.easing, color_scheme);
348 return Some((false, vals));
349 }
350 return None;
351 }
352 }
353
354 if local < delay {
355 if matches!(
356 spec.fill_mode,
357 AnimationFillMode::Backwards | AnimationFillMode::Both
358 ) {
359 let p = iteration_progress(0.0, spec.direction, 0);
361 let vals = sample_keyframes(rule, p, spec.direction, 0, spec.easing, color_scheme);
362 return Some((false, vals));
363 }
364 return Some((true, HashMap::new())); }
366
367 let active_time = local - delay;
368 let iter_progress_total = active_time / duration;
369
370 let (iteration_index, within, finished) = match spec.iteration_count {
371 IterationCount::Infinite => {
372 let idx = iter_progress_total.floor() as u32;
373 (idx, (iter_progress_total - idx as f64) as f32, false)
374 }
375 IterationCount::Finite(count) => {
376 if count <= 0.0 {
377 (0u32, 0.0f32, true)
378 } else if iter_progress_total >= count as f64 {
379 let within = if count.fract() > 0.0 {
381 count.fract()
382 } else {
383 1.0f32
384 };
385 (count.floor() as u32, within, true)
386 } else {
387 let idx = iter_progress_total.floor() as u32;
388 (idx, (iter_progress_total - idx as f64) as f32, false)
389 }
390 }
391 };
392
393 if finished {
394 if matches!(
395 spec.fill_mode,
396 AnimationFillMode::Forwards | AnimationFillMode::Both
397 ) {
398 let last_iter = match spec.iteration_count {
400 IterationCount::Finite(c) if c > 0.0 => (c.ceil() as u32).saturating_sub(1),
401 _ => 0,
402 };
403 let p = iteration_progress(1.0, spec.direction, last_iter);
404 let vals = sample_keyframes(
405 rule,
406 p,
407 spec.direction,
408 last_iter,
409 spec.easing,
410 color_scheme,
411 );
412 return Some((false, vals));
413 }
414 return None;
415 }
416
417 if spec.play_state == AnimationPlayState::Paused {
418 let p = iteration_progress(0.0, spec.direction, iteration_index);
420 let vals = sample_keyframes(
421 rule,
422 p,
423 spec.direction,
424 iteration_index,
425 spec.easing,
426 color_scheme,
427 );
428 return Some((false, vals));
429 }
430
431 let directed = iteration_progress(within.clamp(0.0, 1.0), spec.direction, iteration_index);
432 let vals = sample_keyframes(
433 rule,
434 directed,
435 spec.direction,
436 iteration_index,
437 spec.easing,
438 color_scheme,
439 );
440 Some((true, vals))
441}
442
443fn iteration_progress(t: f32, direction: AnimationDirection, iteration: u32) -> f32 {
444 let t = t.clamp(0.0, 1.0);
445 match direction {
446 AnimationDirection::Normal => t,
447 AnimationDirection::Reverse => 1.0 - t,
448 AnimationDirection::Alternate => {
449 if iteration % 2 == 0 {
450 t
451 } else {
452 1.0 - t
453 }
454 }
455 AnimationDirection::AlternateReverse => {
456 if iteration % 2 == 0 {
457 1.0 - t
458 } else {
459 t
460 }
461 }
462 }
463}
464
465fn sample_keyframes(
466 rule: &KeyframesRule,
467 progress: f32,
468 _direction: AnimationDirection,
469 _iteration: u32,
470 easing: Easing,
471 color_scheme: ColorScheme,
472) -> HashMap<String, CssValue> {
473 let mut stops = rule.stops.clone();
474 if stops.is_empty() {
475 return HashMap::new();
476 }
477 stops.sort_by(|a, b| {
478 a.offset
479 .partial_cmp(&b.offset)
480 .unwrap_or(std::cmp::Ordering::Equal)
481 });
482
483 let mut props: Vec<String> = Vec::new();
485 for stop in &stops {
486 for (name, _) in &stop.declarations {
487 if !props.iter().any(|p| p == name) {
488 props.push(name.clone());
489 }
490 }
491 }
492
493 let mut out = HashMap::new();
494 let p = progress.clamp(0.0, 1.0);
495 for prop in props {
496 let prop_stops: Vec<(f32, CssValue)> = stops
498 .iter()
499 .filter_map(|s| {
500 s.declarations
501 .iter()
502 .find(|(n, _)| n == &prop)
503 .map(|(_, v)| (s.offset, v.clone()))
504 })
505 .collect();
506 if prop_stops.is_empty() {
507 continue;
508 }
509 if prop_stops.len() == 1 {
510 out.insert(prop, prop_stops[0].1.clone());
511 continue;
512 }
513 if p <= prop_stops[0].0 {
515 out.insert(prop, prop_stops[0].1.clone());
516 continue;
517 }
518 if p >= prop_stops[prop_stops.len() - 1].0 {
519 out.insert(prop, prop_stops[prop_stops.len() - 1].1.clone());
520 continue;
521 }
522 let mut chosen = None;
523 for w in prop_stops.windows(2) {
524 let (o0, v0) = &w[0];
525 let (o1, v1) = &w[1];
526 if p >= *o0 && p <= *o1 {
527 let span = (*o1 - *o0).max(f32::EPSILON);
528 let local = ((p - *o0) / span).clamp(0.0, 1.0);
529 let eased = apply_easing(local, easing); let val = interpolate_value(v0, v1, eased, color_scheme)
531 .unwrap_or_else(|| if eased >= 0.5 { v1.clone() } else { v0.clone() });
532 chosen = Some(val);
533 break;
534 }
535 }
536 if let Some(val) = chosen.or_else(|| Some(prop_stops[0].1.clone())) {
537 out.insert(prop, val);
538 }
539 }
540
541 out
542}
543
544#[cfg(test)]
545mod tests {
546 use super::{AnimationInputs, AnimationRuntime, ElementAnimationState};
547
548 use std::collections::HashMap;
549
550 use crate::engine::animation::types::{
551 AnimationDirection, AnimationFillMode, AnimationPlayState, AnimationSpec, Easing,
552 IterationCount, KeyframeStop, KeyframesRule, TransitionSpec,
553 };
554 use crate::engine::css::values::{CssValue, Unit};
555 use crate::engine::layouter::css_resolver::KeyframesRegistry;
556 use crate::engine::layouter::dom_snapshot::NodeId;
557 use crate::engine::layouter::types::ColorScheme;
558
559 fn inputs_with_opacity(node: NodeId, opacity: f32, transition: bool) -> AnimationInputs {
560 let mut base = HashMap::new();
561 base.insert("opacity".to_string(), CssValue::Number(opacity));
562 let transitions = if transition {
563 vec![TransitionSpec {
564 property: "opacity".to_string(),
565 duration_ms: 100.0,
566 delay_ms: 0.0,
567 easing: Easing::Linear,
568 }]
569 } else {
570 vec![]
571 };
572 let mut map = HashMap::new();
573 map.insert(
574 node,
575 ElementAnimationState {
576 transitions,
577 animations: vec![],
578 base_values: base,
579 },
580 );
581 map
582 }
583
584 #[test]
585 fn transition_starts_on_value_change_and_interpolates() {
586 let mut rt = AnimationRuntime::new();
587 let node = 1u32;
588 let kf = KeyframesRegistry::new();
589 let _ = rt.sample(
590 0.0,
591 true,
592 &inputs_with_opacity(node, 0.0, true),
593 &kf,
594 ColorScheme::Light,
595 );
596 let res = rt.sample(
597 50.0,
598 true,
599 &inputs_with_opacity(node, 1.0, true),
600 &kf,
601 ColorScheme::Light,
602 );
603 assert!(res.active || res.dirty);
604 if let Some(CssValue::Number(v)) = res.overrides.get(&node).and_then(|m| m.get("opacity")) {
605 assert!(*v >= 0.0 && *v <= 1.0);
606 }
607 }
608
609 #[test]
610 fn hidden_sampling_is_idle() {
611 let mut rt = AnimationRuntime::new();
612 let node = 1u32;
613 let kf = KeyframesRegistry::new();
614 let _ = rt.sample(
615 0.0,
616 true,
617 &inputs_with_opacity(node, 0.0, true),
618 &kf,
619 ColorScheme::Light,
620 );
621 let res = rt.sample(
622 50.0,
623 false,
624 &inputs_with_opacity(node, 1.0, true),
625 &kf,
626 ColorScheme::Light,
627 );
628 assert!(!res.active);
629 assert!(res.overrides.is_empty());
630 }
631
632 #[test]
633 fn keyframes_animation_produces_interpolated_values() {
634 let mut rt = AnimationRuntime::new();
635 let node = 2u32;
636 let mut kf = KeyframesRegistry::new();
637 kf.insert(
638 "fade".to_string(),
639 KeyframesRule {
640 name: "fade".to_string(),
641 stops: vec![
642 KeyframeStop {
643 offset: 0.0,
644 declarations: vec![("opacity".into(), CssValue::Number(0.0))],
645 },
646 KeyframeStop {
647 offset: 1.0,
648 declarations: vec![("opacity".into(), CssValue::Number(1.0))],
649 },
650 ],
651 },
652 );
653 let mut map = AnimationInputs::new();
654 map.insert(
655 node,
656 ElementAnimationState {
657 transitions: vec![],
658 animations: vec![AnimationSpec {
659 name: "fade".into(),
660 duration_ms: 100.0,
661 delay_ms: 0.0,
662 easing: Easing::Linear,
663 iteration_count: IterationCount::Finite(1.0),
664 direction: AnimationDirection::Normal,
665 fill_mode: AnimationFillMode::None,
666 play_state: AnimationPlayState::Running,
667 }],
668 base_values: HashMap::new(),
669 },
670 );
671 let r0 = rt.sample(0.0, true, &map, &kf, ColorScheme::Light);
672 assert!(r0.active);
673 let r1 = rt.sample(50.0, true, &map, &kf, ColorScheme::Light);
674 let v = r1
675 .overrides
676 .get(&node)
677 .and_then(|m| m.get("opacity"))
678 .cloned();
679 match v {
680 Some(CssValue::Number(n)) => assert!(n > 0.0 && n < 1.0, "got {n}"),
681 other => panic!("unexpected {other:?}"),
682 }
683 }
684
685 #[test]
686 fn animation_fill_forwards_holds_end_value() {
687 let mut rt = AnimationRuntime::new();
688 let node = 3u32;
689 let mut kf = KeyframesRegistry::new();
690 kf.insert(
691 "fade".to_string(),
692 KeyframesRule {
693 name: "fade".to_string(),
694 stops: vec![
695 KeyframeStop {
696 offset: 0.0,
697 declarations: vec![("opacity".into(), CssValue::Number(0.0))],
698 },
699 KeyframeStop {
700 offset: 1.0,
701 declarations: vec![("opacity".into(), CssValue::Number(1.0))],
702 },
703 ],
704 },
705 );
706 let mut map = AnimationInputs::new();
707 map.insert(
708 node,
709 ElementAnimationState {
710 transitions: vec![],
711 animations: vec![AnimationSpec {
712 name: "fade".into(),
713 duration_ms: 100.0,
714 delay_ms: 0.0,
715 easing: Easing::Linear,
716 iteration_count: IterationCount::Finite(1.0),
717 direction: AnimationDirection::Normal,
718 fill_mode: AnimationFillMode::Forwards,
719 play_state: AnimationPlayState::Running,
720 }],
721 base_values: HashMap::new(),
722 },
723 );
724 let _ = rt.sample(0.0, true, &map, &kf, ColorScheme::Light);
725 let r_end = rt.sample(150.0, true, &map, &kf, ColorScheme::Light);
726 let v = r_end
727 .overrides
728 .get(&node)
729 .and_then(|m| m.get("opacity"))
730 .cloned();
731 assert_eq!(v, Some(CssValue::Number(1.0)));
732 }
733
734 #[test]
735 fn transition_property_all_animates_tracked_base_changes() {
736 let mut rt = AnimationRuntime::new();
737 let node = 4u32;
738 let kf = KeyframesRegistry::new();
739 let mut base0 = HashMap::new();
740 base0.insert("opacity".to_string(), CssValue::Number(0.0));
741 let mut map0 = AnimationInputs::new();
742 map0.insert(
743 node,
744 ElementAnimationState {
745 transitions: vec![TransitionSpec {
746 property: "all".to_string(),
747 duration_ms: 100.0,
748 delay_ms: 0.0,
749 easing: Easing::Linear,
750 }],
751 animations: vec![],
752 base_values: base0,
753 },
754 );
755 let _ = rt.sample(0.0, true, &map0, &kf, ColorScheme::Light);
756
757 let mut base1 = HashMap::new();
758 base1.insert("opacity".to_string(), CssValue::Number(1.0));
759 let mut map1 = AnimationInputs::new();
760 map1.insert(
761 node,
762 ElementAnimationState {
763 transitions: vec![TransitionSpec {
764 property: "all".to_string(),
765 duration_ms: 100.0,
766 delay_ms: 0.0,
767 easing: Easing::Linear,
768 }],
769 animations: vec![],
770 base_values: base1,
771 },
772 );
773 let _ = rt.sample(50.0, true, &map1, &kf, ColorScheme::Light);
775 let res = rt.sample(100.0, true, &map1, &kf, ColorScheme::Light);
776 assert!(res.active || res.dirty);
777 if let Some(CssValue::Number(v)) = res.overrides.get(&node).and_then(|m| m.get("opacity")) {
778 assert!(*v > 0.0 && *v < 1.0, "expected mid transition, got {v}");
779 } else {
780 panic!("expected opacity override");
781 }
782 }
783
784 #[test]
785 fn keyframes_animation_interpolates_transform_rotate() {
786 let mut rt = AnimationRuntime::new();
787 let node = 6u32;
788 let mut kf = KeyframesRegistry::new();
789 kf.insert(
790 "spin".to_string(),
791 KeyframesRule {
792 name: "spin".to_string(),
793 stops: vec![
794 KeyframeStop {
795 offset: 0.0,
796 declarations: vec![(
797 "transform".into(),
798 CssValue::Function(
799 "rotate".into(),
800 vec![vec![CssValue::Length(0.0, Unit::Deg)]],
801 ),
802 )],
803 },
804 KeyframeStop {
805 offset: 1.0,
806 declarations: vec![(
807 "transform".into(),
808 CssValue::Function(
809 "rotate".into(),
810 vec![vec![CssValue::Length(360.0, Unit::Deg)]],
811 ),
812 )],
813 },
814 ],
815 },
816 );
817 let mut map = AnimationInputs::new();
818 map.insert(
819 node,
820 ElementAnimationState {
821 transitions: vec![],
822 animations: vec![AnimationSpec {
823 name: "spin".into(),
824 duration_ms: 100.0,
825 delay_ms: 0.0,
826 easing: Easing::Linear,
827 iteration_count: IterationCount::Finite(1.0),
828 direction: AnimationDirection::Normal,
829 fill_mode: AnimationFillMode::None,
830 play_state: AnimationPlayState::Running,
831 }],
832 base_values: HashMap::new(),
833 },
834 );
835 let _ = rt.sample(0.0, true, &map, &kf, ColorScheme::Light);
836 let r_mid = rt.sample(50.0, true, &map, &kf, ColorScheme::Light);
837 let v = r_mid
838 .overrides
839 .get(&node)
840 .and_then(|m| m.get("transform"))
841 .cloned();
842 match v {
843 Some(CssValue::Function(name, args)) if name == "rotate" => {
844 let CssValue::Length(deg, Unit::Deg) = args[0][0] else {
845 panic!("expected Length deg, got {args:?}")
846 };
847 assert!((deg - 180.0).abs() < 1e-4, "expected 180deg, got {deg}");
848 }
849 other => panic!("unexpected {other:?}"),
850 }
851 }
852
853 #[test]
854 fn keyframes_animation_interpolates_transform_translate() {
855 let mut rt = AnimationRuntime::new();
856 let node = 7u32;
857 let mut kf = KeyframesRegistry::new();
858 kf.insert(
859 "slide".to_string(),
860 KeyframesRule {
861 name: "slide".to_string(),
862 stops: vec![
863 KeyframeStop {
864 offset: 0.0,
865 declarations: vec![(
866 "transform".into(),
867 CssValue::Function(
868 "translate".into(),
869 vec![vec![
870 CssValue::Length(0.0, Unit::Px),
871 CssValue::Length(0.0, Unit::Px),
872 ]],
873 ),
874 )],
875 },
876 KeyframeStop {
877 offset: 1.0,
878 declarations: vec![(
879 "transform".into(),
880 CssValue::Function(
881 "translate".into(),
882 vec![vec![
883 CssValue::Length(200.0, Unit::Px),
884 CssValue::Length(100.0, Unit::Px),
885 ]],
886 ),
887 )],
888 },
889 ],
890 },
891 );
892 let mut map = AnimationInputs::new();
893 map.insert(
894 node,
895 ElementAnimationState {
896 transitions: vec![],
897 animations: vec![AnimationSpec {
898 name: "slide".into(),
899 duration_ms: 100.0,
900 delay_ms: 0.0,
901 easing: Easing::Linear,
902 iteration_count: IterationCount::Finite(1.0),
903 direction: AnimationDirection::Normal,
904 fill_mode: AnimationFillMode::None,
905 play_state: AnimationPlayState::Running,
906 }],
907 base_values: HashMap::new(),
908 },
909 );
910 let _ = rt.sample(0.0, true, &map, &kf, ColorScheme::Light);
911 let r_mid = rt.sample(50.0, true, &map, &kf, ColorScheme::Light);
912 let v = r_mid
913 .overrides
914 .get(&node)
915 .and_then(|m| m.get("transform"))
916 .cloned();
917 match v {
918 Some(CssValue::Function(name, args)) if name == "translate" => {
919 assert_eq!(args.len(), 1);
920 let CssValue::Length(x, Unit::Px) = args[0][0] else {
921 panic!("expected Length px, got {args:?}")
922 };
923 let CssValue::Length(y, Unit::Px) = args[0][1] else {
924 panic!("expected Length px, got {args:?}")
925 };
926 assert!((x - 100.0).abs() < 1e-4, "expected 100px, got {x}");
927 assert!((y - 50.0).abs() < 1e-4, "expected 50px, got {y}");
928 }
929 other => panic!("unexpected {other:?}"),
930 }
931 }
932
933 #[test]
934 fn animation_alternate_reverses_second_iteration() {
935 let mut rt = AnimationRuntime::new();
936 let node = 5u32;
937 let mut kf = KeyframesRegistry::new();
938 kf.insert(
939 "fade".to_string(),
940 KeyframesRule {
941 name: "fade".to_string(),
942 stops: vec![
943 KeyframeStop {
944 offset: 0.0,
945 declarations: vec![("opacity".into(), CssValue::Number(0.0))],
946 },
947 KeyframeStop {
948 offset: 1.0,
949 declarations: vec![("opacity".into(), CssValue::Number(1.0))],
950 },
951 ],
952 },
953 );
954 let mut map = AnimationInputs::new();
955 map.insert(
956 node,
957 ElementAnimationState {
958 transitions: vec![],
959 animations: vec![AnimationSpec {
960 name: "fade".into(),
961 duration_ms: 100.0,
962 delay_ms: 0.0,
963 easing: Easing::Linear,
964 iteration_count: IterationCount::Finite(2.0),
965 direction: AnimationDirection::Alternate,
966 fill_mode: AnimationFillMode::None,
967 play_state: AnimationPlayState::Running,
968 }],
969 base_values: HashMap::new(),
970 },
971 );
972 let _ = rt.sample(0.0, true, &map, &kf, ColorScheme::Light);
973 let r1 = rt.sample(50.0, true, &map, &kf, ColorScheme::Light);
975 let r2 = rt.sample(150.0, true, &map, &kf, ColorScheme::Light);
977 let v1 = match r1.overrides.get(&node).and_then(|m| m.get("opacity")) {
978 Some(CssValue::Number(v)) => *v,
979 other => panic!("unexpected {other:?}"),
980 };
981 let v2 = match r2.overrides.get(&node).and_then(|m| m.get("opacity")) {
982 Some(CssValue::Number(v)) => *v,
983 other => panic!("unexpected {other:?}"),
984 };
985 assert!(v1 > 0.3 && v1 < 0.7, "v1={v1}");
986 assert!(v2 > 0.3 && v2 < 0.7, "v2={v2}");
987 }
989
990 #[test]
991 fn keyframes_animation_honors_timing_function() {
992 let mut rt = AnimationRuntime::new();
993 let node = 8u32;
994 let mut kf = KeyframesRegistry::new();
995 kf.insert(
996 "fade".to_string(),
997 KeyframesRule {
998 name: "fade".to_string(),
999 stops: vec![
1000 KeyframeStop {
1001 offset: 0.0,
1002 declarations: vec![("opacity".into(), CssValue::Number(0.0))],
1003 },
1004 KeyframeStop {
1005 offset: 1.0,
1006 declarations: vec![("opacity".into(), CssValue::Number(1.0))],
1007 },
1008 ],
1009 },
1010 );
1011 let mut map = AnimationInputs::new();
1012 map.insert(
1013 node,
1014 ElementAnimationState {
1015 transitions: vec![],
1016 animations: vec![AnimationSpec {
1017 name: "fade".into(),
1018 duration_ms: 100.0,
1019 delay_ms: 0.0,
1020 easing: Easing::Ease,
1021 iteration_count: IterationCount::Finite(1.0),
1022 direction: AnimationDirection::Normal,
1023 fill_mode: AnimationFillMode::None,
1024 play_state: AnimationPlayState::Running,
1025 }],
1026 base_values: HashMap::new(),
1027 },
1028 );
1029 let _ = rt.sample(0.0, true, &map, &kf, ColorScheme::Light);
1030 let mid = rt.sample(50.0, true, &map, &kf, ColorScheme::Light);
1031 match mid.overrides.get(&node).and_then(|m| m.get("opacity")) {
1032 Some(CssValue::Number(v)) => assert!(*v > 0.7, "expected eased progress, got {v}"),
1034 other => panic!("unexpected {other:?}"),
1035 }
1036 }
1037
1038 #[test]
1039 fn transform_transition_from_none_interpolates_from_identity() {
1040 let mut rt = AnimationRuntime::new();
1041 let node = 9u32;
1042 let kf = KeyframesRegistry::new();
1043 let transition = vec![TransitionSpec {
1044 property: "transform".to_string(),
1045 duration_ms: 100.0,
1046 delay_ms: 0.0,
1047 easing: Easing::Linear,
1048 }];
1049
1050 let mut base0 = HashMap::new();
1051 base0.insert("transform".to_string(), CssValue::Keyword("none".into()));
1052 let mut map0 = AnimationInputs::new();
1053 map0.insert(
1054 node,
1055 ElementAnimationState {
1056 transitions: transition.clone(),
1057 animations: vec![],
1058 base_values: base0,
1059 },
1060 );
1061 let _ = rt.sample(0.0, true, &map0, &kf, ColorScheme::Light);
1062
1063 let mut base1 = HashMap::new();
1064 base1.insert(
1065 "transform".to_string(),
1066 CssValue::Function(
1067 "translateX".into(),
1068 vec![vec![CssValue::Length(100.0, Unit::Px)]],
1069 ),
1070 );
1071 let mut map1 = AnimationInputs::new();
1072 map1.insert(
1073 node,
1074 ElementAnimationState {
1075 transitions: transition,
1076 animations: vec![],
1077 base_values: base1,
1078 },
1079 );
1080 let _ = rt.sample(50.0, true, &map1, &kf, ColorScheme::Light);
1081 let mid = rt.sample(100.0, true, &map1, &kf, ColorScheme::Light);
1082 match mid.overrides.get(&node).and_then(|m| m.get("transform")) {
1083 Some(CssValue::Function(name, args)) if name == "translate" => {
1084 let CssValue::Length(x, Unit::Px) = args[0][0] else {
1085 panic!("expected Length px, got {args:?}")
1086 };
1087 assert!((x - 50.0).abs() < 1e-3, "expected 50px, got {x}");
1088 }
1089 other => panic!("unexpected {other:?}"),
1090 }
1091 }
1092}