kolor-64 0.1.8

Color conversions for games & interactive applications.
Documentation
use super::{conversion::ColorConversion, transform::ColorTransform};
use crate::{FType, Vec3};
#[cfg(all(feature = "glam", feature = "f64"))]
use glam::const_dvec3;
#[cfg(all(feature = "glam", feature = "f32"))]
use glam::const_vec3;
#[cfg(feature = "serde1")]
use serde::{Deserialize, Serialize};

/// A [TransformFn] identifies an invertible mapping of colors in a linear [ColorSpace].
#[repr(u8)]
#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
pub enum TransformFn {
    NONE,
    /// The sRGB transfer functions (aka "gamma correction")
    sRGB,
    /// Oklab conversion from xyz
    Oklab,
    /// Oklch (Oklab's LCh variant) conversion from xyz
    Oklch,
    /// CIE xyY transform
    CIE_xyY,
    /// CIELAB transform
    CIELAB,
    /// CIELCh transform
    CIELCh,
    /// CIE 1960 UCS transform
    CIE_1960_UCS,
    /// CIE 1960 UCS transform in uvV form
    CIE_1960_UCS_uvV,
    /// CIE 1964 UVW transform
    CIE_1964_UVW,
    /// CIE 1976 Luv transform
    CIE_1976_Luv,
    /// (Hue, Saturation, Lightness),
    /// where L is defined as the average of the largest and smallest color components
    HSL,
    /// (Hue, Saturation, Value),
    /// where V is defined as the largest component of a color
    HSV,
    /// (Hue, Saturation, Intensity),
    /// where I is defined as the average of the three components
    HSI,
    /// BT.2100 ICtCp with PQ transfer function
    ICtCp_PQ,
    /// BT.2100 ICtCp with HLG transfer function
    ICtCp_HLG,
    /// The BT.601/BT.709/BT.2020 (they are equivalent) OETF and inverse.
    BT_601,
    /// SMPTE ST 2084:2014 aka "Perceptual Quantizer" transfer functions used in BT.2100
    /// for digitally created/distributed HDR content.
    PQ,
    // ACEScc is a logarithmic transform
    // ACES_CC,
    // ACEScct is a logarithmic transform with toe
    // ACES_CCT,
}
impl TransformFn {
    pub const ENUM_COUNT: TransformFn = TransformFn::ICtCp_HLG;
}
/// [RGBPrimaries] is a set of primary colors picked to define an RGB color space.
#[repr(u8)]
#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
pub enum RGBPrimaries {
    // Primaries
    NONE,
    /// BT.709 is the sRGB primaries.
    BT_709,
    // BT_2020 uses the same primaries as BT_2100
    BT_2020,
    AP0,
    AP1,
    /// P3 is the primaries for DCI-P3 and the variations with different white points
    P3,
    ADOBE_1998,
    ADOBE_WIDE,
    APPLE,
    PRO_PHOTO,
    CIE_RGB,
    /// The reference XYZ color space
    CIE_XYZ,
}
impl RGBPrimaries {
    pub const ENUM_COUNT: RGBPrimaries = RGBPrimaries::CIE_XYZ;
    pub const fn values(&self) -> &[[FType; 2]; 3] {
        match self {
            Self::NONE => &[[0.0; 2]; 3],
            Self::BT_709 => &[[0.64, 0.33], [0.30, 0.60], [0.15, 0.06]],
            Self::BT_2020 => &[[0.708, 0.292], [0.17, 0.797], [0.131, 0.046]],
            Self::AP0 => &[[0.7347, 0.2653], [0.0000, 1.0000], [0.0001, -0.0770]],
            Self::AP1 => &[[0.713, 0.293], [0.165, 0.830], [0.128, 0.044]],
            Self::ADOBE_1998 => &[[0.64, 0.33], [0.21, 0.71], [0.15, 0.06]],
            Self::ADOBE_WIDE => &[[0.735, 0.265], [0.115, 0.826], [0.157, 0.018]],
            Self::PRO_PHOTO => &[
                [0.734699, 0.265301],
                [0.159597, 0.840403],
                [0.036598, 0.000105],
            ],
            Self::APPLE => &[[0.625, 0.34], [0.28, 0.595], [0.155, 0.07]],
            Self::P3 => &[[0.680, 0.320], [0.265, 0.690], [0.150, 0.060]],
            Self::CIE_RGB => &[[0.7350, 0.2650], [0.2740, 0.7170], [0.1670, 0.0090]],
            Self::CIE_XYZ => &[[1.0, 0.0], [0.0, 1.0], [0.0, 0.0]],
        }
    }
}

/// A [WhitePoint] defines the color "white" in an RGB color system.
/// White points are derived from an "illuminant" which are defined
/// as some reference lighting condition based on a Spectral Power Distribution.
#[repr(u8)]
#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
pub enum WhitePoint {
    NONE,
    /// Incandescent/tungsten
    A,
    /// Old direct sunlight at noon
    B,
    /// Old daylight
    C,
    /// Equal energy
    E,
    /// ICC profile PCS
    D50,
    /// Mid-morning daylight
    D55,
    D60,
    /// Daylight, sRGB, Adobe-RGB
    D65,
    /// North sky daylight
    D75,
    /// P3-DCI white point, sort of greenish
    P3_DCI,
    /// Cool fluorescent
    F2,
    /// Daylight fluorescent, D65 simulator
    F7,
    /// Ultralume 40, Philips TL84
    F11,
}
impl WhitePoint {
    pub const ENUM_COUNT: WhitePoint = WhitePoint::F11;
    pub const fn values(&self) -> &'static [FType; 3] {
        match self {
            Self::NONE => &[0.0, 0.0, 0.0],
            Self::A => &[1.09850, 1.00000, 0.35585],
            Self::B => &[0.99072, 1.00000, 0.85223],
            Self::C => &[0.98074, 1.00000, 1.18232],
            Self::D50 => &[0.96422, 1.00000, 0.82521],
            Self::D55 => &[0.95682, 1.00000, 0.92149],
            Self::D60 => &[0.9523, 1.00000, 1.00859],
            Self::D65 => &[0.95047, 1.00000, 1.08883],
            Self::D75 => &[0.94972, 1.00000, 1.22638],
            Self::P3_DCI => &[0.89458689458, 1.00000, 0.95441595441],
            Self::E => &[1.00000, 1.00000, 1.00000],
            Self::F2 => &[0.99186, 1.00000, 0.67393],
            Self::F7 => &[0.95041, 1.00000, 1.08747],
            Self::F11 => &[1.00962, 1.00000, 0.64350],
        }
    }
}

/// A color space defined in data by its [Primaries][RGBPrimaries], [white point][WhitePoint], and an optional [invertible transform function][TransformFn].
///
/// See [spaces][crate::spaces] for defined color spaces.
///
/// [ColorSpace] assumes that a color space is one of
/// - the CIE XYZ color space
/// - an RGB color space
/// - a color space which may be defined as an invertible mapping from one the above ([TransformFn])
///
/// An example of a [TransformFn] is the sRGB "opto-eletronic transfer function", or
/// "gamma compensation".
///
/// `kolor` makes the distinction between "linear" and "non-linear" color spaces, where a linear
/// color space can be defined as a linear transformation from the CIE XYZ color space.
///
/// [ColorSpace] contains a reference [WhitePoint] to represent a color space's reference illuminant.
///
/// A linear RGB [ColorSpace] can be thought of as defining a relative coordinate system in the CIE XYZ
/// color coordinate space, where three RGB primaries each define an axis pointing from
/// the black point (0,0,0) in CIE XYZ.
///
/// Non-linear [ColorSpace]s - such as sRGB with gamma compensation applied - are defined as a non-linear mapping from a linear
/// [ColorSpace]'s coordinate system.
#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct ColorSpace {
    primaries: RGBPrimaries,
    white_point: WhitePoint,
    transform_fn: TransformFn,
}
impl ColorSpace {
    pub const fn new(
        primaries: RGBPrimaries,
        white_point: WhitePoint,
        transform_fn: TransformFn,
    ) -> Self {
        Self {
            primaries,
            white_point,
            transform_fn,
        }
    }
    pub(crate) const fn linear(primaries: RGBPrimaries, white_point: WhitePoint) -> Self {
        Self {
            primaries,
            white_point,
            transform_fn: TransformFn::NONE,
        }
    }
    /// Whether the color space has a non-linear transform applied
    pub fn is_linear(&self) -> bool {
        self.transform_fn == TransformFn::NONE
    }
    pub fn as_linear(&self) -> Self {
        Self {
            primaries: self.primaries,
            white_point: self.white_point,
            transform_fn: TransformFn::NONE,
        }
    }
    pub fn primaries(&self) -> RGBPrimaries {
        self.primaries
    }
    pub fn white_point(&self) -> WhitePoint {
        self.white_point
    }
    pub fn transform_function(&self) -> TransformFn {
        self.transform_fn
    }

    /// Creates a new color space with the primaries and white point from `this`,
    /// but with the provided [TransformFn].
    pub fn with_transform(&self, new_transform: TransformFn) -> Self {
        Self {
            primaries: self.primaries,
            white_point: self.white_point,
            transform_fn: new_transform,
        }
    }

    /// Creates a new color space with the transform function and white point from `this`,
    /// but with the provided [WhitePoint].
    pub fn with_whitepoint(&self, new_wp: WhitePoint) -> Self {
        Self {
            primaries: self.primaries,
            white_point: new_wp,
            transform_fn: self.transform_fn,
        }
    }

    /// Creates a new color space with the primaries and transform function from `this`,
    /// but with the provided [RGBPrimaries].
    pub fn with_primaries(&self, primaries: RGBPrimaries) -> Self {
        Self {
            primaries,
            white_point: self.white_point,
            transform_fn: self.transform_fn,
        }
    }

    /// Creates a CIE LAB color space using this space's white point.
    pub fn to_cielab(&self) -> Self {
        Self::new(RGBPrimaries::CIE_XYZ, self.white_point, TransformFn::CIELAB)
    }

    /// Creates a CIE uvV color space using this space's white point.
    #[allow(non_snake_case)]
    pub fn to_cie_xyY(&self) -> Self {
        Self::new(
            RGBPrimaries::CIE_XYZ,
            self.white_point,
            TransformFn::CIE_xyY,
        )
    }

    /// Creates a CIE LCh color space using this space's white point.
    pub fn to_cielch(&self) -> Self {
        Self::new(RGBPrimaries::CIE_XYZ, self.white_point, TransformFn::CIELCh)
    }
}
#[allow(non_upper_case_globals)]
pub mod color_spaces {
    use super::*;

    /// Linear sRGB is a linear encoding in [BT.709 primaries][RGBPrimaries::BT_709]
    /// with a [D65 whitepoint.][WhitePoint::D65]
    /// Linear sRGB is equivalent to [BT_709].
    pub const LINEAR_SRGB: ColorSpace = ColorSpace::linear(RGBPrimaries::BT_709, WhitePoint::D65);

    /// Encoded sRGB is [Linear sRGB][LINEAR_SRGB] with the [sRGB OETF](TransformFn::sRGB) applied (also called "gamma-compressed").
    pub const ENCODED_SRGB: ColorSpace =
        ColorSpace::new(RGBPrimaries::BT_709, WhitePoint::D65, TransformFn::sRGB);

    /// BT.709 is a linear encoding in [BT.709 primaries][RGBPrimaries::BT_709]
    /// with a [D65 whitepoint.][WhitePoint::D65]. It's equivalent to [Linear sRGB][LINEAR_SRGB]
    pub const BT_709: ColorSpace = ColorSpace::linear(RGBPrimaries::BT_709, WhitePoint::D65);

    /// Encoded BT.709 is [BT.709](BT_709) with the [BT.709 OETF](TransformFn::BT_601) applied.
    pub const ENCODED_BT_709: ColorSpace =
        ColorSpace::new(RGBPrimaries::BT_709, WhitePoint::D65, TransformFn::BT_601);

    /// ACEScg is a linear encoding in [AP1 primaries][RGBPrimaries::AP1]
    /// with a [D60 whitepoint][WhitePoint::D60].
    pub const ACES_CG: ColorSpace = ColorSpace::linear(RGBPrimaries::AP1, WhitePoint::D60);

    /// ACES2065-1 is a linear encoding in [AP0 primaries][RGBPrimaries::AP0] with a [D60 whitepoint][WhitePoint::D60].
    pub const ACES2065_1: ColorSpace = ColorSpace::linear(RGBPrimaries::AP0, WhitePoint::D60);

    /// CIE RGB is the original RGB space, defined in [CIE RGB primaries][RGBPrimaries::CIE_RGB]
    /// with white point [E][WhitePoint::E].
    pub const CIE_RGB: ColorSpace = ColorSpace::linear(RGBPrimaries::CIE_RGB, WhitePoint::E);

    /// CIE XYZ reference color space. Uses [CIE XYZ primaries][RGBPrimaries::CIE_XYZ]
    /// with white point [D65][WhitePoint::D65].
    pub const CIE_XYZ: ColorSpace = ColorSpace::linear(RGBPrimaries::CIE_XYZ, WhitePoint::D65);

    /// BT.2020 is a linear encoding in [BT.2020 primaries][RGBPrimaries::BT_2020]
    /// with a [D65 white point][WhitePoint::D65]
    /// BT.2100 has the same linear color space as BT.2020.
    pub const BT_2020: ColorSpace = ColorSpace::linear(RGBPrimaries::BT_2020, WhitePoint::D65);

    /// Encoded BT.2020 is [BT.2020](BT_2020) with the [BT.2020 OETF][TransformFn::BT_601] applied.
    pub const ENCODED_BT_2020: ColorSpace =
        ColorSpace::new(RGBPrimaries::BT_2020, WhitePoint::D65, TransformFn::BT_601);

    /// Encoded BT.2100 PQ is [BT.2020](BT_2020) (equivalent to the linear BT.2100 space) with
    /// the [Perceptual Quantizer inverse EOTF][TransformFn::PQ] applied.
    pub const ENCODED_BT_2100_PQ: ColorSpace =
        ColorSpace::new(RGBPrimaries::BT_2020, WhitePoint::D65, TransformFn::PQ);

    /// Oklab is a non-linear, perceptual encoding in [XYZ][RGBPrimaries::CIE_XYZ],
    /// with a [D65 whitepoint][WhitePoint::D65].
    ///
    /// Oklab's perceptual qualities make it a very attractive color space for performing
    /// blend operations between two colors which you want to be perceptually pleasing.
    /// See [this article](https://bottosson.github.io/posts/oklab/)
    /// for more on why you might want to use the Oklab colorspace.
    pub const OKLAB: ColorSpace =
        ColorSpace::new(RGBPrimaries::CIE_XYZ, WhitePoint::D65, TransformFn::Oklab);

    /// Oklch is a non-linear, perceptual encoding in [XYZ][RGBPrimaries::CIE_XYZ],
    /// with a [D65 whitepoint][WhitePoint::D65]. It is a variant of [Oklab][OKLAB]
    /// with LCh coordinates intead of Lab.
    ///
    /// Oklch's qualities make it a very attractive color space for performing
    /// computational modifications to a color. You can think of it as an improved
    /// version of an HSL/HSV-style color space. See [this article](https://bottosson.github.io/posts/oklab/)
    /// for more on why you might want to use the Oklch colorspace.
    pub const OKLCH: ColorSpace =
        ColorSpace::new(RGBPrimaries::CIE_XYZ, WhitePoint::D65, TransformFn::Oklch);

    /// ICtCp_PQ is a non-linear encoding in [BT.2020 primaries][RGBPrimaries::BT_2020],
    /// with a [D65 whitepoint][WhitePoint::D65], using the PQ transfer function
    pub const ICtCp_PQ: ColorSpace = ColorSpace::new(
        RGBPrimaries::BT_2020,
        WhitePoint::D65,
        TransformFn::ICtCp_PQ,
    );
    /// ICtCp_HLG is a non-linear encoding in [BT.2020 primaries][RGBPrimaries::BT_2020],
    /// with a [D65 whitepoint][WhitePoint::D65], using the HLG transfer function
    pub const ICtCp_HLG: ColorSpace = ColorSpace::new(
        RGBPrimaries::BT_2020,
        WhitePoint::D65,
        TransformFn::ICtCp_HLG,
    );

    /// Encoded Display P3 is [Display P3][DISPLAY_P3] with the [sRGB OETF](TransformFn::sRGB) applied.
    pub const ENCODED_DISPLAY_P3: ColorSpace =
        ColorSpace::new(RGBPrimaries::P3, WhitePoint::D65, TransformFn::sRGB);

    /// Display P3 by Apple is a linear encoding in [P3 primaries][RGBPrimaries::P3]
    /// with a [D65 white point][WhitePoint::D65]
    pub const DISPLAY_P3: ColorSpace = ColorSpace::linear(RGBPrimaries::P3, WhitePoint::D65);

    /// P3-D60 (ACES Cinema) is a linear encoding in [P3 primaries][RGBPrimaries::P3]
    /// with a [D60 white point][WhitePoint::D60]
    pub const P3_D60: ColorSpace = ColorSpace::linear(RGBPrimaries::P3, WhitePoint::D60);

    /// P3-DCI (Theater) is a linear encoding in [P3 primaries][RGBPrimaries::P3]
    /// with a [P3-DCI white point][WhitePoint::P3_DCI]
    pub const P3_THEATER: ColorSpace = ColorSpace::linear(RGBPrimaries::P3, WhitePoint::P3_DCI);

    /// Adobe RGB (1998) is a linear encoding in [Adobe 1998 primaries][RGBPrimaries::ADOBE_1998]
    /// with a [D65 white point][WhitePoint::D65]
    pub const ADOBE_1998: ColorSpace =
        ColorSpace::linear(RGBPrimaries::ADOBE_1998, WhitePoint::D65);

    /// Adobe Wide Gamut RGB is a linear encoding in [Adobe Wide primaries][RGBPrimaries::ADOBE_WIDE]
    /// with a [D50 white point][WhitePoint::D50]
    pub const ADOBE_WIDE: ColorSpace =
        ColorSpace::linear(RGBPrimaries::ADOBE_WIDE, WhitePoint::D50);

    /// Pro Photo RGB is a linear encoding in [Pro Photo primaries][RGBPrimaries::PRO_PHOTO]
    /// with a [D50 white point][WhitePoint::D50]
    pub const PRO_PHOTO: ColorSpace = ColorSpace::linear(RGBPrimaries::PRO_PHOTO, WhitePoint::D50);

    /// Apple RGB is a linear encoding in [Apple primaries][RGBPrimaries::APPLE]
    /// with a [D65 white point][WhitePoint::D65]
    pub const APPLE: ColorSpace = ColorSpace::linear(RGBPrimaries::APPLE, WhitePoint::D65);

    /// Array containing all built-in color spaces.
    pub const ALL_COLOR_SPACES: [ColorSpace; 22] = [
        color_spaces::LINEAR_SRGB,
        color_spaces::ENCODED_SRGB,
        color_spaces::BT_709,
        color_spaces::ENCODED_BT_709,
        color_spaces::BT_2020,
        color_spaces::ENCODED_BT_2020,
        color_spaces::ENCODED_BT_2100_PQ,
        color_spaces::ACES_CG,
        color_spaces::ACES2065_1,
        color_spaces::CIE_RGB,
        color_spaces::CIE_XYZ,
        color_spaces::OKLAB,
        color_spaces::ICtCp_PQ,
        color_spaces::ICtCp_HLG,
        color_spaces::PRO_PHOTO,
        color_spaces::APPLE,
        color_spaces::P3_D60,
        color_spaces::P3_THEATER,
        color_spaces::DISPLAY_P3,
        color_spaces::ENCODED_DISPLAY_P3,
        color_spaces::ADOBE_1998,
        color_spaces::ADOBE_WIDE,
    ];
}

/// [Color] is a 3-component vector defined in a [ColorSpace].
#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct Color {
    pub value: Vec3,
    pub space: ColorSpace,
}
impl Color {
    pub const fn new(x: FType, y: FType, z: FType, space: ColorSpace) -> Self {
        #[cfg(all(feature = "glam", feature = "f64"))]
        return Self {
            value: const_dvec3!([x, y, z]),
            space,
        };
        #[cfg(all(feature = "glam", feature = "f32"))]
        return Self {
            value: const_vec3!([x, y, z]),
            space,
        };
        #[cfg(not(feature = "glam"))]
        return Self {
            value: Vec3::new(x, y, z),
            space,
        };
    }
    pub const fn space(&self) -> ColorSpace {
        self.space
    }

    /// Equivalent to `Color::new(x, y, z, kolor::spaces::ENCODED_SRGB)`
    pub const fn srgb(x: FType, y: FType, z: FType) -> Self {
        #[cfg(all(feature = "glam", feature = "f64"))]
        return Self {
            value: const_dvec3!([x, y, z]),
            space: color_spaces::ENCODED_SRGB,
        };
        #[cfg(all(feature = "glam", feature = "f32"))]
        return Self {
            value: const_vec3!([x, y, z]),
            space: color_spaces::ENCODED_SRGB,
        };
        #[cfg(not(feature = "glam"))]
        return Self {
            value: Vec3::new(x, y, z),
            space: color_spaces::ENCODED_SRGB,
        };
    }

    /// Returns a [Color] with this color converted into the provided [ColorSpace].
    pub fn to(&self, space: ColorSpace) -> Color {
        let conversion = ColorConversion::new(self.space, space);
        let new_color = conversion.convert(self.value);
        Color {
            space,
            value: new_color,
        }
    }
    pub fn to_linear(&self) -> Color {
        if self.space.is_linear() {
            *self
        } else {
            let transform = ColorTransform::new(self.space.transform_function(), TransformFn::NONE)
                .unwrap_or_else(|| {
                    panic!(
                        "expected transform for {:?}",
                        self.space.transform_function()
                    )
                });
            let new_color_value = transform.apply(self.value, self.space().white_point);
            Self {
                value: new_color_value,
                space: self.space.as_linear(),
            }
        }
    }
}

#[cfg(test)]
#[allow(non_snake_case)]
mod test {
    use super::*;
    use crate::details::conversion::LinearColorConversion;
    use color_spaces as spaces;
    #[test]
    fn linear_srgb_to_aces_cg() {
        let conversion = LinearColorConversion::new(spaces::LINEAR_SRGB, spaces::ACES_CG);
        let result = conversion.convert(Vec3::new(0.35, 0.2, 0.8));
        assert!(result.abs_diff_eq(Vec3::new(0.32276854, 0.21838512, 0.72592676), 0.001));
    }

    #[test]
    fn linear_srgb_to_aces_2065_1() {
        let conversion = ColorConversion::new(spaces::LINEAR_SRGB, spaces::ACES2065_1);
        let result = conversion.convert(Vec3::new(0.35, 0.2, 0.8));
        assert!(result.abs_diff_eq(Vec3::new(0.3741492, 0.27154857, 0.7261116), 0.001));
    }

    #[test]
    fn linear_srgb_to_srgb() {
        let transform = ColorTransform::new(TransformFn::NONE, TransformFn::sRGB).unwrap();
        let test = Vec3::new(0.35, 0.1, 0.8);
        let result = transform.apply(test, WhitePoint::D65);
        let expected = Vec3::new(0.6262097, 0.34919018, 0.9063317);
        assert!(
            result.abs_diff_eq(expected, 0.001),
            "{} != {}",
            result,
            expected
        );
    }

    // #[test]
    // fn working_space_conversions() {
    //     // just make sure we aren't missing a conversion
    //     for src in &WORKING_SPACE_BY_WHITE_POINT {
    //         for dst in &WORKING_SPACE_BY_WHITE_POINT {
    //             let conversion = LinearColorConversion::new(*src, *dst);
    //             let mut result = Vec3::new(0.35, 0.2, 0.8);
    //             conversion.apply(&mut result);
    //         }
    //     }
    // }

    #[test]
    fn aces_cg_to_srgb() {
        let conversion = ColorConversion::new(spaces::ACES_CG, spaces::ENCODED_SRGB);
        let result = conversion.convert(Vec3::new(0.35, 0.1, 0.8));
        let expected = Vec3::new(0.713855624199, 0.271821975708, 0.955197274685);
        assert!(
            result.abs_diff_eq(expected, 0.01),
            "{} != {}",
            result,
            expected
        );
    }
}