🔒 Repository is read-only – file editing is disabled.

luna/crates/libluna-core/src/color.rs main

175 lines Raw ← Back
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
//! Kolor w przestrzeni sRGB z kanałem alfa.
//!
//! Kanały są przechowywane jako `f32` w zakresie `0.0..=1.0`, **bez premnożenia**
//! (non-premultiplied). Premnożenie, jeśli backend go wymaga, robi warstwa rysowania.

/// Kolor RGBA w sRGB, kanały `0.0..=1.0`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
    pub r: f32,
    pub g: f32,
    pub b: f32,
    pub a: f32,
}

impl Color {
    pub const TRANSPARENT: Color = Color {
        r: 0.0,
        g: 0.0,
        b: 0.0,
        a: 0.0,
    };
    pub const BLACK: Color = Color {
        r: 0.0,
        g: 0.0,
        b: 0.0,
        a: 1.0,
    };
    pub const WHITE: Color = Color {
        r: 1.0,
        g: 1.0,
        b: 1.0,
        a: 1.0,
    };

    /// Nieprzezroczysty kolor z kanałów `0.0..=1.0`.
    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
        Self { r, g, b, a: 1.0 }
    }

    /// Kolor z kanałów `0.0..=1.0`, z jawną alfą.
    pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
        Self { r, g, b, a }
    }

    /// Z kanałów 8-bitowych (np. `#3584e4` → `from_rgba8(0x35, 0x84, 0xe4, 255)`).
    pub const fn from_rgba8(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self {
            r: r as f32 / 255.0,
            g: g as f32 / 255.0,
            b: b as f32 / 255.0,
            a: a as f32 / 255.0,
        }
    }

    /// Zapis do kanałów 8-bitowych, z przycięciem do zakresu.
    pub fn to_rgba8(&self) -> [u8; 4] {
        [to_u8(self.r), to_u8(self.g), to_u8(self.b), to_u8(self.a)]
    }

    /// Ten sam kolor z inną alfą.
    pub const fn with_alpha(self, a: f32) -> Self {
        Self { a, ..self }
    }

    /// Ten sam kolor (RGB) z alfą pomnożoną przez `factor` (0.0..=1.0 zostaje).
    pub const fn scale_alpha(self, factor: f32) -> Self {
        Self {
            a: self.a * factor,
            ..self
        }
    }

    /// Liniowa interpolacja kanałów (`t = 0` → `self`, `t = 1` → `other`).
    ///
    /// Interpolacja jest w sRGB (wystarczająca dla animacji UI); docelowo można
    /// przełączyć na przestrzeń liniową.
    pub fn lerp(self, other: Color, t: f32) -> Color {
        let t = t.clamp(0.0, 1.0);
        let mix = |a: f32, b: f32| a + (b - a) * t;
        Color {
            r: mix(self.r, other.r),
            g: mix(self.g, other.g),
            b: mix(self.b, other.b),
            a: mix(self.a, other.a),
        }
    }

    /// Kolor w formacie `#rrggbb`, `#rrggbbaa`, `#rgb` lub `#rgba`.
    pub fn from_hex(hex: &str) -> Option<Color> {
        let hex = hex.strip_prefix('#').unwrap_or(hex);
        if !hex.is_ascii() {
            return None;
        }
        let byte = |s: &str| u8::from_str_radix(s, 16).ok();
        match hex.len() {
            3 => {
                let r = byte(&hex[0..1])?;
                let g = byte(&hex[1..2])?;
                let b = byte(&hex[2..3])?;
                Some(Color::from_rgba8(r * 17, g * 17, b * 17, 255))
            }
            4 => {
                let r = byte(&hex[0..1])?;
                let g = byte(&hex[1..2])?;
                let b = byte(&hex[2..3])?;
                let a = byte(&hex[3..4])?;
                Some(Color::from_rgba8(r * 17, g * 17, b * 17, a * 17))
            }
            6 => {
                let r = byte(&hex[0..2])?;
                let g = byte(&hex[2..4])?;
                let b = byte(&hex[4..6])?;
                Some(Color::from_rgba8(r, g, b, 255))
            }
            8 => {
                let r = byte(&hex[0..2])?;
                let g = byte(&hex[2..4])?;
                let b = byte(&hex[4..6])?;
                let a = byte(&hex[6..8])?;
                Some(Color::from_rgba8(r, g, b, a))
            }
            _ => None,
        }
    }
}

fn to_u8(v: f32) -> u8 {
    (v.clamp(0.0, 1.0) * 255.0).round() as u8
}

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

    #[test]
    fn rgba8_roundtrip() {
        let c = Color::from_rgba8(0x35, 0x84, 0xe4, 0xff);
        assert_eq!(c.to_rgba8(), [0x35, 0x84, 0xe4, 0xff]);
    }

    #[test]
    fn hex_parsing() {
        assert_eq!(Color::from_hex("#ffffff"), Some(Color::WHITE));
        assert_eq!(
            Color::from_hex("3584e4"),
            Some(Color::from_rgba8(0x35, 0x84, 0xe4, 255))
        );
        assert_eq!(Color::from_hex("#fff"), Some(Color::WHITE));
        assert_eq!(
            Color::from_hex("#00000080"),
            Some(Color::from_rgba8(0, 0, 0, 0x80))
        );
        assert_eq!(Color::from_hex("#12345"), None);
        assert_eq!(Color::from_hex("nie-hex"), None);
    }

    #[test]
    fn lerp_midpoint() {
        let m = Color::BLACK.lerp(Color::WHITE, 0.5);
        assert!((m.r - 0.5).abs() < 1e-6);
        assert!((m.a - 1.0).abs() < 1e-6);
    }

    #[test]
    fn lerp_clamps_t() {
        assert_eq!(Color::BLACK.lerp(Color::WHITE, -1.0), Color::BLACK);
        assert_eq!(Color::BLACK.lerp(Color::WHITE, 2.0), Color::WHITE);
    }

    #[test]
    fn alpha_helpers() {
        let c = Color::WHITE.scale_alpha(0.5).with_alpha(0.25);
        assert!((c.a - 0.25).abs() < 1e-6);
    }
}