use crate::error::{MunsellError, Result};
use crate::semantic_overlay::parse_hue_to_number;
use crate::types::MunsellColor;
pub const VALUE_CORRECTION: f64 = 0.898734;
pub const CHROMA_CORRECTION: f64 = 4.638094;
const HUE_COEFFS: [f64; 9] = [
-2.930815, 6.500663, 15.948564, -7.674857, 6.927067, 9.247554, -17.936596, -10.873069, 7.078384, ];
const N_HARMONICS: usize = 4;
pub fn predict_hue_correction(hue_degrees: f64) -> f64 {
let hue_rad = hue_degrees.to_radians();
let mut correction = HUE_COEFFS[0];
for k in 1..=N_HARMONICS {
let idx = 2 * k - 1;
correction += HUE_COEFFS[idx] * ((k as f64) * hue_rad).cos();
correction += HUE_COEFFS[idx + 1] * ((k as f64) * hue_rad).sin();
}
correction
}
fn hue_number_to_degrees(hue_num: f64) -> f64 {
hue_num * 9.0
}
fn degrees_to_hue_number(degrees: f64) -> f64 {
let mut deg = degrees % 360.0;
if deg < 0.0 {
deg += 360.0;
}
deg / 9.0
}
fn hue_number_to_string(hue_num: f64) -> String {
let mut hue = hue_num % 40.0;
if hue < 0.0 {
hue += 40.0;
}
let families = ["R", "YR", "Y", "GY", "G", "BG", "B", "PB", "P", "RP"];
let family_idx = (hue / 4.0).floor() as usize % 10;
let family = families[family_idx];
let num_in_family = (hue % 4.0) * 2.5;
if num_in_family == 0.0 {
let prev_family = families[(family_idx + 9) % 10];
format!("10{}", prev_family)
} else {
format!("{:.1}{}", num_in_family, family)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ScreenCorrector {
pub value_correction: f64,
pub chroma_correction: f64,
pub min_chroma: f64,
pub min_value: f64,
pub max_value: f64,
}
impl Default for ScreenCorrector {
fn default() -> Self {
Self::new()
}
}
impl ScreenCorrector {
#[must_use]
pub fn new() -> Self {
Self {
value_correction: VALUE_CORRECTION,
chroma_correction: CHROMA_CORRECTION,
min_chroma: 0.0,
min_value: 0.0,
max_value: 10.0,
}
}
#[must_use]
pub fn with_custom(value_correction: f64, chroma_correction: f64) -> Self {
Self {
value_correction,
chroma_correction,
min_chroma: 0.0,
min_value: 0.0,
max_value: 10.0,
}
}
pub fn correct(&self, screen_color: &MunsellColor) -> Result<MunsellColor> {
if screen_color.is_neutral() {
let corrected_value = (screen_color.value - self.value_correction)
.clamp(self.min_value, self.max_value);
return Ok(MunsellColor::new_neutral(corrected_value));
}
let hue_str = screen_color.hue.as_ref().ok_or_else(|| MunsellError::InvalidNotation {
notation: screen_color.notation.clone(),
reason: "Expected hue for chromatic color".to_string(),
})?;
let hue_num = parse_hue_to_number(hue_str).ok_or_else(|| MunsellError::InvalidNotation {
notation: screen_color.notation.clone(),
reason: format!("Failed to parse hue: {}", hue_str),
})?;
let hue_degrees = hue_number_to_degrees(hue_num);
let hue_correction = predict_hue_correction(hue_degrees);
let corrected_hue_degrees = hue_degrees - hue_correction;
let corrected_hue_num = degrees_to_hue_number(corrected_hue_degrees);
let corrected_hue_str = hue_number_to_string(corrected_hue_num);
let corrected_value = (screen_color.value - self.value_correction)
.clamp(self.min_value, self.max_value);
let screen_chroma = screen_color.chroma.unwrap_or(0.0);
let corrected_chroma = (screen_chroma - self.chroma_correction).max(self.min_chroma);
if corrected_chroma < 0.1 {
return Ok(MunsellColor::new_neutral(corrected_value));
}
Ok(MunsellColor::new_chromatic(
corrected_hue_str,
corrected_value,
corrected_chroma,
))
}
#[must_use]
pub fn get_hue_correction(&self, hue_degrees: f64) -> f64 {
predict_hue_correction(hue_degrees)
}
}
pub fn correct_screen_color(screen_color: &MunsellColor) -> Result<MunsellColor> {
ScreenCorrector::new().correct(screen_color)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hue_correction_cool_colors() {
let correction = predict_hue_correction(180.0);
assert!(correction < -30.0, "Teal should shift toward cyan, got {}", correction);
let correction = predict_hue_correction(182.0);
assert!(correction < -30.0, "Aqua should shift toward cyan, got {}", correction);
}
#[test]
fn test_hue_correction_warm_colors() {
let correction = predict_hue_correction(90.0);
assert!(correction > 20.0, "Beige region should shift toward yellow, got {}", correction);
let correction = predict_hue_correction(110.0);
assert!(correction > 20.0, "Yellow should shift toward yellow, got {}", correction);
}
#[test]
fn test_hue_correction_primaries() {
let correction = predict_hue_correction(20.0);
assert!(correction.abs() < 15.0, "Red should be stable, got {}", correction);
let correction = predict_hue_correction(245.0);
assert!(correction.abs() < 20.0, "Blue should be relatively stable, got {}", correction);
}
#[test]
fn test_value_correction() {
let corrector = ScreenCorrector::new();
let screen = MunsellColor::new_chromatic("5R".to_string(), 7.0, 10.0);
let physical = corrector.correct(&screen).unwrap();
assert!(physical.value < screen.value);
assert!((physical.value - (7.0 - VALUE_CORRECTION)).abs() < 0.01);
}
#[test]
fn test_chroma_correction() {
let corrector = ScreenCorrector::new();
let screen = MunsellColor::new_chromatic("5R".to_string(), 5.0, 12.0);
let physical = corrector.correct(&screen).unwrap();
assert!(physical.chroma.unwrap() < screen.chroma.unwrap());
assert!((physical.chroma.unwrap() - (12.0 - CHROMA_CORRECTION)).abs() < 0.01);
}
#[test]
fn test_neutral_color() {
let corrector = ScreenCorrector::new();
let screen = MunsellColor::new_neutral(7.0);
let physical = corrector.correct(&screen).unwrap();
assert!(physical.is_neutral());
assert!((physical.value - (7.0 - VALUE_CORRECTION)).abs() < 0.01);
}
#[test]
fn test_low_chroma_becomes_neutral() {
let corrector = ScreenCorrector::new();
let screen = MunsellColor::new_chromatic("5R".to_string(), 5.0, 4.0);
let physical = corrector.correct(&screen).unwrap();
assert!(physical.chroma.unwrap_or(0.0) < 0.1);
}
#[test]
fn test_hue_number_conversions() {
let hue_num = 15.0; let degrees = hue_number_to_degrees(hue_num);
assert!((degrees - 135.0).abs() < 0.01);
let back = degrees_to_hue_number(degrees);
assert!((back - hue_num).abs() < 0.01);
}
#[test]
fn test_hue_string_generation() {
assert_eq!(hue_number_to_string(0.0), "10RP");
assert_eq!(hue_number_to_string(2.0), "5.0R");
assert_eq!(hue_number_to_string(4.0), "10R");
assert_eq!(hue_number_to_string(6.0), "5.0YR");
assert_eq!(hue_number_to_string(10.0), "5.0Y");
}
#[test]
fn test_correct_screen_color_function() {
let screen = MunsellColor::new_chromatic("5BG".to_string(), 7.0, 8.0);
let physical = correct_screen_color(&screen).unwrap();
assert!(physical.value < screen.value);
assert!(physical.chroma.unwrap() < screen.chroma.unwrap());
}
}