uzor_interactive/
animated_list.rs1#[derive(Debug, Clone, Copy)]
8pub struct ItemState {
9 pub opacity: f32,
11
12 pub y_offset: f32,
14
15 pub scale: f32,
17}
18
19impl Default for ItemState {
20 fn default() -> Self {
21 Self {
22 opacity: 0.0,
23 y_offset: 0.0,
24 scale: 0.7,
25 }
26 }
27}
28
29impl ItemState {
30 pub fn entry_start() -> Self {
32 Self {
33 opacity: 0.0,
34 y_offset: 20.0,
35 scale: 0.7,
36 }
37 }
38
39 pub fn visible() -> Self {
41 Self {
42 opacity: 1.0,
43 y_offset: 0.0,
44 scale: 1.0,
45 }
46 }
47
48 pub fn exit_end() -> Self {
50 Self {
51 opacity: 0.0,
52 y_offset: 20.0,
53 scale: 0.7,
54 }
55 }
56
57 pub fn lerp(from: Self, to: Self, t: f32) -> Self {
59 let t = t.clamp(0.0, 1.0);
60 Self {
61 opacity: from.opacity + (to.opacity - from.opacity) * t,
62 y_offset: from.y_offset + (to.y_offset - from.y_offset) * t,
63 scale: from.scale + (to.scale - from.scale) * t,
64 }
65 }
66}
67
68#[derive(Debug, Clone)]
72pub struct AnimatedList {
73 item_count: usize,
75
76 states: Vec<ItemAnimationState>,
78
79 pub stagger_delay: f32,
81
82 pub animation_duration: f32,
84}
85
86#[derive(Debug, Clone)]
87struct ItemAnimationState {
88 current: ItemState,
90
91 progress: f32,
93
94 animation: AnimationType,
96
97 start_time: f64,
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102enum AnimationType {
103 None,
104 Entry,
105 Exit,
106}
107
108impl Default for AnimatedList {
109 fn default() -> Self {
110 Self::new(0)
111 }
112}
113
114impl AnimatedList {
115 pub fn new(item_count: usize) -> Self {
117 Self {
118 item_count,
119 states: vec![
120 ItemAnimationState {
121 current: ItemState::entry_start(),
122 progress: 0.0,
123 animation: AnimationType::Entry,
124 start_time: 0.0,
125 };
126 item_count
127 ],
128 stagger_delay: 0.05,
129 animation_duration: 0.2,
130 }
131 }
132
133 pub fn with_stagger_delay(mut self, delay: f32) -> Self {
135 self.stagger_delay = delay;
136 self
137 }
138
139 pub fn with_duration(mut self, duration: f32) -> Self {
141 self.animation_duration = duration;
142 self
143 }
144
145 pub fn set_item_count(&mut self, new_count: usize, current_time: f64) {
147 if new_count > self.item_count {
148 for i in self.item_count..new_count {
150 self.states.push(ItemAnimationState {
151 current: ItemState::entry_start(),
152 progress: 0.0,
153 animation: AnimationType::Entry,
154 start_time: current_time + (i - self.item_count) as f64 * self.stagger_delay as f64,
155 });
156 }
157 } else if new_count < self.item_count {
158 for state in self.states.iter_mut().skip(new_count) {
160 if state.animation != AnimationType::Exit {
161 state.animation = AnimationType::Exit;
162 state.progress = 0.0;
163 state.start_time = current_time;
164 }
165 }
166 }
167
168 self.item_count = new_count;
169 }
170
171 pub fn update(&mut self, current_time: f64) {
173 for (index, state) in self.states.iter_mut().enumerate() {
174 if state.animation == AnimationType::None {
175 continue;
176 }
177
178 let elapsed = (current_time - state.start_time) as f32;
179 let stagger_offset = index as f32 * self.stagger_delay;
180
181 let effective_elapsed = (elapsed - stagger_offset).max(0.0);
183 state.progress = (effective_elapsed / self.animation_duration).clamp(0.0, 1.0);
184
185 let eased_progress = 1.0 - (1.0 - state.progress).powi(3);
187
188 match state.animation {
190 AnimationType::Entry => {
191 state.current = ItemState::lerp(
192 ItemState::entry_start(),
193 ItemState::visible(),
194 eased_progress,
195 );
196
197 if state.progress >= 1.0 {
198 state.animation = AnimationType::None;
199 state.current = ItemState::visible();
200 }
201 }
202 AnimationType::Exit => {
203 state.current = ItemState::lerp(
204 ItemState::visible(),
205 ItemState::exit_end(),
206 eased_progress,
207 );
208 }
209 AnimationType::None => {}
210 }
211 }
212
213 self.states.retain(|state| {
215 state.animation != AnimationType::Exit || state.progress < 1.0
216 });
217 }
218
219 pub fn get_item_state(&self, index: usize) -> Option<ItemState> {
221 self.states.get(index).map(|s| s.current)
222 }
223
224 pub fn item_states(&self) -> impl Iterator<Item = (usize, ItemState)> + '_ {
226 self.states
227 .iter()
228 .enumerate()
229 .filter(|(i, _)| *i < self.item_count)
230 .map(|(i, s)| (i, s.current))
231 }
232
233 pub fn is_animating(&self) -> bool {
235 self.states.iter().any(|s| s.animation != AnimationType::None)
236 }
237
238 pub fn animate_in(&mut self, current_time: f64) {
240 for (index, state) in self.states.iter_mut().enumerate() {
241 state.animation = AnimationType::Entry;
242 state.progress = 0.0;
243 state.start_time = current_time + index as f64 * self.stagger_delay as f64;
244 state.current = ItemState::entry_start();
245 }
246 }
247
248 pub fn animate_out(&mut self, current_time: f64) {
250 for (index, state) in self.states.iter_mut().enumerate() {
251 state.animation = AnimationType::Exit;
252 state.progress = 0.0;
253 state.start_time = current_time + index as f64 * self.stagger_delay as f64;
254 }
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_item_state_lerp() {
264 let start = ItemState::entry_start();
265 let end = ItemState::visible();
266
267 let mid = ItemState::lerp(start, end, 0.5);
268 assert!((mid.opacity - 0.5).abs() < 0.01);
269 assert!(mid.y_offset > 0.0 && mid.y_offset < 20.0);
270 assert!(mid.scale > 0.7 && mid.scale < 1.0);
271 }
272
273 #[test]
274 fn test_animated_list_creation() {
275 let list = AnimatedList::new(5);
276 assert_eq!(list.item_count, 5);
277 assert_eq!(list.states.len(), 5);
278 }
279
280 #[test]
281 fn test_entry_animation() {
282 let mut list = AnimatedList::new(3);
283
284 list.update(0.0);
286 for i in 0..3 {
287 let state = list.get_item_state(i).unwrap();
288 assert!(state.opacity < 0.1); }
290
291 list.update(0.1);
293 let state0 = list.get_item_state(0).unwrap();
294 assert!(state0.opacity > 0.0);
295
296 list.update(1.0);
298 for i in 0..3 {
299 let state = list.get_item_state(i).unwrap();
300 assert!((state.opacity - 1.0).abs() < 0.1);
301 assert!(state.y_offset.abs() < 1.0);
302 }
303 }
304
305 #[test]
306 fn test_stagger_delay() {
307 let mut list = AnimatedList::new(3).with_stagger_delay(0.1);
308
309 list.update(0.0);
310
311 list.update(0.05);
313 let state1 = list.get_item_state(1).unwrap();
314 assert!(state1.opacity < 0.1);
315
316 list.update(0.15);
318 let state1 = list.get_item_state(1).unwrap();
319 assert!(state1.opacity > 0.1);
320 }
321
322 #[test]
323 fn test_add_items() {
324 let mut list = AnimatedList::new(2);
325 list.update(1.0); list.set_item_count(3, 1.0);
329 assert_eq!(list.states.len(), 3);
330
331 list.update(1.0);
333 let state2 = list.get_item_state(2).unwrap();
334 assert!(state2.opacity < 1.0);
335 }
336
337 #[test]
338 fn test_remove_items() {
339 let mut list = AnimatedList::new(3);
340 list.update(1.0); list.set_item_count(2, 1.0);
344
345 list.update(1.0);
347 assert!(list.is_animating());
348
349 list.update(2.0);
351 assert_eq!(list.states.len(), 2);
352 }
353
354 #[test]
355 fn test_animate_in_out() {
356 let mut list = AnimatedList::new(2);
357
358 list.animate_in(0.0);
360 assert!(list.is_animating());
361
362 list.update(1.0);
363 assert!(!list.is_animating());
364
365 list.animate_out(1.0);
367 assert!(list.is_animating());
368
369 list.update(1.15);
371 if let Some(state0) = list.get_item_state(0) {
372 assert!(state0.opacity < 1.0); }
374
375 list.update(2.0);
377 assert_eq!(list.states.len(), 0);
378 }
379}