ling-graphics 2030.0.5

3D/4D rendering, geometry, animation, and font tools for the Ling ecosystem
Documentation
/// Linear RGBA color, all components in [0, 1].
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Color {
    pub r: f32,
    pub g: f32,
    pub b: f32,
    pub a: f32,
}

impl Color {
    pub const BLACK: Self = Self { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
    pub const BLUE: Self = Self { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
    pub const CYAN: Self = Self { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
    pub const GREEN: Self = Self { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
    pub const MAGENTA: Self = Self { r: 1.0, g: 0.0, b: 1.0, a: 1.0 };
    pub const RED: Self = Self { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
    pub const TRANSPARENT: Self = Self { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
    pub const WHITE: Self = Self { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
    pub const YELLOW: Self = Self { r: 1.0, g: 1.0, b: 0.0, a: 1.0 };

    pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
        Self { r, g, b, a }
    }

    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
        Self { r, g, b, a: 1.0 }
    }

    pub fn from_srgb(r: u8, g: u8, b: u8) -> Self {
        Self::rgb(srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b))
    }

    pub fn from_srgba(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self::new(
            srgb_to_linear(r),
            srgb_to_linear(g),
            srgb_to_linear(b),
            a as f32 / 255.0,
        )
    }

    pub fn from_hex(hex: u32) -> Self {
        let r = ((hex >> 16) & 0xFF) as u8;
        let g = ((hex >> 8) & 0xFF) as u8;
        let b = (hex & 0xFF) as u8;
        Self::from_srgb(r, g, b)
    }

    pub fn to_rgba_bytes(self) -> [u8; 4] {
        [
            (self.r.clamp(0.0, 1.0) * 255.0) as u8,
            (self.g.clamp(0.0, 1.0) * 255.0) as u8,
            (self.b.clamp(0.0, 1.0) * 255.0) as u8,
            (self.a.clamp(0.0, 1.0) * 255.0) as u8,
        ]
    }

    pub fn lerp(self, other: Self, t: f32) -> Self {
        Self::new(
            self.r + (other.r - self.r) * t,
            self.g + (other.g - self.g) * t,
            self.b + (other.b - self.b) * t,
            self.a + (other.a - self.a) * t,
        )
    }

    pub fn premultiply_alpha(self) -> Self {
        Self::new(self.r * self.a, self.g * self.a, self.b * self.a, self.a)
    }

    pub fn with_alpha(self, a: f32) -> Self {
        Self { a, ..self }
    }

    /// Clamp all components to [0, 1].
    pub fn clamp(self) -> Self {
        Self::new(
            self.r.clamp(0.0, 1.0),
            self.g.clamp(0.0, 1.0),
            self.b.clamp(0.0, 1.0),
            self.a.clamp(0.0, 1.0),
        )
    }

    pub fn luminance(self) -> f32 {
        0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
    }
}

impl std::ops::Add for Color {
    type Output = Self;

    fn add(self, r: Self) -> Self {
        Self::new(self.r + r.r, self.g + r.g, self.b + r.b, self.a + r.a)
    }
}
impl std::ops::Mul for Color {
    type Output = Self;

    fn mul(self, r: Self) -> Self {
        Self::new(self.r * r.r, self.g * r.g, self.b * r.b, self.a * r.a)
    }
}
impl std::ops::Mul<f32> for Color {
    type Output = Self;

    fn mul(self, s: f32) -> Self {
        Self::new(self.r * s, self.g * s, self.b * s, self.a * s)
    }
}

fn srgb_to_linear(c: u8) -> f32 {
    let f = c as f32 / 255.0;
    if f <= 0.04045 {
        f / 12.92
    } else {
        ((f + 0.055) / 1.055).powf(2.4)
    }
}

// ── Blend modes ───────────────────────────────────────────────────────────────

#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum BlendMode {
    /// Porter-Duff "over" compositing.
    Normal,
    Additive,
    Multiply,
    Screen,
    Overlay,
    Subtract,
}

impl BlendMode {
    pub fn blend(self, src: Color, dst: Color) -> Color {
        let sa = src.a;
        let da = dst.a;
        match self {
            BlendMode::Normal => Color::new(
                src.r * sa + dst.r * (1.0 - sa),
                src.g * sa + dst.g * (1.0 - sa),
                src.b * sa + dst.b * (1.0 - sa),
                sa + da * (1.0 - sa),
            ),
            BlendMode::Additive => Color::new(
                (src.r * sa + dst.r).min(1.0),
                (src.g * sa + dst.g).min(1.0),
                (src.b * sa + dst.b).min(1.0),
                (sa + da).min(1.0),
            ),
            BlendMode::Multiply => Color::new(
                src.r * dst.r,
                src.g * dst.g,
                src.b * dst.b,
                (sa * da).min(1.0),
            ),
            BlendMode::Screen => Color::new(
                1.0 - (1.0 - src.r) * (1.0 - dst.r),
                1.0 - (1.0 - src.g) * (1.0 - dst.g),
                1.0 - (1.0 - src.b) * (1.0 - dst.b),
                (sa + da - sa * da).min(1.0),
            ),
            BlendMode::Overlay => {
                let ov = |s: f32, d: f32| {
                    if d < 0.5 {
                        2.0 * s * d
                    } else {
                        1.0 - 2.0 * (1.0 - s) * (1.0 - d)
                    }
                };
                Color::new(
                    ov(src.r, dst.r),
                    ov(src.g, dst.g),
                    ov(src.b, dst.b),
                    (sa + da - sa * da).min(1.0),
                )
            },
            BlendMode::Subtract => Color::new(
                (dst.r - src.r * sa).max(0.0),
                (dst.g - src.g * sa).max(0.0),
                (dst.b - src.b * sa).max(0.0),
                da,
            ),
        }
    }
}

// ── Color gradient ────────────────────────────────────────────────────────────

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ColorGradient {
    stops: Vec<(f32, Color)>,
}

impl ColorGradient {
    pub fn new(stops: Vec<(f32, Color)>) -> Self {
        let mut s = stops;
        s.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
        Self { stops: s }
    }

    pub fn sample(&self, t: f32) -> Color {
        if self.stops.is_empty() {
            return Color::TRANSPARENT;
        }
        if self.stops.len() == 1 {
            return self.stops[0].1;
        }
        if t <= self.stops[0].0 {
            return self.stops[0].1;
        }
        let last = self.stops.last().unwrap();
        if t >= last.0 {
            return last.1;
        }
        for i in 0..self.stops.len() - 1 {
            let (t0, c0) = self.stops[i];
            let (t1, c1) = self.stops[i + 1];
            if t >= t0 && t <= t1 {
                let local = (t - t0) / (t1 - t0);
                return c0.lerp(c1, local);
            }
        }
        self.stops.last().unwrap().1
    }
}