1use crate::{Hsl, Lab, Lch, LinearRgb, Oklab, Oklch, Srgb, XyzD50, XyzD65};
2
3#[derive(Debug, Clone, Copy, PartialEq, Default)]
5pub enum HueInterpolation {
6 #[default]
7 Shorter,
8 Longer,
9 Increasing,
10 Decreasing,
11}
12
13pub trait ColorMix<T, U>: Sized
19where
20 T: Into<Self>,
21 U: Into<Self>,
22{
23 fn mix(first: T, second: U, percentage: f64) -> Self;
24}
25
26pub trait ColorMixPolar<T, U>: Sized
33where
34 T: Into<Self>,
35 U: Into<Self>,
36{
37 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self;
38}
39
40pub fn interpolate_hue(h1: f64, h2: f64, t: f64, interpolation: HueInterpolation) -> f64 {
43 let (h1, h2) = (h1.rem_euclid(360.0), h2.rem_euclid(360.0));
44
45 let diff = match interpolation {
46 HueInterpolation::Shorter => {
47 let d = h2 - h1;
48 if d.abs() <= 180.0 {
49 d
50 } else if d > 180.0 {
51 d - 360.0
52 } else {
53 d + 360.0
54 }
55 }
56 HueInterpolation::Longer => {
57 let d = h2 - h1;
58 if d.abs() > 180.0 {
59 d
60 } else if d > 0.0 {
61 d - 360.0
62 } else {
63 d + 360.0
64 }
65 }
66 HueInterpolation::Increasing => {
67 let mut d = h2 - h1;
68 if d < 0.0 {
69 d += 360.0;
70 }
71 d
72 }
73 HueInterpolation::Decreasing => {
74 let mut d = h2 - h1;
75 if d > 0.0 {
76 d -= 360.0;
77 }
78 d
79 }
80 };
81
82 (h1 + diff * t).rem_euclid(360.0)
83}
84
85mod sealed {
86 pub trait PolarColor {}
87}
88
89impl sealed::PolarColor for Hsl {}
90impl sealed::PolarColor for Lch {}
91impl sealed::PolarColor for Oklch {}
92
93impl<T, U, V> ColorMix<T, U> for V
94where
95 V: ColorMixPolar<T, U> + sealed::PolarColor + Sized,
96 T: Into<V>,
97 U: Into<V>,
98{
99 fn mix(first: T, second: U, percentage: f64) -> V {
100 ColorMixPolar::mix_polar(first, second, percentage, HueInterpolation::Shorter)
101 }
102}
103
104impl<T, U> ColorMix<T, U> for Srgb
105where
106 Self: From<T> + From<U>,
107{
108 fn mix(first: T, second: U, percentage: f64) -> Self {
109 let first: Self = first.into();
110 let second: Self = second.into();
111 let t = percentage / 100.0;
112 let r = first.red as f64 * (1.0 - t) + second.red as f64 * t;
113 let g = first.green as f64 * (1.0 - t) + second.green as f64 * t;
114 let b = first.blue as f64 * (1.0 - t) + second.blue as f64 * t;
115 let a = first.alpha as f64 * (1.0 - t) + second.alpha as f64 * t;
116 Srgb::new(r.round() as u8, g.round() as u8, b.round() as u8, a as f32)
117 }
118}
119
120impl<T, U> ColorMix<T, U> for LinearRgb
121where
122 Self: From<T> + From<U>,
123{
124 fn mix(first: T, second: U, percentage: f64) -> Self {
125 let first: Self = first.into();
126 let second: Self = second.into();
127 let t = percentage / 100.0;
128 let r = first.red * (1.0 - t) + second.red * t;
129 let g = first.green * (1.0 - t) + second.green * t;
130 let b = first.blue * (1.0 - t) + second.blue * t;
131 let a = first.alpha as f64 * (1.0 - t) + second.alpha as f64 * t;
132 LinearRgb::new(r, g, b, a as f32)
133 }
134}
135
136impl<T, U> ColorMixPolar<T, U> for Hsl
137where
138 Self: From<T> + From<U>,
139{
140 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self {
141 let first: Self = first.into();
142 let second: Self = second.into();
143 let t = percentage / 100.0;
144 let h = interpolate_hue(first.hue as f64, second.hue as f64, t, hue_interpolation);
145 let s = first.saturation as f64 * (1.0 - t) + second.saturation as f64 * t;
146 let l = first.lightness as f64 * (1.0 - t) + second.lightness as f64 * t;
147 let a = first.alpha as f64 * (1.0 - t) + second.alpha as f64 * t;
148 Hsl::new(h as f32, s as f32, l as f32, a as f32)
149 }
150}
151
152impl<T, U> ColorMix<T, U> for Lab
153where
154 Self: From<T> + From<U>,
155{
156 fn mix(first: T, second: U, percentage: f64) -> Self {
157 let first: Self = first.into();
158 let second: Self = second.into();
159 let t = percentage / 100.0;
160 let l = first.lightness * (1.0 - t) + second.lightness * t;
161 let a = first.a * (1.0 - t) + second.a * t;
162 let b = first.b * (1.0 - t) + second.b * t;
163 let alpha = first.alpha as f64 * (1.0 - t) + second.alpha as f64 * t;
164 Lab::new(l, a, b, alpha as f32)
165 }
166}
167
168impl<T, U> ColorMixPolar<T, U> for Lch
169where
170 Self: From<T> + From<U>,
171{
172 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self {
173 let first: Self = first.into();
174 let second: Self = second.into();
175 let t = percentage / 100.0;
176 let l = first.lightness * (1.0 - t) + second.lightness * t;
177 let c = first.chroma * (1.0 - t) + second.chroma * t;
178 let h = interpolate_hue(first.hue, second.hue, t, hue_interpolation);
179 let a = first.alpha as f64 * (1.0 - t) + second.alpha as f64 * t;
180 Lch::new(l, c, h, a as f32)
181 }
182}
183
184impl<T, U> ColorMix<T, U> for Oklab
185where
186 Self: From<T> + From<U>,
187{
188 fn mix(first: T, second: U, percentage: f64) -> Self {
189 let first: Self = first.into();
190 let second: Self = second.into();
191 let t = percentage / 100.0;
192 let l = first.lightness * (1.0 - t) + second.lightness * t;
193 let a = first.a * (1.0 - t) + second.a * t;
194 let b = first.b * (1.0 - t) + second.b * t;
195 let alpha = first.alpha as f64 * (1.0 - t) + second.alpha as f64 * t;
196 Oklab::new(l, a, b, alpha as f32)
197 }
198}
199
200impl<T, U> ColorMixPolar<T, U> for Oklch
201where
202 Self: From<T> + From<U>,
203{
204 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self {
205 let first: Self = first.into();
206 let second: Self = second.into();
207 let t = percentage / 100.0;
208 let l = first.lightness * (1.0 - t) + second.lightness * t;
209 let c = first.chroma * (1.0 - t) + second.chroma * t;
210 let h = interpolate_hue(first.hue, second.hue, t, hue_interpolation);
211 let a = first.alpha as f64 * (1.0 - t) + second.alpha as f64 * t;
212 Oklch::new(l, c, h, a as f32)
213 }
214}
215
216impl<T, U> ColorMix<T, U> for XyzD50
217where
218 Self: From<T> + From<U>,
219{
220 fn mix(first: T, second: U, percentage: f64) -> Self {
221 let first: Self = first.into();
222 let second: Self = second.into();
223 let t = percentage / 100.0;
224 let x = first.x * (1.0 - t) + second.x * t;
225 let y = first.y * (1.0 - t) + second.y * t;
226 let z = first.z * (1.0 - t) + second.z * t;
227 let a = first.alpha as f64 * (1.0 - t) + second.alpha as f64 * t;
228 XyzD50::new(x, y, z, a as f32)
229 }
230}
231
232impl<T, U> ColorMix<T, U> for XyzD65
233where
234 Self: From<T> + From<U>,
235{
236 fn mix(first: T, second: U, percentage: f64) -> Self {
237 let first: Self = first.into();
238 let second: Self = second.into();
239 let t = percentage / 100.0;
240 let x = first.x * (1.0 - t) + second.x * t;
241 let y = first.y * (1.0 - t) + second.y * t;
242 let z = first.z * (1.0 - t) + second.z * t;
243 let a = first.alpha as f64 * (1.0 - t) + second.alpha as f64 * t;
244 XyzD65::new(x, y, z, a as f32)
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use crate::*;
252
253 macro_rules! assert_close_to {
254 ($a: expr, $b: expr) => {
255 assert!($a.close_to($b, COLOR_EPSILON), "Expected {:?} to be (closely) equal to {:?}", $a, $b);
256 };
257 }
258
259 #[test]
260 fn test_basic_mix() {
261 let red = Srgb::new(255, 0, 0, 100.0);
262 let blue = Srgb::new(0, 0, 255, 100.0);
263 assert_close_to!(Srgb::mix(red, blue, 50.0), Srgb::new(128, 0, 128, 100.0));
264 }
265
266 #[test]
267 fn test_mix_named_in_oklab() {
268 assert_close_to!(
269 Oklch::mix(Named::Rebeccapurple, Named::Hotpink, 50.0),
270 Oklch::new(0.5842845967725198, 0.17868573405015944, 327.6838446374328, 100.0)
271 );
272 }
273
274 #[test]
275 fn test_mix_named_in_hsl_polar() {
276 assert_close_to!(Hsl::mix(Named::Rebeccapurple, Named::Hotpink, 50.0), Hsl::new(300.0, 75.0, 55.294117, 100.0));
277 assert_close_to!(
278 Hsl::mix_polar(Named::Rebeccapurple, Named::Hotpink, 50.0, HueInterpolation::Longer),
279 Hsl::new(120.0, 75.0, 55.294117, 100.0)
280 );
281 assert_close_to!(
282 Hsl::mix_polar(Named::Rebeccapurple, Named::Hotpink, 50.0, HueInterpolation::Decreasing),
283 Hsl::new(120.0, 75.0, 55.294117, 100.0)
284 );
285 assert_close_to!(
286 Hsl::mix_polar(Named::Rebeccapurple, Named::Hotpink, 50.0, HueInterpolation::Increasing),
287 Hsl::new(300.0, 75.0, 55.294117, 100.0)
288 );
289 }
290
291 #[test]
292 fn test_alpha_mixing() {
293 let color1 = Srgb::new(255, 0, 0, 80.0);
294 let color2 = Srgb::new(0, 0, 255, 40.0);
295
296 let mixed = Srgb::mix(color1, color2, 50.0);
297 assert_eq!(mixed.red, 128);
298 assert_eq!(mixed.green, 0);
299 assert_eq!(mixed.blue, 128);
300 assert_eq!(mixed.alpha, 60.0);
301 }
302}