#[cfg(feature = "std")]
extern crate std;
use core::any::TypeId;
use crate::{matvecmul, tag::ColorSpaceTag, Chromaticity};
#[cfg(all(not(feature = "std"), not(test)))]
use crate::floatfuncs::FloatFuncs;
pub trait ColorSpace: Clone + Copy + 'static {
const IS_LINEAR: bool = false;
const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::Rectangular;
const TAG: Option<ColorSpaceTag> = None;
const WHITE_POINT: Chromaticity = Chromaticity::D65;
const WHITE_COMPONENTS: [f32; 3];
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3];
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3];
fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
src
} else {
let lin_rgb = Self::to_linear_srgb(src);
TargetCS::from_linear_srgb(lin_rgb)
}
}
fn to_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
let lin_srgb = Self::to_linear_srgb(src);
if Self::WHITE_POINT == Chromaticity::D65 {
lin_srgb
} else {
let lin_srgb_adaptation_matrix = const {
Chromaticity::D65.linear_srgb_chromatic_adaptation_matrix(Self::WHITE_POINT)
};
matvecmul(&lin_srgb_adaptation_matrix, lin_srgb)
}
}
fn from_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
let lin_srgb_adapted = if Self::WHITE_POINT == Chromaticity::D65 {
src
} else {
let lin_srgb_adaptation_matrix = const {
Self::WHITE_POINT.linear_srgb_chromatic_adaptation_matrix(Chromaticity::D65)
};
matvecmul(&lin_srgb_adaptation_matrix, src)
};
Self::from_linear_srgb(lin_srgb_adapted)
}
fn convert_absolute<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
src
} else {
let lin_rgb = Self::to_linear_srgb_absolute(src);
TargetCS::from_linear_srgb_absolute(lin_rgb)
}
}
fn chromatically_adapt(src: [f32; 3], from: Chromaticity, to: Chromaticity) -> [f32; 3] {
if from == to {
return src;
}
let lin_srgb_adaptation_matrix = if from == Chromaticity::D65 && to == Chromaticity::D50 {
Chromaticity::D65.linear_srgb_chromatic_adaptation_matrix(Chromaticity::D50)
} else if from == Chromaticity::D50 && to == Chromaticity::D65 {
Chromaticity::D50.linear_srgb_chromatic_adaptation_matrix(Chromaticity::D65)
} else {
from.linear_srgb_chromatic_adaptation_matrix(to)
};
let lin_srgb_adapted = matvecmul(
&lin_srgb_adaptation_matrix,
Self::to_linear_srgb_absolute(src),
);
Self::from_linear_srgb_absolute(lin_srgb_adapted)
}
fn scale_chroma(src: [f32; 3], scale: f32) -> [f32; 3] {
let rgb = Self::to_linear_srgb(src);
let scaled = LinearSrgb::scale_chroma(rgb, scale);
Self::from_linear_srgb(scaled)
}
fn clip(src: [f32; 3]) -> [f32; 3];
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[non_exhaustive]
pub enum ColorSpaceLayout {
Rectangular,
HueFirst,
HueThird,
}
impl ColorSpaceLayout {
pub(crate) const fn scale(self, components: [f32; 3], scale: f32) -> [f32; 3] {
match self {
Self::Rectangular => [
components[0] * scale,
components[1] * scale,
components[2] * scale,
],
Self::HueFirst => [components[0], components[1] * scale, components[2] * scale],
Self::HueThird => [components[0] * scale, components[1] * scale, components[2]],
}
}
pub(crate) const fn hue_channel(self) -> Option<usize> {
match self {
Self::Rectangular => None,
Self::HueFirst => Some(0),
Self::HueThird => Some(2),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct LinearSrgb;
impl ColorSpace for LinearSrgb {
const IS_LINEAR: bool = true;
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::LinearSrgb);
const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
src
}
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
src
}
fn scale_chroma(src: [f32; 3], scale: f32) -> [f32; 3] {
let lms = matvecmul(&OKLAB_SRGB_TO_LMS, src).map(f32::cbrt);
let l = OKLAB_LMS_TO_LAB[0];
let lightness = l[0] * lms[0] + l[1] * lms[1] + l[2] * lms[2];
let lms_scaled = [
lightness + scale * (lms[0] - lightness),
lightness + scale * (lms[1] - lightness),
lightness + scale * (lms[2] - lightness),
];
matvecmul(&OKLAB_LMS_TO_SRGB, lms_scaled.map(|x| x * x * x))
}
fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
[r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
}
}
impl From<LinearSrgb> for ColorSpaceTag {
fn from(_: LinearSrgb) -> Self {
Self::LinearSrgb
}
}
#[derive(Clone, Copy, Debug)]
pub struct Srgb;
fn srgb_to_lin(x: f32) -> f32 {
if x.abs() <= 0.04045 {
x * (1.0 / 12.92)
} else {
((x.abs() + 0.055) * (1.0 / 1.055)).powf(2.4).copysign(x)
}
}
fn lin_to_srgb(x: f32) -> f32 {
if x.abs() <= 0.0031308 {
x * 12.92
} else {
(1.055 * x.abs().powf(1.0 / 2.4) - 0.055).copysign(x)
}
}
impl ColorSpace for Srgb {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Srgb);
const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
src.map(srgb_to_lin)
}
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
src.map(lin_to_srgb)
}
fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
src
} else if TypeId::of::<TargetCS>() == TypeId::of::<Hsl>() {
rgb_to_hsl(src, true)
} else if TypeId::of::<TargetCS>() == TypeId::of::<Hwb>() {
rgb_to_hwb(src)
} else {
let lin_rgb = Self::to_linear_srgb(src);
TargetCS::from_linear_srgb(lin_rgb)
}
}
fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
[r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
}
}
impl From<Srgb> for ColorSpaceTag {
fn from(_: Srgb) -> Self {
Self::Srgb
}
}
#[derive(Clone, Copy, Debug)]
pub struct DisplayP3;
impl ColorSpace for DisplayP3 {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::DisplayP3);
const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
const LINEAR_DISPLAYP3_TO_SRGB: [[f32; 3]; 3] = [
[1.224_940_2, -0.224_940_18, 0.0],
[-0.042_056_955, 1.042_056_9, 0.0],
[-0.019_637_555, -0.078_636_04, 1.098_273_6],
];
matvecmul(&LINEAR_DISPLAYP3_TO_SRGB, src.map(srgb_to_lin))
}
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
const LINEAR_SRGB_TO_DISPLAYP3: [[f32; 3]; 3] = [
[0.822_461_96, 0.177_538_04, 0.0],
[0.033_194_2, 0.966_805_8, 0.0],
[0.017_082_632, 0.072_397_44, 0.910_519_96],
];
matvecmul(&LINEAR_SRGB_TO_DISPLAYP3, src).map(lin_to_srgb)
}
fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
[r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
}
}
impl From<DisplayP3> for ColorSpaceTag {
fn from(_: DisplayP3) -> Self {
Self::DisplayP3
}
}
#[derive(Clone, Copy, Debug)]
pub struct A98Rgb;
impl ColorSpace for A98Rgb {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::A98Rgb);
const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
fn to_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
#[expect(
clippy::cast_possible_truncation,
reason = "exact rational, truncate at compile-time"
)]
const LINEAR_A98RGB_TO_SRGB: [[f32; 3]; 3] = [
[
(66_942_405. / 47_872_228.) as f32,
(-19_070_177. / 47_872_228.) as f32,
0.,
],
[0., 1., 0.],
[
0.,
(-11_512_411. / 268_173_353.) as f32,
(279_685_764. / 268_173_353.) as f32,
],
];
matvecmul(
&LINEAR_A98RGB_TO_SRGB,
[r, g, b].map(|x| x.abs().powf(563. / 256.).copysign(x)),
)
}
fn from_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
#[expect(
clippy::cast_possible_truncation,
reason = "exact rational, truncate at compile-time"
)]
const LINEAR_SRGB_TO_A98RGB: [[f32; 3]; 3] = [
[
(47_872_228. / 66_942_405.) as f32,
(19_070_177. / 66_942_405.) as f32,
0.0,
],
[0., 1., 0.],
[
0.,
(11_512_411. / 279_685_764.) as f32,
(268_173_353. / 279_685_764.) as f32,
],
];
matvecmul(&LINEAR_SRGB_TO_A98RGB, [r, g, b]).map(|x| x.abs().powf(256. / 563.).copysign(x))
}
fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
[r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
}
}
impl From<A98Rgb> for ColorSpaceTag {
fn from(_: A98Rgb) -> Self {
Self::A98Rgb
}
}
#[derive(Clone, Copy, Debug)]
pub struct ProphotoRgb;
impl ProphotoRgb {
fn transfer_to_linear(x: f32) -> f32 {
if x.abs() <= 16. / 512. {
x / 16.
} else {
x.abs().powf(1.8).copysign(x)
}
}
fn transfer_from_linear(x: f32) -> f32 {
if x.abs() <= 1. / 512. {
x * 16.
} else {
x.abs().powf(1. / 1.8).copysign(x)
}
}
}
impl ColorSpace for ProphotoRgb {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::ProphotoRgb);
const WHITE_POINT: Chromaticity = Chromaticity::D50;
const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
fn to_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
const LINEAR_PROPHOTORGB_TO_SRGB: [[f32; 3]; 3] = [
[2.034_367_6, -0.727_634_5, -0.306_733_07],
[-0.228_826_79, 1.231_753_3, -0.002_926_598],
[-0.008_558_424, -0.153_268_2, 1.161_826_6],
];
matvecmul(
&LINEAR_PROPHOTORGB_TO_SRGB,
[r, g, b].map(Self::transfer_to_linear),
)
}
fn from_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
const LINEAR_SRGB_TO_PROPHOTORGB: [[f32; 3]; 3] = [
[0.529_280_4, 0.330_153, 0.140_566_6],
[0.098_366_22, 0.873_463_9, 0.028_169_824],
[0.016_875_342, 0.117_659_41, 0.865_465_2],
];
matvecmul(&LINEAR_SRGB_TO_PROPHOTORGB, [r, g, b]).map(Self::transfer_from_linear)
}
fn to_linear_srgb_absolute([r, g, b]: [f32; 3]) -> [f32; 3] {
const LINEAR_PROPHOTORGB_TO_SRGB: [[f32; 3]; 3] = [
[
11_822_636_894_621. / 5_517_784_378_314.,
-2_646_118_971_832. / 4_032_227_045_691.,
-2_824_985_149. / 9_114_754_233.,
],
[
-270_896_603_412_176. / 1_163_584_209_404_097.,
107_798_623_831_136. / 89_506_477_646_469.,
822_014_396. / 202_327_283_847.,
],
[
-2412976100974. / 167_796_255_001_401.,
-1_777_081_293_536. / 12_907_404_230_877.,
879_168_464. / 1_006_099_419.,
],
];
matvecmul(
&LINEAR_PROPHOTORGB_TO_SRGB,
[r, g, b].map(Self::transfer_to_linear),
)
}
fn from_linear_srgb_absolute([r, g, b]: [f32; 3]) -> [f32; 3] {
const LINEAR_SRGB_TO_PROPHOTORGB: [[f32; 3]; 3] = [
[
7_356_071_250_722. / 14_722_127_359_275.,
25_825_157_007_599. / 88_332_764_155_650.,
1_109_596_896_521. / 6_309_483_153_975.,
],
[
170_513_936_009. / 1_766_822_975_400.,
18_792_073_269_331. / 21_201_875_704_800.,
91_195_554_323. / 3_028_839_386_400.,
],
[
946_201. / 40_387_053.,
105_017_795. / 726_966_954.,
8_250_997. / 7_162_236.,
],
];
matvecmul(&LINEAR_SRGB_TO_PROPHOTORGB, [r, g, b]).map(Self::transfer_from_linear)
}
fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
[r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
}
}
impl From<ProphotoRgb> for ColorSpaceTag {
fn from(_: ProphotoRgb) -> Self {
Self::ProphotoRgb
}
}
#[derive(Clone, Copy, Debug)]
pub struct Rec2020;
impl Rec2020 {
const A: f32 = 1.099_296_8;
const B: f32 = 0.018_053_97;
}
impl ColorSpace for Rec2020 {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Rec2020);
const WHITE_COMPONENTS: [f32; 3] = [1., 1., 1.];
fn to_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
#[expect(
clippy::cast_possible_truncation,
reason = "exact rational, truncate at compile-time"
)]
const LINEAR_REC2020_TO_SRGB: [[f32; 3]; 3] = [
[
(2_785_571_537. / 1_677_558_947.) as f32,
(-985_802_650. / 1_677_558_947.) as f32,
(-122_209_940. / 1_677_558_947.) as f32,
],
[
(-4_638_020_506. / 37_238_079_773.) as f32,
(42_187_016_744. / 37_238_079_773.) as f32,
(-310_916_465. / 37_238_079_773.) as f32,
],
[
(-97_469_024. / 5_369_968_309.) as f32,
(-3_780_738_464. / 37_589_778_163.) as f32,
(42_052_799_795. / 37_589_778_163.) as f32,
],
];
fn transfer(x: f32) -> f32 {
if x.abs() < Rec2020::B * 4.5 {
x * (1. / 4.5)
} else {
((x.abs() + (Rec2020::A - 1.)) / Rec2020::A)
.powf(1. / 0.45)
.copysign(x)
}
}
matvecmul(&LINEAR_REC2020_TO_SRGB, [r, g, b].map(transfer))
}
fn from_linear_srgb([r, g, b]: [f32; 3]) -> [f32; 3] {
#[expect(
clippy::cast_possible_truncation,
reason = "exact rational, truncate at compile-time"
)]
const LINEAR_SRGB_TO_REC2020: [[f32; 3]; 3] = [
[
(2_939_026_994. / 4_684_425_795.) as f32,
(9_255_011_753. / 28_106_554_770.) as f32,
(173_911_579. / 4_015_222_110.) as f32,
],
[
(76_515_593. / 1_107_360_270.) as f32,
(6_109_575_001. / 6_644_161_620.) as f32,
(75_493_061. / 6_644_161_620.) as f32,
],
[
(12_225_392. / 745_840_075.) as f32,
(1_772_384_008. / 20_137_682_025.) as f32,
(18_035_212_433. / 20_137_682_025.) as f32,
],
];
fn transfer(x: f32) -> f32 {
if x.abs() < Rec2020::B {
x * 4.5
} else {
(Rec2020::A * x.abs().powf(0.45) - (Rec2020::A - 1.)).copysign(x)
}
}
matvecmul(&LINEAR_SRGB_TO_REC2020, [r, g, b]).map(transfer)
}
fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
[r.clamp(0., 1.), g.clamp(0., 1.), b.clamp(0., 1.)]
}
}
impl From<Rec2020> for ColorSpaceTag {
fn from(_: Rec2020) -> Self {
Self::Rec2020
}
}
#[derive(Clone, Copy, Debug)]
pub struct Aces2065_1;
impl ColorSpace for Aces2065_1 {
const IS_LINEAR: bool = true;
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Aces2065_1);
const WHITE_POINT: Chromaticity = Chromaticity::ACES;
const WHITE_COMPONENTS: [f32; 3] = [1.0, 1.0, 1.0];
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
const ACES2065_1_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
[2.521_686, -1.134_131, -0.387_555_2],
[-0.276_479_9, 1.372_719, -0.096_239_17],
[-0.015_378_065, -0.152_975_34, 1.168_353_4],
];
matvecmul(&ACES2065_1_TO_LINEAR_SRGB, src)
}
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
const LINEAR_SRGB_TO_ACES2065_1: [[f32; 3]; 3] = [
[0.439_632_98, 0.382_988_7, 0.177_378_33],
[0.089_776_44, 0.813_439_4, 0.096_784_13],
[0.017_541_17, 0.111_546_55, 0.870_912_25],
];
matvecmul(&LINEAR_SRGB_TO_ACES2065_1, src)
}
fn to_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
const ACES2065_1_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
[
54_120_196_967_290_615. / 21_154_043_450_084_358.,
-320_017_885_460_000. / 285_865_452_028_167.,
-564_067_687_050. / 1_439_638_182_257.,
],
[
-65_267_199_138_999_760. / 234_786_371_866_236_861.,
320_721_924_808_012_000. / 234_786_371_866_236_861.,
-2_987_552_619_450. / 31_956_767_642_063.,
],
[
-581_359_048_862_990. / 33_857_690_407_037_013.,
-457_168_407_800_000. / 3_077_971_855_185_183.,
4_981_730_664_150. / 4_608_369_457_879.,
],
];
matvecmul(&ACES2065_1_TO_LINEAR_SRGB, src)
}
fn from_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
const LINEAR_SRGB_TO_ACES2065_1: [[f32; 3]; 3] = [
[
26_324_697_889_654. / 60_805_826_029_215.,
95_867_335_448_462. / 255_384_469_322_703.,
34_545_867_731_048. / 182_417_478_087_645.,
],
[
1_068_725_544_495_979. / 11_952_668_021_931_000.,
9_008_998_273_654_297. / 11_033_232_020_244_000.,
2_110_950_307_239_113. / 20_490_288_037_596_000.,
],
[
267_367_106. / 13_953_194_325.,
2_967_477_727. / 25_115_749_785.,
33_806_406_089. / 35_879_642_550.,
],
];
matvecmul(&LINEAR_SRGB_TO_ACES2065_1, src)
}
fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
[
r.clamp(-65504., 65504.),
g.clamp(-65504., 65504.),
b.clamp(-65504., 65504.),
]
}
}
impl From<Aces2065_1> for ColorSpaceTag {
fn from(_: Aces2065_1) -> Self {
Self::Aces2065_1
}
}
#[derive(Clone, Copy, Debug)]
pub struct AcesCg;
impl ColorSpace for AcesCg {
const IS_LINEAR: bool = true;
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::AcesCg);
const WHITE_POINT: Chromaticity = Chromaticity::ACES;
const WHITE_COMPONENTS: [f32; 3] = [1.0, 1.0, 1.0];
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
const ACESCG_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
[1.705_051, -0.621_792_14, -0.083_258_875],
[-0.130_256_41, 1.140_804_8, -0.010_548_319],
[-0.024_003_357, -0.128_968_97, 1.152_972_3],
];
matvecmul(&ACESCG_TO_LINEAR_SRGB, src)
}
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
const LINEAR_SRGB_TO_ACESCG: [[f32; 3]; 3] = [
[0.613_097_4, 0.339_523_14, 0.047_379_453],
[0.070_193_72, 0.916_353_9, 0.013_452_399],
[0.020_615_593, 0.109_569_77, 0.869_814_63],
];
matvecmul(&LINEAR_SRGB_TO_ACESCG, src)
}
fn to_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
const ACESCG_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
[
9_932_023_100_445. / 5_736_895_993_442.,
-1_732_666_183_650. / 2_868_447_996_721.,
-229_784_797_280. / 2_868_447_996_721.,
],
[
-194_897_543_280. / 1_480_771_385_773.,
72_258_955_647_750. / 63_673_169_588_239.,
-552_646_980_800. / 63_673_169_588_239.,
],
[
-68_657_089_110. / 2_794_545_067_783.,
-8082548957250. / 64_274_536_559_009.,
14_669_805_440. / 13_766_231_861.,
],
];
matvecmul(&ACESCG_TO_LINEAR_SRGB, src)
}
fn from_linear_srgb_absolute(src: [f32; 3]) -> [f32; 3] {
const LINEAR_SRGB_TO_ACESCG: [[f32; 3]; 3] = [
[
2_095_356_009_722. / 3_474_270_183_447.,
17_006_614_853_437. / 52_114_052_751_705.,
71_464_174_897. / 1_488_972_935_763.,
],
[
1_774_515_482_522. / 25_307_573_950_575.,
69_842_555_782_672. / 75_922_721_851_725.,
276_870_186_577. / 21_692_206_243_350.,
],
[
101_198_449_621. / 4_562_827_993_584.,
31_778_718_978_443. / 273_769_679_615_040.,
1_600_138_878_851. / 1_700_432_792_640.,
],
];
matvecmul(&LINEAR_SRGB_TO_ACESCG, src)
}
fn clip([r, g, b]: [f32; 3]) -> [f32; 3] {
[
r.clamp(-65504., 65504.),
g.clamp(-65504., 65504.),
b.clamp(-65504., 65504.),
]
}
}
impl From<AcesCg> for ColorSpaceTag {
fn from(_: AcesCg) -> Self {
Self::AcesCg
}
}
#[derive(Clone, Copy, Debug)]
pub struct XyzD50;
impl ColorSpace for XyzD50 {
const IS_LINEAR: bool = true;
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::XyzD50);
const WHITE_POINT: Chromaticity = Chromaticity::D50;
const WHITE_COMPONENTS: [f32; 3] = [3457. / 3585., 1., 986. / 1195.];
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
const XYZ_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
[3.134_136, -1.617_386, -0.490_662_22],
[-0.978_795_47, 1.916_254_4, 0.033_442_874],
[0.071_955_39, -0.228_976_76, 1.405_386_1],
];
matvecmul(&XYZ_TO_LINEAR_SRGB, src)
}
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
const LINEAR_SRGB_TO_XYZ: [[f32; 3]; 3] = [
[0.436_065_73, 0.385_151_5, 0.143_078_42],
[0.222_493_17, 0.716_887, 0.060_619_81],
[0.013_923_922, 0.097_081_326, 0.714_099_35],
];
matvecmul(&LINEAR_SRGB_TO_XYZ, src)
}
fn clip([x, y, z]: [f32; 3]) -> [f32; 3] {
[x, y, z]
}
}
impl From<XyzD50> for ColorSpaceTag {
fn from(_: XyzD50) -> Self {
Self::XyzD50
}
}
#[derive(Clone, Copy, Debug)]
pub struct XyzD65;
impl ColorSpace for XyzD65 {
const IS_LINEAR: bool = true;
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::XyzD65);
const WHITE_COMPONENTS: [f32; 3] = [3127. / 3290., 1., 3583. / 3290.];
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
const XYZ_TO_LINEAR_SRGB: [[f32; 3]; 3] = [
[12_831. / 3_959., -329. / 214., -1_974. / 3_959.],
[
-851_781. / 878_810.,
1_648_619. / 878_810.,
36_519. / 878_810.,
],
[705. / 12_673., -2_585. / 12_673., 705. / 667.],
];
matvecmul(&XYZ_TO_LINEAR_SRGB, src)
}
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
const LINEAR_SRGB_TO_XYZ: [[f32; 3]; 3] = [
[506_752. / 1_228_815., 87_881. / 245_763., 12_673. / 70_218.],
[87_098. / 409_605., 175_762. / 245_763., 12_673. / 175_545.],
[
7_918. / 409_605.,
87_881. / 737_289.,
100_1167. / 1_053_270.,
],
];
matvecmul(&LINEAR_SRGB_TO_XYZ, src)
}
fn clip([x, y, z]: [f32; 3]) -> [f32; 3] {
[x, y, z]
}
}
impl From<XyzD65> for ColorSpaceTag {
fn from(_: XyzD65) -> Self {
Self::XyzD65
}
}
#[derive(Clone, Copy, Debug)]
pub struct Oklab;
const OKLAB_LAB_TO_LMS: [[f32; 3]; 3] = [
[1.0, 0.396_337_78, 0.215_803_76],
[1.0, -0.105_561_346, -0.063_854_17],
[1.0, -0.089_484_18, -1.291_485_5],
];
const OKLAB_LMS_TO_SRGB: [[f32; 3]; 3] = [
[4.076_741_7, -3.307_711_6, 0.230_969_94],
[-1.268_438, 2.609_757_4, -0.341_319_38],
[-0.004_196_086_3, -0.703_418_6, 1.707_614_7],
];
const OKLAB_SRGB_TO_LMS: [[f32; 3]; 3] = [
[0.412_221_46, 0.536_332_55, 0.051_445_995],
[0.211_903_5, 0.680_699_5, 0.107_396_96],
[0.088_302_46, 0.281_718_85, 0.629_978_7],
];
const OKLAB_LMS_TO_LAB: [[f32; 3]; 3] = [
[0.210_454_26, 0.793_617_8, -0.004_072_047],
[1.977_998_5, -2.428_592_2, 0.450_593_7],
[0.025_904_037, 0.782_771_77, -0.808_675_77],
];
impl ColorSpace for Oklab {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Oklab);
const WHITE_COMPONENTS: [f32; 3] = [1., 0., 0.];
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
let lms = matvecmul(&OKLAB_LAB_TO_LMS, src).map(|x| x * x * x);
matvecmul(&OKLAB_LMS_TO_SRGB, lms)
}
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
let lms = matvecmul(&OKLAB_SRGB_TO_LMS, src).map(f32::cbrt);
matvecmul(&OKLAB_LMS_TO_LAB, lms)
}
fn scale_chroma([l, a, b]: [f32; 3], scale: f32) -> [f32; 3] {
[l, a * scale, b * scale]
}
fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
src
} else if TypeId::of::<TargetCS>() == TypeId::of::<Oklch>() {
lab_to_lch(src)
} else {
let lin_rgb = Self::to_linear_srgb(src);
TargetCS::from_linear_srgb(lin_rgb)
}
}
fn clip([l, a, b]: [f32; 3]) -> [f32; 3] {
[l.clamp(0., 1.), a, b]
}
}
impl From<Oklab> for ColorSpaceTag {
fn from(_: Oklab) -> Self {
Self::Oklab
}
}
fn lab_to_lch([l, a, b]: [f32; 3]) -> [f32; 3] {
let mut h = b.atan2(a).to_degrees();
if h < 0.0 {
h += 360.0;
}
let c = b.hypot(a);
[l, c, h]
}
fn lch_to_lab([l, c, h]: [f32; 3]) -> [f32; 3] {
let (sin, cos) = h.to_radians().sin_cos();
let a = c * cos;
let b = c * sin;
[l, a, b]
}
#[derive(Clone, Copy, Debug)]
pub struct Oklch;
impl ColorSpace for Oklch {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Oklch);
const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueThird;
const WHITE_COMPONENTS: [f32; 3] = [1., 0., 90.];
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
lab_to_lch(Oklab::from_linear_srgb(src))
}
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
Oklab::to_linear_srgb(lch_to_lab(src))
}
fn scale_chroma([l, c, h]: [f32; 3], scale: f32) -> [f32; 3] {
[l, c * scale, h]
}
fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
src
} else if TypeId::of::<TargetCS>() == TypeId::of::<Oklab>() {
lch_to_lab(src)
} else {
let lin_rgb = Self::to_linear_srgb(src);
TargetCS::from_linear_srgb(lin_rgb)
}
}
fn clip([l, c, h]: [f32; 3]) -> [f32; 3] {
[l.clamp(0., 1.), c.max(0.), h]
}
}
impl From<Oklch> for ColorSpaceTag {
fn from(_: Oklch) -> Self {
Self::Oklch
}
}
#[derive(Clone, Copy, Debug)]
pub struct Lab;
const LAB_SRGB_TO_XYZ: [[f32; 3]; 3] = [
[0.452_211_65, 0.399_412_24, 0.148_376_09],
[0.222_493_17, 0.716_887, 0.060_619_81],
[0.016_875_342, 0.117_659_41, 0.865_465_2],
];
const LAB_XYZ_TO_SRGB: [[f32; 3]; 3] = [
[3.022_233_7, -1.617_386, -0.404_847_65],
[-0.943_848_25, 1.916_254_4, 0.027_593_868],
[0.069_386_27, -0.228_976_76, 1.159_590_5],
];
const EPSILON: f32 = 216. / 24389.;
const KAPPA: f32 = 24389. / 27.;
impl ColorSpace for Lab {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Lab);
const WHITE_COMPONENTS: [f32; 3] = [100., 0., 0.];
fn to_linear_srgb([l, a, b]: [f32; 3]) -> [f32; 3] {
let f1 = l * (1. / 116.) + (16. / 116.);
let f0 = a * (1. / 500.) + f1;
let f2 = f1 - b * (1. / 200.);
let xyz = [f0, f1, f2].map(|value| {
const EPSILON_CBRT: f32 = 0.206_896_56;
if value > EPSILON_CBRT {
value * value * value
} else {
(116. / KAPPA) * value - (16. / KAPPA)
}
});
matvecmul(&LAB_XYZ_TO_SRGB, xyz)
}
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
let xyz = matvecmul(&LAB_SRGB_TO_XYZ, src);
let f = xyz.map(|value| {
if value > EPSILON {
value.cbrt()
} else {
(KAPPA / 116.) * value + (16. / 116.)
}
});
let l = 116. * f[1] - 16.;
let a = 500. * (f[0] - f[1]);
let b = 200. * (f[1] - f[2]);
[l, a, b]
}
fn scale_chroma([l, a, b]: [f32; 3], scale: f32) -> [f32; 3] {
[l, a * scale, b * scale]
}
fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
src
} else if TypeId::of::<TargetCS>() == TypeId::of::<Lch>() {
lab_to_lch(src)
} else {
let lin_rgb = Self::to_linear_srgb(src);
TargetCS::from_linear_srgb(lin_rgb)
}
}
fn clip([l, a, b]: [f32; 3]) -> [f32; 3] {
[l.clamp(0., 100.), a, b]
}
}
impl From<Lab> for ColorSpaceTag {
fn from(_: Lab) -> Self {
Self::Lab
}
}
#[derive(Clone, Copy, Debug)]
pub struct Lch;
impl ColorSpace for Lch {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Lch);
const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueThird;
const WHITE_COMPONENTS: [f32; 3] = [100., 0., 0.];
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
lab_to_lch(Lab::from_linear_srgb(src))
}
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
Lab::to_linear_srgb(lch_to_lab(src))
}
fn scale_chroma([l, c, h]: [f32; 3], scale: f32) -> [f32; 3] {
[l, c * scale, h]
}
fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
src
} else if TypeId::of::<TargetCS>() == TypeId::of::<Lab>() {
lch_to_lab(src)
} else {
let lin_rgb = Self::to_linear_srgb(src);
TargetCS::from_linear_srgb(lin_rgb)
}
}
fn clip([l, c, h]: [f32; 3]) -> [f32; 3] {
[l.clamp(0., 100.), c.max(0.), h]
}
}
impl From<Lch> for ColorSpaceTag {
fn from(_: Lch) -> Self {
Self::Lch
}
}
#[derive(Clone, Copy, Debug)]
pub struct Hsl;
fn hsl_to_rgb([h, s, l]: [f32; 3]) -> [f32; 3] {
let sat = s * 0.01;
let light = l * 0.01;
let a = sat * light.min(1.0 - light);
[0.0, 8.0, 4.0].map(|n| {
let x = n + h * (1.0 / 30.0);
let k = x - 12.0 * (x * (1.0 / 12.0)).floor();
light - a * (k - 3.0).min(9.0 - k).clamp(-1.0, 1.0)
})
}
fn rgb_to_hsl([r, g, b]: [f32; 3], hue_hack: bool) -> [f32; 3] {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let mut hue = 0.0;
let mut sat = 0.0;
let light = 0.5 * (min + max);
let d = max - min;
const EPSILON: f32 = 1e-6;
if d > EPSILON {
let denom = light.min(1.0 - light);
if denom.abs() > EPSILON {
sat = (max - light) / denom;
}
hue = if max == r {
(g - b) / d
} else if max == g {
(b - r) / d + 2.0
} else {
(r - g) / d + 4.0
};
hue *= 60.0;
if hue_hack && sat < 0.0 {
hue += 180.0;
sat = sat.abs();
}
hue -= 360. * (hue * (1.0 / 360.0)).floor();
}
[hue, sat * 100.0, light * 100.0]
}
impl ColorSpace for Hsl {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Hsl);
const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueFirst;
const WHITE_COMPONENTS: [f32; 3] = [0., 0., 100.];
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
let rgb = Srgb::from_linear_srgb(src);
rgb_to_hsl(rgb, true)
}
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
let rgb = hsl_to_rgb(src);
Srgb::to_linear_srgb(rgb)
}
fn scale_chroma([h, s, l]: [f32; 3], scale: f32) -> [f32; 3] {
[h, s * scale, l]
}
fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
src
} else if TypeId::of::<TargetCS>() == TypeId::of::<Srgb>() {
hsl_to_rgb(src)
} else if TypeId::of::<TargetCS>() == TypeId::of::<Hwb>() {
rgb_to_hwb(hsl_to_rgb(src))
} else {
let lin_rgb = Self::to_linear_srgb(src);
TargetCS::from_linear_srgb(lin_rgb)
}
}
fn clip([h, s, l]: [f32; 3]) -> [f32; 3] {
[h, s.max(0.), l.clamp(0., 100.)]
}
}
impl From<Hsl> for ColorSpaceTag {
fn from(_: Hsl) -> Self {
Self::Hsl
}
}
#[derive(Clone, Copy, Debug)]
pub struct Hwb;
fn hwb_to_rgb([h, w, b]: [f32; 3]) -> [f32; 3] {
let white = w * 0.01;
let black = b * 0.01;
if white + black >= 1.0 {
let gray = white / (white + black);
[gray, gray, gray]
} else {
let rgb = hsl_to_rgb([h, 100., 50.]);
rgb.map(|x| white + x * (1.0 - white - black))
}
}
fn rgb_to_hwb([r, g, b]: [f32; 3]) -> [f32; 3] {
let hsl = rgb_to_hsl([r, g, b], false);
let white = r.min(g).min(b);
let black = 1.0 - r.max(g).max(b);
[hsl[0], white * 100., black * 100.]
}
impl ColorSpace for Hwb {
const TAG: Option<ColorSpaceTag> = Some(ColorSpaceTag::Hwb);
const LAYOUT: ColorSpaceLayout = ColorSpaceLayout::HueFirst;
const WHITE_COMPONENTS: [f32; 3] = [0., 100., 0.];
fn from_linear_srgb(src: [f32; 3]) -> [f32; 3] {
let rgb = Srgb::from_linear_srgb(src);
rgb_to_hwb(rgb)
}
fn to_linear_srgb(src: [f32; 3]) -> [f32; 3] {
let rgb = hwb_to_rgb(src);
Srgb::to_linear_srgb(rgb)
}
fn convert<TargetCS: ColorSpace>(src: [f32; 3]) -> [f32; 3] {
if TypeId::of::<Self>() == TypeId::of::<TargetCS>() {
src
} else if TypeId::of::<TargetCS>() == TypeId::of::<Srgb>() {
hwb_to_rgb(src)
} else if TypeId::of::<TargetCS>() == TypeId::of::<Hsl>() {
rgb_to_hsl(hwb_to_rgb(src), true)
} else {
let lin_rgb = Self::to_linear_srgb(src);
TargetCS::from_linear_srgb(lin_rgb)
}
}
fn clip([h, w, b]: [f32; 3]) -> [f32; 3] {
[h, w.clamp(0., 100.), b.clamp(0., 100.)]
}
}
impl From<Hwb> for ColorSpaceTag {
fn from(_: Hwb) -> Self {
Self::Hwb
}
}
#[cfg(test)]
mod tests {
extern crate alloc;
use crate::{
A98Rgb, Aces2065_1, AcesCg, Chromaticity, ColorSpace, DisplayP3, Hsl, Hwb, Lab, Lch,
LinearSrgb, Oklab, Oklch, OpaqueColor, ProphotoRgb, Rec2020, Srgb, XyzD50, XyzD65,
};
use alloc::vec::Vec;
#[must_use]
fn almost_equal<CS: ColorSpace>(col1: [f32; 3], col2: [f32; 3], absolute_epsilon: f32) -> bool {
OpaqueColor::<CS>::new(col1).difference(OpaqueColor::new(col2)) <= absolute_epsilon
}
fn magnitude(col: [f32; 3]) -> f32 {
col[0].abs().max(col[1].abs()).max(col[2].abs())
}
#[test]
fn roundtrip() {
fn test_roundtrips<Source: ColorSpace, Dest: ColorSpace>(colors: &[[f32; 3]]) {
const RELATIVE_EPSILON: f32 = f32::EPSILON * 16.;
for color in colors {
let intermediate = Source::convert::<Dest>(*color);
let roundtripped = Dest::convert::<Source>(intermediate);
let linsrgb_color = Source::to_linear_srgb(*color);
let linsrgb_roundtripped = Source::to_linear_srgb(roundtripped);
let absolute_epsilon = magnitude(linsrgb_color).max(1.) * RELATIVE_EPSILON;
assert!(almost_equal::<LinearSrgb>(
linsrgb_color,
linsrgb_roundtripped,
absolute_epsilon,
));
}
}
let rectangular_values = {
let components = [
0., 1., -1., 0.5, 1234., -1234., 1.000_001, 0.000_001, -0.000_001,
];
let mut values = Vec::new();
for c0 in components {
for c1 in components {
for c2 in components {
values.push([c0, c1, c2]);
}
}
}
values
};
test_roundtrips::<LinearSrgb, Srgb>(&rectangular_values);
test_roundtrips::<DisplayP3, Srgb>(&rectangular_values);
test_roundtrips::<A98Rgb, Srgb>(&rectangular_values);
test_roundtrips::<ProphotoRgb, Srgb>(&rectangular_values);
test_roundtrips::<Rec2020, Srgb>(&rectangular_values);
test_roundtrips::<Aces2065_1, Srgb>(&rectangular_values);
test_roundtrips::<AcesCg, Srgb>(&rectangular_values);
test_roundtrips::<XyzD50, Srgb>(&rectangular_values);
test_roundtrips::<XyzD65, Srgb>(&rectangular_values);
test_roundtrips::<Oklab, Srgb>(&[
[0., 0., 0.],
[1., 0., 0.],
[0.2, 0.2, -0.1],
[2.0, 0., -0.4],
]);
}
#[test]
fn white_components() {
fn check_white<CS: ColorSpace>() {
assert!(almost_equal::<Srgb>(
Srgb::WHITE_COMPONENTS,
CS::convert::<Srgb>(CS::WHITE_COMPONENTS),
1e-4,
));
assert!(almost_equal::<CS>(
CS::WHITE_COMPONENTS,
Srgb::convert::<CS>(Srgb::WHITE_COMPONENTS),
1e-4,
));
}
check_white::<A98Rgb>();
check_white::<DisplayP3>();
check_white::<Hsl>();
check_white::<Hwb>();
check_white::<Lab>();
check_white::<Lch>();
check_white::<LinearSrgb>();
check_white::<Oklab>();
check_white::<Oklch>();
check_white::<ProphotoRgb>();
check_white::<Rec2020>();
check_white::<Aces2065_1>();
check_white::<AcesCg>();
check_white::<XyzD50>();
check_white::<XyzD65>();
}
#[test]
fn a98rgb_srgb() {
for (srgb, a98) in [
([0.1, 0.2, 0.3], [0.155_114, 0.212_317, 0.301_498]),
([0., 1., 0.], [0.564_972, 1., 0.234_424]),
] {
assert!(almost_equal::<Srgb>(
srgb,
A98Rgb::convert::<Srgb>(a98),
1e-4
));
assert!(almost_equal::<A98Rgb>(
a98,
Srgb::convert::<A98Rgb>(srgb),
1e-4
));
}
}
#[test]
fn prophotorgb_srgb() {
for (srgb, prophoto) in [
([0.1, 0.2, 0.3], [0.133136, 0.147659, 0.223581]),
([0., 1., 0.], [0.540282, 0.927599, 0.304566]),
] {
assert!(almost_equal::<Srgb>(
srgb,
ProphotoRgb::convert::<Srgb>(prophoto),
1e-4
));
assert!(almost_equal::<ProphotoRgb>(
prophoto,
Srgb::convert::<ProphotoRgb>(srgb),
1e-4
));
}
}
#[test]
fn rec2020_srgb() {
for (srgb, rec2020) in [
([0.1, 0.2, 0.3], [0.091284, 0.134169, 0.230056]),
([0.05, 0.1, 0.15], [0.029785, 0.043700, 0.083264]),
([0., 1., 0.], [0.567542, 0.959279, 0.268969]),
] {
assert!(almost_equal::<Srgb>(
srgb,
Rec2020::convert::<Srgb>(rec2020),
1e-4
));
assert!(almost_equal::<Rec2020>(
rec2020,
Srgb::convert::<Rec2020>(srgb),
1e-4
));
}
}
#[test]
fn aces2065_1_srgb() {
for (srgb, aces2065_1) in [
([0.6, 0.5, 0.4], [0.245_59, 0.215_57, 0.145_18]),
([0.0, 0.5, 1.0], [0.259_35, 0.270_89, 0.894_79]),
] {
assert!(almost_equal::<Srgb>(
srgb,
Aces2065_1::convert::<Srgb>(aces2065_1),
1e-4
));
assert!(almost_equal::<Aces2065_1>(
aces2065_1,
Srgb::convert::<Aces2065_1>(srgb),
1e-4
));
}
}
#[test]
fn absolute_conversion() {
assert!(almost_equal::<AcesCg>(
Srgb::convert_absolute::<AcesCg>([0.5, 0.2, 0.4]),
[0.14628284, 0.04714393, 0.13361104],
1e-4,
));
assert!(almost_equal::<XyzD65>(
Srgb::convert_absolute::<XyzD50>([0.5, 0.2, 0.4]),
Srgb::convert::<XyzD65>([0.5, 0.2, 0.4]),
1e-4,
));
}
#[test]
fn chromatic_adaptation() {
assert!(almost_equal::<Srgb>(
XyzD50::convert_absolute::<Srgb>(Srgb::convert::<XyzD50>([0.5, 0.2, 0.4])),
Srgb::chromatically_adapt([0.5, 0.2, 0.4], Chromaticity::D65, Chromaticity::D50),
1e-4,
));
}
#[test]
fn implicit_vs_explicit_chromatic_adaptation() {
fn test<Source: ColorSpace, Dest: ColorSpace>(src: [f32; 3]) {
let convert = Source::convert::<Dest>(src);
let convert_absolute_then_adapt = Dest::chromatically_adapt(
Source::convert_absolute::<Dest>(src),
Source::WHITE_POINT,
Dest::WHITE_POINT,
);
let adapt_then_convert_absolute = Source::convert_absolute::<Dest>(
Source::chromatically_adapt(src, Source::WHITE_POINT, Dest::WHITE_POINT),
);
assert!(almost_equal::<LinearSrgb>(
Dest::to_linear_srgb(convert),
Dest::to_linear_srgb(convert_absolute_then_adapt),
1e-4,
));
assert!(almost_equal::<LinearSrgb>(
Dest::to_linear_srgb(convert),
Dest::to_linear_srgb(adapt_then_convert_absolute),
1e-4,
));
}
test::<Srgb, LinearSrgb>([0.5, 0.2, 0.4]);
test::<Srgb, Lab>([0.5, 0.2, 0.4]);
test::<Srgb, Lch>([0.5, 0.2, 0.4]);
test::<Srgb, Hsl>([0.5, 0.2, 0.4]);
test::<Srgb, Hwb>([0.5, 0.2, 0.4]);
test::<Srgb, Oklab>([0.5, 0.2, 0.4]);
test::<Srgb, Oklch>([0.5, 0.2, 0.4]);
test::<Srgb, DisplayP3>([0.5, 0.2, 0.4]);
test::<Srgb, A98Rgb>([0.5, 0.2, 0.4]);
test::<Srgb, ProphotoRgb>([0.5, 0.2, 0.4]);
test::<Srgb, Rec2020>([0.5, 0.2, 0.4]);
test::<Srgb, Aces2065_1>([0.5, 0.2, 0.4]);
test::<Srgb, AcesCg>([0.5, 0.2, 0.4]);
test::<Srgb, XyzD50>([0.5, 0.2, 0.4]);
test::<Srgb, XyzD65>([0.5, 0.2, 0.4]);
test::<AcesCg, Srgb>([0.5, 0.2, 0.4]);
test::<AcesCg, LinearSrgb>([0.5, 0.2, 0.4]);
test::<AcesCg, Lab>([0.5, 0.2, 0.4]);
test::<AcesCg, Lch>([0.5, 0.2, 0.4]);
test::<AcesCg, Hsl>([0.5, 0.2, 0.4]);
test::<AcesCg, Hwb>([0.5, 0.2, 0.4]);
test::<AcesCg, Oklab>([0.5, 0.2, 0.4]);
test::<AcesCg, Oklch>([0.5, 0.2, 0.4]);
test::<AcesCg, DisplayP3>([0.5, 0.2, 0.4]);
test::<AcesCg, A98Rgb>([0.5, 0.2, 0.4]);
test::<AcesCg, ProphotoRgb>([0.5, 0.2, 0.4]);
test::<AcesCg, Rec2020>([0.5, 0.2, 0.4]);
test::<AcesCg, Aces2065_1>([0.5, 0.2, 0.4]);
test::<AcesCg, XyzD50>([0.5, 0.2, 0.4]);
test::<AcesCg, XyzD65>([0.5, 0.2, 0.4]);
}
}