1use core::{marker::PhantomData, ops::Mul};
4
5use crate::{
6 angle::RealAngle,
7 bool_mask::{HasBoolMask, LazySelect},
8 convert::FromColorUnclamped,
9 hues::LuvHueIter,
10 luv_bounds::LuvBounds,
11 num::{Arithmetics, Hypot, PartialCmp, Powi, Real, Zero},
12 white_point::D65,
13 Alpha, FromColor, GetHue, Hsluv, Luv, LuvHue, Xyz,
14};
15
16pub type Lchuva<Wp = D65, T = f32> = Alpha<Lchuv<Wp, T>, T>;
19
20#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
27#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
28#[palette(
29 palette_internal,
30 white_point = "Wp",
31 component = "T",
32 skip_derives(Luv, Lchuv, Hsluv)
33)]
34#[repr(C)]
35pub struct Lchuv<Wp = D65, T = f32> {
36 pub l: T,
39
40 pub chroma: T,
46
47 #[palette(unsafe_same_layout_as = "T")]
50 pub hue: LuvHue<T>,
51
52 #[cfg_attr(feature = "serializing", serde(skip))]
55 #[palette(unsafe_zero_sized)]
56 pub white_point: PhantomData<Wp>,
57}
58
59impl<Wp, T> Lchuv<Wp, T> {
60 pub fn new<H: Into<LuvHue<T>>>(l: T, chroma: T, hue: H) -> Self {
62 Self::new_const(l, chroma, hue.into())
63 }
64
65 pub const fn new_const(l: T, chroma: T, hue: LuvHue<T>) -> Self {
69 Lchuv {
70 l,
71 chroma,
72 hue,
73 white_point: PhantomData,
74 }
75 }
76
77 pub fn into_components(self) -> (T, T, LuvHue<T>) {
79 (self.l, self.chroma, self.hue)
80 }
81
82 pub fn from_components<H: Into<LuvHue<T>>>((l, chroma, hue): (T, T, H)) -> Self {
84 Self::new(l, chroma, hue)
85 }
86}
87
88impl<Wp, T> Lchuv<Wp, T>
89where
90 T: Zero + Real,
91{
92 pub fn min_l() -> T {
94 T::zero()
95 }
96
97 pub fn max_l() -> T {
99 T::from_f64(100.0)
100 }
101
102 pub fn min_chroma() -> T {
104 T::zero()
105 }
106
107 pub fn max_chroma() -> T {
109 T::from_f64(180.0)
110 }
111}
112
113impl<Wp, T, A> Alpha<Lchuv<Wp, T>, A> {
115 pub fn new<H: Into<LuvHue<T>>>(l: T, chroma: T, hue: H, alpha: A) -> Self {
117 Self::new_const(l, chroma, hue.into(), alpha)
118 }
119
120 pub const fn new_const(l: T, chroma: T, hue: LuvHue<T>, alpha: A) -> Self {
124 Alpha {
125 color: Lchuv::new_const(l, chroma, hue),
126 alpha,
127 }
128 }
129
130 pub fn into_components(self) -> (T, T, LuvHue<T>, A) {
132 (self.color.l, self.color.chroma, self.color.hue, self.alpha)
133 }
134
135 pub fn from_components<H: Into<LuvHue<T>>>((l, chroma, hue, alpha): (T, T, H, A)) -> Self {
137 Self::new(l, chroma, hue, alpha)
138 }
139}
140
141impl_reference_component_methods_hue!(Lchuv<Wp>, [l, chroma], white_point);
142impl_struct_of_arrays_methods_hue!(Lchuv<Wp>, [l, chroma], white_point);
143
144impl<Wp, T> FromColorUnclamped<Lchuv<Wp, T>> for Lchuv<Wp, T> {
145 fn from_color_unclamped(color: Lchuv<Wp, T>) -> Self {
146 color
147 }
148}
149
150impl<Wp, T> FromColorUnclamped<Luv<Wp, T>> for Lchuv<Wp, T>
151where
152 T: Zero + Hypot,
153 Luv<Wp, T>: GetHue<Hue = LuvHue<T>>,
154{
155 fn from_color_unclamped(color: Luv<Wp, T>) -> Self {
156 Lchuv {
157 hue: color.get_hue(),
158 l: color.l,
159 chroma: color.u.hypot(color.v),
160 white_point: PhantomData,
161 }
162 }
163}
164
165impl<Wp, T> FromColorUnclamped<Hsluv<Wp, T>> for Lchuv<Wp, T>
166where
167 T: Real + RealAngle + Into<f64> + Powi + Mul<Output = T> + Clone,
168{
169 fn from_color_unclamped(color: Hsluv<Wp, T>) -> Self {
170 let max_chroma =
173 LuvBounds::from_lightness(color.l.clone()).max_chroma_at_hue(color.hue.clone());
174
175 Lchuv::new(
176 color.l,
177 color.saturation * max_chroma * T::from_f64(0.01),
178 color.hue,
179 )
180 }
181}
182
183impl_tuple_conversion_hue!(Lchuv<Wp> as (T, T, H), LuvHue);
184
185impl_is_within_bounds! {
186 Lchuv<Wp> {
187 l => [Self::min_l(), Self::max_l()],
188 chroma => [Self::min_chroma(), Self::max_chroma()]
189 }
190 where T: Real + Zero
191}
192impl_clamp! {
193 Lchuv<Wp> {
194 l => [Self::min_l(), Self::max_l()],
195 chroma => [Self::min_chroma(), Self::max_chroma()]
196 }
197 other {hue, white_point}
198 where T: Real + Zero
199}
200
201impl_mix_hue!(Lchuv<Wp> {l, chroma} phantom: white_point);
202impl_lighten!(Lchuv<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {hue, chroma} phantom: white_point);
203impl_saturate!(Lchuv<Wp> increase {chroma => [Self::min_chroma(), Self::max_chroma()]} other {hue, l} phantom: white_point);
204impl_hue_ops!(Lchuv<Wp>, LuvHue);
205
206impl<Wp, T> HasBoolMask for Lchuv<Wp, T>
207where
208 T: HasBoolMask,
209{
210 type Mask = T::Mask;
211}
212
213impl<Wp, T> Default for Lchuv<Wp, T>
214where
215 T: Zero + Real,
216 LuvHue<T>: Default,
217{
218 fn default() -> Lchuv<Wp, T> {
219 Lchuv::new(Self::min_l(), Self::min_chroma(), LuvHue::default())
220 }
221}
222
223impl_color_add!(Lchuv<Wp>, [l, chroma, hue], white_point);
224impl_color_sub!(Lchuv<Wp>, [l, chroma, hue], white_point);
225
226impl_array_casts!(Lchuv<Wp, T>, [T; 3]);
227impl_simd_array_conversion_hue!(Lchuv<Wp>, [l, chroma], white_point);
228impl_struct_of_array_traits_hue!(Lchuv<Wp>, LuvHueIter, [l, chroma], white_point);
229
230impl_eq_hue!(Lchuv<Wp>, LuvHue, [l, chroma, hue]);
231impl_copy_clone!(Lchuv<Wp>, [l, chroma, hue], white_point);
232
233#[allow(deprecated)]
234impl<Wp, T> crate::RelativeContrast for Lchuv<Wp, T>
235where
236 T: Real + Arithmetics + PartialCmp,
237 T::Mask: LazySelect<T>,
238 Xyz<Wp, T>: FromColor<Self>,
239{
240 type Scalar = T;
241
242 #[inline]
243 fn get_contrast_ratio(self, other: Self) -> T {
244 let xyz1 = Xyz::from_color(self);
245 let xyz2 = Xyz::from_color(other);
246
247 crate::contrast_ratio(xyz1.y, xyz2.y)
248 }
249}
250
251impl_rand_traits_cylinder!(
252 UniformLchuv,
253 Lchuv<Wp> {
254 hue: UniformLuvHue => LuvHue,
255 height: l => [|l: T| l * Lchuv::<Wp, T>::max_l()],
256 radius: chroma => [|chroma| chroma * Lchuv::<Wp, T>::max_chroma()]
257 }
258 phantom: white_point: PhantomData<Wp>
259 where T: Real + Zero + core::ops::Mul<Output = T>,
260);
261
262#[cfg(feature = "bytemuck")]
263unsafe impl<Wp, T> bytemuck::Zeroable for Lchuv<Wp, T> where T: bytemuck::Zeroable {}
264
265#[cfg(feature = "bytemuck")]
266unsafe impl<Wp: 'static, T> bytemuck::Pod for Lchuv<Wp, T> where T: bytemuck::Pod {}
267
268#[cfg(test)]
269mod test {
270 use crate::white_point::D65;
271 use crate::Lchuv;
272
273 test_convert_into_from_xyz!(Lchuv);
274
275 #[test]
276 fn ranges() {
277 assert_ranges! {
278 Lchuv<D65, f64>;
279 clamped {
280 l: 0.0 => 100.0,
281 chroma: 0.0 => 180.0
282 }
283 clamped_min {
284 }
285 unclamped {
286 hue: -360.0 => 360.0
287 }
288 }
289 }
290
291 #[test]
294 fn test_arithmetic() {
295 let lchuv = Lchuv::<D65>::new(120.0, 40.0, 30.0);
296 let lchuv2 = Lchuv::new(200.0, 30.0, 40.0);
297 let mut _lchuv3 = lchuv + lchuv2;
298 _lchuv3 += lchuv2;
299 let mut _lchuv4 = lchuv2 + 0.3;
300 _lchuv4 += 0.1;
301
302 _lchuv3 = lchuv2 - lchuv;
303 _lchuv3 = _lchuv4 - 0.1;
304 _lchuv4 -= _lchuv3;
305 _lchuv3 -= 0.1;
306 }
307
308 raw_pixel_conversion_tests!(Lchuv<D65>: l, chroma, hue);
309 raw_pixel_conversion_fail_tests!(Lchuv<D65>: l, chroma, hue);
310
311 #[test]
312 fn check_min_max_components() {
313 assert_eq!(Lchuv::<D65, f32>::min_l(), 0.0);
314 assert_eq!(Lchuv::<D65, f32>::max_l(), 100.0);
315 assert_eq!(Lchuv::<D65, f32>::min_chroma(), 0.0);
316 assert_eq!(Lchuv::<D65, f32>::max_chroma(), 180.0);
317 }
318
319 struct_of_arrays_tests!(
320 Lchuv<D65>[l, chroma, hue] phantom: white_point,
321 super::Lchuva::new(0.1f32, 0.2, 0.3, 0.4),
322 super::Lchuva::new(0.2, 0.3, 0.4, 0.5),
323 super::Lchuva::new(0.3, 0.4, 0.5, 0.6)
324 );
325
326 #[cfg(feature = "serializing")]
327 #[test]
328 fn serialize() {
329 let serialized = ::serde_json::to_string(&Lchuv::<D65>::new(80.0, 70.0, 130.0)).unwrap();
330
331 assert_eq!(serialized, r#"{"l":80.0,"chroma":70.0,"hue":130.0}"#);
332 }
333
334 #[cfg(feature = "serializing")]
335 #[test]
336 fn deserialize() {
337 let deserialized: Lchuv =
338 ::serde_json::from_str(r#"{"l":70.0,"chroma":80.0,"hue":130.0}"#).unwrap();
339
340 assert_eq!(deserialized, Lchuv::new(70.0, 80.0, 130.0));
341 }
342
343 test_uniform_distribution! {
344 Lchuv<D65, f32> as crate::Luv {
345 l: (0.0, 100.0),
346 u: (-80.0, 80.0),
347 v: (-80.0, 80.0),
348 },
349 min: Lchuv::new(0.0f32, 0.0, 0.0),
350 max: Lchuv::new(100.0, 180.0, 360.0)
351 }
352}