argui-animation 0.1.0

Animation timing and interpolation for Argui interfaces
Documentation
use argui_core::{Color, Point, Rect, Size, Transform2D};
use std::fmt;

pub trait MotionValue: Copy + PartialEq {
    fn zero() -> Self;

    fn add(self, other: Self) -> Self;

    fn subtract(self, other: Self) -> Self;

    fn scale(self, factor: f64) -> Self;

    fn magnitude(self) -> f64;
}

macro_rules! scalar_motion {
    ($type:ty) => {
        impl MotionValue for $type {
            fn zero() -> Self {
                0.0
            }

            fn add(self, other: Self) -> Self {
                self + other
            }

            fn subtract(self, other: Self) -> Self {
                self - other
            }

            fn scale(self, factor: f64) -> Self {
                self * factor as Self
            }

            fn magnitude(self) -> f64 {
                self.abs() as f64
            }
        }
    };
}

scalar_motion!(f32);
scalar_motion!(f64);

impl<const N: usize> MotionValue for [f32; N] {
    fn zero() -> Self {
        [0.0; N]
    }

    fn add(mut self, other: Self) -> Self {
        for (value, other) in self.iter_mut().zip(other) {
            *value += other;
        }
        self
    }

    fn subtract(mut self, other: Self) -> Self {
        for (value, other) in self.iter_mut().zip(other) {
            *value -= other;
        }
        self
    }

    fn scale(mut self, factor: f64) -> Self {
        for value in &mut self {
            *value *= factor as f32;
        }
        self
    }

    fn magnitude(self) -> f64 {
        self.into_iter()
            .map(|value| f64::from(value).powi(2))
            .sum::<f64>()
            .sqrt()
    }
}

impl MotionValue for Point {
    fn zero() -> Self {
        Self::default()
    }

    fn add(self, other: Self) -> Self {
        Self::new(self.x + other.x, self.y + other.y)
    }

    fn subtract(self, other: Self) -> Self {
        Self::new(self.x - other.x, self.y - other.y)
    }

    fn scale(self, factor: f64) -> Self {
        Self::new(self.x * factor as f32, self.y * factor as f32)
    }

    fn magnitude(self) -> f64 {
        f64::from(self.x).hypot(f64::from(self.y))
    }
}

impl MotionValue for Size {
    fn zero() -> Self {
        Self::default()
    }

    fn add(self, other: Self) -> Self {
        Self::new(self.width + other.width, self.height + other.height)
    }

    fn subtract(self, other: Self) -> Self {
        Self::new(self.width - other.width, self.height - other.height)
    }

    fn scale(self, factor: f64) -> Self {
        Self::new(self.width * factor as f32, self.height * factor as f32)
    }

    fn magnitude(self) -> f64 {
        f64::from(self.width).hypot(f64::from(self.height))
    }
}

impl MotionValue for Rect {
    fn zero() -> Self {
        Self::default()
    }

    fn add(self, other: Self) -> Self {
        Self::new(self.origin.add(other.origin), self.size.add(other.size))
    }

    fn subtract(self, other: Self) -> Self {
        Self::new(
            self.origin.subtract(other.origin),
            self.size.subtract(other.size),
        )
    }

    fn scale(self, factor: f64) -> Self {
        Self::new(self.origin.scale(factor), self.size.scale(factor))
    }

    fn magnitude(self) -> f64 {
        self.origin.magnitude().hypot(self.size.magnitude())
    }
}

impl MotionValue for Color {
    fn zero() -> Self {
        Self::TRANSPARENT
    }

    fn add(self, other: Self) -> Self {
        map_color(self, other, |left, right| left + right)
    }

    fn subtract(self, other: Self) -> Self {
        map_color(self, other, |left, right| left - right)
    }

    fn scale(self, factor: f64) -> Self {
        let [red, green, blue, alpha] = self.to_linear_rgba();
        let factor = factor as f32;
        Self::linear_rgba(red * factor, green * factor, blue * factor, alpha * factor)
    }

    fn magnitude(self) -> f64 {
        self.to_linear_rgba()
            .into_iter()
            .map(|channel| f64::from(channel).powi(2))
            .sum::<f64>()
            .sqrt()
    }
}

impl MotionValue for Transform2D {
    fn zero() -> Self {
        Self {
            translation: Point::default(),
            scale: Point::default(),
            rotation: 0.0,
            skew: Point::default(),
        }
    }

    fn add(self, other: Self) -> Self {
        Self {
            translation: self.translation.add(other.translation),
            scale: self.scale.add(other.scale),
            rotation: self.rotation + other.rotation,
            skew: self.skew.add(other.skew),
        }
    }

    fn subtract(self, other: Self) -> Self {
        Self {
            translation: self.translation.subtract(other.translation),
            scale: self.scale.subtract(other.scale),
            rotation: self.rotation - other.rotation,
            skew: self.skew.subtract(other.skew),
        }
    }

    fn scale(self, factor: f64) -> Self {
        Self {
            translation: self.translation.scale(factor),
            scale: self.scale.scale(factor),
            rotation: self.rotation * factor as f32,
            skew: self.skew.scale(factor),
        }
    }

    fn magnitude(self) -> f64 {
        self.translation
            .magnitude()
            .hypot(self.scale.magnitude())
            .hypot(f64::from(self.rotation))
            .hypot(self.skew.magnitude())
    }
}

fn map_color(left: Color, right: Color, operation: impl Fn(f32, f32) -> f32) -> Color {
    let left = left.to_linear_rgba();
    let right = right.to_linear_rgba();
    Color::linear_rgba(
        operation(left[0], right[0]),
        operation(left[1], right[1]),
        operation(left[2], right[2]),
        operation(left[3], right[3]),
    )
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PhysicsError {
    InvalidMass,
    InvalidStiffness,
    InvalidDamping,
    InvalidRestThreshold,
    InvalidDecay,
    InvalidBounds,
}

impl fmt::Display for PhysicsError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidMass => formatter.write_str("spring mass must be finite and positive"),
            Self::InvalidStiffness => {
                formatter.write_str("spring stiffness must be finite and positive")
            }
            Self::InvalidDamping => {
                formatter.write_str("spring damping must be finite and non-negative")
            }
            Self::InvalidRestThreshold => {
                formatter.write_str("rest thresholds must be finite and non-negative")
            }
            Self::InvalidDecay => formatter.write_str("decay rate must be finite and positive"),
            Self::InvalidBounds => formatter.write_str("inertia bounds must be finite and ordered"),
        }
    }
}

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