use crate::base::palette::{SYSTEM_16, XTERM_256};
use crate::base::Rgba;
const CUBE_THRESHOLDS: [u8; 5] = [48, 115, 155, 195, 235];
fn cube_index(v: u8) -> usize {
CUBE_THRESHOLDS.iter().position(|&t| v < t).unwrap_or(5)
}
fn sq_dist(a: Rgba, b: Rgba) -> u32 {
let d = |x: u8, y: u8| {
let d = x as i32 - y as i32;
(d * d) as u32
};
d(a.r, b.r) + d(a.g, b.g) + d(a.b, b.b)
}
fn luma(c: Rgba) -> u32 {
(2126 * c.r as u32 + 7152 * c.g as u32 + 722 * c.b as u32) / 10000
}
pub fn nearest_xterm256(c: Rgba) -> u8 {
let ci = (cube_index(c.r), cube_index(c.g), cube_index(c.b));
let cube_idx = (16 + 36 * ci.0 + 6 * ci.1 + ci.2) as u8;
let cube_dist = sq_dist(c, XTERM_256[cube_idx as usize]);
let gray_step = (luma(c) as i32 - 8 + 5).div_euclid(10).clamp(0, 23);
let gray_idx = (232 + gray_step) as u8;
let gray_dist = sq_dist(c, XTERM_256[gray_idx as usize]);
if gray_dist < cube_dist {
gray_idx
} else {
cube_idx
}
}
pub fn nearest_ansi16(c: Rgba) -> u8 {
nearest_in(&SYSTEM_16, 0, c, &[], None).0
}
pub fn quantize_pair_256(fg: Rgba, bg: Rgba) -> (u8, u8) {
quantize_pair_256_assigned(fg, bg, &[])
}
pub type PaletteAssignment<'a> = &'a [(Rgba, u8)];
pub fn nearest_xterm256_assigned(c: Rgba, assignment: PaletteAssignment) -> u8 {
assignment
.iter()
.find(|(a, _)| rgb_eq(*a, c))
.map_or_else(|| nearest_xterm256(c), |(_, i)| *i)
}
pub fn quantize_pair_256_assigned(fg: Rgba, bg: Rgba, assignment: PaletteAssignment) -> (u8, u8) {
let qbg = nearest_xterm256_assigned(bg, assignment);
let qfg = nearest_xterm256_assigned(fg, assignment);
if qfg != qbg || rgb_eq(fg, bg) {
return (qfg, qbg);
}
let (nudged, _) = nearest_in(
&XTERM_256[16..],
16,
fg,
&[qbg],
Some((qbg, ordering(fg, bg))),
);
(nudged, qbg)
}
pub fn quantize_pair_16(fg: Rgba, bg: Rgba) -> (u8, u8) {
let qbg = nearest_ansi16(bg);
let qfg = nearest_ansi16(fg);
if qfg != qbg || rgb_eq(fg, bg) {
return (qfg, qbg);
}
let (nudged, _) = nearest_in(&SYSTEM_16, 0, fg, &[qbg], Some((qbg, ordering(fg, bg))));
(nudged, qbg)
}
const ASSIGNABLE_256: usize = 240;
pub fn quantize_set_256<const N: usize>(colors: [Rgba; N]) -> [u8; N] {
let mut out = [0u8; N];
quantize_set_256_into(&colors, &mut out);
out
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum PairIntent {
Distinct,
Same,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct GroundIntent<'a>(&'a [(usize, usize, PairIntent)]);
impl GroundIntent<'static> {
pub const UNDECLARED: GroundIntent<'static> = GroundIntent(&[]);
}
impl<'a> GroundIntent<'a> {
pub const fn new(pairs: &'a [(usize, usize, PairIntent)]) -> Self {
GroundIntent(pairs)
}
pub const fn pairs(&self) -> &'a [(usize, usize, PairIntent)] {
self.0
}
pub fn get(&self, i: usize, j: usize) -> Option<PairIntent> {
self.0
.iter()
.find(|&&(a, b, _)| (a == i && b == j) || (a == j && b == i))
.map(|&(_, _, k)| k)
}
}
pub fn quantize_set_256_into(colors: &[Rgba], out: &mut [u8]) {
quantize_set_256_into_with(colors, GroundIntent::UNDECLARED, out);
}
pub fn quantize_set_256_into_with(colors: &[Rgba], intent: GroundIntent, out: &mut [u8]) {
let n = colors.len();
assert!(
out.len() >= n,
"quantize_set_256_into: out is {} long for {n} colors",
out.len()
);
let pairs = intent.pairs();
for (p, &(a, b, _)) in pairs.iter().enumerate() {
assert!(
a < n && b < n,
"GroundIntent names pair ({a}, {b}) in a set of {n} colors"
);
assert!(
a != b,
"GroundIntent names ground {a} as a pair with itself"
);
assert!(
!pairs[..p]
.iter()
.any(|&(x, y, _)| (x == a && y == b) || (x == b && y == a)),
"GroundIntent names pair ({a}, {b}) twice — the second is silently ignored"
);
}
let may_merge = |i: usize, j: usize| intent.get(i, j) == Some(PairIntent::Same);
let natural: Vec<u8> = colors.iter().map(|c| nearest_xterm256(*c)).collect();
let mut order: Vec<usize> = (0..n).collect();
order.sort_by_key(|&k| (sq_dist(XTERM_256[natural[k] as usize], colors[k]), k));
let mut assigned: Vec<Option<u8>> = vec![None; n];
for &k in order.iter() {
if let Some(same) = (0..n).find(|&j| assigned[j].is_some() && rgb_eq(colors[j], colors[k]))
{
assigned[k] = assigned[same];
continue;
}
let Some(blocker) = (0..n).find(|&j| assigned[j] == Some(natural[k])) else {
assigned[k] = Some(natural[k]);
continue;
};
if (0..n).all(|j| assigned[j] != Some(natural[k]) || may_merge(j, k)) {
assigned[k] = Some(natural[k]);
continue;
}
let mut blocked: Vec<u8> = (0..n).filter_map(|j| assigned[j]).collect();
blocked.extend(
(0..n)
.filter(|&j| j != k && assigned[j].is_none())
.map(|j| natural[j]),
);
if blocked.len() >= ASSIGNABLE_256 {
assigned[k] = Some(natural[k]);
continue;
}
let anchor = assigned[blocker].expect("the blocker holds an index by construction");
let lighter = luma(colors[k]) >= luma(colors[blocker]);
let (idx, _) = nearest_in(
&XTERM_256[16..],
16,
colors[k],
&blocked,
Some((anchor, lighter)),
);
assigned[k] = Some(idx);
}
for (i, slot) in assigned.into_iter().enumerate() {
out[i] = slot.expect("every color is placed exactly once");
}
}
fn rgb_eq(a: Rgba, b: Rgba) -> bool {
a.r == b.r && a.g == b.g && a.b == b.b
}
fn ordering(fg: Rgba, bg: Rgba) -> bool {
luma(fg) >= luma(bg)
}
fn nearest_in(
table: &[Rgba],
base: u8,
c: Rgba,
blocked: &[u8],
ordered_against: Option<(u8, bool)>,
) -> (u8, u32) {
let anchor_luma = ordered_against.map(|(i, _)| luma(XTERM_256[i as usize]));
let c_not_darker = ordered_against.map(|(_, o)| o);
let mut best: Option<(u8, u32)> = None;
let mut best_unordered: Option<(u8, u32)> = None;
for (i, &entry) in table.iter().enumerate() {
let idx = base + i as u8;
if blocked.contains(&idx) {
continue;
}
let d = sq_dist(c, entry);
if best_unordered.is_none_or(|(_, bd)| d < bd) {
best_unordered = Some((idx, d));
}
if let (Some(lighter), Some(anchor)) = (c_not_darker, anchor_luma) {
let ok = if lighter {
luma(entry) >= anchor
} else {
luma(entry) <= anchor
};
if !ok {
continue;
}
}
if best.is_none_or(|(_, bd)| d < bd) {
best = Some((idx, d));
}
}
best.or(best_unordered)
.expect("palette tables are non-empty")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::palette;
#[test]
fn thresholds_are_base_level_midpoints() {
for (i, &t) in CUBE_THRESHOLDS.iter().enumerate() {
let lo = palette::CUBE_LEVELS[i] as u16;
let hi = palette::CUBE_LEVELS[i + 1] as u16;
assert_eq!(t as u16, (lo + hi).div_ceil(2), "midpoint {i}");
}
assert_eq!(palette::CUBE_LEVELS, [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]);
}
#[test]
fn cube_corners_map_exactly() {
assert_eq!(nearest_xterm256(Rgba::rgb(0, 0, 0)), 16);
assert_eq!(nearest_xterm256(Rgba::rgb(255, 255, 255)), 231);
assert_eq!(nearest_xterm256(Rgba::rgb(255, 0, 0)), 196);
assert_eq!(nearest_xterm256(Rgba::rgb(0, 255, 0)), 46);
assert_eq!(nearest_xterm256(Rgba::rgb(0, 0, 255)), 21);
assert_eq!(nearest_xterm256(Rgba::rgb(95, 135, 175)), 67); for c in [Rgba::rgb(3, 7, 250), Rgba::rgb(130, 128, 126)] {
let idx = nearest_xterm256(c);
assert!(idx >= 16);
let _ = palette::xterm_256(idx); }
}
#[test]
fn grays_prefer_the_ramp() {
assert_eq!(nearest_xterm256(Rgba::rgb(128, 128, 128)), 244);
assert_eq!(nearest_xterm256(Rgba::rgb(8, 8, 8)), 232);
assert_eq!(nearest_xterm256(Rgba::rgb(238, 238, 238)), 255);
}
#[test]
fn ansi16_primaries_against_shared_table() {
assert_eq!(nearest_ansi16(Rgba::rgb(0, 0, 0)), 0);
assert_eq!(nearest_ansi16(Rgba::rgb(255, 0, 0)), 9);
assert_eq!(nearest_ansi16(Rgba::rgb(130, 10, 10)), 1); assert_eq!(nearest_ansi16(Rgba::rgb(255, 255, 255)), 15);
assert_eq!(nearest_ansi16(Rgba::rgb(0, 190, 190)), 6);
assert_eq!(nearest_ansi16(Rgba::rgb(192, 192, 192)), 7);
}
#[test]
fn pair_preserves_dark_theme_faint_text() {
let bg = Rgba::rgb(26, 27, 38);
let fg = Rgba::rgb(30, 30, 40);
assert_eq!(
nearest_xterm256(bg),
nearest_xterm256(fg),
"premise: collision"
);
let (qfg, qbg) = quantize_pair_256(fg, bg);
assert_ne!(qfg, qbg, "distinct colors stay distinct");
assert!(
luma(XTERM_256[qfg as usize]) >= luma(XTERM_256[qbg as usize]),
"ordering preserved: fg {qfg} vs bg {qbg}"
);
}
#[test]
fn pair_without_collision_is_plain_nearest() {
let fg = Rgba::rgb(255, 0, 0);
let bg = Rgba::rgb(0, 0, 0);
assert_eq!(quantize_pair_256(fg, bg), (196, 16));
assert_eq!(quantize_pair_16(fg, bg), (9, 0));
}
#[test]
fn pair_identical_colors_stay_identical() {
let c = Rgba::rgb(30, 30, 40);
let (qfg, qbg) = quantize_pair_256(c, c);
assert_eq!(qfg, qbg, "genuinely identical colors may collapse");
}
#[test]
fn set_identical_colors_share_one_index() {
let a = Rgba::rgb(30, 30, 40);
let far = Rgba::rgb(200, 30, 30);
let out = quantize_set_256([a, a, far]);
assert_eq!(out[0], out[1], "identical colors may share an entry");
assert_eq!(out[0], nearest_xterm256(a), "and it is the natural one");
assert_ne!(out[2], out[0]);
}
#[test]
fn set_without_collision_is_plain_nearest() {
let colors = [
Rgba::rgb(255, 0, 0),
Rgba::rgb(0, 0, 0),
Rgba::rgb(255, 255, 255),
];
assert_eq!(quantize_set_256(colors), [196, 16, 231]);
}
#[test]
fn set_separates_a_three_way_pileup_keeping_order() {
let dark = Rgba::rgb(26, 27, 38);
let mid = Rgba::rgb(28, 29, 36);
let light = Rgba::rgb(30, 30, 40);
let n = nearest_xterm256(dark);
assert!(
nearest_xterm256(mid) == n && nearest_xterm256(light) == n,
"premise: all three collide on {n}"
);
let [qd, qm, ql] = quantize_set_256([dark, mid, light]);
assert!(qd != qm && qm != ql && qd != ql, "{qd} {qm} {ql}");
let l = |i: u8| luma(XTERM_256[i as usize]);
assert!(l(qd) <= l(qm) && l(qm) <= l(ql), "{qd} {qm} {ql}");
}
#[test]
fn set_never_moves_an_exactly_representable_color() {
let white = Rgba::rgb(255, 255, 255);
let off = Rgba::rgb(250, 250, 250);
assert_eq!(
nearest_xterm256(white),
nearest_xterm256(off),
"premise: collision"
);
let [qw, qo] = quantize_set_256([white, off]);
assert_eq!(qw, 231, "the exact one keeps its entry");
assert_ne!(qo, 231);
let [qo2, qw2] = quantize_set_256([off, white]);
assert_eq!((qw2, qo2), (qw, qo));
}
#[test]
fn set_degenerate_sizes_are_total() {
assert_eq!(quantize_set_256([Rgba::rgb(255, 0, 0)]), [196]);
assert_eq!(quantize_set_256::<0>([]), [0u8; 0]);
}
fn set_with(colors: &[Rgba], intent: GroundIntent) -> Vec<u8> {
let mut out = vec![0u8; colors.len()];
quantize_set_256_into_with(colors, intent, &mut out);
out
}
#[test]
fn set_a_pair_declared_same_merges_what_silence_separates() {
let white = Rgba::rgb(255, 255, 255);
let off = Rgba::rgb(250, 250, 250);
assert_eq!(
nearest_xterm256(white),
nearest_xterm256(off),
"premise: collision"
);
let colors = [white, off];
let silent = set_with(&colors, GroundIntent::UNDECLARED);
assert_ne!(silent[0], silent[1], "undeclared: elevation wins");
let same = set_with(&colors, GroundIntent::new(&[(0, 1, PairIntent::Same)]));
assert_eq!(
same,
vec![231, 231],
"declared same: one surface, on the natural entry"
);
assert_eq!(
same,
set_with(&colors, GroundIntent::new(&[(1, 0, PairIntent::Same)]))
);
}
#[test]
fn set_an_empty_or_distinct_declaration_is_exactly_silence() {
let white = Rgba::rgb(255, 255, 255);
let off = Rgba::rgb(250, 250, 250);
let colors = [white, off];
let silent = set_with(&colors, GroundIntent::UNDECLARED);
assert_ne!(silent[0], silent[1], "premise: silence separates");
assert_eq!(silent[0], 231, "the exactly-represented one keeps it");
assert!(luma(XTERM_256[silent[1] as usize]) <= luma(XTERM_256[silent[0] as usize]));
assert_eq!(
set_with(&colors, GroundIntent::new(&[])),
silent,
"an empty declaration is not a declaration of sameness"
);
assert_eq!(
set_with(&colors, GroundIntent::new(&[(0, 1, PairIntent::Distinct)])),
silent,
"declaring the default is the default"
);
}
#[test]
fn set_merge_never_crosses_a_non_same_pair_through_a_third_ground() {
let colors = [
Rgba::rgb(26, 27, 38),
Rgba::rgb(28, 29, 36),
Rgba::rgb(30, 30, 40),
];
let n = nearest_xterm256(colors[0]);
assert!(
colors.iter().all(|c| nearest_xterm256(*c) == n),
"premise: all three collide on {n}"
);
let out = set_with(
&colors,
GroundIntent::new(&[(0, 1, PairIntent::Same), (1, 2, PairIntent::Same)]),
);
assert_ne!(out[0], out[2], "the non-Same pair stays apart: {out:?}");
assert!(
out[1] == out[0] || out[1] == out[2],
"the Same-with-both one merges rather than taking a third entry: {out:?}"
);
}
#[test]
fn set_identical_colors_share_even_when_declared_distinct() {
let c = Rgba::rgb(30, 30, 40);
let out = set_with(&[c, c], GroundIntent::new(&[(0, 1, PairIntent::Distinct)]));
assert_eq!(out[0], out[1]);
assert_eq!(out[0], nearest_xterm256(c));
}
#[test]
#[should_panic(expected = "names pair (0, 5) in a set of 2 colors")]
fn set_declaration_out_of_range_is_a_panic_not_a_shrug() {
set_with(
&[Rgba::rgb(0, 0, 0), Rgba::rgb(255, 255, 255)],
GroundIntent::new(&[(0, 5, PairIntent::Same)]),
);
}
#[test]
#[should_panic(expected = "names ground 1 as a pair with itself")]
fn set_declaring_a_ground_against_itself_is_a_panic() {
set_with(
&[Rgba::rgb(0, 0, 0), Rgba::rgb(255, 255, 255)],
GroundIntent::new(&[(1, 1, PairIntent::Same)]),
);
}
#[test]
#[should_panic(expected = "names pair (1, 0) twice")]
fn set_declaring_one_pair_twice_is_a_panic_not_first_wins() {
set_with(
&[Rgba::rgb(0, 0, 0), Rgba::rgb(255, 255, 255)],
GroundIntent::new(&[(0, 1, PairIntent::Same), (1, 0, PairIntent::Distinct)]),
);
}
#[test]
fn pair_16_collision_nudges_with_ordering() {
let bg = Rgba::rgb(10, 10, 10);
let fg = Rgba::rgb(40, 40, 40);
assert_eq!(nearest_ansi16(bg), nearest_ansi16(fg), "premise: collision");
let (qfg, qbg) = quantize_pair_16(fg, bg);
assert_ne!(qfg, qbg);
assert!(luma(SYSTEM_16[qfg as usize]) >= luma(SYSTEM_16[qbg as usize]));
}
#[test]
fn pair_darker_fg_ordering() {
let bg = Rgba::rgb(255, 255, 255);
let fg = Rgba::rgb(246, 246, 248);
let (qfg, qbg) = quantize_pair_256(fg, bg);
assert_ne!(qfg, qbg);
assert!(luma(XTERM_256[qfg as usize]) <= luma(XTERM_256[qbg as usize]));
}
}