1use astrelis_core::math::Vec2;
23use astrelis_render::Color;
24
25#[derive(Debug, Clone)]
27pub struct TextEffect {
28 pub effect_type: TextEffectType,
30 pub enabled: bool,
32}
33
34#[derive(Debug, Clone, PartialEq)]
36pub enum TextEffectType {
37 Shadow {
39 offset: Vec2,
41 blur_radius: f32,
43 color: Color,
45 },
46 Outline {
48 width: f32,
50 color: Color,
52 },
53 Glow {
55 radius: f32,
57 color: Color,
59 intensity: f32,
61 },
62 InnerShadow {
64 offset: Vec2,
66 blur_radius: f32,
68 color: Color,
70 },
71}
72
73impl TextEffect {
74 pub fn new(effect_type: TextEffectType) -> Self {
76 Self {
77 effect_type,
78 enabled: true,
79 }
80 }
81
82 pub fn shadow(offset: Vec2, color: Color) -> Self {
89 Self::new(TextEffectType::Shadow {
90 offset,
91 blur_radius: 0.0,
92 color,
93 })
94 }
95
96 pub fn shadow_blurred(offset: Vec2, blur_radius: f32, color: Color) -> Self {
98 Self::new(TextEffectType::Shadow {
99 offset,
100 blur_radius,
101 color,
102 })
103 }
104
105 pub fn outline(width: f32, color: Color) -> Self {
112 Self::new(TextEffectType::Outline { width, color })
113 }
114
115 pub fn glow(radius: f32, color: Color, intensity: f32) -> Self {
123 Self::new(TextEffectType::Glow {
124 radius,
125 color,
126 intensity,
127 })
128 }
129
130 pub fn inner_shadow(offset: Vec2, blur_radius: f32, color: Color) -> Self {
132 Self::new(TextEffectType::InnerShadow {
133 offset,
134 blur_radius,
135 color,
136 })
137 }
138
139 pub fn set_enabled(&mut self, enabled: bool) {
141 self.enabled = enabled;
142 }
143
144 pub fn is_enabled(&self) -> bool {
146 self.enabled
147 }
148
149 pub fn effect_type(&self) -> &TextEffectType {
151 &self.effect_type
152 }
153
154 pub fn set_effect_type(&mut self, effect_type: TextEffectType) {
156 self.effect_type = effect_type;
157 }
158
159 pub fn requires_multi_pass(&self) -> bool {
161 matches!(
162 self.effect_type,
163 TextEffectType::Shadow { blur_radius, .. } if blur_radius > 0.0
164 ) || matches!(self.effect_type, TextEffectType::Glow { .. })
165 }
166
167 pub fn render_priority(&self) -> i32 {
172 match self.effect_type {
173 TextEffectType::Shadow { .. } => 0,
174 TextEffectType::InnerShadow { .. } => 1,
175 TextEffectType::Glow { .. } => 2,
176 TextEffectType::Outline { .. } => 3,
177 }
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct EffectRenderConfig {
184 pub max_blur_radius: f32,
186 pub max_glow_radius: f32,
188 pub blur_samples: u32,
190}
191
192impl Default for EffectRenderConfig {
193 fn default() -> Self {
194 Self {
195 max_blur_radius: 10.0,
196 max_glow_radius: 20.0,
197 blur_samples: 9,
198 }
199 }
200}
201
202impl EffectRenderConfig {
203 pub fn low() -> Self {
205 Self {
206 max_blur_radius: 5.0,
207 max_glow_radius: 10.0,
208 blur_samples: 5,
209 }
210 }
211
212 pub fn medium() -> Self {
214 Self::default()
215 }
216
217 pub fn high() -> Self {
219 Self {
220 max_blur_radius: 20.0,
221 max_glow_radius: 40.0,
222 blur_samples: 13,
223 }
224 }
225}
226
227#[derive(Debug, Clone, Default)]
229pub struct TextEffects {
230 effects: Vec<TextEffect>,
232}
233
234impl TextEffects {
235 pub fn new() -> Self {
237 Self {
238 effects: Vec::new(),
239 }
240 }
241
242 pub fn add(&mut self, effect: TextEffect) {
244 self.effects.push(effect);
245 }
246
247 pub fn clear(&mut self) {
249 self.effects.clear();
250 }
251
252 pub fn effects(&self) -> &[TextEffect] {
254 &self.effects
255 }
256
257 pub fn effects_mut(&mut self) -> &mut Vec<TextEffect> {
259 &mut self.effects
260 }
261
262 pub fn has_enabled_effects(&self) -> bool {
264 self.effects.iter().any(|e| e.enabled)
265 }
266
267 pub fn sorted_by_priority(&self) -> Vec<&TextEffect> {
269 let mut sorted: Vec<_> = self.effects.iter().filter(|e| e.enabled).collect();
270 sorted.sort_by_key(|e| e.render_priority());
271 sorted
272 }
273
274 pub fn calculate_bounds_expansion(&self) -> (f32, f32, f32, f32) {
278 let mut left = 0.0f32;
279 let mut top = 0.0f32;
280 let mut right = 0.0f32;
281 let mut bottom = 0.0f32;
282
283 for effect in &self.effects {
284 if !effect.enabled {
285 continue;
286 }
287
288 match &effect.effect_type {
289 TextEffectType::Shadow {
290 offset,
291 blur_radius,
292 ..
293 } => {
294 let expansion = *blur_radius * 2.0;
295 left = left.max(-offset.x + expansion);
296 top = top.max(-offset.y + expansion);
297 right = right.max(offset.x + expansion);
298 bottom = bottom.max(offset.y + expansion);
299 }
300 TextEffectType::Outline { width, .. } => {
301 let expansion = *width;
302 left = left.max(expansion);
303 top = top.max(expansion);
304 right = right.max(expansion);
305 bottom = bottom.max(expansion);
306 }
307 TextEffectType::Glow { radius, .. } => {
308 left = left.max(*radius);
309 top = top.max(*radius);
310 right = right.max(*radius);
311 bottom = bottom.max(*radius);
312 }
313 TextEffectType::InnerShadow { .. } => {
314 }
316 }
317 }
318
319 (left, top, right, bottom)
320 }
321}
322
323pub struct TextEffectsBuilder {
325 effects: TextEffects,
326}
327
328impl TextEffectsBuilder {
329 pub fn new() -> Self {
331 Self {
332 effects: TextEffects::new(),
333 }
334 }
335
336 pub fn shadow(mut self, offset: Vec2, color: Color) -> Self {
338 self.effects.add(TextEffect::shadow(offset, color));
339 self
340 }
341
342 pub fn shadow_blurred(mut self, offset: Vec2, blur_radius: f32, color: Color) -> Self {
344 self.effects
345 .add(TextEffect::shadow_blurred(offset, blur_radius, color));
346 self
347 }
348
349 pub fn outline(mut self, width: f32, color: Color) -> Self {
351 self.effects.add(TextEffect::outline(width, color));
352 self
353 }
354
355 pub fn glow(mut self, radius: f32, color: Color, intensity: f32) -> Self {
357 self.effects.add(TextEffect::glow(radius, color, intensity));
358 self
359 }
360
361 pub fn inner_shadow(mut self, offset: Vec2, blur_radius: f32, color: Color) -> Self {
363 self.effects
364 .add(TextEffect::inner_shadow(offset, blur_radius, color));
365 self
366 }
367
368 pub fn effect(mut self, effect: TextEffect) -> Self {
370 self.effects.add(effect);
371 self
372 }
373
374 pub fn build(self) -> TextEffects {
376 self.effects
377 }
378}
379
380impl Default for TextEffectsBuilder {
381 fn default() -> Self {
382 Self::new()
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn test_shadow_effect() {
392 let effect = TextEffect::shadow(Vec2::new(2.0, 2.0), Color::BLACK);
393 assert!(effect.is_enabled());
394 assert_eq!(effect.render_priority(), 0);
395 }
396
397 #[test]
398 fn test_outline_effect() {
399 let effect = TextEffect::outline(1.0, Color::WHITE);
400 assert!(effect.is_enabled());
401 assert_eq!(effect.render_priority(), 3);
402 }
403
404 #[test]
405 fn test_glow_effect() {
406 let effect = TextEffect::glow(5.0, Color::BLUE, 0.8);
407 assert!(effect.is_enabled());
408 assert!(effect.requires_multi_pass());
409 }
410
411 #[test]
412 fn test_effects_builder() {
413 let effects = TextEffectsBuilder::new()
414 .shadow(Vec2::new(1.0, 1.0), Color::BLACK)
415 .outline(1.0, Color::WHITE)
416 .glow(3.0, Color::BLUE, 0.5)
417 .build();
418
419 assert_eq!(effects.effects().len(), 3);
420 assert!(effects.has_enabled_effects());
421 }
422
423 #[test]
424 fn test_effects_priority_sorting() {
425 let mut effects = TextEffects::new();
426 effects.add(TextEffect::outline(1.0, Color::WHITE)); effects.add(TextEffect::shadow(Vec2::ZERO, Color::BLACK)); effects.add(TextEffect::glow(5.0, Color::BLUE, 1.0)); let sorted = effects.sorted_by_priority();
431 assert_eq!(sorted[0].render_priority(), 0);
432 assert_eq!(sorted[1].render_priority(), 2);
433 assert_eq!(sorted[2].render_priority(), 3);
434 }
435
436 #[test]
437 fn test_bounds_expansion() {
438 let effects = TextEffectsBuilder::new()
439 .shadow(Vec2::new(2.0, 2.0), Color::BLACK)
440 .outline(1.0, Color::WHITE)
441 .build();
442
443 let (left, top, right, bottom) = effects.calculate_bounds_expansion();
444 assert!(left > 0.0);
445 assert!(top > 0.0);
446 assert!(right > 0.0);
447 assert!(bottom > 0.0);
448 }
449}