cranpose_render_common/
geometry.rs1use 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> {
22 if rhs.width <= 0.0 || rhs.height <= 0.0 {
23 return lhs;
24 }
25
26 Some(match lhs {
27 Some(current) => current.union(rhs),
28 None => rhs,
29 })
30}
31
32pub fn blur_reach_px(radius_px: f32) -> f32 {
40 if radius_px.is_nan() || radius_px <= 0.0 {
41 return 1.0;
42 }
43 let block = blur_scratch_block(radius_px) as f32;
44 let reach = radius_px.min(BLUR_MAX_TAPS as f32 * block) + 3.0 * block + 1.0;
45 (reach / block).ceil() * block
46}
47
48pub fn blur_reach(blur_radius: f32, scale: f32) -> f32 {
51 let scale = if scale.is_finite() && scale > 0.0 {
52 scale
53 } else {
54 1.0
55 };
56 blur_reach_px(blur_radius.max(0.0) * scale) / scale
57}
58
59pub fn blur_reach_for_minimum_scale(blur_radius: f32, minimum_scale: f32) -> f32 {
62 if !minimum_scale.is_finite() || minimum_scale <= 0.0 {
63 return f32::INFINITY;
64 }
65
66 let radius = blur_radius.max(0.0);
67 let minimum = f64::from(minimum_scale);
68 let max_block = f64::from(blur_scratch_block(f32::INFINITY));
69 let additive_bound = f64::from(radius) + (4.0 * max_block + 1.0) / minimum;
70 let saturated_bound = f64::from(blur_reach_px(f32::INFINITY)) / minimum;
71 let bound = additive_bound.min(saturated_bound);
72 let rounded = bound as f32;
73 if rounded.is_finite() && f64::from(rounded) < bound {
74 f32::from_bits(rounded.to_bits() + 1)
75 } else {
76 rounded
77 }
78}
79
80pub fn expand_blurred_rect(
84 mut rect: Rect,
85 blur_radius: f32,
86 scale: f32,
87 clip: Option<Rect>,
88) -> Option<Rect> {
89 let blur_margin = blur_reach(blur_radius, scale);
90 rect.x -= blur_margin;
91 rect.y -= blur_margin;
92 rect.width += blur_margin * 2.0;
93 rect.height += blur_margin * 2.0;
94 if let Some(clip) = clip {
95 rect = rect.intersect(clip)?;
96 }
97 Some(rect)
98}
99
100#[cfg(test)]
101#[path = "tests/geometry_tests.rs"]
102mod tests;
103
104pub const BLUR_TAP_PAIRS: usize = (BLUR_MAX_TAPS / 2) as usize;
107
108#[derive(Clone, Copy, Debug, Default, PartialEq)]
114pub struct BlurTapPair {
115 pub inner: f32,
117 pub outer: f32,
119 pub offset: f32,
121 pub weight: f32,
123}
124
125#[derive(Clone, Copy, Debug, PartialEq)]
129pub struct BlurKernel {
130 pub pairs: [BlurTapPair; BLUR_TAP_PAIRS],
132 pub pair_count: u32,
134 pub total_weight: f32,
136}
137
138impl BlurKernel {
139 pub fn of_radius(radius: f32) -> Self {
142 let radius = radius.max(0.0);
143 let sigma = (radius * 0.5).max(0.001);
144 let tap_count = (radius.ceil() as u32).min(BLUR_MAX_TAPS);
145 let inv_2sigma2 = 1.0 / (2.0 * sigma * sigma);
146 let mut pairs = [BlurTapPair::default(); BLUR_TAP_PAIRS];
147 let mut total_weight = 1.0f32;
148 let mut pair_count = 0;
149 for i in (1..=tap_count).step_by(2) {
150 let fi = i as f32;
151 let fj = fi + 1.0;
152 let inner = (-(fi * fi) * inv_2sigma2).exp();
153 let outer = if i < tap_count {
154 (-(fj * fj) * inv_2sigma2).exp()
155 } else {
156 0.0
157 };
158 total_weight += 2.0 * (inner + outer);
159 let weight = inner + outer;
160 let offset = if weight > 0.0 {
161 (fi * inner + fj * outer) / weight
162 } else {
163 0.0
164 };
165 pairs[pair_count] = BlurTapPair {
166 inner,
167 outer,
168 offset,
169 weight,
170 };
171 pair_count += 1;
172 }
173 Self {
174 pairs,
175 pair_count: pair_count as u32,
176 total_weight,
177 }
178 }
179}
180
181#[cfg(test)]
182#[path = "tests/geometry_blur_kernel_tests.rs"]
183mod blur_kernel_tests;