Skip to main content

ling_graphics/
color.rs

1/// Linear RGBA color, all components in [0, 1].
2#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
3pub struct Color {
4    pub r: f32,
5    pub g: f32,
6    pub b: f32,
7    pub a: f32,
8}
9
10impl Color {
11    pub const BLACK: Self = Self { r: 0.0, g: 0.0, b: 0.0, a: 1.0 };
12    pub const BLUE: Self = Self { r: 0.0, g: 0.0, b: 1.0, a: 1.0 };
13    pub const CYAN: Self = Self { r: 0.0, g: 1.0, b: 1.0, a: 1.0 };
14    pub const GREEN: Self = Self { r: 0.0, g: 1.0, b: 0.0, a: 1.0 };
15    pub const MAGENTA: Self = Self { r: 1.0, g: 0.0, b: 1.0, a: 1.0 };
16    pub const RED: Self = Self { r: 1.0, g: 0.0, b: 0.0, a: 1.0 };
17    pub const TRANSPARENT: Self = Self { r: 0.0, g: 0.0, b: 0.0, a: 0.0 };
18    pub const WHITE: Self = Self { r: 1.0, g: 1.0, b: 1.0, a: 1.0 };
19    pub const YELLOW: Self = Self { r: 1.0, g: 1.0, b: 0.0, a: 1.0 };
20
21    pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
22        Self { r, g, b, a }
23    }
24
25    pub const fn rgb(r: f32, g: f32, b: f32) -> Self {
26        Self { r, g, b, a: 1.0 }
27    }
28
29    pub fn from_srgb(r: u8, g: u8, b: u8) -> Self {
30        Self::rgb(srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b))
31    }
32
33    pub fn from_srgba(r: u8, g: u8, b: u8, a: u8) -> Self {
34        Self::new(
35            srgb_to_linear(r),
36            srgb_to_linear(g),
37            srgb_to_linear(b),
38            a as f32 / 255.0,
39        )
40    }
41
42    pub fn from_hex(hex: u32) -> Self {
43        let r = ((hex >> 16) & 0xFF) as u8;
44        let g = ((hex >> 8) & 0xFF) as u8;
45        let b = (hex & 0xFF) as u8;
46        Self::from_srgb(r, g, b)
47    }
48
49    pub fn to_rgba_bytes(self) -> [u8; 4] {
50        [
51            (self.r.clamp(0.0, 1.0) * 255.0) as u8,
52            (self.g.clamp(0.0, 1.0) * 255.0) as u8,
53            (self.b.clamp(0.0, 1.0) * 255.0) as u8,
54            (self.a.clamp(0.0, 1.0) * 255.0) as u8,
55        ]
56    }
57
58    pub fn lerp(self, other: Self, t: f32) -> Self {
59        Self::new(
60            self.r + (other.r - self.r) * t,
61            self.g + (other.g - self.g) * t,
62            self.b + (other.b - self.b) * t,
63            self.a + (other.a - self.a) * t,
64        )
65    }
66
67    pub fn premultiply_alpha(self) -> Self {
68        Self::new(self.r * self.a, self.g * self.a, self.b * self.a, self.a)
69    }
70
71    pub fn with_alpha(self, a: f32) -> Self {
72        Self { a, ..self }
73    }
74
75    /// Clamp all components to [0, 1].
76    pub fn clamp(self) -> Self {
77        Self::new(
78            self.r.clamp(0.0, 1.0),
79            self.g.clamp(0.0, 1.0),
80            self.b.clamp(0.0, 1.0),
81            self.a.clamp(0.0, 1.0),
82        )
83    }
84
85    pub fn luminance(self) -> f32 {
86        0.2126 * self.r + 0.7152 * self.g + 0.0722 * self.b
87    }
88}
89
90impl std::ops::Add for Color {
91    type Output = Self;
92
93    fn add(self, r: Self) -> Self {
94        Self::new(self.r + r.r, self.g + r.g, self.b + r.b, self.a + r.a)
95    }
96}
97impl std::ops::Mul for Color {
98    type Output = Self;
99
100    fn mul(self, r: Self) -> Self {
101        Self::new(self.r * r.r, self.g * r.g, self.b * r.b, self.a * r.a)
102    }
103}
104impl std::ops::Mul<f32> for Color {
105    type Output = Self;
106
107    fn mul(self, s: f32) -> Self {
108        Self::new(self.r * s, self.g * s, self.b * s, self.a * s)
109    }
110}
111
112fn srgb_to_linear(c: u8) -> f32 {
113    let f = c as f32 / 255.0;
114    if f <= 0.04045 {
115        f / 12.92
116    } else {
117        ((f + 0.055) / 1.055).powf(2.4)
118    }
119}
120
121// ── Blend modes ───────────────────────────────────────────────────────────────
122
123#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
124pub enum BlendMode {
125    /// Porter-Duff "over" compositing.
126    Normal,
127    Additive,
128    Multiply,
129    Screen,
130    Overlay,
131    Subtract,
132}
133
134impl BlendMode {
135    pub fn blend(self, src: Color, dst: Color) -> Color {
136        let sa = src.a;
137        let da = dst.a;
138        match self {
139            BlendMode::Normal => Color::new(
140                src.r * sa + dst.r * (1.0 - sa),
141                src.g * sa + dst.g * (1.0 - sa),
142                src.b * sa + dst.b * (1.0 - sa),
143                sa + da * (1.0 - sa),
144            ),
145            BlendMode::Additive => Color::new(
146                (src.r * sa + dst.r).min(1.0),
147                (src.g * sa + dst.g).min(1.0),
148                (src.b * sa + dst.b).min(1.0),
149                (sa + da).min(1.0),
150            ),
151            BlendMode::Multiply => Color::new(
152                src.r * dst.r,
153                src.g * dst.g,
154                src.b * dst.b,
155                (sa * da).min(1.0),
156            ),
157            BlendMode::Screen => Color::new(
158                1.0 - (1.0 - src.r) * (1.0 - dst.r),
159                1.0 - (1.0 - src.g) * (1.0 - dst.g),
160                1.0 - (1.0 - src.b) * (1.0 - dst.b),
161                (sa + da - sa * da).min(1.0),
162            ),
163            BlendMode::Overlay => {
164                let ov = |s: f32, d: f32| {
165                    if d < 0.5 {
166                        2.0 * s * d
167                    } else {
168                        1.0 - 2.0 * (1.0 - s) * (1.0 - d)
169                    }
170                };
171                Color::new(
172                    ov(src.r, dst.r),
173                    ov(src.g, dst.g),
174                    ov(src.b, dst.b),
175                    (sa + da - sa * da).min(1.0),
176                )
177            },
178            BlendMode::Subtract => Color::new(
179                (dst.r - src.r * sa).max(0.0),
180                (dst.g - src.g * sa).max(0.0),
181                (dst.b - src.b * sa).max(0.0),
182                da,
183            ),
184        }
185    }
186}
187
188// ── Color gradient ────────────────────────────────────────────────────────────
189
190#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
191pub struct ColorGradient {
192    stops: Vec<(f32, Color)>,
193}
194
195impl ColorGradient {
196    pub fn new(stops: Vec<(f32, Color)>) -> Self {
197        let mut s = stops;
198        s.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
199        Self { stops: s }
200    }
201
202    pub fn sample(&self, t: f32) -> Color {
203        if self.stops.is_empty() {
204            return Color::TRANSPARENT;
205        }
206        if self.stops.len() == 1 {
207            return self.stops[0].1;
208        }
209        if t <= self.stops[0].0 {
210            return self.stops[0].1;
211        }
212        let last = self.stops.last().unwrap();
213        if t >= last.0 {
214            return last.1;
215        }
216        for i in 0..self.stops.len() - 1 {
217            let (t0, c0) = self.stops[i];
218            let (t1, c1) = self.stops[i + 1];
219            if t >= t0 && t <= t1 {
220                let local = (t - t0) / (t1 - t0);
221                return c0.lerp(c1, local);
222            }
223        }
224        self.stops.last().unwrap().1
225    }
226}