use crate::core::is_achromatic_chroma_hue;
use crate::termco::AnsiColor;
use crate::theme::Theme;
use crate::{Bits, Color, ColorSpace, Float};
#[derive(Debug)]
struct GrayEntry {
spec: AnsiColor,
lr: Float,
}
impl GrayEntry {
fn new(spec: AnsiColor, value: &Color) -> Option<GrayEntry> {
let [lr, c, h] = *value.to(ColorSpace::Oklrch).as_ref();
if !spec.is_achromatic()
|| !is_achromatic_chroma_hue(c, h, HueLightnessTable::ACHROMATIC_THRESHOLD)
{
return None;
}
Some(GrayEntry { spec, lr })
}
fn key(&self) -> Bits {
self.lr.to_bits()
}
}
#[derive(Debug)]
struct ColorEntry {
spec: AnsiColor,
lr: Float,
h: Float,
}
impl ColorEntry {
fn new(spec: AnsiColor, value: &Color) -> Option<Self> {
let [lr, c, mut h] = *value.to(ColorSpace::Oklrch).as_ref();
if spec.is_achromatic()
|| is_achromatic_chroma_hue(c, h, HueLightnessTable::ACHROMATIC_THRESHOLD)
{
return None;
}
h = h.rem_euclid(360.0);
Some(ColorEntry { spec, lr, h })
}
fn base(&self) -> AnsiColor {
self.spec.to_base()
}
}
#[derive(Debug)]
pub(crate) struct HueLightnessTable {
grays: Vec<GrayEntry>,
colors: Vec<ColorEntry>,
}
impl HueLightnessTable {
const ACHROMATIC_THRESHOLD: Float = 0.05;
pub fn new(theme: &Theme) -> Option<HueLightnessTable> {
use AnsiColor::*;
let mut grays = Vec::with_capacity(4);
for index in [Black, White, BrightBlack, BrightWhite] {
grays.push(GrayEntry::new(index, &theme[index])?);
}
grays.sort_by_key(GrayEntry::key);
let mut colors = Vec::with_capacity(12);
for index in [Red, Yellow, Green, Cyan, Blue, Magenta] {
let regular = ColorEntry::new(index, &theme[index])?;
let index = index.to_bright();
let bright = ColorEntry::new(index, &theme[index])?;
if regular.h <= bright.h {
colors.push(regular);
colors.push(bright);
} else {
colors.push(bright);
colors.push(regular);
}
}
let mut min_hue = Float::MAX;
let mut min_index = usize::MAX;
for (index, entry) in colors.iter().enumerate() {
if entry.h < min_hue {
min_hue = entry.h;
min_index = index;
}
}
if 0 < min_index {
colors.rotate_left(min_index);
}
min_hue = -1.0;
for entry in colors.iter() {
if entry.h < min_hue {
return None;
}
min_hue = entry.h;
}
Some(HueLightnessTable { grays, colors })
}
pub fn find_match(&self, color: &Color) -> AnsiColor {
let [lr, c, h] = *color.to(ColorSpace::Oklrch).as_ref();
if is_achromatic_chroma_hue(c, h, Self::ACHROMATIC_THRESHOLD) {
for index in 0..(self.grays.len() - 1) {
let entry1 = &self.grays[index];
let entry2 = &self.grays[index + 1];
if lr < entry1.lr + (entry2.lr - entry1.lr) / 2.0 {
return entry1.spec;
}
}
return self.grays[self.grays.len() - 1].spec;
}
let length = self.colors.len();
for index in 0..length {
let next_entry = &self.colors[index];
if next_entry.h < h && (index != 0 || h < self.colors[length - 1].h) {
continue;
}
let previous_entry = &self.colors[(index - 1 + length).rem_euclid(length)];
if previous_entry.base() == next_entry.base() {
let result = self.pick_lightness(lr, previous_entry, next_entry);
return result;
}
let mut previous_hue = previous_entry.h;
let next_hue = next_entry.h;
if h < previous_hue {
assert!(
index == 0,
"a decrease in hue only happens between last and first hues"
);
previous_hue -= 360.0
}
if h - previous_hue <= next_hue - h {
let twice_previous_entry = &self.colors[(index + length - 2).rem_euclid(length)];
return self.pick_lightness(lr, twice_previous_entry, previous_entry);
} else {
let twice_next_entry = &self.colors[(index + 1).rem_euclid(length)];
return self.pick_lightness(lr, next_entry, twice_next_entry);
}
}
unreachable!();
}
fn pick_lightness(&self, lr: Float, entry1: &ColorEntry, entry2: &ColorEntry) -> AnsiColor {
if (entry1.lr - lr).abs() <= (entry2.lr - lr).abs() {
entry1.spec
} else {
entry2.spec
}
}
}