1#![forbid(unsafe_code)]
2
3use sequoia_core::SpanMask;
4
5use crate::effect::Backdrop;
6
7#[derive(Clone, Copy, Debug, PartialEq)]
8#[non_exhaustive]
9pub enum Mask {
10 LinearFade {
11 from_alpha: f32,
12 to_alpha: f32,
13 angle_rad: f32,
14 },
15
16 Uniform(f32),
17}
18
19impl Mask {
20 pub fn strength_at(&self, t: f32) -> f32 {
21 let t = t.clamp(0.0, 1.0);
22 match *self {
23 Mask::LinearFade {
24 from_alpha,
25 to_alpha,
26 ..
27 } => from_alpha + (to_alpha - from_alpha) * t,
28 Mask::Uniform(v) => v.clamp(0.0, 1.0),
29 }
30 }
31
32 pub fn span_mask(&self) -> SpanMask {
33 match *self {
34 Mask::LinearFade {
35 from_alpha,
36 to_alpha,
37 angle_rad,
38 } => SpanMask::Linear {
39 from: from_alpha,
40 to: to_alpha,
41 angle_rad,
42 },
43 Mask::Uniform(v) => SpanMask::Uniform(v),
44 }
45 }
46}
47
48pub const VERTICAL: f32 = std::f32::consts::FRAC_PI_2;
49
50pub fn gradient_mask<G>(gradient: G) -> Mask
51where
52 G: GradientAlphas,
53{
54 let (a, b) = gradient.endpoint_alphas();
55 let (dx, dy) = gradient.direction();
56 let angle = if dx == 0.0 && dy == 0.0 {
57 VERTICAL
58 } else {
59 dy.atan2(dx)
60 };
61 Mask::LinearFade {
62 from_alpha: a.clamp(0.0, 1.0),
63 to_alpha: b.clamp(0.0, 1.0),
64 angle_rad: angle,
65 }
66}
67
68pub trait GradientAlphas {
69 fn endpoint_alphas(&self) -> (f32, f32);
70
71 fn direction(&self) -> (f32, f32) {
72 (0.0, 1.0)
73 }
74}
75
76#[derive(Clone, Copy, Debug, Default, PartialEq)]
77pub struct Filter {
78 pub blur: f32,
79 pub mask: Option<Mask>,
80}
81
82impl Filter {
83 pub const NONE: Self = Self {
84 blur: 0.0,
85 mask: None,
86 };
87
88 pub const fn blur(radius: f32) -> Self {
89 Self {
90 blur: if radius > 0.0 { radius } else { 0.0 },
91 mask: None,
92 }
93 }
94
95 pub fn blur_radius(mut self, radius: f32) -> Self {
96 self.blur = radius.max(0.0);
97 self
98 }
99
100 pub fn mask(mut self, mask: Mask) -> Self {
101 self.mask = Some(mask);
102 self
103 }
104
105 pub fn is_empty(&self) -> bool {
106 !(self.blur > 0.0 && self.blur.is_finite())
107 }
108
109 pub fn blur_px(&self) -> f32 {
110 if self.blur.is_finite() {
111 self.blur.max(0.0)
112 } else {
113 0.0
114 }
115 }
116
117 pub fn span_mask(&self) -> SpanMask {
118 self.mask.map(|m| m.span_mask()).unwrap_or(SpanMask::None)
119 }
120}
121
122#[derive(Clone, Copy, Debug, PartialEq)]
123#[non_exhaustive]
124pub enum Effect {
125 Blur(Filter),
126 Shadow(crate::ShadowSpec),
127 Backdrop(Backdrop),
128}
129
130impl Effect {
131 pub fn is_empty(&self) -> bool {
132 match self {
133 Effect::Blur(b) => b.is_empty(),
134 Effect::Shadow(s) => !s.is_visible(),
135 Effect::Backdrop(b) => b.is_empty(),
136 }
137 }
138}
139
140pub fn blur(radius: f32) -> Filter {
141 Filter::blur(radius.max(0.0))
142}
143
144pub fn gradient(from_alpha: f32, to_alpha: f32) -> Mask {
145 gradient_angled(from_alpha, to_alpha, VERTICAL)
146}
147
148pub fn gradient_angled(from_alpha: f32, to_alpha: f32, angle_rad: f32) -> Mask {
149 Mask::LinearFade {
150 from_alpha: from_alpha.clamp(0.0, 1.0),
151 to_alpha: to_alpha.clamp(0.0, 1.0),
152 angle_rad,
153 }
154}
155
156pub fn backdrop_blur(radius: f32) -> Backdrop {
157 Backdrop::blur(radius)
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use sequoia_core::Color;
164
165 #[test]
166 fn filter_vs_backdrop_semantics() {
167 assert!(Filter::NONE.is_empty());
168 assert!(!Filter::blur(12.0).is_empty());
169 assert!(Backdrop::NONE.is_empty());
170 assert!(!Backdrop::blur(4.0).is_empty());
171
172 assert!(Backdrop::NONE.radius(12.0).is_empty());
173 }
174
175 #[test]
176 fn blur_mask_builder() {
177 let b = blur(30.0).mask(gradient(1.0, 0.0));
178 assert_eq!(b.blur, 30.0);
179 assert!(b.mask.is_some());
180 let m = b.mask.as_ref().unwrap();
181 assert_eq!(m.strength_at(0.0), 1.0, "the mask holds the ramp");
182 assert_eq!(m.strength_at(1.0), 0.0);
183 assert_eq!(
184 m.span_mask(),
185 SpanMask::Linear {
186 from: 1.0,
187 to: 0.0,
188 angle_rad: VERTICAL,
189 },
190 "pionowa rampa do GPU"
191 );
192 }
193
194 #[test]
195 fn angled_mask_encodes_direction() {
196 let m = gradient_angled(0.0, 1.0, 0.0);
197 assert_eq!(m.span_mask().mask_vec(), [1.0, 0.0, 1.0, 0.0]);
198 assert_eq!(Filter::NONE.span_mask(), SpanMask::None);
199 assert_eq!(
200 Backdrop::blur(4.0).span_mask(),
201 SpanMask::None,
202 "no mask, full strength"
203 );
204 }
205
206 #[test]
207 fn backdrop_builder_with_mask() {
208 let f = Backdrop::blur(16.0)
209 .dim(Color::BLACK)
210 .noise(0.06)
211 .radius(12.0)
212 .mask(gradient(1.0, 0.0));
213 assert_eq!(f.blur, 16.0);
214 assert_eq!(f.radius, 12.0);
215 assert!(!f.is_empty());
216 assert!(f.mask.is_some());
217 assert!(f.span_mask() != SpanMask::None);
218 }
219}