1use sequoia_core::{Color, Point, Vec2};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct GradientStop {
5 pub at: f32,
6
7 pub color: Color,
8}
9
10impl GradientStop {
11 pub fn new(at: f32, color: Color) -> Self {
12 Self { at, color }
13 }
14}
15
16#[derive(Clone, Debug, PartialEq)]
17#[non_exhaustive]
18pub enum Gradient {
19 Linear {
20 from: Point,
21
22 to: Point,
23
24 stops: Vec<GradientStop>,
25 },
26
27 Radial {
28 center: Point,
29
30 radius: f32,
31
32 stops: Vec<GradientStop>,
33 },
34}
35
36impl Gradient {
37 pub fn linear(
38 from: impl Into<Point>,
39 to: impl Into<Point>,
40 stops: impl Into<Vec<GradientStop>>,
41 ) -> Self {
42 Self::Linear {
43 from: from.into(),
44 to: to.into(),
45 stops: sorted(stops.into()),
46 }
47 }
48
49 pub fn radial(
50 center: impl Into<Point>,
51 radius: f32,
52 stops: impl Into<Vec<GradientStop>>,
53 ) -> Self {
54 Self::Radial {
55 center: center.into(),
56 radius,
57 stops: sorted(stops.into()),
58 }
59 }
60
61 pub fn stops(&self) -> &[GradientStop] {
62 match self {
63 Self::Linear { stops, .. } | Self::Radial { stops, .. } => stops,
64 }
65 }
66
67 pub fn is_vertical(&self) -> bool {
68 match self {
69 Self::Linear { from, to, .. } => {
70 let d = Vec2::new(to.x - from.x, to.y - from.y);
71 d.y.abs() > d.x.abs()
72 }
73 Self::Radial { .. } => false,
74 }
75 }
76
77 pub fn direction(&self) -> Vec2 {
78 match self {
79 Self::Linear { from, to, .. } => Vec2::new(to.x - from.x, to.y - from.y),
80 Self::Radial { .. } => Vec2::ZERO,
81 }
82 }
83
84 pub fn color_at(&self, p: Point) -> Color {
85 let t = match self {
86 Self::Linear { from, to, .. } => {
87 let d = Vec2::new(to.x - from.x, to.y - from.y);
88 let len2 = d.x * d.x + d.y * d.y;
89 if len2 <= f32::MIN_POSITIVE {
90 0.0
91 } else {
92 ((p.x - from.x) * d.x + (p.y - from.y) * d.y) / len2
93 }
94 }
95 Self::Radial { center, radius, .. } => {
96 if !(*radius > 0.0) || !radius.is_finite() {
97 0.0
98 } else {
99 let d = Vec2::new(p.x - center.x, p.y - center.y);
100 (d.x * d.x + d.y * d.y).sqrt() / *radius
101 }
102 }
103 };
104 sample_stops(t, self.stops())
105 }
106}
107
108fn sorted(mut stops: Vec<GradientStop>) -> Vec<GradientStop> {
109 stops.sort_by(|a, b| a.at.partial_cmp(&b.at).unwrap_or(std::cmp::Ordering::Equal));
110 stops
111}
112
113fn sample_stops(t: f32, stops: &[GradientStop]) -> Color {
114 let Some(first) = stops.first() else {
115 return Color::TRANSPARENT;
116 };
117 if t <= first.at || stops.len() == 1 {
118 return first.color;
119 }
120 let last = stops[stops.len() - 1];
121 if t >= last.at {
122 return last.color;
123 }
124 for pair in stops.windows(2) {
125 let (a, b) = (pair[0], pair[1]);
126 if t <= b.at {
127 let span = b.at - a.at;
128 let k = if span <= f32::MIN_POSITIVE {
129 0.0
130 } else {
131 (t - a.at) / span
132 };
133 return lerp_color(a.color, b.color, k);
134 }
135 }
136 last.color
137}
138
139fn lerp_color(a: Color, b: Color, k: f32) -> Color {
140 let k = k.clamp(0.0, 1.0);
141 let mix = |x: f32, y: f32| x + (y - x) * k;
142 Color {
143 r: mix(a.r, b.r),
144 g: mix(a.g, b.g),
145 b: mix(a.b, b.b),
146 a: mix(a.a, b.a),
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 fn red() -> Color {
155 Color::rgb8(255, 0, 0)
156 }
157
158 fn blue() -> Color {
159 Color::rgb8(0, 0, 255)
160 }
161
162 #[test]
163 fn linear_gradient_runs_along_the_axis() {
164 let g = Gradient::linear(
165 (0.0, 0.0),
166 (100.0, 0.0),
167 vec![
168 GradientStop::new(0.0, red()),
169 GradientStop::new(1.0, blue()),
170 ],
171 );
172 assert_eq!(g.color_at(Point::new(0.0, 50.0)), red());
173 assert_eq!(g.color_at(Point::new(100.0, 50.0)), blue());
174 let mid = g.color_at(Point::new(50.0, 0.0));
175 assert!(
176 (mid.r - 0.5).abs() < 1e-6 && (mid.b - 0.5).abs() < 1e-6,
177 "{mid:?}"
178 );
179
180 assert_eq!(g.color_at(Point::new(-40.0, 0.0)), red());
181 assert_eq!(g.color_at(Point::new(400.0, 0.0)), blue());
182 assert!(!g.is_vertical());
183 }
184
185 #[test]
186 fn vertical_linear_gradient_is_detected() {
187 let g = Gradient::linear(
188 (0.0, 0.0),
189 (0.0, 100.0),
190 vec![
191 GradientStop::new(0.0, red()),
192 GradientStop::new(1.0, blue()),
193 ],
194 );
195 assert!(g.is_vertical());
196 let quarter = g.color_at(Point::new(80.0, 25.0));
197 assert!((quarter.b - 0.25).abs() < 1e-6, "{quarter:?}");
198 }
199
200 #[test]
201 fn radial_gradient_uses_distance() {
202 let g = Gradient::radial(
203 (50.0, 50.0),
204 40.0,
205 vec![
206 GradientStop::new(0.0, red()),
207 GradientStop::new(1.0, blue()),
208 ],
209 );
210 assert_eq!(g.color_at(Point::new(50.0, 50.0)), red());
211 assert_eq!(g.color_at(Point::new(90.0, 50.0)), blue());
212 assert!(!g.is_vertical());
213 let mid = g.color_at(Point::new(50.0, 70.0));
214 assert!((mid.b - 0.5).abs() < 1e-6, "{mid:?}");
215
216 let dot = Gradient::radial((0.0, 0.0), 0.0, vec![GradientStop::new(0.0, red())]);
217 assert_eq!(dot.color_at(Point::new(10.0, 10.0)), red());
218 }
219
220 #[test]
221 fn stops_are_sorted_and_three_stops_interpolate() {
222 let g = Gradient::linear(
223 (0.0, 0.0),
224 (100.0, 0.0),
225 vec![
226 GradientStop::new(1.0, blue()),
227 GradientStop::new(0.0, red()),
228 GradientStop::new(0.5, Color::rgb8(0, 255, 0)),
229 ],
230 );
231 let at = g.stops().iter().map(|s| s.at).collect::<Vec<_>>();
232 assert_eq!(at, vec![0.0, 0.5, 1.0], "input order does not matter");
233 let mid = g.color_at(Point::new(25.0, 0.0));
234 assert!(
235 (mid.g - 0.5).abs() < 1e-6 && (mid.r - 0.5).abs() < 1e-6,
236 "{mid:?}"
237 );
238 }
239
240 #[test]
241 fn empty_gradient_is_transparent() {
242 let g = Gradient::linear((0.0, 0.0), (10.0, 0.0), Vec::new());
243 assert_eq!(g.color_at(Point::new(5.0, 0.0)), Color::TRANSPARENT);
244 }
245
246 #[test]
247 fn alpha_interpolates_too() {
248 let g = Gradient::linear(
249 (0.0, 0.0),
250 (10.0, 0.0),
251 vec![
252 GradientStop::new(0.0, Color { a: 0.0, ..red() }),
253 GradientStop::new(1.0, Color { a: 1.0, ..red() }),
254 ],
255 );
256 assert!((g.color_at(Point::new(5.0, 0.0)).a - 0.5).abs() < 1e-6);
257 }
258}