1use crate::{
4 color::Color,
5 geometry::{Point, Rect},
6 render_effect::TileMode,
7};
8
9#[derive(Clone, Debug, PartialEq)]
11pub enum Brush {
12 Solid(Color),
14 LinearGradient {
16 colors: Vec<Color>,
18 stops: Option<Vec<f32>>,
21 start: Point,
24 end: Point,
27 tile_mode: TileMode,
29 },
30 RadialGradient {
32 colors: Vec<Color>,
34 stops: Option<Vec<f32>>,
36 center: Point,
38 radius: f32,
40 tile_mode: TileMode,
42 },
43 SweepGradient {
46 colors: Vec<Color>,
48 stops: Option<Vec<f32>>,
50 center: Point,
52 },
53}
54
55fn split_color_stops(color_stops: Vec<(f32, Color)>) -> (Vec<Color>, Vec<f32>) {
56 let mut colors = Vec::with_capacity(color_stops.len());
57 let mut stops = Vec::with_capacity(color_stops.len());
58 for (stop, color) in color_stops {
59 colors.push(color);
60 stops.push(stop);
61 }
62 (colors, stops)
63}
64
65impl From<Color> for Brush {
68 fn from(color: Color) -> Self {
69 Brush::Solid(color)
70 }
71}
72
73impl Brush {
74 pub fn solid(color: Color) -> Self {
76 Brush::Solid(color)
77 }
78
79 pub fn colors(&self) -> &[Color] {
81 match self {
82 Brush::Solid(color) => std::slice::from_ref(color),
83 Brush::LinearGradient { colors, .. }
84 | Brush::RadialGradient { colors, .. }
85 | Brush::SweepGradient { colors, .. } => colors,
86 }
87 }
88
89 pub fn paints(&self) -> bool {
92 self.colors().iter().any(|color| color.3 > 0.0)
93 }
94
95 pub fn linear_gradient(colors: Vec<Color>) -> Self {
98 Self::linear_gradient_with_tile_mode(
99 colors,
100 Point { x: 0.0, y: 0.0 },
101 Point {
102 x: f32::INFINITY,
103 y: f32::INFINITY,
104 },
105 TileMode::Clamp,
106 )
107 }
108
109 pub fn linear_gradient_range(colors: Vec<Color>, start: Point, end: Point) -> Self {
112 Self::linear_gradient_with_tile_mode(colors, start, end, TileMode::Clamp)
113 }
114
115 pub fn linear_gradient_with_tile_mode(
118 colors: Vec<Color>,
119 start: Point,
120 end: Point,
121 tile_mode: TileMode,
122 ) -> Self {
123 Brush::LinearGradient {
124 colors,
125 stops: None,
126 start,
127 end,
128 tile_mode,
129 }
130 }
131
132 pub fn linear_gradient_stops(
135 color_stops: Vec<(f32, Color)>,
136 start: Point,
137 end: Point,
138 tile_mode: TileMode,
139 ) -> Self {
140 let (colors, stops) = split_color_stops(color_stops);
141 Brush::LinearGradient {
142 colors,
143 stops: Some(stops),
144 start,
145 end,
146 tile_mode,
147 }
148 }
149
150 pub fn vertical_gradient(colors: Vec<Color>, start_y: f32, end_y: f32) -> Self {
153 Self::vertical_gradient_tiled(colors, start_y, end_y, TileMode::Clamp)
154 }
155
156 pub fn vertical_gradient_tiled(
159 colors: Vec<Color>,
160 start_y: f32,
161 end_y: f32,
162 tile_mode: TileMode,
163 ) -> Self {
164 Self::linear_gradient_with_tile_mode(
165 colors,
166 Point { x: 0.0, y: start_y },
167 Point { x: 0.0, y: end_y },
168 tile_mode,
169 )
170 }
171
172 pub fn vertical_gradient_default(colors: Vec<Color>) -> Self {
175 Self::vertical_gradient_tiled(colors, 0.0, f32::INFINITY, TileMode::Clamp)
176 }
177
178 pub fn vertical_gradient_stops(
181 color_stops: Vec<(f32, Color)>,
182 start_y: f32,
183 end_y: f32,
184 tile_mode: TileMode,
185 ) -> Self {
186 Self::linear_gradient_stops(
187 color_stops,
188 Point { x: 0.0, y: start_y },
189 Point { x: 0.0, y: end_y },
190 tile_mode,
191 )
192 }
193
194 pub fn horizontal_gradient(colors: Vec<Color>, start_x: f32, end_x: f32) -> Self {
197 Self::horizontal_gradient_tiled(colors, start_x, end_x, TileMode::Clamp)
198 }
199
200 pub fn horizontal_gradient_tiled(
203 colors: Vec<Color>,
204 start_x: f32,
205 end_x: f32,
206 tile_mode: TileMode,
207 ) -> Self {
208 Self::linear_gradient_with_tile_mode(
209 colors,
210 Point { x: start_x, y: 0.0 },
211 Point { x: end_x, y: 0.0 },
212 tile_mode,
213 )
214 }
215
216 pub fn horizontal_gradient_default(colors: Vec<Color>) -> Self {
219 Self::horizontal_gradient_tiled(colors, 0.0, f32::INFINITY, TileMode::Clamp)
220 }
221
222 pub fn horizontal_gradient_stops(
225 color_stops: Vec<(f32, Color)>,
226 start_x: f32,
227 end_x: f32,
228 tile_mode: TileMode,
229 ) -> Self {
230 Self::linear_gradient_stops(
231 color_stops,
232 Point { x: start_x, y: 0.0 },
233 Point { x: end_x, y: 0.0 },
234 tile_mode,
235 )
236 }
237
238 pub fn radial_gradient(colors: Vec<Color>, center: Point, radius: f32) -> Self {
241 Self::radial_gradient_tiled(colors, center, radius, TileMode::Clamp)
242 }
243
244 pub fn radial_gradient_tiled(
247 colors: Vec<Color>,
248 center: Point,
249 radius: f32,
250 tile_mode: TileMode,
251 ) -> Self {
252 Brush::RadialGradient {
253 colors,
254 stops: None,
255 center,
256 radius,
257 tile_mode,
258 }
259 }
260
261 pub fn radial_gradient_stops(
264 color_stops: Vec<(f32, Color)>,
265 center: Point,
266 radius: f32,
267 tile_mode: TileMode,
268 ) -> Self {
269 let (colors, stops) = split_color_stops(color_stops);
270 Brush::RadialGradient {
271 colors,
272 stops: Some(stops),
273 center,
274 radius,
275 tile_mode,
276 }
277 }
278
279 pub fn sweep_gradient(colors: Vec<Color>, center: Point) -> Self {
281 Brush::SweepGradient {
282 colors,
283 stops: None,
284 center,
285 }
286 }
287
288 pub fn sweep_gradient_stops(color_stops: Vec<(f32, Color)>, center: Point) -> Self {
291 let (colors, stops) = split_color_stops(color_stops);
292 Brush::SweepGradient {
293 colors,
294 stops: Some(stops),
295 center,
296 }
297 }
298}
299
300impl Brush {
301 pub fn color_at(&self, rect: Rect, point: Point) -> Color {
306 let (x, y) = (point.x, point.y);
307 let (colors, stops, t, tile_mode) = match self {
308 Brush::Solid(color) => return *color,
309 Brush::LinearGradient {
310 colors,
311 stops,
312 start,
313 end,
314 tile_mode,
315 } => {
316 let sx = resolve_gradient_point(rect.x, rect.width, start.x);
317 let sy = resolve_gradient_point(rect.y, rect.height, start.y);
318 let ex = resolve_gradient_point(rect.x, rect.width, end.x);
319 let ey = resolve_gradient_point(rect.y, rect.height, end.y);
320 let (dx, dy) = (ex - sx, ey - sy);
321 let denom = (dx * dx + dy * dy).max(f32::EPSILON);
322 let t = ((x - sx) * dx + (y - sy) * dy) / denom;
323 (colors, stops, t, *tile_mode)
324 }
325 Brush::RadialGradient {
326 colors,
327 stops,
328 center,
329 radius,
330 tile_mode,
331 } => {
332 let dx = x - (rect.x + center.x);
333 let dy = y - (rect.y + center.y);
334 let t = (dx * dx + dy * dy).sqrt() / radius.max(f32::EPSILON);
335 (colors, stops, t, *tile_mode)
336 }
337 Brush::SweepGradient {
338 colors,
339 stops,
340 center,
341 } => {
342 let angle = (y - (rect.y + center.y)).atan2(x - (rect.x + center.x));
343 let t = (angle / std::f32::consts::TAU).rem_euclid(1.0);
345 (colors, stops, t, TileMode::Clamp)
346 }
347 };
348 match tiled_gradient_t(t, tile_mode) {
349 Some(t) => interpolate_colors(colors, stops.as_deref(), t),
350 None => TRANSPARENT,
351 }
352 }
353}
354
355const TRANSPARENT: Color = Color(0.0, 0.0, 0.0, 0.0);
356
357fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
358 if value.is_finite() {
359 origin + value
360 } else if value.is_sign_positive() {
361 origin + extent
362 } else {
363 origin
364 }
365}
366
367fn tiled_gradient_t(t: f32, tile_mode: TileMode) -> Option<f32> {
370 match tile_mode {
371 TileMode::Clamp => Some(t.clamp(0.0, 1.0)),
372 TileMode::Decal => (0.0..=1.0).contains(&t).then_some(t),
373 TileMode::Repeated => Some(t.rem_euclid(1.0)),
374 TileMode::Mirror => {
375 let wrapped = t.rem_euclid(2.0);
376 Some(if wrapped <= 1.0 {
377 wrapped
378 } else {
379 2.0 - wrapped
380 })
381 }
382 }
383}
384
385fn interpolate_colors(colors: &[Color], stops: Option<&[f32]>, t: f32) -> Color {
386 let (Some(&first), Some(&last)) = (colors.first(), colors.last()) else {
387 return TRANSPARENT;
388 };
389 if colors.len() == 1 {
390 return first;
391 }
392 let clamped = t.clamp(0.0, 1.0);
393 if let Some(stops) = stops.filter(|stops| stops.len() == colors.len()) {
394 if stops.first().is_some_and(|&start| clamped <= start) {
395 return first;
396 }
397 for (pair, span) in colors.windows(2).zip(stops.windows(2)) {
398 if clamped <= span[1] {
399 let width = (span[1] - span[0]).max(f32::EPSILON);
400 return lerp_color(
401 pair[0],
402 pair[1],
403 ((clamped - span[0]) / width).clamp(0.0, 1.0),
404 );
405 }
406 }
407 return last;
408 }
409 let scaled = clamped * (colors.len() - 1) as f32;
410 let index = scaled.floor() as usize;
411 match colors.get(index..index + 2) {
412 Some(&[from, to]) => lerp_color(from, to, scaled - index as f32),
413 _ => last,
414 }
415}
416
417fn lerp_color(a: Color, b: Color, t: f32) -> Color {
418 let lerp = |start: f32, end: f32| start + (end - start) * t;
419 Color(
420 lerp(a.0, b.0),
421 lerp(a.1, b.1),
422 lerp(a.2, b.2),
423 lerp(a.3, b.3),
424 )
425}
426
427#[cfg(test)]
428#[path = "tests/brush_tests.rs"]
429mod tests;