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