use crate::color::ColorComponents;
use crate::values::normalize;
type Transform = euclid::default::Transform3D<f32>;
type Vector = euclid::default::Vector3D<f32>;
#[inline]
pub fn normalize_hue(hue: f32) -> f32 {
hue - 360. * (hue / 360.).floor()
}
#[inline]
fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) {
let max = red.max(green).max(blue);
let min = red.min(green).min(blue);
let delta = max - min;
let hue = if delta != 0.0 {
60.0 * if max == red {
(green - blue) / delta + if green < blue { 6.0 } else { 0.0 }
} else if max == green {
(blue - red) / delta + 2.0
} else {
(red - green) / delta + 4.0
}
} else {
f32::NAN
};
(hue, min, max)
}
#[inline]
pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents {
fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 {
let hue = normalize_hue(hue);
if hue * 6.0 < 360.0 {
t1 + (t2 - t1) * hue / 60.0
} else if hue * 2.0 < 360.0 {
t2
} else if hue * 3.0 < 720.0 {
t1 + (t2 - t1) * (240.0 - hue) / 60.0
} else {
t1
}
}
let ColorComponents(hue, saturation, lightness) = from.map(normalize);
let saturation = saturation / 100.0;
let lightness = lightness / 100.0;
let t2 = if lightness <= 0.5 {
lightness * (saturation + 1.0)
} else {
lightness + saturation - lightness * saturation
};
let t1 = lightness * 2.0 - t2;
ColorComponents(
hue_to_rgb(t1, t2, hue + 120.0),
hue_to_rgb(t1, t2, hue),
hue_to_rgb(t1, t2, hue - 120.0),
)
}
pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents {
let ColorComponents(red, green, blue) = *from;
let (hue, min, max) = rgb_to_hue_min_max(red, green, blue);
let lightness = (min + max) / 2.0;
let delta = max - min;
let saturation = if delta != 0.0 {
if lightness == 0.0 || lightness == 1.0 {
0.0
} else {
(max - lightness) / lightness.min(1.0 - lightness)
}
} else {
0.0
};
ColorComponents(hue, saturation * 100.0, lightness * 100.0)
}
#[inline]
pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents {
let ColorComponents(hue, whiteness, blackness) = from.map(normalize);
let whiteness = whiteness / 100.0;
let blackness = blackness / 100.0;
if whiteness + blackness >= 1.0 {
let gray = whiteness / (whiteness + blackness);
return ColorComponents(gray, gray, gray);
}
let x = 1.0 - whiteness - blackness;
hsl_to_rgb(&ColorComponents(hue, 100.0, 50.0)).map(|v| v * x + whiteness)
}
#[inline]
pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents {
let ColorComponents(red, green, blue) = *from;
let (hue, min, max) = rgb_to_hue_min_max(red, green, blue);
let whiteness = min;
let blackness = 1.0 - max;
ColorComponents(hue, whiteness * 100.0, blackness * 100.0)
}
#[inline]
pub fn epsilon_for_range(min: f32, max: f32) -> f32 {
(max - min) / 1.0e5
}
#[inline]
pub fn orthogonal_to_polar(from: &ColorComponents, e: f32) -> ColorComponents {
let ColorComponents(lightness, a, b) = *from;
let chroma = (a * a + b * b).sqrt();
let hue = if a.abs() < e && b.abs() < e {
f32::NAN
} else if chroma.abs() < e {
f32::NAN
} else {
normalize_hue(b.atan2(a).to_degrees())
};
ColorComponents(lightness, chroma, hue)
}
#[inline]
pub fn polar_to_orthogonal(from: &ColorComponents) -> ColorComponents {
let ColorComponents(lightness, chroma, hue) = *from;
if hue.is_nan() {
return ColorComponents(lightness, 0.0, 0.0);
}
let hue = hue.to_radians();
let a = chroma * hue.cos();
let b = chroma * hue.sin();
ColorComponents(lightness, a, b)
}
#[inline]
fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents {
let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2));
ColorComponents(result.x, result.y, result.z)
}
fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents {
#[rustfmt::skip]
const MAT: Transform = Transform::new(
1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.0,
0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0.0,
-0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0.0,
0.0, 0.0, 0.0, 1.0,
);
transform(from, &MAT)
}
fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents {
#[rustfmt::skip]
const MAT: Transform = Transform::new(
0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0.0,
-0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0.0,
0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0.0,
0.0, 0.0, 0.0, 1.0,
);
transform(from, &MAT)
}
pub enum WhitePoint {
D50,
D65,
}
impl WhitePoint {
const fn values(&self) -> ColorComponents {
match self {
WhitePoint::D50 => ColorComponents(0.9642956764295677, 1.0, 0.8251046025104602),
WhitePoint::D65 => ColorComponents(0.9504559270516716, 1.0, 1.0890577507598784),
}
}
}
fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) {
match (from, to) {
(WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components),
(WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components),
_ => {},
}
}
pub trait ColorSpaceConversion {
const WHITE_POINT: WhitePoint;
fn to_linear_light(from: &ColorComponents) -> ColorComponents;
fn to_xyz(from: &ColorComponents) -> ColorComponents;
fn from_xyz(from: &ColorComponents) -> ColorComponents;
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents;
}
pub fn to_xyz<From: ColorSpaceConversion>(from: &ColorComponents) -> (ColorComponents, WhitePoint) {
let result = From::to_linear_light(from);
(From::to_xyz(&result), From::WHITE_POINT)
}
pub fn from_xyz<To: ColorSpaceConversion>(
from: &ColorComponents,
white_point: WhitePoint,
) -> ColorComponents {
let mut xyz = from.clone();
convert_white_point(white_point, To::WHITE_POINT, &mut xyz);
let result = To::from_xyz(&xyz);
To::to_gamma_encoded(&result)
}
pub struct Srgb;
impl Srgb {
#[rustfmt::skip]
const TO_XYZ: Transform = Transform::new(
0.4123907992659595, 0.21263900587151036, 0.01933081871559185, 0.0,
0.35758433938387796, 0.7151686787677559, 0.11919477979462599, 0.0,
0.1804807884018343, 0.07219231536073371, 0.9505321522496606, 0.0,
0.0, 0.0, 0.0, 1.0,
);
#[rustfmt::skip]
const FROM_XYZ: Transform = Transform::new(
3.2409699419045213, -0.9692436362808798, 0.05563007969699361, 0.0,
-1.5373831775700935, 1.8759675015077206, -0.20397695888897657, 0.0,
-0.4986107602930033, 0.04155505740717561, 1.0569715142428786, 0.0,
0.0, 0.0, 0.0, 1.0,
);
}
impl ColorSpaceConversion for Srgb {
const WHITE_POINT: WhitePoint = WhitePoint::D65;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone().map(|value| {
let abs = value.abs();
if abs < 0.04045 {
value / 12.92
} else {
value.signum() * ((abs + 0.055) / 1.055).powf(2.4)
}
})
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
transform(from, &Self::TO_XYZ)
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
transform(from, &Self::FROM_XYZ)
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
from.clone().map(|value| {
let abs = value.abs();
if abs > 0.0031308 {
value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055)
} else {
12.92 * value
}
})
}
}
pub struct Hsl;
impl ColorSpaceConversion for Hsl {
const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
Srgb::to_linear_light(&hsl_to_rgb(from))
}
#[inline]
fn to_xyz(from: &ColorComponents) -> ColorComponents {
Srgb::to_xyz(from)
}
#[inline]
fn from_xyz(from: &ColorComponents) -> ColorComponents {
Srgb::from_xyz(from)
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
rgb_to_hsl(&Srgb::to_gamma_encoded(from))
}
}
pub struct Hwb;
impl ColorSpaceConversion for Hwb {
const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
Srgb::to_linear_light(&hwb_to_rgb(from))
}
#[inline]
fn to_xyz(from: &ColorComponents) -> ColorComponents {
Srgb::to_xyz(from)
}
#[inline]
fn from_xyz(from: &ColorComponents) -> ColorComponents {
Srgb::from_xyz(from)
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
rgb_to_hwb(&Srgb::to_gamma_encoded(from))
}
}
pub struct SrgbLinear;
impl ColorSpaceConversion for SrgbLinear {
const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
Srgb::to_xyz(from)
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
Srgb::from_xyz(from)
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
from.clone()
}
}
pub struct DisplayP3;
impl DisplayP3 {
#[rustfmt::skip]
const TO_XYZ: Transform = Transform::new(
0.48657094864821626, 0.22897456406974884, 0.0, 0.0,
0.26566769316909294, 0.6917385218365062, 0.045113381858902575, 0.0,
0.1982172852343625, 0.079286914093745, 1.0439443689009757, 0.0,
0.0, 0.0, 0.0, 1.0,
);
#[rustfmt::skip]
const FROM_XYZ: Transform = Transform::new(
2.4934969119414245, -0.829488969561575, 0.035845830243784335, 0.0,
-0.9313836179191236, 1.7626640603183468, -0.07617238926804171, 0.0,
-0.40271078445071684, 0.02362468584194359, 0.9568845240076873, 0.0,
0.0, 0.0, 0.0, 1.0,
);
}
impl ColorSpaceConversion for DisplayP3 {
const WHITE_POINT: WhitePoint = WhitePoint::D65;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
Srgb::to_linear_light(from)
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
transform(from, &Self::TO_XYZ)
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
transform(from, &Self::FROM_XYZ)
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
Srgb::to_gamma_encoded(from)
}
}
pub struct A98Rgb;
impl A98Rgb {
#[rustfmt::skip]
const TO_XYZ: Transform = Transform::new(
0.5766690429101308, 0.29734497525053616, 0.027031361386412378, 0.0,
0.18555823790654627, 0.627363566255466, 0.07068885253582714, 0.0,
0.18822864623499472, 0.07529145849399789, 0.9913375368376389, 0.0,
0.0, 0.0, 0.0, 1.0,
);
#[rustfmt::skip]
const FROM_XYZ: Transform = Transform::new(
2.041587903810746, -0.9692436362808798, 0.013444280632031024, 0.0,
-0.5650069742788596, 1.8759675015077206, -0.11836239223101824, 0.0,
-0.3447313507783295, 0.04155505740717561, 1.0151749943912054, 0.0,
0.0, 0.0, 0.0, 1.0,
);
}
impl ColorSpaceConversion for A98Rgb {
const WHITE_POINT: WhitePoint = WhitePoint::D65;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone().map(|v| v.signum() * v.abs().powf(2.19921875))
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
transform(from, &Self::TO_XYZ)
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
transform(from, &Self::FROM_XYZ)
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
from.clone()
.map(|v| v.signum() * v.abs().powf(0.4547069271758437))
}
}
pub struct ProphotoRgb;
impl ProphotoRgb {
#[rustfmt::skip]
const TO_XYZ: Transform = Transform::new(
0.7977604896723027, 0.2880711282292934, 0.0, 0.0,
0.13518583717574031, 0.7118432178101014, 0.0, 0.0,
0.0313493495815248, 0.00008565396060525902, 0.8251046025104601, 0.0,
0.0, 0.0, 0.0, 1.0,
);
#[rustfmt::skip]
const FROM_XYZ: Transform = Transform::new(
1.3457989731028281, -0.5446224939028347, 0.0, 0.0,
-0.25558010007997534, 1.5082327413132781, 0.0, 0.0,
-0.05110628506753401, 0.02053603239147973, 1.2119675456389454, 0.0,
0.0, 0.0, 0.0, 1.0,
);
}
impl ColorSpaceConversion for ProphotoRgb {
const WHITE_POINT: WhitePoint = WhitePoint::D50;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone().map(|value| {
const ET2: f32 = 16.0 / 512.0;
let abs = value.abs();
if abs <= ET2 {
value / 16.0
} else {
value.signum() * abs.powf(1.8)
}
})
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
transform(from, &Self::TO_XYZ)
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
transform(from, &Self::FROM_XYZ)
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
const ET: f32 = 1.0 / 512.0;
from.clone().map(|v| {
let abs = v.abs();
if abs >= ET {
v.signum() * abs.powf(1.0 / 1.8)
} else {
16.0 * v
}
})
}
}
pub struct Rec2020;
impl Rec2020 {
const ALPHA: f32 = 1.09929682680944;
const BETA: f32 = 0.018053968510807;
#[rustfmt::skip]
const TO_XYZ: Transform = Transform::new(
0.6369580483012913, 0.26270021201126703, 0.0, 0.0,
0.14461690358620838, 0.677998071518871, 0.028072693049087508, 0.0,
0.16888097516417205, 0.059301716469861945, 1.0609850577107909, 0.0,
0.0, 0.0, 0.0, 1.0,
);
#[rustfmt::skip]
const FROM_XYZ: Transform = Transform::new(
1.7166511879712676, -0.666684351832489, 0.017639857445310915, 0.0,
-0.3556707837763924, 1.616481236634939, -0.042770613257808655, 0.0,
-0.2533662813736598, 0.01576854581391113, 0.942103121235474, 0.0,
0.0, 0.0, 0.0, 1.0,
);
}
impl ColorSpaceConversion for Rec2020 {
const WHITE_POINT: WhitePoint = WhitePoint::D65;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone().map(|value| {
let abs = value.abs();
if abs < Self::BETA * 4.5 {
value / 4.5
} else {
value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45)
}
})
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
transform(from, &Self::TO_XYZ)
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
transform(from, &Self::FROM_XYZ)
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
from.clone().map(|v| {
let abs = v.abs();
if abs > Self::BETA {
v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0))
} else {
4.5 * v
}
})
}
}
pub struct XyzD50;
impl ColorSpaceConversion for XyzD50 {
const WHITE_POINT: WhitePoint = WhitePoint::D50;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
from.clone()
}
}
pub struct XyzD65;
impl ColorSpaceConversion for XyzD65 {
const WHITE_POINT: WhitePoint = WhitePoint::D65;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
from.clone()
}
}
pub struct Lab;
impl Lab {
const KAPPA: f32 = 24389.0 / 27.0;
const EPSILON: f32 = 216.0 / 24389.0;
}
impl ColorSpaceConversion for Lab {
const WHITE_POINT: WhitePoint = WhitePoint::D50;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
let ColorComponents(lightness, a, b) = *from;
let f1 = (lightness + 16.0) / 116.0;
let f0 = f1 + a / 500.0;
let f2 = f1 - b / 200.0;
let f0_cubed = f0 * f0 * f0;
let x = if f0_cubed > Self::EPSILON {
f0_cubed
} else {
(116.0 * f0 - 16.0) / Self::KAPPA
};
let y = if lightness > Self::KAPPA * Self::EPSILON {
let v = (lightness + 16.0) / 116.0;
v * v * v
} else {
lightness / Self::KAPPA
};
let f2_cubed = f2 * f2 * f2;
let z = if f2_cubed > Self::EPSILON {
f2_cubed
} else {
(116.0 * f2 - 16.0) / Self::KAPPA
};
ColorComponents(x, y, z) * Self::WHITE_POINT.values()
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
let adapted = *from / Self::WHITE_POINT.values();
let ColorComponents(f0, f1, f2) = adapted.map(|v| {
if v > Self::EPSILON {
v.cbrt()
} else {
(Self::KAPPA * v + 16.0) / 116.0
}
});
let lightness = 116.0 * f1 - 16.0;
let a = 500.0 * (f0 - f1);
let b = 200.0 * (f1 - f2);
ColorComponents(lightness, a, b)
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
from.clone()
}
}
pub struct Lch;
impl ColorSpaceConversion for Lch {
const WHITE_POINT: WhitePoint = Lab::WHITE_POINT;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
let lab = polar_to_orthogonal(from);
Lab::to_xyz(&lab)
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
let lab = Lab::from_xyz(&from);
orthogonal_to_polar(&lab, epsilon_for_range(0.0, 100.0))
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
from.clone()
}
}
pub struct Oklab;
impl Oklab {
#[rustfmt::skip]
const XYZ_TO_LMS: Transform = Transform::new(
0.8190224432164319, 0.0329836671980271, 0.048177199566046255, 0.0,
0.3619062562801221, 0.9292868468965546, 0.26423952494422764, 0.0,
-0.12887378261216414, 0.03614466816999844, 0.6335478258136937, 0.0,
0.0, 0.0, 0.0, 1.0,
);
#[rustfmt::skip]
const LMS_TO_OKLAB: Transform = Transform::new(
0.2104542553, 1.9779984951, 0.0259040371, 0.0,
0.7936177850, -2.4285922050, 0.7827717662, 0.0,
-0.0040720468, 0.4505937099, -0.8086757660, 0.0,
0.0, 0.0, 0.0, 1.0,
);
#[rustfmt::skip]
const LMS_TO_XYZ: Transform = Transform::new(
1.2268798733741557, -0.04057576262431372, -0.07637294974672142, 0.0,
-0.5578149965554813, 1.1122868293970594, -0.4214933239627914, 0.0,
0.28139105017721583, -0.07171106666151701, 1.5869240244272418, 0.0,
0.0, 0.0, 0.0, 1.0,
);
#[rustfmt::skip]
const OKLAB_TO_LMS: Transform = Transform::new(
0.99999999845051981432, 1.0000000088817607767, 1.0000000546724109177, 0.0,
0.39633779217376785678, -0.1055613423236563494, -0.089484182094965759684, 0.0,
0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399, 0.0,
0.0, 0.0, 0.0, 1.0,
);
}
impl ColorSpaceConversion for Oklab {
const WHITE_POINT: WhitePoint = WhitePoint::D65;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
let lms = transform(&from, &Self::OKLAB_TO_LMS);
let lms = lms.map(|v| v * v * v);
transform(&lms, &Self::LMS_TO_XYZ)
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
let lms = transform(&from, &Self::XYZ_TO_LMS);
let lms = lms.map(|v| v.cbrt());
transform(&lms, &Self::LMS_TO_OKLAB)
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
from.clone()
}
}
pub struct Oklch;
impl ColorSpaceConversion for Oklch {
const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT;
fn to_linear_light(from: &ColorComponents) -> ColorComponents {
from.clone()
}
fn to_xyz(from: &ColorComponents) -> ColorComponents {
let oklab = polar_to_orthogonal(from);
Oklab::to_xyz(&oklab)
}
fn from_xyz(from: &ColorComponents) -> ColorComponents {
let lab = Oklab::from_xyz(&from);
orthogonal_to_polar(&lab, epsilon_for_range(0.0, 1.0))
}
fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
from.clone()
}
}