pub use alpha::Okhsla;
use crate::{
angle::FromAngle,
convert::{FromColorUnclamped, IntoColorUnclamped},
num::{Arithmetics, Cbrt, Hypot, IsValidDivisor, MinMax, One, Powi, Real, Sqrt, Zero},
ok_utils::{toe, ChromaValues},
stimulus::{FromStimulus, Stimulus},
white_point::D65,
GetHue, HasBoolMask, LinSrgb, Oklab, OklabHue,
};
pub use self::properties::Iter;
#[cfg(feature = "random")]
pub use self::random::UniformOkhsl;
mod alpha;
mod properties;
#[cfg(feature = "random")]
mod random;
#[cfg(test)]
#[cfg(feature = "approx")]
mod visual_eq;
#[derive(Debug, Copy, Clone, ArrayCast, FromColorUnclamped, WithAlpha)]
#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))]
#[palette(
palette_internal,
white_point = "D65",
component = "T",
skip_derives(Oklab)
)]
#[repr(C)]
pub struct Okhsl<T = f32> {
#[palette(unsafe_same_layout_as = "T")]
pub hue: OklabHue<T>,
pub saturation: T,
pub lightness: T,
}
impl<T> Okhsl<T> {
pub fn new<H: Into<OklabHue<T>>>(hue: H, saturation: T, lightness: T) -> Self {
Self {
hue: hue.into(),
saturation,
lightness,
}
}
pub const fn new_const(hue: OklabHue<T>, saturation: T, lightness: T) -> Self {
Self {
hue,
saturation,
lightness,
}
}
pub fn into_format<U>(self) -> Okhsl<U>
where
U: FromStimulus<T> + FromAngle<T>,
{
Okhsl {
hue: self.hue.into_format(),
saturation: U::from_stimulus(self.saturation),
lightness: U::from_stimulus(self.lightness),
}
}
pub fn from_format<U>(color: Okhsl<U>) -> Self
where
T: FromStimulus<U> + FromAngle<U>,
{
color.into_format()
}
pub fn into_components(self) -> (OklabHue<T>, T, T) {
(self.hue, self.saturation, self.lightness)
}
pub fn from_components<H: Into<OklabHue<T>>>((hue, saturation, lightness): (H, T, T)) -> Self {
Self::new(hue, saturation, lightness)
}
}
impl<T> Okhsl<T>
where
T: Stimulus,
{
pub fn min_saturation() -> T {
T::zero()
}
pub fn max_saturation() -> T {
T::max_intensity()
}
pub fn min_lightness() -> T {
T::zero()
}
pub fn max_lightness() -> T {
T::max_intensity()
}
}
impl_reference_component_methods_hue!(Okhsl, [saturation, lightness]);
impl_struct_of_arrays_methods_hue!(Okhsl, [saturation, lightness]);
impl<T> FromColorUnclamped<Oklab<T>> for Okhsl<T>
where
T: Real
+ One
+ Zero
+ Arithmetics
+ Powi
+ Sqrt
+ Hypot
+ MinMax
+ Cbrt
+ IsValidDivisor<Mask = bool>
+ HasBoolMask<Mask = bool>
+ PartialOrd
+ Clone,
Oklab<T>: GetHue<Hue = OklabHue<T>> + IntoColorUnclamped<LinSrgb<T>>,
{
fn from_color_unclamped(lab: Oklab<T>) -> Self {
let l = toe(lab.l.clone());
let chroma = lab.get_chroma();
let hue = lab.get_hue();
if chroma.is_valid_divisor() {
let cs = ChromaValues::from_normalized(lab.l, lab.a / &chroma, lab.b / &chroma);
let mid = T::from_f64(0.8);
let mid_inv = T::from_f64(1.25);
let s = if chroma < cs.mid {
let k_1 = mid.clone() * cs.zero;
let k_2 = T::one() - k_1.clone() / cs.mid;
let t = chroma.clone() / (k_1 + k_2 * chroma);
t * mid
} else {
let k_0 = cs.mid.clone();
let k_1 = (T::one() - &mid) * (cs.mid.clone() * mid_inv).powi(2) / cs.zero;
let k_2 = T::one() - k_1.clone() / (cs.max - cs.mid);
let t = (chroma.clone() - &k_0) / (k_1 + k_2 * (chroma - k_0));
mid.clone() + (T::one() - mid) * t
};
Self::new(hue, s, l)
} else {
Self::new(T::zero(), T::zero(), l)
}
}
}
impl<T> HasBoolMask for Okhsl<T>
where
T: HasBoolMask,
{
type Mask = T::Mask;
}
impl<T> Default for Okhsl<T>
where
T: Stimulus,
OklabHue<T>: Default,
{
fn default() -> Okhsl<T> {
Okhsl::new(
OklabHue::default(),
Self::min_saturation(),
Self::min_lightness(),
)
}
}
#[cfg(feature = "bytemuck")]
unsafe impl<T> bytemuck::Zeroable for Okhsl<T> where T: bytemuck::Zeroable {}
#[cfg(feature = "bytemuck")]
unsafe impl<T> bytemuck::Pod for Okhsl<T> where T: bytemuck::Pod {}
#[cfg(test)]
mod tests {
use core::str::FromStr;
use crate::convert::FromColorUnclamped;
use crate::rgb::Rgb;
use crate::visual::{VisualColor, VisuallyEqual};
use crate::{encoding, LinSrgb, Okhsl, Oklab, Srgb};
test_convert_into_from_xyz!(Okhsl);
#[test]
fn test_roundtrip_okhsl_oklab_is_original() {
let colors = [
(
"red",
Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 0.0)),
),
(
"green",
Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 0.0)),
),
(
"cyan",
Oklab::from_color_unclamped(LinSrgb::new(0.0, 1.0, 1.0)),
),
(
"magenta",
Oklab::from_color_unclamped(LinSrgb::new(1.0, 0.0, 1.0)),
),
(
"black",
Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 0.0)),
),
(
"grey",
Oklab::from_color_unclamped(LinSrgb::new(0.5, 0.5, 0.5)),
),
(
"yellow",
Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 0.0)),
),
(
"blue",
Oklab::from_color_unclamped(LinSrgb::new(0.0, 0.0, 1.0)),
),
(
"white",
Oklab::from_color_unclamped(LinSrgb::new(1.0, 1.0, 1.0)),
),
];
const EPSILON: f64 = 1e-8;
for (name, color) in colors {
let rgb: Rgb<encoding::Srgb, u8> =
crate::Srgb::<f64>::from_color_unclamped(color).into_format();
println!(
"\n\
roundtrip of {} (#{:x} / {:?})\n\
=================================================",
name, rgb, color
);
println!("Color is white: {}", color.is_white(EPSILON));
let okhsl = Okhsl::from_color_unclamped(color);
println!("Okhsl: {:?}", okhsl);
let roundtrip_color = Oklab::from_color_unclamped(okhsl);
assert!(
Oklab::visually_eq(roundtrip_color, color, EPSILON),
"'{}' failed.\n{:?}\n!=\n{:?}",
name,
roundtrip_color,
color
);
}
}
#[test]
fn test_blue() {
let lab = Oklab::new(
0.45201371519623734_f64,
-0.03245697990291002,
-0.3115281336419824,
);
let okhsl = Okhsl::<f64>::from_color_unclamped(lab);
assert!(
abs_diff_eq!(
okhsl.hue.into_raw_degrees(),
360.0 * 0.7334778365225699,
epsilon = 1e-10
),
"{}\n!=\n{}",
okhsl.hue.into_raw_degrees(),
360.0 * 0.7334778365225699
);
assert!(
abs_diff_eq!(okhsl.saturation, 0.9999999897262261, epsilon = 1e-8),
"{}\n!=\n{}",
okhsl.saturation,
0.9999999897262261
);
assert!(
abs_diff_eq!(okhsl.lightness, 0.366565335813274, epsilon = 1e-10),
"{}\n!=\n{}",
okhsl.lightness,
0.366565335813274
);
}
#[test]
fn test_srgb_to_okhsl() {
let red_hex = "#834941";
let rgb: Srgb<f64> = Srgb::from_str(red_hex).unwrap().into_format();
let lin_rgb = LinSrgb::<f64>::from_color_unclamped(rgb);
let oklab = Oklab::from_color_unclamped(lin_rgb);
println!(
"RGB: {:?}\n\
LinRgb: {:?}\n\
Oklab: {:?}",
rgb, lin_rgb, oklab
);
let okhsl = Okhsl::from_color_unclamped(oklab);
assert_relative_eq!(
okhsl.hue.into_raw_degrees(),
360.0 * 0.07992730371382328,
epsilon = 1e-10,
max_relative = 1e-13
);
assert_relative_eq!(okhsl.saturation, 0.4629217183454986, epsilon = 1e-10);
assert_relative_eq!(okhsl.lightness, 0.3900998146147427, epsilon = 1e-10);
}
#[test]
fn test_okhsl_to_srgb() {
let okhsl = Okhsl::new(0.0_f32, 0.5, 0.5);
let rgb = Srgb::from_color_unclamped(okhsl);
let rgb8: Rgb<encoding::Srgb, u8> = rgb.into_format();
let hex_str = format!("{:x}", rgb8);
assert_eq!(hex_str, "aa5a74");
}
#[test]
fn test_okhsl_to_srgb_saturated_black() {
let okhsl = Okhsl::new(0.0_f32, 1.0, 0.0);
let rgb = Srgb::from_color_unclamped(okhsl);
assert_relative_eq!(rgb, Srgb::new(0.0, 0.0, 0.0));
}
struct_of_arrays_tests!(
Okhsl,
Okhsl::new(0.1f32, 0.2, 0.3),
Okhsl::new(0.2, 0.3, 0.4),
Okhsl::new(0.3, 0.4, 0.5)
);
mod alpha {
use crate::okhsl::Okhsla;
struct_of_arrays_tests!(
Okhsla,
Okhsla::new(0.1f32, 0.2, 0.3, 0.4),
Okhsla::new(0.2, 0.3, 0.4, 0.5),
Okhsla::new(0.3, 0.4, 0.5, 0.6)
);
}
}