use super::*;
use std::sync::OnceLock;
use std::cmp::Ordering;
use std::ops::{Deref, DerefMut};
use palette::{
Lighten,
Darken,
Desaturate,
Mix,
Clamp,
FromColor,
color_difference::{DeltaE, ImprovedDeltaE},
cam16::{
Cam16,
Cam16Jmh,
Cam16UcsJab,
Cam16UcsJmh,
BakedParameters,
Parameters,
StaticWp,
Surround,
},
hues::Cam16Hue,
convert::FromColorUnclamped,
white_point::D65,
rgb::Rgb,
encoding::{
Srgb as SrgbEnc,
Linear
}
};
#[derive(Debug)]
pub struct Salience;
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Spec(Cam16UcsJmh<f32>);
pub type Hist = Histo<Spec>;
pub const DARKEST: f32 = 5.0;
pub const LIGHTEST: f32 = 95.0;
pub const MIN_COLORFULNESS: f32 = 10.0;
pub const COL_DARK_MIN_SAL: f32 = 7.5;
pub const COL_LIGHT_MIN_SAL: f32 = 15.0;
pub const BG_DARK_ENFORCE_L_DELTA: f32 = 5.0;
pub const BG_LIGHT_ENFORCE_L_DELTA: f32 = 10.0;
pub const BG_DARK_MIN_SAL: f32 = 7.5;
pub const BG_LIGHT_MIN_SAL: f32 = 15.0;
pub const BG_DARK_L_SOFTMIN: f32 = 2.0;
pub const BG_DARK_L_SOFTMAX: f32 = 5.0;
pub const BG_LIGHT_L_SOFTMIN: f32 = 90.0;
pub const BG_LIGHT_L_SOFTMAX: f32 = 95.0;
pub const BG_COLORFULNESS_MIN: f32 = 2.5;
pub const BG_COLORFULNESS_MAX: f32 = 7.5;
pub const WARM: f32 = 30.0;
pub const COOL: f32 = 210.0;
pub const L_BND: f32 = 100.0;
pub const C_BND: f32 = 128.0;
pub const DARKEST_COL: Spec = Spec::new_const(DARKEST, MIN_COLORFULNESS, Cam16Hue::new(210.0));
pub const LIGHTEST_COL: Spec = Spec::new_const(LIGHTEST, MIN_COLORFULNESS, Cam16Hue::new(30.0));
pub static CAM16_VIEW: OnceLock<BakedParameters<StaticWp<D65>, f32>> = OnceLock::new();
impl BuildHisto<Spec> for Salience {
fn init(bytes: &[u8], threshold: u8, mix: bool, ord: &ColorOrder) -> Option<Vec<Histo<Spec>>> {
if CAM16_VIEW.set(init_view(ord)).is_err() {};
let b = Self::read(bytes);
let ret = Self::gather_cols(b, threshold, mix);
if ret.len() < 2 { None } else { Some(ret) }
}
fn process_deduped(histo: Vec<Histo<Spec>>, ord: &ColorOrder, _bytes: &[u8])-> Vec<Histo<Spec>> {
let mut histo = histo;
let (idx, _) = histo.iter().enumerate().max_by_key(|(_, item)| item.count).unwrap();
let max_histo = histo.remove(idx);
let max_histo_og = max_histo;
let bg = max_histo.color;
let cams: Vec<Spec> = histo.iter().map(|h| h.color).collect();
let bg = constrain_col_as_bg(bg, &cams, ord);
let res = constrain_col_against_cols(bg, &cams, ord, &[get_bg_min_sal(ord)]);
let bg = res[0];
histo = sort_histogram_by_score(histo, bg);
let max_cols = (MAX_COLS-1) as usize;
let len = histo.len();
let last = if len > max_cols {max_cols} else {len};
histo.insert(last, max_histo_og);
histo
}
fn filter_cols(histo: Vec<Spec>) -> Vec<Spec> {
let lights = histo.iter().map(|c| c.lightness).collect::<Vec<_>>();
let darkest = lights.iter().fold(f32::INFINITY, |a, &b| a.min(b)).max(DARKEST);
let lightest = lights.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b)).min(LIGHTEST);
let colorfulnesses = histo.iter().map(|c| c.colorfulness).collect::<Vec<_>>();
let origcl = util::avg(&colorfulnesses);
let lesscl = colorfulnesses.iter().fold(f32::INFINITY, |a, &b| a.min(b));
let ch = if origcl <= MIN_COLORFULNESS { lesscl } else { origcl / 2.5 };
let filt = |x: &Spec| {
x.lightness >= darkest
&& x.lightness <= lightest
&& x.colorfulness >= ch
&& x.improved_salience_naive(&ColorOrder::LightFirst) > COL_DARK_MIN_SAL
&& x.improved_salience_naive(&ColorOrder::DarkFirst) > COL_LIGHT_MIN_SAL
};
histo.into_iter().filter(filt).collect()
}
fn clip_cols(histo: Vec<Histo<Spec>>, ord: &ColorOrder) -> Vec<Histo<Spec>> {
let mut histo = histo;
let bg_histo = if histo.len() > MAX_COLS.into() {histo.remove(MAX_COLS as usize - 1)} else {histo.pop().expect("not empty")};
let bg = bg_histo.color;
let cams: Vec<Spec> = histo.iter().map(|h| h.color).collect();
let bg = constrain_col_as_bg(bg, &cams, ord);
let lt_mod = match ord {
ColorOrder::LightFirst => BG_DARK_ENFORCE_L_DELTA,
ColorOrder::DarkFirst => -BG_LIGHT_ENFORCE_L_DELTA,
};
let l_threshold = bg.lightness + lt_mod;
let filt = |h: &Histo<Spec>| match ord {
ColorOrder::LightFirst => h.color.lightness > l_threshold,
ColorOrder::DarkFirst => h.color.lightness < l_threshold
};
let mut histo: Vec<Histo<Spec>> = histo.into_iter().filter(filt).collect();
let max_cols = (MAX_COLS-1) as usize;
let len = histo.len();
let last = if len > max_cols {max_cols} else {len};
histo.insert(last, bg_histo);
histo.truncate(MAX_COLS.into());
histo
}
fn sort_col(histo: Vec<Hist>, ord: &ColorOrder) -> Vec<Hist> {
let mut histo = histo;
let histo_bg = histo.pop().expect("not empty");
let histo_bg_og = histo_bg;
let bg = histo_bg.color;
let cols: Vec<Spec> = histo.iter().map(|h| h.color).collect();
let bg = constrain_col_as_bg(bg, &cols, ord);
let res = constrain_col_against_cols(bg, &cols, ord, &[get_bg_min_sal(ord)]);
let bg = *res.first().expect("not empty");
histo.sort_by(|a, b| a.color.improved_salience(&bg).partial_cmp(&b.color.improved_salience(&bg)).unwrap_or(Ordering::Equal));
histo.insert(0, histo_bg_og);
histo
}
fn sort_by_key_fn(a: Hist) -> impl Ord {
a.color.colorfulness as i32
}
}
pub fn get_dark_view() -> Parameters<StaticWp<D65>, f32> {
let mut view = Parameters::default_static_wp(140.0); view.background_luminance = 0.2;
view.surround = Surround::Average;
view
}
pub fn get_light_view() -> Parameters<StaticWp<D65>, f32> {
let mut view = Parameters::default_static_wp(500.0); view.background_luminance = 0.8;
view.surround = Surround::Average;
view
}
impl Deref for Spec {
type Target = Cam16UcsJmh<f32>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Spec {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Spec {
pub fn new<H: Into<Cam16Hue<f32>>>(lightness: f32, colorfulness: f32, hue: H) -> Self {
Spec(Cam16UcsJmh::<f32>::new(lightness, colorfulness, hue.into()))
}
pub const fn new_const(lightness: f32, colorfulness: f32, hue: Cam16Hue<f32>) -> Self {
Self(Cam16UcsJmh::<f32>::new_const(lightness, colorfulness, hue))
}
}
impl ColorTrait for Spec { }
impl Difference for Spec {
fn col_diff(&self, &a: &Self, threshold: u8) -> bool {
let t_sclr = 0.80;
self.improved_delta_e(a) <= f32::from(threshold) * t_sclr
}
}
impl DeltaE for Spec {
type Scalar = f32;
fn delta_e(self, other: Self) -> Self::Scalar {
self.0.delta_e(other.0)
}
}
impl ImprovedDeltaE for Spec {
fn improved_delta_e(self, other: Self) -> Self::Scalar {
self.0.improved_delta_e(other.0)
}
}
pub trait DeltaENaive {
fn delta_e_naive(self, other: Self) -> f32;
}
impl DeltaENaive for Spec {
fn delta_e_naive(self, other: Self) -> f32 {
let dl = self.lightness - other.lightness;
let dm = self.colorfulness - other.colorfulness;
(dl.powi(2) + dm.powi(2)).powf(0.5)
}
}
pub trait ImprovedDeltaENaive {
fn improved_delta_e_naive(self, other: Self) -> f32;
}
impl ImprovedDeltaENaive for Spec {
fn improved_delta_e_naive(self, other: Self) -> f32 {
let dl = (self.lightness - other.lightness).powi(2);
let dm = (self.colorfulness - other.colorfulness).powi(2);
1.55 * (dl + dm).powf(0.63 * 0.5)
}
}
impl IntoColor<Srgb> for Spec {
fn into_color(self) -> Srgb {
let view = CAM16_VIEW.get().expect("is set");
let cam16: Cam16Jmh<f32> = self.0.into_color();
let xyz = cam16.into_xyz(*view);
Srgb::from_color(xyz)
}
}
impl FromColorUnclamped<Srgb> for Spec {
fn from_color_unclamped(val: Srgb) -> Self {
let rgb = val;
let view = CAM16_VIEW.get().expect("is set");
let cam16 = Cam16::from_xyz(rgb.into_color(), *view);
let ucs_from_rgb = Cam16UcsJmh::from_color(cam16);
Spec(ucs_from_rgb)
}
}
impl Mix for Spec {
type Scalar = f32;
fn mix(self, other: Self, factor: Self::Scalar) -> Self {
Spec(self.0.mix(other.0, factor))
}
}
impl Clamp for Spec {
fn clamp(self) -> Self {
Spec(self.0.clamp())
}
}
impl FromColorUnclamped<Rgb<Linear<SrgbEnc>>> for Spec {
fn from_color_unclamped(val: Rgb<Linear<SrgbEnc>>) -> Self {
let view = CAM16_VIEW.get().expect("is set");
let cam16 = Cam16::from_xyz(val.into_color(), *view);
let ucs_from_rgb = Cam16UcsJmh::from_color(cam16);
Spec(ucs_from_rgb)
}
}
impl Lighten for Spec {
type Scalar = f32;
fn lighten(self, factor: Self::Scalar) -> Self {
Self(self.0.lighten(factor))
}
fn lighten_fixed(self, amount: Self::Scalar) -> Self {
Self(self.0.lighten_fixed(amount))
}
}
impl Desaturate for Spec {
type Scalar = f32;
fn desaturate(self, factor: Self::Scalar) -> Self {
Self(self.0.desaturate(factor))
}
fn desaturate_fixed(self, amount: Self::Scalar) -> Self {
Self(self.0.desaturate_fixed(amount))
}
}
pub fn init_view(ord: &ColorOrder) -> BakedParameters<StaticWp<D65>, f32> {
let view = match ord {
ColorOrder::LightFirst => get_dark_view(),
ColorOrder::DarkFirst => get_light_view()
};
view.into()
}
pub trait Salient {
fn salience(&self, o: &Self) -> f32;
fn salience_naive(&self, ord: &ColorOrder) -> f32;
fn improved_salience(&self, o: &Self) -> f32;
fn improved_salience_naive(&self, ord: &ColorOrder) -> f32;
}
fn get_sal_weights() -> (f32, f32) {
let wl: f32 = 1.0; let wc: f32 = 1.50;
let norm = normalize_to_sum(&[wl, wc, wc], 3.0);
(norm[0], norm[1])
}
fn normalize_to_sum(slice: &[f32], target_sum: f32) -> Vec<f32> {
let current_sum: f32 = slice.iter().sum();
if current_sum == 0.0 {
let n = slice.len() as f32;
return vec![target_sum / n; slice.len()];
}
slice.iter().map(|&x| x * target_sum / current_sum).collect()
}
impl Salient for Spec {
fn salience(&self, &o: &Self) -> f32 {
let s = Cam16UcsJab::from_color_unclamped(self.0);
let o = Cam16UcsJab::from_color_unclamped(o.0);
let mut dl = s.lightness - o.lightness;
let mut da = s.a - o.a;
let mut db = s.b - o.b;
let (wl, wc) = get_sal_weights();
dl *= wl;
da *= wc;
db *= wc;
(dl.powi(2) + da.powi(2) + db.powi(2)).sqrt()
}
fn salience_naive(&self, ord: &ColorOrder) -> f32 {
let s = self.0;
let o = get_bg_naive(ord);
let mut dl = s.lightness - o.lightness;
let mut dc = s.colorfulness - o.colorfulness;
let (wl, wc) = get_sal_weights();
dl *= wl;
dc *= wc;
(dl.powi(2) + dc.powi(2)).sqrt()
}
fn improved_salience(&self, &o: &Self) -> f32 {
1.67 * self.salience(&o).powf(0.64) }
fn improved_salience_naive(&self, ord: &ColorOrder) -> f32 {
1.79 * self.salience_naive(ord).powf(0.64) }
}
pub fn constrain_col_against_cols(context: Spec, cols: &[Spec], ord: &ColorOrder, thresholds: &[f32]) -> Vec<Spec> {
if thresholds.is_empty() { return vec!(cols[0]) }
let mut thresholds = thresholds.to_vec();
thresholds.reverse();
let mut cols = cols.to_vec();
cols.sort_by(|a, b| a.improved_salience(&context).partial_cmp(&b.improved_salience(&context)).unwrap_or(Ordering::Equal));
let least_sal = *cols.first().expect("not empty");
let dec_sal_l = get_dec_sal_l_fn(ord);
let mut mod_and_sort = |mut ctx, mut other: Spec, t: f32, is_initial_bg: bool| -> Spec {
let dl_enforce = match ord {
ColorOrder::LightFirst => BG_DARK_ENFORCE_L_DELTA,
ColorOrder::DarkFirst => BG_LIGHT_ENFORCE_L_DELTA,
};
while other.improved_salience(&ctx) < t || ((ctx.lightness - other.lightness).abs() < dl_enforce && is_initial_bg) {
ctx = dec_sal_l(ctx, 0.05);
ctx = ctx.clamp();
if ctx.lightness < BG_DARK_L_SOFTMIN || ctx.lightness > BG_LIGHT_L_SOFTMAX {break};
cols.sort_by(|a, b| a.improved_salience(&ctx).partial_cmp(&b.improved_salience(&ctx)).unwrap_or(Ordering::Equal));
if is_initial_bg { other = *cols.first().expect("not empty") };
};
ctx
};
let mut ret: Vec<Spec> = Vec::new();
let mut threshold = thresholds.pop().expect("not empty");
let context = mod_and_sort(context, least_sal, threshold, true);
ret.push(context);
while !thresholds.is_empty() {
threshold = thresholds.pop().expect("not empty");
ret.push(mod_and_sort(context, context, threshold, false))
}
ret.reverse();
ret
}
pub fn get_dec_sal_l_fn(ord: &ColorOrder) -> fn(Spec, f32) -> Spec {
match ord {
ColorOrder::LightFirst => Spec::darken,
ColorOrder::DarkFirst => Spec::lighten,
}
}
pub fn get_inc_sal_l_fn(ord: &ColorOrder) -> fn(Spec, f32) -> Spec {
match ord {
ColorOrder::LightFirst => Spec::lighten,
ColorOrder::DarkFirst => Spec::darken,
}
}
pub fn constrain_col_as_bg(col: Spec, cams: &[Spec], ord: &ColorOrder) -> Spec {
let mut bg = col;
let least_sal: &Spec = cams.first().expect("not empty");
bg.lightness = match ord {
ColorOrder::LightFirst => least_sal.lightness.max(bg.lightness).clamp(BG_DARK_L_SOFTMIN, BG_DARK_L_SOFTMAX),
ColorOrder::DarkFirst => least_sal.lightness.min(bg.lightness).clamp(BG_LIGHT_L_SOFTMIN, BG_LIGHT_L_SOFTMAX)
};
bg.colorfulness = least_sal.colorfulness.min(bg.colorfulness).clamp(BG_COLORFULNESS_MIN, BG_COLORFULNESS_MAX);
bg.clamp()
}
pub fn get_bg_naive(ord: &ColorOrder) -> Spec {
match ord {
ColorOrder::LightFirst => DARKEST_COL,
ColorOrder::DarkFirst => LIGHTEST_COL,
}
}
pub fn get_lightness_bound(ord: &ColorOrder) -> f32 {
match ord {
ColorOrder::LightFirst => DARKEST,
ColorOrder::DarkFirst => LIGHTEST,
}
}
pub fn get_bg_min_sal(ord: &ColorOrder) -> f32 {
match ord {
ColorOrder::LightFirst => BG_DARK_MIN_SAL,
ColorOrder::DarkFirst => BG_LIGHT_MIN_SAL,
}
}