use std::time::Duration;
use super::{CellAnimation, ColorMode, Playback};
use crate::color::Rgb;
use crate::theme::{Paint, Theme};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CellFrame {
pub index: usize,
pub color: Option<Rgb>,
pub finished: bool,
pub next: Option<Duration>,
pub smooth: bool,
}
struct Position {
index: usize,
following: usize,
progress: f32,
remaining: Duration,
finished: bool,
}
impl CellAnimation {
#[must_use]
pub fn sample(&self, theme: &Theme, fg: Option<Rgb>, now: Duration, since: Option<Duration>) -> CellFrame {
if self.frames.is_empty() {
return CellFrame { index: 0, color: fg, finished: true, next: None, smooth: false };
}
let Some(since) = since else {
let index = self.rest_index();
let color = self.paint(index, theme, fg).map(|paint| match paint {
Paint::Pulse(_, strong) => strong,
Paint::Solid(color) => color,
});
return CellFrame { index, color, finished: true, next: None, smooth: false };
};
let position = self.position(theme, now.saturating_sub(since));
let phase = pulse_phase(theme, now);
let here = self.paint(position.index, theme, fg);
let mut smooth = here.is_some_and(Paint::is_animated);
let mut color = here.map(|paint| paint.at(phase));
if self.colors == ColorMode::Blend && position.following != position.index {
let there = self.paint(position.following, theme, fg);
smooth |= there.is_some_and(Paint::is_animated);
if let (Some(from), Some(to)) = (color, there.map(|paint| paint.at(phase))) {
smooth |= from != to;
color = Some(from.mix(to, position.progress));
}
}
let next = (!position.finished).then_some(position.remaining);
CellFrame { index: position.index, color, finished: position.finished, next, smooth }
}
fn paint(&self, index: usize, theme: &Theme, fg: Option<Rgb>) -> Option<Paint> {
let own = self.frames.get(index).and_then(|frame| frame.color.as_ref());
match own {
Some(color) => {
let widget = fg.or_else(|| theme.color("text")).unwrap_or(Rgb::new(0, 0, 0));
color.resolve(theme, widget).ok().or(fg.map(Paint::Solid))
}
None => fg.map(Paint::Solid),
}
}
fn frame_millis(&self, index: usize, theme: &Theme) -> u128 {
let time = self.frames.get(index).and_then(|frame| frame.duration).unwrap_or(self.frame_time);
time.resolve(&theme.motion()).as_millis().max(1)
}
fn sequence(&self) -> impl Iterator<Item = usize> + Clone + '_ {
let count = self.frames.len();
let back = if self.playback == Playback::Bounce { count.saturating_sub(1) } else { 0 };
(0..count).chain((1..back).rev())
}
fn position(&self, theme: &Theme, elapsed: Duration) -> Position {
let sequence: Vec<(usize, u128)> =
self.sequence().map(|index| (index, self.frame_millis(index, theme))).collect();
let total: u128 = sequence.iter().map(|(_, millis)| millis).sum();
let elapsed = elapsed.as_millis();
let last = self.frames.len() - 1;
if self.playback == Playback::Once && elapsed >= total {
return Position { index: last, following: last, progress: 0.0, remaining: Duration::ZERO, finished: true };
}
let mut into = elapsed % total.max(1);
for (step, (index, millis)) in sequence.iter().enumerate() {
if into < *millis {
let following = match sequence.get(step + 1) {
Some((next, _)) => *next,
None if self.playback == Playback::Once => *index,
None => sequence[0].0,
};
let remaining = Duration::from_millis(u64::try_from(millis - into).unwrap_or(u64::MAX));
let progress = into as f32 / *millis as f32;
return Position { index: *index, following, progress, remaining, finished: false };
}
into -= millis;
}
Position { index: last, following: last, progress: 0.0, remaining: Duration::ZERO, finished: true }
}
}
fn pulse_phase(theme: &Theme, now: Duration) -> f32 {
let period = theme.motion().pulse_period.as_millis().max(1);
(now.as_millis() % period) as f32 / period as f32
}