argui-animation 0.1.0

Animation timing and interpolation for Argui interfaces
Documentation
use crate::Duration;

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct CueId(usize);

impl CueId {
    #[must_use]
    pub const fn from_index(index: usize) -> Self {
        Self(index)
    }

    #[must_use]
    pub const fn index(self) -> usize {
        self.0
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Cue {
    pub start: Duration,
    pub duration: Duration,
}

impl Cue {
    #[must_use]
    pub fn progress(self, elapsed: Duration) -> f32 {
        if self.duration == Duration::ZERO {
            return (elapsed >= self.start) as u8 as f32;
        }
        let local = elapsed.as_nanos().saturating_sub(self.start.as_nanos());
        (local as f32 / self.duration.as_nanos() as f32).clamp(0.0, 1.0)
    }
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Schedule {
    cues: Box<[Cue]>,
    duration: Duration,
}

impl Schedule {
    #[must_use]
    pub fn sequence(durations: impl IntoIterator<Item = Duration>) -> Self {
        let mut builder = ScheduleBuilder::new();
        for duration in durations {
            builder.then(duration);
        }
        builder.build()
    }

    #[must_use]
    pub fn parallel(durations: impl IntoIterator<Item = Duration>) -> Self {
        let cues: Vec<_> = durations
            .into_iter()
            .map(|duration| Cue {
                start: Duration::ZERO,
                duration,
            })
            .collect();
        Self::from_cues(cues)
    }

    #[must_use]
    pub fn stagger(count: usize, duration: Duration, interval: Duration) -> Self {
        let cues = (0..count)
            .map(|index| Cue {
                start: Duration::from_nanos(interval.as_nanos().saturating_mul(index as u64)),
                duration,
            })
            .collect();
        Self::from_cues(cues)
    }

    fn from_cues(cues: Vec<Cue>) -> Self {
        let duration = cues
            .iter()
            .map(|cue| cue.start + cue.duration)
            .max()
            .unwrap_or(Duration::ZERO);
        Self {
            cues: cues.into_boxed_slice(),
            duration,
        }
    }

    #[must_use]
    pub fn cue(&self, id: CueId) -> Option<Cue> {
        self.cues.get(id.0).copied()
    }

    #[must_use]
    pub const fn duration(&self) -> Duration {
        self.duration
    }

    #[must_use]
    pub fn len(&self) -> usize {
        self.cues.len()
    }

    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.cues.is_empty()
    }
}

#[derive(Clone, Debug, Default)]
pub struct ScheduleBuilder {
    cues: Vec<Cue>,
    cursor: Duration,
    group_start: Duration,
}

impl ScheduleBuilder {
    #[must_use]
    pub const fn new() -> Self {
        Self {
            cues: Vec::new(),
            cursor: Duration::ZERO,
            group_start: Duration::ZERO,
        }
    }

    pub fn then(&mut self, duration: Duration) -> CueId {
        self.group_start = self.cursor;
        let id = self.push(self.cursor, duration);
        self.cursor += duration;
        id
    }

    pub fn with(&mut self, duration: Duration) -> CueId {
        let id = self.push(self.group_start, duration);
        self.cursor = self.cursor.max(self.group_start + duration);
        id
    }

    pub fn after(&mut self, dependency: CueId, delay: Duration, duration: Duration) -> CueId {
        let start = self
            .cues
            .get(dependency.0)
            .map_or(delay, |cue| cue.start + cue.duration + delay);
        let id = self.push(start, duration);
        self.cursor = self.cursor.max(start + duration);
        id
    }

    fn push(&mut self, start: Duration, duration: Duration) -> CueId {
        let id = CueId(self.cues.len());
        self.cues.push(Cue { start, duration });
        id
    }

    #[must_use]
    pub fn build(self) -> Schedule {
        Schedule::from_cues(self.cues)
    }
}