use crate::*;
pub trait Gamut: Sized {
fn in_gamut(&self) -> bool;
fn clamp_to_gamut(&self) -> Self;
fn map_to_gamut(self) -> Self;
}
fn raytrace_to_linear_rgb(oklch: Oklch) -> LinearRgb {
let alpha = oklch.alpha;
if oklch.lightness >= 1.0 {
return LinearRgb::new(1.0, 1.0, 1.0, alpha);
}
if oklch.lightness <= 0.0 {
return LinearRgb::new(0.0, 0.0, 0.0, alpha);
}
let l_origin = oklch.lightness;
let h_origin = oklch.hue;
let anchor_oklch = Oklch::new(l_origin, 0.0, h_origin, alpha);
let anchor_rgb = LinearRgb::from(Oklab::from(anchor_oklch));
let mut anchor = [anchor_rgb.red, anchor_rgb.green, anchor_rgb.blue];
let origin_rgb = LinearRgb::from(Oklab::from(oklch));
let mut origin_rgb = [origin_rgb.red, origin_rgb.green, origin_rgb.blue];
let low = 1e-6;
let high = 1.0 - low;
let mut last = origin_rgb;
for i in 0..4 {
if i > 0 {
let rgb = LinearRgb::new(origin_rgb[0], origin_rgb[1], origin_rgb[2], alpha);
let mut current_oklch = Oklch::from(Oklab::from(XyzD65::from(rgb)));
current_oklch.lightness = l_origin;
current_oklch.hue = h_origin;
let rgb = LinearRgb::from(XyzD65::from(Oklab::from(current_oklch)));
origin_rgb = [rgb.red, rgb.green, rgb.blue];
}
let intersection = raytrace_box(&anchor, &origin_rgb);
match intersection {
None => {
origin_rgb = last;
break;
}
Some(hit) => {
if i > 0 && origin_rgb.iter().all(|&x| low < x && x < high) {
anchor = origin_rgb;
}
origin_rgb = hit;
last = hit;
}
}
}
LinearRgb::new(origin_rgb[0].clamp(0.0, 1.0), origin_rgb[1].clamp(0.0, 1.0), origin_rgb[2].clamp(0.0, 1.0), alpha)
}
macro_rules! impl_map_to_gamut_raytrace {
($ty:ident, $to_oklch:expr, $from_linear:expr) => {
impl $ty {
fn raytrace_map_to_gamut(self) -> Self {
if self.in_gamut() {
return self;
}
let oklch = $to_oklch(self);
$from_linear(raytrace_to_linear_rgb(oklch))
}
}
};
}
impl_map_to_gamut_raytrace!(LinearRgb, |c: LinearRgb| Oklch::from(Oklab::from(XyzD65::from(c))), |rgb: LinearRgb| rgb
.clamp_to_gamut());
impl_map_to_gamut_raytrace!(DisplayP3, |c: DisplayP3| Oklch::from(Oklab::from(XyzD65::from(c))), |rgb: LinearRgb| {
DisplayP3::from(XyzD65::from(rgb)).clamp_to_gamut()
});
impl_map_to_gamut_raytrace!(
A98Rgb,
|c: A98Rgb| Oklch::from(Oklab::from(XyzD65::from(LinearRgb::from(c)))),
|rgb: LinearRgb| A98Rgb::from(rgb).clamp_to_gamut()
);
impl_map_to_gamut_raytrace!(
ProphotoRgb,
|c: ProphotoRgb| Oklch::from(Oklab::from(XyzD65::from(XyzD50::from(c)))),
|rgb: LinearRgb| ProphotoRgb::from(XyzD50::from(XyzD65::from(rgb))).clamp_to_gamut()
);
impl_map_to_gamut_raytrace!(Rec2020, |c: Rec2020| Oklch::from(Oklab::from(XyzD65::from(c))), |rgb: LinearRgb| {
Rec2020::from(XyzD65::from(rgb)).clamp_to_gamut()
});
fn raytrace_box(start: &[f64; 3], end: &[f64; 3]) -> Option<[f64; 3]> {
let mut tfar = f64::INFINITY;
let mut tnear = f64::NEG_INFINITY;
let mut direction = [0.0_f64; 3];
for i in 0..3 {
let a = start[i]; let b = end[i]; let d = b - a; direction[i] = d;
if d.abs() > 1e-12 {
let inv_d = 1.0 / d; let t1 = (0.0 - a) * inv_d; let t2 = (1.0 - a) * inv_d; tnear = tnear.max(t1.min(t2)); tfar = tfar.min(t1.max(t2)); }
else if !(0.0..=1.0).contains(&a) {
return None;
}
}
if tnear > tfar || tfar < 0.0 {
return None;
}
if tnear < 0.0 {
tnear = tfar;
}
if !tnear.is_finite() {
return None;
}
Some([start[0] + direction[0] * tnear, start[1] + direction[1] * tnear, start[2] + direction[2] * tnear])
}
const GAMUT_EPSILON: f64 = 1e-6;
fn in_unit(v: f64) -> bool {
(-GAMUT_EPSILON..=1.0 + GAMUT_EPSILON).contains(&v)
}
fn in_percent(v: f32) -> bool {
(0.0..=100.0).contains(&v)
}
macro_rules! impl_gamut_rgb_f64 {
($ty:ident) => {
impl Gamut for $ty {
fn in_gamut(&self) -> bool {
in_unit(self.red) && in_unit(self.green) && in_unit(self.blue)
}
fn clamp_to_gamut(&self) -> Self {
Self::new(self.red.clamp(0.0, 1.0), self.green.clamp(0.0, 1.0), self.blue.clamp(0.0, 1.0), self.alpha)
}
fn map_to_gamut(self) -> Self {
self.raytrace_map_to_gamut()
}
}
};
}
impl_gamut_rgb_f64!(LinearRgb);
impl_gamut_rgb_f64!(A98Rgb);
impl_gamut_rgb_f64!(DisplayP3);
impl_gamut_rgb_f64!(ProphotoRgb);
impl_gamut_rgb_f64!(Rec2020);
impl Gamut for Srgb {
fn in_gamut(&self) -> bool {
true
}
fn clamp_to_gamut(&self) -> Self {
*self
}
fn map_to_gamut(self) -> Self {
self.clamp_to_gamut()
}
}
impl Gamut for Hex {
fn in_gamut(&self) -> bool {
true
}
fn clamp_to_gamut(&self) -> Self {
*self
}
fn map_to_gamut(self) -> Self {
self.clamp_to_gamut()
}
}
impl Gamut for Lab {
fn in_gamut(&self) -> bool {
(0.0..=100.0).contains(&self.lightness)
&& (-125.0..=125.0).contains(&self.a)
&& (-125.0..=125.0).contains(&self.b)
}
fn clamp_to_gamut(&self) -> Self {
Self::new(
self.lightness.clamp(0.0, 100.0),
self.a.clamp(-125.0, 125.0),
self.b.clamp(-125.0, 125.0),
self.alpha,
)
}
fn map_to_gamut(self) -> Self {
let rgb = LinearRgb::from(XyzD65::from(XyzD50::from(self)));
if rgb.in_gamut() {
return self;
}
let oklch = Oklch::from(Oklab::from(XyzD65::from(XyzD50::from(self))));
let mapped_rgb = raytrace_to_linear_rgb(oklch);
Lab::from(XyzD50::from(XyzD65::from(mapped_rgb))).clamp_to_gamut()
}
}
impl Gamut for Oklab {
fn in_gamut(&self) -> bool {
(0.0..=1.0).contains(&self.lightness) && (-0.4..=0.4).contains(&self.a) && (-0.4..=0.4).contains(&self.b)
}
fn clamp_to_gamut(&self) -> Self {
Self::new(self.lightness.clamp(0.0, 1.0), self.a.clamp(-0.4, 0.4), self.b.clamp(-0.4, 0.4), self.alpha)
}
fn map_to_gamut(self) -> Self {
let rgb = LinearRgb::from(XyzD65::from(self));
if rgb.in_gamut() {
return self;
}
let oklch = Oklch::from(self);
let mapped_rgb = raytrace_to_linear_rgb(oklch);
Oklab::from(XyzD65::from(mapped_rgb)).clamp_to_gamut()
}
}
impl Gamut for Lch {
fn in_gamut(&self) -> bool {
(0.0..=100.0).contains(&self.lightness) && (0.0..=150.0).contains(&self.chroma)
}
fn clamp_to_gamut(&self) -> Self {
Self::new(self.lightness.clamp(0.0, 100.0), self.chroma.clamp(0.0, 150.0), self.hue, self.alpha)
}
fn map_to_gamut(self) -> Self {
let rgb = LinearRgb::from(XyzD65::from(XyzD50::from(Lab::from(self))));
if rgb.in_gamut() {
return self;
}
let oklch = Oklch::from(Oklab::from(XyzD65::from(XyzD50::from(Lab::from(self)))));
let mapped_rgb = raytrace_to_linear_rgb(oklch);
Lch::from(Lab::from(XyzD50::from(XyzD65::from(mapped_rgb)))).clamp_to_gamut()
}
}
impl Gamut for Oklch {
fn in_gamut(&self) -> bool {
(0.0..=1.0).contains(&self.lightness) && (0.0..=0.4).contains(&self.chroma)
}
fn clamp_to_gamut(&self) -> Self {
Self::new(self.lightness.clamp(0.0, 1.0), self.chroma.clamp(0.0, 0.4), self.hue, self.alpha)
}
fn map_to_gamut(self) -> Self {
let rgb = LinearRgb::from(XyzD65::from(Oklab::from(self)));
if rgb.in_gamut() {
return self;
}
let mapped_rgb = raytrace_to_linear_rgb(self);
Oklch::from(Oklab::from(XyzD65::from(mapped_rgb))).clamp_to_gamut()
}
}
impl Gamut for Hsl {
fn in_gamut(&self) -> bool {
in_percent(self.saturation) && in_percent(self.lightness)
}
fn clamp_to_gamut(&self) -> Self {
Self::new(self.hue, self.saturation.clamp(0.0, 100.0), self.lightness.clamp(0.0, 100.0), self.alpha)
}
fn map_to_gamut(self) -> Self {
let rgb = LinearRgb::from(self);
if rgb.in_gamut() {
return self;
}
let oklch = Oklch::from(Oklab::from(XyzD65::from(rgb)));
let mapped_rgb = raytrace_to_linear_rgb(oklch);
Hsl::from(mapped_rgb).clamp_to_gamut()
}
}
impl Gamut for Hwb {
fn in_gamut(&self) -> bool {
in_percent(self.whiteness) && in_percent(self.blackness)
}
fn clamp_to_gamut(&self) -> Self {
Self::new(self.hue, self.whiteness.clamp(0.0, 100.0), self.blackness.clamp(0.0, 100.0), self.alpha)
}
fn map_to_gamut(self) -> Self {
let rgb = LinearRgb::from(self);
if rgb.in_gamut() {
return self;
}
let oklch = Oklch::from(Oklab::from(XyzD65::from(rgb)));
let mapped_rgb = raytrace_to_linear_rgb(oklch);
Hwb::from(mapped_rgb).clamp_to_gamut()
}
}
impl Gamut for Hsv {
fn in_gamut(&self) -> bool {
true
}
fn clamp_to_gamut(&self) -> Self {
*self
}
fn map_to_gamut(self) -> Self {
self.clamp_to_gamut()
}
}
impl Gamut for XyzD50 {
fn in_gamut(&self) -> bool {
self.x >= 0.0 && self.y >= 0.0 && self.z >= 0.0 && self.y <= 100.0
}
fn clamp_to_gamut(&self) -> Self {
Self::new(self.x.max(0.0), self.y.clamp(0.0, 100.0), self.z.max(0.0), self.alpha)
}
fn map_to_gamut(self) -> Self {
let rgb = LinearRgb::from(XyzD65::from(self));
if rgb.in_gamut() {
return self;
}
let oklch = Oklch::from(Oklab::from(XyzD65::from(self)));
let mapped_rgb = raytrace_to_linear_rgb(oklch);
XyzD50::from(XyzD65::from(mapped_rgb)).clamp_to_gamut()
}
}
impl Gamut for XyzD65 {
fn in_gamut(&self) -> bool {
self.x >= 0.0 && self.y >= 0.0 && self.z >= 0.0 && self.y <= 100.0
}
fn clamp_to_gamut(&self) -> Self {
Self::new(self.x.max(0.0), self.y.clamp(0.0, 100.0), self.z.max(0.0), self.alpha)
}
fn map_to_gamut(self) -> Self {
let rgb = LinearRgb::from(self);
if rgb.in_gamut() {
return self;
}
let oklch = Oklch::from(Oklab::from(self));
let mapped_rgb = raytrace_to_linear_rgb(oklch);
XyzD65::from(mapped_rgb).clamp_to_gamut()
}
}
impl Color {
pub fn color_space(&self) -> Option<ColorSpace> {
match self {
Color::Srgb(_)
| Color::Hex(_)
| Color::Named(_)
| Color::Hsl(_)
| Color::Hwb(_)
| Color::Hsv(_)
| Color::LinearRgb(_) => Some(ColorSpace::Srgb),
Color::DisplayP3(_) => Some(ColorSpace::DisplayP3),
Color::A98Rgb(_) => Some(ColorSpace::A98Rgb),
Color::ProphotoRgb(_) => Some(ColorSpace::ProphotoRgb),
Color::Rec2020(_) => Some(ColorSpace::Rec2020),
Color::Lab(_) | Color::Lch(_) | Color::Oklab(_) | Color::Oklch(_) | Color::XyzD50(_) | Color::XyzD65(_) => {
None
}
}
}
pub fn in_gamut_of(&self, space: ColorSpace) -> bool {
if let Some(src) = self.color_space()
&& space.contains(src)
&& self.in_gamut()
{
return true;
}
match space {
ColorSpace::Srgb => LinearRgb::from(XyzD65::from(*self)).in_gamut(),
ColorSpace::DisplayP3 => DisplayP3::from(XyzD65::from(*self)).in_gamut(),
ColorSpace::A98Rgb => A98Rgb::from(XyzD65::from(*self)).in_gamut(),
ColorSpace::ProphotoRgb => ProphotoRgb::from(XyzD65::from(*self)).in_gamut(),
ColorSpace::Rec2020 => Rec2020::from(XyzD65::from(*self)).in_gamut(),
}
}
}
impl Gamut for Color {
fn in_gamut(&self) -> bool {
match self {
Color::A98Rgb(c) => c.in_gamut(),
Color::DisplayP3(c) => c.in_gamut(),
Color::Hex(c) => c.in_gamut(),
Color::Hsv(c) => c.in_gamut(),
Color::Hsl(c) => c.in_gamut(),
Color::Hwb(c) => c.in_gamut(),
Color::Lab(c) => c.in_gamut(),
Color::Lch(c) => c.in_gamut(),
Color::LinearRgb(c) => c.in_gamut(),
Color::Named(_) => true,
Color::Oklab(c) => c.in_gamut(),
Color::Oklch(c) => c.in_gamut(),
Color::ProphotoRgb(c) => c.in_gamut(),
Color::Rec2020(c) => c.in_gamut(),
Color::Srgb(c) => c.in_gamut(),
Color::XyzD50(c) => c.in_gamut(),
Color::XyzD65(c) => c.in_gamut(),
}
}
fn clamp_to_gamut(&self) -> Self {
match self {
Color::A98Rgb(c) => Color::A98Rgb(c.clamp_to_gamut()),
Color::DisplayP3(c) => Color::DisplayP3(c.clamp_to_gamut()),
Color::Hex(c) => Color::Hex(c.clamp_to_gamut()),
Color::Hsv(c) => Color::Hsv(c.clamp_to_gamut()),
Color::Hsl(c) => Color::Hsl(c.clamp_to_gamut()),
Color::Hwb(c) => Color::Hwb(c.clamp_to_gamut()),
Color::Lab(c) => Color::Lab(c.clamp_to_gamut()),
Color::Lch(c) => Color::Lch(c.clamp_to_gamut()),
Color::LinearRgb(c) => Color::LinearRgb(c.clamp_to_gamut()),
Color::Named(n) => Color::Named(*n),
Color::Oklab(c) => Color::Oklab(c.clamp_to_gamut()),
Color::Oklch(c) => Color::Oklch(c.clamp_to_gamut()),
Color::ProphotoRgb(c) => Color::ProphotoRgb(c.clamp_to_gamut()),
Color::Rec2020(c) => Color::Rec2020(c.clamp_to_gamut()),
Color::Srgb(c) => Color::Srgb(c.clamp_to_gamut()),
Color::XyzD50(c) => Color::XyzD50(c.clamp_to_gamut()),
Color::XyzD65(c) => Color::XyzD65(c.clamp_to_gamut()),
}
}
fn map_to_gamut(self) -> Self {
match self {
Color::A98Rgb(c) => Color::A98Rgb(c.map_to_gamut()),
Color::DisplayP3(c) => Color::DisplayP3(c.map_to_gamut()),
Color::Hex(c) => Color::Hex(c.map_to_gamut()),
Color::Hsv(c) => Color::Hsv(c.map_to_gamut()),
Color::Hsl(c) => Color::Hsl(c.map_to_gamut()),
Color::Hwb(c) => Color::Hwb(c.map_to_gamut()),
Color::Lab(c) => Color::Lab(c.map_to_gamut()),
Color::Lch(c) => Color::Lch(c.map_to_gamut()),
Color::LinearRgb(c) => Color::LinearRgb(c.map_to_gamut()),
Color::Named(n) => Color::Named(n),
Color::Oklab(c) => Color::Oklab(c.map_to_gamut()),
Color::Oklch(c) => Color::Oklch(c.map_to_gamut()),
Color::ProphotoRgb(c) => Color::ProphotoRgb(c.map_to_gamut()),
Color::Rec2020(c) => Color::Rec2020(c.map_to_gamut()),
Color::Srgb(c) => Color::Srgb(c.map_to_gamut()),
Color::XyzD50(c) => Color::XyzD50(c.map_to_gamut()),
Color::XyzD65(c) => Color::XyzD65(c.map_to_gamut()),
}
}
}