Skip to main content

chromashift/
mix.rs

1use crate::{
2	A98Rgb, DisplayP3, Hsl, HueInterpolation, Hwb, InterpolationSpace, Lab, Lch, LinearRgb, Oklab, Oklch, ProphotoRgb,
3	Rec2020, Srgb, XyzD50, XyzD65,
4};
5
6/// Trait for calculating mixing two colors together.
7///
8/// This trait provides a static method which will receive two colours, and can output a Self which should be the result
9/// of both colours mixed by the given percentage (the percentage pertains to how much the second colour should apply to
10/// the first). Colours mix in the [`InterpolationSpace`] of `Self` as [`InterpolationSpace::mix`] mixes them, with hues
11/// in polar spaces taking the shorter way round.
12pub trait ColorMix<T, U>: Sized
13where
14	T: Into<Self>,
15	U: Into<Self>,
16{
17	fn mix(first: T, second: U, percentage: f64) -> Self;
18}
19
20macro_rules! impl_color_mix {
21	($($ty:ident => $space:expr),+ $(,)?) => {
22		$(impl<T, U> ColorMix<T, U> for $ty
23		where
24			Self: From<T> + From<U>,
25		{
26			fn mix(first: T, second: U, percentage: f64) -> Self {
27				let space = $space;
28				let channels = |color: Self| space.channels(color.into());
29				space.color(space.mix(channels(first.into()), channels(second.into()), percentage)).into()
30			}
31		})+
32	};
33}
34
35impl_color_mix! {
36	Srgb => InterpolationSpace::Srgb,
37	LinearRgb => InterpolationSpace::SrgbLinear,
38	A98Rgb => InterpolationSpace::A98Rgb,
39	DisplayP3 => InterpolationSpace::DisplayP3,
40	ProphotoRgb => InterpolationSpace::ProphotoRgb,
41	Rec2020 => InterpolationSpace::Rec2020,
42	Lab => InterpolationSpace::Lab,
43	Oklab => InterpolationSpace::Oklab,
44	XyzD50 => InterpolationSpace::XyzD50,
45	XyzD65 => InterpolationSpace::XyzD65,
46	Hsl => InterpolationSpace::Hsl(HueInterpolation::Shorter),
47	Hwb => InterpolationSpace::Hwb(HueInterpolation::Shorter),
48	Lch => InterpolationSpace::Lch(HueInterpolation::Shorter),
49	Oklch => InterpolationSpace::Oklch(HueInterpolation::Shorter),
50}
51
52#[cfg(test)]
53mod tests {
54	use super::*;
55	use crate::*;
56
57	macro_rules! assert_close_to {
58		($a: expr, $b: expr) => {
59			assert!($a.close_to($b, COLOR_EPSILON), "Expected {:?} to be (closely) equal to {:?}", $a, $b);
60		};
61	}
62
63	#[test]
64	fn test_basic_mix() {
65		let red = Srgb::from_u8(255, 0, 0, 100.0);
66		let blue = Srgb::from_u8(0, 0, 255, 100.0);
67		assert_close_to!(Srgb::mix(red, blue, 50.0), Srgb::new(0.5, 0.0, 0.5, 100.0));
68	}
69
70	#[test]
71	fn test_mix_named_in_oklab() {
72		assert_close_to!(
73			Oklch::mix(Named::Rebeccapurple, Named::Hotpink, 50.0),
74			Oklch::new(0.5842845967725198, 0.17868573405015944, 327.6838446374328, 100.0)
75		);
76	}
77
78	#[test]
79	fn test_alpha_mixing() {
80		let color1 = Srgb::from_u8(255, 0, 0, 80.0);
81		let color2 = Srgb::from_u8(0, 0, 255, 40.0);
82
83		let mixed = Srgb::mix(color1, color2, 50.0);
84		assert_eq!(mixed.to_u8(), [170, 0, 85]);
85		assert_eq!(mixed.alpha, 60.0);
86	}
87
88	#[test]
89	fn test_hwb_mix() {
90		// Hwb is polar - default mix uses Shorter hue interpolation
91		// 0° to 240°: diff=240 > 180, so shorter wraps via 360°, midpoint is 300°
92		let red = Hwb::new(0.0, 0.0, 0.0, 100.0);
93		let blue = Hwb::new(240.0, 0.0, 0.0, 100.0);
94		let mixed = Hwb::mix(red, blue, 50.0);
95		assert_close_to!(mixed, Hwb::new(300.0, 0.0, 0.0, 100.0));
96	}
97
98	#[test]
99	fn test_a98_rgb_mix() {
100		let c1 = A98Rgb::new(1.0, 0.0, 0.0, 100.0);
101		let c2 = A98Rgb::new(0.0, 0.0, 1.0, 100.0);
102		let mixed = A98Rgb::mix(c1, c2, 50.0);
103		assert_close_to!(mixed, A98Rgb::new(0.5, 0.0, 0.5, 100.0));
104	}
105
106	/// WPT: color-mix(in lab, lab(10 20 30 / .4), lab(50 60 70 / .8)) → lab(36.666664 46.666664 56.666664 / 0.6)
107	#[test]
108	fn test_premultiplied_alpha_lab() {
109		let c1 = Lab::new(10.0, 20.0, 30.0, 40.0);
110		let c2 = Lab::new(50.0, 60.0, 70.0, 80.0);
111		let mixed = Lab::mix(c1, c2, 50.0);
112		assert_close_to!(mixed, Lab::new(36.666664, 46.666664, 56.666664, 60.0));
113	}
114
115	/// WPT: color-mix(in lab, lab(10 20 30 / .4) 25%, lab(50 60 70 / .8)) → lab(44.285713 54.285717 64.28571 / 0.7)
116	#[test]
117	fn test_premultiplied_alpha_lab_25_75() {
118		let c1 = Lab::new(10.0, 20.0, 30.0, 40.0);
119		let c2 = Lab::new(50.0, 60.0, 70.0, 80.0);
120		// 25% first, 75% second: mix_percentage = 75
121		let mixed = Lab::mix(c1, c2, 75.0);
122		assert_close_to!(mixed, Lab::new(44.285713, 54.285717, 64.28571, 70.0));
123	}
124
125	/// WPT: color-mix(in oklch, oklch(0.1 0.2 30deg / .4), oklch(0.5 0.6 70deg / .8)) → oklch(0.36666664 0.46666664 50 / 0.6)
126	#[test]
127	fn test_premultiplied_alpha_oklch() {
128		let c1 = Oklch::new(0.1, 0.2, 30.0, 40.0);
129		let c2 = Oklch::new(0.5, 0.6, 70.0, 80.0);
130		let mixed = Oklch::mix(c1, c2, 50.0);
131		assert_close_to!(mixed, Oklch::new(0.36666664, 0.46666664, 50.0, 60.0));
132	}
133
134	/// When both alphas are 100%, premultiplied interpolation == simple interpolation.
135	#[test]
136	fn test_premultiplied_alpha_opaque_same_as_simple() {
137		let c1 = Lab::new(10.0, 20.0, 30.0, 100.0);
138		let c2 = Lab::new(50.0, 60.0, 70.0, 100.0);
139		let mixed = Lab::mix(c1, c2, 50.0);
140		assert_close_to!(mixed, Lab::new(30.0, 40.0, 50.0, 100.0));
141	}
142}