use crate::error::Result;
use crate::types::MunsellColor;
use super::MunsellConverter;
impl MunsellConverter {
pub(crate) fn algorithmic_srgb_to_munsell(&self, rgb: [u8; 3]) -> Result<MunsellColor> {
if rgb[0] == 0 && rgb[1] == 0 && rgb[2] == 0 {
return Ok(MunsellColor::new_neutral(0.0));
}
const INV_255: f64 = 1.0 / 255.0;
let srgb_norm = [
rgb[0] as f64 * INV_255,
rgb[1] as f64 * INV_255,
rgb[2] as f64 * INV_255,
];
let linear_rgb = self.srgb_to_linear_rgb(srgb_norm);
let xyz_d65 = self.linear_rgb_to_xyz_d65(linear_rgb);
let xyz_final = xyz_d65;
let xyy = self.xyz_to_xyy(xyz_final);
self.xyy_to_munsell_iterative(xyy)
}
#[allow(dead_code)]
pub(crate) fn xyy_to_munsell(&self, xyy: [f64; 3]) -> Result<MunsellColor> {
let [x, y, big_y] = xyy;
if self.is_achromatic(x, y) {
let value = self.xyz_y_to_munsell_value(big_y);
return Ok(MunsellColor::new_neutral((value * 10.0).round() / 10.0));
}
let white_x = 0.31271; let white_y = 0.32902;
let hue_angle = (y - white_y).atan2(x - white_x);
let hue_degrees = hue_angle.to_degrees();
let munsell_hue = self.degrees_to_munsell_hue(hue_degrees);
let value = self.xyz_y_to_munsell_value(big_y);
let rounded_value = (value * 10.0).round() / 10.0;
let chroma = self.calculate_munsell_chroma(x, y, big_y);
let rounded_chroma = (chroma * 10.0).round() / 10.0;
Ok(MunsellColor::new_chromatic(munsell_hue, rounded_value, rounded_chroma))
}
pub(crate) fn xyy_to_munsell_iterative(&self, xyy: [f64; 3]) -> Result<MunsellColor> {
let [x, y, big_y] = xyy;
let value = self.xyz_y_to_munsell_value(big_y);
if self.is_achromatic(x, y) {
return Ok(MunsellColor::new_neutral((value * 10.0).round() / 10.0));
}
let xyz = self.xyy_to_xyz(xyy);
let lab = self.xyz_to_lab_d65(xyz);
let lchab = self.lab_to_lchab(lab);
let (_hue_initial, _chroma_initial) = self.lchab_to_munsell_estimate(lchab);
let interpolated_result = self.spatial_interpolation_munsell(xyy, value);
if let Some(result) = interpolated_result {
return Ok(result);
}
let hue_degrees = (y - 0.32902).atan2(x - 0.31271).to_degrees();
let munsell_hue = self.degrees_to_munsell_hue(hue_degrees);
let chroma = self.calculate_munsell_chroma(x, y, big_y);
Ok(MunsellColor::new_chromatic(
munsell_hue,
(value * 10.0).round() / 10.0,
(chroma * 10.0).round() / 10.0,
))
}
#[inline]
pub(crate) fn is_achromatic(&self, x: f64, y: f64) -> bool {
let d65_white_x = 0.31271;
let d65_white_y = 0.32902;
let distance = ((x - d65_white_x).powi(2) + (y - d65_white_y).powi(2)).sqrt();
let python_threshold = 0.001;
distance < python_threshold
}
pub(crate) fn xyz_y_to_munsell_value(&self, y: f64) -> f64 {
let y_percent = y * 100.0;
self.munsell_value_astm_d1535(y_percent)
}
fn munsell_value_astm_d1535(&self, y: f64) -> f64 {
if y <= 0.0 {
return 0.0;
}
if y >= 100.0 {
return 10.0;
}
let astm_table = [
(0.0, 0.0),
(1.0, 0.863),
(2.0, 1.386),
(3.0, 1.796),
(4.0, 2.157),
(5.0, 2.645),
(6.0, 2.976),
(7.0, 3.282),
(8.0, 3.568),
(9.0, 3.837),
(10.0, 3.721),
(15.0, 4.502),
(20.0, 5.082),
(25.0, 5.551),
(30.0, 6.061),
(35.0, 6.515),
(40.0, 6.927),
(45.0, 7.305),
(50.0, 7.538),
(55.0, 7.912),
(60.0, 8.264),
(65.0, 8.597),
(70.0, 8.671),
(75.0, 9.021),
(80.0, 9.357),
(85.0, 9.679),
(90.0, 9.596),
(95.0, 9.886),
(100.0, 10.000),
];
for i in 0..astm_table.len() - 1 {
let (y1, v1) = astm_table[i];
let (y2, v2) = astm_table[i + 1];
if y >= y1 && y <= y2 {
if y2 == y1 {
return v1;
}
let ratio = (y - y1) / (y2 - y1);
let interpolated = v1 + ratio * (v2 - v1);
return interpolated.max(0.0).min(10.0);
}
}
if y < 1.0 {
(y * 1.211).max(0.0)
} else {
10.0
}
}
pub(crate) fn degrees_to_munsell_hue(&self, degrees: f64) -> String {
let normalized = ((degrees % 360.0) + 360.0) % 360.0;
let hue_families = [
(0.0, "R"), (36.0, "YR"), (72.0, "Y"), (108.0, "GY"), (144.0, "G"),
(180.0, "BG"), (216.0, "B"), (252.0, "PB"), (288.0, "P"), (324.0, "RP")
];
for i in 0..hue_families.len() {
let (start_angle, family) = hue_families[i];
let next_angle = if i == hue_families.len() - 1 { 360.0 } else { hue_families[i + 1].0 };
if normalized >= start_angle && normalized < next_angle {
let degrees_within_family = normalized - start_angle;
let hue_step = (degrees_within_family / 3.6) + 1.0;
let floored_hue = (hue_step * 10.0).floor() / 10.0;
let clamped_hue = floored_hue.max(1.0).min(10.0);
if (clamped_hue.fract()).abs() < 0.05 {
return format!("{:.0}{}", clamped_hue.round(), family);
} else {
return format!("{:.1}{}", clamped_hue, family);
}
}
}
"5R".to_string()
}
pub(crate) fn calculate_munsell_chroma(&self, x: f64, y: f64, big_y: f64) -> f64 {
let d65_white_x = 0.31271;
let d65_white_y = 0.32902;
let chromaticity_distance = ((x - d65_white_x).powi(2) + (y - d65_white_y).powi(2)).sqrt();
let luminance_factor = if big_y > 0.0 {
let y_percent = big_y * 100.0;
y_percent.powf(1.0/3.0) / 4.64
} else {
0.02
};
let base_chroma_scaling = 85.0;
let distance_factor = if chromaticity_distance > 0.05 {
1.2
} else if chromaticity_distance < 0.01 {
0.5
} else {
1.0
};
let chroma = chromaticity_distance * base_chroma_scaling * luminance_factor * distance_factor;
chroma.max(0.0).min(25.0)
}
pub(crate) fn lchab_to_munsell_estimate(&self, lchab: [f64; 3]) -> (f64, f64) {
let [_l, c, h] = lchab;
let munsell_hue_approx = (h + 30.0) % 360.0 / 36.0;
let munsell_chroma_approx = c / 8.0;
(munsell_hue_approx, munsell_chroma_approx.max(0.0).min(30.0))
}
}