use crate::color_difference::EuclideanDistance;
use crate::{
impl_componentwise_vector_space, Alpha, ColorToComponents, ColorToPacked, Gray, LinearRgba,
Luminance, Mix, StandardColor, Xyza,
};
use bevy_math::{Vec3, Vec4};
use bevy_reflect::prelude::*;
use thiserror::Error;
#[doc = include_str!("../docs/conversion.md")]
#[doc = include_str!("../docs/diagrams/model_graph.svg")]
#[derive(Debug, Clone, Copy, PartialEq, Reflect)]
#[reflect(PartialEq, Default)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct Srgba {
pub red: f32,
pub green: f32,
pub blue: f32,
pub alpha: f32,
}
impl StandardColor for Srgba {}
impl_componentwise_vector_space!(Srgba, [red, green, blue, alpha]);
impl Srgba {
pub const BLACK: Srgba = Srgba::new(0.0, 0.0, 0.0, 1.0);
#[doc(alias = "transparent")]
pub const NONE: Srgba = Srgba::new(0.0, 0.0, 0.0, 0.0);
pub const WHITE: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0);
pub const RED: Self = Self {
red: 1.0,
green: 0.0,
blue: 0.0,
alpha: 1.0,
};
pub const GREEN: Self = Self {
red: 0.0,
green: 1.0,
blue: 0.0,
alpha: 1.0,
};
pub const BLUE: Self = Self {
red: 0.0,
green: 0.0,
blue: 1.0,
alpha: 1.0,
};
pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
Self {
red,
green,
blue,
alpha,
}
}
pub const fn rgb(red: f32, green: f32, blue: f32) -> Self {
Self {
red,
green,
blue,
alpha: 1.0,
}
}
pub const fn with_red(self, red: f32) -> Self {
Self { red, ..self }
}
pub const fn with_green(self, green: f32) -> Self {
Self { green, ..self }
}
pub const fn with_blue(self, blue: f32) -> Self {
Self { blue, ..self }
}
pub fn hex<T: AsRef<str>>(hex: T) -> Result<Self, HexColorError> {
let hex = hex.as_ref();
let hex = hex.strip_prefix('#').unwrap_or(hex);
match hex.len() {
3 => {
let [l, b] = u16::from_str_radix(hex, 16)?.to_be_bytes();
let (r, g, b) = (l & 0x0F, (b & 0xF0) >> 4, b & 0x0F);
Ok(Self::rgb_u8(r << 4 | r, g << 4 | g, b << 4 | b))
}
4 => {
let [l, b] = u16::from_str_radix(hex, 16)?.to_be_bytes();
let (r, g, b, a) = ((l & 0xF0) >> 4, l & 0xF, (b & 0xF0) >> 4, b & 0x0F);
Ok(Self::rgba_u8(
r << 4 | r,
g << 4 | g,
b << 4 | b,
a << 4 | a,
))
}
6 => {
let [_, r, g, b] = u32::from_str_radix(hex, 16)?.to_be_bytes();
Ok(Self::rgb_u8(r, g, b))
}
8 => {
let [r, g, b, a] = u32::from_str_radix(hex, 16)?.to_be_bytes();
Ok(Self::rgba_u8(r, g, b, a))
}
_ => Err(HexColorError::Length),
}
}
pub fn to_hex(&self) -> String {
let [r, g, b, a] = self.to_u8_array();
match a {
255 => format!("#{:02X}{:02X}{:02X}", r, g, b),
_ => format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a),
}
}
pub fn rgb_u8(r: u8, g: u8, b: u8) -> Self {
Self::from_u8_array_no_alpha([r, g, b])
}
pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Self {
Self::from_u8_array([r, g, b, a])
}
pub fn gamma_function(value: f32) -> f32 {
if value <= 0.0 {
return value;
}
if value <= 0.04045 {
value / 12.92 } else {
((value + 0.055) / 1.055).powf(2.4) }
}
pub fn gamma_function_inverse(value: f32) -> f32 {
if value <= 0.0 {
return value;
}
if value <= 0.0031308 {
value * 12.92 } else {
(1.055 * value.powf(1.0 / 2.4)) - 0.055 }
}
}
impl Default for Srgba {
fn default() -> Self {
Self::WHITE
}
}
impl Luminance for Srgba {
#[inline]
fn luminance(&self) -> f32 {
let linear: LinearRgba = (*self).into();
linear.luminance()
}
#[inline]
fn with_luminance(&self, luminance: f32) -> Self {
let linear: LinearRgba = (*self).into();
linear
.with_luminance(Srgba::gamma_function(luminance))
.into()
}
#[inline]
fn darker(&self, amount: f32) -> Self {
let linear: LinearRgba = (*self).into();
linear.darker(amount).into()
}
#[inline]
fn lighter(&self, amount: f32) -> Self {
let linear: LinearRgba = (*self).into();
linear.lighter(amount).into()
}
}
impl Mix for Srgba {
#[inline]
fn mix(&self, other: &Self, factor: f32) -> Self {
let n_factor = 1.0 - factor;
Self {
red: self.red * n_factor + other.red * factor,
green: self.green * n_factor + other.green * factor,
blue: self.blue * n_factor + other.blue * factor,
alpha: self.alpha * n_factor + other.alpha * factor,
}
}
}
impl Alpha for Srgba {
#[inline]
fn with_alpha(&self, alpha: f32) -> Self {
Self { alpha, ..*self }
}
#[inline]
fn alpha(&self) -> f32 {
self.alpha
}
#[inline]
fn set_alpha(&mut self, alpha: f32) {
self.alpha = alpha;
}
}
impl EuclideanDistance for Srgba {
#[inline]
fn distance_squared(&self, other: &Self) -> f32 {
let dr = self.red - other.red;
let dg = self.green - other.green;
let db = self.blue - other.blue;
dr * dr + dg * dg + db * db
}
}
impl Gray for Srgba {
const BLACK: Self = Self::BLACK;
const WHITE: Self = Self::WHITE;
}
impl ColorToComponents for Srgba {
fn to_f32_array(self) -> [f32; 4] {
[self.red, self.green, self.blue, self.alpha]
}
fn to_f32_array_no_alpha(self) -> [f32; 3] {
[self.red, self.green, self.blue]
}
fn to_vec4(self) -> Vec4 {
Vec4::new(self.red, self.green, self.blue, self.alpha)
}
fn to_vec3(self) -> Vec3 {
Vec3::new(self.red, self.green, self.blue)
}
fn from_f32_array(color: [f32; 4]) -> Self {
Self {
red: color[0],
green: color[1],
blue: color[2],
alpha: color[3],
}
}
fn from_f32_array_no_alpha(color: [f32; 3]) -> Self {
Self {
red: color[0],
green: color[1],
blue: color[2],
alpha: 1.0,
}
}
fn from_vec4(color: Vec4) -> Self {
Self {
red: color[0],
green: color[1],
blue: color[2],
alpha: color[3],
}
}
fn from_vec3(color: Vec3) -> Self {
Self {
red: color[0],
green: color[1],
blue: color[2],
alpha: 1.0,
}
}
}
impl ColorToPacked for Srgba {
fn to_u8_array(self) -> [u8; 4] {
[self.red, self.green, self.blue, self.alpha]
.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
}
fn to_u8_array_no_alpha(self) -> [u8; 3] {
[self.red, self.green, self.blue].map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8)
}
fn from_u8_array(color: [u8; 4]) -> Self {
Self::from_f32_array(color.map(|u| u as f32 / 255.0))
}
fn from_u8_array_no_alpha(color: [u8; 3]) -> Self {
Self::from_f32_array_no_alpha(color.map(|u| u as f32 / 255.0))
}
}
impl From<LinearRgba> for Srgba {
#[inline]
fn from(value: LinearRgba) -> Self {
Self {
red: Srgba::gamma_function_inverse(value.red),
green: Srgba::gamma_function_inverse(value.green),
blue: Srgba::gamma_function_inverse(value.blue),
alpha: value.alpha,
}
}
}
impl From<Srgba> for LinearRgba {
#[inline]
fn from(value: Srgba) -> Self {
Self {
red: Srgba::gamma_function(value.red),
green: Srgba::gamma_function(value.green),
blue: Srgba::gamma_function(value.blue),
alpha: value.alpha,
}
}
}
impl From<Xyza> for Srgba {
fn from(value: Xyza) -> Self {
LinearRgba::from(value).into()
}
}
impl From<Srgba> for Xyza {
fn from(value: Srgba) -> Self {
LinearRgba::from(value).into()
}
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum HexColorError {
#[error("Invalid hex string")]
Parse(#[from] std::num::ParseIntError),
#[error("Unexpected length of hex string")]
Length,
#[error("Invalid hex char")]
Char(char),
}
#[cfg(test)]
mod tests {
use crate::testing::assert_approx_eq;
use super::*;
#[test]
fn test_to_from_linear() {
let srgba = Srgba::new(0.0, 0.5, 1.0, 1.0);
let linear_rgba: LinearRgba = srgba.into();
assert_eq!(linear_rgba.red, 0.0);
assert_approx_eq!(linear_rgba.green, 0.2140, 0.0001);
assert_approx_eq!(linear_rgba.blue, 1.0, 0.0001);
assert_eq!(linear_rgba.alpha, 1.0);
let srgba2: Srgba = linear_rgba.into();
assert_eq!(srgba2.red, 0.0);
assert_approx_eq!(srgba2.green, 0.5, 0.0001);
assert_approx_eq!(srgba2.blue, 1.0, 0.0001);
assert_eq!(srgba2.alpha, 1.0);
}
#[test]
fn euclidean_distance() {
let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
let b = Srgba::new(1.0, 1.0, 1.0, 1.0);
assert_eq!(a.distance_squared(&b), 3.0);
let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
let b = Srgba::new(1.0, 1.0, 1.0, 0.0);
assert_eq!(a.distance_squared(&b), 3.0);
let a = Srgba::new(0.0, 0.0, 0.0, 1.0);
let b = Srgba::new(1.0, 0.0, 0.0, 1.0);
assert_eq!(a.distance_squared(&b), 1.0);
}
#[test]
fn darker_lighter() {
let color = Srgba::new(0.4, 0.5, 0.6, 1.0);
let darker1 = color.darker(0.1);
let darker2 = darker1.darker(0.1);
let twice_as_dark = color.darker(0.2);
assert!(darker2.distance_squared(&twice_as_dark) < 0.0001);
let lighter1 = color.lighter(0.1);
let lighter2 = lighter1.lighter(0.1);
let twice_as_light = color.lighter(0.2);
assert!(lighter2.distance_squared(&twice_as_light) < 0.0001);
}
#[test]
fn hex_color() {
assert_eq!(Srgba::hex("FFF"), Ok(Srgba::WHITE));
assert_eq!(Srgba::hex("FFFF"), Ok(Srgba::WHITE));
assert_eq!(Srgba::hex("FFFFFF"), Ok(Srgba::WHITE));
assert_eq!(Srgba::hex("FFFFFFFF"), Ok(Srgba::WHITE));
assert_eq!(Srgba::hex("000"), Ok(Srgba::BLACK));
assert_eq!(Srgba::hex("000F"), Ok(Srgba::BLACK));
assert_eq!(Srgba::hex("000000"), Ok(Srgba::BLACK));
assert_eq!(Srgba::hex("000000FF"), Ok(Srgba::BLACK));
assert_eq!(Srgba::hex("03a9f4"), Ok(Srgba::rgb_u8(3, 169, 244)));
assert_eq!(Srgba::hex("yy"), Err(HexColorError::Length));
assert_eq!(Srgba::hex("#f2a"), Ok(Srgba::rgb_u8(255, 34, 170)));
assert_eq!(Srgba::hex("#e23030"), Ok(Srgba::rgb_u8(226, 48, 48)));
assert_eq!(Srgba::hex("#ff"), Err(HexColorError::Length));
assert_eq!(Srgba::hex("11223344"), Ok(Srgba::rgba_u8(17, 34, 51, 68)));
assert_eq!(Srgba::hex("1234"), Ok(Srgba::rgba_u8(17, 34, 51, 68)));
assert_eq!(Srgba::hex("12345678"), Ok(Srgba::rgba_u8(18, 52, 86, 120)));
assert_eq!(Srgba::hex("4321"), Ok(Srgba::rgba_u8(68, 51, 34, 17)));
assert!(matches!(Srgba::hex("yyy"), Err(HexColorError::Parse(_))));
assert!(matches!(Srgba::hex("##fff"), Err(HexColorError::Parse(_))));
}
}