Skip to main content

cranpose_animation/
color.rs

1//! Color animation for Cranpose
2//!
3//! Mirrors Jetpack Compose's `animateColorAsState` from
4//! `androidx.compose.animation.SingleValueAnimation` by layering a [`Lerp`]
5//! implementation for [`Color`] over the generic [`crate::Animatable`] machinery.
6//!
7//! Note: This module uses camelCase for function names to maintain 1:1 API
8//! parity with Jetpack Compose.
9
10#![allow(non_snake_case)]
11
12use cranpose_core::State;
13use cranpose_ui_graphics::Color;
14
15use crate::animation::{AnimationType, Lerp, SpringScalar, animateValueAsState};
16
17impl Lerp for Color {
18    fn lerp(&self, target: &Self, fraction: f32) -> Self {
19        Color(
20            self.0.lerp(&target.0, fraction).clamp(0.0, 1.0),
21            self.1.lerp(&target.1, fraction).clamp(0.0, 1.0),
22            self.2.lerp(&target.2, fraction).clamp(0.0, 1.0),
23            self.3.lerp(&target.3, fraction).clamp(0.0, 1.0),
24        )
25    }
26}
27
28/// Colors spring as four independent channels, mirroring how Compose animates
29/// `Color` as an `AnimationVector4D`. Channels are clamped back into `[0, 1]`
30/// when the vector is rebuilt, so overshooting springs still produce valid
31/// colors (Compose's gamut coercion).
32impl SpringScalar for Color {
33    const DIMENSIONS: usize = 4;
34
35    fn dimension(&self, index: usize) -> f32 {
36        match index {
37            0 => self.0,
38            1 => self.1,
39            2 => self.2,
40            _ => self.3,
41        }
42    }
43
44    fn from_dimensions(dimensions: [f32; crate::animation::SPRING_MAX_DIMENSIONS]) -> Self {
45        Color(
46            dimensions[0].clamp(0.0, 1.0),
47            dimensions[1].clamp(0.0, 1.0),
48            dimensions[2].clamp(0.0, 1.0),
49            dimensions[3].clamp(0.0, 1.0),
50        )
51    }
52}
53
54/// Fire-and-forget color animation. Returns a [`State`] whose value is
55/// updated by animations towards the provided `target` whenever `target`
56/// changes.
57///
58/// Mirrors Jetpack Compose:
59/// `animateColorAsState(targetValue, animationSpec, label)`.
60///
61/// The interpolation happens linearly per RGBA channel, including alpha (see
62/// [`Lerp`] for [`Color`] for how this relates to Compose's Oklab lerp).
63#[track_caller]
64pub fn animateColorAsState(target: Color, animation: AnimationType, label: &str) -> State<Color> {
65    animateValueAsState(target, animation, label)
66}
67
68#[cfg(test)]
69#[path = "tests/color_tests.rs"]
70mod tests;