use crate::hct::cam16::Cam16;
use crate::hct::hct_solver::HctSolver;
use crate::hct::viewing_conditions::ViewingConditions;
use crate::utils::color_utils::{Argb, ColorUtils};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Hct {
hue: f64,
chroma: f64,
tone: f64,
argb: Argb,
}
impl Eq for Hct {}
impl std::hash::Hash for Hct {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.argb.hash(state);
}
}
impl Hct {
fn new_internal(argb: Argb) -> Self {
let cam = Cam16::from_argb(argb);
Self {
hue: cam.hue,
chroma: cam.chroma,
tone: argb.lstar(),
argb,
}
}
#[must_use]
pub fn new(hue: f64, chroma: f64, tone: f64) -> Self {
let argb = HctSolver::solve_to_argb(hue, chroma, tone);
Self::new_internal(argb)
}
#[must_use]
pub fn from_argb(argb: Argb) -> Self {
Self::new_internal(argb)
}
#[must_use]
pub const fn hue(&self) -> f64 {
self.hue
}
#[must_use]
pub const fn chroma(&self) -> f64 {
self.chroma
}
#[must_use]
pub const fn tone(&self) -> f64 {
self.tone
}
#[must_use]
pub const fn to_argb(&self) -> Argb {
self.argb
}
pub fn set_hue(&mut self, new_hue: f64) {
self.set_internal_state(HctSolver::solve_to_argb(new_hue, self.chroma, self.tone));
}
pub fn set_chroma(&mut self, new_chroma: f64) {
self.set_internal_state(HctSolver::solve_to_argb(self.hue, new_chroma, self.tone));
}
pub fn set_tone(&mut self, new_tone: f64) {
self.set_internal_state(HctSolver::solve_to_argb(self.hue, self.chroma, new_tone));
}
fn set_internal_state(&mut self, argb: Argb) {
self.argb = argb;
let cam = Cam16::from_argb(argb);
self.hue = cam.hue;
self.chroma = cam.chroma;
self.tone = argb.lstar();
}
#[must_use]
pub fn in_viewing_conditions(&self, vc: &ViewingConditions) -> Self {
let cam16 = Cam16::from_argb(self.argb);
let viewed_in_vc = cam16.xyz_in_viewing_conditions(vc);
let recast_in_vc = Cam16::from_xyz_in_viewing_conditions(
viewed_in_vc.x,
viewed_in_vc.y,
viewed_in_vc.z,
&ViewingConditions::default(),
);
Self::new(
recast_in_vc.hue,
recast_in_vc.chroma,
ColorUtils::lstar_from_y(viewed_in_vc.y),
)
}
#[must_use]
pub fn is_blue(hue: f64) -> bool {
(250.0..270.0).contains(&hue)
}
#[must_use]
pub fn is_yellow(hue: f64) -> bool {
(105.0..125.0).contains(&hue)
}
#[must_use]
pub fn is_cyan(hue: f64) -> bool {
(170.0..207.0).contains(&hue)
}
}
impl fmt::Display for Hct {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"HCT({}, {}, {})",
self.hue.round(),
self.chroma.round(),
self.tone.round()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hct_red() {
let hct = Hct::new(0.0, 50.0, 50.0);
assert!((hct.hue() - 0.0).abs() < 1.0 || (hct.hue() - 360.0).abs() < 1.0);
assert!(hct.chroma() > 0.0);
assert!((hct.tone() - 50.0).abs() < 1.0);
}
#[test]
fn test_hct_from_argb() {
let argb = Argb(0xFF00FF00); let hct = Hct::from_argb(argb);
assert_eq!(hct.to_argb(), argb);
assert!(hct.chroma() > 0.0);
}
#[test]
fn test_hct_setters() {
let mut hct = Hct::new(120.0, 60.0, 50.0);
hct.set_hue(200.0);
assert!((hct.hue() - 200.0).abs() < 1.0);
hct.set_chroma(30.0);
assert!((hct.chroma() - 30.0).abs() < 1.0);
hct.set_tone(80.0);
assert!((hct.tone() - 80.0).abs() < 1.0);
}
#[test]
fn test_hct_in_viewing_conditions() {
let hct = Hct::new(0.0, 50.0, 50.0);
let vc = ViewingConditions::default();
let hct_vc = hct.in_viewing_conditions(&vc);
assert!((hct.hue() - hct_vc.hue()).abs() < 1.0);
assert!((hct.chroma() - hct_vc.chroma()).abs() < 1.0);
assert!((hct.tone() - hct_vc.tone()).abs() < 1.0);
}
#[test]
fn test_hct_hue_checks() {
assert!(Hct::is_blue(260.0));
assert!(!Hct::is_blue(100.0));
assert!(Hct::is_yellow(110.0));
assert!(Hct::is_cyan(180.0));
}
#[test]
fn test_hct_roundtrip_in_gamut() {
let hue = 67.0;
let chroma = 20.0;
let tone = 52.0;
let hct = Hct::new(hue, chroma, tone);
let argb = hct.to_argb();
let argb_string = format!("{:X}", argb.0);
assert_eq!(argb_string, "FF967655");
let back_convert = Hct::from_argb(argb);
assert!((back_convert.hue - hue).abs() < 0.5);
assert!((back_convert.chroma - chroma).abs() < 0.5);
assert!((back_convert.tone - tone).abs() < 0.5);
}
#[test]
fn test_hct_clipping() {
let hct = Hct::new(67.0, 91.0, 52.0);
assert!((hct.hue() - 67.0).abs() < 1.0);
assert!(hct.chroma() < 50.0); assert!((hct.tone() - 52.0).abs() < 1.0);
assert_eq!(format!("{:X}", hct.to_argb().0), "FFB26C00");
}
}