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}