1use std::time::Duration;
4
5use super::{CellAnimation, ColorMode, Playback};
6use crate::color::Rgb;
7use crate::theme::{Paint, Theme};
8
9#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct CellFrame {
12 pub index: usize,
14 pub color: Option<Rgb>,
16 pub finished: bool,
18 pub next: Option<Duration>,
20 pub smooth: bool,
23}
24
25struct Position {
27 index: usize,
28 following: usize,
29 progress: f32,
31 remaining: Duration,
32 finished: bool,
33}
34
35impl CellAnimation {
36 #[must_use]
45 pub fn sample(&self, theme: &Theme, fg: Option<Rgb>, now: Duration, since: Option<Duration>) -> CellFrame {
46 if self.frames.is_empty() {
47 return CellFrame { index: 0, color: fg, finished: true, next: None, smooth: false };
48 }
49 let Some(since) = since else {
50 let index = self.rest_index();
51 let color = self.paint(index, theme, fg).map(|paint| match paint {
52 Paint::Pulse(_, strong) => strong,
54 Paint::Solid(color) => color,
55 });
56 return CellFrame { index, color, finished: true, next: None, smooth: false };
57 };
58 let position = self.position(theme, now.saturating_sub(since));
59 let phase = pulse_phase(theme, now);
60 let here = self.paint(position.index, theme, fg);
61 let mut smooth = here.is_some_and(Paint::is_animated);
62 let mut color = here.map(|paint| paint.at(phase));
63 if self.colors == ColorMode::Blend && position.following != position.index {
64 let there = self.paint(position.following, theme, fg);
65 smooth |= there.is_some_and(Paint::is_animated);
66 if let (Some(from), Some(to)) = (color, there.map(|paint| paint.at(phase))) {
67 smooth |= from != to;
68 color = Some(from.mix(to, position.progress));
69 }
70 }
71 let next = (!position.finished).then_some(position.remaining);
72 CellFrame { index: position.index, color, finished: position.finished, next, smooth }
73 }
74
75 fn paint(&self, index: usize, theme: &Theme, fg: Option<Rgb>) -> Option<Paint> {
77 let own = self.frames.get(index).and_then(|frame| frame.color.as_ref());
78 match own {
79 Some(color) => {
80 let widget = fg.or_else(|| theme.color("text")).unwrap_or(Rgb::new(0, 0, 0));
81 color.resolve(theme, widget).ok().or(fg.map(Paint::Solid))
82 }
83 None => fg.map(Paint::Solid),
84 }
85 }
86
87 fn frame_millis(&self, index: usize, theme: &Theme) -> u128 {
90 let time = self.frames.get(index).and_then(|frame| frame.duration).unwrap_or(self.frame_time);
91 time.resolve(&theme.motion()).as_millis().max(1)
92 }
93
94 fn sequence(&self) -> impl Iterator<Item = usize> + Clone + '_ {
96 let count = self.frames.len();
97 let back = if self.playback == Playback::Bounce { count.saturating_sub(1) } else { 0 };
98 (0..count).chain((1..back).rev())
99 }
100
101 fn position(&self, theme: &Theme, elapsed: Duration) -> Position {
102 let sequence: Vec<(usize, u128)> =
103 self.sequence().map(|index| (index, self.frame_millis(index, theme))).collect();
104 let total: u128 = sequence.iter().map(|(_, millis)| millis).sum();
105 let elapsed = elapsed.as_millis();
106 let last = self.frames.len() - 1;
107 if self.playback == Playback::Once && elapsed >= total {
108 return Position { index: last, following: last, progress: 0.0, remaining: Duration::ZERO, finished: true };
109 }
110 let mut into = elapsed % total.max(1);
111 for (step, (index, millis)) in sequence.iter().enumerate() {
112 if into < *millis {
113 let following = match sequence.get(step + 1) {
114 Some((next, _)) => *next,
115 None if self.playback == Playback::Once => *index,
116 None => sequence[0].0,
117 };
118 let remaining = Duration::from_millis(u64::try_from(millis - into).unwrap_or(u64::MAX));
119 let progress = into as f32 / *millis as f32;
120 return Position { index: *index, following, progress, remaining, finished: false };
121 }
122 into -= millis;
123 }
124 Position { index: last, following: last, progress: 0.0, remaining: Duration::ZERO, finished: true }
126 }
127}
128
129fn pulse_phase(theme: &Theme, now: Duration) -> f32 {
132 let period = theme.motion().pulse_period.as_millis().max(1);
133 (now.as_millis() % period) as f32 / period as f32
134}