Skip to main content

gpui_ce_motion/
color.rs

1//! Linear-light RGBA interpolation and interruptible color motion.
2use crate::{MotionValue, Transition};
3use std::time::Duration;
4
5/// A linear-light RGBA color, constructed and read as sRGB channels in zero through one.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct Color {
8    linear: [f64; 4],
9}
10
11impl Color {
12    /// Validates sRGB channels and alpha. Panics unless all are in zero through one.
13    pub fn rgba(r: f64, g: f64, b: f64, a: f64) -> Self {
14        assert!(
15            [r, g, b, a].iter().all(|v| (0.0..=1.0).contains(v)),
16            "invalid color"
17        );
18        let linear = |v: f64| {
19            if v <= 0.04045 {
20                v / 12.92
21            } else {
22                ((v + 0.055) / 1.055).powf(2.4)
23            }
24        };
25
26        Self {
27            linear: [linear(r), linear(g), linear(b), a],
28        }
29    }
30
31    /// Creates an opaque color from `0xRRGGBB`.
32    pub fn rgb(hex: u32) -> Self {
33        Self::rgba(
34            f64::from((hex >> 16) & 255) / 255.0,
35            f64::from((hex >> 8) & 255) / 255.0,
36            f64::from(hex & 255) / 255.0,
37            1.0,
38        )
39    }
40
41    /// Reads clamped sRGB channels and alpha.
42    pub fn channels(self) -> [f32; 4] {
43        let srgb = |v: f64| {
44            let v = v.clamp(0.0, 1.0);
45
46            if v <= 0.0031308 {
47                12.92 * v
48            } else {
49                1.055 * v.powf(1.0 / 2.4) - 0.055
50            }
51        };
52
53        [
54            srgb(self.linear[0]) as f32,
55            srgb(self.linear[1]) as f32,
56            srgb(self.linear[2]) as f32,
57            self.linear[3].clamp(0.0, 1.0) as f32,
58        ]
59    }
60
61    /// Mixes in linear light; finite progress is clamped to zero through one.
62    pub fn mix(self, other: Self, progress: f64) -> Self {
63        assert!(progress.is_finite(), "invalid color progress");
64        let t = progress.clamp(0.0, 1.0);
65
66        Self {
67            linear: std::array::from_fn(|ix| {
68                self.linear[ix] + (other.linear[ix] - self.linear[ix]) * t
69            }),
70        }
71    }
72}
73
74/// Retained, interruptible color motion. Each linear-light channel retains spring velocity.
75#[derive(Debug)]
76pub struct ColorState {
77    channels: [MotionValue; 4],
78}
79
80impl ColorState {
81    /// Creates a resting color.
82    pub fn new(color: Color) -> Self {
83        Self {
84            channels: color.linear.map(MotionValue::new),
85        }
86    }
87
88    /// Samples a target color with no heap allocation. Returns color and whether motion is active.
89    pub fn sample(
90        &mut self,
91        color: Color,
92        transition: Transition,
93        now: Duration,
94        reduced_motion: bool,
95    ) -> (Color, bool) {
96        let mut active = false;
97
98        let linear = std::array::from_fn(|ix| {
99            self.channels[ix].animate(color.linear[ix], transition, now);
100            let frame = self.channels[ix].sample(now, reduced_motion);
101
102            active |= frame.is_animating();
103
104            frame.value()
105        });
106
107        (Color { linear }, active)
108    }
109}
110
111impl crate::MotionData<4> for Color {
112    fn channels(self) -> [f64; 4] {
113        self.linear
114    }
115
116    fn with_channels(self, values: [f64; 4]) -> Self {
117        Self { linear: values }
118    }
119
120    fn compatible(self, _: Self) -> bool {
121        true
122    }
123}