use super::*;
pub(super) fn z(v: Option<f64>) -> f64 {
v.unwrap_or(0.0)
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub(super) enum ChannelCategory {
Red,
Green,
Blue,
Lightness,
Colorfulness, Hue,
LabA,
LabB,
Whiteness,
Blackness,
}
pub(super) fn channel_category(space: ColorSpace, idx: usize) -> Option<ChannelCategory> {
use ChannelCategory::*;
use ColorSpace::*;
Some(match (space, idx) {
(Rgb, 0)
| (Srgb, 0)
| (SrgbLinear, 0)
| (DisplayP3, 0)
| (DisplayP3Linear, 0)
| (A98Rgb, 0)
| (ProphotoRgb, 0)
| (Rec2020, 0) => Red,
(Rgb, 1)
| (Srgb, 1)
| (SrgbLinear, 1)
| (DisplayP3, 1)
| (DisplayP3Linear, 1)
| (A98Rgb, 1)
| (ProphotoRgb, 1)
| (Rec2020, 1) => Green,
(Rgb, 2)
| (Srgb, 2)
| (SrgbLinear, 2)
| (DisplayP3, 2)
| (DisplayP3Linear, 2)
| (A98Rgb, 2)
| (ProphotoRgb, 2)
| (Rec2020, 2) => Blue,
(Hsl, 0) | (Hwb, 0) | (Lch, 2) | (Oklch, 2) => Hue,
(Hsl, 1) | (Lch, 1) | (Oklch, 1) => Colorfulness,
(Hsl, 2) | (Lab, 0) | (Lch, 0) | (Oklab, 0) | (Oklch, 0) => Lightness,
(Hwb, 1) => Whiteness,
(Hwb, 2) => Blackness,
(Lab, 1) | (Oklab, 1) => LabA,
(Lab, 2) | (Oklab, 2) => LabB,
(XyzD65, 0) | (XyzD50, 0) => Red,
(XyzD65, 1) | (XyzD50, 1) => Green,
(XyzD65, 2) | (XyzD50, 2) => Blue,
_ => return None,
})
}
pub(crate) fn convert_modern(mc: &ModernColor, target: ColorSpace) -> ModernColor {
if mc.space == target {
return mc.clone();
}
let src = [z(mc.channels[0]), z(mc.channels[1]), z(mc.channels[2])];
let out = crate::builtins::colorspace::convert(mc.space, target, src);
let missing_in_src = |cat: ChannelCategory| {
(0..3).any(|i| mc.channels[i].is_none() && channel_category(mc.space, i) == Some(cat))
};
let mk = |i: usize| match channel_category(target, i) {
Some(cat) if missing_in_src(cat) => None,
_ => Some(out[i]),
};
let mut channels = [mk(0), mk(1), mk(2)];
if matches!(target, ColorSpace::Lch | ColorSpace::Oklch) && out[1].abs() < 1e-10 {
channels[2] = None;
}
let lch_to_lab = mc.space == ColorSpace::Lch && target == ColorSpace::Lab;
if lch_to_lab && (channels[0].is_none() || out[0].abs() < 1e-11) {
channels[1] = None;
channels[2] = None;
}
if target == ColorSpace::Hsl && out[1].abs() < 1e-11 {
channels[0] = None;
}
if target == ColorSpace::Hwb {
let sum = out[1] + out[2];
if sum > 100.0 || (sum - 100.0).abs() < 1e-11 {
channels[0] = None;
}
}
ModernColor {
space: target,
channels,
alpha: mc.alpha,
}
}
pub(super) fn convert_modern_filled(mc: &ModernColor, target: ColorSpace) -> ModernColor {
let out = convert_modern(mc, target);
if mc.space == target || !target.is_legacy() {
return out;
}
ModernColor {
space: target,
channels: [
Some(out.channels[0].unwrap_or(0.0)),
Some(out.channels[1].unwrap_or(0.0)),
Some(out.channels[2].unwrap_or(0.0)),
],
alpha: Some(out.alpha.unwrap_or(0.0)),
}
}
pub(crate) fn legacy_to_modern(c: &Color) -> ModernColor {
if let Some(m) = &c.modern {
return (**m).clone();
}
ModernColor {
space: ColorSpace::Rgb,
channels: [Some(c.r), Some(c.g), Some(c.b)],
alpha: Some(c.a),
}
}
pub(super) fn make_modern(mc: ModernColor) -> Color {
let mc = normalize_polar(mc);
let srgb = convert_modern(&mc, ColorSpace::Rgb);
let mut c = Color::rgb(
z(srgb.channels[0]).clamp(0.0, 255.0),
z(srgb.channels[1]).clamp(0.0, 255.0),
z(srgb.channels[2]).clamp(0.0, 255.0),
mc.alpha.unwrap_or(1.0),
);
c.modern = Some(Box::new(mc));
c
}
fn normalize_hue(hue: f64, invert: bool) -> f64 {
let shift = if invert { 180.0 } else { 0.0 };
(hue % 360.0 + 360.0 + shift) % 360.0
}
fn normalize_polar(mut mc: ModernColor) -> ModernColor {
let fuzzy_zero = |v: f64| v.abs() < 1e-11;
let (hue_idx, mag_idx) = match mc.space {
ColorSpace::Hsl => (0, Some(1)),
ColorSpace::Hwb => (0, None),
ColorSpace::Lch | ColorSpace::Oklch => (2, Some(1)),
_ => return mc,
};
let mut invert = false;
if let Some(i) = mag_idx {
if let Some(c) = mc.channels[i] {
invert = c < 0.0 && !fuzzy_zero(c);
mc.channels[i] = Some(c.abs());
}
}
if let Some(h) = mc.channels[hue_idx] {
mc.channels[hue_idx] = Some(normalize_hue(h, invert));
}
mc
}
pub(super) fn predefined_space(name: &str) -> Option<ColorSpace> {
match name {
"srgb" => Some(ColorSpace::Srgb),
"srgb-linear" => Some(ColorSpace::SrgbLinear),
"display-p3" => Some(ColorSpace::DisplayP3),
"display-p3-linear" => Some(ColorSpace::DisplayP3Linear),
"a98-rgb" => Some(ColorSpace::A98Rgb),
"prophoto-rgb" => Some(ColorSpace::ProphotoRgb),
"rec2020" => Some(ColorSpace::Rec2020),
"xyz" | "xyz-d65" => Some(ColorSpace::XyzD65),
"xyz-d50" => Some(ColorSpace::XyzD50),
_ => None,
}
}
pub(super) fn modern_channel(v: &Value, pct_base: f64) -> Option<f64> {
if is_none_keyword(v) {
return None;
}
if let Value::Calc(node) = v {
if let Some(c) = degenerate_const(node) {
return Some(c);
}
}
match v {
Value::Number(num) => {
if num.unit() == "%" {
Some(num.value / 100.0 * pct_base)
} else {
Some(num.value)
}
}
Value::Slash(num, _) => Some(num.value),
_ => Some(0.0),
}
}
pub(super) fn modern_hue(v: &Value) -> Option<f64> {
if is_none_keyword(v) {
return None;
}
if let Value::Calc(node) = v {
if let Some(c) = degenerate_const(node) {
return Some(c);
}
}
match v {
Value::Number(num) => Some(match num.unit() {
"rad" => num.value.to_degrees(),
"grad" => num.value * 360.0 / 400.0,
"turn" => num.value * 360.0,
_ => num.value,
}),
Value::Slash(num, _) => Some(num.value),
_ => Some(0.0),
}
}
pub(super) fn modern_alpha(v: Option<&Value>) -> Option<f64> {
match v {
None => Some(1.0),
Some(a) if is_none_keyword(a) => None,
Some(a) => {
if let Some(c) = degenerate_value(a) {
return Some(if c.is_nan() { 0.0 } else { c.clamp(0.0, 1.0) });
}
match a {
Value::Number(num) => {
let val = if num.unit() == "%" {
num.value / 100.0
} else {
num.value
};
Some(val.clamp(0.0, 1.0))
}
Value::Slash(num, _) => Some(num.value.clamp(0.0, 1.0)),
_ => Some(1.0),
}
}
}
}
pub(super) fn color_space_of(c: &Color) -> ColorSpace {
c.modern.as_ref().map(|m| m.space).unwrap_or(ColorSpace::Rgb)
}
pub(super) fn channel_index_in(space: ColorSpace, channel: &str) -> Option<usize> {
space.channel_names().iter().position(|n| *n == channel)
}
pub(crate) fn space_arg(v: &Value, pos: Pos) -> Result<ColorSpace, Error> {
let name = match v {
Value::Str(s) if !s.quoted => s.text.clone(),
Value::Str(s) => {
return Err(Error::at(
format!("$space: Expected \"{}\" to be an unquoted string.", s.text),
pos,
))
}
other => {
return Err(Error::at(
format!("$space: {} is not a string.", other.to_css(false)),
pos,
))
}
};
ColorSpace::from_name(&name)
.ok_or_else(|| Error::at(format!("$space: Unknown color space \"{name}\"."), pos))
}
pub(crate) fn make_modern_in(mc: ModernColor, _space: ColorSpace) -> Color {
if mc.space == ColorSpace::Rgb && mc.channels.iter().all(|c| c.is_some()) && mc.alpha.is_some() {
let r = mc.channels[0].unwrap_or(0.0);
let g = mc.channels[1].unwrap_or(0.0);
let b = mc.channels[2].unwrap_or(0.0);
let a = mc.alpha.unwrap_or(1.0);
let mut c = Color::rgb(r, g, b, a);
let in_gamut = |v: f64| (-1e-9..=255.0 + 1e-9).contains(&v);
if !(in_gamut(r) && in_gamut(g) && in_gamut(b)) {
c.modern = Some(Box::new(mc));
} else {
c.repr = named_repr(r, g, b, a);
}
return c;
}
make_modern(mc)
}
pub(super) fn convert_to_space(mc: &ModernColor, space: ColorSpace) -> ModernColor {
convert_modern_filled(mc, space)
}
pub(super) fn channel_number(space: ColorSpace, idx: usize, raw: f64) -> Number {
use ColorSpace::*;
let names = space.channel_names();
let cname = names[idx];
let pct = |v: f64, max: f64| Number::with_unit(v * 100.0 / max, "%".to_string());
let deg = |v: f64| Number::with_unit(v, "deg".to_string());
let plain = |v: f64| Number::unitless(v);
match (space, cname) {
(Hsl, "saturation") | (Hsl, "lightness") => pct(raw, 100.0),
(Hwb, "whiteness") | (Hwb, "blackness") => pct(raw, 100.0),
(Lab, "lightness") | (Lch, "lightness") => pct(raw, 100.0),
(Oklab, "lightness") | (Oklch, "lightness") => pct(raw, 1.0),
(_, "hue") => deg(raw),
_ => plain(raw),
}
}
pub(super) fn channel_bounds(space: ColorSpace) -> Option<[Option<(f64, f64)>; 3]> {
use ColorSpace::*;
Some(match space {
Rgb => [Some((0.0, 255.0)); 3],
Srgb | SrgbLinear | DisplayP3 | DisplayP3Linear | A98Rgb | ProphotoRgb | Rec2020 => {
[Some((0.0, 1.0)); 3]
}
Hsl | Hwb => [None, Some((0.0, 100.0)), Some((0.0, 100.0))],
_ => return None,
})
}
pub(super) fn is_in_gamut(mc: &ModernColor) -> bool {
let Some(bounds) = channel_bounds(mc.space) else {
return true;
};
let fuzzy_eq = |a: f64, b: f64| (a - b).abs() < 1e-11;
for (bound, channel) in bounds.iter().zip(&mc.channels) {
if let Some((min, max)) = bound {
let v = channel.unwrap_or(0.0);
let ok = (v < *max || fuzzy_eq(v, *max)) && (v > *min || fuzzy_eq(v, *min));
if !ok {
return false;
}
}
}
true
}
pub(super) fn clip_in_own_space(mc: &ModernColor) -> ModernColor {
let Some(bounds) = channel_bounds(mc.space) else {
return mc.clone();
};
let clamp1 = |v: Option<f64>, b: Option<(f64, f64)>| match (v, b) {
(Some(v), Some((min, max))) => Some(if v.is_nan() { min } else { v.clamp(min, max) }),
_ => v,
};
ModernColor {
space: mc.space,
channels: [
clamp1(mc.channels[0], bounds[0]),
clamp1(mc.channels[1], bounds[1]),
clamp1(mc.channels[2], bounds[2]),
],
alpha: mc.alpha,
}
}
pub(super) fn in_gamut(mc: &ModernColor, space: ColorSpace) -> bool {
if mc.space == space {
return is_in_gamut(mc);
}
is_in_gamut(&convert_modern(mc, space))
}
pub(super) fn channel_powerless(space: ColorSpace, idx: usize, conv: &ModernColor) -> bool {
use ColorSpace::*;
let ch = |i: usize| conv.channels[i].unwrap_or(0.0);
let fuzzy_zero = |v: f64| v.abs() < 1e-11;
match (space, idx) {
(Hsl, 0) => fuzzy_zero(ch(1)),
(Hwb, 0) => ch(1) + ch(2) >= 100.0 - 1e-11,
(Lch, 2) | (Oklch, 2) => fuzzy_zero(ch(1)),
_ => false,
}
}
pub(super) fn null_powerless(mc: &ModernColor, space: ColorSpace) -> ModernColor {
let mut out = mc.clone();
for idx in 0..3 {
if channel_powerless(space, idx, mc) {
out.channels[idx] = None;
}
}
out
}
pub(super) fn gamut_map(color: &ModernColor) -> ModernColor {
let fuzzy_eq = |a: f64, b: f64| (a - b).abs() < 1e-11;
let origin_oklch = convert_modern(color, ColorSpace::Oklch);
let lightness = origin_oklch.channels[0];
let hue = origin_oklch.channels[2];
let alpha = color.alpha;
let l = lightness.unwrap_or(0.0);
if l > 1.0 || fuzzy_eq(l, 1.0) {
return if color.space.is_legacy() {
convert_modern(
&ModernColor {
space: ColorSpace::Rgb,
channels: [Some(255.0); 3],
alpha,
},
color.space,
)
} else {
ModernColor {
space: color.space,
channels: [Some(1.0); 3],
alpha,
}
};
}
if l < 0.0 || fuzzy_eq(l, 0.0) {
return convert_modern(
&ModernColor {
space: ColorSpace::Rgb,
channels: [Some(0.0); 3],
alpha,
},
color.space,
);
}
let mut clipped = if is_in_gamut(color) {
color.clone()
} else {
clip_in_own_space(color)
};
if delta_eok(&clipped, color) < 0.02 {
return clipped;
}
let mut max = origin_oklch.channels[1].unwrap_or(0.0);
let mut min = 0.0;
let mut min_in_gamut = true;
while max - min > 0.0001 {
let chroma = (min + max) / 2.0;
let current = convert_modern(
&ModernColor {
space: ColorSpace::Oklch,
channels: [lightness, Some(chroma), hue],
alpha,
},
color.space,
);
if min_in_gamut && is_in_gamut(¤t) {
min = chroma;
continue;
}
clipped = if is_in_gamut(¤t) {
current.clone()
} else {
clip_in_own_space(¤t)
};
let e = delta_eok(&clipped, ¤t);
if e < 0.02 {
if 0.02 - e < 0.0001 {
return clipped;
}
min = chroma;
min_in_gamut = false;
} else {
max = chroma;
}
}
clipped
}
fn delta_eok(a: &ModernColor, b: &ModernColor) -> f64 {
let a = convert_modern(a, ColorSpace::Oklab);
let b = convert_modern(b, ColorSpace::Oklab);
let dl = z(a.channels[0]) - z(b.channels[0]);
let da = z(a.channels[1]) - z(b.channels[1]);
let db = z(a.channels[2]) - z(b.channels[2]);
(dl * dl + da * da + db * db).sqrt()
}
pub(super) fn interpolate_mix(
c1: &Color,
c2: &Color,
weight: f64,
space: ColorSpace,
hue_method: HueMethod,
) -> Color {
let p = weight / 100.0;
let m1 = convert_modern(&blank_powerless(legacy_to_modern(c1)), space);
let m2 = convert_modern(&blank_powerless(legacy_to_modern(c2)), space);
let a1 = m1.alpha;
let a2 = m2.alpha;
let ra1 = a1.unwrap_or_else(|| a2.unwrap_or(1.0));
let ra2 = a2.unwrap_or_else(|| a1.unwrap_or(1.0));
let result_alpha = ra1 * p + ra2 * (1.0 - p);
let hue_idx = match space {
ColorSpace::Hsl | ColorSpace::Hwb => Some(0),
ColorSpace::Lch | ColorSpace::Oklch => Some(2),
_ => None,
};
let mut out = [None; 3];
for (i, slot) in out.iter_mut().enumerate() {
let v1 = m1.channels[i];
let v2 = m2.channels[i];
if v1.is_none() && v2.is_none() {
*slot = None;
continue;
}
let x1 = v1.unwrap_or_else(|| v2.unwrap_or(0.0));
let x2 = v2.unwrap_or_else(|| v1.unwrap_or(0.0));
if Some(i) == hue_idx {
*slot = Some(interpolate_hue(x1, x2, p, hue_method));
} else {
let pa1 = a1.unwrap_or(1.0);
let pa2 = a2.unwrap_or(1.0);
let premul = x1 * pa1 * p + x2 * pa2 * (1.0 - p);
*slot = Some(if result_alpha.abs() < 1e-12 {
x1 * p + x2 * (1.0 - p)
} else {
premul / result_alpha
});
}
}
let alpha = if a1.is_none() && a2.is_none() {
None
} else {
Some(result_alpha)
};
let mc = ModernColor {
space,
channels: out,
alpha,
};
let dest = legacy_to_modern(c1).space;
let back = convert_modern_filled(&mc, dest);
make_modern_in(back, dest)
}
pub(super) fn interpolate_hue(h1: f64, h2: f64, p: f64, method: HueMethod) -> f64 {
let mut a = h1.rem_euclid(360.0);
let mut b = h2.rem_euclid(360.0);
match method {
HueMethod::Shorter => {
let diff = b - a;
if diff > 180.0 {
a += 360.0;
} else if diff < -180.0 {
b += 360.0;
}
}
HueMethod::Longer => {
let diff = b - a;
if (0.0..180.0).contains(&diff) {
b += 360.0;
} else if (-180.0..0.0).contains(&diff) {
a += 360.0;
}
}
HueMethod::Increasing => {
if b < a {
b += 360.0;
}
}
HueMethod::Decreasing => {
if a < b {
a += 360.0;
}
}
}
a * p + b * (1.0 - p)
}
pub(super) fn blank_powerless(mut mc: ModernColor) -> ModernColor {
let ch = |i: usize| mc.channels[i].unwrap_or(0.0);
match mc.space {
ColorSpace::Hsl if ch(1) == 0.0 => mc.channels[0] = None,
ColorSpace::Hwb if ch(1) + ch(2) >= 100.0 => mc.channels[0] = None,
ColorSpace::Lch | ColorSpace::Oklch if ch(1) == 0.0 => mc.channels[2] = None,
_ => {}
}
mc
}
pub(super) fn scale_bounds(space: ColorSpace, idx: usize) -> Option<(f64, f64)> {
use ColorSpace::*;
let names = space.channel_names();
if names[idx] == "hue" {
return None;
}
Some(match space {
Rgb => (0.0, 255.0),
Srgb | SrgbLinear | DisplayP3 | DisplayP3Linear | A98Rgb | ProphotoRgb | Rec2020 => (0.0, 1.0),
Hsl | Hwb => (0.0, 100.0),
Lab => {
if idx == 0 {
(0.0, 100.0)
} else {
(-125.0, 125.0)
}
}
Lch => {
if idx == 0 {
(0.0, 100.0)
} else {
(0.0, 150.0)
}
}
Oklab => {
if idx == 0 {
(0.0, 1.0)
} else {
(-0.4, 0.4)
}
}
Oklch => {
if idx == 0 {
(0.0, 1.0)
} else {
(0.0, 0.4)
}
}
XyzD65 | XyzD50 => (0.0, 1.0),
})
}
pub(super) fn modify_channel_value(space: ColorSpace, idx: usize, v: &Value) -> Option<f64> {
if space.is_polar(idx) {
modern_hue(v)
} else {
modern_channel(v, channel_pct_base(space, idx))
}
}
impl ColorSpace {
fn is_polar(self, index: usize) -> bool {
self.channel_names()[index] == "hue"
}
}
fn channel_pct_base(space: ColorSpace, idx: usize) -> f64 {
use ColorSpace::*;
let names = space.channel_names();
match (space, names[idx]) {
(Rgb, _) => 255.0,
(Srgb | SrgbLinear | DisplayP3 | DisplayP3Linear | A98Rgb | ProphotoRgb | Rec2020, _) => 1.0,
(Hsl | Hwb, _) => 100.0,
(Lab, "lightness") | (Lch, "lightness") => 100.0,
(Oklab, "lightness") | (Oklch, "lightness") => 1.0,
(Lab, _) => 125.0,
(Oklab, _) => 0.4,
(Lch, "chroma") => 150.0,
(Oklch, "chroma") => 0.4,
(XyzD65 | XyzD50, _) => 1.0,
_ => 1.0,
}
}
pub(super) fn scale_pct(v: &Value, pos: Pos) -> Result<f64, Error> {
match v {
Value::Number(n) if n.unit() == "%" => {
if n.value < -100.0 || n.value > 100.0 {
return Err(Error::at(
format!("Expected {} to be within -100% and 100%.", n.to_css(false)),
pos,
));
}
Ok(n.value / 100.0)
}
Value::Number(n) => Err(Error::at(
format!("$amount: Expected {} to have unit \"%\".", n.to_css(false)),
pos,
)),
other => Err(Error::at(
format!("$amount: {} is not a number.", other.to_css(false)),
pos,
)),
}
}
pub(super) fn scale_to(current: f64, factor: f64, bounds: (f64, f64)) -> f64 {
if factor == 0.0 {
current
} else if factor > 0.0 {
if current >= bounds.1 {
current
} else {
current + (bounds.1 - current) * factor
}
} else if current <= bounds.0 {
current
} else {
current + (current - bounds.0) * factor
}
}
pub(super) fn validate_modify_unit(
space: ColorSpace,
idx: usize,
name: &str,
v: &Value,
pos: Pos,
) -> Result<(), Error> {
let num = match v {
Value::Number(n) => n,
Value::Slash(..) | Value::Calc(_) => return Ok(()),
other => {
return Err(Error::at(
format!("${name}: {} is not a number.", other.to_css(false)),
pos,
))
}
};
if space.is_polar(idx) {
if space.is_legacy() {
return Ok(());
}
let ok = num.is_unitless() || matches!(num.unit(), "deg" | "grad" | "rad" | "turn");
if !ok {
return Err(Error::at(
format!(
"${name}: Expected {} to have an angle unit (deg, grad, rad, turn).",
num.to_css(false)
),
pos,
));
}
} else if space == ColorSpace::Hsl {
} else if space == ColorSpace::Hwb {
if num.unit() != "%" {
return Err(Error::at(
format!("${name}: Expected {} to have unit \"%\".", num.to_css(false)),
pos,
));
}
} else if !num.is_unitless() && num.unit() != "%" {
return Err(Error::at(
format!(
"${name}: Expected {} to have unit \"%\" or no units.",
num.to_css(false)
),
pos,
));
}
Ok(())
}
pub(super) fn clamp_adjust_channel(space: ColorSpace, idx: usize, v: f64) -> f64 {
use ColorSpace::*;
let names = space.channel_names();
match (space, names[idx]) {
(Rgb, _) => v.clamp(0.0, 255.0),
(Lab, "lightness") | (Lch, "lightness") => v.clamp(0.0, 100.0),
(Oklab, "lightness") | (Oklch, "lightness") => v.clamp(0.0, 1.0),
(Lch, "chroma") | (Oklch, "chroma") => v.max(0.0),
(Hsl, "saturation") => v.max(0.0),
_ => v,
}
}
pub(super) fn invert_channels(space: ColorSpace, src: &ModernColor) -> ModernColor {
use ColorSpace::*;
let ch = src.channels;
let inv_max = |v: Option<f64>, max: f64| v.map(|x| max - x);
let negate = |v: Option<f64>| v.map(|x| -x);
let shift_hue = |v: Option<f64>| v.map(|x| x + 180.0);
let channels = match space {
Rgb => [
inv_max(ch[0], 255.0),
inv_max(ch[1], 255.0),
inv_max(ch[2], 255.0),
],
Srgb | SrgbLinear | DisplayP3 | DisplayP3Linear | A98Rgb | ProphotoRgb | Rec2020 | XyzD65
| XyzD50 => [inv_max(ch[0], 1.0), inv_max(ch[1], 1.0), inv_max(ch[2], 1.0)],
Hsl => [shift_hue(ch[0]), ch[1], inv_max(ch[2], 100.0)],
Hwb => [shift_hue(ch[0]), ch[2], ch[1]],
Lab => [inv_max(ch[0], 100.0), negate(ch[1]), negate(ch[2])],
Oklab => [inv_max(ch[0], 1.0), negate(ch[1]), negate(ch[2])],
Lch => [inv_max(ch[0], 100.0), ch[1], shift_hue(ch[2])],
Oklch => [inv_max(ch[0], 1.0), ch[1], shift_hue(ch[2])],
};
ModernColor {
space,
channels,
alpha: src.alpha,
}
}
pub(super) fn invert_modifies(space: ColorSpace, idx: usize) -> bool {
use ColorSpace::*;
!matches!(
(space, idx),
(Hsl, 1) | (Hwb, 1) | (Hwb, 2) | (Lch, 1) | (Oklch, 1)
)
}