use core::marker::PhantomData;
use crate::{
angle::RealAngle,
bool_mask::{HasBoolMask, LazySelect},
convert::FromColorUnclamped,
hues::LuvHueIter,
luv_bounds::LuvBounds,
num::{Arithmetics, PartialCmp, Powi, Real, Zero},
white_point::D65,
Alpha, FromColor, Lchuv, LuvHue, Xyz,
};
pub type Hsluva<Wp = D65, T = f32> = Alpha<Hsluv<Wp, T>, T>;
#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)]
#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
#[palette(
palette_internal,
white_point = "Wp",
component = "T",
skip_derives(Lchuv, Hsluv)
)]
#[repr(C)]
pub struct Hsluv<Wp = D65, T = f32> {
#[palette(unsafe_same_layout_as = "T")]
pub hue: LuvHue<T>,
pub saturation: T,
pub l: T,
#[cfg_attr(feature = "serializing", serde(skip))]
#[palette(unsafe_zero_sized)]
pub white_point: PhantomData<Wp>,
}
impl<Wp, T> Hsluv<Wp, T> {
pub fn new<H: Into<LuvHue<T>>>(hue: H, saturation: T, l: T) -> Self {
Self::new_const(hue.into(), saturation, l)
}
pub const fn new_const(hue: LuvHue<T>, saturation: T, l: T) -> Self {
Hsluv {
hue,
saturation,
l,
white_point: PhantomData,
}
}
pub fn into_components(self) -> (LuvHue<T>, T, T) {
(self.hue, self.saturation, self.l)
}
pub fn from_components<H: Into<LuvHue<T>>>((hue, saturation, l): (H, T, T)) -> Self {
Self::new(hue, saturation, l)
}
}
impl<Wp, T> Hsluv<Wp, T>
where
T: Zero + Real,
{
pub fn min_saturation() -> T {
T::zero()
}
pub fn max_saturation() -> T {
T::from_f64(100.0)
}
pub fn min_l() -> T {
T::zero()
}
pub fn max_l() -> T {
T::from_f64(100.0)
}
}
impl<Wp, T, A> Alpha<Hsluv<Wp, T>, A> {
pub fn new<H: Into<LuvHue<T>>>(hue: H, saturation: T, l: T, alpha: A) -> Self {
Self::new_const(hue.into(), saturation, l, alpha)
}
pub const fn new_const(hue: LuvHue<T>, saturation: T, l: T, alpha: A) -> Self {
Alpha {
color: Hsluv::new_const(hue, saturation, l),
alpha,
}
}
pub fn into_components(self) -> (LuvHue<T>, T, T, A) {
(
self.color.hue,
self.color.saturation,
self.color.l,
self.alpha,
)
}
pub fn from_components<H: Into<LuvHue<T>>>((hue, saturation, l, alpha): (H, T, T, A)) -> Self {
Self::new(hue, saturation, l, alpha)
}
}
impl_reference_component_methods_hue!(Hsluv<Wp>, [saturation, l], white_point);
impl_struct_of_arrays_methods_hue!(Hsluv<Wp>, [saturation, l], white_point);
impl<Wp, T> FromColorUnclamped<Hsluv<Wp, T>> for Hsluv<Wp, T> {
fn from_color_unclamped(hsluv: Hsluv<Wp, T>) -> Self {
hsluv
}
}
impl<Wp, T> FromColorUnclamped<Lchuv<Wp, T>> for Hsluv<Wp, T>
where
T: Real + RealAngle + Into<f64> + Powi + Arithmetics + Clone,
{
fn from_color_unclamped(color: Lchuv<Wp, T>) -> Self {
let max_chroma =
LuvBounds::from_lightness(color.l.clone()).max_chroma_at_hue(color.hue.clone());
Hsluv::new(
color.hue,
color.chroma / max_chroma * T::from_f64(100.0),
color.l,
)
}
}
impl_tuple_conversion_hue!(Hsluv<Wp> as (H, T, T), LuvHue);
impl_is_within_bounds! {
Hsluv<Wp> {
saturation => [Self::min_saturation(), Self::max_saturation()],
l => [Self::min_l(), Self::max_l()]
}
where T: Real + Zero
}
impl_clamp! {
Hsluv<Wp> {
saturation => [Self::min_saturation(), Self::max_saturation()],
l => [Self::min_l(), Self::max_l()]
}
other {hue, white_point}
where T: Real + Zero
}
impl_mix_hue!(Hsluv<Wp> {saturation, l} phantom: white_point);
impl_lighten!(Hsluv<Wp> increase {l => [Self::min_l(), Self::max_l()]} other {hue, saturation} phantom: white_point);
impl_saturate!(Hsluv<Wp> increase {saturation => [Self::min_saturation(), Self::max_saturation()]} other {hue, l} phantom: white_point);
impl_hue_ops!(Hsluv<Wp>, LuvHue);
impl<Wp, T> HasBoolMask for Hsluv<Wp, T>
where
T: HasBoolMask,
{
type Mask = T::Mask;
}
impl<Wp, T> Default for Hsluv<Wp, T>
where
T: Real + Zero,
LuvHue<T>: Default,
{
fn default() -> Hsluv<Wp, T> {
Hsluv::new(LuvHue::default(), Self::min_saturation(), Self::min_l())
}
}
impl_color_add!(Hsluv<Wp>, [hue, saturation, l], white_point);
impl_color_sub!(Hsluv<Wp>, [hue, saturation, l], white_point);
impl_array_casts!(Hsluv<Wp, T>, [T; 3]);
impl_simd_array_conversion_hue!(Hsluv<Wp>, [saturation, l], white_point);
impl_struct_of_array_traits_hue!(Hsluv<Wp>, LuvHueIter, [saturation, l], white_point);
impl_eq_hue!(Hsluv<Wp>, LuvHue, [hue, saturation, l]);
impl_copy_clone!(Hsluv<Wp>, [hue, saturation, l], white_point);
#[allow(deprecated)]
impl<Wp, T> crate::RelativeContrast for Hsluv<Wp, T>
where
T: Real + Arithmetics + PartialCmp,
T::Mask: LazySelect<T>,
Xyz<Wp, T>: FromColor<Self>,
{
type Scalar = T;
#[inline]
fn get_contrast_ratio(self, other: Self) -> T {
let xyz1 = Xyz::from_color(self);
let xyz2 = Xyz::from_color(other);
crate::contrast_ratio(xyz1.y, xyz2.y)
}
}
impl_rand_traits_hsl_bicone!(
UniformHsluv,
Hsluv<Wp> {
hue: UniformLuvHue => LuvHue,
height: l => [|l: T| l * T::from_f64(100.0), |l: T| l / T::from_f64(100.0)],
radius: saturation => [|s: T| s * T::from_f64(100.0), |s: T| s / T::from_f64(100.0)]
}
phantom: white_point: PhantomData<Wp>
);
#[cfg(feature = "bytemuck")]
unsafe impl<Wp, T> bytemuck::Zeroable for Hsluv<Wp, T> where T: bytemuck::Zeroable {}
#[cfg(feature = "bytemuck")]
unsafe impl<Wp: 'static, T> bytemuck::Pod for Hsluv<Wp, T> where T: bytemuck::Pod {}
#[cfg(test)]
mod test {
use super::Hsluv;
use crate::white_point::D65;
test_convert_into_from_xyz!(Hsluv);
#[cfg(feature = "approx")]
#[cfg_attr(miri, ignore)]
#[test]
fn lchuv_round_trip() {
use crate::{FromColor, Lchuv, LuvHue};
for hue in (0..=20).map(|x| x as f64 * 18.0) {
for sat in (0..=20).map(|x| x as f64 * 5.0) {
for l in (1..=20).map(|x| x as f64 * 5.0) {
let hsluv = Hsluv::<D65, _>::new(hue, sat, l);
let lchuv = Lchuv::from_color(hsluv);
let mut to_hsluv = Hsluv::from_color(lchuv);
if to_hsluv.l < 1e-8 {
to_hsluv.hue = LuvHue::from(0.0);
}
assert_relative_eq!(hsluv, to_hsluv, epsilon = 1e-5);
}
}
}
}
#[test]
fn ranges() {
assert_ranges! {
Hsluv<D65, f64>;
clamped {
saturation: 0.0 => 100.0,
l: 0.0 => 100.0
}
clamped_min {}
unclamped {
hue: -360.0 => 360.0
}
}
}
#[test]
fn test_arithmetic() {
let hsl = Hsluv::<D65>::new(120.0, 40.0, 30.0);
let hsl2 = Hsluv::new(200.0, 30.0, 40.0);
let mut _hsl3 = hsl + hsl2;
_hsl3 += hsl2;
let mut _hsl4 = hsl2 + 0.3;
_hsl4 += 0.1;
_hsl3 = hsl2 - hsl;
_hsl3 = _hsl4 - 0.1;
_hsl4 -= _hsl3;
_hsl3 -= 0.1;
}
#[cfg(feature = "approx")]
#[test]
fn saturate() {
use crate::Saturate;
for sat in (0..=10).map(|s| s as f64 * 10.0) {
for a in (0..=10).map(|l| l as f64 * 10.0) {
let hsl = Hsluv::<D65, _>::new(150.0, sat, a);
let hsl_sat_fixed = hsl.saturate_fixed(0.1);
let expected_sat_fixed = Hsluv::new(150.0, (sat + 10.0).min(100.0), a);
assert_relative_eq!(hsl_sat_fixed, expected_sat_fixed);
let hsl_sat = hsl.saturate(0.1);
let expected_sat = Hsluv::new(150.0, (sat + (100.0 - sat) * 0.1).min(100.0), a);
assert_relative_eq!(hsl_sat, expected_sat);
}
}
}
raw_pixel_conversion_tests!(Hsluv<D65>: hue, saturation, lightness);
raw_pixel_conversion_fail_tests!(Hsluv<D65>: hue, saturation, lightness);
#[test]
fn check_min_max_components() {
assert_eq!(Hsluv::<D65>::min_saturation(), 0.0);
assert_eq!(Hsluv::<D65>::min_l(), 0.0);
assert_eq!(Hsluv::<D65>::max_saturation(), 100.0);
assert_eq!(Hsluv::<D65>::max_l(), 100.0);
}
struct_of_arrays_tests!(
Hsluv<D65>[hue, saturation, l] phantom: white_point,
super::Hsluva::new(0.1f32, 0.2, 0.3, 0.4),
super::Hsluva::new(0.2, 0.3, 0.4, 0.5),
super::Hsluva::new(0.3, 0.4, 0.5, 0.6)
);
#[cfg(feature = "serializing")]
#[test]
fn serialize() {
let serialized = ::serde_json::to_string(&Hsluv::<D65>::new(120.0, 80.0, 60.0)).unwrap();
assert_eq!(serialized, r#"{"hue":120.0,"saturation":80.0,"l":60.0}"#);
}
#[cfg(feature = "serializing")]
#[test]
fn deserialize() {
let deserialized: Hsluv =
::serde_json::from_str(r#"{"hue":120.0,"saturation":80.0,"l":60.0}"#).unwrap();
assert_eq!(deserialized, Hsluv::new(120.0, 80.0, 60.0));
}
}