1use crate::{
2 A98Rgb, DisplayP3, Hsl, Hwb, Lab, Lch, LinearRgb, Oklab, Oklch, ProphotoRgb, Rec2020, Srgb, XyzD50, XyzD65,
3};
4
5#[derive(Debug, Clone, Copy, PartialEq, Default)]
7pub enum HueInterpolation {
8 #[default]
9 Shorter,
10 Longer,
11 Increasing,
12 Decreasing,
13}
14
15pub trait ColorMix<T, U>: Sized
26where
27 T: Into<Self>,
28 U: Into<Self>,
29{
30 fn mix(first: T, second: U, percentage: f64) -> Self;
31}
32
33pub trait ColorMixPolar<T, U>: Sized
43where
44 T: Into<Self>,
45 U: Into<Self>,
46{
47 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self;
48}
49
50fn premultiply_lerp(c1: f64, a1: f64, c2: f64, a2: f64, t: f64, result_alpha: f64) -> f64 {
58 if result_alpha == 0.0 {
59 return c1 * (1.0 - t) + c2 * t;
60 }
61 let pm1 = c1 * a1;
62 let pm2 = c2 * a2;
63 (pm1 * (1.0 - t) + pm2 * t) / result_alpha
64}
65
66pub fn interpolate_hue(h1: f64, h2: f64, t: f64, interpolation: HueInterpolation) -> f64 {
69 let (h1, h2) = (h1.rem_euclid(360.0), h2.rem_euclid(360.0));
70
71 let diff = match interpolation {
72 HueInterpolation::Shorter => {
73 let d = h2 - h1;
74 if d.abs() <= 180.0 {
75 d
76 } else if d > 180.0 {
77 d - 360.0
78 } else {
79 d + 360.0
80 }
81 }
82 HueInterpolation::Longer => {
83 let d = h2 - h1;
84 if d.abs() > 180.0 {
85 d
86 } else if d > 0.0 {
87 d - 360.0
88 } else {
89 d + 360.0
90 }
91 }
92 HueInterpolation::Increasing => {
93 let mut d = h2 - h1;
94 if d < 0.0 {
95 d += 360.0;
96 }
97 d
98 }
99 HueInterpolation::Decreasing => {
100 let mut d = h2 - h1;
101 if d > 0.0 {
102 d -= 360.0;
103 }
104 d
105 }
106 };
107
108 (h1 + diff * t).rem_euclid(360.0)
109}
110
111mod sealed {
112 pub trait PolarColor {}
113}
114
115impl sealed::PolarColor for Hsl {}
116impl sealed::PolarColor for Hwb {}
117impl sealed::PolarColor for Lch {}
118impl sealed::PolarColor for Oklch {}
119
120impl<T, U, V> ColorMix<T, U> for V
121where
122 V: ColorMixPolar<T, U> + sealed::PolarColor + Sized,
123 T: Into<V>,
124 U: Into<V>,
125{
126 fn mix(first: T, second: U, percentage: f64) -> V {
127 ColorMixPolar::mix_polar(first, second, percentage, HueInterpolation::Shorter)
128 }
129}
130
131impl<T, U> ColorMix<T, U> for Srgb
132where
133 Self: From<T> + From<U>,
134{
135 fn mix(first: T, second: U, percentage: f64) -> Self {
136 let first: Self = first.into();
137 let second: Self = second.into();
138 let t = percentage / 100.0;
139 let a1 = first.alpha as f64 / 100.0;
140 let a2 = second.alpha as f64 / 100.0;
141 let a = a1 * (1.0 - t) + a2 * t;
142 let r = premultiply_lerp(first.red as f64, a1, second.red as f64, a2, t, a);
143 let g = premultiply_lerp(first.green as f64, a1, second.green as f64, a2, t, a);
144 let b = premultiply_lerp(first.blue as f64, a1, second.blue as f64, a2, t, a);
145 Srgb::new(r.round() as u8, g.round() as u8, b.round() as u8, (a * 100.0) as f32)
146 }
147}
148
149impl<T, U> ColorMix<T, U> for LinearRgb
150where
151 Self: From<T> + From<U>,
152{
153 fn mix(first: T, second: U, percentage: f64) -> Self {
154 let first: Self = first.into();
155 let second: Self = second.into();
156 let t = percentage / 100.0;
157 let a1 = first.alpha as f64 / 100.0;
158 let a2 = second.alpha as f64 / 100.0;
159 let a = a1 * (1.0 - t) + a2 * t;
160 let r = premultiply_lerp(first.red, a1, second.red, a2, t, a);
161 let g = premultiply_lerp(first.green, a1, second.green, a2, t, a);
162 let b = premultiply_lerp(first.blue, a1, second.blue, a2, t, a);
163 LinearRgb::new(r, g, b, (a * 100.0) as f32)
164 }
165}
166
167impl<T, U> ColorMixPolar<T, U> for Hsl
168where
169 Self: From<T> + From<U>,
170{
171 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self {
172 let first: Self = first.into();
173 let second: Self = second.into();
174 let t = percentage / 100.0;
175 let a1 = first.alpha as f64 / 100.0;
176 let a2 = second.alpha as f64 / 100.0;
177 let a = a1 * (1.0 - t) + a2 * t;
178 let h = interpolate_hue(first.hue as f64, second.hue as f64, t, hue_interpolation);
179 let s = premultiply_lerp(first.saturation as f64, a1, second.saturation as f64, a2, t, a);
180 let l = premultiply_lerp(first.lightness as f64, a1, second.lightness as f64, a2, t, a);
181 Hsl::new(h as f32, s as f32, l as f32, (a * 100.0) as f32)
182 }
183}
184
185impl<T, U> ColorMixPolar<T, U> for Hwb
186where
187 Self: From<T> + From<U>,
188{
189 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self {
190 let first: Self = first.into();
191 let second: Self = second.into();
192 let t = percentage / 100.0;
193 let a1 = first.alpha as f64 / 100.0;
194 let a2 = second.alpha as f64 / 100.0;
195 let a = a1 * (1.0 - t) + a2 * t;
196 let h = interpolate_hue(first.hue as f64, second.hue as f64, t, hue_interpolation);
197 let w = premultiply_lerp(first.whiteness as f64, a1, second.whiteness as f64, a2, t, a);
198 let b = premultiply_lerp(first.blackness as f64, a1, second.blackness as f64, a2, t, a);
199 Hwb::new(h as f32, w as f32, b as f32, (a * 100.0) as f32)
200 }
201}
202
203impl<T, U> ColorMix<T, U> for A98Rgb
204where
205 Self: From<T> + From<U>,
206{
207 fn mix(first: T, second: U, percentage: f64) -> Self {
208 let first: Self = first.into();
209 let second: Self = second.into();
210 let t = percentage / 100.0;
211 let a1 = first.alpha as f64 / 100.0;
212 let a2 = second.alpha as f64 / 100.0;
213 let a = a1 * (1.0 - t) + a2 * t;
214 let r = premultiply_lerp(first.red, a1, second.red, a2, t, a);
215 let g = premultiply_lerp(first.green, a1, second.green, a2, t, a);
216 let b = premultiply_lerp(first.blue, a1, second.blue, a2, t, a);
217 A98Rgb::new(r, g, b, (a * 100.0) as f32)
218 }
219}
220
221impl<T, U> ColorMix<T, U> for DisplayP3
222where
223 Self: From<T> + From<U>,
224{
225 fn mix(first: T, second: U, percentage: f64) -> Self {
226 let first: Self = first.into();
227 let second: Self = second.into();
228 let t = percentage / 100.0;
229 let a1 = first.alpha as f64 / 100.0;
230 let a2 = second.alpha as f64 / 100.0;
231 let a = a1 * (1.0 - t) + a2 * t;
232 let r = premultiply_lerp(first.red, a1, second.red, a2, t, a);
233 let g = premultiply_lerp(first.green, a1, second.green, a2, t, a);
234 let b = premultiply_lerp(first.blue, a1, second.blue, a2, t, a);
235 DisplayP3::new(r, g, b, (a * 100.0) as f32)
236 }
237}
238
239impl<T, U> ColorMix<T, U> for ProphotoRgb
240where
241 Self: From<T> + From<U>,
242{
243 fn mix(first: T, second: U, percentage: f64) -> Self {
244 let first: Self = first.into();
245 let second: Self = second.into();
246 let t = percentage / 100.0;
247 let a1 = first.alpha as f64 / 100.0;
248 let a2 = second.alpha as f64 / 100.0;
249 let a = a1 * (1.0 - t) + a2 * t;
250 let r = premultiply_lerp(first.red, a1, second.red, a2, t, a);
251 let g = premultiply_lerp(first.green, a1, second.green, a2, t, a);
252 let b = premultiply_lerp(first.blue, a1, second.blue, a2, t, a);
253 ProphotoRgb::new(r, g, b, (a * 100.0) as f32)
254 }
255}
256
257impl<T, U> ColorMix<T, U> for Rec2020
258where
259 Self: From<T> + From<U>,
260{
261 fn mix(first: T, second: U, percentage: f64) -> Self {
262 let first: Self = first.into();
263 let second: Self = second.into();
264 let t = percentage / 100.0;
265 let a1 = first.alpha as f64 / 100.0;
266 let a2 = second.alpha as f64 / 100.0;
267 let a = a1 * (1.0 - t) + a2 * t;
268 let r = premultiply_lerp(first.red, a1, second.red, a2, t, a);
269 let g = premultiply_lerp(first.green, a1, second.green, a2, t, a);
270 let b = premultiply_lerp(first.blue, a1, second.blue, a2, t, a);
271 Rec2020::new(r, g, b, (a * 100.0) as f32)
272 }
273}
274
275impl<T, U> ColorMix<T, U> for Lab
276where
277 Self: From<T> + From<U>,
278{
279 fn mix(first: T, second: U, percentage: f64) -> Self {
280 let first: Self = first.into();
281 let second: Self = second.into();
282 let t = percentage / 100.0;
283 let a1 = first.alpha as f64 / 100.0;
284 let a2 = second.alpha as f64 / 100.0;
285 let a = a1 * (1.0 - t) + a2 * t;
286 let l = premultiply_lerp(first.lightness, a1, second.lightness, a2, t, a);
287 let ab_a = premultiply_lerp(first.a, a1, second.a, a2, t, a);
288 let ab_b = premultiply_lerp(first.b, a1, second.b, a2, t, a);
289 Lab::new(l, ab_a, ab_b, (a * 100.0) as f32)
290 }
291}
292
293impl<T, U> ColorMixPolar<T, U> for Lch
294where
295 Self: From<T> + From<U>,
296{
297 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self {
298 let first: Self = first.into();
299 let second: Self = second.into();
300 let t = percentage / 100.0;
301 let a1 = first.alpha as f64 / 100.0;
302 let a2 = second.alpha as f64 / 100.0;
303 let a = a1 * (1.0 - t) + a2 * t;
304 let l = premultiply_lerp(first.lightness, a1, second.lightness, a2, t, a);
305 let c = premultiply_lerp(first.chroma, a1, second.chroma, a2, t, a);
306 let h = interpolate_hue(first.hue, second.hue, t, hue_interpolation);
307 Lch::new(l, c, h, (a * 100.0) as f32)
308 }
309}
310
311impl<T, U> ColorMix<T, U> for Oklab
312where
313 Self: From<T> + From<U>,
314{
315 fn mix(first: T, second: U, percentage: f64) -> Self {
316 let first: Self = first.into();
317 let second: Self = second.into();
318 let t = percentage / 100.0;
319 let a1 = first.alpha as f64 / 100.0;
320 let a2 = second.alpha as f64 / 100.0;
321 let a = a1 * (1.0 - t) + a2 * t;
322 let l = premultiply_lerp(first.lightness, a1, second.lightness, a2, t, a);
323 let ab_a = premultiply_lerp(first.a, a1, second.a, a2, t, a);
324 let ab_b = premultiply_lerp(first.b, a1, second.b, a2, t, a);
325 Oklab::new(l, ab_a, ab_b, (a * 100.0) as f32)
326 }
327}
328
329impl<T, U> ColorMixPolar<T, U> for Oklch
330where
331 Self: From<T> + From<U>,
332{
333 fn mix_polar(first: T, second: U, percentage: f64, hue_interpolation: HueInterpolation) -> Self {
334 let first: Self = first.into();
335 let second: Self = second.into();
336 let t = percentage / 100.0;
337 let a1 = first.alpha as f64 / 100.0;
338 let a2 = second.alpha as f64 / 100.0;
339 let a = a1 * (1.0 - t) + a2 * t;
340 let l = premultiply_lerp(first.lightness, a1, second.lightness, a2, t, a);
341 let c = premultiply_lerp(first.chroma, a1, second.chroma, a2, t, a);
342 let h = interpolate_hue(first.hue, second.hue, t, hue_interpolation);
343 Oklch::new(l, c, h, (a * 100.0) as f32)
344 }
345}
346
347impl<T, U> ColorMix<T, U> for XyzD50
348where
349 Self: From<T> + From<U>,
350{
351 fn mix(first: T, second: U, percentage: f64) -> Self {
352 let first: Self = first.into();
353 let second: Self = second.into();
354 let t = percentage / 100.0;
355 let a1 = first.alpha as f64 / 100.0;
356 let a2 = second.alpha as f64 / 100.0;
357 let a = a1 * (1.0 - t) + a2 * t;
358 let x = premultiply_lerp(first.x, a1, second.x, a2, t, a);
359 let y = premultiply_lerp(first.y, a1, second.y, a2, t, a);
360 let z = premultiply_lerp(first.z, a1, second.z, a2, t, a);
361 XyzD50::new(x, y, z, (a * 100.0) as f32)
362 }
363}
364
365impl<T, U> ColorMix<T, U> for XyzD65
366where
367 Self: From<T> + From<U>,
368{
369 fn mix(first: T, second: U, percentage: f64) -> Self {
370 let first: Self = first.into();
371 let second: Self = second.into();
372 let t = percentage / 100.0;
373 let a1 = first.alpha as f64 / 100.0;
374 let a2 = second.alpha as f64 / 100.0;
375 let a = a1 * (1.0 - t) + a2 * t;
376 let x = premultiply_lerp(first.x, a1, second.x, a2, t, a);
377 let y = premultiply_lerp(first.y, a1, second.y, a2, t, a);
378 let z = premultiply_lerp(first.z, a1, second.z, a2, t, a);
379 XyzD65::new(x, y, z, (a * 100.0) as f32)
380 }
381}
382
383#[cfg(test)]
384mod tests {
385 use super::*;
386 use crate::*;
387
388 macro_rules! assert_close_to {
389 ($a: expr, $b: expr) => {
390 assert!($a.close_to($b, COLOR_EPSILON), "Expected {:?} to be (closely) equal to {:?}", $a, $b);
391 };
392 }
393
394 #[test]
395 fn test_basic_mix() {
396 let red = Srgb::new(255, 0, 0, 100.0);
397 let blue = Srgb::new(0, 0, 255, 100.0);
398 assert_close_to!(Srgb::mix(red, blue, 50.0), Srgb::new(128, 0, 128, 100.0));
399 }
400
401 #[test]
402 fn test_mix_named_in_oklab() {
403 assert_close_to!(
404 Oklch::mix(Named::Rebeccapurple, Named::Hotpink, 50.0),
405 Oklch::new(0.5842845967725198, 0.17868573405015944, 327.6838446374328, 100.0)
406 );
407 }
408
409 #[test]
410 fn test_mix_named_in_hsl_polar() {
411 assert_close_to!(Hsl::mix(Named::Rebeccapurple, Named::Hotpink, 50.0), Hsl::new(300.0, 75.0, 55.294117, 100.0));
412 assert_close_to!(
413 Hsl::mix_polar(Named::Rebeccapurple, Named::Hotpink, 50.0, HueInterpolation::Longer),
414 Hsl::new(120.0, 75.0, 55.294117, 100.0)
415 );
416 assert_close_to!(
417 Hsl::mix_polar(Named::Rebeccapurple, Named::Hotpink, 50.0, HueInterpolation::Decreasing),
418 Hsl::new(120.0, 75.0, 55.294117, 100.0)
419 );
420 assert_close_to!(
421 Hsl::mix_polar(Named::Rebeccapurple, Named::Hotpink, 50.0, HueInterpolation::Increasing),
422 Hsl::new(300.0, 75.0, 55.294117, 100.0)
423 );
424 }
425
426 #[test]
427 fn test_alpha_mixing() {
428 let color1 = Srgb::new(255, 0, 0, 80.0);
429 let color2 = Srgb::new(0, 0, 255, 40.0);
430
431 let mixed = Srgb::mix(color1, color2, 50.0);
432 assert_eq!(mixed.red, 170);
433 assert_eq!(mixed.green, 0);
434 assert_eq!(mixed.blue, 85);
435 assert_eq!(mixed.alpha, 60.0);
436 }
437
438 #[test]
439 fn test_hwb_mix() {
440 let red = Hwb::new(0.0, 0.0, 0.0, 100.0);
443 let blue = Hwb::new(240.0, 0.0, 0.0, 100.0);
444 let mixed = Hwb::mix(red, blue, 50.0);
445 assert_close_to!(mixed, Hwb::new(300.0, 0.0, 0.0, 100.0));
446 }
447
448 #[test]
449 fn test_hwb_mix_polar() {
450 let red = Hwb::new(0.0, 0.0, 0.0, 100.0);
452 let blue = Hwb::new(240.0, 0.0, 0.0, 100.0);
453 assert_close_to!(Hwb::mix_polar(red, blue, 50.0, HueInterpolation::Longer), Hwb::new(120.0, 0.0, 0.0, 100.0));
454 }
455
456 #[test]
457 fn test_a98_rgb_mix() {
458 let c1 = A98Rgb::new(1.0, 0.0, 0.0, 100.0);
459 let c2 = A98Rgb::new(0.0, 0.0, 1.0, 100.0);
460 let mixed = A98Rgb::mix(c1, c2, 50.0);
461 assert_close_to!(mixed, A98Rgb::new(0.5, 0.0, 0.5, 100.0));
462 }
463
464 #[test]
466 fn test_premultiplied_alpha_lab() {
467 let c1 = Lab::new(10.0, 20.0, 30.0, 40.0);
468 let c2 = Lab::new(50.0, 60.0, 70.0, 80.0);
469 let mixed = Lab::mix(c1, c2, 50.0);
470 assert_close_to!(mixed, Lab::new(36.666664, 46.666664, 56.666664, 60.0));
471 }
472
473 #[test]
475 fn test_premultiplied_alpha_lab_25_75() {
476 let c1 = Lab::new(10.0, 20.0, 30.0, 40.0);
477 let c2 = Lab::new(50.0, 60.0, 70.0, 80.0);
478 let mixed = Lab::mix(c1, c2, 75.0);
480 assert_close_to!(mixed, Lab::new(44.285713, 54.285717, 64.28571, 70.0));
481 }
482
483 #[test]
485 fn test_premultiplied_alpha_oklch() {
486 let c1 = Oklch::new(0.1, 0.2, 30.0, 40.0);
487 let c2 = Oklch::new(0.5, 0.6, 70.0, 80.0);
488 let mixed = Oklch::mix(c1, c2, 50.0);
489 assert_close_to!(mixed, Oklch::new(0.36666664, 0.46666664, 50.0, 60.0));
490 }
491
492 #[test]
494 fn test_premultiplied_alpha_opaque_same_as_simple() {
495 let c1 = Lab::new(10.0, 20.0, 30.0, 100.0);
496 let c2 = Lab::new(50.0, 60.0, 70.0, 100.0);
497 let mixed = Lab::mix(c1, c2, 50.0);
498 assert_close_to!(mixed, Lab::new(30.0, 40.0, 50.0, 100.0));
499 }
500}