chromashift/
mix.rs

1use crate::{Hsl, Lab, Lch, LinearRgb, Oklab, Oklch, Srgb, XyzD50, XyzD65};
2
3/// A direction to interopolate hue values between, when mixing colours.
4#[derive(Debug, Clone, Copy, PartialEq, Default)]
5pub enum HueInterpolation {
6	#[default]
7	Shorter,
8	Longer,
9	Increasing,
10	Decreasing,
11}
12
13/// Trait for calculating mixing two colors together.
14///
15/// This trait provides a static method which will receive two colours, and can output a Self which should be the result
16/// of both colours mixed by the given percentage (the percentage pertains to how much the second colour should apply to
17/// the first).
18pub 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
26/// Trait for calculating mixing two colors together, with a hue direction for Polar colour spaces.
27///
28/// This trait provides a static method which will receive two colours, and can output a Self which should be the result
29/// of both colours mixed by the given percentage (the percentage pertains to how much the second colour should apply to
30/// the first). The Hue direction should be respected. If the colour space is not Polar then consider [ColorMix]
31/// instead.
32pub 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
40/// Given two hues (`h1`, `h2`), a percentage transform (`t`), and an interpolation direction, return a new Hue rotation
41/// transformed by that amount.
42pub 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}