argui-animation 0.2.0

Animation timing and interpolation for Argui interfaces
Documentation
use crate::{Easing, Interpolate, TimingError};

#[derive(Clone, Debug, PartialEq)]
pub struct Keyframe<T> {
    pub offset: f32,
    pub value: T,
    pub easing: Easing,
    pub hold: bool,
}

impl<T> Keyframe<T> {
    #[must_use]
    pub fn new(offset: f32, value: T) -> Self {
        Self {
            offset,
            value,
            easing: Easing::Linear,
            hold: false,
        }
    }

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

    #[must_use]
    pub const fn hold(mut self) -> Self {
        self.hold = true;
        self
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct Keyframes<T> {
    frames: Box<[Keyframe<T>]>,
}

impl<T> Keyframes<T> {
    pub fn new(frames: impl Into<Vec<Keyframe<T>>>) -> Result<Self, TimingError> {
        let frames = frames.into();
        if frames.len() < 2
            || frames.first().is_none_or(|frame| frame.offset != 0.0)
            || frames.last().is_none_or(|frame| frame.offset != 1.0)
            || frames
                .iter()
                .any(|frame| !frame.offset.is_finite() || !(0.0..=1.0).contains(&frame.offset))
            || frames
                .windows(2)
                .any(|pair| pair[0].offset > pair[1].offset)
        {
            return Err(TimingError::InvalidKeyframes);
        }
        Ok(Self {
            frames: frames.into_boxed_slice(),
        })
    }

    #[must_use]
    pub fn as_slice(&self) -> &[Keyframe<T>] {
        &self.frames
    }
}

impl<T: Clone + Interpolate> Keyframes<T> {
    #[must_use]
    pub fn sample(&self, progress: f32) -> T {
        let progress = progress.clamp(0.0, 1.0);
        let upper = self
            .frames
            .partition_point(|frame| frame.offset <= progress);
        if upper == 0 {
            return self.frames[0].value.clone();
        }
        if upper == self.frames.len() {
            return self.frames[upper - 1].value.clone();
        }
        let from = &self.frames[upper - 1];
        let to = &self.frames[upper];
        if from.hold || from.offset == to.offset {
            return from.value.clone();
        }
        let local = (progress - from.offset) / (to.offset - from.offset);
        from.value
            .clone()
            .interpolate(to.value.clone(), from.easing.sample(local))
    }
}