use crate::Rgb;
const XTERM_256: [(u8, u8, u8); 256] = build_xterm_256();
const ANSI16_NAMES: [&str; 16] = [
"Black", "Red", "Green", "Yellow",
"Blue", "Magenta", "Cyan", "White",
"Bright Black", "Bright Red", "Bright Green", "Bright Yellow",
"Bright Blue", "Bright Magenta", "Bright Cyan", "Bright White",
];
const fn build_xterm_256() -> [(u8, u8, u8); 256] {
let mut p = [(0u8, 0u8, 0u8); 256];
p[0] = (0, 0, 0);
p[1] = (128, 0, 0);
p[2] = (0, 128, 0);
p[3] = (128, 128, 0);
p[4] = (0, 0, 128);
p[5] = (128, 0, 128);
p[6] = (0, 128, 128);
p[7] = (192, 192, 192);
p[8] = (128, 128, 128);
p[9] = (255, 0, 0);
p[10] = (0, 255, 0);
p[11] = (255, 255, 0);
p[12] = (0, 0, 255);
p[13] = (255, 0, 255);
p[14] = (0, 255, 255);
p[15] = (255, 255, 255);
let mut i = 16usize;
let levels = [0u8, 95, 135, 175, 215, 255];
let mut r = 0;
while r < 6 {
let mut g = 0;
while g < 6 {
let mut b = 0;
while b < 6 {
p[i] = (levels[r], levels[g], levels[b]);
i += 1;
b += 1;
}
g += 1;
}
r += 1;
}
let mut j = 232usize;
let mut k = 8u8;
while j < 256 {
p[j] = (k, k, k);
j += 1;
k = k.saturating_add(10);
}
p
}
#[must_use]
pub fn fallback_256(rgb: Rgb) -> u8 {
let tl = linear_luma(rgb);
let mut best_idx = 0u8;
let mut best_d = f32::INFINITY;
let mut i = 0u16;
while i < 256 {
let (r, g, b) = XTERM_256[i as usize];
let dr = f32::from(rgb.0) - f32::from(r);
let dg = f32::from(rgb.1) - f32::from(g);
let db = f32::from(rgb.2) - f32::from(b);
let il = linear_luma(Rgb(r, g, b));
let d = dr * dr + dg * dg + db * db + 0.5 * (tl - il).powi(2);
if d < best_d {
best_d = d;
best_idx = i as u8;
}
i += 1;
}
best_idx
}
#[must_use]
pub fn fallback_16_name(rgb: Rgb) -> &'static str {
let mut best = 0usize;
let mut best_d = f32::INFINITY;
let mut i = 0usize;
while i < 16 {
let (r, g, b) = XTERM_256[i];
let dr = f32::from(rgb.0) - f32::from(r);
let dg = f32::from(rgb.1) - f32::from(g);
let db = f32::from(rgb.2) - f32::from(b);
let d = dr * dr + dg * dg + db * db;
if d < best_d {
best_d = d;
best = i;
}
i += 1;
}
ANSI16_NAMES[best]
}
fn linear_luma(rgb: Rgb) -> f32 {
fn lin(byte: u8) -> f32 {
let c = f32::from(byte) / 255.0;
if c <= 0.03928 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
0.2126 * lin(rgb.0) + 0.7152 * lin(rgb.1) + 0.0722 * lin(rgb.2)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn black_maps_to_index_0() {
assert_eq!(fallback_256(Rgb(0, 0, 0)), 0);
}
#[test]
fn white_maps_to_index_15() {
assert_eq!(fallback_256(Rgb(255, 255, 255)), 15);
}
#[test]
fn brand_red_near_256_red() {
let idx = fallback_256(Rgb(0xB5, 0x24, 0x30));
assert!(idx == 125 || idx == 9, "got {idx}, expected ~125 (cube red) or 9 (ANSI bright red)");
}
#[test]
fn ansi_name_for_black() {
assert_eq!(fallback_16_name(Rgb(0, 0, 0)), "Black");
}
#[test]
fn ansi_name_for_brand_red_is_red() {
let name = fallback_16_name(Rgb(0xB5, 0x24, 0x30));
assert!(name.contains("Red"), "expected a Red-ish name, got {name}");
}
}