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> {
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 blur_reach_for_minimum_scale(blur_radius: f32, minimum_scale: f32) -> f32 {
60 if !minimum_scale.is_finite() || minimum_scale <= 0.0 {
61 return f32::INFINITY;
62 }
63
64 let radius = blur_radius.max(0.0);
65 let minimum = f64::from(minimum_scale);
66 let max_block = f64::from(blur_scratch_block(f32::INFINITY));
67 let additive_bound = f64::from(radius) + (4.0 * max_block + 1.0) / minimum;
68 let saturated_bound = f64::from(blur_reach_px(f32::INFINITY)) / minimum;
69 let bound = additive_bound.min(saturated_bound);
70 let rounded = bound as f32;
71 if rounded.is_finite() && f64::from(rounded) < bound {
72 f32::from_bits(rounded.to_bits() + 1)
73 } else {
74 rounded
75 }
76}
77
78pub fn expand_blurred_rect(
79 mut rect: Rect,
80 blur_radius: f32,
81 scale: f32,
82 clip: Option<Rect>,
83) -> Option<Rect> {
84 let blur_margin = blur_reach(blur_radius, scale);
85 rect.x -= blur_margin;
86 rect.y -= blur_margin;
87 rect.width += blur_margin * 2.0;
88 rect.height += blur_margin * 2.0;
89 if let Some(clip) = clip {
90 rect = rect.intersect(clip)?;
91 }
92 Some(rect)
93}
94
95#[cfg(test)]
96#[path = "tests/geometry_tests.rs"]
97mod tests;
98
99pub const BLUR_TAP_PAIRS: usize = (BLUR_MAX_TAPS / 2) as usize;
102
103#[derive(Clone, Copy, Debug, Default, PartialEq)]
109pub struct BlurTapPair {
110 pub inner: f32,
111 pub outer: f32,
112 pub offset: f32,
113 pub weight: f32,
114}
115
116#[derive(Clone, Copy, Debug, PartialEq)]
120pub struct BlurKernel {
121 pub pairs: [BlurTapPair; BLUR_TAP_PAIRS],
122 pub pair_count: u32,
123 pub total_weight: f32,
124}
125
126impl BlurKernel {
127 pub fn of_radius(radius: f32) -> Self {
130 let radius = radius.max(0.0);
131 let sigma = (radius * 0.5).max(0.001);
132 let tap_count = (radius.ceil() as u32).min(BLUR_MAX_TAPS);
133 let inv_2sigma2 = 1.0 / (2.0 * sigma * sigma);
134 let mut pairs = [BlurTapPair::default(); BLUR_TAP_PAIRS];
135 let mut total_weight = 1.0f32;
136 let mut pair_count = 0;
137 for i in (1..=tap_count).step_by(2) {
138 let fi = i as f32;
139 let fj = fi + 1.0;
140 let inner = (-(fi * fi) * inv_2sigma2).exp();
141 let outer = if i < tap_count {
142 (-(fj * fj) * inv_2sigma2).exp()
143 } else {
144 0.0
145 };
146 total_weight += 2.0 * (inner + outer);
147 let weight = inner + outer;
148 let offset = if weight > 0.0 {
149 (fi * inner + fj * outer) / weight
150 } else {
151 0.0
152 };
153 pairs[pair_count] = BlurTapPair {
154 inner,
155 outer,
156 offset,
157 weight,
158 };
159 pair_count += 1;
160 }
161 Self {
162 pairs,
163 pair_count: pair_count as u32,
164 total_weight,
165 }
166 }
167}
168
169#[cfg(test)]
170#[path = "tests/geometry_blur_kernel_tests.rs"]
171mod blur_kernel_tests;