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