argui-animation 0.1.0

Animation timing and interpolation for Argui interfaces
Documentation
use std::{fmt, sync::Arc};

type CustomFunction = dyn Fn(f32) -> f32 + Send + Sync + 'static;

#[derive(Clone, Default)]
pub enum Easing {
    #[default]
    Linear,
    CubicBezier(CubicBezier),
    Steps(Steps),
    PiecewiseLinear(Box<[LinearStop]>),
    Custom(Arc<CustomFunction>),
}

impl Easing {
    #[must_use]
    pub fn custom(function: impl Fn(f32) -> f32 + Send + Sync + 'static) -> Self {
        Self::Custom(Arc::new(function))
    }

    pub fn piecewise_linear(stops: impl Into<Vec<LinearStop>>) -> Result<Self, EasingError> {
        let stops = stops.into();
        if stops.len() < 2
            || stops.first().is_none_or(|stop| stop.input != 0.0)
            || stops.last().is_none_or(|stop| stop.input != 1.0)
            || stops.iter().any(|stop| {
                !stop.input.is_finite()
                    || !stop.output.is_finite()
                    || !(0.0..=1.0).contains(&stop.input)
            })
            || stops.windows(2).any(|pair| pair[0].input > pair[1].input)
        {
            return Err(EasingError::InvalidStops);
        }
        Ok(Self::PiecewiseLinear(stops.into_boxed_slice()))
    }

    #[must_use]
    pub fn sample(&self, progress: f32) -> f32 {
        let progress = progress.clamp(0.0, 1.0);
        match self {
            Self::Linear => progress,
            Self::CubicBezier(curve) => curve.sample(progress),
            Self::Steps(steps) => steps.sample(progress),
            Self::PiecewiseLinear(stops) => sample_stops(stops, progress),
            Self::Custom(function) => function(progress),
        }
    }
}

impl fmt::Debug for Easing {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Linear => formatter.write_str("Linear"),
            Self::CubicBezier(value) => formatter.debug_tuple("CubicBezier").field(value).finish(),
            Self::Steps(value) => formatter.debug_tuple("Steps").field(value).finish(),
            Self::PiecewiseLinear(value) => formatter
                .debug_tuple("PiecewiseLinear")
                .field(value)
                .finish(),
            Self::Custom(_) => formatter.write_str("Custom(..)"),
        }
    }
}

impl PartialEq for Easing {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Linear, Self::Linear) => true,
            (Self::CubicBezier(left), Self::CubicBezier(right)) => left == right,
            (Self::Steps(left), Self::Steps(right)) => left == right,
            (Self::PiecewiseLinear(left), Self::PiecewiseLinear(right)) => left == right,
            (Self::Custom(left), Self::Custom(right)) => Arc::ptr_eq(left, right),
            _ => false,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CubicBezier {
    x1: f32,
    y1: f32,
    x2: f32,
    y2: f32,
}

impl CubicBezier {
    pub fn new(x1: f32, y1: f32, x2: f32, y2: f32) -> Result<Self, EasingError> {
        if [x1, y1, x2, y2].iter().any(|value| !value.is_finite())
            || !(0.0..=1.0).contains(&x1)
            || !(0.0..=1.0).contains(&x2)
        {
            return Err(EasingError::InvalidBezier);
        }
        Ok(Self { x1, y1, x2, y2 })
    }

    #[must_use]
    pub fn sample(self, progress: f32) -> f32 {
        let progress = progress.clamp(0.0, 1.0);
        let mut low = 0.0;
        let mut high = 1.0;
        let mut parameter = progress;
        for _ in 0..16 {
            let x = cubic(parameter, self.x1, self.x2);
            if (x - progress).abs() <= 1.0e-5 {
                break;
            }
            if x < progress {
                low = parameter;
            } else {
                high = parameter;
            }
            parameter = (low + high) * 0.5;
        }
        cubic(parameter, self.y1, self.y2)
    }
}

fn cubic(parameter: f32, first: f32, second: f32) -> f32 {
    let inverse = 1.0 - parameter;
    3.0 * inverse * inverse * parameter * first
        + 3.0 * inverse * parameter * parameter * second
        + parameter * parameter * parameter
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StepPosition {
    JumpStart,
    JumpEnd,
    JumpNone,
    JumpBoth,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Steps {
    count: u32,
    position: StepPosition,
}

impl Steps {
    pub fn new(count: u32, position: StepPosition) -> Result<Self, EasingError> {
        if count == 0 || (position == StepPosition::JumpNone && count == 1) {
            return Err(EasingError::InvalidSteps);
        }
        Ok(Self { count, position })
    }

    #[must_use]
    pub fn sample(self, progress: f32) -> f32 {
        let progress = progress.clamp(0.0, 1.0);
        let count = self.count as f32;
        match self.position {
            StepPosition::JumpStart => ((progress * count).floor() + 1.0).min(count) / count,
            StepPosition::JumpEnd => (progress * count).floor() / count,
            StepPosition::JumpNone => ((progress * count).floor() / (count - 1.0)).clamp(0.0, 1.0),
            StepPosition::JumpBoth => ((progress * count).floor() + 1.0) / (count + 1.0),
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LinearStop {
    pub input: f32,
    pub output: f32,
}

impl LinearStop {
    #[must_use]
    pub const fn new(input: f32, output: f32) -> Self {
        Self { input, output }
    }
}

fn sample_stops(stops: &[LinearStop], progress: f32) -> f32 {
    let upper = stops.partition_point(|stop| stop.input <= progress);
    if upper == stops.len() {
        return stops[upper - 1].output;
    }
    let from = stops[upper - 1];
    let to = stops[upper];
    if from.input == to.input {
        return to.output;
    }
    let local = (progress - from.input) / (to.input - from.input);
    from.output + (to.output - from.output) * local
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum EasingError {
    InvalidBezier,
    InvalidSteps,
    InvalidStops,
}

impl fmt::Display for EasingError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidBezier => formatter.write_str(
                "Bezier x coordinates must be within 0..=1 and all coordinates must be finite",
            ),
            Self::InvalidSteps => {
                formatter.write_str("steps require at least one jump, or two for jump-none")
            }
            Self::InvalidStops => {
                formatter.write_str("linear stops must be finite, sorted, and cover 0..=1")
            }
        }
    }
}

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