use crate::difference::difference_euclidean;
use crate::gamut::clamp::clamp_gamut;
use crate::gamut::in_gamut::{color_to_rgb, in_gamut};
use crate::spaces::{Oklab, Oklch, Rgb};
use crate::Color;
pub fn to_gamut(color: Color, mode: &str) -> Color {
let dest_has_gamut = matches!(
mode,
"rgb" | "hsl" | "hsv" | "hwb" | "p3" | "rec2020" | "a98" | "prophoto"
);
if !dest_has_gamut {
return convert_to_mode(color, mode);
}
let mut candidate = to_oklch(color);
if candidate.l.is_nan() {
candidate.l = 0.0;
}
if candidate.c.is_nan() {
candidate.c = 0.0;
}
if candidate.l >= 1.0 {
return dest_white(mode, candidate.alpha);
}
if candidate.l <= 0.0 {
return dest_black(mode, candidate.alpha);
}
let candidate_color = Color::Oklch(candidate);
if in_gamut(&candidate_color, mode) {
return convert_to_mode(candidate_color, mode);
}
const EPSILON: f64 = 0.4 / 4000.0;
const JND: f64 = 0.02;
let mut start = 0.0;
let mut end = candidate.c;
let mut last_clipped: Oklch = unwrap_oklch(clamp_gamut(Color::Oklch(candidate), mode));
let de = difference_euclidean("oklch");
while end - start > EPSILON {
candidate.c = (start + end) * 0.5;
let working = Color::Oklch(candidate);
let clipped = unwrap_oklch(clamp_gamut(working, mode));
let in_g = in_gamut(&working, mode);
let delta = de(&working, &Color::Oklch(clipped));
if in_g || delta <= JND {
start = candidate.c;
} else {
end = candidate.c;
}
last_clipped = clipped;
}
let final_candidate = Color::Oklch(candidate);
if in_gamut(&final_candidate, mode) {
convert_to_mode(final_candidate, mode)
} else {
convert_to_mode(Color::Oklch(last_clipped), mode)
}
}
fn unwrap_oklch(c: Color) -> Oklch {
match c {
Color::Oklch(x) => x,
other => panic!("internal: expected Oklch, got {other:?}"),
}
}
fn dest_white(mode: &str, alpha: Option<f64>) -> Color {
let white = Color::Rgb(Rgb {
r: 1.0,
g: 1.0,
b: 1.0,
alpha,
});
convert_to_mode(white, mode)
}
fn dest_black(mode: &str, alpha: Option<f64>) -> Color {
let black = Color::Rgb(Rgb {
r: 0.0,
g: 0.0,
b: 0.0,
alpha,
});
convert_to_mode(black, mode)
}
fn to_oklch(color: Color) -> Oklch {
use crate::traits::ColorSpace;
match color {
Color::Oklch(x) => x,
Color::Oklab(x) => x.into(),
Color::Rgb(x) => x.into(),
Color::LinearRgb(x) => Oklab::from(x).into(),
other => Oklch::from(Oklab::from_xyz65(crate::gamut::clamp::to_xyz65(other))),
}
}
fn convert_to_mode(color: Color, mode: &str) -> Color {
use crate::spaces::{
Hsl, Hsv, Hwb, Lab, Lch, LinearRgb, ProphotoRgb, Rec2020, Xyz50, Xyz65, A98, P3,
};
let rgb = color_to_rgb(color);
match mode {
"rgb" => Color::Rgb(rgb),
"lrgb" => Color::LinearRgb(LinearRgb::from(rgb)),
"hsl" => Color::Hsl(Hsl::from(rgb)),
"hsv" => Color::Hsv(Hsv::from(rgb)),
"hwb" => Color::Hwb(Hwb::from(Hsv::from(rgb))),
"lab" => Color::Lab(Lab::from(rgb)),
"lch" => Color::Lch(Lch::from(rgb)),
"oklab" => Color::Oklab(Oklab::from(rgb)),
"oklch" => Color::Oklch(Oklch::from(rgb)),
"xyz50" => Color::Xyz50(crate::convert::<Rgb, Xyz50>(rgb)),
"xyz65" => Color::Xyz65(crate::convert::<Rgb, Xyz65>(rgb)),
"p3" => Color::P3(crate::convert::<Rgb, P3>(rgb)),
"rec2020" => Color::Rec2020(crate::convert::<Rgb, Rec2020>(rgb)),
"a98" => Color::A98(crate::convert::<Rgb, A98>(rgb)),
"prophoto" => Color::ProphotoRgb(crate::convert::<Rgb, ProphotoRgb>(rgb)),
other => panic!("to_gamut: unknown mode '{other}'"),
}
}