use core::str::FromStr;
#[cfg(feature = "pyffi")]
use pyo3::prelude::*;
use crate::core::{
clip, convert, delta_e_ok, format, from_24bit, in_gamut, interpolate, is_achromatic, normalize,
parse, prepare_to_interpolate, scale_lightness, to_24bit, to_contrast,
to_contrast_luminance_p3, to_contrast_luminance_srgb, to_eq_coordinates, to_gamut, ColorSpace,
HueInterpolation,
};
use crate::Float;
#[macro_export]
macro_rules! rgb {
($r:expr, $g:expr, $b:expr) => {
$crate::Color::new(
$crate::ColorSpace::Srgb,
[
$r as $crate::Float / 255.0,
$g as $crate::Float / 255.0,
$b as $crate::Float / 255.0,
],
)
};
}
#[cfg_attr(
feature = "pyffi",
pyclass(eq, frozen, hash, sequence, module = "prettypretty.color")
)]
#[derive(Clone)]
pub struct Color {
space: ColorSpace,
coordinates: [Float; 3],
}
#[cfg_attr(feature = "pyffi", pymethods)]
impl Color {
#[cfg(feature = "pyffi")]
#[new]
#[inline]
pub const fn new(space: ColorSpace, coordinates: [Float; 3]) -> Self {
Self { space, coordinates }
}
#[cfg(not(feature = "pyffi"))]
#[inline]
pub const fn new(space: ColorSpace, coordinates: [Float; 3]) -> Self {
Self { space, coordinates }
}
#[cfg(feature = "pyffi")]
#[staticmethod]
pub fn parse(s: &str) -> Result<Color, crate::error::ColorFormatError> {
use core::str::FromStr;
Color::from_str(s)
}
#[cfg(feature = "pyffi")]
#[staticmethod]
pub fn srgb(r: Float, g: Float, b: Float) -> Self {
Self::new(ColorSpace::Srgb, [r, g, b])
}
#[cfg(feature = "pyffi")]
#[staticmethod]
pub fn p3(r: Float, g: Float, b: Float) -> Self {
Self::new(ColorSpace::DisplayP3, [r, g, b])
}
#[cfg(feature = "pyffi")]
#[staticmethod]
pub fn oklab(l: Float, a: Float, b: Float) -> Self {
Self::new(ColorSpace::Oklab, [l, a, b])
}
#[cfg(feature = "pyffi")]
#[staticmethod]
pub fn oklrab(lr: Float, a: Float, b: Float) -> Self {
Self::new(ColorSpace::Oklab, [lr, a, b])
}
#[cfg(feature = "pyffi")]
#[staticmethod]
pub fn oklch(l: Float, c: Float, h: Float) -> Self {
Self::new(ColorSpace::Oklch, [l, c, h])
}
#[cfg(feature = "pyffi")]
#[staticmethod]
pub fn oklrch(lr: Float, c: Float, h: Float) -> Self {
Self::new(ColorSpace::Oklch, [lr, c, h])
}
#[cfg(feature = "pyffi")]
#[staticmethod]
#[inline]
pub fn from_24bit(r: u8, g: u8, b: u8) -> Self {
Self::new(ColorSpace::Srgb, from_24bit(r, g, b))
}
#[cfg(not(feature = "pyffi"))]
#[inline]
pub fn from_24bit(r: u8, g: u8, b: u8) -> Self {
Self::new(ColorSpace::Srgb, from_24bit(r, g, b))
}
#[inline]
pub fn space(&self) -> ColorSpace {
self.space
}
#[cfg(feature = "pyffi")]
pub fn coordinates(&self) -> [Float; 3] {
self.coordinates
}
#[cfg(feature = "pyffi")]
pub fn __len__(&self) -> usize {
3
}
#[cfg(feature = "pyffi")]
pub fn __getitem__(&self, index: isize) -> PyResult<Float> {
match index {
-3..=-1 => Ok(self.coordinates[(3 + index) as usize]),
0..=2 => Ok(self.coordinates[index as usize]),
_ => Err(pyo3::exceptions::PyIndexError::new_err(
"Invalid coordinate index",
)),
}
}
#[inline]
pub fn is_default(&self) -> bool {
self.space == ColorSpace::Xyz && self.coordinates == [0.0, 0.0, 0.0]
}
#[cfg(feature = "pyffi")]
#[classattr]
pub const ACHROMATIC_THRESHOLD: Float = 0.01;
#[cfg(not(feature = "pyffi"))]
pub const ACHROMATIC_THRESHOLD: Float = 0.01;
#[inline]
pub fn is_achromatic(&self) -> bool {
is_achromatic(self.space, &self.coordinates, Color::ACHROMATIC_THRESHOLD)
}
#[cfg(not(feature = "pyffi"))]
pub fn is_achromatic_threshold(&self, threshold: Float) -> Result<bool, Float> {
if threshold.is_sign_negative() {
Err(threshold)
} else {
Ok(is_achromatic(self.space, &self.coordinates, threshold))
}
}
#[cfg(feature = "pyffi")]
pub fn is_achromatic_threshold(&self, threshold: Float) -> PyResult<bool> {
if threshold.is_sign_negative() {
Err(pyo3::exceptions::PyValueError::new_err(format!(
"negative achromatic threshold {}",
threshold
)))
} else {
Ok(is_achromatic(self.space, &self.coordinates, threshold))
}
}
#[inline]
#[must_use = "method returns a new color and does not mutate original value"]
pub fn normalize(&self) -> Self {
Self::new(self.space, normalize(self.space, &self.coordinates))
}
pub fn hue_chroma(&self) -> (Float, Float) {
let [_, c, h] = match self.space {
ColorSpace::Oklch | ColorSpace::Oklrch => self.coordinates,
ColorSpace::Oklrab => self.to(ColorSpace::Oklrch).coordinates,
_ => self.to(ColorSpace::Oklch).coordinates,
};
(h.to_radians(), c)
}
pub fn xy_chromaticity(&self) -> (Float, Float) {
let [x, y, z] = self.to(ColorSpace::Xyz).coordinates;
let sum = x + y + z;
(x / sum, y / sum)
}
pub fn uv_prime_chromaticity(&self) -> (Float, Float) {
const MINUS_TWO: Float = -2.0;
let (x, y) = self.xy_chromaticity();
(
4.0 * x / (MINUS_TWO.mul_add(x, 12.0 * y) + 3.0),
9.0 * y / (MINUS_TWO.mul_add(x, 12.0 * y) + 3.0),
)
}
#[inline]
#[must_use = "method returns a new color and does not mutate original value"]
pub fn to(&self, target: ColorSpace) -> Self {
Self::new(target, convert(self.space, target, &self.coordinates))
}
#[inline]
pub fn in_gamut(&self) -> bool {
in_gamut(self.space, &self.coordinates)
}
#[inline]
#[must_use = "method returns a new color and does not mutate original value"]
pub fn clip(&self) -> Self {
Self::new(self.space, clip(self.space, &self.coordinates))
}
#[inline]
#[must_use = "method returns a new color and does not mutate original value"]
pub fn to_gamut(&self) -> Self {
Self::new(self.space, to_gamut(self.space, &self.coordinates))
}
#[inline]
pub fn distance(&self, other: &Self, version: OkVersion) -> f64 {
delta_e_ok(
&self.to(version.cartesian_space()).coordinates,
&other.to(version.cartesian_space()).coordinates,
)
}
#[inline]
#[must_use = "method returns interpolator and does not mutate original values"]
pub fn interpolate(
&self,
color: &Self,
interpolation_space: ColorSpace,
interpolation_strategy: HueInterpolation,
) -> Interpolator {
Interpolator::new(self, color, interpolation_space, interpolation_strategy)
}
#[inline]
#[must_use = "method returns a new color and does not mutate original value"]
pub fn lighten(&self, factor: Float) -> Self {
Self::new(
ColorSpace::Oklrch,
scale_lightness(self.space, &self.coordinates, factor),
)
}
#[inline]
#[must_use = "method returns a new color and does not mutate original value"]
pub fn darken(&self, factor: Float) -> Self {
Self::new(
ColorSpace::Oklrch,
scale_lightness(self.space, &self.coordinates, factor.recip()),
)
}
pub fn contrast_against(&self, background: &Self) -> f64 {
let fg = self.to(ColorSpace::Srgb);
let bg = background.to(ColorSpace::Srgb);
if fg.in_gamut() && bg.in_gamut() {
return to_contrast(
to_contrast_luminance_srgb(&fg.coordinates),
to_contrast_luminance_srgb(&bg.coordinates),
);
};
let fg = self.to(ColorSpace::DisplayP3);
let bg = background.to(ColorSpace::DisplayP3);
to_contrast(
to_contrast_luminance_p3(&fg.coordinates),
to_contrast_luminance_p3(&bg.coordinates),
)
}
pub fn use_black_text(&self) -> bool {
let background = self.to(ColorSpace::Srgb);
let luminance = if background.in_gamut() {
to_contrast_luminance_srgb(&background.coordinates)
} else {
to_contrast_luminance_p3(&self.to(ColorSpace::DisplayP3).coordinates)
};
-to_contrast(1.0, luminance) <= to_contrast(0.0, luminance)
}
pub fn use_black_background(&self) -> bool {
let text = self.to(ColorSpace::Srgb);
let luminance = if text.in_gamut() {
to_contrast_luminance_srgb(&text.coordinates)
} else {
to_contrast_luminance_p3(&self.to(ColorSpace::DisplayP3).coordinates)
};
to_contrast(luminance, 0.0) <= -to_contrast(luminance, 1.0)
}
pub fn to_24bit(&self) -> [u8; 3] {
to_24bit(
ColorSpace::Srgb,
self.to(ColorSpace::Srgb).to_gamut().as_ref(),
)
}
#[inline]
pub fn to_hex_format(&self) -> String {
let [r, g, b] = self.to_24bit();
format!("#{:02x}{:02x}{:02x}", r, g, b)
}
#[cfg(feature = "pyffi")]
pub fn __repr__(&self) -> String {
format!("{:?}", self)
}
#[cfg(feature = "pyffi")]
pub fn __str__(&self) -> String {
format!("{}", self)
}
}
#[cfg(not(feature = "pyffi"))]
impl Color {
pub fn srgb<I, J, K>(r: I, g: J, b: K) -> Self
where
I: Into<Float>,
J: Into<Float>,
K: Into<Float>,
{
Self::new(ColorSpace::Srgb, [r.into(), g.into(), b.into()])
}
pub fn p3<I, J, K>(r: I, g: J, b: K) -> Self
where
I: Into<Float>,
J: Into<Float>,
K: Into<Float>,
{
Self::new(ColorSpace::DisplayP3, [r.into(), g.into(), b.into()])
}
pub fn oklab<I, J, K>(l: I, a: J, b: K) -> Self
where
I: Into<Float>,
J: Into<Float>,
K: Into<Float>,
{
Self::new(ColorSpace::Oklab, [l.into(), a.into(), b.into()])
}
pub fn oklrab<I, J, K>(lr: I, a: J, b: K) -> Self
where
I: Into<Float>,
J: Into<Float>,
K: Into<Float>,
{
Self::new(ColorSpace::Oklrab, [lr.into(), a.into(), b.into()])
}
pub fn oklch<I, J, K>(l: I, c: J, h: K) -> Self
where
I: Into<Float>,
J: Into<Float>,
K: Into<Float>,
{
Self::new(ColorSpace::Oklch, [l.into(), c.into(), h.into()])
}
pub fn oklrch<I, J, K>(lr: I, c: J, h: K) -> Self
where
I: Into<Float>,
J: Into<Float>,
K: Into<Float>,
{
Self::new(ColorSpace::Oklrch, [lr.into(), c.into(), h.into()])
}
}
impl Color {
pub fn find_closest_ok<'c, C>(&self, candidates: C, version: OkVersion) -> Option<usize>
where
C: IntoIterator<Item = &'c Self>,
{
self.find_closest(candidates, version.cartesian_space(), delta_e_ok)
}
pub fn find_closest<'c, C, F>(
&self,
candidates: C,
space: ColorSpace,
mut compute_distance: F,
) -> Option<usize>
where
C: IntoIterator<Item = &'c Color>,
F: FnMut(&[f64; 3], &[f64; 3]) -> f64,
{
let origin = self.to(space);
let mut min_distance = f64::INFINITY;
let mut min_index = None;
for (index, candidate) in candidates.into_iter().enumerate() {
let candidate = candidate.to(space);
let distance = compute_distance(&origin.coordinates, &candidate.coordinates);
if distance < min_distance {
min_distance = distance;
min_index = Some(index);
}
}
min_index
}
}
impl Default for Color {
#[inline]
fn default() -> Self {
Self::new(ColorSpace::Xyz, [0.0, 0.0, 0.0])
}
}
impl core::str::FromStr for Color {
type Err = crate::error::ColorFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
parse(s).map(|(space, coordinates)| Self::new(space, coordinates))
}
}
impl TryFrom<&str> for Color {
type Error = crate::error::ColorFormatError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Color::from_str(value)
}
}
impl TryFrom<String> for Color {
type Error = crate::error::ColorFormatError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Color::from_str(value.as_str())
}
}
impl AsRef<[Float; 3]> for Color {
fn as_ref(&self) -> &[Float; 3] {
&self.coordinates
}
}
impl core::ops::Index<usize> for Color {
type Output = f64;
#[inline]
fn index(&self, index: usize) -> &Self::Output {
&self.coordinates[index]
}
}
impl core::hash::Hash for Color {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.space.hash(state);
let [n1, n2, n3] = to_eq_coordinates(self.space, &self.coordinates);
n1.hash(state);
n2.hash(state);
n3.hash(state);
}
}
impl PartialEq for Color {
fn eq(&self, other: &Self) -> bool {
if self.space != other.space {
return false;
} else if self.coordinates == other.coordinates {
return true;
}
let n1 = to_eq_coordinates(self.space, &self.coordinates);
let n2 = to_eq_coordinates(other.space, &other.coordinates);
n1 == n2
}
}
impl Eq for Color {}
impl core::fmt::Debug for Color {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let [c1, c2, c3] = self.coordinates;
f.write_fmt(format_args!(
"Color({:?}, [{}, {}, {}])",
self.space, c1, c2, c3
))
}
}
impl core::fmt::Display for Color {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
format(self.space, &self.coordinates, f)
}
}
#[cfg_attr(
feature = "pyffi",
pyclass(eq, eq_int, frozen, hash, module = "prettypretty.color")
)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum OkVersion {
Original,
Revised,
}
#[cfg_attr(feature = "pyffi", pymethods)]
impl OkVersion {
pub const fn cartesian_space(&self) -> ColorSpace {
match *self {
Self::Original => ColorSpace::Oklab,
Self::Revised => ColorSpace::Oklrab,
}
}
pub const fn polar_space(&self) -> ColorSpace {
match *self {
Self::Original => ColorSpace::Oklch,
Self::Revised => ColorSpace::Oklrch,
}
}
}
#[cfg_attr(feature = "pyffi", pyclass(module = "prettypretty.color"))]
#[derive(Clone, Debug)]
pub struct Interpolator {
space: ColorSpace,
coordinates1: [Float; 3],
coordinates2: [Float; 3],
}
#[cfg_attr(feature = "pyffi", pymethods)]
impl Interpolator {
#[cfg(feature = "pyffi")]
#[new]
pub fn new(
color1: &Color,
color2: &Color,
space: ColorSpace,
strategy: HueInterpolation,
) -> Self {
let (coordinates1, coordinates2) = prepare_to_interpolate(
color1.space,
&color1.coordinates,
color2.space,
&color2.coordinates,
space,
strategy,
);
Self {
space,
coordinates1,
coordinates2,
}
}
#[cfg(not(feature = "pyffi"))]
#[inline]
pub fn new(
color1: &Color,
color2: &Color,
space: ColorSpace,
strategy: HueInterpolation,
) -> Self {
let (coordinates1, coordinates2) = prepare_to_interpolate(
color1.space,
&color1.coordinates,
color2.space,
&color2.coordinates,
space,
strategy,
);
Self {
space,
coordinates1,
coordinates2,
}
}
#[inline]
pub fn at(&self, fraction: f64) -> Color {
let [c1, c2, c3] = interpolate(fraction, &self.coordinates1, &self.coordinates2);
Color::new(self.space, [c1, c2, c3])
}
#[cfg(feature = "pyffi")]
pub fn __repr__(&self) -> String {
format!(
"Interpolator({:?}, {:?}, {:?})",
self.space, self.coordinates1, self.coordinates2
)
}
}