1use cranpose_ui_graphics::{Brush, Color, Rect, TileMode};
2
3const TRANSPARENT: Color = Color(0.0, 0.0, 0.0, 0.0);
4
5#[doc(hidden)]
6pub fn color_to_rgba(color: Color) -> [f32; 4] {
7 [
8 color.0.clamp(0.0, 1.0),
9 color.1.clamp(0.0, 1.0),
10 color.2.clamp(0.0, 1.0),
11 color.3.clamp(0.0, 1.0),
12 ]
13}
14
15#[doc(hidden)]
16pub fn sample_brush_rgba(brush: &Brush, rect: Rect, x: f32, y: f32) -> [f32; 4] {
17 match brush {
18 Brush::Solid(color) => color_to_rgba(*color),
19 Brush::LinearGradient {
20 colors,
21 stops,
22 start,
23 end,
24 tile_mode,
25 } => {
26 let sx = resolve_gradient_point(rect.x, rect.width, start.x);
27 let sy = resolve_gradient_point(rect.y, rect.height, start.y);
28 let ex = resolve_gradient_point(rect.x, rect.width, end.x);
29 let ey = resolve_gradient_point(rect.y, rect.height, end.y);
30 let dx = ex - sx;
31 let dy = ey - sy;
32 let denom = (dx * dx + dy * dy).max(f32::EPSILON);
33 let t = ((x - sx) * dx + (y - sy) * dy) / denom;
34 match normalize_gradient_t(t, *tile_mode) {
35 Some(sample_t) => {
36 color_to_rgba(interpolate_colors(colors, stops.as_deref(), sample_t))
37 }
38 None => color_to_rgba(TRANSPARENT),
39 }
40 }
41 Brush::RadialGradient {
42 colors,
43 stops,
44 center,
45 radius,
46 tile_mode,
47 } => {
48 let cx = rect.x + center.x;
49 let cy = rect.y + center.y;
50 let radius = (*radius).max(f32::EPSILON);
51 let dx = x - cx;
52 let dy = y - cy;
53 let distance = (dx * dx + dy * dy).sqrt();
54 let t = distance / radius;
55 match normalize_gradient_t(t, *tile_mode) {
56 Some(sample_t) => {
57 color_to_rgba(interpolate_colors(colors, stops.as_deref(), sample_t))
58 }
59 None => color_to_rgba(TRANSPARENT),
60 }
61 }
62 Brush::SweepGradient {
63 colors,
64 stops,
65 center,
66 } => {
67 let cx = rect.x + center.x;
68 let cy = rect.y + center.y;
69 let dx = x - cx;
70 let dy = y - cy;
71 let angle = dy.atan2(dx);
72 let t = (angle / std::f32::consts::TAU + 0.5).clamp(0.0, 1.0);
73 color_to_rgba(interpolate_colors(colors, stops.as_deref(), t))
74 }
75 }
76}
77
78fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
79 if value.is_finite() {
80 origin + value
81 } else if value.is_sign_positive() {
82 origin + extent
83 } else {
84 origin
85 }
86}
87
88#[doc(hidden)]
89pub fn normalize_gradient_t(t: f32, tile_mode: TileMode) -> Option<f32> {
90 match tile_mode {
91 TileMode::Clamp => Some(t.clamp(0.0, 1.0)),
92 TileMode::Decal => {
93 if (0.0..=1.0).contains(&t) {
94 Some(t)
95 } else {
96 None
97 }
98 }
99 TileMode::Repeated => Some(t.rem_euclid(1.0)),
100 TileMode::Mirror => {
101 let wrapped = t.rem_euclid(2.0);
102 if wrapped <= 1.0 {
103 Some(wrapped)
104 } else {
105 Some(2.0 - wrapped)
106 }
107 }
108 }
109}
110
111fn interpolate_colors(colors: &[Color], stops: Option<&[f32]>, t: f32) -> Color {
112 if colors.is_empty() {
113 return TRANSPARENT;
114 }
115 if colors.len() == 1 {
116 return colors[0];
117 }
118 let clamped = t.clamp(0.0, 1.0);
119
120 if let Some(stops) = stops {
121 if stops.len() == colors.len() {
122 if clamped <= stops[0] {
123 return colors[0];
124 }
125 for index in 0..(stops.len() - 1) {
126 let start = stops[index];
127 let end = stops[index + 1];
128 if clamped <= end {
129 let span = (end - start).max(f32::EPSILON);
130 let frac = ((clamped - start) / span).clamp(0.0, 1.0);
131 return lerp_color(colors[index], colors[index + 1], frac);
132 }
133 }
134 return last_color(colors);
135 }
136 }
137
138 let segments = (colors.len() - 1) as f32;
139 let scaled = clamped * segments;
140 let index = scaled.floor() as usize;
141 if index >= colors.len() - 1 {
142 return last_color(colors);
143 }
144 let frac = scaled - index as f32;
145 lerp_color(colors[index], colors[index + 1], frac)
146}
147
148fn last_color(colors: &[Color]) -> Color {
149 colors.last().copied().unwrap_or(TRANSPARENT)
150}
151
152fn lerp_color(a: Color, b: Color, t: f32) -> Color {
153 let lerp = |start: f32, end: f32| start + (end - start) * t;
154 Color(
155 lerp(a.0, b.0),
156 lerp(a.1, b.1),
157 lerp(a.2, b.2),
158 lerp(a.3, b.3),
159 )
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use cranpose_ui_graphics::Point;
166
167 fn sample_rect() -> Rect {
168 Rect {
169 x: 0.0,
170 y: 0.0,
171 width: 100.0,
172 height: 40.0,
173 }
174 }
175
176 #[test]
177 fn empty_gradient_samples_transparent_instead_of_panicking() {
178 let brush =
179 Brush::linear_gradient_range(Vec::new(), Point::new(0.0, 0.0), Point::new(100.0, 0.0));
180 assert_eq!(
181 sample_brush_rgba(&brush, sample_rect(), 50.0, 10.0),
182 [0.0, 0.0, 0.0, 0.0]
183 );
184 }
185
186 #[test]
187 fn clamped_gradient_samples_last_color_at_end() {
188 let brush = Brush::linear_gradient_range(
189 vec![Color::RED, Color::BLUE],
190 Point::new(0.0, 0.0),
191 Point::new(100.0, 0.0),
192 );
193 assert_eq!(
194 sample_brush_rgba(&brush, sample_rect(), 120.0, 10.0),
195 color_to_rgba(Color::BLUE)
196 );
197 }
198
199 #[test]
200 fn mirror_tile_mode_normalizes_across_repeated_segments() {
201 assert_eq!(normalize_gradient_t(1.25, TileMode::Mirror), Some(0.75));
202 assert_eq!(normalize_gradient_t(1.75, TileMode::Mirror), Some(0.25));
203 }
204}