#[derive(Debug, Clone, Copy)]
pub struct ItemState {
pub opacity: f32,
pub y_offset: f32,
pub scale: f32,
}
impl Default for ItemState {
fn default() -> Self {
Self {
opacity: 0.0,
y_offset: 0.0,
scale: 0.7,
}
}
}
impl ItemState {
pub fn entry_start() -> Self {
Self {
opacity: 0.0,
y_offset: 20.0,
scale: 0.7,
}
}
pub fn visible() -> Self {
Self {
opacity: 1.0,
y_offset: 0.0,
scale: 1.0,
}
}
pub fn exit_end() -> Self {
Self {
opacity: 0.0,
y_offset: 20.0,
scale: 0.7,
}
}
pub fn lerp(from: Self, to: Self, t: f32) -> Self {
let t = t.clamp(0.0, 1.0);
Self {
opacity: from.opacity + (to.opacity - from.opacity) * t,
y_offset: from.y_offset + (to.y_offset - from.y_offset) * t,
scale: from.scale + (to.scale - from.scale) * t,
}
}
}
#[derive(Debug, Clone)]
pub struct AnimatedList {
item_count: usize,
states: Vec<ItemAnimationState>,
pub stagger_delay: f32,
pub animation_duration: f32,
}
#[derive(Debug, Clone)]
struct ItemAnimationState {
current: ItemState,
progress: f32,
animation: AnimationType,
start_time: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AnimationType {
None,
Entry,
Exit,
}
impl Default for AnimatedList {
fn default() -> Self {
Self::new(0)
}
}
impl AnimatedList {
pub fn new(item_count: usize) -> Self {
Self {
item_count,
states: vec![
ItemAnimationState {
current: ItemState::entry_start(),
progress: 0.0,
animation: AnimationType::Entry,
start_time: 0.0,
};
item_count
],
stagger_delay: 0.05,
animation_duration: 0.2,
}
}
pub fn with_stagger_delay(mut self, delay: f32) -> Self {
self.stagger_delay = delay;
self
}
pub fn with_duration(mut self, duration: f32) -> Self {
self.animation_duration = duration;
self
}
pub fn set_item_count(&mut self, new_count: usize, current_time: f64) {
if new_count > self.item_count {
for i in self.item_count..new_count {
self.states.push(ItemAnimationState {
current: ItemState::entry_start(),
progress: 0.0,
animation: AnimationType::Entry,
start_time: current_time + (i - self.item_count) as f64 * self.stagger_delay as f64,
});
}
} else if new_count < self.item_count {
for state in self.states.iter_mut().skip(new_count) {
if state.animation != AnimationType::Exit {
state.animation = AnimationType::Exit;
state.progress = 0.0;
state.start_time = current_time;
}
}
}
self.item_count = new_count;
}
pub fn update(&mut self, current_time: f64) {
for (index, state) in self.states.iter_mut().enumerate() {
if state.animation == AnimationType::None {
continue;
}
let elapsed = (current_time - state.start_time) as f32;
let stagger_offset = index as f32 * self.stagger_delay;
let effective_elapsed = (elapsed - stagger_offset).max(0.0);
state.progress = (effective_elapsed / self.animation_duration).clamp(0.0, 1.0);
let eased_progress = 1.0 - (1.0 - state.progress).powi(3);
match state.animation {
AnimationType::Entry => {
state.current = ItemState::lerp(
ItemState::entry_start(),
ItemState::visible(),
eased_progress,
);
if state.progress >= 1.0 {
state.animation = AnimationType::None;
state.current = ItemState::visible();
}
}
AnimationType::Exit => {
state.current = ItemState::lerp(
ItemState::visible(),
ItemState::exit_end(),
eased_progress,
);
}
AnimationType::None => {}
}
}
self.states.retain(|state| {
state.animation != AnimationType::Exit || state.progress < 1.0
});
}
pub fn get_item_state(&self, index: usize) -> Option<ItemState> {
self.states.get(index).map(|s| s.current)
}
pub fn item_states(&self) -> impl Iterator<Item = (usize, ItemState)> + '_ {
self.states
.iter()
.enumerate()
.filter(|(i, _)| *i < self.item_count)
.map(|(i, s)| (i, s.current))
}
pub fn is_animating(&self) -> bool {
self.states.iter().any(|s| s.animation != AnimationType::None)
}
pub fn animate_in(&mut self, current_time: f64) {
for (index, state) in self.states.iter_mut().enumerate() {
state.animation = AnimationType::Entry;
state.progress = 0.0;
state.start_time = current_time + index as f64 * self.stagger_delay as f64;
state.current = ItemState::entry_start();
}
}
pub fn animate_out(&mut self, current_time: f64) {
for (index, state) in self.states.iter_mut().enumerate() {
state.animation = AnimationType::Exit;
state.progress = 0.0;
state.start_time = current_time + index as f64 * self.stagger_delay as f64;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_item_state_lerp() {
let start = ItemState::entry_start();
let end = ItemState::visible();
let mid = ItemState::lerp(start, end, 0.5);
assert!((mid.opacity - 0.5).abs() < 0.01);
assert!(mid.y_offset > 0.0 && mid.y_offset < 20.0);
assert!(mid.scale > 0.7 && mid.scale < 1.0);
}
#[test]
fn test_animated_list_creation() {
let list = AnimatedList::new(5);
assert_eq!(list.item_count, 5);
assert_eq!(list.states.len(), 5);
}
#[test]
fn test_entry_animation() {
let mut list = AnimatedList::new(3);
list.update(0.0);
for i in 0..3 {
let state = list.get_item_state(i).unwrap();
assert!(state.opacity < 0.1); }
list.update(0.1);
let state0 = list.get_item_state(0).unwrap();
assert!(state0.opacity > 0.0);
list.update(1.0);
for i in 0..3 {
let state = list.get_item_state(i).unwrap();
assert!((state.opacity - 1.0).abs() < 0.1);
assert!(state.y_offset.abs() < 1.0);
}
}
#[test]
fn test_stagger_delay() {
let mut list = AnimatedList::new(3).with_stagger_delay(0.1);
list.update(0.0);
list.update(0.05);
let state1 = list.get_item_state(1).unwrap();
assert!(state1.opacity < 0.1);
list.update(0.15);
let state1 = list.get_item_state(1).unwrap();
assert!(state1.opacity > 0.1);
}
#[test]
fn test_add_items() {
let mut list = AnimatedList::new(2);
list.update(1.0);
list.set_item_count(3, 1.0);
assert_eq!(list.states.len(), 3);
list.update(1.0);
let state2 = list.get_item_state(2).unwrap();
assert!(state2.opacity < 1.0);
}
#[test]
fn test_remove_items() {
let mut list = AnimatedList::new(3);
list.update(1.0);
list.set_item_count(2, 1.0);
list.update(1.0);
assert!(list.is_animating());
list.update(2.0);
assert_eq!(list.states.len(), 2);
}
#[test]
fn test_animate_in_out() {
let mut list = AnimatedList::new(2);
list.animate_in(0.0);
assert!(list.is_animating());
list.update(1.0);
assert!(!list.is_animating());
list.animate_out(1.0);
assert!(list.is_animating());
list.update(1.15);
if let Some(state0) = list.get_item_state(0) {
assert!(state0.opacity < 1.0); }
list.update(2.0);
assert_eq!(list.states.len(), 0);
}
}