mod matrix_utils;
pub mod color_space;
mod parse;
pub mod conversion;
pub mod accessibility;
use std::fmt;
use color_space::*;
use parse::*;
use crate::accessibility::{get_contrast_color as get_contrast_color_impl, get_contrast_ratio as get_contrast_ratio_impl};
pub use peniko;
#[derive(Debug, Clone)]
pub struct BigColor {
oklch: OKLCH,
original_input: String,
format: ColorFormat,
ok: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ColorFormat {
RGB,
PRGB,
HEX,
HEX3,
HEX6,
HEX8,
HSL,
HSV,
HSB,
LAB,
LCH,
OKLAB,
OKLCH,
CMYK,
NAME,
INVALID,
}
impl PartialEq for BigColor {
fn eq(&self, other: &Self) -> bool {
self.oklch.l == other.oklch.l &&
self.oklch.c == other.oklch.c &&
self.oklch.h == other.oklch.h &&
self.oklch.alpha == other.oklch.alpha
}
}
impl Default for BigColor {
fn default() -> Self {
BigColor {
oklch: OKLCH {
l: 0.0,
c: 0.0,
h: 0.0,
alpha: 1.0,
},
original_input: String::new(),
format: ColorFormat::INVALID,
ok: false,
}
}
}
impl BigColor {
pub fn new<T: Into<String>>(color: T) -> Self {
let color_str = color.into();
if color_str.is_empty() {
return BigColor::default();
}
let rgb = input_to_rgb(&color_str);
if !rgb.ok {
return BigColor::default();
}
let oklch = rgb_to_oklch(rgb.r, rgb.g, rgb.b, rgb.a);
BigColor {
oklch,
original_input: color_str,
format: rgb.format,
ok: rgb.ok,
}
}
pub fn from_string<T: Into<String>>(input: T) -> Result<Self, String> {
let color = Self::new(input);
if color.is_valid() {
Ok(color)
} else {
Err(format!("Invalid color: {}", color.get_original_input()))
}
}
pub fn is_dark(&self) -> bool {
self.oklch.l < 0.5
}
pub fn is_light(&self) -> bool {
!self.is_dark()
}
pub fn is_valid(&self) -> bool {
self.ok
}
pub fn get_original_input(&self) -> &str {
&self.original_input
}
pub fn get_format(&self) -> ColorFormat {
self.format
}
pub fn get_alpha(&self) -> f32 {
self.oklch.alpha
}
pub fn get_brightness(&self) -> f32 {
self.oklch.l * 255.0
}
pub fn get_luminance(&self) -> f32 {
let rgb = self.to_rgb();
let r_srgb = rgb.r as f32 / 255.0;
let g_srgb = rgb.g as f32 / 255.0;
let b_srgb = rgb.b as f32 / 255.0;
let r = if r_srgb <= 0.03928 {
r_srgb / 12.92
} else {
((r_srgb + 0.055) / 1.055).powf(2.4)
};
let g = if g_srgb <= 0.03928 {
g_srgb / 12.92
} else {
((g_srgb + 0.055) / 1.055).powf(2.4)
};
let b = if b_srgb <= 0.03928 {
b_srgb / 12.92
} else {
((b_srgb + 0.055) / 1.055).powf(2.4)
};
0.2126 * r + 0.7152 * g + 0.0722 * b
}
pub fn set_alpha(&mut self, value: f32) -> &mut Self {
self.oklch.alpha = bound_alpha(value);
self
}
pub fn to_hsv(&self) -> HSV {
let (r, g, b, _) = oklch_to_rgb(self.oklch);
let hsv = rgb_to_hsv(r, g, b);
HSV {
h: hsv.h * 360.0,
s: hsv.s,
v: hsv.v,
a: self.oklch.alpha,
}
}
pub fn to_hsv_string(&self) -> String {
let hsv = self.to_hsv();
let h = hsv.h.round() as i32;
let s = (hsv.s * 100.0).round() as i32;
let v = (hsv.v * 100.0).round() as i32;
if (self.oklch.alpha - 1.0).abs() < f32::EPSILON {
format!("hsv({}, {}%, {}%)", h, s, v)
} else {
format!("hsva({}, {}%, {}%, {})", h, s, v, (self.oklch.alpha * 100.0).round() / 100.0)
}
}
pub fn to_hsl(&self) -> HSL {
let (r, g, b, _) = oklch_to_rgb(self.oklch);
let r_norm = r as f32 / 255.0;
let g_norm = g as f32 / 255.0;
let b_norm = b as f32 / 255.0;
let max = r_norm.max(g_norm).max(b_norm);
let min = r_norm.min(g_norm).min(b_norm);
let mut h = 0.0;
let mut s = 0.0;
let l = (max + min) / 2.0;
if max != min {
let d = max - min;
s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) };
if max == r_norm {
h = (g_norm - b_norm) / d + (if g_norm < b_norm { 6.0 } else { 0.0 });
} else if max == g_norm {
h = (b_norm - r_norm) / d + 2.0;
} else if max == b_norm {
h = (r_norm - g_norm) / d + 4.0;
}
h /= 6.0;
}
HSL {
h: h * 360.0,
s,
l,
a: self.oklch.alpha,
}
}
pub fn to_hsl_string(&self) -> String {
let hsl = self.to_hsl();
let h = hsl.h.round() as i32;
let s = (hsl.s * 100.0).round() as i32;
let l = (hsl.l * 100.0).round() as i32;
if (self.oklch.alpha - 1.0).abs() < f32::EPSILON {
format!("hsl({}, {}%, {}%)", h, s, l)
} else {
format!("hsla({}, {}%, {}%, {})", h, s, l, (self.oklch.alpha * 100.0).round() / 100.0)
}
}
pub fn to_oklch(&self) -> OKLCH {
self.oklch
}
pub fn to_oklch_string(&self) -> String {
let l = (self.oklch.l * 100.0).round() / 100.0;
let c = (self.oklch.c * 100.0).round() / 100.0;
let h = self.oklch.h.round();
if (self.oklch.alpha - 1.0).abs() < f32::EPSILON {
format!("oklch({}% {} {})", l * 100.0, c, h)
} else {
format!("oklch({}% {} {} / {})", l * 100.0, c, h, self.oklch.alpha)
}
}
pub fn to_oklab(&self) -> OKLab {
oklch_to_oklab(self.oklch)
}
pub fn to_oklab_string(&self) -> String {
let oklab = self.to_oklab();
let l = (oklab.l * 100.0).round() / 100.0;
let a = (oklab.a * 100.0).round() / 100.0;
let b = (oklab.b * 100.0).round() / 100.0;
if (self.oklch.alpha - 1.0).abs() < f32::EPSILON {
format!("oklab({}% {} {})", l * 100.0, a, b)
} else {
format!("oklab({}% {} {} / {})", l * 100.0, a, b, self.oklch.alpha)
}
}
pub fn to_lch(&self) -> LCH {
let (r, g, b, _) = oklch_to_rgb(self.oklch);
rgb_to_lch(r, g, b, self.oklch.alpha)
}
pub fn to_lch_string(&self) -> String {
let lch = self.to_lch();
let l = lch.l.round();
let c = lch.c.round();
let h = lch.h.round();
if (self.oklch.alpha - 1.0).abs() < f32::EPSILON {
format!("lch({} {} {})", l, c, h)
} else {
format!("lch({} {} {} / {})", l, c, h, self.oklch.alpha)
}
}
pub fn to_lab(&self) -> Lab {
let (r, g, b, _) = oklch_to_rgb(self.oklch);
let xyz_d65 = rgb_to_xyz_d65(r, g, b, self.oklch.alpha);
let xyz_d50 = xyz_d65_to_xyz_d50(xyz_d65);
xyz_d50_to_lab(xyz_d50)
}
pub fn to_lab_string(&self) -> String {
let lab = self.to_lab();
let l = lab.l.round();
let a = lab.a.round();
let b = lab.b.round();
if (self.oklch.alpha - 1.0).abs() < f32::EPSILON {
format!("lab({} {} {})", l, a, b)
} else {
format!("lab({} {} {} / {})", l, a, b, self.oklch.alpha)
}
}
pub fn to_hex(&self, allow_3_char: bool) -> String {
let (r, g, b, _) = oklch_to_rgb(self.oklch);
rgb_to_hex(r, g, b, allow_3_char)
}
pub fn to_hex_string(&self, allow_3_char: bool) -> String {
format!("#{}", self.to_hex(allow_3_char))
}
pub fn to_hex8(&self, allow_4_char: bool) -> String {
let (r, g, b, _) = oklch_to_rgb(self.oklch);
rgba_to_hex(r, g, b, self.oklch.alpha, allow_4_char)
}
pub fn to_hex8_string(&self, allow_4_char: bool) -> String {
format!("#{}", self.to_hex8(allow_4_char))
}
pub fn to_rgb(&self) -> RGB {
let (r, g, b, a) = oklch_to_rgb(self.oklch);
RGB {
r,
g,
b,
a,
}
}
pub fn to_rgb_string(&self) -> String {
let rgb = self.to_rgb();
if (self.oklch.alpha - 1.0).abs() < f32::EPSILON {
format!("rgb({}, {}, {})", rgb.r, rgb.g, rgb.b)
} else {
format!("rgba({}, {}, {}, {})", rgb.r, rgb.g, rgb.b, (rgb.a * 100.0).round() / 100.0)
}
}
pub fn to_percentage_rgb(&self) -> PercentageRGB {
let rgb = self.to_rgb();
PercentageRGB {
r: (bound_01(rgb.r as f32, 255.0) * 100.0).round(),
g: (bound_01(rgb.g as f32, 255.0) * 100.0).round(),
b: (bound_01(rgb.b as f32, 255.0) * 100.0).round(),
a: rgb.a,
}
}
pub fn to_percentage_rgb_string(&self) -> String {
let prgb = self.to_percentage_rgb();
if (self.oklch.alpha - 1.0).abs() < f32::EPSILON {
format!("rgb({}%, {}%, {}%)", prgb.r, prgb.g, prgb.b)
} else {
format!("rgba({}%, {}%, {}%, {})", prgb.r, prgb.g, prgb.b, (prgb.a * 100.0).round() / 100.0)
}
}
pub fn to_name(&self) -> Option<&'static str> {
if self.oklch.alpha == 0.0 {
Some("transparent")
} else if self.oklch.alpha < 1.0 {
None
} else {
let (r, g, b, _) = oklch_to_rgb(self.oklch);
let hex = rgb_to_hex(r, g, b, true);
hex_names().get(&hex).map(|&name| name)
}
}
pub fn to_string(&self, format: Option<ColorFormat>) -> String {
let format = format.unwrap_or(self.format);
let has_alpha = self.oklch.alpha < 1.0 && self.oklch.alpha >= 0.0;
let needs_alpha_format =
format == ColorFormat::HEX ||
format == ColorFormat::HEX6 ||
format == ColorFormat::HEX3 ||
format == ColorFormat::HEX8 ||
format == ColorFormat::NAME;
if has_alpha && needs_alpha_format {
if format == ColorFormat::NAME && self.oklch.alpha == 0.0 {
if let Some(name) = self.to_name() {
return name.to_string();
}
}
return self.to_rgb_string();
}
match format {
ColorFormat::RGB => self.to_rgb_string(),
ColorFormat::PRGB => self.to_percentage_rgb_string(),
ColorFormat::HEX | ColorFormat::HEX6 => self.to_hex_string(false),
ColorFormat::HEX3 => self.to_hex_string(true),
ColorFormat::HEX8 => self.to_hex8_string(false),
ColorFormat::NAME => {
if let Some(name) = self.to_name() {
name.to_string()
} else {
self.to_hex_string(false)
}
},
ColorFormat::HSL => self.to_hsl_string(),
ColorFormat::HSV => self.to_hsv_string(),
ColorFormat::HSB => self.to_hsb_string(),
ColorFormat::LAB => self.to_lab_string(),
ColorFormat::LCH => self.to_lch_string(),
ColorFormat::OKLAB => self.to_oklab_string(),
ColorFormat::OKLCH => self.to_oklch_string(),
ColorFormat::CMYK => self.to_cmyk_string(),
_ => self.to_hex_string(false),
}
}
pub fn clone_color(&self) -> Self {
BigColor::new(self.to_string(None))
}
pub fn lighten(&mut self, amount: Option<f32>) -> &mut Self {
let amount = amount.unwrap_or(10.0);
self.oklch.l = (self.oklch.l + amount / 100.0).min(1.0).max(0.0);
self
}
pub fn brighten(&mut self, amount: Option<f32>) -> &mut Self {
let amount = amount.unwrap_or(10.0);
let (r, g, b, a) = oklch_to_rgb(self.oklch);
let r_new = (r as f32 - (255.0 * -(amount / 100.0))).round().max(0.0).min(255.0) as u8;
let g_new = (g as f32 - (255.0 * -(amount / 100.0))).round().max(0.0).min(255.0) as u8;
let b_new = (b as f32 - (255.0 * -(amount / 100.0))).round().max(0.0).min(255.0) as u8;
self.oklch = rgb_to_oklch(r_new, g_new, b_new, a);
self
}
pub fn darken(&mut self, amount: Option<f32>) -> &mut Self {
let amount = amount.unwrap_or(10.0);
self.oklch.l = (self.oklch.l - amount / 100.0).min(1.0).max(0.0);
self
}
pub fn desaturate(&mut self, amount: Option<f32>) -> &mut Self {
let amount = amount.unwrap_or(10.0);
self.oklch.c = (self.oklch.c - amount / 100.0).max(0.0);
self
}
pub fn saturate(&mut self, amount: Option<f32>) -> &mut Self {
let amount = amount.unwrap_or(10.0);
self.oklch.c = self.oklch.c + amount / 100.0;
self
}
pub fn greyscale(&mut self) -> &mut Self {
self.oklch.c = 0.0;
self
}
pub fn spin(&mut self, amount: f32) -> &mut Self {
let mut h = (self.oklch.h + amount) % 360.0;
if h < 0.0 {
h += 360.0;
}
self.oklch.h = h;
self
}
pub fn analogous(&self, results: Option<usize>, slices: Option<usize>) -> Vec<BigColor> {
let results = results.unwrap_or(6);
let slices = slices.unwrap_or(30);
let part = 360.0 / slices as f32;
let mut ret = vec![self.clone()];
let mut h = (self.oklch.h - ((part * results as f32) / 2.0) + 720.0) % 360.0;
for _ in 0..results-1 {
h = (h + part) % 360.0;
let mut new_color = self.clone();
new_color.oklch.h = h;
ret.push(new_color);
}
ret
}
pub fn complement(&self) -> BigColor {
let mut new_color = self.clone();
new_color.oklch.h = (new_color.oklch.h + 180.0) % 360.0;
new_color
}
pub fn monochromatic(&self, results: Option<usize>) -> Vec<BigColor> {
let results = results.unwrap_or(6);
let step = 1.0 / results as f32;
let mut ret = Vec::new();
let mut l: f32 = 0.0;
for _ in 0..results {
let mut new_color = self.clone();
new_color.oklch.l = l.min(1.0);
ret.push(new_color);
l += step;
}
ret
}
pub fn split_complement(&self) -> Vec<BigColor> {
let h = self.oklch.h;
let mut color1 = self.clone();
color1.oklch.h = (h + 72.0) % 360.0;
let mut color2 = self.clone();
color2.oklch.h = (h + 216.0) % 360.0;
vec![self.clone(), color1, color2]
}
pub fn triad(&self) -> Vec<BigColor> {
self.polyad(3)
}
pub fn tetrad(&self) -> Vec<BigColor> {
self.polyad(4)
}
pub fn polyad(&self, number: usize) -> Vec<BigColor> {
if number == 0 {
panic!("Argument to polyad must be a positive number");
}
let mut result = vec![self.clone()];
let step = 360.0 / number as f32;
for i in 1..number {
let mut new_color = self.clone();
new_color.oklch.h = (self.oklch.h + i as f32 * step) % 360.0;
result.push(new_color);
}
result
}
pub fn from_rgb(r: u8, g: u8, b: u8, a: f32) -> Self {
let input = format!("rgba({},{},{},{})", r, g, b, a);
BigColor::new(input)
}
pub fn from_hsl(h: f32, s: f32, l: f32, a: f32) -> Self {
let rgb = hsl_to_rgb(h/360.0, s, l);
BigColor::from_rgb(rgb.r, rgb.g, rgb.b, a)
}
pub fn from_hsv(h: f32, s: f32, v: f32, a: f32) -> Self {
let rgb = hsv_to_rgb(h/360.0, s, v);
BigColor::from_rgb(rgb.r, rgb.g, rgb.b, a)
}
pub fn from_lch(l: f32, c: f32, h: f32, a: f32) -> Self {
let lch = LCH { l, c, h, alpha: a };
let (r, g, b, a) = lch_to_rgb(lch);
BigColor::from_rgb(r, g, b, a)
}
pub fn from_oklch(l: f32, c: f32, h: f32, a: f32) -> Self {
let oklch = OKLCH { l, c, h, alpha: a };
let mut color = BigColor::default();
color.oklch = oklch;
color.ok = true;
color.format = ColorFormat::OKLCH;
color
}
pub fn from_ratio(color: &str) -> Self {
BigColor::new(color)
}
pub fn to_hsb(&self) -> HSV {
self.to_hsv()
}
pub fn to_hsb_string(&self) -> String {
let hsv = self.to_hsv();
let h = hsv.h.round() as i32;
let s = (hsv.s * 100.0).round() as i32;
let b = (hsv.v * 100.0).round() as i32;
if (self.oklch.alpha - 1.0).abs() < f32::EPSILON {
format!("hsb({}, {}%, {}%)", h, s, b)
} else {
format!("hsba({}, {}%, {}%, {})", h, s, b, (self.oklch.alpha * 100.0).round() / 100.0)
}
}
pub fn from_hsb(h: f32, s: f32, b: f32, a: f32) -> Self {
Self::from_hsv(h, s, b, a)
}
pub fn to_cmyk(&self) -> CMYK {
let rgb = self.to_rgb();
rgb_to_cmyk(rgb.r, rgb.g, rgb.b, rgb.a)
}
pub fn to_cmyk_string(&self) -> String {
let cmyk = self.to_cmyk();
format!(
"cmyk({}%, {}%, {}%, {}%)",
cmyk.c.round() as i32,
cmyk.m.round() as i32,
cmyk.y.round() as i32,
cmyk.k.round() as i32
)
}
pub fn from_cmyk(c: f32, m: f32, y: f32, k: f32, a: f32) -> Self {
let (r, g, b, a) = cmyk_to_rgb(CMYK { c, m, y, k, a });
let oklch = rgb_to_oklch(r, g, b, a);
BigColor {
oklch,
original_input: format!("cmyk({}%, {}%, {}%, {}%)", c, m, y, k),
format: ColorFormat::CMYK,
ok: true,
}
}
pub fn to(&self, format: ColorFormat) -> String {
if !self.is_valid() {
return String::from("invalid");
}
match format {
ColorFormat::HEX => self.to_hex_string(false),
ColorFormat::HEX3 => self.to_hex_string(true),
ColorFormat::HEX6 => self.to_hex_string(false),
ColorFormat::HEX8 => self.to_hex8_string(false),
ColorFormat::RGB => self.to_rgb_string(),
ColorFormat::PRGB => self.to_percentage_rgb_string(),
ColorFormat::HSL => self.to_hsl_string(),
ColorFormat::HSV => self.to_hsv_string(),
ColorFormat::HSB => self.to_hsb_string(),
ColorFormat::OKLAB => self.to_oklab_string(),
ColorFormat::OKLCH => self.to_oklch_string(),
ColorFormat::LAB => self.to_lab_string(),
ColorFormat::LCH => self.to_lch_string(),
ColorFormat::CMYK => self.to_cmyk_string(),
ColorFormat::NAME => self.to_name().unwrap_or(&self.original_input).to_string(),
ColorFormat::INVALID => String::from("invalid"),
}
}
pub fn get_contrast_color(&self, intensity: f32) -> BigColor {
get_contrast_color_impl(self, intensity)
}
pub fn get_contrast_ratio(&self, other: &BigColor) -> f32 {
get_contrast_ratio_impl(self, other)
}
}
pub fn random() -> BigColor {
BigColor::from_rgb(128, 43, 128, 1.0)
}
pub fn equals(color1: &BigColor, color2: &BigColor) -> bool {
color1.to_rgb_string() == color2.to_rgb_string()
}
pub fn mix(color1: &BigColor, color2: &BigColor, amount: Option<f32>) -> BigColor {
let amount = amount.unwrap_or(50.0);
let rgb1 = color1.to_rgb();
let rgb2 = color2.to_rgb();
let p = amount / 100.0;
let r = (rgb2.r as f32 - rgb1.r as f32) * p + rgb1.r as f32;
let g = (rgb2.g as f32 - rgb1.g as f32) * p + rgb1.g as f32;
let b = (rgb2.b as f32 - rgb1.b as f32) * p + rgb1.b as f32;
let a = (rgb2.a - rgb1.a) * p + rgb1.a;
BigColor::from_rgb(r as u8, g as u8, b as u8, a)
}
pub fn readability(color1: &BigColor, color2: &BigColor) -> f32 {
let l1 = color1.get_luminance();
let l2 = color2.get_luminance();
let max = l1.max(l2);
let min = l1.min(l2);
(max + 0.05) / (min + 0.05)
}
#[derive(Debug, Clone, Copy)]
pub struct WCAG2Params {
pub level: WCAG2Level,
pub size: WCAG2Size,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WCAG2Level {
AA,
AAA,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WCAG2Size {
Small,
Large,
}
impl Default for WCAG2Params {
fn default() -> Self {
WCAG2Params {
level: WCAG2Level::AA,
size: WCAG2Size::Small,
}
}
}
fn validate_wcag2_params(params: Option<WCAG2Params>) -> WCAG2Params {
params.unwrap_or_default()
}
pub fn is_readable(color1: &BigColor, color2: &BigColor, wcag2: Option<WCAG2Params>) -> bool {
let readability_value = readability(color1, color2);
let wcag2_params = validate_wcag2_params(wcag2);
match (wcag2_params.level, wcag2_params.size) {
(WCAG2Level::AA, WCAG2Size::Small) | (WCAG2Level::AAA, WCAG2Size::Large) => {
readability_value >= 4.5
},
(WCAG2Level::AA, WCAG2Size::Large) => {
readability_value >= 3.0
},
(WCAG2Level::AAA, WCAG2Size::Small) => {
readability_value >= 7.0
},
}
}
#[derive(Debug, Clone, Copy)]
pub struct MostReadableArgs {
pub include_fallback_colors: bool,
pub wcag2: WCAG2Params,
}
impl Default for MostReadableArgs {
fn default() -> Self {
MostReadableArgs {
include_fallback_colors: false,
wcag2: WCAG2Params::default(),
}
}
}
pub fn most_readable(
base_color: &BigColor,
color_list: &[BigColor],
args: Option<MostReadableArgs>,
) -> BigColor {
let args = args.unwrap_or_default();
let mut best_color = None;
let mut best_score = 0.0;
for color in color_list {
let readability_value = readability(base_color, color);
if readability_value > best_score {
best_score = readability_value;
best_color = Some(color.clone());
}
}
if let Some(best) = best_color {
if is_readable(base_color, &best, Some(args.wcag2)) || !args.include_fallback_colors {
best
} else {
let white = BigColor::new("#fff");
let black = BigColor::new("#000");
let mut args_without_fallback = args;
args_without_fallback.include_fallback_colors = false;
most_readable(base_color, &[white, black], Some(args_without_fallback))
}
} else {
BigColor::new("#000")
}
}
impl fmt::Display for BigColor {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_string(None))
}
}