ph-color 0.1.1

Fixed-point no_std color math for embedded targets: conversion, transfer functions, matrices, gain, and interpolation
Documentation
//! Typed color triple.

use core::marker::PhantomData;

use crate::encoding::{Encoded, Encoding, Linear};
use crate::fixed::Q0_16;
use crate::space::{ColorSpace, Perceptual};

#[cfg(feature = "f32")]
pub use crate::color_f32::ColorF32;

/// Three [`Q0_16`] channels tagged with space and encoding.
pub struct Color<S: ColorSpace, E: Encoding> {
    /// Channel values.
    pub ch: [Q0_16; 3],
    _pd: PhantomData<fn() -> (S, E)>,
}

impl<S: ColorSpace, E: Encoding> Copy for Color<S, E> {}

impl<S: ColorSpace, E: Encoding> Clone for Color<S, E> {
    fn clone(&self) -> Self {
        *self
    }
}

impl<S: ColorSpace, E: Encoding> PartialEq for Color<S, E> {
    fn eq(&self, other: &Self) -> bool {
        self.ch == other.ch
    }
}

impl<S: ColorSpace, E: Encoding> Eq for Color<S, E> {}

impl<S: ColorSpace, E: Encoding> core::fmt::Debug for Color<S, E> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("Color").field("ch", &self.ch).finish()
    }
}

impl<S: ColorSpace, E: Encoding> Color<S, E> {
    /// Construct from three [`Q0_16`] channels.
    #[must_use]
    pub const fn new(ch: [Q0_16; 3]) -> Self {
        Self {
            ch,
            _pd: PhantomData,
        }
    }

    /// Convert channels to unit `f32` (`feature = "f32"`).
    #[cfg(feature = "f32")]
    #[must_use]
    pub const fn to_f32(self) -> crate::ColorF32<S, E> {
        let [c0, c1, c2] = self.ch;
        crate::ColorF32::new([c0.to_f32(), c1.to_f32(), c2.to_f32()])
    }
}

impl<S: ColorSpace> Color<S, Linear> {
    /// Encode with a same-space transfer. Numeric LUT apply is a later issue.
    #[must_use]
    pub const fn encode(self) -> Color<S, Encoded> {
        Color::new(self.ch)
    }

    /// Linear interpolation between two linear colors of the same space.
    ///
    /// `t` is [`Q0_16`]: `0` returns `self`, `65535` returns `other`.
    #[must_use]
    pub const fn lerp(self, other: Self, t: Q0_16) -> Self {
        let [a0, a1, a2] = self.ch;
        let [b0, b1, b2] = other.ch;
        Color::new([
            crate::arith::lerp_q0_16(a0, b0, t),
            crate::arith::lerp_q0_16(a1, b1, t),
            crate::arith::lerp_q0_16(a2, b2, t),
        ])
    }
}

impl<S: ColorSpace> Color<S, Encoded> {
    /// Decode with a same-space transfer. Numeric LUT apply is a later issue.
    #[must_use]
    pub const fn decode(self) -> Color<S, Linear> {
        Color::new(self.ch)
    }
}

impl<S: ColorSpace + Perceptual> Color<S, Encoded> {
    /// Interpolate encoded values only when the space is [`Perceptual`].
    #[must_use]
    pub const fn lerp(self, other: Self, t: Q0_16) -> Self {
        let [a0, a1, a2] = self.ch;
        let [b0, b1, b2] = other.ch;
        Color::new([
            crate::arith::lerp_q0_16(a0, b0, t),
            crate::arith::lerp_q0_16(a1, b1, t),
            crate::arith::lerp_q0_16(a2, b2, t),
        ])
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::space::Srgb;

    #[test]
    fn constructs_linear_and_encoded_srgb() {
        let lin = Color::<Srgb, Linear>::new(Q0_16::array_from_raw([1, 2, 3]));
        let enc = Color::<Srgb, Encoded>::new(Q0_16::array_from_raw([4, 5, 6]));
        assert_eq!(lin.ch, Q0_16::array_from_raw([1, 2, 3]));
        assert_eq!(enc.ch, Q0_16::array_from_raw([4, 5, 6]));
        let _ = lin.encode();
        let _ = enc.decode();
    }

    fn assert_send_sync<T: Send + Sync>() {}

    #[test]
    fn color_matrix_gain_are_send_sync() {
        assert_send_sync::<Color<Srgb, Linear>>();
        assert_send_sync::<Color<Srgb, Encoded>>();
        assert_send_sync::<crate::matrix::Matrix3<Srgb, Srgb>>();
        assert_send_sync::<crate::gain::Gain<Srgb>>();
    }
}