use std::fmt::{Debug, Formatter};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Color {
#[doc(hidden)]
Rgba32(u32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ColorParseError {
WrongSize(usize),
#[allow(missing_docs)]
NotHex { idx: usize, byte: u8 },
}
impl Color {
pub const fn rgb8(r: u8, g: u8, b: u8) -> Color {
Color::from_rgba32_u32(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | 0xff)
}
pub const fn rgba8(r: u8, g: u8, b: u8, a: u8) -> Color {
Color::from_rgba32_u32(
((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (a as u32),
)
}
pub const fn from_rgba32_u32(rgba: u32) -> Color {
Color::Rgba32(rgba)
}
pub const fn from_hex_str(hex: &str) -> Result<Color, ColorParseError> {
match get_4bit_hex_channels(hex) {
Ok(channels) => Ok(color_from_4bit_hex(channels)),
Err(e) => Err(e),
}
}
pub const fn grey8(grey: u8) -> Color {
Color::rgb8(grey, grey, grey)
}
pub fn grey(grey: f64) -> Color {
Color::rgb(grey, grey, grey)
}
pub fn rgba(r: f64, g: f64, b: f64, a: f64) -> Color {
let r = (r.clamp(0.0, 1.0) * 255.0).round() as u32;
let g = (g.clamp(0.0, 1.0) * 255.0).round() as u32;
let b = (b.clamp(0.0, 1.0) * 255.0).round() as u32;
let a = (a.clamp(0.0, 1.0) * 255.0).round() as u32;
Color::from_rgba32_u32((r << 24) | (g << 16) | (b << 8) | a)
}
pub fn rgb(r: f64, g: f64, b: f64) -> Color {
let r = (r.clamp(0.0, 1.0) * 255.0).round() as u32;
let g = (g.clamp(0.0, 1.0) * 255.0).round() as u32;
let b = (b.clamp(0.0, 1.0) * 255.0).round() as u32;
Color::from_rgba32_u32((r << 24) | (g << 16) | (b << 8) | 0xff)
}
#[allow(non_snake_case)]
#[allow(clippy::many_single_char_names)]
#[allow(clippy::unreadable_literal)]
pub fn hlc(h: f64, L: f64, c: f64) -> Color {
fn f_inv(t: f64) -> f64 {
let d = 6. / 29.;
if t > d {
t.powi(3)
} else {
3. * d * d * (t - 4. / 29.)
}
}
let th = h * (std::f64::consts::PI / 180.);
let a = c * th.cos();
let b = c * th.sin();
let ll = (L + 16.) * (1. / 116.);
let X = f_inv(ll + a * (1. / 500.));
let Y = f_inv(ll);
let Z = f_inv(ll - b * (1. / 200.));
let r_lin = 3.02172918 * X - 1.61692294 * Y - 0.40480625 * Z;
let g_lin = -0.94339358 * X + 1.91584267 * Y + 0.02755094 * Z;
let b_lin = 0.06945666 * X - 0.22903204 * Y + 1.15957526 * Z;
fn gamma(u: f64) -> f64 {
if u <= 0.0031308 {
12.92 * u
} else {
1.055 * u.powf(1. / 2.4) - 0.055
}
}
Color::rgb(gamma(r_lin), gamma(g_lin), gamma(b_lin))
}
pub fn hlca(h: f64, l: f64, c: f64, a: f64) -> Color {
Color::hlc(h, c, l).with_alpha(a)
}
pub fn with_alpha(self, a: f64) -> Color {
let a = (a.clamp(0.0, 1.0) * 255.0).round() as u32;
Color::from_rgba32_u32((self.as_rgba_u32() & !0xff) | a)
}
pub fn as_rgba_u32(self) -> u32 {
match self {
Color::Rgba32(rgba) => rgba,
}
}
pub fn as_rgba8(self) -> (u8, u8, u8, u8) {
let rgba = self.as_rgba_u32();
(
(rgba >> 24 & 255) as u8,
((rgba >> 16) & 255) as u8,
((rgba >> 8) & 255) as u8,
(rgba & 255) as u8,
)
}
pub fn as_rgba(self) -> (f64, f64, f64, f64) {
let rgba = self.as_rgba_u32();
(
(rgba >> 24) as f64 / 255.0,
((rgba >> 16) & 255) as f64 / 255.0,
((rgba >> 8) & 255) as f64 / 255.0,
(rgba & 255) as f64 / 255.0,
)
}
pub const AQUA: Color = Color::rgb8(0, 255, 255);
pub const BLACK: Color = Color::rgb8(0, 0, 0);
pub const BLUE: Color = Color::rgb8(0, 0, 255);
pub const FUCHSIA: Color = Color::rgb8(255, 0, 255);
pub const GRAY: Color = Color::grey8(128);
pub const GREEN: Color = Color::rgb8(0, 128, 0);
pub const LIME: Color = Color::rgb8(0, 255, 0);
pub const MAROON: Color = Color::rgb8(128, 0, 0);
pub const NAVY: Color = Color::rgb8(0, 0, 128);
pub const OLIVE: Color = Color::rgb8(128, 128, 0);
pub const PURPLE: Color = Color::rgb8(128, 0, 128);
pub const RED: Color = Color::rgb8(255, 0, 0);
pub const SILVER: Color = Color::grey8(192);
pub const TEAL: Color = Color::rgb8(0, 128, 128);
pub const TRANSPARENT: Color = Color::rgba8(0, 0, 0, 0);
pub const WHITE: Color = Color::grey8(255);
pub const YELLOW: Color = Color::rgb8(255, 255, 0);
}
const fn get_4bit_hex_channels(hex_str: &str) -> Result<[u8; 8], ColorParseError> {
let mut four_bit_channels = match hex_str.as_bytes() {
&[b'#', r, g, b] | &[r, g, b] => [r, r, g, g, b, b, b'f', b'f'],
&[b'#', r, g, b, a] | &[r, g, b, a] => [r, r, g, g, b, b, a, a],
&[b'#', r0, r1, g0, g1, b0, b1] | &[r0, r1, g0, g1, b0, b1] => {
[r0, r1, g0, g1, b0, b1, b'f', b'f']
}
&[b'#', r0, r1, g0, g1, b0, b1, a0, a1] | &[r0, r1, g0, g1, b0, b1, a0, a1] => {
[r0, r1, g0, g1, b0, b1, a0, a1]
}
other => return Err(ColorParseError::WrongSize(other.len())),
};
let mut i = 0;
while i < four_bit_channels.len() {
let ascii = four_bit_channels[i];
let as_hex = match hex_from_ascii_byte(ascii) {
Ok(hex) => hex,
Err(byte) => return Err(ColorParseError::NotHex { idx: i, byte }),
};
four_bit_channels[i] = as_hex;
i += 1;
}
Ok(four_bit_channels)
}
const fn color_from_4bit_hex(components: [u8; 8]) -> Color {
let [r0, r1, g0, g1, b0, b1, a0, a1] = components;
Color::rgba8(r0 << 4 | r1, g0 << 4 | g1, b0 << 4 | b1, a0 << 4 | a1)
}
const fn hex_from_ascii_byte(b: u8) -> Result<u8, u8> {
match b {
b'0'..=b'9' => Ok(b - b'0'),
b'A'..=b'F' => Ok(b - b'A' + 10),
b'a'..=b'f' => Ok(b - b'a' + 10),
_ => Err(b),
}
}
impl Debug for Color {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
write!(f, "#{:08x}", self.as_rgba_u32())
}
}
impl std::fmt::Display for ColorParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ColorParseError::WrongSize(n) => write!(f, "Input string has invalid length {n}"),
ColorParseError::NotHex { idx, byte } => {
write!(f, "byte {byte:X} at index {idx} is not valid hex digit")
}
}
}
}
impl std::error::Error for ColorParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_from_hex() {
assert_eq!(Color::from_hex_str("#BAD"), Color::from_hex_str("BBAADD"));
assert_eq!(
Color::from_hex_str("#BAD"),
Ok(Color::from_rgba32_u32(0xBBAADDFF))
);
assert_eq!(Color::from_hex_str("BAD"), Color::from_hex_str("BBAADD"));
assert_eq!(Color::from_hex_str("#BADF"), Color::from_hex_str("BAD"));
assert_eq!(Color::from_hex_str("#BBAADDFF"), Color::from_hex_str("BAD"));
assert_eq!(Color::from_hex_str("BBAADDFF"), Color::from_hex_str("BAD"));
assert_eq!(Color::from_hex_str("bBAadDfF"), Color::from_hex_str("BAD"));
assert_eq!(Color::from_hex_str("#0f6"), Ok(Color::rgb8(0, 0xff, 0x66)));
assert_eq!(
Color::from_hex_str("#0f6a"),
Ok(Color::rgba8(0, 0xff, 0x66, 0xaa))
);
assert!(Color::from_hex_str("#0f6aa").is_err());
assert!(Color::from_hex_str("#0f").is_err());
assert!(Color::from_hex_str("x0f").is_err());
assert!(Color::from_hex_str("#0afa1").is_err());
}
}