mirage-engine 0.1.1

Mirage, an immediate-mode 3D engine for simple games on desktop and the browser
Documentation
use bytemuck::{Pod, Zeroable};

/// A red, green, blue, and opacity color, each a linear value the engine
/// encodes to sRGB on its way to the window.
///
/// Nothing clamps a channel: a value past `1.0` is light the frame holds
/// and the tone map brings down, which is what an emissive material or a
/// bright light is written with. `0.0..=1.0` is what the window can show.
///
/// Values are linear; the engine writes sRGB-encoded values to the screen.
/// A color channel past `1.0` holds light no screen draws, which
/// [`FrameContext::set_bloom`](crate::FrameContext::set_bloom) spreads.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
pub struct Color {
    /// Red.
    pub red: f32,
    /// Green.
    pub green: f32,
    /// Blue.
    pub blue: f32,
    /// Opacity: `1.0` is fully opaque, `0.0` is empty.
    pub alpha: f32,
}

impl Color {
    /// Opaque black.
    pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);

    /// Opaque white.
    pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);

    /// An opaque color, each channel a fraction of `1.0`.
    pub const fn rgb(red: f32, green: f32, blue: f32) -> Self {
        Self::rgba(red, green, blue, 1.0)
    }

    /// A color with opacity `alpha`, each channel and `alpha` a fraction of
    /// `1.0`.
    pub const fn rgba(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
        Self {
            red,
            green,
            blue,
            alpha,
        }
    }

    /// The same channels at opacity `alpha`, a fraction of `1.0`.
    pub const fn with_alpha(self, alpha: f32) -> Self {
        Self { alpha, ..self }
    }

    /// The color three sRGB-encoded bytes hold, red, green and blue, fully
    /// opaque: what a texture's own texels are read back as.
    pub(crate) fn of_srgb(texel: [u8; 3]) -> Self {
        let linear = |byte: u8| {
            let encoded = f32::from(byte) / 255.0;
            match encoded <= 0.040_45 {
                true => encoded / 12.92,
                false => ((encoded + 0.055) / 1.055).powf(2.4),
            }
        };

        Self::rgb(linear(texel[0]), linear(texel[1]), linear(texel[2]))
    }

    /// Its red, green, and blue scaled by `factor`, a fraction of each
    /// channel's own value, keeping its opacity.
    pub const fn dimmed(self, factor: f32) -> Self {
        Self {
            red: self.red * factor,
            green: self.green * factor,
            blue: self.blue * factor,
            alpha: self.alpha,
        }
    }
}

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

    #[test]
    fn a_color_keeps_its_channels_through_an_opacity_change_and_its_opacity_through_a_dim() {
        let color = Color::rgba(0.4, 0.6, 0.8, 0.5);

        assert_eq!(color.with_alpha(0.25), Color::rgba(0.4, 0.6, 0.8, 0.25));
        assert_eq!(color.dimmed(0.5), Color::rgba(0.2, 0.3, 0.4, 0.5));
    }
}