argui-animation 0.2.1

Animation timing and interpolation for Argui interfaces
Documentation
use argui_animation::{
    Duration, Easing, Keyframe, Keyframes, Motion, MotionState, MotionTrack, SpringConfig, Time,
    Timeline, Timing, Tween,
};

#[test]
fn tween_retargets_from_the_presented_value() {
    let motion = Motion::new(0.0_f32);
    motion.animate_to(
        10.0,
        Tween::new(Duration::from_millis(100)).easing(Easing::Linear),
    );
    assert!(!MotionTrack::advance(&motion, Time::from_nanos(1)));
    assert!(MotionTrack::advance(&motion, Time::from_nanos(50_000_001)));
    let presented = motion.value();
    assert!((presented - 5.0).abs() < 0.001);

    motion.animate_to(20.0, Tween::new(Duration::from_millis(100)));
    assert!(!MotionTrack::advance(&motion, Time::from_nanos(60_000_000)));
    assert_eq!(motion.value(), presented);
    assert!(MotionTrack::advance(&motion, Time::from_nanos(110_000_000)));
    assert!((motion.value() - 12.5).abs() < 0.001);
}

#[test]
fn playing_a_low_level_timeline_finishes_at_its_terminal_value() {
    let timeline = Timeline::new(
        Keyframes::new([Keyframe::new(0.0, 2.0_f32), Keyframe::new(1.0, 12.0_f32)]).unwrap(),
        Timing::new(Duration::from_millis(100)),
    )
    .unwrap();
    let motion = Motion::new(2.0_f32);
    motion.play(timeline);

    assert!(!MotionTrack::advance(&motion, Time::from_nanos(1)));
    assert!(MotionTrack::advance(&motion, Time::from_nanos(100_000_001)));
    assert_eq!(motion.value(), 12.0);
    assert_eq!(motion.target(), 12.0);
    assert_eq!(motion.state(), MotionState::Finished);
}

#[test]
fn spring_retarget_preserves_velocity() {
    let motion = Motion::new(0.0_f32);
    motion.spring_to(10.0, SpringConfig::default()).unwrap();
    MotionTrack::advance(&motion, Time::from_nanos(1));
    MotionTrack::advance(&motion, Time::from_nanos(16_000_001));
    let velocity = motion.velocity();
    assert!(velocity > 0.0);
    let presented = motion.value();

    motion.spring_to(-4.0, SpringConfig::default()).unwrap();
    assert_eq!(motion.value(), presented);
    assert_eq!(motion.velocity(), velocity);
}

#[test]
fn controls_have_explicit_terminal_values() {
    let motion = Motion::new(2.0_f32);
    motion.animate_to(8.0, Tween::new(Duration::from_secs(1)));
    motion.pause();
    assert_eq!(motion.state(), MotionState::Paused);
    motion.resume();
    assert_eq!(motion.state(), MotionState::Running);
    motion.cancel();
    assert_eq!(motion.state(), MotionState::Canceled);
    assert_eq!(motion.value(), 2.0);

    motion.animate_to(8.0, Tween::new(Duration::from_secs(1)));
    motion.finish();
    assert_eq!(motion.state(), MotionState::Finished);
    assert_eq!(motion.value(), 8.0);
    assert!(!motion.is_active());

    motion.set(3.0);
    assert_eq!(motion.state(), MotionState::Idle);
    assert_eq!(motion.value(), 3.0);
}

#[test]
fn zero_duration_tweens_finish_synchronously() {
    let motion = Motion::new(1.0_f32);
    motion.animate_to(9.0, Tween::new(Duration::ZERO));

    assert_eq!(motion.value(), 9.0);
    assert_eq!(motion.target(), 9.0);
    assert_eq!(motion.state(), MotionState::Finished);
    assert!(!motion.is_active());
}

#[test]
fn delayed_unfilled_timeline_keeps_the_presented_value_until_it_starts() {
    let animation = Timeline::new(
        Keyframes::new([Keyframe::new(0.0, 0.0_f32), Keyframe::new(1.0, 10.0)]).unwrap(),
        Timing::new(Duration::from_millis(100)).delay(Duration::from_millis(100)),
    )
    .unwrap();
    let motion = Motion::new(7.0);
    motion.play(animation);
    assert!(!motion.advance_spring(Time::ZERO));
    assert!(!motion.advance(Time::ZERO).unwrap());
    assert!(!motion.advance(Time::from_nanos(50_000_000)).unwrap());
    assert_eq!(motion.value(), 7.0);
    assert!(motion.advance(Time::from_nanos(150_000_000)).unwrap());
    assert_eq!(motion.value(), 5.0);
}

#[test]
fn spring_reaches_a_terminal_state_without_an_explicit_finish() {
    let motion = Motion::new(0.0_f32);
    motion.spring_to(10.0, SpringConfig::default()).unwrap();
    motion.pause();
    motion.resume();
    assert!(!motion.advance(Time::ZERO).unwrap());
    assert_eq!(motion.value(), 0.0);
    for index in 1..1000 {
        motion.advance_spring(Time::from_nanos(index * 16_000_000));
        if !motion.is_active() {
            break;
        }
    }
    assert_eq!(motion.state(), MotionState::Finished);
    assert_eq!(motion.value(), 10.0);
    assert!(!motion.advance_spring(Time::from_nanos(20_000_000_000)));
}

#[test]
fn identity_debug_and_idle_controls_are_stable() {
    let motion = Motion::new(3.0_f32);
    let clone = motion.clone();
    let other = Motion::new(3.0_f32);

    motion.pause();
    motion.resume();
    assert_eq!(motion.state(), MotionState::Idle);
    assert_eq!(motion, clone);
    assert_ne!(motion, other);
    assert_eq!(motion.identity(), clone.identity());
    assert!(format!("{motion:?}").contains("Motion"));
    assert_eq!(motion.velocity(), 0.0);
    assert!(!motion.advance_spring(Time::ZERO));
}

#[test]
fn timeline_pause_resume_and_restart_preserve_presented_values() {
    let motion = Motion::new(0.0_f32);
    motion.restart(2.0, 12.0, Tween::new(Duration::from_millis(100)));
    MotionTrack::advance(&motion, Time::from_nanos(1));
    MotionTrack::advance(&motion, Time::from_nanos(40_000_001));
    let paused = motion.value();
    motion.pause();
    assert_eq!(motion.state(), MotionState::Paused);
    assert!(!MotionTrack::advance(&motion, Time::from_nanos(80_000_001)));
    assert_eq!(motion.value(), paused);

    motion.resume();
    assert!(!MotionTrack::advance(&motion, Time::from_nanos(90_000_001)));
    assert!(MotionTrack::advance(&motion, Time::from_nanos(150_000_001)));
    assert!(motion.value() > paused);
}

#[test]
fn settled_and_restarted_springs_have_explicit_states() {
    let motion = Motion::new(5.0_f32);
    motion.spring_to(5.0, SpringConfig::default()).unwrap();
    assert_eq!(motion.state(), MotionState::Finished);
    assert!(!motion.advance_spring(Time::ZERO));

    motion
        .restart_spring(0.0, 5.0, SpringConfig::default())
        .unwrap();
    assert_eq!(motion.value(), 0.0);
    assert_eq!(motion.target(), 5.0);
    assert_eq!(motion.state(), MotionState::Running);
}