1use core::{any::TypeId, marker::PhantomData, ops::Not};
4
5use crate::{
6 angle::{FromAngle, RealAngle},
7 bool_mask::{BitOps, BoolMask, HasBoolMask, LazySelect, Select},
8 convert::FromColorUnclamped,
9 encoding::Srgb,
10 hues::RgbHueIter,
11 num::{Arithmetics, IsValidDivisor, MinMax, One, PartialCmp, Real, Zero},
12 rgb::{Rgb, RgbSpace, RgbStandard},
13 stimulus::{FromStimulus, Stimulus},
14 Alpha, FromColor, Hsv, RgbHue, Xyz,
15};
16
17pub type Hsla<S = Srgb, T = f32> = Alpha<Hsl<S, T>, T>;
20
21#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
47#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
48#[palette(
49 palette_internal,
50 rgb_standard = "S",
51 component = "T",
52 skip_derives(Rgb, Hsv, Hsl)
53)]
54#[repr(C)]
55pub struct Hsl<S = Srgb, T = f32> {
56 #[palette(unsafe_same_layout_as = "T")]
59 pub hue: RgbHue<T>,
60
61 pub saturation: T,
64
65 pub lightness: T,
68
69 #[cfg_attr(feature = "serializing", serde(skip))]
72 #[palette(unsafe_zero_sized)]
73 pub standard: PhantomData<S>,
74}
75
76impl<T> Hsl<Srgb, T> {
77 pub fn new_srgb<H: Into<RgbHue<T>>>(hue: H, saturation: T, lightness: T) -> Self {
80 Self::new_const(hue.into(), saturation, lightness)
81 }
82
83 pub const fn new_srgb_const(hue: RgbHue<T>, saturation: T, lightness: T) -> Self {
86 Self::new_const(hue, saturation, lightness)
87 }
88}
89
90impl<S, T> Hsl<S, T> {
91 pub fn new<H: Into<RgbHue<T>>>(hue: H, saturation: T, lightness: T) -> Self {
93 Self::new_const(hue.into(), saturation, lightness)
94 }
95
96 pub const fn new_const(hue: RgbHue<T>, saturation: T, lightness: T) -> Self {
99 Hsl {
100 hue,
101 saturation,
102 lightness,
103 standard: PhantomData,
104 }
105 }
106
107 pub fn into_format<U>(self) -> Hsl<S, U>
109 where
110 U: FromStimulus<T> + FromAngle<T>,
111 {
112 Hsl {
113 hue: self.hue.into_format(),
114 saturation: U::from_stimulus(self.saturation),
115 lightness: U::from_stimulus(self.lightness),
116 standard: PhantomData,
117 }
118 }
119
120 pub fn from_format<U>(color: Hsl<S, U>) -> Self
122 where
123 T: FromStimulus<U> + FromAngle<U>,
124 {
125 color.into_format()
126 }
127
128 pub fn into_components(self) -> (RgbHue<T>, T, T) {
130 (self.hue, self.saturation, self.lightness)
131 }
132
133 pub fn from_components<H: Into<RgbHue<T>>>((hue, saturation, lightness): (H, T, T)) -> Self {
135 Self::new(hue, saturation, lightness)
136 }
137
138 #[inline]
139 fn reinterpret_as<St>(self) -> Hsl<St, T> {
140 Hsl {
141 hue: self.hue,
142 saturation: self.saturation,
143 lightness: self.lightness,
144 standard: PhantomData,
145 }
146 }
147}
148
149impl<S, T> Hsl<S, T>
150where
151 T: Stimulus,
152{
153 pub fn min_saturation() -> T {
155 T::zero()
156 }
157
158 pub fn max_saturation() -> T {
160 T::max_intensity()
161 }
162
163 pub fn min_lightness() -> T {
165 T::zero()
166 }
167
168 pub fn max_lightness() -> T {
170 T::max_intensity()
171 }
172}
173
174impl<T, A> Alpha<Hsl<Srgb, T>, A> {
176 pub fn new_srgb<H: Into<RgbHue<T>>>(hue: H, saturation: T, lightness: T, alpha: A) -> Self {
179 Self::new_const(hue.into(), saturation, lightness, alpha)
180 }
181
182 pub const fn new_srgb_const(hue: RgbHue<T>, saturation: T, lightness: T, alpha: A) -> Self {
186 Self::new_const(hue, saturation, lightness, alpha)
187 }
188}
189
190impl<S, T, A> Alpha<Hsl<S, T>, A> {
192 pub fn new<H: Into<RgbHue<T>>>(hue: H, saturation: T, lightness: T, alpha: A) -> Self {
194 Self::new_const(hue.into(), saturation, lightness, alpha)
195 }
196
197 pub const fn new_const(hue: RgbHue<T>, saturation: T, lightness: T, alpha: A) -> Self {
201 Alpha {
202 color: Hsl::new_const(hue, saturation, lightness),
203 alpha,
204 }
205 }
206 pub fn into_format<U, B>(self) -> Alpha<Hsl<S, U>, B>
208 where
209 U: FromStimulus<T> + FromAngle<T>,
210 B: FromStimulus<A>,
211 {
212 Alpha {
213 color: self.color.into_format(),
214 alpha: B::from_stimulus(self.alpha),
215 }
216 }
217
218 pub fn from_format<U, B>(color: Alpha<Hsl<S, U>, B>) -> Self
220 where
221 T: FromStimulus<U> + FromAngle<U>,
222 A: FromStimulus<B>,
223 {
224 color.into_format()
225 }
226
227 pub fn into_components(self) -> (RgbHue<T>, T, T, A) {
229 (
230 self.color.hue,
231 self.color.saturation,
232 self.color.lightness,
233 self.alpha,
234 )
235 }
236
237 pub fn from_components<H: Into<RgbHue<T>>>(
239 (hue, saturation, lightness, alpha): (H, T, T, A),
240 ) -> Self {
241 Self::new(hue, saturation, lightness, alpha)
242 }
243}
244
245impl_reference_component_methods_hue!(Hsl<S>, [saturation, lightness], standard);
246impl_struct_of_arrays_methods_hue!(Hsl<S>, [saturation, lightness], standard);
247
248impl<S1, S2, T> FromColorUnclamped<Hsl<S1, T>> for Hsl<S2, T>
249where
250 S1: RgbStandard + 'static,
251 S2: RgbStandard + 'static,
252 S1::Space: RgbSpace<WhitePoint = <S2::Space as RgbSpace>::WhitePoint>,
253 Rgb<S1, T>: FromColorUnclamped<Hsl<S1, T>>,
254 Rgb<S2, T>: FromColorUnclamped<Rgb<S1, T>>,
255 Self: FromColorUnclamped<Rgb<S2, T>>,
256{
257 #[inline]
258 fn from_color_unclamped(hsl: Hsl<S1, T>) -> Self {
259 if TypeId::of::<S1>() == TypeId::of::<S2>() {
260 hsl.reinterpret_as()
261 } else {
262 let rgb = Rgb::<S1, T>::from_color_unclamped(hsl);
263 let converted_rgb = Rgb::<S2, T>::from_color_unclamped(rgb);
264 Self::from_color_unclamped(converted_rgb)
265 }
266 }
267}
268
269impl<S, T> FromColorUnclamped<Rgb<S, T>> for Hsl<S, T>
270where
271 T: RealAngle + Zero + One + MinMax + Arithmetics + PartialCmp + Clone,
272 T::Mask: BoolMask + BitOps + LazySelect<T> + Clone + 'static,
273{
274 fn from_color_unclamped(rgb: Rgb<S, T>) -> Self {
275 let red = rgb.red.max(T::zero());
277 let green = rgb.green.max(T::zero());
278 let blue = rgb.blue.max(T::zero());
279
280 if TypeId::of::<T::Mask>() == TypeId::of::<bool>() {
282 let (max, min, sep, coeff) = {
283 let (max, min, sep, coeff) = if red.gt(&green).is_true() {
284 (red.clone(), green.clone(), green.clone() - &blue, T::zero())
285 } else {
286 (
287 green.clone(),
288 red.clone(),
289 blue.clone() - &red,
290 T::from_f64(2.0),
291 )
292 };
293 if blue.gt(&max).is_true() {
294 (blue, min, red - green, T::from_f64(4.0))
295 } else {
296 let min_val = if blue.lt(&min).is_true() { blue } else { min };
297 (max, min_val, sep, coeff)
298 }
299 };
300
301 let mut h = T::zero();
302 let mut s = T::zero();
303
304 let sum = max.clone() + &min;
305 let l = sum.clone() / T::from_f64(2.0);
306 if max.neq(&min).is_true() {
307 let d = max - min;
308 s = if sum.gt(&T::one()).is_true() {
309 d.clone() / (T::from_f64(2.0) - sum)
310 } else {
311 d.clone() / sum
312 };
313 h = ((sep / d) + coeff) * T::from_f64(60.0);
314 };
315
316 Hsl {
317 hue: h.into(),
318 saturation: s,
319 lightness: l,
320 standard: PhantomData,
321 }
322 } else {
323 let six = T::from_f64(6.0);
331
332 let max = red.clone().max(green.clone()).max(blue.clone());
333 let min = red.clone().min(green.clone()).min(blue.clone());
334
335 let sum = max.clone() + &min;
336 let lightness = T::from_f64(0.5) * ∑
337
338 let chroma = max.clone() - &min;
339 let saturation = lazy_select! {
340 if min.eq(&max) => T::zero(),
341 else => chroma.clone() /
342 sum.gt(&T::one()).select(T::from_f64(2.0) - &sum, sum.clone()),
343 };
344
345 let x = max.neq(&red);
349 let y = max.eq(&red) | max.neq(&green);
350 let z = max.eq(&red) | max.eq(&green);
351
352 let hue_base = x.clone().select(
355 z.clone().select(T::from_f64(-4.0), T::from_f64(4.0)),
356 T::zero(),
357 ) + &six;
358
359 let red_m = lazy_select! {
364 if x => y.clone().select(red.clone(), -red),
365 else => T::zero(),
366 };
367 let green_m = lazy_select! {
368 if y.clone() => z.clone().select(green.clone(), -green),
369 else => T::zero(),
370 };
371 let blue_m = lazy_select! {
372 if z => y.select(-blue.clone(), blue),
373 else => T::zero(),
374 };
375
376 let hue = lazy_select! {
383 if chroma.eq(&T::zero()) => T::zero(),
384 else => hue_base + (red_m + green_m + blue_m) / &chroma,
385 };
386
387 let hue_sub = hue.gt_eq(&six).select(six, T::zero());
391 let hue = hue - hue_sub;
392
393 Hsl {
394 hue: RgbHue::from_degrees(hue * T::from_f64(60.0)),
395 saturation,
396 lightness,
397 standard: PhantomData,
398 }
399 }
400 }
401}
402
403impl<S, T> FromColorUnclamped<Hsv<S, T>> for Hsl<S, T>
404where
405 T: Real + Zero + One + IsValidDivisor + Arithmetics + PartialCmp + Clone,
406 T::Mask: LazySelect<T> + Not<Output = T::Mask>,
407{
408 fn from_color_unclamped(hsv: Hsv<S, T>) -> Self {
409 let Hsv {
410 hue,
411 saturation,
412 value,
413 ..
414 } = hsv;
415
416 let x = (T::from_f64(2.0) - &saturation) * &value;
417 let saturation = lazy_select! {
418 if !value.is_valid_divisor() => T::zero(),
419 if x.lt(&T::one()) => {
420 lazy_select!{
421 if x.is_valid_divisor() => saturation.clone() * &value / &x,
422 else => T::zero(),
423 }
424 },
425 else => {
426 let denom = T::from_f64(2.0) - &x;
427 lazy_select! {
428 if denom.is_valid_divisor() => saturation.clone() * &value / denom,
429 else => T::zero(),
430 }
431 },
432 };
433
434 Hsl {
435 hue,
436 saturation,
437 lightness: x / T::from_f64(2.0),
438 standard: PhantomData,
439 }
440 }
441}
442
443impl_tuple_conversion_hue!(Hsl<S> as (H, T, T), RgbHue);
444
445impl_is_within_bounds! {
446 Hsl<S> {
447 saturation => [Self::min_saturation(), Self::max_saturation()],
448 lightness => [Self::min_lightness(), Self::max_lightness()]
449 }
450 where T: Stimulus
451}
452impl_clamp! {
453 Hsl<S> {
454 saturation => [Self::min_saturation(), Self::max_saturation()],
455 lightness => [Self::min_lightness(), Self::max_lightness()]
456 }
457 other {hue, standard}
458 where T: Stimulus
459}
460
461impl_mix_hue!(Hsl<S> {saturation, lightness} phantom: standard);
462impl_lighten!(Hsl<S> increase {lightness => [Self::min_lightness(), Self::max_lightness()]} other {hue, saturation} phantom: standard where T: Stimulus);
463impl_saturate!(Hsl<S> increase {saturation => [Self::min_saturation(), Self::max_saturation()]} other {hue, lightness} phantom: standard where T: Stimulus);
464impl_hue_ops!(Hsl<S>, RgbHue);
465
466impl<S, T> HasBoolMask for Hsl<S, T>
467where
468 T: HasBoolMask,
469{
470 type Mask = T::Mask;
471}
472
473impl<S, T> Default for Hsl<S, T>
474where
475 T: Stimulus,
476 RgbHue<T>: Default,
477{
478 fn default() -> Hsl<S, T> {
479 Hsl::new(
480 RgbHue::default(),
481 Self::min_saturation(),
482 Self::min_lightness(),
483 )
484 }
485}
486
487impl_color_add!(Hsl<S>, [hue, saturation, lightness], standard);
488impl_color_sub!(Hsl<S>, [hue, saturation, lightness], standard);
489
490impl_array_casts!(Hsl<S, T>, [T; 3]);
491impl_simd_array_conversion_hue!(Hsl<S>, [saturation, lightness], standard);
492impl_struct_of_array_traits_hue!(Hsl<S>, RgbHueIter, [saturation, lightness], standard);
493
494impl_eq_hue!(Hsl<S>, RgbHue, [hue, saturation, lightness]);
495impl_copy_clone!(Hsl<S>, [hue, saturation, lightness], standard);
496
497#[allow(deprecated)]
498impl<S, T> crate::RelativeContrast for Hsl<S, T>
499where
500 T: Real + Arithmetics + PartialCmp,
501 T::Mask: LazySelect<T>,
502 S: RgbStandard,
503 Xyz<<S::Space as RgbSpace>::WhitePoint, T>: FromColor<Self>,
504{
505 type Scalar = T;
506
507 #[inline]
508 fn get_contrast_ratio(self, other: Self) -> T {
509 let xyz1 = Xyz::from_color(self);
510 let xyz2 = Xyz::from_color(other);
511
512 crate::contrast_ratio(xyz1.y, xyz2.y)
513 }
514}
515
516impl_rand_traits_hsl_bicone!(
517 UniformHsl,
518 Hsl<S> {
519 hue: UniformRgbHue => RgbHue,
520 height: lightness,
521 radius: saturation
522 }
523 phantom: standard: PhantomData<S>
524);
525
526#[cfg(feature = "bytemuck")]
527unsafe impl<S, T> bytemuck::Zeroable for Hsl<S, T> where T: bytemuck::Zeroable {}
528
529#[cfg(feature = "bytemuck")]
530unsafe impl<S: 'static, T> bytemuck::Pod for Hsl<S, T> where T: bytemuck::Pod {}
531
532#[cfg(test)]
533mod test {
534 use super::Hsl;
535
536 test_convert_into_from_xyz!(Hsl);
537
538 #[cfg(feature = "approx")]
539 mod conversion {
540 use crate::{FromColor, Hsl, Hsv, Srgb};
541
542 #[test]
543 fn red() {
544 let a = Hsl::from_color(Srgb::new(1.0, 0.0, 0.0));
545 let b = Hsl::new_srgb(0.0, 1.0, 0.5);
546 let c = Hsl::from_color(Hsv::new_srgb(0.0, 1.0, 1.0));
547
548 assert_relative_eq!(a, b);
549 assert_relative_eq!(a, c);
550 }
551
552 #[test]
553 fn orange() {
554 let a = Hsl::from_color(Srgb::new(1.0, 0.5, 0.0));
555 let b = Hsl::new_srgb(30.0, 1.0, 0.5);
556 let c = Hsl::from_color(Hsv::new_srgb(30.0, 1.0, 1.0));
557
558 assert_relative_eq!(a, b);
559 assert_relative_eq!(a, c);
560 }
561
562 #[test]
563 fn green() {
564 let a = Hsl::from_color(Srgb::new(0.0, 1.0, 0.0));
565 let b = Hsl::new_srgb(120.0, 1.0, 0.5);
566 let c = Hsl::from_color(Hsv::new_srgb(120.0, 1.0, 1.0));
567
568 assert_relative_eq!(a, b);
569 assert_relative_eq!(a, c);
570 }
571
572 #[test]
573 fn blue() {
574 let a = Hsl::from_color(Srgb::new(0.0, 0.0, 1.0));
575 let b = Hsl::new_srgb(240.0, 1.0, 0.5);
576 let c = Hsl::from_color(Hsv::new_srgb(240.0, 1.0, 1.0));
577
578 assert_relative_eq!(a, b);
579 assert_relative_eq!(a, c);
580 }
581
582 #[test]
583 fn purple() {
584 let a = Hsl::from_color(Srgb::new(0.5, 0.0, 1.0));
585 let b = Hsl::new_srgb(270.0, 1.0, 0.5);
586 let c = Hsl::from_color(Hsv::new_srgb(270.0, 1.0, 1.0));
587
588 assert_relative_eq!(a, b);
589 assert_relative_eq!(a, c);
590 }
591 }
592
593 #[test]
594 fn ranges() {
595 assert_ranges! {
596 Hsl<crate::encoding::Srgb, f64>;
597 clamped {
598 saturation: 0.0 => 1.0,
599 lightness: 0.0 => 1.0
600 }
601 clamped_min {}
602 unclamped {
603 hue: -360.0 => 360.0
604 }
605 }
606 }
607
608 raw_pixel_conversion_tests!(Hsl<crate::encoding::Srgb>: hue, saturation, lightness);
609 raw_pixel_conversion_fail_tests!(Hsl<crate::encoding::Srgb>: hue, saturation, lightness);
610
611 #[test]
612 fn check_min_max_components() {
613 use crate::encoding::Srgb;
614
615 assert_eq!(Hsl::<Srgb>::min_saturation(), 0.0);
616 assert_eq!(Hsl::<Srgb>::min_lightness(), 0.0);
617 assert_eq!(Hsl::<Srgb>::max_saturation(), 1.0);
618 assert_eq!(Hsl::<Srgb>::max_lightness(), 1.0);
619 }
620
621 struct_of_arrays_tests!(
622 Hsl<crate::encoding::Srgb>[hue, saturation, lightness] phantom: standard,
623 super::Hsla::new(0.1f32, 0.2, 0.3, 0.4),
624 super::Hsla::new(0.2, 0.3, 0.4, 0.5),
625 super::Hsla::new(0.3, 0.4, 0.5, 0.6)
626 );
627
628 #[cfg(feature = "serializing")]
629 #[test]
630 fn serialize() {
631 let serialized = ::serde_json::to_string(&Hsl::new_srgb(0.3, 0.8, 0.1)).unwrap();
632
633 assert_eq!(
634 serialized,
635 r#"{"hue":0.3,"saturation":0.8,"lightness":0.1}"#
636 );
637 }
638
639 #[cfg(feature = "serializing")]
640 #[test]
641 fn deserialize() {
642 let deserialized: Hsl =
643 ::serde_json::from_str(r#"{"hue":0.3,"saturation":0.8,"lightness":0.1}"#).unwrap();
644
645 assert_eq!(deserialized, Hsl::new(0.3, 0.8, 0.1));
646 }
647
648 test_uniform_distribution! {
649 Hsl<crate::encoding::Srgb, f32> as crate::rgb::Rgb {
650 red: (0.0, 1.0),
651 green: (0.0, 1.0),
652 blue: (0.0, 1.0)
653 },
654 min: Hsl::new(0.0f32, 0.0, 0.0),
655 max: Hsl::new(360.0, 1.0, 1.0)
656 }
657
658 #[cfg(feature = "random")]
661 #[test]
662 #[should_panic(expected = "is not uniform enough")]
663 fn uniform_distribution_fail() {
664 use rand::Rng;
665
666 const BINS: usize = crate::random_sampling::test_utils::BINS;
667 const SAMPLES: usize = crate::random_sampling::test_utils::SAMPLES;
668
669 let mut red = [0; BINS];
670 let mut green = [0; BINS];
671 let mut blue = [0; BINS];
672
673 let mut rng = rand_mt::Mt::new(1234); for _ in 0..SAMPLES {
676 let color = Hsl::<crate::encoding::Srgb, f32>::new(
677 rng.gen::<f32>() * 360.0,
678 rng.gen(),
679 rng.gen(),
680 );
681 let color: crate::rgb::Rgb = crate::IntoColor::into_color(color);
682 red[((color.red * BINS as f32) as usize).min(9)] += 1;
683 green[((color.green * BINS as f32) as usize).min(9)] += 1;
684 blue[((color.blue * BINS as f32) as usize).min(9)] += 1;
685 }
686
687 assert_uniform_distribution!(red);
688 assert_uniform_distribution!(green);
689 assert_uniform_distribution!(blue);
690 }
691}