1use cranpose_ui_graphics::Rect;
2
3pub const BLUR_MAX_TAPS: u32 = 32;
6
7pub fn blur_scratch_block(radius_px: f32) -> u32 {
10 if radius_px < 6.0 {
11 1
12 } else if radius_px < 16.0 {
13 2
14 } else {
15 4
16 }
17}
18
19pub fn union_rect(lhs: Option<Rect>, rhs: Rect) -> Option<Rect> {
20 if rhs.width <= 0.0 || rhs.height <= 0.0 {
21 return lhs;
22 }
23
24 Some(match lhs {
25 Some(current) => current.union(rhs),
26 None => rhs,
27 })
28}
29
30pub fn blur_reach_px(radius_px: f32) -> f32 {
38 if radius_px.is_nan() || radius_px <= 0.0 {
39 return 1.0;
40 }
41 let block = blur_scratch_block(radius_px) as f32;
42 let reach = radius_px.min(BLUR_MAX_TAPS as f32 * block) + 3.0 * block + 1.0;
43 (reach / block).ceil() * block
44}
45
46pub fn blur_reach(blur_radius: f32, scale: f32) -> f32 {
49 let scale = if scale.is_finite() && scale > 0.0 {
50 scale
51 } else {
52 1.0
53 };
54 blur_reach_px(blur_radius.max(0.0) * scale) / scale
55}
56
57pub fn expand_blurred_rect(
58 mut rect: Rect,
59 blur_radius: f32,
60 scale: f32,
61 clip: Option<Rect>,
62) -> Option<Rect> {
63 let blur_margin = blur_reach(blur_radius, scale);
64 rect.x -= blur_margin;
65 rect.y -= blur_margin;
66 rect.width += blur_margin * 2.0;
67 rect.height += blur_margin * 2.0;
68 if let Some(clip) = clip {
69 rect = rect.intersect(clip)?;
70 }
71 Some(rect)
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
79 fn union_rect_ignores_empty_rhs() {
80 let lhs = Some(Rect {
81 x: 1.0,
82 y: 2.0,
83 width: 3.0,
84 height: 4.0,
85 });
86 let rhs = Rect {
87 x: 5.0,
88 y: 6.0,
89 width: 0.0,
90 height: 7.0,
91 };
92
93 assert_eq!(union_rect(lhs, rhs), lhs);
94 }
95
96 #[test]
97 fn union_rect_merges_extents() {
98 let lhs = Some(Rect {
99 x: 8.0,
100 y: 4.0,
101 width: 3.0,
102 height: 5.0,
103 });
104 let rhs = Rect {
105 x: 2.0,
106 y: 7.0,
107 width: 12.0,
108 height: 4.0,
109 };
110
111 assert_eq!(
112 union_rect(lhs, rhs),
113 Some(Rect {
114 x: 2.0,
115 y: 4.0,
116 width: 12.0,
117 height: 7.0,
118 })
119 );
120 }
121
122 #[test]
123 fn a_blur_reaches_its_kernel_and_its_scratch_blocks_past_the_source() {
124 assert_eq!(blur_reach_px(0.0), 1.0);
125 assert_eq!(blur_reach_px(-5.0), 1.0);
126 assert_eq!(blur_reach_px(2.0), 6.0);
127 assert_eq!(blur_reach_px(10.0), 18.0);
128 assert_eq!(blur_reach_px(44.0), 60.0);
129 assert_eq!(blur_reach_px(200.0), 144.0);
130 }
131
132 #[test]
133 fn the_logical_reach_follows_the_device_scale() {
134 assert_eq!(blur_reach(2.0, 1.0), 6.0);
135 assert!((blur_reach(20.0, 2.25) - 60.0 / 2.25).abs() < 1e-5);
136 assert_eq!(blur_reach(2.0, 0.0), 6.0);
137 assert_eq!(blur_reach(2.0, f32::NAN), 6.0);
138 }
139
140 #[test]
141 fn expand_blurred_rect_applies_margin_and_clip() {
142 let expanded = expand_blurred_rect(
143 Rect {
144 x: 10.0,
145 y: 20.0,
146 width: 30.0,
147 height: 40.0,
148 },
149 2.0,
150 1.0,
151 Some(Rect {
152 x: 8.0,
153 y: 18.0,
154 width: 20.0,
155 height: 20.0,
156 }),
157 );
158
159 assert_eq!(
160 expanded,
161 Some(Rect {
162 x: 8.0,
163 y: 18.0,
164 width: 20.0,
165 height: 20.0,
166 })
167 );
168 }
169}
170
171pub const BLUR_TAP_PAIRS: usize = (BLUR_MAX_TAPS / 2) as usize;
174
175#[derive(Clone, Copy, Debug, Default, PartialEq)]
181pub struct BlurTapPair {
182 pub inner: f32,
183 pub outer: f32,
184 pub offset: f32,
185 pub weight: f32,
186}
187
188#[derive(Clone, Copy, Debug, PartialEq)]
192pub struct BlurKernel {
193 pub pairs: [BlurTapPair; BLUR_TAP_PAIRS],
194 pub pair_count: u32,
195 pub total_weight: f32,
196}
197
198impl BlurKernel {
199 pub fn of_radius(radius: f32) -> Self {
202 let radius = radius.max(0.0);
203 let sigma = (radius * 0.5).max(0.001);
204 let tap_count = (radius.ceil() as u32).min(BLUR_MAX_TAPS);
205 let inv_2sigma2 = 1.0 / (2.0 * sigma * sigma);
206 let mut pairs = [BlurTapPair::default(); BLUR_TAP_PAIRS];
207 let mut total_weight = 1.0f32;
208 let mut pair_count = 0;
209 for i in (1..=tap_count).step_by(2) {
210 let fi = i as f32;
211 let fj = fi + 1.0;
212 let inner = (-(fi * fi) * inv_2sigma2).exp();
213 let outer = if i < tap_count {
214 (-(fj * fj) * inv_2sigma2).exp()
215 } else {
216 0.0
217 };
218 total_weight += 2.0 * (inner + outer);
219 let weight = inner + outer;
220 let offset = if weight > 0.0 {
221 (fi * inner + fj * outer) / weight
222 } else {
223 0.0
224 };
225 pairs[pair_count] = BlurTapPair {
226 inner,
227 outer,
228 offset,
229 weight,
230 };
231 pair_count += 1;
232 }
233 Self {
234 pairs,
235 pair_count: pair_count as u32,
236 total_weight,
237 }
238 }
239}
240
241#[cfg(test)]
242mod blur_kernel_tests {
243 use super::*;
244
245 fn gaussian(tap: u32, radius: f32) -> f32 {
246 let sigma = radius * 0.5;
247 (-((tap * tap) as f32) / (2.0 * sigma * sigma)).exp()
248 }
249
250 #[test]
251 fn a_kernel_pairs_its_taps_where_their_weights_meet() {
252 let kernel = BlurKernel::of_radius(5.5);
253 assert_eq!(
254 kernel.pair_count, 3,
255 "a radius of 5.5 takes six taps a side"
256 );
257 let mut total = 1.0;
258 for (k, pair) in kernel.pairs[..3].iter().enumerate() {
259 let (i, j) = (2 * k as u32 + 1, 2 * k as u32 + 2);
260 assert!((pair.inner - gaussian(i, 5.5)).abs() <= 1e-6);
261 assert!((pair.outer - gaussian(j, 5.5)).abs() <= 1e-6);
262 assert_eq!(pair.weight, pair.inner + pair.outer);
263 assert!(pair.offset > i as f32 && pair.offset < j as f32);
264 total += 2.0 * (pair.inner + pair.outer);
265 }
266 assert_eq!(kernel.total_weight, total);
267 assert_eq!(kernel.pairs[3], BlurTapPair::default());
268 }
269
270 #[test]
271 fn an_odd_tap_count_leaves_its_last_fetch_on_the_inner_tap() {
272 let kernel = BlurKernel::of_radius(7.0);
273 assert_eq!(kernel.pair_count, 4);
274 let tail = kernel.pairs[3];
275 assert_eq!(tail.outer, 0.0);
276 assert_eq!(tail.offset, 7.0);
277 assert_eq!(tail.weight, tail.inner);
278 }
279
280 #[test]
281 fn a_kernel_stops_at_the_tap_cap_and_a_zero_radius_has_no_pairs() {
282 let capped = BlurKernel::of_radius(100.0);
283 assert_eq!(capped.pair_count, BLUR_TAP_PAIRS as u32);
284 assert!(capped.pairs[BLUR_TAP_PAIRS - 1].outer > 0.0);
285 let none = BlurKernel::of_radius(0.0);
286 assert_eq!((none.pair_count, none.total_weight), (0, 1.0));
287 }
288}