Skip to main content

sequoia_core/
brush.rs

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}