colr 0.1.0

Type-safe, zero-cost color science library with compile-time color space transforms
Documentation
//! Compile-time matrix arithmetic ([`Mat3`]) and transcendental math
//! injection ([`MathState`]).

#[cfg(all(not(feature = "std"), not(feature = "libm")))]
compile_error!(
    "color requires either the `std` or `libm` feature. \
     Add color = {{ features = [\"std\"] }} to your Cargo.toml."
);

/// Injects transcendental math as a zero-sized type parameter.
///
/// Operates on `f32` scalars. Callers apply it channel-by-channel.
/// No runtime overhead beyond the underlying function calls.
pub trait MathState: 'static {
    /// base^exp.
    fn powf(base: f32, exp: f32) -> f32;
    /// Cube root.
    fn cbrt(v: f32) -> f32;
    /// Round to nearest integer.
    fn round(v: f32) -> f32;
    /// Square root.
    fn sqrt(v: f32) -> f32;
    /// Natural logarithm.
    fn ln(v: f32) -> f32;
    /// Natural exponential.
    fn exp(v: f32) -> f32;
    /// Sine (radians).
    fn sin(v: f32) -> f32;
    /// Cosine (radians).
    fn cos(v: f32) -> f32;
    /// atan2(y, x).
    fn atan2(y: f32, x: f32) -> f32;
}

/// Math provider using the Rust standard library.
#[cfg(feature = "std")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct StdMath;

#[cfg(feature = "std")]
impl MathState for StdMath {
    #[inline(always)]
    fn powf(b: f32, e: f32) -> f32 {
        b.powf(e)
    }
    #[inline(always)]
    fn cbrt(v: f32) -> f32 {
        v.cbrt()
    }
    #[inline(always)]
    fn round(v: f32) -> f32 {
        v.round()
    }
    #[inline(always)]
    fn sqrt(v: f32) -> f32 {
        v.sqrt()
    }
    #[inline(always)]
    fn ln(v: f32) -> f32 {
        v.ln()
    }
    #[inline(always)]
    fn exp(v: f32) -> f32 {
        v.exp()
    }
    #[inline(always)]
    fn sin(v: f32) -> f32 {
        v.sin()
    }
    #[inline(always)]
    fn cos(v: f32) -> f32 {
        v.cos()
    }
    #[inline(always)]
    fn atan2(y: f32, x: f32) -> f32 {
        y.atan2(x)
    }
}

/// Math provider using `libm` for `no_std` targets.
#[cfg(feature = "libm")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LibmMath;

#[cfg(feature = "libm")]
impl MathState for LibmMath {
    #[inline(always)]
    fn powf(b: f32, e: f32) -> f32 {
        libm::powf(b, e)
    }
    #[inline(always)]
    fn cbrt(v: f32) -> f32 {
        libm::cbrtf(v)
    }
    #[inline(always)]
    fn round(v: f32) -> f32 {
        libm::roundf(v)
    }
    #[inline(always)]
    fn sqrt(v: f32) -> f32 {
        libm::sqrtf(v)
    }
    #[inline(always)]
    fn ln(v: f32) -> f32 {
        libm::logf(v)
    }
    #[inline(always)]
    fn exp(v: f32) -> f32 {
        libm::expf(v)
    }
    #[inline(always)]
    fn sin(v: f32) -> f32 {
        libm::sinf(v)
    }
    #[inline(always)]
    fn cos(v: f32) -> f32 {
        libm::cosf(v)
    }
    #[inline(always)]
    fn atan2(y: f32, x: f32) -> f32 {
        libm::atan2f(y, x)
    }
}

#[cfg(feature = "std")]
/// Default math provider. Uses std when available, libm otherwise.
pub type DefaultMath = StdMath;
#[cfg(all(not(feature = "std"), feature = "libm"))]
/// Default math provider. Uses std when available, libm otherwise.
pub type DefaultMath = LibmMath;

/// Column-major 3x3 `f32` matrix with `const fn` arithmetic.
///
/// Each column is `[f32; 4]` padded to 16 bytes for SIMD alignment.
/// `glam::Mat3` is not `const fn`, so this type exists for compile-time
/// derivation of RGB-to-XYZ matrices in `primaries.rs`.
#[repr(C, align(16))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Mat3 {
    /// Column 0, rows [0..2] with padding 0.0 at index 3.
    pub col0: [f32; 4],
    /// Column 1, rows [0..2] with padding 0.0 at index 3.
    pub col1: [f32; 4],
    /// Column 2, rows [0..2] with padding 0.0 at index 3.
    pub col2: [f32; 4],
}

impl Mat3 {
    /// Left-multiply `a * b`. `const fn`.
    pub const fn mul(a: &Self, b: &Self) -> Self {
        macro_rules! e {
            ($r:expr, $c:expr) => {
                a.col0[$r] * $c[0] + a.col1[$r] * $c[1] + a.col2[$r] * $c[2]
            };
        }
        Self {
            col0: [e!(0, b.col0), e!(1, b.col0), e!(2, b.col0), 0.0],
            col1: [e!(0, b.col1), e!(1, b.col1), e!(2, b.col1), 0.0],
            col2: [e!(0, b.col2), e!(1, b.col2), e!(2, b.col2), 0.0],
        }
    }

    /// Invert via Cramer's rule. `const fn`. Returns a matrix with `NaN`
    /// or `INFINITY` entries if the determinant is zero or near-zero.
    /// All standard RGB primary matrices have non-zero determinants.
    pub const fn invert(m: &Self) -> Self {
        let [a, b, c, _] = m.col0;
        let [d, e, f, _] = m.col1;
        let [g, h, i, _] = m.col2;
        let det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g);
        Self {
            col0: [
                (e * i - f * h) / det,
                -(b * i - c * h) / det,
                (b * f - c * e) / det,
                0.0,
            ],
            col1: [
                -(d * i - f * g) / det,
                (a * i - c * g) / det,
                -(a * f - c * d) / det,
                0.0,
            ],
            col2: [
                (d * h - e * g) / det,
                -(a * h - b * g) / det,
                (a * e - b * d) / det,
                0.0,
            ],
        }
    }

    /// Apply to a `[f32; 3]`.
    #[inline]
    pub fn apply(&self, v: [f32; 3]) -> [f32; 3] {
        [
            self.col0[0] * v[0] + self.col1[0] * v[1] + self.col2[0] * v[2],
            self.col0[1] * v[0] + self.col1[1] * v[1] + self.col2[1] * v[2],
            self.col0[2] * v[0] + self.col1[2] * v[1] + self.col2[2] * v[2],
        ]
    }

    /// Apply to a `glam::Vec4`. Lane 3 preserved.
    #[cfg(feature = "glam")]
    #[inline]
    pub fn apply_glam(&self, v: glam::Vec4) -> glam::Vec4 {
        let m = glam::Mat3::from_cols(
            glam::Vec3::from_array([self.col0[0], self.col0[1], self.col0[2]]),
            glam::Vec3::from_array([self.col1[0], self.col1[1], self.col1[2]]),
            glam::Vec3::from_array([self.col2[0], self.col2[1], self.col2[2]]),
        );
        (m * v.truncate()).extend(v.w)
    }

    /// Y row of an RGB->XYZ matrix as CIE luminance weights `[w_r, w_g, w_b]`.
    #[inline]
    pub const fn luminance_weights(&self) -> [f32; 3] {
        [self.col0[1], self.col1[1], self.col2[1]]
    }
}