1use core::{
4 marker::PhantomData,
5 ops::{Add, Mul, Neg},
6};
7
8use crate::{
9 angle::RealAngle,
10 bool_mask::{HasBoolMask, LazySelect},
11 convert::FromColorUnclamped,
12 num::{Arithmetics, MinMax, PartialCmp, Powf, Powi, Real, Recip, Trigonometry, Zero},
13 white_point::{WhitePoint, D65},
14 Alpha, FromColor, GetHue, Lchuv, LuvHue, Xyz,
15};
16
17pub type Luva<Wp = D65, T = f32> = Alpha<Luv<Wp, T>, T>;
20
21#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
32#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
33#[palette(
34 palette_internal,
35 white_point = "Wp",
36 component = "T",
37 skip_derives(Xyz, Luv, Lchuv)
38)]
39#[repr(C)]
40pub struct Luv<Wp = D65, T = f32> {
41 pub l: T,
44
45 pub u: T,
49
50 pub v: T,
54
55 #[cfg_attr(feature = "serializing", serde(skip))]
58 #[palette(unsafe_zero_sized)]
59 pub white_point: PhantomData<Wp>,
60}
61
62impl<Wp, T> Luv<Wp, T> {
63 pub const fn new(l: T, u: T, v: T) -> Self {
65 Luv {
66 l,
67 u,
68 v,
69 white_point: PhantomData,
70 }
71 }
72
73 pub fn into_components(self) -> (T, T, T) {
75 (self.l, self.u, self.v)
76 }
77
78 pub fn from_components((l, u, v): (T, T, T)) -> Self {
80 Self::new(l, u, v)
81 }
82}
83
84impl<Wp, T> Luv<Wp, T>
85where
86 T: Zero + Real,
87{
88 pub fn min_l() -> T {
90 T::zero()
91 }
92
93 pub fn max_l() -> T {
95 T::from_f64(100.0)
96 }
97
98 pub fn min_u() -> T {
100 T::from_f64(-84.0)
101 }
102
103 pub fn max_u() -> T {
105 T::from_f64(176.0)
106 }
107
108 pub fn min_v() -> T {
110 T::from_f64(-135.0)
111 }
112
113 pub fn max_v() -> T {
115 T::from_f64(108.0)
116 }
117}
118
119impl<Wp, T, A> Alpha<Luv<Wp, T>, A> {
121 pub const fn new(l: T, u: T, v: T, alpha: A) -> Self {
123 Alpha {
124 color: Luv::new(l, u, v),
125 alpha,
126 }
127 }
128
129 pub fn into_components(self) -> (T, T, T, A) {
131 (self.color.l, self.color.u, self.color.v, self.alpha)
132 }
133
134 pub fn from_components((l, u, v, alpha): (T, T, T, A)) -> Self {
136 Self::new(l, u, v, alpha)
137 }
138}
139
140impl_reference_component_methods!(Luv<Wp>, [l, u, v], white_point);
141impl_struct_of_arrays_methods!(Luv<Wp>, [l, u, v], white_point);
142
143impl<Wp, T> FromColorUnclamped<Luv<Wp, T>> for Luv<Wp, T> {
144 fn from_color_unclamped(color: Luv<Wp, T>) -> Self {
145 color
146 }
147}
148
149impl<Wp, T> FromColorUnclamped<Lchuv<Wp, T>> for Luv<Wp, T>
150where
151 T: RealAngle + Zero + MinMax + Trigonometry + Mul<Output = T> + Clone,
152{
153 fn from_color_unclamped(color: Lchuv<Wp, T>) -> Self {
154 let (sin_hue, cos_hue) = color.hue.into_raw_radians().sin_cos();
155 let chroma = color.chroma.max(T::zero());
156
157 Luv::new(color.l, chroma.clone() * cos_hue, chroma * sin_hue)
158 }
159}
160
161impl<Wp, T> FromColorUnclamped<Xyz<Wp, T>> for Luv<Wp, T>
162where
163 Wp: WhitePoint<T>,
164 T: Real
165 + Zero
166 + Powi
167 + Powf
168 + Recip
169 + Arithmetics
170 + PartialOrd
171 + Clone
172 + HasBoolMask<Mask = bool>,
173{
174 fn from_color_unclamped(color: Xyz<Wp, T>) -> Self {
175 let w = Wp::get_xyz();
176
177 let kappa = T::from_f64(29.0 / 3.0).powi(3);
178 let epsilon = T::from_f64(6.0 / 29.0).powi(3);
179
180 let prime_denom =
181 color.x.clone() + T::from_f64(15.0) * &color.y + T::from_f64(3.0) * color.z;
182 if prime_denom == T::from_f64(0.0) {
183 return Luv::new(T::zero(), T::zero(), T::zero());
184 }
185 let prime_denom_recip = prime_denom.recip();
186 let prime_ref_denom_recip =
187 (w.x.clone() + T::from_f64(15.0) * &w.y + T::from_f64(3.0) * w.z).recip();
188
189 let u_prime: T = T::from_f64(4.0) * color.x * &prime_denom_recip;
190 let u_ref_prime = T::from_f64(4.0) * w.x * &prime_ref_denom_recip;
191
192 let v_prime: T = T::from_f64(9.0) * &color.y * prime_denom_recip;
193 let v_ref_prime = T::from_f64(9.0) * &w.y * prime_ref_denom_recip;
194
195 let y_r = color.y / w.y;
196 let l = if y_r > epsilon {
197 T::from_f64(116.0) * y_r.powf(T::from_f64(1.0 / 3.0)) - T::from_f64(16.0)
198 } else {
199 kappa * y_r
200 };
201
202 Luv {
203 u: T::from_f64(13.0) * &l * (u_prime - u_ref_prime),
204 v: T::from_f64(13.0) * &l * (v_prime - v_ref_prime),
205 l,
206 white_point: PhantomData,
207 }
208 }
209}
210
211impl_tuple_conversion!(Luv<Wp> as (T, T, T));
212
213impl_is_within_bounds! {
214 Luv<Wp> {
215 l => [Self::min_l(), Self::max_l()],
216 u => [Self::min_u(), Self::max_u()],
217 v => [Self::min_v(), Self::max_v()]
218 }
219 where T: Real + Zero
220}
221impl_clamp! {
222 Luv<Wp> {
223 l => [Self::min_l(), Self::max_l()],
224 u => [Self::min_u(), Self::max_u()],
225 v => [Self::min_v(), Self::max_v()]
226 }
227 other {white_point}
228 where T: Real + Zero
229}
230
231impl_mix!(Luv<Wp>);
232impl_lighten!(Luv<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {u, v} phantom: white_point);
233impl_premultiply!(Luv<Wp> {l, u, v} phantom: white_point);
234impl_euclidean_distance!(Luv<Wp> {l, u, v});
235impl_hyab!(Luv<Wp> {lightness: l, chroma1: u, chroma2: v});
236impl_lab_color_schemes!(Luv<Wp>[u, v][l, white_point]);
237
238impl<Wp, T> GetHue for Luv<Wp, T>
239where
240 T: RealAngle + Trigonometry + Add<T, Output = T> + Neg<Output = T> + Clone,
241{
242 type Hue = LuvHue<T>;
243
244 fn get_hue(&self) -> LuvHue<T> {
245 LuvHue::from_cartesian(self.u.clone(), self.v.clone())
246 }
247}
248
249impl<Wp, T> HasBoolMask for Luv<Wp, T>
250where
251 T: HasBoolMask,
252{
253 type Mask = T::Mask;
254}
255
256impl<Wp, T> Default for Luv<Wp, T>
257where
258 T: Zero,
259{
260 fn default() -> Luv<Wp, T> {
261 Luv::new(T::zero(), T::zero(), T::zero())
262 }
263}
264
265impl_color_add!(Luv<Wp>, [l, u, v], white_point);
266impl_color_sub!(Luv<Wp>, [l, u, v], white_point);
267impl_color_mul!(Luv<Wp>, [l, u, v], white_point);
268impl_color_div!(Luv<Wp>, [l, u, v], white_point);
269
270impl_array_casts!(Luv<Wp, T>, [T; 3]);
271impl_simd_array_conversion!(Luv<Wp>, [l, u, v], white_point);
272impl_struct_of_array_traits!(Luv<Wp>, [l, u, v], white_point);
273
274impl_eq!(Luv<Wp>, [l, u, v]);
275impl_copy_clone!(Luv<Wp>, [l, u, v], white_point);
276
277#[allow(deprecated)]
278impl<Wp, T> crate::RelativeContrast for Luv<Wp, T>
279where
280 T: Real + Arithmetics + PartialCmp,
281 T::Mask: LazySelect<T>,
282 Wp: WhitePoint<T>,
283 Xyz<Wp, T>: FromColor<Self>,
284{
285 type Scalar = T;
286
287 #[inline]
288 fn get_contrast_ratio(self, other: Self) -> T {
289 let xyz1 = Xyz::from_color(self);
290 let xyz2 = Xyz::from_color(other);
291
292 crate::contrast_ratio(xyz1.y, xyz2.y)
293 }
294}
295
296impl_rand_traits_cartesian!(
297 UniformLuv,
298 Luv<Wp> {
299 l => [|x| x * T::from_f64(100.0)],
300 u => [|x| x * T::from_f64(260.0) - T::from_f64(84.0)],
301 v => [|x| x * T::from_f64(243.0) - T::from_f64(135.0)]
302 }
303 phantom: white_point: PhantomData<Wp>
304 where T: Real + core::ops::Sub<Output = T> + core::ops::Mul<Output = T>
305);
306
307#[cfg(feature = "bytemuck")]
308unsafe impl<Wp, T> bytemuck::Zeroable for Luv<Wp, T> where T: bytemuck::Zeroable {}
309
310#[cfg(feature = "bytemuck")]
311unsafe impl<Wp: 'static, T> bytemuck::Pod for Luv<Wp, T> where T: bytemuck::Pod {}
312
313#[cfg(test)]
314mod test {
315 use super::Luv;
316 use crate::white_point::D65;
317
318 #[cfg(feature = "approx")]
319 use crate::Lchuv;
320
321 test_convert_into_from_xyz!(Luv);
322
323 #[cfg(feature = "approx")]
324 mod conversion {
325 use crate::{FromColor, LinSrgb, Luv};
326
327 #[test]
328 fn red() {
329 let u = Luv::from_color(LinSrgb::new(1.0, 0.0, 0.0));
330 let v = Luv::new(53.237116, 175.0098, 37.7650);
331 assert_relative_eq!(u, v, epsilon = 0.01);
332 }
333
334 #[test]
335 fn green() {
336 let u = Luv::from_color(LinSrgb::new(0.0, 1.0, 0.0));
337 let v = Luv::new(87.73703, -83.07975, 107.40136);
338 assert_relative_eq!(u, v, epsilon = 0.01);
339 }
340
341 #[test]
342 fn blue() {
343 let u = Luv::from_color(LinSrgb::new(0.0, 0.0, 1.0));
344 let v = Luv::new(32.30087, -9.40241, -130.35109);
345 assert_relative_eq!(u, v, epsilon = 0.01);
346 }
347 }
348
349 #[test]
350 fn ranges() {
351 assert_ranges! {
352 Luv<D65, f64>;
353 clamped {
354 l: 0.0 => 100.0,
355 u: -84.0 => 176.0,
356 v: -135.0 => 108.0
357 }
358 clamped_min {}
359 unclamped {}
360 }
361 }
362 #[test]
365 fn test_arithmetic() {
366 let luv = Luv::<D65>::new(120.0, 40.0, 30.0);
367 let luv2 = Luv::new(200.0, 30.0, 40.0);
368 let mut _luv3 = luv + luv2;
369 _luv3 += luv2;
370 let mut _luv4 = luv2 + 0.3;
371 _luv4 += 0.1;
372
373 _luv3 = luv2 - luv;
374 _luv3 = _luv4 - 0.1;
375 _luv4 -= _luv3;
376 _luv3 -= 0.1;
377 }
378
379 raw_pixel_conversion_tests!(Luv<D65>: l, u, v);
380 raw_pixel_conversion_fail_tests!(Luv<D65>: l, u, v);
381
382 #[test]
383 fn check_min_max_components() {
384 assert_eq!(Luv::<D65, f32>::min_l(), 0.0);
385 assert_eq!(Luv::<D65, f32>::min_u(), -84.0);
386 assert_eq!(Luv::<D65, f32>::min_v(), -135.0);
387 assert_eq!(Luv::<D65, f32>::max_l(), 100.0);
388 assert_eq!(Luv::<D65, f32>::max_u(), 176.0);
389 assert_eq!(Luv::<D65, f32>::max_v(), 108.0);
390 }
391
392 struct_of_arrays_tests!(
393 Luv<D65>[l, u, v] phantom: white_point,
394 super::Luva::new(0.1f32, 0.2, 0.3, 0.4),
395 super::Luva::new(0.2, 0.3, 0.4, 0.5),
396 super::Luva::new(0.3, 0.4, 0.5, 0.6)
397 );
398
399 #[cfg(feature = "serializing")]
400 #[test]
401 fn serialize() {
402 let serialized = ::serde_json::to_string(&Luv::<D65>::new(80.0, 20.0, 30.0)).unwrap();
403
404 assert_eq!(serialized, r#"{"l":80.0,"u":20.0,"v":30.0}"#);
405 }
406
407 #[cfg(feature = "serializing")]
408 #[test]
409 fn deserialize() {
410 let deserialized: Luv = ::serde_json::from_str(r#"{"l":80.0,"u":20.0,"v":30.0}"#).unwrap();
411
412 assert_eq!(deserialized, Luv::new(80.0, 20.0, 30.0));
413 }
414
415 test_uniform_distribution! {
416 Luv<D65, f32> {
417 l: (0.0, 100.0),
418 u: (-84.0, 176.0),
419 v: (-135.0, 108.0)
420 },
421 min: Luv::new(0.0f32, -84.0, -135.0),
422 max: Luv::new(100.0, 176.0, 108.0)
423 }
424
425 test_lab_color_schemes!(Luv / Lchuv [u, v][l, white_point]);
426}