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