argui-animation 0.2.0

Animation timing and interpolation for Argui interfaces
Documentation
use argui_animation::{CubicBezier, Easing, EasingError, LinearStop, StepPosition, Steps};

fn close(actual: f32, expected: f32) {
    assert!((actual - expected).abs() < 1.0e-4, "{actual} != {expected}");
}

#[test]
fn linear_custom_and_bezier_easing_sample_bounded_input() {
    close(Easing::Linear.sample(-1.0), 0.0);
    close(Easing::Linear.sample(2.0), 1.0);
    close(Easing::custom(|value| value * value).sample(0.5), 0.25);

    let linear_bezier = CubicBezier::new(0.0, 0.0, 1.0, 1.0).unwrap();
    close(linear_bezier.sample(0.35), 0.35);
    close(Easing::CubicBezier(linear_bezier).sample(0.7), 0.7);
    assert_eq!(format!("{:?}", Easing::custom(|value| value)), "Custom(..)");
}

#[test]
fn invalid_bezier_coordinates_are_rejected() {
    assert_eq!(
        CubicBezier::new(0.0, 0.0, 1.1, 1.0),
        Err(EasingError::InvalidBezier)
    );
    assert_eq!(
        CubicBezier::new(-0.1, 0.0, 1.0, 1.0),
        Err(EasingError::InvalidBezier)
    );
    assert_eq!(
        CubicBezier::new(0.0, f32::NAN, 1.0, 1.0),
        Err(EasingError::InvalidBezier)
    );
    assert!(EasingError::InvalidBezier.to_string().contains("Bezier"));
}

#[test]
fn every_step_position_has_explicit_boundary_behavior() {
    close(
        Steps::new(4, StepPosition::JumpStart).unwrap().sample(0.0),
        0.25,
    );
    close(
        Steps::new(4, StepPosition::JumpEnd).unwrap().sample(0.1),
        0.0,
    );
    close(
        Steps::new(4, StepPosition::JumpNone).unwrap().sample(0.5),
        2.0 / 3.0,
    );
    close(
        Steps::new(4, StepPosition::JumpBoth).unwrap().sample(0.5),
        3.0 / 5.0,
    );
    assert_eq!(
        Steps::new(0, StepPosition::JumpEnd),
        Err(EasingError::InvalidSteps)
    );
    assert_eq!(
        Steps::new(1, StepPosition::JumpNone),
        Err(EasingError::InvalidSteps)
    );
}

#[test]
fn piecewise_linear_easing_interpolates_and_supports_hard_stops() {
    let easing = Easing::piecewise_linear(vec![
        LinearStop::new(0.0, 0.0),
        LinearStop::new(0.5, 0.25),
        LinearStop::new(0.5, 0.75),
        LinearStop::new(1.0, 1.0),
    ])
    .unwrap();
    close(easing.sample(0.25), 0.125);
    close(easing.sample(0.5), 0.75);
    close(easing.sample(1.0), 1.0);

    for invalid in [
        vec![
            LinearStop::new(0.0, 0.0),
            LinearStop::new(f32::NAN, 0.5),
            LinearStop::new(1.0, 1.0),
        ],
        vec![
            LinearStop::new(0.0, 0.0),
            LinearStop::new(0.5, f32::INFINITY),
            LinearStop::new(1.0, 1.0),
        ],
        vec![
            LinearStop::new(0.0, 0.0),
            LinearStop::new(1.5, 0.5),
            LinearStop::new(1.0, 1.0),
        ],
        vec![LinearStop::new(0.0, 0.0)],
        vec![LinearStop::new(0.1, 0.0), LinearStop::new(1.0, 1.0)],
        vec![LinearStop::new(0.0, 0.0), LinearStop::new(0.9, f32::NAN)],
        vec![
            LinearStop::new(0.0, 0.0),
            LinearStop::new(0.8, 1.0),
            LinearStop::new(0.7, 1.0),
            LinearStop::new(1.0, 1.0),
        ],
    ] {
        assert!(matches!(
            Easing::piecewise_linear(invalid),
            Err(EasingError::InvalidStops)
        ));
    }
}