use crate::color::Color;
use crate::contrast::{contrast_ratio, LIGHT_BG};
#[derive(Debug, Clone)]
pub struct RoleAssignment {
pub primary: Color,
pub secondary: Color,
pub chart: Vec<Color>,
}
pub fn surface_fitness(color: &Color) -> f64 {
let bg = Color::from_hex(LIGHT_BG).expect("constant");
let cr = contrast_ratio(&bg, color);
let contrast_score = cr.ln().max(0.0);
let lightness_penalty = (color.l - 0.55).powi(2);
let chroma_penalty = (color.c - 0.10).max(0.0).powi(2) * 4.0;
contrast_score - lightness_penalty - chroma_penalty
}
pub fn assign_roles(brand_colors: &[Color]) -> RoleAssignment {
if brand_colors.is_empty() {
let fallback = Color::from_hex("#3f6089").expect("constant");
return RoleAssignment {
primary: fallback,
secondary: fallback,
chart: Vec::new(),
};
}
if brand_colors.len() == 1 {
let only = brand_colors[0];
return RoleAssignment {
primary: only,
secondary: only,
chart: Vec::new(),
};
}
let mut ranked: Vec<(usize, Color)> = brand_colors.iter().copied().enumerate().collect();
ranked.sort_by(|a, b| {
surface_fitness(&b.1)
.partial_cmp(&surface_fitness(&a.1))
.unwrap_or(std::cmp::Ordering::Equal)
});
let primary = ranked[0].1;
let secondary = ranked[1].1;
let chart: Vec<Color> = ranked[2..].iter().map(|(_, c)| *c).collect();
RoleAssignment {
primary,
secondary,
chart,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c(hex: &str) -> Color {
Color::from_hex(hex).unwrap()
}
#[test]
fn five_inputs_yield_one_primary_one_secondary_three_chart() {
let inputs = vec![
c("#3f6089"),
c("#c9572e"),
c("#2e7d5b"),
c("#8a4cb4"),
c("#d4a017"),
];
let r = assign_roles(&inputs);
assert_eq!(r.chart.len(), 3);
assert!(surface_fitness(&r.primary) >= surface_fitness(&r.secondary));
}
#[test]
fn one_input_uses_sentinel_secondary_equal_to_primary() {
let r = assign_roles(&[c("#0d9488")]);
assert!(r.chart.is_empty());
assert_eq!(r.primary.to_hex(), r.secondary.to_hex());
}
#[test]
fn two_inputs_yield_empty_chart() {
let r = assign_roles(&[c("#3f6089"), c("#c9572e")]);
assert!(r.chart.is_empty());
}
}