use super::Hct;
use bevy::prelude::Color;
use std::collections::HashMap;
pub const STANDARD_TONES: &[u8] = &[
0, 4, 6, 10, 12, 17, 20, 22, 24, 30, 40, 50, 60, 70, 80, 87, 90, 92, 94, 95, 96, 98, 99, 100,
];
#[derive(Debug, Clone)]
pub struct TonalPalette {
hue: f64,
chroma: f64,
cache: HashMap<u8, u32>,
}
impl TonalPalette {
pub fn new(hue: f64, chroma: f64) -> Self {
Self {
hue,
chroma,
cache: HashMap::new(),
}
}
pub fn from_hct(hct: &Hct) -> Self {
Self::new(hct.hue(), hct.chroma())
}
pub fn from_argb(argb: u32) -> Self {
Self::from_hct(&Hct::from_argb(argb))
}
pub fn from_bevy_color(color: Color) -> Self {
Self::from_hct(&Hct::from_bevy_color(color))
}
pub fn hue(&self) -> f64 {
self.hue
}
pub fn chroma(&self) -> f64 {
self.chroma
}
pub fn tone(&mut self, tone: u8) -> u32 {
let tone = tone.min(100);
if let Some(&cached) = self.cache.get(&tone) {
return cached;
}
let hct = Hct::new(self.hue, self.chroma, tone as f64);
let argb = hct.to_argb();
self.cache.insert(tone, argb);
argb
}
pub fn tone_color(&mut self, tone: u8) -> Color {
let argb = self.tone(tone);
argb_to_bevy_color(argb)
}
pub fn tone_hct(&self, tone: u8) -> Hct {
Hct::new(self.hue, self.chroma, tone.min(100) as f64)
}
pub fn cache_standard_tones(&mut self) {
for &tone in STANDARD_TONES {
self.tone(tone);
}
}
}
#[derive(Debug, Clone)]
pub struct CorePalette {
pub primary: TonalPalette,
pub secondary: TonalPalette,
pub tertiary: TonalPalette,
pub neutral: TonalPalette,
pub neutral_variant: TonalPalette,
pub error: TonalPalette,
}
impl CorePalette {
pub fn from_argb(seed: u32) -> Self {
let hct = Hct::from_argb(seed);
Self::from_hct(&hct)
}
pub fn from_hct(seed: &Hct) -> Self {
let hue = seed.hue();
let chroma = seed.chroma();
Self {
primary: TonalPalette::new(hue, chroma.max(48.0)),
secondary: TonalPalette::new(hue, 16.0),
tertiary: TonalPalette::new((hue + 60.0) % 360.0, 24.0),
neutral: TonalPalette::new(hue, 4.0),
neutral_variant: TonalPalette::new(hue, 8.0),
error: TonalPalette::new(25.0, 84.0),
}
}
pub fn from_bevy_color(color: Color) -> Self {
Self::from_hct(&Hct::from_bevy_color(color))
}
pub fn cache_all(&mut self) {
self.primary.cache_standard_tones();
self.secondary.cache_standard_tones();
self.tertiary.cache_standard_tones();
self.neutral.cache_standard_tones();
self.neutral_variant.cache_standard_tones();
self.error.cache_standard_tones();
}
}
fn argb_to_bevy_color(argb: u32) -> Color {
let r = ((argb >> 16) & 0xFF) as f32 / 255.0;
let g = ((argb >> 8) & 0xFF) as f32 / 255.0;
let b = (argb & 0xFF) as f32 / 255.0;
Color::srgb(r, g, b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tonal_palette_creation() {
let palette = TonalPalette::new(270.0, 50.0);
assert!((palette.hue() - 270.0).abs() < 0.001);
assert!((palette.chroma() - 50.0).abs() < 0.001);
}
#[test]
fn test_tonal_palette_tones() {
let mut palette = TonalPalette::new(270.0, 50.0);
let tone_0 = palette.tone(0);
let r = (tone_0 >> 16) & 0xFF;
let g = (tone_0 >> 8) & 0xFF;
let b = tone_0 & 0xFF;
assert!(r < 20 && g < 20 && b < 20, "Tone 0 should be near black");
let tone_100 = palette.tone(100);
let r = (tone_100 >> 16) & 0xFF;
let g = (tone_100 >> 8) & 0xFF;
let b = tone_100 & 0xFF;
assert!(
r > 240 && g > 240 && b > 240,
"Tone 100 should be near white"
);
}
#[test]
fn test_core_palette() {
let palette = CorePalette::from_argb(0xFF6750A4);
assert!((palette.primary.chroma() - 48.0).abs() < 0.001);
assert!((palette.secondary.chroma() - 16.0).abs() < 0.001);
assert!(palette.neutral.chroma() <= 8.0);
assert!(palette.error.hue() < 50.0 || palette.error.hue() > 330.0);
}
#[test]
fn test_palette_caching() {
let mut palette = TonalPalette::new(200.0, 40.0);
let first = palette.tone(50);
let second = palette.tone(50);
assert_eq!(first, second);
}
#[test]
fn test_bevy_color_conversion() {
let mut palette = TonalPalette::new(120.0, 40.0);
let color = palette.tone_color(50);
let srgba = color.to_srgba();
assert!(srgba.red >= 0.0 && srgba.red <= 1.0);
assert!(srgba.green >= 0.0 && srgba.green <= 1.0);
assert!(srgba.blue >= 0.0 && srgba.blue <= 1.0);
}
}