cranpose-animation 0.1.113

Animation system for Cranpose
Documentation
use std::{cell::RefCell, rc::Rc};

use cranpose_core::{Composition, MemoryApplier, State, location_key, with_current_composer};

use super::*;
use crate::animation::AnimationSpec;

fn drain_frames(
    composition: &mut Composition<MemoryApplier>,
    frame_time: &mut u64,
    until_nanos: u64,
) {
    let runtime = composition.runtime_handle();
    while *frame_time < until_nanos {
        *frame_time += 16_666_667;
        runtime.drain_frame_callbacks(*frame_time);
        let _ = composition.process_invalid_scopes();
    }
}

#[test]
fn transition_with_three_children_reports_running_until_the_last_settles() {
    let mut composition = Composition::new(MemoryApplier::new());
    let root_key = location_key(file!(), line!(), column!());
    let group_key = location_key(file!(), line!(), column!());
    let transition_slot = Rc::new(RefCell::new(None::<Transition<bool>>));
    let visible = Rc::new(RefCell::new(false));

    let mut pass = {
        let transition_slot = Rc::clone(&transition_slot);
        let visible = Rc::clone(&visible);
        move || {
            let transition_slot = Rc::clone(&transition_slot);
            let target = if *visible.borrow() { 1.0 } else { 0.0 };
            with_current_composer(|composer| {
                composer.with_group(group_key, |_| {
                    let transition = updateTransition(target > 0.0, "visibility");
                    let _fast = transition.animateFloat(
                        target,
                        AnimationType::Tween(AnimationSpec::linear(100)),
                        "fast",
                    );
                    let _medium = transition.animateFloat(
                        target,
                        AnimationType::Tween(AnimationSpec::linear(300)),
                        "medium",
                    );
                    let _slow = transition.animateFloat(
                        target,
                        AnimationType::Tween(AnimationSpec::linear(500)),
                        "slow",
                    );
                    transition_slot.borrow_mut().replace(transition);
                });
            });
        }
    };

    composition
        .render(root_key, &mut pass)
        .expect("initial render succeeds");
    let is_running = || {
        transition_slot
            .borrow()
            .as_ref()
            .expect("transition available")
            .is_running()
    };
    assert!(
        !is_running(),
        "a freshly created transition with no target change yet should not be running"
    );

    *visible.borrow_mut() = true;
    composition
        .render(root_key, &mut pass)
        .expect("target change render succeeds");
    assert!(
        is_running(),
        "changing the target state should start every child animating"
    );

    let mut frame_time = 0u64;
    drain_frames(&mut composition, &mut frame_time, 150_000_000);
    assert!(
        is_running(),
        "should still report running while the slower children are mid-flight"
    );

    drain_frames(&mut composition, &mut frame_time, 600_000_000);
    assert!(
        !is_running(),
        "should stop running only once every child has settled"
    );
}

#[test]
fn transition_retargets_mid_flight_instead_of_snapping() {
    let mut composition = Composition::new(MemoryApplier::new());
    let root_key = location_key(file!(), line!(), column!());
    let group_key = location_key(file!(), line!(), column!());
    let state_slot = Rc::new(RefCell::new(None::<State<f32>>));
    let stage = Rc::new(RefCell::new(0u8));

    let mut pass = {
        let state_slot = Rc::clone(&state_slot);
        let stage = Rc::clone(&stage);
        move || {
            let state_slot = Rc::clone(&state_slot);
            let stage_value = *stage.borrow();
            with_current_composer(|composer| {
                composer.with_group(group_key, |_| {
                    let transition = updateTransition(stage_value, "stage");
                    let target = match stage_value {
                        0 => 0.0,
                        1 => 100.0,
                        _ => -60.0,
                    };
                    let state = transition.animateFloat(
                        target,
                        AnimationType::Tween(AnimationSpec::linear(400)),
                        "value",
                    );
                    state_slot.borrow_mut().replace(state);
                });
            });
        }
    };

    composition
        .render(root_key, &mut pass)
        .expect("initial render succeeds");
    let value = || state_slot.borrow().as_ref().expect("state available").get();
    assert_eq!(value(), 0.0);

    *stage.borrow_mut() = 1;
    composition
        .render(root_key, &mut pass)
        .expect("first target render succeeds");

    let mut frame_time = 0u64;
    drain_frames(&mut composition, &mut frame_time, 100_000_000);
    let mid_flight = value();
    assert!(
        mid_flight > 0.0 && mid_flight < 100.0,
        "should be mid-flight before retargeting, got {mid_flight}"
    );

    *stage.borrow_mut() = 2;
    composition
        .render(root_key, &mut pass)
        .expect("retarget render succeeds");
    assert_eq!(
        value(),
        mid_flight,
        "changing the transition's target state mid-flight must not snap the child's value"
    );

    drain_frames(&mut composition, &mut frame_time, 600_000_000);
    assert_eq!(
        value(),
        -60.0,
        "should go on to settle at the new target state's value"
    );
}