🔒 Repository is read-only – file editing is disabled.
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);
}
}