Skip to main content

cvkg_layout/
animation.rs

1use cvkg_core::{LayoutCache, LayoutView, Rect};
2use std::collections::HashMap;
3
4/// Manages active physics transitions for layout bounding boxes.
5pub struct AnimationEngine {
6    pub active_transitions: HashMap<u64, cvkg_anim::physics::ViscousSpring>,
7    /// Generation counter for transition eviction.
8    pub eviction_generation: u64,
9    /// Tracks which generation each transition was last touched in.
10    pub transition_generation: HashMap<u64, u64>,
11    /// Number of generations a transition can go untouched before eviction.
12    pub eviction_threshold: u64,
13}
14
15impl Default for AnimationEngine {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl AnimationEngine {
22    /// Creates a new AnimationEngine.
23    pub fn new() -> Self {
24        Self {
25            active_transitions: HashMap::new(),
26            eviction_generation: 0,
27            transition_generation: HashMap::new(),
28            eviction_threshold: 300,
29        }
30    }
31
32    /// Retrieves or initializes the AnimationEngine in the layout cache.
33    pub fn get_or_insert_engine(cache: &mut LayoutCache) -> &mut Self {
34        if cache.animators.is_none() {
35            cache.animators = Some(Box::new(AnimationEngine::new()));
36        }
37        cache
38            .animators
39            .as_mut()
40            .unwrap()
41            .downcast_mut::<AnimationEngine>()
42            .unwrap()
43    }
44
45    /// Evict settled transitions that haven't been touched for N generations.
46    pub fn evict_stale_transitions(&mut self) {
47        self.eviction_generation += 1;
48        let threshold = self.eviction_threshold;
49        let current_gen = self.eviction_generation;
50        self.active_transitions.retain(|hash, spring| {
51            let recent = self
52                .transition_generation
53                .get(hash)
54                .map_or(false, |g| current_gen - *g < threshold);
55            let unsettled = spring.velocity_a.length_sq() > 0.0001 || spring.velocity_b.length_sq() > 0.0001;
56            recent || unsettled
57        });
58        self.transition_generation
59            .retain(|hash, _| self.active_transitions.contains_key(hash));
60    }
61}
62
63/// Applies view transitions to calculated layout rects.
64pub fn apply_layout_animations(
65    rects: Vec<Rect>,
66    subviews: &mut [&mut dyn LayoutView],
67    cache: &mut LayoutCache,
68) {
69    let mut transitions_to_update = Vec::new();
70
71    for (child, target_rect) in subviews.iter().zip(&rects) {
72        let hash = child.view_hash();
73        if hash != 0 {
74            if let Some(prev) = cache.previous_rects.get(&hash) {
75                let dx = (prev.x - target_rect.x).abs();
76                let dy = (prev.y - target_rect.y).abs();
77                let dw = (prev.width - target_rect.width).abs();
78                let dh = (prev.height - target_rect.height).abs();
79                let epsilon = 1e-3;
80                if dx > epsilon || dy > epsilon || dw > epsilon || dh > epsilon {
81                    transitions_to_update.push((hash, *prev, *target_rect));
82                }
83            }
84            cache.previous_rects.insert(hash, *target_rect);
85            cache.previous_rects_generation.insert(hash, cache.eviction_generation);
86        }
87    }
88
89    let mut interpolated_rects = HashMap::new();
90    let delta = cache.delta_time;
91    let scale = cache.scale_factor;
92    let anim_engine = AnimationEngine::get_or_insert_engine(cache);
93
94    for (hash, prev, target_rect) in transitions_to_update {
95        let mut spring = if let Some(mut existing) = anim_engine.active_transitions.remove(&hash) {
96            existing.position_b =
97                cvkg_anim::physics::Vec3::new(target_rect.x, target_rect.y, target_rect.width);
98            existing
99        } else {
100            cvkg_anim::physics::ViscousSpring::new(
101                cvkg_anim::physics::Vec3::new(prev.x, prev.y, prev.width),
102                cvkg_anim::physics::Vec3::new(target_rect.x, target_rect.y, target_rect.width),
103                0.9,
104                1000.0,
105            )
106        };
107        spring.step(delta);
108
109        let speed = (spring.velocity_a.length_sq() + spring.velocity_b.length_sq()).sqrt();
110        let snap = |v: f32| (v * scale).round() / scale;
111
112        let (rx, ry, rw) = if speed < 0.05 {
113            (
114                snap(spring.position_a.x),
115                snap(spring.position_a.y),
116                snap(spring.position_a.z),
117            )
118        } else {
119            (
120                spring.position_a.x,
121                spring.position_a.y,
122                spring.position_a.z,
123            )
124        };
125
126        interpolated_rects.insert(
127            hash,
128            Rect {
129                x: rx,
130                y: ry,
131                width: rw,
132                height: target_rect.height,
133            },
134        );
135        anim_engine.active_transitions.insert(hash, spring);
136        anim_engine.transition_generation.insert(hash, anim_engine.eviction_generation);
137    }
138
139    cache.evict_stale_entries();
140
141    let anim_engine = AnimationEngine::get_or_insert_engine(cache);
142    anim_engine.evict_stale_transitions();
143
144    for (child, mut target_rect) in subviews.iter_mut().zip(rects) {
145        let hash = child.view_hash();
146        if let Some(interp) = interpolated_rects.get(&hash) {
147            target_rect = *interp;
148        }
149        let is_visible = if let Some(viewport) = cache.viewport {
150            target_rect.intersects(&viewport)
151        } else {
152            true
153        };
154        if is_visible {
155            crate::with_layout_cycle_guard_void(hash, || {
156                child.place_subviews(target_rect, &mut [], cache);
157            });
158        }
159    }
160}