aura-anim-iced 0.2.1

Iced-first animation primitives.
Documentation
use float_cmp::assert_approx_eq;

use super::{Keyframe, KeyframesBuilder, segment::KeyframeSegment};
use crate::{
    property::{
        BACKGROUND, OPACITY, PropertyEntry, PropertyKey, PropertySnapshot, PropertySpec,
        PropertyValue, SCALE, Size, Vector2, WIDTH,
    },
    timing::{Delay, Direction, Easing, FillMode, IterationCount, Timing},
};

const OFFSET: PropertySpec<Vector2> = PropertySpec::new(PropertyKey::new("test", "offset"), 20);
const BOX_SIZE: PropertySpec<Size> = PropertySpec::new(PropertyKey::new("test", "size"), 30);
const SCALE_AS_COLOR: PropertySpec<crate::property::Color> =
    PropertySpec::new(PropertyKey::new("aura", "scale"), 20);

fn scalar(snapshot: &PropertySnapshot, spec: PropertySpec<crate::property::Scalar>) -> f32 {
    let Some(entry) = snapshot.find_property(&spec.raw()) else {
        panic!("expected scalar property {}", spec.raw().key().name());
    };
    let PropertyValue::Scalar(value) = entry.value() else {
        panic!("expected scalar value");
    };

    *value
}

#[test]
fn new_keyframes_start_empty_with_default_timing() {
    let builder = KeyframesBuilder::new();
    let keyframes = builder.clone().finish();

    assert!(keyframes.is_empty());
    assert_eq!(keyframes.track_count(), 0);
    assert!(builder.frames.is_empty());
    assert_eq!(*builder.timing(), Timing::default());
}

#[test]
fn offsets_are_normalized_sorted_and_duplicate_offsets_merge() {
    let keyframes = KeyframesBuilder::new()
        .at(1.25, (OPACITY, 1.0))
        .at(-0.25, (OPACITY, 0.0))
        .at(f32::NAN, (SCALE, 1.5))
        .at(0.0, (OPACITY, 0.25));

    let offsets = keyframes
        .frames
        .iter()
        .map(Keyframe::offset)
        .collect::<Vec<_>>();

    assert_eq!(offsets, vec![0.0, 1.0]);
    assert_approx_eq!(
        f32,
        scalar(keyframes.frames[0].snapshot(), OPACITY),
        0.25,
        epsilon = 1e-5
    );
    assert_approx_eq!(
        f32,
        scalar(keyframes.frames[0].snapshot(), SCALE),
        1.5,
        epsilon = 1e-5
    );
}

#[test]
fn helper_builders_insert_common_typed_properties() {
    let color = iced::Color::from_rgb(0.2, 0.3, 0.4);
    let keyframes = KeyframesBuilder::new()
        .background_color(0.5, color)
        .opacity(0.5, 0.8)
        .scale(1.0, 1.2)
        .with_timing(Timing::new(120.0).with_delay(Delay::from_millis(20.0)));

    assert_eq!(keyframes.frames.len(), 2);
    assert_approx_eq!(
        f32,
        scalar(keyframes.frames[0].snapshot(), OPACITY),
        0.8,
        epsilon = 1e-5
    );
    assert_eq!(
        keyframes.frames[0]
            .find_property(&BACKGROUND.raw())
            .map(PropertyEntry::value),
        Some(&PropertyValue::Color(color))
    );
    assert_eq!(keyframes.timing().delay(), Delay::from_millis(20.0));
}

#[test]
fn segment_lookup_covers_empty_single_exact_and_between_cases() {
    assert_eq!(
        KeyframeSegment::find(&KeyframesBuilder::new().frames, 0.5),
        KeyframeSegment::Empty
    );

    let single = KeyframesBuilder::new().at(0.5, (OPACITY, 0.5));
    assert!(KeyframeSegment::find(&single.frames, 0.2).is_resolved());

    let keyframes = KeyframesBuilder::new()
        .at(0.25, (OPACITY, 0.25))
        .at(0.75, (OPACITY, 0.75));

    let KeyframeSegment::Exact(first) = KeyframeSegment::find(&keyframes.frames, 0.0) else {
        panic!("expected edge exact segment");
    };
    assert_approx_eq!(f32, first.offset(), 0.25, epsilon = 1e-5);

    let KeyframeSegment::Between { from, to, progress } =
        KeyframeSegment::find(&keyframes.frames, 0.5)
    else {
        panic!("expected interpolating segment");
    };
    assert_approx_eq!(f32, from.offset(), 0.25, epsilon = 1e-5);
    assert_approx_eq!(f32, to.offset(), 0.75, epsilon = 1e-5);
    assert_approx_eq!(f32, progress, 0.5, epsilon = 1e-5);
}

#[test]
fn sample_at_interpolates_scalar_color_vector_and_size_values() {
    let from_color = iced::Color::from_rgb(0.0, 0.2, 0.4);
    let to_color = iced::Color::from_rgb(1.0, 0.6, 0.0);
    let keyframes = KeyframesBuilder::new()
        .at(
            0.0,
            PropertySnapshot::from(vec![
                PropertyEntry::new(OPACITY, 0.0),
                PropertyEntry::new(WIDTH, 100.0),
                PropertyEntry::new(OFFSET, iced::Vector::new(0.0, 10.0)),
                PropertyEntry::new(BOX_SIZE, iced::Size::new(20.0, 40.0)),
                PropertyEntry::new(BACKGROUND, from_color),
            ]),
        )
        .at(
            1.0,
            PropertySnapshot::from(vec![
                PropertyEntry::new(OPACITY, 1.0),
                PropertyEntry::new(WIDTH, 200.0),
                PropertyEntry::new(OFFSET, iced::Vector::new(10.0, 30.0)),
                PropertyEntry::new(BOX_SIZE, iced::Size::new(40.0, 80.0)),
                PropertyEntry::new(BACKGROUND, to_color),
            ]),
        )
        .finish();

    let sampled = keyframes.sample_at(0.5).expect("sample");

    assert_approx_eq!(f32, scalar(&sampled, OPACITY), 0.5, epsilon = 1e-5);
    assert_approx_eq!(f32, scalar(&sampled, WIDTH), 150.0, epsilon = 1e-5);
    assert_eq!(
        sampled
            .find_property(&OFFSET.raw())
            .map(PropertyEntry::value),
        Some(&PropertyValue::Vector2(iced::Vector::new(5.0, 20.0)))
    );
    assert_eq!(
        sampled
            .find_property(&BOX_SIZE.raw())
            .map(PropertyEntry::value),
        Some(&PropertyValue::Size(iced::Size::new(30.0, 60.0)))
    );
    let Some(PropertyValue::Color(color)) = sampled
        .find_property(&BACKGROUND.raw())
        .map(PropertyEntry::value)
    else {
        panic!("expected color");
    };
    assert_approx_eq!(f32, color.r, 0.5, epsilon = 1e-5);
    assert_approx_eq!(f32, color.g, 0.4, epsilon = 1e-5);
}

#[test]
fn mismatched_value_shapes_drop_that_property_instead_of_panicking() {
    let keyframes = KeyframesBuilder::new()
        .at(
            0.0,
            PropertySnapshot::from(vec![PropertyEntry::new(SCALE, 1.0)]),
        )
        .at(
            1.0,
            PropertySnapshot::from(vec![PropertyEntry::new(SCALE_AS_COLOR, iced::Color::WHITE)]),
        )
        .finish();

    let sampled = keyframes.sample_at(0.5).expect("empty sample");

    assert!(sampled.is_empty());
}

#[test]
fn timing_fill_controls_elapsed_sampling() {
    let keyframes = KeyframesBuilder::new()
        .with_timing(
            Timing::new(100.0)
                .with_delay(Delay::from_millis(50.0))
                .with_fill_mode(FillMode::None),
        )
        .opacity(0.0, 0.0)
        .opacity(1.0, 1.0);

    let before = keyframes.timing().normalize_elapsed(25.0);
    assert!(!before.has_sample());

    let after = keyframes
        .with_timing(Timing::new(100.0).with_fill_mode(FillMode::Forwards))
        .timing()
        .normalize_elapsed(120.0);
    assert!(after.has_sample());
    assert_approx_eq!(f64, after.iteration_progress, 1.0, epsilon = 1e-10);
}

#[test]
fn easing_is_applied_between_neighboring_keyframes() {
    let keyframes = KeyframesBuilder::new()
        .with_timing(Timing::new(100.0).with_easing(Easing::EaseIn))
        .opacity(0.0, 0.0)
        .opacity(1.0, 1.0)
        .finish();

    let sampled = keyframes.sample_at(0.5).expect("sample");

    assert_approx_eq!(
        f32,
        scalar(&sampled, OPACITY),
        Easing::EaseIn.value(0.5),
        epsilon = 1e-5
    );
}

#[test]
fn sample_completion_respects_direction_and_iteration_count() {
    let sample_completion = |timing: Timing| {
        KeyframesBuilder::new()
            .with_timing(timing)
            .opacity(0.0, 0.0)
            .opacity(1.0, 1.0)
            .finish()
            .sample_completion()
            .expect("completion sample")
    };

    assert_approx_eq!(
        f32,
        scalar(&sample_completion(Timing::new(100.0)), OPACITY),
        1.0,
        epsilon = 1e-5
    );
    assert_approx_eq!(
        f32,
        scalar(
            &sample_completion(Timing::new(100.0).with_direction(Direction::Reverse)),
            OPACITY
        ),
        0.0,
        epsilon = 1e-5
    );
    assert_approx_eq!(
        f32,
        scalar(
            &sample_completion(
                Timing::new(100.0)
                    .with_direction(Direction::Alternate)
                    .with_iterations(2)
            ),
            OPACITY
        ),
        0.0,
        epsilon = 1e-5
    );
    assert_approx_eq!(
        f32,
        scalar(
            &sample_completion(
                Timing::new(100.0)
                    .with_direction(Direction::AlternateReverse)
                    .with_iterations(2)
            ),
            OPACITY
        ),
        1.0,
        epsilon = 1e-5
    );

    let infinite = KeyframesBuilder::new()
        .with_timing(Timing::new(100.0).with_iterations(IterationCount::infinite()))
        .opacity(0.0, 0.0)
        .opacity(1.0, 1.0)
        .finish();

    assert_eq!(infinite.sample_completion(), None);
}