use palette::{rgb::Rgb, FromColor, GetHue, Hsl, IntoColor};
use serde::{Deserialize, Serialize, Serializer};
use std::fmt;
use std::str::FromStr;
use crate::error::TintedBuilderError;
#[derive(Debug, Clone, Deserialize)]
pub struct Color {
pub hex: (String, String, String),
pub rgb: (u8, u8, u8),
pub dec: (f32, f32, f32),
pub name: ColorName,
pub variant: ColorVariant,
}
impl Color {
pub fn new(
hex_color: &str,
name: Option<ColorName>,
variant: Option<ColorVariant>,
) -> Result<Self, TintedBuilderError> {
let hex_full = process_hex_input(hex_color).ok_or(TintedBuilderError::HexInputFormat)?;
let hex: (String, String, String) = (
hex_full[0..2].to_lowercase(),
hex_full[2..4].to_lowercase(),
hex_full[4..6].to_lowercase(),
);
let rgb = hex_to_rgb(&hex)?;
let inv_255: f32 = 1.0 / 255.0;
let dec: (f32, f32, f32) = (
f32::from(rgb.0) * inv_255,
f32::from(rgb.1) * inv_255,
f32::from(rgb.2) * inv_255,
);
Ok(Self {
hex,
rgb,
dec,
name: name.unwrap_or(ColorName::Other),
variant: variant.unwrap_or(ColorVariant::Normal),
})
}
#[must_use]
pub fn to_hex(&self) -> String {
format!("{}{}{}", &self.hex.0, &self.hex.1, &self.hex.2)
}
#[allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::missing_errors_doc
)]
pub fn try_to_variant(&self, color_variant: &ColorVariant) -> Result<Self, TintedBuilderError> {
let rgb = Rgb::new(self.rgb.0, self.rgb.1, self.rgb.2);
let hsl: Hsl = Hsl::from_color(rgb.into_format::<f32>());
let updated_hsl = adjust_normal_hsl_for_variant(hsl, color_variant);
let updated_rgb: Rgb = updated_hsl.into_color();
let updated_rgb_r: u8 = (updated_rgb.red.clamp(0.0, 1.0) * 255.0).round() as u8;
let updated_rgb_g: u8 = (updated_rgb.green.clamp(0.0, 1.0) * 255.0).round() as u8;
let updated_rgb_b: u8 = (updated_rgb.blue.clamp(0.0, 1.0) * 255.0).round() as u8;
let updated_hex = format!("{updated_rgb_r:02X}{updated_rgb_g:02X}{updated_rgb_b:02X}");
Self::new(
&updated_hex,
Some(self.name.clone()),
Some(color_variant.clone()),
)
}
#[allow(
clippy::missing_errors_doc,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
pub fn try_to_color(&self, target_color_name: &ColorName) -> Result<Self, TintedBuilderError> {
let from_target_color_name = &self.name.clone();
let to_target_color_name = target_color_name.clone();
let to_color_variant = &self.variant.clone();
match (&from_target_color_name, &to_target_color_name) {
(ColorName::Yellow, ColorName::Orange) => {
let from_rgb = Rgb::new(self.rgb.0, self.rgb.1, self.rgb.2);
let from_hsl: Hsl = Hsl::from_color(from_rgb.into_format::<f32>());
let from_hsl_h = from_hsl.get_hue().into_degrees();
let from_hsl_s = from_hsl.saturation;
let from_hsl_l = from_hsl.lightness;
let h_prime = (from_hsl_h - 10.0 + 360.0) % 360.0;
let to_hsl: Hsl = Hsl::new(h_prime, from_hsl_s, from_hsl_l);
let to_rgb: Rgb = to_hsl.into_color();
let [to_rgb_r, to_rgb_g, to_rgb_b]: [u8; 3] =
[to_rgb.red, to_rgb.green, to_rgb.blue]
.map(|c| (c.clamp(0.0, 1.0) * 255.0).round() as u8);
let to_hex = format!("{to_rgb_r:02X}{to_rgb_g:02X}{to_rgb_b:02X}");
Self::new(
&to_hex,
Some(to_target_color_name.clone()),
Some(to_color_variant.clone()),
)
}
(ColorName::Yellow, ColorName::Brown) => {
let from_rgb = Rgb::new(self.rgb.0, self.rgb.1, self.rgb.2);
let from_hsl: Hsl = Hsl::from_color(from_rgb.into_format::<f32>());
let from_hsl_h = from_hsl.get_hue().into_degrees();
let from_hsl_s = from_hsl.saturation;
let from_hsl_l = from_hsl.lightness;
let h_difference = 15.0;
let l_difference = 0.3;
let s_perc_difference = 0.65;
let s_prime = (from_hsl_s * s_perc_difference).clamp(0.0, 1.0);
let l_prime = (from_hsl_l - l_difference).clamp(0.0, 1.0);
let to_hsl: Hsl = Hsl::new(from_hsl_h - h_difference, s_prime, l_prime);
let to_rgb: Rgb = to_hsl.into_color();
let [to_rgb_r, to_rgb_g, to_rgb_b]: [u8; 3] =
[to_rgb.red, to_rgb.green, to_rgb.blue]
.map(|c| (c.clamp(0.0, 1.0) * 255.0).round() as u8);
let to_hex = format!("{to_rgb_r:02X}{to_rgb_g:02X}{to_rgb_b:02X}");
Self::new(
&to_hex,
Some(to_target_color_name.clone()),
Some(to_color_variant.clone()),
)
}
_ => Err(TintedBuilderError::UnsupportedColorDerivation {
from_color: self.name.to_string(),
target: target_color_name.to_string(),
supported_derivations: "yellow→orange, yellow→brown".to_string(),
}),
}
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "#{}", &self.to_hex())
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub enum ColorVariant {
Dim,
Normal,
Bright,
}
impl fmt::Display for ColorVariant {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Dim => write!(f, "dim"),
Self::Normal => write!(f, "normal"),
Self::Bright => write!(f, "bright"),
}
}
}
impl FromStr for ColorVariant {
type Err = TintedBuilderError;
fn from_str(variant_str: &str) -> Result<Self, Self::Err> {
match variant_str {
"dim" => Ok(Self::Dim),
"normal" => Ok(Self::Normal),
"bright" => Ok(Self::Bright),
_ => Err(TintedBuilderError::InvalidColorVariant(
variant_str.to_string(),
)),
}
}
}
impl ColorVariant {
#[must_use]
pub const fn get_list<'a>() -> &'a [Self] {
&[Self::Dim, Self::Normal, Self::Bright]
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[non_exhaustive]
pub enum ColorName {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
Orange,
Gray,
Brown,
Other, }
impl fmt::Display for ColorName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Black => write!(f, "black"),
Self::Red => write!(f, "red"),
Self::Green => write!(f, "green"),
Self::Yellow => write!(f, "yellow"),
Self::Blue => write!(f, "blue"),
Self::Magenta => write!(f, "magenta"),
Self::Cyan => write!(f, "cyan"),
Self::White => write!(f, "white"),
Self::Orange => write!(f, "orange"),
Self::Gray => write!(f, "gray"),
Self::Brown => write!(f, "brown"),
Self::Other => write!(f, "other"),
}
}
}
impl ColorName {
#[must_use]
pub const fn get_list<'a>() -> &'a [Self] {
&[
Self::Black,
Self::Red,
Self::Green,
Self::Yellow,
Self::Blue,
Self::Magenta,
Self::Cyan,
Self::White,
Self::Orange,
Self::Gray,
Self::Brown,
]
}
}
pub struct ColorType(pub ColorName, pub ColorVariant);
impl FromStr for ColorName {
type Err = TintedBuilderError;
fn from_str(name_str: &str) -> Result<Self, Self::Err> {
match name_str {
"black" => Ok(Self::Black),
"red" => Ok(Self::Red),
"green" => Ok(Self::Green),
"yellow" => Ok(Self::Yellow),
"blue" => Ok(Self::Blue),
"magenta" => Ok(Self::Magenta),
"cyan" => Ok(Self::Cyan),
"white" => Ok(Self::White),
"orange" => Ok(Self::Orange),
"gray" => Ok(Self::Gray),
"brown" => Ok(Self::Brown),
"other" => Ok(Self::Other),
_ => Err(TintedBuilderError::InvalidColorName(name_str.to_string())),
}
}
}
impl FromStr for ColorType {
type Err = TintedBuilderError;
fn from_str(color_str: &str) -> Result<Self, Self::Err> {
let trimmed = color_str.trim();
let lower = trimmed.to_lowercase();
let (name, variant) = lower
.split_once('_')
.or_else(|| lower.split_once('-'))
.ok_or_else(|| TintedBuilderError::InvalidColorType(trimmed.to_string()))?;
Ok(Self(
ColorName::from_str(name)?,
ColorVariant::from_str(variant)?,
))
}
}
fn hex_to_rgb(hex: &(String, String, String)) -> Result<(u8, u8, u8), TintedBuilderError> {
let r = u8::from_str_radix(hex.0.as_str(), 16)?;
let g = u8::from_str_radix(hex.1.as_str(), 16)?;
let b = u8::from_str_radix(hex.2.as_str(), 16)?;
Ok((r, g, b))
}
fn process_hex_input(input: &str) -> Option<String> {
let hex_str = input.strip_prefix('#').unwrap_or(input);
match hex_str.len() {
3 => {
if hex_str.chars().all(|c| c.is_ascii_hexdigit()) {
Some(
hex_str
.chars()
.flat_map(|c| std::iter::repeat(c).take(2))
.collect(),
)
} else {
None }
}
6 => {
if hex_str.chars().all(|c| c.is_ascii_hexdigit()) {
Some(hex_str.to_string())
} else {
None }
}
_ => None,
}
}
const DL: f32 = 0.12;
fn adjust_normal_hsl_for_variant(hsl: Hsl, color_variant: &ColorVariant) -> Hsl {
let mut updated_s = hsl.saturation;
let mut updated_l = hsl.lightness;
match color_variant {
ColorVariant::Dim => {
let k: f32 = if hsl.lightness < 0.4 {
1.04
} else if hsl.lightness < 0.7 {
1.07
} else {
1.1
};
let delta_l = DL.min(hsl.lightness);
updated_l = (hsl.lightness - delta_l).clamp(0.0, 1.0);
updated_s = (hsl.saturation * k).clamp(0.0, 1.0);
}
ColorVariant::Bright => {
let k: f32 = if hsl.lightness < 0.5 {
1.08
} else if hsl.lightness < 0.8 {
1.00
} else {
0.9
};
let delta_l = DL.min(1.0 - hsl.lightness);
updated_l = (hsl.lightness + delta_l).clamp(0.0, 1.0);
updated_s = (hsl.saturation * k).clamp(0.0, 1.0);
}
_ => {}
}
Hsl::new(hsl.hue, updated_s, updated_l)
}
#[derive(Serialize)]
struct RgbSer {
r: u8,
g: u8,
b: u8,
}
#[derive(Serialize)]
struct Rgb16Ser {
r: u16,
g: u16,
b: u16,
}
#[derive(Serialize)]
struct DecSer {
r: String,
g: String,
b: String,
}
impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(8))?;
map.serialize_entry("hex", &self.to_hex())?;
map.serialize_entry("hex-r", &self.hex.0)?;
map.serialize_entry("hex-g", &self.hex.1)?;
map.serialize_entry("hex-b", &self.hex.2)?;
let hex_bgr = format!("{}{}{}", self.hex.2, self.hex.1, self.hex.0);
map.serialize_entry("hex-bgr", &hex_bgr)?;
let rgb = RgbSer {
r: self.rgb.0,
g: self.rgb.1,
b: self.rgb.2,
};
map.serialize_entry("rgb", &rgb)?;
let rgb16 = Rgb16Ser {
r: u16::from(self.rgb.0) * 257,
g: u16::from(self.rgb.1) * 257,
b: u16::from(self.rgb.2) * 257,
};
map.serialize_entry("rgb16", &rgb16)?;
let dec = DecSer {
r: format!("{:.8}", f64::from(self.dec.0)),
g: format!("{:.8}", f64::from(self.dec.1)),
b: format!("{:.8}", f64::from(self.dec.2)),
};
map.serialize_entry("dec", &dec)?;
map.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serializes_to_color_object() {
let color = Color::new("#AABBCC", Some(ColorName::Blue), Some(ColorVariant::Normal))
.expect("unable to create new color");
let yaml = serde_yaml::to_string(&color).expect("unable to serialize color");
assert!(yaml.contains("hex: aabbcc"));
assert!(yaml.contains("hex-r: aa"));
assert!(yaml.contains("hex-g: bb"));
assert!(yaml.contains("hex-b: cc"));
assert!(yaml.contains("hex-bgr: ccbbaa"));
assert!(yaml.contains(
"rgb:
r: 170
g: 187
b: 204"
));
assert!(yaml.contains(
"rgb16:
r: 43690
g: 48059
b: 52428"
));
assert!(yaml.contains(
"dec:
r: '0.66666669'
g: '0.73333335'
b: '0.80000007'"
));
}
#[test]
fn color_object_field_types() {
let color = Color::new("#112233", Some(ColorName::Blue), Some(ColorVariant::Normal))
.expect("unable to create color");
let val = serde_yaml::to_value(&color).expect("unable to deserialize color");
let map = val.as_mapping().expect("unable to create mapping");
assert!(map
.get(serde_yaml::Value::String("hex".into()))
.expect("unable to get 'hex' property")
.as_str()
.is_some());
let rgb = map
.get(serde_yaml::Value::String("rgb".into()))
.expect("unable to get 'rgb' property")
.as_mapping()
.expect("unable to create mapping");
assert!(rgb
.get(serde_yaml::Value::String("r".into()))
.expect("unable to get 'rgb.r' property")
.as_i64()
.is_some());
let rgb16 = map
.get(serde_yaml::Value::String("rgb16".into()))
.expect("unable to get 'rgb16' property")
.as_mapping()
.expect("unable to create mapping");
assert!(rgb16
.get(serde_yaml::Value::String("r".into()))
.expect("unable to get 'rgb16.r' property")
.as_i64()
.is_some());
let dec = map
.get(serde_yaml::Value::String("dec".into()))
.expect("unable to get 'dec' property")
.as_mapping()
.expect("unable to create mapping");
assert!(dec
.get(serde_yaml::Value::String("r".into()))
.expect("unable to get 'dec.r' property")
.as_str()
.is_some());
}
}