1use animato_core::Interpolate;
4use palette::{FromColor, IntoColor, Lab, LinSrgb, Mix, Oklch};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Copy, Debug, PartialEq)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15pub struct InLab<C>(pub C);
16
17#[derive(Clone, Copy, Debug, PartialEq)]
22#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
23pub struct InOklch<C>(pub C);
24
25#[derive(Clone, Copy, Debug, PartialEq)]
29#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
30pub struct InLinear<C>(pub C);
31
32impl<C> InLab<C> {
33 #[inline]
35 pub const fn new(color: C) -> Self {
36 Self(color)
37 }
38
39 #[inline]
41 pub const fn as_inner(&self) -> &C {
42 &self.0
43 }
44
45 #[inline]
47 pub fn into_inner(self) -> C {
48 self.0
49 }
50}
51
52impl<C> InOklch<C> {
53 #[inline]
55 pub const fn new(color: C) -> Self {
56 Self(color)
57 }
58
59 #[inline]
61 pub const fn as_inner(&self) -> &C {
62 &self.0
63 }
64
65 #[inline]
67 pub fn into_inner(self) -> C {
68 self.0
69 }
70}
71
72impl<C> InLinear<C> {
73 #[inline]
75 pub const fn new(color: C) -> Self {
76 Self(color)
77 }
78
79 #[inline]
81 pub const fn as_inner(&self) -> &C {
82 &self.0
83 }
84
85 #[inline]
87 pub fn into_inner(self) -> C {
88 self.0
89 }
90}
91
92impl<C> Interpolate for InLab<C>
93where
94 C: Clone + IntoColor<Lab> + FromColor<Lab> + 'static,
95{
96 #[inline]
97 fn lerp(&self, other: &Self, t: f32) -> Self {
98 let t = clamp01(t);
99 let start: Lab = self.0.clone().into_color();
100 let end: Lab = other.0.clone().into_color();
101 Self(C::from_color(start.mix(end, t)))
102 }
103}
104
105impl<C> Interpolate for InOklch<C>
106where
107 C: Clone + IntoColor<Oklch> + FromColor<Oklch> + 'static,
108{
109 #[inline]
110 fn lerp(&self, other: &Self, t: f32) -> Self {
111 let t = clamp01(t);
112 let start: Oklch = self.0.clone().into_color();
113 let end: Oklch = other.0.clone().into_color();
114 Self(C::from_color(start.mix(end, t)))
115 }
116}
117
118impl<C> Interpolate for InLinear<C>
119where
120 C: Clone + IntoColor<LinSrgb> + FromColor<LinSrgb> + 'static,
121{
122 #[inline]
123 fn lerp(&self, other: &Self, t: f32) -> Self {
124 let t = clamp01(t);
125 let start: LinSrgb = self.0.clone().into_color();
126 let end: LinSrgb = other.0.clone().into_color();
127 Self(C::from_color(start.mix(end, t)))
128 }
129}
130
131#[inline]
132fn clamp01(t: f32) -> f32 {
133 t.clamp(0.0, 1.0)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139 use palette::Srgb;
140
141 fn approx(a: f32, b: f32) -> bool {
142 (a - b).abs() <= 0.0001
143 }
144
145 fn assert_rgb_close(a: Srgb, b: Srgb) {
146 assert!(approx(a.red, b.red), "red: {} != {}", a.red, b.red);
147 assert!(
148 approx(a.green, b.green),
149 "green: {} != {}",
150 a.green,
151 b.green
152 );
153 assert!(approx(a.blue, b.blue), "blue: {} != {}", a.blue, b.blue);
154 }
155
156 #[test]
157 fn lab_interpolation_keeps_endpoints() {
158 let red = Srgb::new(1.0, 0.0, 0.0);
159 let blue = Srgb::new(0.0, 0.0, 1.0);
160
161 assert_rgb_close(
162 InLab::new(red).lerp(&InLab::new(blue), 0.0).into_inner(),
163 red,
164 );
165 assert_rgb_close(
166 InLab::new(red).lerp(&InLab::new(blue), 1.0).into_inner(),
167 blue,
168 );
169 }
170
171 #[test]
172 fn lab_midpoint_between_red_and_blue_is_not_muddy_brown() {
173 let red = InLab::new(Srgb::new(1.0, 0.0, 0.0));
174 let blue = InLab::new(Srgb::new(0.0, 0.0, 1.0));
175 let midpoint = red.lerp(&blue, 0.5).into_inner();
176
177 assert!(midpoint.red > 0.45);
178 assert!(midpoint.blue > 0.45);
179 assert!(midpoint.green < 0.35);
180 }
181
182 #[test]
183 fn linear_and_lab_midpoints_differ() {
184 let red = Srgb::new(1.0, 0.0, 0.0);
185 let blue = Srgb::new(0.0, 0.0, 1.0);
186
187 let lab = InLab::new(red).lerp(&InLab::new(blue), 0.5).into_inner();
188 let linear = InLinear::new(red)
189 .lerp(&InLinear::new(blue), 0.5)
190 .into_inner();
191
192 assert!((lab.red - linear.red).abs() > 0.01 || (lab.blue - linear.blue).abs() > 0.01);
193 }
194
195 #[test]
196 fn oklch_midpoint_is_finite() {
197 let red = InOklch::new(Srgb::new(1.0, 0.0, 0.0));
198 let blue = InOklch::new(Srgb::new(0.0, 0.0, 1.0));
199 let midpoint = red.lerp(&blue, 0.5).into_inner();
200
201 assert!(midpoint.red.is_finite());
202 assert!(midpoint.green.is_finite());
203 assert!(midpoint.blue.is_finite());
204 assert!((0.0..=1.0).contains(&midpoint.red));
205 assert!((0.0..=1.0).contains(&midpoint.green));
206 assert!((0.0..=1.0).contains(&midpoint.blue));
207 }
208
209 #[test]
210 fn interpolation_factor_is_clamped() {
211 let red = Srgb::new(1.0, 0.0, 0.0);
212 let blue = Srgb::new(0.0, 0.0, 1.0);
213
214 let before = InLinear::new(red)
215 .lerp(&InLinear::new(blue), -1.0)
216 .into_inner();
217 let after = InLinear::new(red)
218 .lerp(&InLinear::new(blue), 2.0)
219 .into_inner();
220
221 assert_rgb_close(before, red);
222 assert_rgb_close(after, blue);
223 }
224}