#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b, a: 1.0 }
}
pub fn from_hex(hex: &str) -> Result<Self, String> {
let hex = hex.trim_start_matches('#');
match hex.len() {
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|e| e.to_string())?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|e| e.to_string())?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|e| e.to_string())?;
Ok(Color::rgb(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
))
}
8 => {
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|e| e.to_string())?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|e| e.to_string())?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|e| e.to_string())?;
let a = u8::from_str_radix(&hex[6..8], 16).map_err(|e| e.to_string())?;
Ok(Color::rgba(
r as f32 / 255.0,
g as f32 / 255.0,
b as f32 / 255.0,
a as f32 / 255.0,
))
}
_ => Err(format!("invalid hex length: {}", hex.len())),
}
}
pub fn to_hex(&self) -> String {
let r = (self.r.clamp(0.0, 1.0) * 255.0).round() as u8;
let g = (self.g.clamp(0.0, 1.0) * 255.0).round() as u8;
let b = (self.b.clamp(0.0, 1.0) * 255.0).round() as u8;
format!("#{:02X}{:02X}{:02X}", r, g, b)
}
pub fn to_array(&self) -> [f32; 4] {
[self.r, self.g, self.b, self.a]
}
pub fn lerp(&self, other: Color, t: f32) -> Color {
let t = t.clamp(0.0, 1.0);
Color {
r: self.r + (other.r - self.r) * t,
g: self.g + (other.g - self.g) * t,
b: self.b + (other.b - self.b) * t,
a: self.a + (other.a - self.a) * t,
}
}
pub fn to_srgb_u8(&self) -> [u8; 4] {
[
(self.r.clamp(0.0, 1.0) * 255.0).round() as u8,
(self.g.clamp(0.0, 1.0) * 255.0).round() as u8,
(self.b.clamp(0.0, 1.0) * 255.0).round() as u8,
(self.a.clamp(0.0, 1.0) * 255.0).round() as u8,
]
}
pub const WHITE: Color = Color::rgba(1.0, 1.0, 1.0, 1.0);
pub const BLACK: Color = Color::rgba(0.0, 0.0, 0.0, 1.0);
pub const TRANSPARENT: Color = Color::rgba(0.0, 0.0, 0.0, 0.0);
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ColorTheme {
pub name: String,
pub skin: Color,
pub hair: Color,
pub eye: Color,
pub lip: Color,
}
impl ColorTheme {
pub fn new(name: impl Into<String>, skin: Color, hair: Color, eye: Color, lip: Color) -> Self {
Self {
name: name.into(),
skin,
hair,
eye,
lip,
}
}
pub fn caucasian() -> Self {
Self::new(
"caucasian",
Color::from_hex("#F5D5C0").unwrap_or(Color::WHITE),
Color::from_hex("#6B3A2A").unwrap_or(Color::WHITE),
Color::from_hex("#5B8DB8").unwrap_or(Color::WHITE),
Color::from_hex("#C7736A").unwrap_or(Color::WHITE),
)
}
pub fn african() -> Self {
Self::new(
"african",
Color::from_hex("#6B3A2A").unwrap_or(Color::WHITE),
Color::from_hex("#1A0F0A").unwrap_or(Color::WHITE),
Color::from_hex("#5C3317").unwrap_or(Color::WHITE),
Color::from_hex("#8B4A3A").unwrap_or(Color::WHITE),
)
}
pub fn asian() -> Self {
Self::new(
"asian",
Color::from_hex("#E8C9A0").unwrap_or(Color::WHITE),
Color::from_hex("#1A0F0A").unwrap_or(Color::WHITE),
Color::from_hex("#5C3317").unwrap_or(Color::WHITE),
Color::from_hex("#C06050").unwrap_or(Color::WHITE),
)
}
pub fn albino() -> Self {
Self::new(
"albino",
Color::from_hex("#FFF5F0").unwrap_or(Color::WHITE),
Color::from_hex("#FAFAFA").unwrap_or(Color::WHITE),
Color::from_hex("#F0C0C0").unwrap_or(Color::WHITE),
Color::from_hex("#FFCCCC").unwrap_or(Color::WHITE),
)
}
pub fn custom(skin_hex: &str, hair_hex: &str, eye_hex: &str) -> Result<Self, String> {
let skin = Color::from_hex(skin_hex)?;
let hair = Color::from_hex(hair_hex)?;
let eye = Color::from_hex(eye_hex)?;
let lip = Color::rgb(
(skin.r * 0.9 + 0.1).clamp(0.0, 1.0),
skin.g * 0.7,
skin.b * 0.7,
);
Ok(Self::new("custom", skin, hair, eye, lip))
}
pub fn apply_skin_to_vertices(&self, n_verts: usize) -> Vec<[f32; 4]> {
vec![self.skin.to_array(); n_verts]
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).unwrap_or(serde_json::Value::Null)
}
}
pub struct ThemePalette {
themes: Vec<ColorTheme>,
}
impl ThemePalette {
pub fn new() -> Self {
Self { themes: Vec::new() }
}
pub fn add(&mut self, theme: ColorTheme) {
self.themes.push(theme);
}
pub fn get(&self, name: &str) -> Option<&ColorTheme> {
let lower = name.to_lowercase();
self.themes.iter().find(|t| t.name.to_lowercase() == lower)
}
pub fn names(&self) -> Vec<&str> {
self.themes.iter().map(|t| t.name.as_str()).collect()
}
pub fn standard() -> Self {
let mut palette = Self::new();
palette.add(ColorTheme::caucasian());
palette.add(ColorTheme::african());
palette.add(ColorTheme::asian());
palette.add(ColorTheme::albino());
palette
}
}
impl Default for ThemePalette {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_hex_valid() {
assert!(Color::from_hex("#FF8040").is_ok());
}
#[test]
fn from_hex_without_hash() {
assert!(Color::from_hex("FF8040").is_ok());
}
#[test]
fn from_hex_invalid() {
assert!(Color::from_hex("ZZZZZZ").is_err());
}
#[test]
fn from_hex_with_alpha() {
let c = Color::from_hex("#FF804080").expect("should succeed");
assert!((c.a - 0.502).abs() < 0.005, "alpha was {}", c.a);
}
#[test]
fn to_hex_roundtrip() {
let hex = "#FF8040";
let c = Color::from_hex(hex).expect("should succeed");
assert_eq!(c.to_hex(), hex);
}
#[test]
fn lerp_midpoint() {
let mid = Color::WHITE.lerp(Color::BLACK, 0.5);
assert!((mid.r - 0.5).abs() < 1e-6, "r was {}", mid.r);
}
#[test]
fn to_srgb_u8_white() {
assert_eq!(Color::WHITE.to_srgb_u8(), [255, 255, 255, 255]);
}
#[test]
fn standard_palette_has_four() {
assert_eq!(ThemePalette::standard().names().len(), 4);
}
#[test]
fn get_theme_case_insensitive() {
let palette = ThemePalette::standard();
assert!(palette.get("CAUCASIAN").is_some());
}
#[test]
fn apply_skin_fills_verts() {
let theme = ColorTheme::caucasian();
assert_eq!(theme.apply_skin_to_vertices(10).len(), 10);
}
}