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
15const LEVEL: f32 = 1.0 / 255.0;
17
18pub fn gradient_dither_offset(x: f32, y: f32) -> f32 {
51 let px = x.floor().max(0.0) as u32 + 1;
52 let py = y.floor().max(0.0) as u32 + 1;
53 let m = ((py & 1) << 3) | ((px & 1) << 2) | (py & 2) | ((px & 2) >> 1);
54 m as f32 * (1.0 / 16.0) - (15.0 / 32.0)
55}
56
57fn dither_gradient(rgba: [f32; 4], x: f32, y: f32) -> [f32; 4] {
66 if rgba[3] <= 0.0 {
67 return rgba;
68 }
69 let offset = gradient_dither_offset(x, y) * LEVEL;
70 [
71 (rgba[0] + offset).clamp(0.0, 1.0),
72 (rgba[1] + offset).clamp(0.0, 1.0),
73 (rgba[2] + offset).clamp(0.0, 1.0),
74 rgba[3],
75 ]
76}
77
78#[doc(hidden)]
79pub fn sample_brush_rgba(brush: &Brush, rect: Rect, x: f32, y: f32) -> [f32; 4] {
80 match brush {
81 Brush::Solid(color) => color_to_rgba(*color),
82 Brush::LinearGradient {
83 colors,
84 stops,
85 start,
86 end,
87 tile_mode,
88 } => {
89 let sx = resolve_gradient_point(rect.x, rect.width, start.x);
90 let sy = resolve_gradient_point(rect.y, rect.height, start.y);
91 let ex = resolve_gradient_point(rect.x, rect.width, end.x);
92 let ey = resolve_gradient_point(rect.y, rect.height, end.y);
93 let dx = ex - sx;
94 let dy = ey - sy;
95 let denom = (dx * dx + dy * dy).max(f32::EPSILON);
96 let t = ((x - sx) * dx + (y - sy) * dy) / denom;
97 match normalize_gradient_t(t, *tile_mode) {
98 Some(sample_t) => dither_gradient(
99 color_to_rgba(interpolate_colors(colors, stops.as_deref(), sample_t)),
100 x,
101 y,
102 ),
103 None => color_to_rgba(TRANSPARENT),
104 }
105 }
106 Brush::RadialGradient {
107 colors,
108 stops,
109 center,
110 radius,
111 tile_mode,
112 } => {
113 let cx = rect.x + center.x;
114 let cy = rect.y + center.y;
115 let radius = (*radius).max(f32::EPSILON);
116 let dx = x - cx;
117 let dy = y - cy;
118 let distance = (dx * dx + dy * dy).sqrt();
119 let t = distance / radius;
120 match normalize_gradient_t(t, *tile_mode) {
121 Some(sample_t) => dither_gradient(
122 color_to_rgba(interpolate_colors(colors, stops.as_deref(), sample_t)),
123 x,
124 y,
125 ),
126 None => color_to_rgba(TRANSPARENT),
127 }
128 }
129 Brush::SweepGradient {
130 colors,
131 stops,
132 center,
133 } => {
134 let cx = rect.x + center.x;
135 let cy = rect.y + center.y;
136 let dx = x - cx;
137 let dy = y - cy;
138 let angle = dy.atan2(dx);
139 let t = (angle / std::f32::consts::TAU + 0.5).clamp(0.0, 1.0);
140 dither_gradient(
141 color_to_rgba(interpolate_colors(colors, stops.as_deref(), t)),
142 x,
143 y,
144 )
145 }
146 }
147}
148
149fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
150 if value.is_finite() {
151 origin + value
152 } else if value.is_sign_positive() {
153 origin + extent
154 } else {
155 origin
156 }
157}
158
159#[doc(hidden)]
160pub fn normalize_gradient_t(t: f32, tile_mode: TileMode) -> Option<f32> {
161 match tile_mode {
162 TileMode::Clamp => Some(t.clamp(0.0, 1.0)),
163 TileMode::Decal => {
164 if (0.0..=1.0).contains(&t) {
165 Some(t)
166 } else {
167 None
168 }
169 }
170 TileMode::Repeated => Some(t.rem_euclid(1.0)),
171 TileMode::Mirror => {
172 let wrapped = t.rem_euclid(2.0);
173 if wrapped <= 1.0 {
174 Some(wrapped)
175 } else {
176 Some(2.0 - wrapped)
177 }
178 }
179 }
180}
181
182fn interpolate_colors(colors: &[Color], stops: Option<&[f32]>, t: f32) -> Color {
183 if colors.is_empty() {
184 return TRANSPARENT;
185 }
186 if colors.len() == 1 {
187 return colors[0];
188 }
189 let clamped = t.clamp(0.0, 1.0);
190
191 if let Some(stops) = stops {
192 if stops.len() == colors.len() {
193 if clamped <= stops[0] {
194 return colors[0];
195 }
196 for index in 0..(stops.len() - 1) {
197 let start = stops[index];
198 let end = stops[index + 1];
199 if clamped <= end {
200 let span = (end - start).max(f32::EPSILON);
201 let frac = ((clamped - start) / span).clamp(0.0, 1.0);
202 return lerp_color(colors[index], colors[index + 1], frac);
203 }
204 }
205 return last_color(colors);
206 }
207 }
208
209 let segments = (colors.len() - 1) as f32;
210 let scaled = clamped * segments;
211 let index = scaled.floor() as usize;
212 if index >= colors.len() - 1 {
213 return last_color(colors);
214 }
215 let frac = scaled - index as f32;
216 lerp_color(colors[index], colors[index + 1], frac)
217}
218
219fn last_color(colors: &[Color]) -> Color {
220 colors.last().copied().unwrap_or(TRANSPARENT)
221}
222
223fn lerp_color(a: Color, b: Color, t: f32) -> Color {
224 let lerp = |start: f32, end: f32| start + (end - start) * t;
225 Color(
226 lerp(a.0, b.0),
227 lerp(a.1, b.1),
228 lerp(a.2, b.2),
229 lerp(a.3, b.3),
230 )
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use cranpose_ui_graphics::Point;
237
238 fn sample_rect() -> Rect {
239 Rect {
240 x: 0.0,
241 y: 0.0,
242 width: 100.0,
243 height: 40.0,
244 }
245 }
246
247 #[test]
248 fn empty_gradient_samples_transparent_instead_of_panicking() {
249 let brush =
250 Brush::linear_gradient_range(Vec::new(), Point::new(0.0, 0.0), Point::new(100.0, 0.0));
251 assert_eq!(
252 sample_brush_rgba(&brush, sample_rect(), 50.0, 10.0),
253 [0.0, 0.0, 0.0, 0.0]
254 );
255 }
256
257 #[test]
258 fn clamped_gradient_samples_last_color_at_end() {
259 let brush = Brush::linear_gradient_range(
260 vec![Color::RED, Color::BLUE],
261 Point::new(0.0, 0.0),
262 Point::new(100.0, 0.0),
263 );
264 for y in 0..4 {
268 for x in 0..4 {
269 let sampled = sample_brush_rgba(&brush, sample_rect(), 120.0 + x as f32, y as f32);
270 let bytes: Vec<u8> = sampled.iter().map(|c| (c * 255.0).round() as u8).collect();
271 assert_eq!(bytes, vec![0, 0, 255, 255], "cell ({x}, {y})");
272 }
273 }
274 }
275
276 #[test]
277 fn mirror_tile_mode_normalizes_across_repeated_segments() {
278 assert_eq!(normalize_gradient_t(1.25, TileMode::Mirror), Some(0.75));
279 assert_eq!(normalize_gradient_t(1.75, TileMode::Mirror), Some(0.25));
280 }
281
282 const BAYER_4X4: [[u32; 4]; 4] = [[0, 4, 1, 5], [8, 12, 9, 13], [2, 6, 3, 7], [10, 14, 11, 15]];
286
287 #[test]
288 fn the_dither_lays_out_skias_bayer_matrix() {
289 for y in 0..4u32 {
290 for x in 0..4u32 {
291 let expected = BAYER_4X4[y as usize][x as usize] as f32 / 16.0 - 15.0 / 32.0;
292 assert_eq!(
296 gradient_dither_offset(x as f32 - 1.0 + 4.0, y as f32 - 1.0 + 4.0),
297 expected,
298 "cell ({x}, {y})"
299 );
300 }
301 }
302 }
303
304 #[test]
305 fn the_dither_is_a_pixel_ahead_of_the_fragment() {
306 assert_eq!(
307 gradient_dither_offset(4.0, 4.0),
308 gradient_dither_offset(5.0 - 1.0, 5.0 - 1.0),
309 );
310 assert_eq!(
311 gradient_dither_offset(3.0, 3.0),
312 BAYER_4X4[0][0] as f32 / 16.0 - 15.0 / 32.0,
313 );
314 }
315
316 #[test]
317 fn the_dither_repeats_every_four_pixels_and_never_moves_a_whole_level() {
318 for y in 0..16u32 {
319 for x in 0..16u32 {
320 assert_eq!(
321 gradient_dither_offset(x as f32, y as f32),
322 gradient_dither_offset((x % 4) as f32, (y % 4) as f32),
323 );
324 }
325 }
326 let offsets: Vec<f32> = (0..4)
327 .flat_map(|y| (0..4).map(move |x| gradient_dither_offset(x as f32, y as f32)))
328 .collect();
329 assert!(offsets.iter().all(|offset| offset.abs() < 0.5));
330 let mean = offsets.iter().sum::<f32>() / offsets.len() as f32;
333 assert!(mean.abs() < 1e-6, "mean offset {mean}");
334 }
335
336 #[test]
337 fn a_solid_brush_is_left_alone() {
338 let brush = Brush::Solid(Color(0.25, 0.5, 0.75, 1.0));
339 for y in 0..4 {
340 for x in 0..4 {
341 assert_eq!(
342 sample_brush_rgba(&brush, sample_rect(), x as f32, y as f32),
343 [0.25, 0.5, 0.75, 1.0],
344 );
345 }
346 }
347 }
348
349 #[test]
350 fn the_dither_moves_a_flat_gradient_off_one_value_onto_two() {
351 let grey = 100.4 / 255.0;
355 let brush = Brush::linear_gradient_range(
356 vec![Color(grey, grey, grey, 1.0), Color(grey, grey, grey, 1.0)],
357 Point::new(0.0, 0.0),
358 Point::new(100.0, 0.0),
359 );
360 let mut levels = std::collections::BTreeSet::new();
361 for y in 0..4 {
362 for x in 0..4 {
363 let sampled = sample_brush_rgba(&brush, sample_rect(), x as f32, y as f32);
364 levels.insert((sampled[0] * 255.0).round() as u8);
365 }
366 }
367 assert_eq!(levels.into_iter().collect::<Vec<_>>(), vec![100, 101]);
368 }
369}