argui-animation 0.2.0

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

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Iterations {
    Finite(f64),
    Infinite,
}

impl Default for Iterations {
    fn default() -> Self {
        Self::Finite(1.0)
    }
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum Direction {
    #[default]
    Normal,
    Reverse,
    Alternate,
    AlternateReverse,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum FillMode {
    #[default]
    None,
    Forwards,
    Backwards,
    Both,
}

impl FillMode {
    pub(crate) const fn fills_before(self) -> bool {
        matches!(self, Self::Backwards | Self::Both)
    }

    pub(crate) const fn fills_after(self) -> bool {
        matches!(self, Self::Forwards | Self::Both)
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Timing {
    pub duration: Duration,
    pub delay: Duration,
    pub end_delay: Duration,
    pub iterations: Iterations,
    pub direction: Direction,
    pub fill: FillMode,
    pub playback_rate: f64,
}

impl Timing {
    #[must_use]
    pub const fn new(duration: Duration) -> Self {
        Self {
            duration,
            delay: Duration::ZERO,
            end_delay: Duration::ZERO,
            iterations: Iterations::Finite(1.0),
            direction: Direction::Normal,
            fill: FillMode::None,
            playback_rate: 1.0,
        }
    }

    #[must_use]
    pub const fn delay(mut self, delay: Duration) -> Self {
        self.delay = delay;
        self
    }

    #[must_use]
    pub const fn end_delay(mut self, end_delay: Duration) -> Self {
        self.end_delay = end_delay;
        self
    }

    #[must_use]
    pub const fn iterations(mut self, iterations: Iterations) -> Self {
        self.iterations = iterations;
        self
    }

    #[must_use]
    pub const fn direction(mut self, direction: Direction) -> Self {
        self.direction = direction;
        self
    }

    #[must_use]
    pub const fn fill(mut self, fill: FillMode) -> Self {
        self.fill = fill;
        self
    }

    #[must_use]
    pub const fn playback_rate(mut self, playback_rate: f64) -> Self {
        self.playback_rate = playback_rate;
        self
    }

    pub(crate) fn validate(self) -> Result<Self, TimingError> {
        let iterations_valid = match self.iterations {
            Iterations::Finite(value) => value.is_finite() && value > 0.0,
            Iterations::Infinite => true,
        };
        if self.duration == Duration::ZERO {
            Err(TimingError::ZeroDuration)
        } else if !iterations_valid {
            Err(TimingError::InvalidIterations)
        } else if !self.playback_rate.is_finite() || self.playback_rate == 0.0 {
            Err(TimingError::InvalidPlaybackRate)
        } else {
            Ok(self)
        }
    }

    pub(crate) fn active_seconds(self) -> f64 {
        match self.iterations {
            Iterations::Finite(iterations) => self.duration.as_secs_f64() * iterations,
            Iterations::Infinite => f64::INFINITY,
        }
    }

    pub(crate) fn total_seconds(self) -> f64 {
        self.delay.as_secs_f64() + self.active_seconds() + self.end_delay.as_secs_f64()
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TimingError {
    ZeroDuration,
    InvalidIterations,
    InvalidPlaybackRate,
    InvalidKeyframes,
}

impl fmt::Display for TimingError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::ZeroDuration => {
                formatter.write_str("animation duration must be greater than zero")
            }
            Self::InvalidIterations => {
                formatter.write_str("animation iterations must be positive and finite, or infinite")
            }
            Self::InvalidPlaybackRate => {
                formatter.write_str("animation playback rate must be finite and non-zero")
            }
            Self::InvalidKeyframes => {
                formatter.write_str("keyframes must be finite, sorted, and cover offsets 0..=1")
            }
        }
    }
}

impl std::error::Error for TimingError {}