pub mod colorspace;
pub mod delta_e;
pub mod distinct;
mod helper;
pub mod named;
pub mod parser;
pub mod random;
mod types;
use std::fmt;
use colorspace::ColorSpace;
pub use helper::Fraction;
use helper::{clamp, interpolate, interpolate_angle, mod_positive};
use types::{Hue, Scalar};
#[derive(Clone)]
pub struct Color {
hue: Hue,
saturation: Scalar,
lightness: Scalar,
alpha: Scalar,
}
const D65_XN: Scalar = 0.950_470;
const D65_YN: Scalar = 1.0;
const D65_ZN: Scalar = 1.088_830;
impl Color {
pub fn from_hsla(hue: Scalar, saturation: Scalar, lightness: Scalar, alpha: Scalar) -> Color {
Self::from(&HSLA {
h: hue,
s: saturation,
l: lightness,
alpha,
})
}
pub fn from_hsl(hue: Scalar, saturation: Scalar, lightness: Scalar) -> Color {
Self::from(&HSLA {
h: hue,
s: saturation,
l: lightness,
alpha: 1.0,
})
}
pub fn from_rgba(r: u8, g: u8, b: u8, alpha: Scalar) -> Color {
Self::from(&RGBA::<u8> { r, g, b, alpha })
}
pub fn from_rgb(r: u8, g: u8, b: u8) -> Color {
Self::from(&RGBA::<u8> {
r,
g,
b,
alpha: 1.0,
})
}
pub fn from_rgba_float(r: Scalar, g: Scalar, b: Scalar, alpha: Scalar) -> Color {
Self::from(&RGBA::<f64> { r, g, b, alpha })
}
pub fn from_rgb_float(r: Scalar, g: Scalar, b: Scalar) -> Color {
Self::from(&RGBA::<f64> {
r,
g,
b,
alpha: 1.0,
})
}
pub fn from_xyz(x: Scalar, y: Scalar, z: Scalar, alpha: Scalar) -> Color {
Self::from(&XYZ { x, y, z, alpha })
}
pub fn from_lms(l: Scalar, m: Scalar, s: Scalar, alpha: Scalar) -> Color {
Self::from(&LMS { l, m, s, alpha })
}
pub fn from_lab(l: Scalar, a: Scalar, b: Scalar, alpha: Scalar) -> Color {
Self::from(&Lab { l, a, b, alpha })
}
pub fn from_lch(l: Scalar, c: Scalar, h: Scalar, alpha: Scalar) -> Color {
Self::from(&LCh { l, c, h, alpha })
}
pub fn from_cmyk(c: Scalar, m: Scalar, y: Scalar, k: Scalar) -> Color {
Self::from(&CMYK { c, m, y, k })
}
pub fn from_hex(hex_string: &str) -> Color {
parser::parse_hex(hex_string).unwrap().1
}
pub fn to_hsla(&self) -> HSLA {
HSLA::from(self)
}
pub fn to_hsl_string(&self, format: Format) -> String {
format!(
"hsl({:.0},{space}{:.1}%,{space}{:.1}%)",
self.hue.value(),
100.0 * self.saturation,
100.0 * self.lightness,
space = if format == Format::Spaces { " " } else { "" }
)
}
pub fn to_rgba(&self) -> RGBA<u8> {
RGBA::<u8>::from(self)
}
pub fn to_rgb_string(&self, format: Format) -> String {
let rgba = RGBA::<u8>::from(self);
format!(
"rgb({r},{space}{g},{space}{b})",
r = rgba.r,
g = rgba.g,
b = rgba.b,
space = if format == Format::Spaces { " " } else { "" }
)
}
pub fn to_cmyk(&self) -> CMYK {
CMYK::from(self)
}
pub fn to_cmyk_string(&self, format: Format) -> String {
let cmyk = CMYK::from(self);
format!(
"cmyk({c},{space}{m},{space}{y},{space}{k})",
c = (cmyk.c * 100.0).round(),
m = (cmyk.m * 100.0).round(),
y = (cmyk.y * 100.0).round(),
k = (cmyk.k * 100.0).round(),
space = if format == Format::Spaces { " " } else { "" }
)
}
pub fn to_rgb_float_string(&self, format: Format) -> String {
let rgba = RGBA::<f64>::from(self);
format!(
"rgb({r:.3},{space}{g:.3},{space}{b:.3})",
r = rgba.r,
g = rgba.g,
b = rgba.b,
space = if format == Format::Spaces { " " } else { "" }
)
}
pub fn to_rgb_hex_string(&self, leading_hash: bool) -> String {
let hex = HEX::from(self);
format!(
"{}{}",
if leading_hash { "#" } else { "" },
hex.val
)
}
pub fn to_rgba_float(&self) -> RGBA<Scalar> {
RGBA::<f64>::from(self)
}
pub fn to_u32(&self) -> u32 {
let rgba = self.to_rgba();
u32::from(rgba.r).wrapping_shl(16) + u32::from(rgba.g).wrapping_shl(8) + u32::from(rgba.b)
}
pub fn to_xyz(&self) -> XYZ {
XYZ::from(self)
}
pub fn to_lms(&self) -> LMS {
LMS::from(self)
}
pub fn to_hex(&self) -> HEX {
HEX::from(self)
}
pub fn to_lab(&self) -> Lab {
Lab::from(self)
}
pub fn to_lab_string(&self, format: Format) -> String {
let lab = Lab::from(self);
format!(
"Lab({l:.0},{space}{a:.0},{space}{b:.0})",
l = lab.l,
a = lab.a,
b = lab.b,
space = if format == Format::Spaces { " " } else { "" }
)
}
pub fn to_lch(&self) -> LCh {
LCh::from(self)
}
pub fn to_lch_string(&self, format: Format) -> String {
let lch = LCh::from(self);
format!(
"LCh({l:.0},{space}{c:.0},{space}{h:.0})",
l = lch.l,
c = lch.c,
h = lch.h,
space = if format == Format::Spaces { " " } else { "" }
)
}
pub fn black() -> Color {
Color::from_hsl(0.0, 0.0, 0.0)
}
pub fn white() -> Color {
Color::from_hsl(0.0, 0.0, 1.0)
}
pub fn red() -> Color {
Color::from_rgb(255, 0, 0)
}
pub fn green() -> Color {
Color::from_rgb(0, 128, 0)
}
pub fn blue() -> Color {
Color::from_rgb(0, 0, 255)
}
pub fn yellow() -> Color {
Color::from_rgb(255, 255, 0)
}
pub fn fuchsia() -> Color {
Color::from_rgb(255, 0, 255)
}
pub fn aqua() -> Color {
Color::from_rgb(0, 255, 255)
}
pub fn lime() -> Color {
Color::from_rgb(0, 255, 0)
}
pub fn maroon() -> Color {
Color::from_rgb(128, 0, 0)
}
pub fn olive() -> Color {
Color::from_rgb(128, 128, 0)
}
pub fn navy() -> Color {
Color::from_rgb(0, 0, 128)
}
pub fn purple() -> Color {
Color::from_rgb(128, 0, 128)
}
pub fn teal() -> Color {
Color::from_rgb(0, 128, 128)
}
pub fn silver() -> Color {
Color::from_rgb(192, 192, 192)
}
pub fn gray() -> Color {
Color::from_rgb(128, 128, 128)
}
pub fn graytone(lightness: Scalar) -> Color {
Color::from_hsl(0.0, 0.0, lightness)
}
pub fn rotate_hue(&self, delta: Scalar) -> Color {
Self::from_hsla(
self.hue.value() + delta,
self.saturation,
self.lightness,
self.alpha,
)
}
pub fn complementary(&self) -> Color {
self.rotate_hue(180.0)
}
pub fn lighten(&self, f: Scalar) -> Color {
Self::from_hsla(
self.hue.value(),
self.saturation,
self.lightness + f,
self.alpha,
)
}
pub fn darken(&self, f: Scalar) -> Color {
self.lighten(-f)
}
pub fn saturate(&self, f: Scalar) -> Color {
Self::from_hsla(
self.hue.value(),
self.saturation + f,
self.lightness,
self.alpha,
)
}
pub fn desaturate(&self, f: Scalar) -> Color {
self.saturate(-f)
}
pub fn simulate_colorblindness(&self, cb_ty: ColorblindnessType) -> Color {
let (l, m, s, alpha) = match cb_ty {
ColorblindnessType::Protanopia => {
let LMS { m, s, alpha, .. } = self.to_lms();
let l = 1.051_182_94 * m - 0.051_160_99 * s;
(l, m, s, alpha)
}
ColorblindnessType::Deuteranopia => {
let LMS { l, s, alpha, .. } = self.to_lms();
let m = 0.951_309_2 * l + 0.048_669_92 * s;
(l, m, s, alpha)
}
ColorblindnessType::Tritanopia => {
let LMS { l, m, alpha, .. } = self.to_lms();
let s = -0.867_447_36 * l + 1.867_270_89 * m;
(l, m, s, alpha)
}
};
Color::from_lms(l, m, s, alpha)
}
pub fn to_gray(&self) -> Color {
let hue = self.hue;
let c = self.to_lch();
let mut gray = Color::from_lch(c.l, 0.0, 0.0, 1.0).desaturate(1.0);
gray.hue = hue;
gray
}
pub fn brightness(&self) -> Scalar {
let c = self.to_rgba_float();
(299.0 * c.r + 587.0 * c.g + 114.0 * c.b) / 1000.0
}
pub fn is_light(&self) -> bool {
self.brightness() > 0.5
}
pub fn luminance(&self) -> Scalar {
fn f(s: Scalar) -> Scalar {
if s <= 0.03928 {
s / 12.92
} else {
Scalar::powf((s + 0.055) / 1.055, 2.4)
}
}
let c = self.to_rgba_float();
let r = f(c.r);
let g = f(c.g);
let b = f(c.b);
0.2126 * r + 0.7152 * g + 0.0722 * b
}
pub fn contrast_ratio(&self, other: &Color) -> Scalar {
let l_self = self.luminance();
let l_other = other.luminance();
if l_self > l_other {
(l_self + 0.05) / (l_other + 0.05)
} else {
(l_other + 0.05) / (l_self + 0.05)
}
}
pub fn text_color(&self) -> Color {
const THRESHOLD: Scalar = 0.179;
if self.luminance() > THRESHOLD {
Color::black()
} else {
Color::white()
}
}
pub fn distance_delta_e_cie76(&self, other: &Color) -> Scalar {
delta_e::cie76(&self.to_lab(), &other.to_lab())
}
pub fn distance_delta_e_ciede2000(&self, other: &Color) -> Scalar {
delta_e::ciede2000(&self.to_lab(), &other.to_lab())
}
pub fn mix<C: ColorSpace>(self: &Color, other: &Color, fraction: Fraction) -> Color {
C::from_color(self)
.mix(&C::from_color(other), fraction)
.into_color()
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", HSLA::from(self).to_string())
}
}
impl fmt::Debug for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Color::from_{}", self.to_rgb_string(Format::NoSpaces))
}
}
impl PartialEq for Color {
fn eq(&self, other: &Color) -> bool {
self.to_rgba() == other.to_rgba()
}
}
impl From<&HSLA> for Color {
fn from(color: &HSLA) -> Self {
Color {
hue: Hue::from(color.h),
saturation: clamp(0.0, 1.0, color.s),
lightness: clamp(0.0, 1.0, color.l),
alpha: clamp(0.0, 1.0, color.alpha),
}
}
}
impl From<&RGBA<u8>> for Color {
fn from(color: &RGBA<u8>) -> Self {
let max_chroma = u8::max(u8::max(color.r, color.g), color.b);
let min_chroma = u8::min(u8::min(color.r, color.g), color.b);
let chroma = max_chroma - min_chroma;
let chroma_s = Scalar::from(chroma) / 255.0;
let r_s = Scalar::from(color.r) / 255.0;
let g_s = Scalar::from(color.g) / 255.0;
let b_s = Scalar::from(color.b) / 255.0;
let hue = 60.0
* (if chroma == 0 {
0.0
} else if color.r == max_chroma {
mod_positive((g_s - b_s) / chroma_s, 6.0)
} else if color.g == max_chroma {
(b_s - r_s) / chroma_s + 2.0
} else {
(r_s - g_s) / chroma_s + 4.0
});
let lightness = (Scalar::from(max_chroma) + Scalar::from(min_chroma)) / (255.0 * 2.0);
let saturation = if chroma == 0 {
0.0
} else {
chroma_s / (1.0 - Scalar::abs(2.0 * lightness - 1.0))
};
Self::from(&HSLA {
h: hue,
s: saturation,
l: lightness,
alpha: color.alpha,
})
}
}
impl From<&RGBA<f64>> for Color {
fn from(color: &RGBA<f64>) -> Self {
let r = Scalar::round(clamp(0.0, 255.0, 255.0 * color.r)) as u8;
let g = Scalar::round(clamp(0.0, 255.0, 255.0 * color.g)) as u8;
let b = Scalar::round(clamp(0.0, 255.0, 255.0 * color.b)) as u8;
Self::from(&RGBA::<u8> {
r,
g,
b,
alpha: color.alpha,
})
}
}
impl From<&XYZ> for Color {
fn from(color: &XYZ) -> Self {
#![allow(clippy::many_single_char_names)]
let f = |c| {
if c <= 0.003_130_8 {
12.92 * c
} else {
1.055 * Scalar::powf(c, 1.0 / 2.4) - 0.055
}
};
let r = f(3.2406 * color.x - 1.5372 * color.y - 0.4986 * color.z);
let g = f(-0.9689 * color.x + 1.8758 * color.y + 0.0415 * color.z);
let b = f(0.0557 * color.x - 0.2040 * color.y + 1.0570 * color.z);
Self::from(&RGBA::<f64> {
r,
g,
b,
alpha: color.alpha,
})
}
}
impl From<&LMS> for Color {
fn from(color: &LMS) -> Self {
#![allow(clippy::many_single_char_names)]
let x = 1.91020 * color.l - 1.112_120 * color.m + 0.201_908 * color.s;
let y = 0.37095 * color.l + 0.629_054 * color.m + 0.000_000 * color.s;
let z = 0.00000 * color.l + 0.000_000 * color.m + 1.000_000 * color.s;
Self::from(&XYZ {
x,
y,
z,
alpha: color.alpha,
})
}
}
impl From<&HEX> for Color {
fn from(hex: &HEX) -> Self {
parser::parse_color(&hex.val).unwrap()
}
}
impl From<&Lab> for Color {
fn from(color: &Lab) -> Self {
#![allow(clippy::many_single_char_names)]
const DELTA: Scalar = 6.0 / 29.0;
let finv = |t| {
if t > DELTA {
Scalar::powf(t, 3.0)
} else {
3.0 * DELTA * DELTA * (t - 4.0 / 29.0)
}
};
let l_ = (color.l + 16.0) / 116.0;
let x = D65_XN * finv(l_ + color.a / 500.0);
let y = D65_YN * finv(l_);
let z = D65_ZN * finv(l_ - color.b / 200.0);
Self::from(&XYZ {
x,
y,
z,
alpha: color.alpha,
})
}
}
impl From<&LCh> for Color {
fn from(color: &LCh) -> Self {
#![allow(clippy::many_single_char_names)]
const DEG2RAD: Scalar = std::f64::consts::PI / 180.0;
let a = color.c * Scalar::cos(color.h * DEG2RAD);
let b = color.c * Scalar::sin(color.h * DEG2RAD);
Self::from(&Lab {
l: color.l,
a,
b,
alpha: color.alpha,
})
}
}
impl From<&CMYK> for Color {
fn from(color: &CMYK) -> Self {
#![allow(clippy::many_single_char_names)]
let r = 255.0 * ((1.0 - color.c) / 100.0) * ((1.0 - color.k) / 100.0);
let g = 255.0 * ((1.0 - color.m) / 100.0) * ((1.0 - color.k) / 100.0);
let b = 255.0 * ((1.0 - color.y) / 100.0) * ((1.0 - color.k) / 100.0);
Color::from(&RGBA::<f64> {
r,
g,
b,
alpha: 1.0,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RGBA<T> {
pub r: T,
pub g: T,
pub b: T,
pub alpha: Scalar,
}
impl ColorSpace for RGBA<f64> {
fn from_color(c: &Color) -> Self {
c.to_rgba_float()
}
fn into_color(&self) -> Color {
Color::from_rgba_float(self.r, self.g, self.b, self.alpha)
}
fn mix(&self, other: &Self, fraction: Fraction) -> Self {
Self {
r: interpolate(self.r, other.r, fraction),
g: interpolate(self.g, other.g, fraction),
b: interpolate(self.b, other.b, fraction),
alpha: interpolate(self.alpha, other.alpha, fraction),
}
}
}
impl From<&Color> for RGBA<f64> {
fn from(color: &Color) -> Self {
let h_s = color.hue.value() / 60.0;
let chr = (1.0 - Scalar::abs(2.0 * color.lightness - 1.0)) * color.saturation;
let m = color.lightness - chr / 2.0;
let x = chr * (1.0 - Scalar::abs(h_s % 2.0 - 1.0));
struct RGB(Scalar, Scalar, Scalar);
let col = if h_s < 1.0 {
RGB(chr, x, 0.0)
} else if 1.0 <= h_s && h_s < 2.0 {
RGB(x, chr, 0.0)
} else if 2.0 <= h_s && h_s < 3.0 {
RGB(0.0, chr, x)
} else if 3.0 <= h_s && h_s < 4.0 {
RGB(0.0, x, chr)
} else if 4.0 <= h_s && h_s < 5.0 {
RGB(x, 0.0, chr)
} else {
RGB(chr, 0.0, x)
};
RGBA {
r: col.0 + m,
g: col.1 + m,
b: col.2 + m,
alpha: color.alpha,
}
}
}
impl From<&Color> for RGBA<u8> {
fn from(color: &Color) -> Self {
let c = RGBA::<f64>::from(color);
let r = Scalar::round(255.0 * c.r) as u8;
let g = Scalar::round(255.0 * c.g) as u8;
let b = Scalar::round(255.0 * c.b) as u8;
RGBA {
r,
g,
b,
alpha: color.alpha,
}
}
}
impl fmt::Display for RGBA<f64> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "rgb({r}, {g}, {b})", r = self.r, g = self.g, b = self.b,)
}
}
impl fmt::Display for RGBA<u8> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "rgb({r}, {g}, {b})", r = self.r, g = self.g, b = self.b,)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct HSLA {
pub h: Scalar,
pub s: Scalar,
pub l: Scalar,
pub alpha: Scalar,
}
impl ColorSpace for HSLA {
fn from_color(c: &Color) -> Self {
c.to_hsla()
}
fn into_color(&self) -> Color {
Color::from_hsla(self.h, self.s, self.l, self.alpha)
}
fn mix(&self, other: &Self, fraction: Fraction) -> Self {
let self_hue = if self.s < 0.0001 { other.h } else { self.h };
let other_hue = if other.s < 0.0001 { self.h } else { other.h };
Self {
h: interpolate_angle(self_hue, other_hue, fraction),
s: interpolate(self.s, other.s, fraction),
l: interpolate(self.l, other.l, fraction),
alpha: interpolate(self.alpha, other.alpha, fraction),
}
}
}
impl From<&Color> for HSLA {
fn from(color: &Color) -> Self {
HSLA {
h: color.hue.value(),
s: color.saturation,
l: color.lightness,
alpha: color.alpha,
}
}
}
impl fmt::Display for HSLA {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "hsl({h}, {s}, {l})", h = self.h, s = self.s, l = self.l,)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct XYZ {
pub x: Scalar,
pub y: Scalar,
pub z: Scalar,
pub alpha: Scalar,
}
impl From<&Color> for XYZ {
fn from(color: &Color) -> Self {
#![allow(clippy::many_single_char_names)]
let finv = |c_: f64| {
if c_ <= 0.04045 {
c_ / 12.92
} else {
Scalar::powf((c_ + 0.055) / 1.055, 2.4)
}
};
let rec = RGBA::from(color);
let r = finv(rec.r);
let g = finv(rec.g);
let b = finv(rec.b);
let x = 0.4124 * r + 0.3576 * g + 0.1805 * b;
let y = 0.2126 * r + 0.7152 * g + 0.0722 * b;
let z = 0.0193 * r + 0.1192 * g + 0.9505 * b;
XYZ {
x,
y,
z,
alpha: color.alpha,
}
}
}
impl fmt::Display for XYZ {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "XYZ({x}, {y}, {z})", x = self.x, y = self.y, z = self.z,)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LMS {
pub l: Scalar,
pub m: Scalar,
pub s: Scalar,
pub alpha: Scalar,
}
impl From<&Color> for LMS {
fn from(color: &Color) -> Self {
let XYZ { x, y, z, alpha } = XYZ::from(color);
let l = 0.38971 * x + 0.68898 * y - 0.07868 * z;
let m = -0.22981 * x + 1.18340 * y + 0.04641 * z;
let s = 0.00000 * x + 0.00000 * y + 1.00000 * z;
LMS { l, m, s, alpha }
}
}
impl fmt::Display for LMS {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "LMS({l}, {m}, {s})", l = self.l, m = self.m, s = self.s,)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Lab {
pub l: Scalar,
pub a: Scalar,
pub b: Scalar,
pub alpha: Scalar,
}
impl ColorSpace for Lab {
fn from_color(c: &Color) -> Self {
c.to_lab()
}
fn into_color(&self) -> Color {
Color::from_lab(self.l, self.a, self.b, self.alpha)
}
fn mix(&self, other: &Self, fraction: Fraction) -> Self {
Self {
l: interpolate(self.l, other.l, fraction),
a: interpolate(self.a, other.a, fraction),
b: interpolate(self.b, other.b, fraction),
alpha: interpolate(self.alpha, other.alpha, fraction),
}
}
}
impl From<&Color> for Lab {
fn from(color: &Color) -> Self {
let rec = XYZ::from(color);
let cut = Scalar::powf(6.0 / 29.0, 3.0);
let f = |t| {
if t > cut {
Scalar::powf(t, 1.0 / 3.0)
} else {
(1.0 / 3.0) * Scalar::powf(29.0 / 6.0, 2.0) * t + 4.0 / 29.0
}
};
let fy = f(rec.y / D65_YN);
let l = 116.0 * fy - 16.0;
let a = 500.0 * (f(rec.x / D65_XN) - fy);
let b = 200.0 * (fy - f(rec.z / D65_ZN));
Lab {
l,
a,
b,
alpha: color.alpha,
}
}
}
impl fmt::Display for Lab {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Lab({l}, {a}, {b})", l = self.l, a = self.a, b = self.b,)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LCh {
pub l: Scalar,
pub c: Scalar,
pub h: Scalar,
pub alpha: Scalar,
}
impl ColorSpace for LCh {
fn from_color(c: &Color) -> Self {
c.to_lch()
}
fn into_color(&self) -> Color {
Color::from_lch(self.l, self.c, self.h, self.alpha)
}
fn mix(&self, other: &Self, fraction: Fraction) -> Self {
let self_hue = if self.c < 0.1 { other.h } else { self.h };
let other_hue = if other.c < 0.1 { self.h } else { other.h };
Self {
l: interpolate(self.l, other.l, fraction),
c: interpolate(self.c, other.c, fraction),
h: interpolate_angle(self_hue, other_hue, fraction),
alpha: interpolate(self.alpha, other.alpha, fraction),
}
}
}
impl From<&Color> for LCh {
fn from(color: &Color) -> Self {
let Lab { l, a, b, alpha } = Lab::from(color);
const RAD2DEG: Scalar = 180.0 / std::f64::consts::PI;
let c = Scalar::sqrt(a * a + b * b);
let h = mod_positive(Scalar::atan2(b, a) * RAD2DEG, 360.0);
LCh { l, c, h, alpha }
}
}
impl fmt::Display for LCh {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "LCh({l}, {c}, {h})", l = self.l, c = self.c, h = self.h,)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct CMYK {
pub c: Scalar,
pub m: Scalar,
pub y: Scalar,
pub k: Scalar,
}
impl From<&Color> for CMYK {
fn from(color: &Color) -> Self {
let rgba = RGBA::<u8>::from(color);
let r = (rgba.r as f64) / 255.0;
let g = (rgba.g as f64) / 255.0;
let b = (rgba.b as f64) / 255.0;
let biggest = if r >= g && r >= b {
r
} else if g >= r && g >= b {
g
} else {
b
};
let out_k = 1.0 - biggest;
let out_c = (1.0 - r - out_k) / biggest;
let out_m = (1.0 - g - out_k) / biggest;
let out_y = (1.0 - b - out_k) / biggest;
CMYK {
c: if out_c.is_nan() { 0.0 } else { out_c },
m: if out_m.is_nan() { 0.0 } else { out_m },
y: if out_y.is_nan() { 0.0 } else { out_y },
k: out_k,
}
}
}
impl fmt::Display for CMYK {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"cmyk({c}, {m}, {y}, {k})",
c = self.c,
m = self.m,
y = self.y,
k = self.k,
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct HEX {
pub val: String,
}
impl From<&Color> for HEX {
fn from(color: &Color) -> Self {
let rgb = RGBA::<u8>::from(color);
HEX{ val: format!("{:02x}{:02x}{:02x}",
rgb.r,
rgb.g,
rgb.b)
}
}
}
impl fmt::Display for HEX {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#{}", self.val )
}
}
pub enum ColorblindnessType {
Protanopia,
Deuteranopia,
Tritanopia,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Format {
Spaces,
NoSpaces,
}
#[derive(Debug, Clone)]
struct ColorStop {
color: Color,
position: Fraction,
}
#[derive(Debug, Clone)]
pub struct ColorScale {
color_stops: Vec<ColorStop>,
}
impl ColorScale {
pub fn empty() -> Self {
Self {
color_stops: Vec::new(),
}
}
pub fn add_stop(&mut self, color: Color, position: Fraction) -> &mut Self {
#![allow(clippy::float_cmp)]
let same_position = self
.color_stops
.iter_mut()
.find(|c| position.value() == c.position.value());
match same_position {
Some(color_stop) => color_stop.color = color,
None => {
let next_index = self
.color_stops
.iter()
.position(|c| position.value() < c.position.value());
let index = next_index.unwrap_or_else(|| self.color_stops.len());
let color_stop = ColorStop { color, position };
self.color_stops.insert(index, color_stop);
}
};
self
}
pub fn sample(
&self,
position: Fraction,
mix: &dyn Fn(&Color, &Color, Fraction) -> Color,
) -> Option<Color> {
if self.color_stops.len() < 2 {
return None;
}
let left_stop = self
.color_stops
.iter()
.rev()
.find(|c| position.value() >= c.position.value());
let right_stop = self
.color_stops
.iter()
.find(|c| position.value() <= c.position.value());
match (left_stop, right_stop) {
(Some(left_stop), Some(right_stop)) => {
let diff_color_stops = right_stop.position.value() - left_stop.position.value();
let diff_position = position.value() - left_stop.position.value();
let local_position = Fraction::from(diff_position / diff_color_stops);
let color = mix(&left_stop.color, &right_stop.color, local_position);
Some(color)
}
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn assert_almost_equal(c1: &Color, c2: &Color) {
let c1 = c1.to_rgba();
let c2 = c2.to_rgba();
assert!((c1.r as i32 - c2.r as i32).abs() <= 1);
assert!((c1.g as i32 - c2.g as i32).abs() <= 1);
assert!((c1.b as i32 - c2.b as i32).abs() <= 1);
}
#[test]
fn color_partial_eq() {
assert_eq!(
Color::from_hsl(120.0, 0.3, 0.5),
Color::from_hsl(360.0 + 120.0, 0.3, 0.5),
);
assert_eq!(
Color::from_rgba(1, 2, 3, 0.3),
Color::from_rgba(1, 2, 3, 0.3),
);
assert_eq!(Color::black(), Color::from_hsl(123.0, 0.3, 0.0));
assert_eq!(Color::white(), Color::from_hsl(123.0, 0.3, 1.0));
assert_ne!(
Color::from_hsl(120.0, 0.3, 0.5),
Color::from_hsl(122.0, 0.3, 0.5),
);
assert_ne!(
Color::from_hsl(120.0, 0.3, 0.5),
Color::from_hsl(120.0, 0.32, 0.5),
);
assert_ne!(
Color::from_hsl(120.0, 0.3, 0.5),
Color::from_hsl(120.0, 0.3, 0.52),
);
assert_ne!(
Color::from_hsla(120.0, 0.3, 0.5, 0.9),
Color::from_hsla(120.0, 0.3, 0.5, 0.901),
);
assert_ne!(
Color::from_rgba(1, 2, 3, 0.3),
Color::from_rgba(2, 2, 3, 0.3),
);
assert_ne!(
Color::from_rgba(1, 2, 3, 0.3),
Color::from_rgba(1, 3, 3, 0.3),
);
assert_ne!(
Color::from_rgba(1, 2, 3, 0.3),
Color::from_rgba(1, 2, 4, 0.3),
);
}
#[test]
fn rgb_to_hsl_conversion() {
assert_eq!(Color::white(), Color::from_rgb_float(1.0, 1.0, 1.0));
assert_eq!(Color::gray(), Color::from_rgb_float(0.5, 0.5, 0.5));
assert_eq!(Color::black(), Color::from_rgb_float(0.0, 0.0, 0.0));
assert_eq!(Color::red(), Color::from_rgb_float(1.0, 0.0, 0.0));
assert_eq!(
Color::from_hsl(60.0, 1.0, 0.375),
Color::from_rgb_float(0.75, 0.75, 0.0)
);
assert_eq!(Color::green(), Color::from_rgb_float(0.0, 0.5, 0.0));
assert_eq!(
Color::from_hsl(240.0, 1.0, 0.75),
Color::from_rgb_float(0.5, 0.5, 1.0)
);
assert_eq!(
Color::from_hsl(49.5, 0.893, 0.497),
Color::from_rgb_float(0.941, 0.785, 0.053)
);
assert_eq!(
Color::from_hsl(162.4, 0.779, 0.447),
Color::from_rgb_float(0.099, 0.795, 0.591)
);
}
#[test]
fn rgb_roundtrip_conversion() {
let roundtrip = |h, s, l| {
let color1 = Color::from_hsl(h, s, l);
let rgb = color1.to_rgba();
let color2 = Color::from_rgb(rgb.r, rgb.g, rgb.b);
assert_eq!(color1, color2);
};
roundtrip(0.0, 0.0, 1.0);
roundtrip(0.0, 0.0, 0.5);
roundtrip(0.0, 0.0, 0.0);
roundtrip(60.0, 1.0, 0.375);
roundtrip(120.0, 1.0, 0.25);
roundtrip(240.0, 1.0, 0.75);
roundtrip(49.5, 0.893, 0.497);
roundtrip(162.4, 0.779, 0.447);
for degree in 0..360 {
roundtrip(Scalar::from(degree), 0.5, 0.8);
}
}
#[test]
fn to_u32() {
assert_eq!(0, Color::black().to_u32());
assert_eq!(0xff0000, Color::red().to_u32());
assert_eq!(0xffffff, Color::white().to_u32());
assert_eq!(0xf4230f, Color::from_rgb(0xf4, 0x23, 0x0f).to_u32());
}
#[test]
fn xyz_conversion() {
assert_eq!(Color::white(), Color::from_xyz(0.9505, 1.0, 1.0890, 1.0));
assert_eq!(Color::red(), Color::from_xyz(0.4123, 0.2126, 0.01933, 1.0));
assert_eq!(
Color::from_hsl(109.999, 0.08654, 0.407843),
Color::from_xyz(0.13123, 0.15372, 0.13174, 1.0)
);
let roundtrip = |h, s, l| {
let color1 = Color::from_hsl(h, s, l);
let xyz1 = color1.to_xyz();
let color2 = Color::from_xyz(xyz1.x, xyz1.y, xyz1.z, 1.0);
assert_almost_equal(&color1, &color2);
};
for hue in 0..360 {
roundtrip(Scalar::from(hue), 0.2, 0.8);
}
}
#[test]
fn lms_conversion() {
let roundtrip = |h, s, l| {
let color1 = Color::from_hsl(h, s, l);
let lms1 = color1.to_lms();
let color2 = Color::from_lms(lms1.l, lms1.m, lms1.s, 1.0);
assert_almost_equal(&color1, &color2);
};
for hue in 0..360 {
roundtrip(Scalar::from(hue), 0.2, 0.8);
}
}
#[test]
fn lab_conversion() {
assert_eq!(Color::red(), Color::from_lab(53.233, 80.109, 67.22, 1.0));
let roundtrip = |h, s, l| {
let color1 = Color::from_hsl(h, s, l);
let lab1 = color1.to_lab();
let color2 = Color::from_lab(lab1.l, lab1.a, lab1.b, 1.0);
assert_almost_equal(&color1, &color2);
};
for hue in 0..360 {
roundtrip(Scalar::from(hue), 0.2, 0.8);
}
}
#[test]
fn lch_conversion() {
assert_eq!(
Color::from_hsl(0.0, 1.0, 0.245),
Color::from_lch(24.829, 60.093, 38.18, 1.0)
);
let roundtrip = |h, s, l| {
let color1 = Color::from_hsl(h, s, l);
let lch1 = color1.to_lch();
let color2 = Color::from_lch(lch1.l, lch1.c, lch1.h, 1.0);
assert_almost_equal(&color1, &color2);
};
for hue in 0..360 {
roundtrip(Scalar::from(hue), 0.2, 0.8);
}
}
#[test]
fn rotate_hue() {
assert_eq!(Color::lime(), Color::red().rotate_hue(120.0));
}
#[test]
fn complementary() {
assert_eq!(Color::fuchsia(), Color::lime().complementary());
assert_eq!(Color::lime(), Color::fuchsia().complementary());
}
#[test]
fn lighten() {
assert_eq!(
Color::from_hsl(90.0, 0.5, 0.7),
Color::from_hsl(90.0, 0.5, 0.3).lighten(0.4)
);
assert_eq!(
Color::from_hsl(90.0, 0.5, 1.0),
Color::from_hsl(90.0, 0.5, 0.3).lighten(0.8)
);
}
#[test]
fn to_gray() {
let salmon = Color::from_rgb(250, 128, 114);
assert_eq!(0.0, salmon.to_gray().to_hsla().s);
assert_relative_eq!(
salmon.luminance(),
salmon.to_gray().luminance(),
max_relative = 0.01
);
assert_eq!(Color::graytone(0.3), Color::graytone(0.3).to_gray());
}
#[test]
fn brightness() {
assert_eq!(0.0, Color::black().brightness());
assert_eq!(1.0, Color::white().brightness());
assert_eq!(0.5, Color::graytone(0.5).brightness());
}
#[test]
fn luminance() {
assert_eq!(1.0, Color::white().luminance());
let hotpink = Color::from_rgb(255, 105, 180);
assert_relative_eq!(0.347, hotpink.luminance(), max_relative = 0.01);
assert_eq!(0.0, Color::black().luminance());
}
#[test]
fn contrast_ratio() {
assert_relative_eq!(21.0, Color::black().contrast_ratio(&Color::white()));
assert_relative_eq!(21.0, Color::white().contrast_ratio(&Color::black()));
assert_relative_eq!(1.0, Color::white().contrast_ratio(&Color::white()));
assert_relative_eq!(1.0, Color::red().contrast_ratio(&Color::red()));
assert_relative_eq!(
4.26,
Color::from_rgb(255, 119, 153).contrast_ratio(&Color::from_rgb(0, 68, 85)),
max_relative = 0.01
);
}
#[test]
fn text_color() {
assert_eq!(Color::white(), Color::graytone(0.4).text_color());
assert_eq!(Color::black(), Color::graytone(0.6).text_color());
}
#[test]
fn distance_delta_e_cie76() {
let c = Color::from_rgb(255, 127, 14);
assert_eq!(0.0, c.distance_delta_e_cie76(&c));
let c1 = Color::from_rgb(50, 100, 200);
let c2 = Color::from_rgb(200, 10, 0);
assert_eq!(123.0, c1.distance_delta_e_cie76(&c2).round());
}
#[test]
fn to_hsl_string() {
let c = Color::from_hsl(91.3, 0.541, 0.983);
assert_eq!("hsl(91, 54.1%, 98.3%)", c.to_hsl_string(Format::Spaces));
}
#[test]
fn to_rgb_string() {
let c = Color::from_rgb(255, 127, 4);
assert_eq!("rgb(255, 127, 4)", c.to_rgb_string(Format::Spaces));
}
#[test]
fn to_rgb_float_string() {
assert_eq!(
"rgb(0.000, 0.000, 0.000)",
Color::black().to_rgb_float_string(Format::Spaces)
);
assert_eq!(
"rgb(1.000, 1.000, 1.000)",
Color::white().to_rgb_float_string(Format::Spaces)
);
let c = Color::from_rgb_float(0.12, 0.45, 0.78);
assert_eq!(
"rgb(0.122, 0.451, 0.780)",
c.to_rgb_float_string(Format::Spaces)
);
}
#[test]
fn to_rgb_hex_string() {
let c = Color::from_rgb(255, 127, 4);
assert_eq!("ff7f04", c.to_rgb_hex_string(false));
assert_eq!("#ff7f04", c.to_rgb_hex_string(true));
}
#[test]
fn to_lab_string() {
let c = Color::from_lab(41.0, 83.0, -93.0, 1.0);
assert_eq!("Lab(41, 83, -93)", c.to_lab_string(Format::Spaces));
}
#[test]
fn to_lch_string() {
let c = Color::from_lch(52.0, 44.0, 271.0, 1.0);
assert_eq!("LCh(52, 44, 271)", c.to_lch_string(Format::Spaces));
}
#[test]
fn mix() {
assert_eq!(
Color::purple(),
Color::red().mix::<RGBA<f64>>(&Color::blue(), Fraction::from(0.5))
);
assert_eq!(
Color::fuchsia(),
Color::red().mix::<HSLA>(&Color::blue(), Fraction::from(0.5))
);
}
#[test]
fn mixing_with_gray_preserves_hue() {
let hue = 123.0;
let input = Color::from_hsla(hue, 0.5, 0.5, 1.0);
let hue_after_mixing = |other| input.mix::<HSLA>(&other, Fraction::from(0.5)).to_hsla().h;
assert_eq!(hue, hue_after_mixing(Color::black()));
assert_eq!(hue, hue_after_mixing(Color::graytone(0.2)));
assert_eq!(hue, hue_after_mixing(Color::graytone(0.7)));
assert_eq!(hue, hue_after_mixing(Color::white()));
}
#[test]
fn color_scale_add_preserves_ordering() {
let mut color_scale = ColorScale::empty();
color_scale
.add_stop(Color::red(), Fraction::from(0.5))
.add_stop(Color::gray(), Fraction::from(0.0))
.add_stop(Color::blue(), Fraction::from(1.0));
assert_eq!(color_scale.color_stops.get(0).unwrap().color, Color::gray());
assert_eq!(color_scale.color_stops.get(1).unwrap().color, Color::red());
assert_eq!(color_scale.color_stops.get(2).unwrap().color, Color::blue());
}
#[test]
fn color_scale_empty_sample_none() {
let mix = Color::mix::<Lab>;
let color_scale = ColorScale::empty();
let color = color_scale.sample(Fraction::from(0.0), &mix);
assert_eq!(color, None);
}
#[test]
fn color_scale_one_color_sample_none() {
let mix = Color::mix::<Lab>;
let mut color_scale = ColorScale::empty();
color_scale.add_stop(Color::red(), Fraction::from(0.0));
let color = color_scale.sample(Fraction::from(0.0), &mix);
assert_eq!(color, None);
}
#[test]
fn color_scale_sample_same_position() {
let mix = Color::mix::<Lab>;
let mut color_scale = ColorScale::empty();
color_scale
.add_stop(Color::red(), Fraction::from(0.0))
.add_stop(Color::green(), Fraction::from(1.0))
.add_stop(Color::blue(), Fraction::from(0.0))
.add_stop(Color::white(), Fraction::from(1.0));
let sample_blue = color_scale.sample(Fraction::from(0.0), &mix).unwrap();
let sample_white = color_scale.sample(Fraction::from(1.0), &mix).unwrap();
assert_eq!(sample_blue, Color::blue());
assert_eq!(sample_white, Color::white());
}
#[test]
fn color_scale_sample() {
let mix = Color::mix::<Lab>;
let mut color_scale = ColorScale::empty();
color_scale
.add_stop(Color::green(), Fraction::from(1.0))
.add_stop(Color::red(), Fraction::from(0.0));
let sample_red_green = color_scale.sample(Fraction::from(0.5), &mix).unwrap();
let mix_red_green = mix(&Color::red(), &Color::green(), Fraction::from(0.5));
assert_eq!(sample_red_green, mix_red_green);
}
#[test]
fn color_scale_sample_position() {
let mix = Color::mix::<Lab>;
let mut color_scale = ColorScale::empty();
color_scale
.add_stop(Color::green(), Fraction::from(0.5))
.add_stop(Color::red(), Fraction::from(0.0))
.add_stop(Color::blue(), Fraction::from(1.0));
let sample_red = color_scale.sample(Fraction::from(0.0), &mix).unwrap();
let sample_green = color_scale.sample(Fraction::from(0.5), &mix).unwrap();
let sample_blue = color_scale.sample(Fraction::from(1.0), &mix).unwrap();
let sample_red_green = color_scale.sample(Fraction::from(0.25), &mix).unwrap();
let sample_green_blue = color_scale.sample(Fraction::from(0.75), &mix).unwrap();
let mix_red_green = mix(&Color::red(), &Color::green(), Fraction::from(0.50));
let mix_green_blue = mix(&Color::green(), &Color::blue(), Fraction::from(0.50));
assert_eq!(sample_red, Color::red());
assert_eq!(sample_green, Color::green());
assert_eq!(sample_blue, Color::blue());
assert_eq!(sample_red_green, mix_red_green);
assert_eq!(sample_green_blue, mix_green_blue);
}
#[test]
fn to_cmyk_string() {
let white = Color::from_rgb(255, 255, 255);
assert_eq!("cmyk(0, 0, 0, 0)", white.to_cmyk_string(Format::Spaces));
let black = Color::from_rgb(0, 0, 0);
assert_eq!("cmyk(0, 0, 0, 100)", black.to_cmyk_string(Format::Spaces));
let c = Color::from_rgb(19, 19, 1);
assert_eq!("cmyk(0, 0, 95, 93)", c.to_cmyk_string(Format::Spaces));
let c1 = Color::from_rgb(55, 55, 55);
assert_eq!("cmyk(0, 0, 0, 78)", c1.to_cmyk_string(Format::Spaces));
let c2 = Color::from_rgb(136, 117, 78);
assert_eq!("cmyk(0, 14, 43, 47)", c2.to_cmyk_string(Format::Spaces));
let c3 = Color::from_rgb(143, 111, 76);
assert_eq!("cmyk(0, 22, 47, 44)", c3.to_cmyk_string(Format::Spaces));
}
}