1#![forbid(unsafe_code)]
2
3use crate::paint::Color;
4
5#[derive(Clone, Copy, Debug, PartialEq)]
6pub struct GradientStop {
7 pub at: f32,
8 pub color: Color,
9}
10
11impl GradientStop {
12 pub fn new(at: f32, color: Color) -> Self {
13 Self { at, color }
14 }
15}
16
17#[derive(Clone, Debug, Default, PartialEq)]
18pub struct GradientStops {
19 stops: Vec<GradientStop>,
20}
21
22pub const MAX_GRADIENT_STOPS: usize = 4;
23
24impl GradientStops {
25 pub fn new(mut stops: Vec<GradientStop>) -> Self {
26 for s in &mut stops {
27 s.at = if s.at.is_finite() {
28 s.at.clamp(0.0, 1.0)
29 } else {
30 0.0
31 };
32 }
33 stops.sort_by(|a, b| a.at.partial_cmp(&b.at).unwrap_or(std::cmp::Ordering::Equal));
34
35 let mut out: Vec<GradientStop> = Vec::with_capacity(stops.len());
36 for s in stops {
37 if let Some(last) = out.last_mut()
38 && (last.at - s.at).abs() < f32::EPSILON
39 {
40 *last = s;
41 continue;
42 }
43 out.push(s);
44 }
45 Self { stops: out }
46 }
47
48 pub fn solid(color: Color) -> Self {
49 Self {
50 stops: vec![GradientStop::new(0.0, color)],
51 }
52 }
53
54 pub fn two(from: Color, to: Color) -> Self {
55 Self::new(vec![
56 GradientStop::new(0.0, from),
57 GradientStop::new(1.0, to),
58 ])
59 }
60
61 pub fn is_empty(&self) -> bool {
62 self.stops.is_empty()
63 }
64
65 pub fn len(&self) -> usize {
66 self.stops.len()
67 }
68
69 pub fn stops(&self) -> &[GradientStop] {
70 &self.stops
71 }
72
73 pub fn for_gpu(&self) -> Option<([[f32; 4]; MAX_GRADIENT_STOPS], [f32; 4], u32)> {
74 if self.stops.is_empty() || self.stops.len() > MAX_GRADIENT_STOPS {
75 return None;
76 }
77 let mut colors = [[0.0; 4]; MAX_GRADIENT_STOPS];
78 let mut pos = [0.0; 4];
79 for (i, s) in self.stops.iter().enumerate() {
80 let c = s.color.to_linear();
81 colors[i] = [c.r, c.g, c.b, c.a];
82 pos[i] = s.at;
83 }
84
85 if self.stops.len() == 1 {
86 for i in 1..MAX_GRADIENT_STOPS {
87 colors[i] = colors[0];
88 pos[i] = 1.0;
89 }
90 }
91 Some((colors, pos, self.stops.len() as u32))
92 }
93
94 pub fn cache_key(&self) -> u64 {
95 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
96 let mut mix = |b: u8| {
97 hash ^= u64::from(b);
98 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
99 };
100 for s in &self.stops {
101 for b in s.at.to_bits().to_le_bytes() {
102 mix(b);
103 }
104 let (r, g, b, a) = s.color.to_rgba8();
105 mix(r);
106 mix(g);
107 mix(b);
108 mix(a);
109 }
110 hash ^= 0x9e37_79b9_7f4a_7c15;
111 hash
112 }
113
114 pub fn sample_linear(&self, t: f32) -> [f32; 4] {
115 let Some(first) = self.stops.first() else {
116 return [0.0, 0.0, 0.0, 0.0];
117 };
118 if self.stops.len() == 1 || !t.is_finite() {
119 let c = first.color.to_linear();
120 return [c.r, c.g, c.b, c.a];
121 }
122 let t = t.clamp(0.0, 1.0);
123 if t <= first.at {
124 let c = first.color.to_linear();
125 return [c.r, c.g, c.b, c.a];
126 }
127 let last = self.stops[self.stops.len() - 1];
128 if t >= last.at {
129 let c = last.color.to_linear();
130 return [c.r, c.g, c.b, c.a];
131 }
132 for pair in self.stops.windows(2) {
133 let (a, b) = (pair[0], pair[1]);
134 if t <= b.at {
135 let span = b.at - a.at;
136 let k = if span <= f32::EPSILON {
137 0.0
138 } else {
139 ((t - a.at) / span).clamp(0.0, 1.0)
140 };
141 let ca = a.color.to_linear();
142 let cb = b.color.to_linear();
143
144 let mix = |x: f32, y: f32| x + (y - x) * k;
145 return [
146 mix(ca.r, cb.r),
147 mix(ca.g, cb.g),
148 mix(ca.b, cb.b),
149 mix(ca.a, cb.a),
150 ];
151 }
152 }
153 let c = last.color.to_linear();
154 [c.r, c.g, c.b, c.a]
155 }
156
157 pub fn bake_lut256(&self) -> Vec<u8> {
158 let mut out = Vec::with_capacity(256 * 4);
159 for i in 0..256 {
160 let t = i as f32 / 255.0;
161 let [r, g, b, a] = self.sample_linear(t);
162 let q = |c: f32| (c.clamp(0.0, 1.0) * 255.0).round() as u8;
163 out.push(q(r));
164 out.push(q(g));
165 out.push(q(b));
166 out.push(q(a));
167 }
168 out
169 }
170}
171
172impl From<Vec<GradientStop>> for GradientStops {
173 fn from(v: Vec<GradientStop>) -> Self {
174 Self::new(v)
175 }
176}
177
178#[derive(Clone, Copy, Debug, PartialEq)]
179pub enum GradientKind {
180 Linear { angle_deg: f32 },
181 Radial { radius: f32 },
182}
183
184impl GradientKind {
185 pub fn horizontal() -> Self {
186 Self::Linear { angle_deg: 0.0 }
187 }
188
189 pub fn vertical() -> Self {
190 Self::Linear { angle_deg: 90.0 }
191 }
192
193 pub fn cover() -> Self {
194 Self::Radial { radius: 0.0 }
195 }
196
197 pub fn direction(self) -> (f32, f32) {
198 match self {
199 Self::Linear { angle_deg } => {
200 let a = if angle_deg.is_finite() {
201 angle_deg.to_radians()
202 } else {
203 0.0
204 };
205 (a.cos(), a.sin())
206 }
207 Self::Radial { .. } => (0.0, 0.0),
208 }
209 }
210}
211
212#[derive(Clone, Debug, PartialEq)]
213pub enum Fill {
214 Solid(Color),
215 Gradient {
216 kind: GradientKind,
217 stops: GradientStops,
218 },
219}
220
221impl Fill {
222 pub fn solid(color: Color) -> Self {
223 Self::Solid(color)
224 }
225
226 pub fn linear(stops: impl Into<GradientStops>, angle_deg: f32) -> Self {
227 Self::Gradient {
228 kind: GradientKind::Linear { angle_deg },
229 stops: stops.into(),
230 }
231 }
232
233 pub fn horizontal(stops: impl Into<GradientStops>) -> Self {
234 Self::Gradient {
235 kind: GradientKind::horizontal(),
236 stops: stops.into(),
237 }
238 }
239
240 pub fn vertical(stops: impl Into<GradientStops>) -> Self {
241 Self::Gradient {
242 kind: GradientKind::vertical(),
243 stops: stops.into(),
244 }
245 }
246
247 pub fn radial(stops: impl Into<GradientStops>) -> Self {
248 Self::Gradient {
249 kind: GradientKind::cover(),
250 stops: stops.into(),
251 }
252 }
253
254 pub fn radial_radius(stops: impl Into<GradientStops>, radius: f32) -> Self {
255 let radius = if radius.is_finite() {
256 radius.max(0.0)
257 } else {
258 0.0
259 };
260 Self::Gradient {
261 kind: GradientKind::Radial { radius },
262 stops: stops.into(),
263 }
264 }
265
266 pub fn is_solid(&self) -> bool {
267 matches!(self, Self::Solid(_))
268 }
269
270 pub fn is_visible(&self) -> bool {
271 match self {
272 Self::Solid(c) => c.a > 0.0,
273 Self::Gradient { stops, .. } => !stops.is_empty(),
274 }
275 }
276}
277
278impl From<Color> for Fill {
279 fn from(c: Color) -> Self {
280 Self::Solid(c)
281 }
282}
283
284#[derive(Clone, Copy, Debug, PartialEq)]
285pub struct Stroke {
286 pub width: f32,
287 pub rounded: bool,
288}
289
290impl Stroke {
291 pub fn new(width: f32) -> Self {
292 Self {
293 width: if width.is_finite() {
294 width.max(0.0)
295 } else {
296 0.0
297 },
298 rounded: false,
299 }
300 }
301
302 pub fn round(width: f32) -> Self {
303 Self {
304 width: if width.is_finite() {
305 width.max(0.0)
306 } else {
307 0.0
308 },
309 rounded: true,
310 }
311 }
312
313 pub fn is_visible(self) -> bool {
314 self.width > 0.0
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn stops_sort_and_clamp() {
324 let s = GradientStops::new(vec![
325 GradientStop::new(1.5, Color::BLUE),
326 GradientStop::new(-1.0, Color::RED),
327 GradientStop::new(0.5, Color::GREEN),
328 ]);
329 let ats: Vec<f32> = s.stops().iter().map(|x| x.at).collect();
330 assert_eq!(ats, vec![0.0, 0.5, 1.0]);
331 }
332
333 #[test]
334 fn empty_is_transparent() {
335 let s = GradientStops::new(vec![]);
336 assert_eq!(s.sample_linear(0.5), [0.0, 0.0, 0.0, 0.0]);
337 assert_eq!(s.bake_lut256().len(), 256 * 4);
338 }
339
340 #[test]
341 fn linear_mid_is_not_dark() {
342 let s = GradientStops::two(Color::RED, Color::BLUE);
343 let mid = s.sample_linear(0.5);
344
345 assert!((mid[0] - 0.5).abs() < 1e-5, "{mid:?}");
346 assert!((mid[2] - 0.5).abs() < 1e-5, "{mid:?}");
347
348 let gray = GradientStops::two(Color::WHITE, Color::BLACK).sample_linear(0.5);
349 assert!((gray[0] - 0.5).abs() < 1e-5, "{gray:?}");
350 }
351
352 #[test]
353 fn lut_roundtrip_ends() {
354 let s = GradientStops::two(Color::RED, Color::BLUE);
355 let lut = s.bake_lut256();
356 assert_eq!(lut.len(), 1024);
357 assert_eq!(&lut[0..4], &[255, 0, 0, 255]);
358 assert_eq!(&lut[1020..1024], &[0, 0, 255, 255]);
359 }
360
361 #[test]
362 fn cache_key_stable() {
363 let a = GradientStops::two(Color::RED, Color::BLUE).cache_key();
364 let b = GradientStops::two(Color::RED, Color::BLUE).cache_key();
365 let c = GradientStops::two(Color::BLUE, Color::RED).cache_key();
366 assert_eq!(a, b);
367 assert_ne!(a, c);
368 }
369}