colr-types 0.3.1

Color model ZSTs and marker traits for colr.
Documentation
//! CIE standard illuminants and reference white points.
//!
//! White point XYZ values are Y=1 normalized tristimulus. Values for the
//! Cie1931 observer are from ASTM E308 and CIE 015:2018. Values for the
//! Cie1964 observer are from CIE 015:2018, Table 1.
//!
//! ACES white point per Academy TB-2018-001 and SMPTE ST 2065-1:2021.
//!
//! SPD data is from CIE 015:2018 Tables T.1 and T.2, normalized to 1.0 at
//! 560 nm. The `spd` const fn on each illuminant struct linearly interpolates
//! the 5 nm source table onto any WavelengthGrid and can be used in statics.

#![allow(clippy::excessive_precision)]

use core::marker::PhantomData;

use crate::model::WavelengthGrid;
use crate::observer::{Cie1931, Cie1964, StandardObserver};

/// A CIE standard illuminant defining a reference white point under a
/// specific standard observer.
///
/// Integrating the same illuminant SPD against different observer CMFs yields
/// different tristimulus values, so white point constants are observer-relative.
pub trait Illuminant: 'static {
    /// The standard observer these white point values are relative to.
    type Observer: StandardObserver;

    /// xy chromaticity of the reference white under this observer.
    const WHITE_POINT_XY: [f32; 2];

    /// XYZ reference white normalized to Y = 1 under this observer.
    const WHITE_POINT_XYZ: [f32; 3];
}

/// CIE standard illuminant D65 (~6504 K) under observer O.
///
/// Reference white for sRGB (IEC 61966-2-1), Rec. 709, Display P3, and
/// Rec. 2020 (ITU-R BT.2020).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct D65<O: StandardObserver = Cie1931>(PhantomData<O>);

impl Illuminant for D65<Cie1931> {
    type Observer = Cie1931;
    const WHITE_POINT_XY: [f32; 2] = [0.3127, 0.3290];
    const WHITE_POINT_XYZ: [f32; 3] = [0.95047, 1.00000, 1.08883];
}

impl Illuminant for D65<Cie1964> {
    type Observer = Cie1964;
    /// CIE 015:2018 Table 1.
    const WHITE_POINT_XY: [f32; 2] = [0.3138, 0.3310];
    /// CIE 015:2018 Table 1.
    const WHITE_POINT_XYZ: [f32; 3] = [0.94811, 1.00000, 1.07304];
}

impl D65<Cie1931> {
    /// CIE D65 relative SPD at 5 nm, 380-780 nm (81 bands), normalized to
    /// 1.0 at 560 nm. Source: CIE 015:2018 Table T.2.
    #[rustfmt::skip]
    pub const SPD_5NM: &'static [f32; 81] = &[
        0.499755, 0.523118, 0.546482, 0.687015, 0.827549, 0.871204, 0.914860, 0.924589, 0.934318, 0.900570,
        0.866823, 0.957736, 1.048650, 1.109360, 1.170080, 1.174100, 1.178120, 1.163360, 1.148610, 1.153920,
        1.159230, 1.123670, 1.088110, 1.090820, 1.093540, 1.085780, 1.078020, 1.062960, 1.047900, 1.062390,
        1.076890, 1.060470, 1.044050, 1.042250, 1.040460, 1.020230, 1.000000, 0.981671, 0.963342, 0.960611,
        0.957880, 0.922368, 0.886856, 0.893459, 0.900062, 0.898026, 0.895991, 0.886489, 0.876987, 0.854936,
        0.832886, 0.834939, 0.836992, 0.818630, 0.800268, 0.801207, 0.802146, 0.812462, 0.822778, 0.802810,
        0.782842, 0.740027, 0.697213, 0.706652, 0.716091, 0.729790, 0.743490, 0.679765, 0.616040, 0.657448,
        0.698856, 0.724862, 0.750869, 0.693398, 0.635927, 0.550054, 0.464182, 0.566118, 0.668054, 0.650942,
        0.633830,
    ];

    /// Sample the D65 SPD onto wavelength grid `G`.
    ///
    /// Linearly interpolates the 5 nm source table. Bands outside 380-780 nm
    /// are zero. The result is suitable for use in a `static` initializer.
    pub const fn spd<const BANDS: usize, G: WavelengthGrid<BANDS>>() -> [f32; BANDS] {
        let mut out = [0.0f32; BANDS];
        let mut i = 0;
        while i < BANDS {
            let nm = G::START_NM + i as f32 * G::STEP_NM;
            if nm >= 380.0 && nm <= 780.0 {
                let t = (nm - 380.0) / 5.0;
                let lo = t as usize;
                let hi = if lo < 80 { lo + 1 } else { 80 };
                let frac = t - lo as f32;
                out[i] = Self::SPD_5NM[lo] * (1.0 - frac) + Self::SPD_5NM[hi] * frac;
            }
            i += 1;
        }
        out
    }
}

/// CIE standard illuminant D50 (~5003 K) under observer O.
///
/// D50 is the reference white for the ICC profile connection space
/// (ICC.1:2022) and ProPhoto RGB (ROMM RGB, ISO 22028-2), both defined
/// under Cie1931. Use `D50<Cie1964>` for surface color evaluation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct D50<O: StandardObserver = Cie1931>(PhantomData<O>);

impl Illuminant for D50<Cie1931> {
    type Observer = Cie1931;
    const WHITE_POINT_XY: [f32; 2] = [0.3457, 0.3585];
    const WHITE_POINT_XYZ: [f32; 3] = [0.96422, 1.00000, 0.82521];
}

impl Illuminant for D50<Cie1964> {
    type Observer = Cie1964;
    /// CIE 015:2018 Table 1.
    const WHITE_POINT_XY: [f32; 2] = [0.3477, 0.3595];
    /// CIE 015:2018 Table 1.
    const WHITE_POINT_XYZ: [f32; 3] = [0.96720, 1.00000, 0.81427];
}

impl D50<Cie1931> {
    /// CIE D50 relative SPD at 5 nm, 380-780 nm (81 bands), normalized to
    /// 1.0 at 560 nm. Source: CIE 015:2018 Table T.1.
    #[rustfmt::skip]
    pub const SPD_5NM: &'static [f32; 81] = &[
        0.266024, 0.314300, 0.333033, 0.443867, 0.588167, 0.664700, 0.741233, 0.771244, 0.801267, 0.769711,
        0.738156, 0.865411, 0.992667, 1.056422, 1.120189, 1.116278, 1.112367, 1.127200, 1.142033, 1.139233,
        1.136433, 1.111322, 1.086211, 1.089200, 1.092189, 1.083844, 1.075489, 1.064311, 1.053144, 1.067956,
        1.082767, 1.064311, 1.045833, 1.049189, 1.052544, 1.026278, 1.000000, 0.994489, 0.988978, 0.986311,
        0.983644, 0.949211, 0.914778, 0.924544, 0.934311, 0.926900, 0.919478, 0.909322, 0.899167, 0.877911,
        0.856656, 0.858400, 0.860144, 0.844711, 0.829278, 0.829922, 0.830567, 0.839733, 0.848900, 0.830722,
        0.812533, 0.770511, 0.728478, 0.738467, 0.748456, 0.763511, 0.778578, 0.715111, 0.651644, 0.703344,
        0.755044, 0.791544, 0.828056, 0.765333, 0.702611, 0.611144, 0.519689, 0.638233, 0.756789, 0.738189,
        0.719600,
    ];

    /// Sample the D50 SPD onto wavelength grid `G`.
    ///
    /// Linearly interpolates the 5 nm source table. Bands outside 380-780 nm
    /// are zero. The result is suitable for use in a `static` initializer.
    pub const fn spd<const BANDS: usize, G: WavelengthGrid<BANDS>>() -> [f32; BANDS] {
        let mut out = [0.0f32; BANDS];
        let mut i = 0;
        while i < BANDS {
            let nm = G::START_NM + i as f32 * G::STEP_NM;
            if nm >= 380.0 && nm <= 780.0 {
                let t = (nm - 380.0) / 5.0;
                let lo = t as usize;
                let hi = if lo < 80 { lo + 1 } else { 80 };
                let frac = t - lo as f32;
                out[i] = Self::SPD_5NM[lo] * (1.0 - frac) + Self::SPD_5NM[hi] * frac;
            }
            i += 1;
        }
        out
    }
}

/// CIE standard illuminant D60 (~6004 K) under observer O.
///
/// A true CIE daylight illuminant at 6000 K nominal CCT. Distinct from
/// the ACES white point. See [`AcesWhitePoint`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct D60<O: StandardObserver = Cie1931>(PhantomData<O>);

impl Illuminant for D60<Cie1931> {
    type Observer = Cie1931;
    const WHITE_POINT_XY: [f32; 2] = [0.32163, 0.33774];
    const WHITE_POINT_XYZ: [f32; 3] = [0.95230, 1.00000, 1.00856];
}

impl Illuminant for D60<Cie1964> {
    type Observer = Cie1964;
    /// CIE 015:2018 Table 1.
    const WHITE_POINT_XY: [f32; 2] = [0.3223, 0.3348];
    /// CIE 015:2018 Table 1.
    const WHITE_POINT_XYZ: [f32; 3] = [0.95002, 1.00000, 1.00350];
}

/// DCI white point for theatrical projection (SMPTE EG 432-1).
///
/// Not a CIE D-series illuminant. Defined in Cie1931 XYZ. Used only for
/// theatrical DCI-P3 projection; consumer Display P3 uses D65 instead.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DciWhite;

impl Illuminant for DciWhite {
    type Observer = Cie1931;
    const WHITE_POINT_XY: [f32; 2] = [0.3140, 0.3510];
    const WHITE_POINT_XYZ: [f32; 3] = [0.89459, 1.00000, 0.95442];
}

/// ACES white point per SMPTE ST 2065-1:2021 and Academy TB-2018-001.
///
/// Commonly called "D60" but is not a true CIE D-series illuminant. Defined
/// in Cie1931 XYZ. Use [`D60`] for the true CIE D60 illuminant.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AcesWhitePoint;

impl Illuminant for AcesWhitePoint {
    type Observer = Cie1931;
    const WHITE_POINT_XY: [f32; 2] = [0.32168, 0.33767];
    const WHITE_POINT_XYZ: [f32; 3] = [0.95265, 1.00000, 1.00883];
}