Skip to main content

lgui_core/core/
animation.rs

1use std::collections::{HashMap, HashSet};
2
3use super::UiId;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub enum AnimProperty {
7    Hover,
8    Active,
9    Pressed,
10    Focus,
11    Opacity,
12    TranslateX,
13    TranslateY,
14    Custom(&'static str),
15}
16
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct AnimationBinding {
19    pub property: AnimProperty,
20    pub idle: f32,
21    pub active: f32,
22    pub timing: AnimationTiming,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub enum AnimationTiming {
27    Exponential { speed: f32 },
28    Duration { fade_in_ms: f32, fade_out_ms: f32 },
29}
30
31impl AnimationBinding {
32    pub const fn new(property: AnimProperty, idle: f32, active: f32) -> Self {
33        Self {
34            property,
35            idle,
36            active,
37            timing: AnimationTiming::Exponential { speed: 0.018 },
38        }
39    }
40
41    pub const fn speed(mut self, speed: f32) -> Self {
42        self.timing = AnimationTiming::Exponential { speed };
43        self
44    }
45
46    pub const fn duration(mut self, fade_in_ms: f32, fade_out_ms: f32) -> Self {
47        self.timing = AnimationTiming::Duration {
48            fade_in_ms,
49            fade_out_ms,
50        };
51        self
52    }
53}
54
55#[derive(Clone, Copy, Debug)]
56pub struct AnimatedValue {
57    pub value: f32,
58    pub target: f32,
59    pub timing: AnimationTiming,
60}
61
62impl AnimatedValue {
63    pub fn new(value: f32) -> Self {
64        Self {
65            value,
66            target: value,
67            timing: AnimationTiming::Exponential { speed: 0.018 },
68        }
69    }
70
71    pub fn with_timing(value: f32, timing: AnimationTiming) -> Self {
72        Self {
73            value,
74            target: value,
75            timing,
76        }
77    }
78
79    pub fn set_target(&mut self, target: f32) -> bool {
80        if (self.target - target).abs() <= f32::EPSILON {
81            return false;
82        }
83        self.target = target;
84        true
85    }
86
87    pub fn eased(self) -> f32 {
88        smootherstep(self.value)
89    }
90
91    pub fn is_running(self) -> bool {
92        (self.value - self.target).abs() >= 0.001
93    }
94
95    pub fn advance(&mut self, elapsed_ms: f32) -> bool {
96        let previous = self.value;
97        match self.timing {
98            AnimationTiming::Exponential { speed } => {
99                let t = (elapsed_ms * speed).clamp(0.0, 1.0);
100                self.value += (self.target - self.value) * t;
101            }
102            AnimationTiming::Duration {
103                fade_in_ms,
104                fade_out_ms,
105            } => {
106                let duration = if self.target > self.value {
107                    fade_in_ms
108                } else {
109                    fade_out_ms
110                };
111                let step = (elapsed_ms / duration.max(1.0)).clamp(0.0, 1.0);
112                self.value = move_towards(self.value, self.target, step);
113            }
114        }
115        if (self.value - self.target).abs() < 0.001 {
116            self.value = self.target;
117        }
118        (self.value - previous).abs() > f32::EPSILON
119    }
120}
121
122fn smootherstep(value: f32) -> f32 {
123    let value = value.clamp(0.0, 1.0);
124    value * value * value * (value * (value * 6.0 - 15.0) + 10.0)
125}
126
127#[derive(Default)]
128pub struct AnimationRegistry {
129    values: HashMap<(UiId, AnimProperty), AnimatedValue>,
130    dirty_ids: HashSet<UiId>,
131}
132
133impl AnimationRegistry {
134    pub fn value(&self, id: UiId, property: AnimProperty) -> f32 {
135        self.values
136            .get(&(id, property))
137            .map(|value| value.value)
138            .unwrap_or(0.0)
139    }
140
141    pub fn set_binding_target(
142        &mut self,
143        id: UiId,
144        binding: AnimationBinding,
145        active: bool,
146    ) -> bool {
147        let target = if active { binding.active } else { binding.idle };
148        let value = self
149            .values
150            .entry((id.clone(), binding.property))
151            .or_insert_with(|| AnimatedValue::with_timing(binding.idle, binding.timing));
152        value.timing = binding.timing;
153        if value.set_target(target) {
154            self.dirty_ids.insert(id);
155            return true;
156        }
157        false
158    }
159
160    pub fn sync_binding_target(
161        &mut self,
162        id: UiId,
163        binding: AnimationBinding,
164        active: bool,
165    ) -> bool {
166        let target = if active { binding.active } else { binding.idle };
167        let key = (id.clone(), binding.property);
168        let initialized = !self.values.contains_key(&key);
169        let value = self
170            .values
171            .entry(key)
172            .or_insert_with(|| AnimatedValue::with_timing(target, binding.timing));
173        value.timing = binding.timing;
174        if initialized || value.set_target(target) {
175            self.dirty_ids.insert(id);
176            return true;
177        }
178        false
179    }
180
181    pub fn set_target(&mut self, id: UiId, property: AnimProperty, target: f32) -> bool {
182        let value = self
183            .values
184            .entry((id.clone(), property))
185            .or_insert_with(|| AnimatedValue::new(target));
186        if value.set_target(target) {
187            self.dirty_ids.insert(id);
188            return true;
189        }
190        false
191    }
192
193    pub fn clear_targets(&mut self, properties: &[AnimProperty]) -> bool {
194        let mut changed = false;
195        for ((id, property), value) in &mut self.values {
196            if !properties.contains(property) {
197                continue;
198            }
199            if value.value.abs() > f32::EPSILON || value.target.abs() > f32::EPSILON {
200                value.value = 0.0;
201                value.target = 0.0;
202                self.dirty_ids.insert(id.clone());
203                changed = true;
204            }
205        }
206        changed
207    }
208
209    pub fn clear_absent_values(
210        &mut self,
211        present_ids: &HashSet<UiId>,
212        properties: &[AnimProperty],
213    ) -> bool {
214        let mut removed_ids = Vec::new();
215        self.values.retain(|(id, property), _| {
216            let remove = properties.contains(property) && !present_ids.contains(id);
217            if remove {
218                removed_ids.push(id.clone());
219            }
220            !remove
221        });
222        if removed_ids.is_empty() {
223            return false;
224        }
225        self.dirty_ids.extend(removed_ids);
226        true
227    }
228
229    pub(crate) fn clear_absent_values_by(
230        &mut self,
231        mut is_present: impl FnMut(&UiId) -> bool,
232        properties: &[AnimProperty],
233    ) -> bool {
234        let mut removed_ids = Vec::new();
235        self.values.retain(|(id, property), _| {
236            let remove = properties.contains(property) && !is_present(id);
237            if remove {
238                removed_ids.push(id.clone());
239            }
240            !remove
241        });
242        if removed_ids.is_empty() {
243            return false;
244        }
245        self.dirty_ids.extend(removed_ids);
246        true
247    }
248
249    pub fn advance(&mut self, elapsed_ms: f32) -> bool {
250        let mut changed = false;
251        for ((id, _), value) in &mut self.values {
252            if value.advance(elapsed_ms) {
253                self.dirty_ids.insert(id.clone());
254                changed = true;
255            }
256        }
257        changed
258    }
259
260    pub(crate) fn is_running(&self) -> bool {
261        self.values.values().any(|value| value.is_running())
262    }
263
264    pub fn take_dirty_ids(&mut self) -> Vec<UiId> {
265        self.dirty_ids.drain().collect()
266    }
267
268    pub fn snapshot(&self, id: UiId) -> AnimationSnapshot<'_> {
269        AnimationSnapshot { registry: self, id }
270    }
271}
272
273fn move_towards(current: f32, target: f32, step: f32) -> f32 {
274    if current < target {
275        (current + step).min(target)
276    } else {
277        (current - step).max(target)
278    }
279}
280
281#[derive(Clone)]
282pub struct AnimationSnapshot<'a> {
283    registry: &'a AnimationRegistry,
284    id: UiId,
285}
286
287impl AnimationSnapshot<'_> {
288    pub fn get(self, property: AnimProperty) -> f32 {
289        self.registry.value(self.id.clone(), property)
290    }
291}