1use crate::{
2 A98Rgb, Channel, DisplayP3, Hsl, Hwb, Lab, Lch, LinearRgb, Oklab, Oklch, PolarLayout, ProphotoRgb, Rec2020, Srgb,
3 XyzD50, XyzD65,
4};
5
6#[derive(Debug, Clone, Copy, PartialEq, Default)]
8pub enum HueInterpolation {
9 #[default]
10 Shorter,
11 Longer,
12 Increasing,
13 Decreasing,
14}
15
16pub trait ColorMix<T, U>: Sized
27where
28 T: Into<Self>,
29 U: Into<Self>,
30{
31 fn mix(first: T, second: U, percentage: f64) -> Self;
32}
33
34pub trait ColorMixPolar<T, U>: Sized
44where
45 T: Into<Self>,
46 U: Into<Self>,
47{
48 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self;
49}
50
51fn premultiply_lerp(c1: f64, a1: f64, c2: f64, a2: f64, t: f64, result_alpha: f64) -> f64 {
59 if result_alpha == 0.0 {
60 return c1 * (1.0 - t) + c2 * t;
61 }
62 let pm1 = c1 * a1;
63 let pm2 = c2 * a2;
64 (pm1 * (1.0 - t) + pm2 * t) / result_alpha
65}
66
67pub fn interpolate_hue(h1: f64, h2: f64, t: f64, interpolation: HueInterpolation) -> f64 {
70 let (h1, h2) = (h1.rem_euclid(360.0), h2.rem_euclid(360.0));
71
72 let diff = match interpolation {
73 HueInterpolation::Shorter => {
74 let d = h2 - h1;
75 if d.abs() <= 180.0 {
76 d
77 } else if d > 180.0 {
78 d - 360.0
79 } else {
80 d + 360.0
81 }
82 }
83 HueInterpolation::Longer => {
84 let d = h2 - h1;
85 if d.abs() > 180.0 {
86 d
87 } else if d > 0.0 {
88 d - 360.0
89 } else {
90 d + 360.0
91 }
92 }
93 HueInterpolation::Increasing => {
94 let mut d = h2 - h1;
95 if d < 0.0 {
96 d += 360.0;
97 }
98 d
99 }
100 HueInterpolation::Decreasing => {
101 let mut d = h2 - h1;
102 if d > 0.0 {
103 d -= 360.0;
104 }
105 d
106 }
107 };
108
109 (h1 + diff * t).rem_euclid(360.0)
110}
111
112pub fn mix_channels(
118 first: [Channel; 4],
119 second: [Channel; 4],
120 percentage: f64,
121 hue_index: Option<usize>,
122 hue_interpolation: HueInterpolation,
123) -> [Channel; 4] {
124 let t = percentage / 100.0;
125 let mut out = [Channel::default(); 4];
126
127 let (alpha_a, alpha_b) = match (first[3], second[3]) {
129 (None, None) => (0.0, 0.0),
130 (None, Some(b)) => (b, b),
131 (Some(a), None) => (a, a),
132 (Some(a), Some(b)) => (a, b),
133 };
134 let a1 = alpha_a / 100.0;
135 let a2 = alpha_b / 100.0;
136 let alpha = a1 * (1.0 - t) + a2 * t;
137 out[3] = if first[3].or(second[3]).is_none() { None } else { Some(alpha * 100.0) };
138
139 for i in 0..3 {
140 let (l, r) = (first[i], second[i]);
141 let (a, b) = match (l, r) {
142 (None, None) => (None, None),
143 (None, Some(b)) => (Some(b), Some(b)),
144 (Some(a), None) => (Some(a), Some(a)),
145 (Some(a), Some(b)) => (Some(a), Some(b)),
146 };
147 out[i] = match (a, b) {
148 (None, _) | (_, None) => None,
149 (Some(av), Some(bv)) => Some(if hue_index == Some(i) {
150 interpolate_hue(av, bv, t, hue_interpolation)
151 } else {
152 premultiply_lerp(av, a1, bv, a2, t, alpha)
153 }),
154 };
155 }
156
157 out
158}
159
160fn mix_in<C, T, U>(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> C
162where
163 C: From<T> + From<U> + From<[Channel; 4]> + Into<[Channel; 4]> + PolarLayout,
164{
165 let first: [Channel; 4] = C::from(first).into();
166 let second: [Channel; 4] = C::from(second).into();
167 let out = mix_channels(first, second, percentage, C::HUE_INDEX, hue_interpolation);
168 C::from(out)
169}
170
171mod sealed {
172 pub trait PolarColor {}
173}
174
175impl sealed::PolarColor for Hsl {}
176impl sealed::PolarColor for Hwb {}
177impl sealed::PolarColor for Lch {}
178impl sealed::PolarColor for Oklch {}
179
180impl<T, U, V> ColorMix<T, U> for V
181where
182 V: ColorMixPolar<T, U> + sealed::PolarColor + Sized,
183 T: Into<V>,
184 U: Into<V>,
185{
186 fn mix(first: T, second: U, percentage: f64) -> V {
187 ColorMixPolar::mix_polar(first, second, percentage, HueInterpolation::Shorter)
188 }
189}
190
191macro_rules! impl_color_mix {
193 ($ty:ty) => {
194 impl<T, U> ColorMix<T, U> for $ty
195 where
196 Self: From<T> + From<U>,
197 {
198 fn mix(first: T, second: U, percentage: f64) -> Self {
199 mix_in::<Self, T, U>(first, second, percentage, HueInterpolation::Shorter)
200 }
201 }
202 };
203}
204
205macro_rules! impl_color_mix_polar {
207 ($ty:ty) => {
208 impl<T, U> ColorMixPolar<T, U> for $ty
209 where
210 Self: From<T> + From<U>,
211 {
212 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self {
213 mix_in::<Self, T, U>(first, second, percentage, hue_interpolation)
214 }
215 }
216 };
217}
218
219impl_color_mix!(Srgb);
220impl_color_mix!(LinearRgb);
221impl_color_mix!(A98Rgb);
222impl_color_mix!(DisplayP3);
223impl_color_mix!(ProphotoRgb);
224impl_color_mix!(Rec2020);
225impl_color_mix!(Lab);
226impl_color_mix!(Oklab);
227impl_color_mix!(XyzD50);
228impl_color_mix!(XyzD65);
229impl_color_mix_polar!(Hsl);
230impl_color_mix_polar!(Hwb);
231impl_color_mix_polar!(Lch);
232impl_color_mix_polar!(Oklch);
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use crate::*;
238
239 macro_rules! assert_close_to {
240 ($a: expr, $b: expr) => {
241 assert!($a.close_to($b, COLOR_EPSILON), "Expected {:?} to be (closely) equal to {:?}", $a, $b);
242 };
243 }
244
245 #[test]
246 fn test_basic_mix() {
247 let red = Srgb::new(255, 0, 0, 100.0);
248 let blue = Srgb::new(0, 0, 255, 100.0);
249 assert_close_to!(Srgb::mix(red, blue, 50.0), Srgb::new(128, 0, 128, 100.0));
250 }
251
252 #[test]
253 fn test_mix_named_in_oklab() {
254 assert_close_to!(
255 Oklch::mix(Named::Rebeccapurple, Named::Hotpink, 50.0),
256 Oklch::new(0.5842845967725198, 0.17868573405015944, 327.6838446374328, 100.0)
257 );
258 }
259
260 #[test]
261 fn test_mix_named_in_hsl_polar() {
262 assert_close_to!(Hsl::mix(Named::Rebeccapurple, Named::Hotpink, 50.0), Hsl::new(300.0, 75.0, 55.294117, 100.0));
263 assert_close_to!(
264 Hsl::mix_polar(Named::Rebeccapurple, Named::Hotpink, 50.0, HueInterpolation::Longer),
265 Hsl::new(120.0, 75.0, 55.294117, 100.0)
266 );
267 assert_close_to!(
268 Hsl::mix_polar(Named::Rebeccapurple, Named::Hotpink, 50.0, HueInterpolation::Decreasing),
269 Hsl::new(120.0, 75.0, 55.294117, 100.0)
270 );
271 assert_close_to!(
272 Hsl::mix_polar(Named::Rebeccapurple, Named::Hotpink, 50.0, HueInterpolation::Increasing),
273 Hsl::new(300.0, 75.0, 55.294117, 100.0)
274 );
275 }
276
277 #[test]
278 fn test_alpha_mixing() {
279 let color1 = Srgb::new(255, 0, 0, 80.0);
280 let color2 = Srgb::new(0, 0, 255, 40.0);
281
282 let mixed = Srgb::mix(color1, color2, 50.0);
283 assert_eq!(mixed.red, 170);
284 assert_eq!(mixed.green, 0);
285 assert_eq!(mixed.blue, 85);
286 assert_eq!(mixed.alpha, 60.0);
287 }
288
289 #[test]
290 fn test_hwb_mix() {
291 let red = Hwb::new(0.0, 0.0, 0.0, 100.0);
294 let blue = Hwb::new(240.0, 0.0, 0.0, 100.0);
295 let mixed = Hwb::mix(red, blue, 50.0);
296 assert_close_to!(mixed, Hwb::new(300.0, 0.0, 0.0, 100.0));
297 }
298
299 #[test]
300 fn test_hwb_mix_polar() {
301 let red = Hwb::new(0.0, 0.0, 0.0, 100.0);
303 let blue = Hwb::new(240.0, 0.0, 0.0, 100.0);
304 assert_close_to!(Hwb::mix_polar(red, blue, 50.0, HueInterpolation::Longer), Hwb::new(120.0, 0.0, 0.0, 100.0));
305 }
306
307 #[test]
308 fn test_a98_rgb_mix() {
309 let c1 = A98Rgb::new(1.0, 0.0, 0.0, 100.0);
310 let c2 = A98Rgb::new(0.0, 0.0, 1.0, 100.0);
311 let mixed = A98Rgb::mix(c1, c2, 50.0);
312 assert_close_to!(mixed, A98Rgb::new(0.5, 0.0, 0.5, 100.0));
313 }
314
315 #[test]
317 fn test_premultiplied_alpha_lab() {
318 let c1 = Lab::new(10.0, 20.0, 30.0, 40.0);
319 let c2 = Lab::new(50.0, 60.0, 70.0, 80.0);
320 let mixed = Lab::mix(c1, c2, 50.0);
321 assert_close_to!(mixed, Lab::new(36.666664, 46.666664, 56.666664, 60.0));
322 }
323
324 #[test]
326 fn test_premultiplied_alpha_lab_25_75() {
327 let c1 = Lab::new(10.0, 20.0, 30.0, 40.0);
328 let c2 = Lab::new(50.0, 60.0, 70.0, 80.0);
329 let mixed = Lab::mix(c1, c2, 75.0);
331 assert_close_to!(mixed, Lab::new(44.285713, 54.285717, 64.28571, 70.0));
332 }
333
334 #[test]
336 fn test_premultiplied_alpha_oklch() {
337 let c1 = Oklch::new(0.1, 0.2, 30.0, 40.0);
338 let c2 = Oklch::new(0.5, 0.6, 70.0, 80.0);
339 let mixed = Oklch::mix(c1, c2, 50.0);
340 assert_close_to!(mixed, Oklch::new(0.36666664, 0.46666664, 50.0, 60.0));
341 }
342
343 #[test]
345 fn test_premultiplied_alpha_opaque_same_as_simple() {
346 let c1 = Lab::new(10.0, 20.0, 30.0, 100.0);
347 let c2 = Lab::new(50.0, 60.0, 70.0, 100.0);
348 let mixed = Lab::mix(c1, c2, 50.0);
349 assert_close_to!(mixed, Lab::new(30.0, 40.0, 50.0, 100.0));
350 }
351
352 #[test]
354 fn test_none_channel_substitution() {
355 let first = [None, Some(0.0), Some(0.0), Some(100.0)];
356 let second: [Channel; 4] = Srgb::new(200, 0, 0, 100.0).into();
357 let out = mix_channels(first, second, 50.0, Srgb::HUE_INDEX, HueInterpolation::Shorter);
358 let mixed: Srgb = out.into();
359 assert_eq!(mixed, Srgb::new(200, 0, 0, 100.0));
360 }
361}