cranpose-animation 0.1.120

Animation system for Cranpose
Documentation

Cranpose Animation

A physics-based animation library designed for the Cranpose composition model.

When to Use

Use this crate to create smooth, interruptible animations. Unlike traditional timeline-based animation systems, Cranpose animations are driven by state changes. When a target value changes, the animation system automatically calculates the transition from the current value to the new target, maintaining velocity and continuity.

Key Concepts

  • Animatable<T, V>: A low-level value holder that tracks the current value and velocity. It is the primitive used to build higher-level animation APIs.
  • AnimationSpec: Defines the behavior of an animation. Common types include:
    • Spring: Physical simulation based on stiffness and damping ratio.
    • Tween: Duration-based interpolation with an easing curve.
  • animate*AsState: Composable functions that subscribe to a target value and return a State object representing the current animated value. animateFloatAsState and animateColorAsState are joined by animateDpAsState, animateOffsetAsState, animateSizeAsState and animateRectAsState, all built over the generic animateValueAsState and the SpringScalar vector-converter core -- any type that decomposes into a fixed-size float vector (see SpringScalar/Lerp) gets a specialization for free.
  • Transition<S>: A finite, state-driven animation obtained from updateTransition. Multiple child animations (transition.animateFloat { }, .animateDp { }, .animateColor { }, or the generic .animateValue { }) each derive their own target from the same state and run in lockstep; transition.is_running() is true until every child has settled.

Example: Interruptible Spring Animation

use cranpose::prelude::*;

#[composable]
fn AnimatedBox(target_size: f32) {
    // animateFloatAsState automatically handles interruptions.
    // If target_size changes while animating, it will seamlessly retarget
    // preserving current velocity.
    let size = animateFloatAsState(
        target_size, 
        Some(spring(Spring::DampingRatioMediumBouncy, Spring::StiffnessLow))
    );
    
    Box(
        Modifier
            .size(size.value())
            .background(Color::Blue)
    );
}

Example: Infinite Transition

use cranpose_animation::{
    infiniteRepeatable, rememberInfiniteTransition, AnimationSpec, Easing, RepeatMode, StartOffset,
};
use cranpose_ui::*;

#[composable]
fn PulsingDot() {
    let transition = rememberInfiniteTransition("pulse");
    let alpha = transition.animateFloat(
        0.0,
        1.0,
        infiniteRepeatable(
            AnimationSpec::tween(900, Easing::EaseInOut),
            RepeatMode::Reverse,
            StartOffset::default(),
        ),
        "pulse_alpha",
    );

    Box(
        Modifier::empty()
            .width(24.0)
            .height(24.0)
            .background(Color(0.2, 0.5, 0.9, alpha.value())),
        BoxSpec::default(),
        || {},
    );
}

Example: Finite, State-Driven Transition

use cranpose_animation::{updateTransition, AnimationSpec, AnimationType, Easing};
use cranpose_ui_graphics::Color;

#[composable]
fn ExpandingCard(expanded: bool) {
    let transition = updateTransition(expanded, "card");
    let tween = AnimationType::Tween(AnimationSpec::tween(240, Easing::FastOutSlowInEasing));

    let height = transition.animateFloat(if expanded { 320.0 } else { 96.0 }, tween, "height");
    let tint = transition.animateColor(
        if expanded { Color(0.1, 0.1, 0.15, 1.0) } else { Color(0.9, 0.9, 0.95, 1.0) },
        tween,
        "tint",
    );

    // transition.is_running() stays true until both children have settled,
    // and flipping `expanded` again mid-animation retargets in place rather
    // than snapping.
    Box(
        Modifier::empty().height(height.value()).background(tint.value()),
        BoxSpec::default(),
        || {},
    );
}