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 linear_gradient(colors: Vec<Color>) -> Self {
82 Self::linear_gradient_with_tile_mode(
83 colors,
84 Point { x: 0.0, y: 0.0 },
85 Point {
86 x: f32::INFINITY,
87 y: f32::INFINITY,
88 },
89 TileMode::Clamp,
90 )
91 }
92
93 pub fn linear_gradient_range(colors: Vec<Color>, start: Point, end: Point) -> Self {
96 Self::linear_gradient_with_tile_mode(colors, start, end, TileMode::Clamp)
97 }
98
99 pub fn linear_gradient_with_tile_mode(
102 colors: Vec<Color>,
103 start: Point,
104 end: Point,
105 tile_mode: TileMode,
106 ) -> Self {
107 Brush::LinearGradient {
108 colors,
109 stops: None,
110 start,
111 end,
112 tile_mode,
113 }
114 }
115
116 pub fn linear_gradient_stops(
119 color_stops: Vec<(f32, Color)>,
120 start: Point,
121 end: Point,
122 tile_mode: TileMode,
123 ) -> Self {
124 let (colors, stops) = split_color_stops(color_stops);
125 Brush::LinearGradient {
126 colors,
127 stops: Some(stops),
128 start,
129 end,
130 tile_mode,
131 }
132 }
133
134 pub fn vertical_gradient(colors: Vec<Color>, start_y: f32, end_y: f32) -> Self {
137 Self::vertical_gradient_tiled(colors, start_y, end_y, TileMode::Clamp)
138 }
139
140 pub fn vertical_gradient_tiled(
143 colors: Vec<Color>,
144 start_y: f32,
145 end_y: f32,
146 tile_mode: TileMode,
147 ) -> Self {
148 Self::linear_gradient_with_tile_mode(
149 colors,
150 Point { x: 0.0, y: start_y },
151 Point { x: 0.0, y: end_y },
152 tile_mode,
153 )
154 }
155
156 pub fn vertical_gradient_default(colors: Vec<Color>) -> Self {
159 Self::vertical_gradient_tiled(colors, 0.0, f32::INFINITY, TileMode::Clamp)
160 }
161
162 pub fn vertical_gradient_stops(
165 color_stops: Vec<(f32, Color)>,
166 start_y: f32,
167 end_y: f32,
168 tile_mode: TileMode,
169 ) -> Self {
170 Self::linear_gradient_stops(
171 color_stops,
172 Point { x: 0.0, y: start_y },
173 Point { x: 0.0, y: end_y },
174 tile_mode,
175 )
176 }
177
178 pub fn horizontal_gradient(colors: Vec<Color>, start_x: f32, end_x: f32) -> Self {
181 Self::horizontal_gradient_tiled(colors, start_x, end_x, TileMode::Clamp)
182 }
183
184 pub fn horizontal_gradient_tiled(
187 colors: Vec<Color>,
188 start_x: f32,
189 end_x: f32,
190 tile_mode: TileMode,
191 ) -> Self {
192 Self::linear_gradient_with_tile_mode(
193 colors,
194 Point { x: start_x, y: 0.0 },
195 Point { x: end_x, y: 0.0 },
196 tile_mode,
197 )
198 }
199
200 pub fn horizontal_gradient_default(colors: Vec<Color>) -> Self {
203 Self::horizontal_gradient_tiled(colors, 0.0, f32::INFINITY, TileMode::Clamp)
204 }
205
206 pub fn horizontal_gradient_stops(
209 color_stops: Vec<(f32, Color)>,
210 start_x: f32,
211 end_x: f32,
212 tile_mode: TileMode,
213 ) -> Self {
214 Self::linear_gradient_stops(
215 color_stops,
216 Point { x: start_x, y: 0.0 },
217 Point { x: end_x, y: 0.0 },
218 tile_mode,
219 )
220 }
221
222 pub fn radial_gradient(colors: Vec<Color>, center: Point, radius: f32) -> Self {
225 Self::radial_gradient_tiled(colors, center, radius, TileMode::Clamp)
226 }
227
228 pub fn radial_gradient_tiled(
231 colors: Vec<Color>,
232 center: Point,
233 radius: f32,
234 tile_mode: TileMode,
235 ) -> Self {
236 Brush::RadialGradient {
237 colors,
238 stops: None,
239 center,
240 radius,
241 tile_mode,
242 }
243 }
244
245 pub fn radial_gradient_stops(
248 color_stops: Vec<(f32, Color)>,
249 center: Point,
250 radius: f32,
251 tile_mode: TileMode,
252 ) -> Self {
253 let (colors, stops) = split_color_stops(color_stops);
254 Brush::RadialGradient {
255 colors,
256 stops: Some(stops),
257 center,
258 radius,
259 tile_mode,
260 }
261 }
262
263 pub fn sweep_gradient(colors: Vec<Color>, center: Point) -> Self {
265 Brush::SweepGradient {
266 colors,
267 stops: None,
268 center,
269 }
270 }
271
272 pub fn sweep_gradient_stops(color_stops: Vec<(f32, Color)>, center: Point) -> Self {
275 let (colors, stops) = split_color_stops(color_stops);
276 Brush::SweepGradient {
277 colors,
278 stops: Some(stops),
279 center,
280 }
281 }
282}
283
284impl Brush {
285 pub fn color_at(&self, rect: Rect, point: Point) -> Color {
290 let (x, y) = (point.x, point.y);
291 let (colors, stops, t, tile_mode) = match self {
292 Brush::Solid(color) => return *color,
293 Brush::LinearGradient {
294 colors,
295 stops,
296 start,
297 end,
298 tile_mode,
299 } => {
300 let sx = resolve_gradient_point(rect.x, rect.width, start.x);
301 let sy = resolve_gradient_point(rect.y, rect.height, start.y);
302 let ex = resolve_gradient_point(rect.x, rect.width, end.x);
303 let ey = resolve_gradient_point(rect.y, rect.height, end.y);
304 let (dx, dy) = (ex - sx, ey - sy);
305 let denom = (dx * dx + dy * dy).max(f32::EPSILON);
306 let t = ((x - sx) * dx + (y - sy) * dy) / denom;
307 (colors, stops, t, *tile_mode)
308 }
309 Brush::RadialGradient {
310 colors,
311 stops,
312 center,
313 radius,
314 tile_mode,
315 } => {
316 let dx = x - (rect.x + center.x);
317 let dy = y - (rect.y + center.y);
318 let t = (dx * dx + dy * dy).sqrt() / radius.max(f32::EPSILON);
319 (colors, stops, t, *tile_mode)
320 }
321 Brush::SweepGradient {
322 colors,
323 stops,
324 center,
325 } => {
326 let angle = (y - (rect.y + center.y)).atan2(x - (rect.x + center.x));
327 let t = (angle / std::f32::consts::TAU).rem_euclid(1.0);
329 (colors, stops, t, TileMode::Clamp)
330 }
331 };
332 match tiled_gradient_t(t, tile_mode) {
333 Some(t) => interpolate_colors(colors, stops.as_deref(), t),
334 None => TRANSPARENT,
335 }
336 }
337}
338
339const TRANSPARENT: Color = Color(0.0, 0.0, 0.0, 0.0);
340
341fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
342 if value.is_finite() {
343 origin + value
344 } else if value.is_sign_positive() {
345 origin + extent
346 } else {
347 origin
348 }
349}
350
351fn tiled_gradient_t(t: f32, tile_mode: TileMode) -> Option<f32> {
354 match tile_mode {
355 TileMode::Clamp => Some(t.clamp(0.0, 1.0)),
356 TileMode::Decal => (0.0..=1.0).contains(&t).then_some(t),
357 TileMode::Repeated => Some(t.rem_euclid(1.0)),
358 TileMode::Mirror => {
359 let wrapped = t.rem_euclid(2.0);
360 Some(if wrapped <= 1.0 {
361 wrapped
362 } else {
363 2.0 - wrapped
364 })
365 }
366 }
367}
368
369fn interpolate_colors(colors: &[Color], stops: Option<&[f32]>, t: f32) -> Color {
370 let (Some(&first), Some(&last)) = (colors.first(), colors.last()) else {
371 return TRANSPARENT;
372 };
373 if colors.len() == 1 {
374 return first;
375 }
376 let clamped = t.clamp(0.0, 1.0);
377 if let Some(stops) = stops.filter(|stops| stops.len() == colors.len()) {
378 if stops.first().is_some_and(|&start| clamped <= start) {
379 return first;
380 }
381 for (pair, span) in colors.windows(2).zip(stops.windows(2)) {
382 if clamped <= span[1] {
383 let width = (span[1] - span[0]).max(f32::EPSILON);
384 return lerp_color(
385 pair[0],
386 pair[1],
387 ((clamped - span[0]) / width).clamp(0.0, 1.0),
388 );
389 }
390 }
391 return last;
392 }
393 let scaled = clamped * (colors.len() - 1) as f32;
394 let index = scaled.floor() as usize;
395 match colors.get(index..index + 2) {
396 Some(&[from, to]) => lerp_color(from, to, scaled - index as f32),
397 _ => last,
398 }
399}
400
401fn lerp_color(a: Color, b: Color, t: f32) -> Color {
402 let lerp = |start: f32, end: f32| start + (end - start) * t;
403 Color(
404 lerp(a.0, b.0),
405 lerp(a.1, b.1),
406 lerp(a.2, b.2),
407 lerp(a.3, b.3),
408 )
409}
410
411#[cfg(test)]
412#[path = "tests/brush_tests.rs"]
413mod tests;