1#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct Stop {
39 pub offset: f32,
42 pub color: u32,
44}
45
46impl Stop {
47 pub fn new(offset: f32, color: u32) -> Self {
49 Stop {
50 offset: offset.clamp(0.0, 1.0),
51 color,
52 }
53 }
54}
55
56#[non_exhaustive]
58#[derive(Clone, Debug, PartialEq)]
59pub enum Gradient {
60 Linear { angle_deg: f32, stops: Vec<Stop> },
64 Radial {
69 cx: f32,
70 cy: f32,
71 radius: f32,
72 stops: Vec<Stop>,
73 },
74}
75
76impl Gradient {
77 pub fn linear(angle_deg: f32, stops: impl IntoIterator<Item = Stop>) -> Self {
81 let mut s: Vec<Stop> = stops.into_iter().collect();
82 s.sort_by(|a, b| {
83 a.offset
84 .partial_cmp(&b.offset)
85 .unwrap_or(std::cmp::Ordering::Equal)
86 });
87 Gradient::Linear {
88 angle_deg,
89 stops: s,
90 }
91 }
92
93 pub fn radial(cx: f32, cy: f32, radius: f32, stops: impl IntoIterator<Item = Stop>) -> Self {
96 let mut s: Vec<Stop> = stops.into_iter().collect();
97 s.sort_by(|a, b| {
98 a.offset
99 .partial_cmp(&b.offset)
100 .unwrap_or(std::cmp::Ordering::Equal)
101 });
102 Gradient::Radial {
103 cx,
104 cy,
105 radius,
106 stops: s,
107 }
108 }
109
110 pub fn stops(&self) -> &[Stop] {
112 match self {
113 Gradient::Linear { stops, .. } => stops,
114 Gradient::Radial { stops, .. } => stops,
115 }
116 }
117
118 pub fn sample(&self, t: f32) -> Option<u32> {
127 let stops = self.stops();
128 if stops.is_empty() {
129 return None;
130 }
131 let t = t.clamp(0.0, 1.0);
132 if t <= stops[0].offset {
133 return Some(stops[0].color);
134 }
135 if t >= stops[stops.len() - 1].offset {
136 return Some(stops[stops.len() - 1].color);
137 }
138 let mut idx = 0;
140 for (i, s) in stops.iter().enumerate() {
141 if s.offset <= t {
142 idx = i;
143 } else {
144 break;
145 }
146 }
147 let a = &stops[idx];
148 let b = &stops[idx + 1];
149 let span = b.offset - a.offset;
150 let f = if span <= 0.0 {
151 0.0
152 } else {
153 (t - a.offset) / span
154 };
155 Some(lerp_color(a.color, b.color, f))
156 }
157}
158
159#[non_exhaustive]
165#[derive(Clone, Debug, PartialEq)]
166pub enum Paint {
167 Solid(u32),
169 Gradient(Gradient),
171}
172
173impl Paint {
174 pub fn solid(rgba: u32) -> Self {
176 Paint::Solid(rgba)
177 }
178
179 pub fn gradient(g: Gradient) -> Self {
181 Paint::Gradient(g)
182 }
183
184 pub fn sample(&self, t: f32) -> u32 {
189 match self {
190 Paint::Solid(rgba) => *rgba,
191 Paint::Gradient(g) => g.sample(t).unwrap_or(0),
192 }
193 }
194}
195
196fn lerp_color(a: u32, b: u32, t: f32) -> u32 {
201 let ac = [
202 ((a >> 24) & 0xff) as f32,
203 ((a >> 16) & 0xff) as f32,
204 ((a >> 8) & 0xff) as f32,
205 (a & 0xff) as f32,
206 ];
207 let bc = [
208 ((b >> 24) & 0xff) as f32,
209 ((b >> 16) & 0xff) as f32,
210 ((b >> 8) & 0xff) as f32,
211 (b & 0xff) as f32,
212 ];
213 let mut out = 0u32;
214 for i in 0..4 {
215 let v = (ac[i] + (bc[i] - ac[i]) * t).clamp(0.0, 255.0) as u32;
216 out |= v << ((3 - i) * 8);
217 }
218 out
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn stop_clamps_offset() {
227 assert_eq!(Stop::new(-0.5, 0).offset, 0.0);
228 assert_eq!(Stop::new(2.0, 0).offset, 1.0);
229 assert_eq!(Stop::new(0.25, 0).offset, 0.25);
230 }
231
232 #[test]
233 fn linear_constructor_sorts_stops() {
234 let g = Gradient::linear(
235 45.0,
236 vec![
237 Stop::new(1.0, 0xFFFFFFFF),
238 Stop::new(0.0, 0x000000FF),
239 Stop::new(0.5, 0x808080FF),
240 ],
241 );
242 let s = g.stops();
243 assert_eq!(s.len(), 3);
244 assert!(s[0].offset <= s[1].offset);
245 assert!(s[1].offset <= s[2].offset);
246 }
247
248 #[test]
249 fn sample_clamps_to_endpoints() {
250 let g = Gradient::linear(
251 0.0,
252 vec![Stop::new(0.0, 0xFF000000), Stop::new(1.0, 0x00FF0000)],
253 );
254 assert_eq!(g.sample(-1.0), Some(0xFF000000));
255 assert_eq!(g.sample(2.0), Some(0x00FF0000));
256 }
257
258 #[test]
259 fn sample_midpoint_lerps_per_channel() {
260 let g = Gradient::linear(
262 0.0,
263 vec![Stop::new(0.0, 0x000000FF), Stop::new(1.0, 0xFFFFFFFF)],
264 );
265 let mid = g.sample(0.5).unwrap();
266 let r = (mid >> 24) & 0xff;
267 let g_ = (mid >> 16) & 0xff;
268 let b = (mid >> 8) & 0xff;
269 let a = mid & 0xff;
270 assert!((100..=155).contains(&r), "r={r}");
271 assert!((100..=155).contains(&g_), "g={g_}");
272 assert!((100..=155).contains(&b), "b={b}");
273 assert_eq!(a, 0xff);
274 }
275
276 #[test]
277 fn sample_picks_correct_segment_with_three_stops() {
278 let g = Gradient::linear(
279 0.0,
280 vec![
281 Stop::new(0.0, 0xFF0000FF), Stop::new(0.5, 0x00FF00FF), Stop::new(1.0, 0x0000FFFF), ],
285 );
286 let v = g.sample(0.25).unwrap();
288 let r = (v >> 24) & 0xff;
289 let g_ = (v >> 16) & 0xff;
290 let b = (v >> 8) & 0xff;
291 assert!(r > 100 && r < 155, "r={r}");
292 assert!(g_ > 100 && g_ < 155, "g={g_}");
293 assert_eq!(b, 0);
294
295 let v = g.sample(0.75).unwrap();
297 let r = (v >> 24) & 0xff;
298 let g_ = (v >> 16) & 0xff;
299 let b = (v >> 8) & 0xff;
300 assert_eq!(r, 0);
301 assert!(g_ > 100 && g_ < 155, "g={g_}");
302 assert!(b > 100 && b < 155, "b={b}");
303 }
304
305 #[test]
306 fn empty_gradient_returns_none() {
307 let g = Gradient::linear(0.0, Vec::<Stop>::new());
308 assert!(g.sample(0.5).is_none());
309 }
310
311 #[test]
312 fn radial_constructor_sorts_stops() {
313 let g = Gradient::radial(
314 0.5,
315 0.5,
316 0.5,
317 vec![Stop::new(1.0, 0x000000FF), Stop::new(0.0, 0xFFFFFFFF)],
318 );
319 let s = g.stops();
320 assert_eq!(s.len(), 2);
321 assert_eq!(s[0].offset, 0.0);
322 assert_eq!(s[1].offset, 1.0);
323 if let Gradient::Radial {
324 cx,
325 cy,
326 radius,
327 stops,
328 } = &g
329 {
330 assert_eq!(*cx, 0.5);
331 assert_eq!(*cy, 0.5);
332 assert_eq!(*radius, 0.5);
333 assert_eq!(stops.len(), 2);
334 } else {
335 panic!("expected Radial");
336 }
337 }
338
339 #[test]
340 fn paint_solid_sample_is_constant() {
341 let p = Paint::solid(0x123456FF);
342 assert_eq!(p.sample(0.0), 0x123456FF);
343 assert_eq!(p.sample(0.5), 0x123456FF);
344 assert_eq!(p.sample(1.0), 0x123456FF);
345 }
346
347 #[test]
348 fn paint_gradient_sample_delegates() {
349 let p = Paint::gradient(Gradient::linear(
350 0.0,
351 vec![Stop::new(0.0, 0x000000FF), Stop::new(1.0, 0xFFFFFFFF)],
352 ));
353 let v = p.sample(0.5);
354 let r = (v >> 24) & 0xff;
355 assert!((100..=155).contains(&r));
356 }
357
358 #[test]
359 fn paint_empty_gradient_samples_zero() {
360 let p = Paint::gradient(Gradient::linear(0.0, Vec::<Stop>::new()));
361 assert_eq!(p.sample(0.5), 0);
362 }
363}