Skip to main content

astrelis_text/
effects.rs

1//! Text effects including shadows, outlines, and glows.
2//!
3//! Provides visual effects for text rendering to enhance readability and aesthetics.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use astrelis_text::*;
9//!
10//! let text = Text::new("Hello")
11//!     .size(24.0)
12//!     .with_effect(TextEffect::shadow(
13//!         Vec2::new(2.0, 2.0),
14//!         Color::BLACK,
15//!     ))
16//!     .with_effect(TextEffect::outline(
17//!         1.0,
18//!         Color::WHITE,
19//!     ));
20//! ```
21
22use astrelis_core::math::Vec2;
23use astrelis_render::Color;
24
25/// A visual effect applied to text.
26#[derive(Debug, Clone)]
27pub struct TextEffect {
28    /// The type of effect
29    pub effect_type: TextEffectType,
30    /// Whether the effect is enabled
31    pub enabled: bool,
32}
33
34/// Types of text effects.
35#[derive(Debug, Clone, PartialEq)]
36pub enum TextEffectType {
37    /// Drop shadow effect
38    Shadow {
39        /// Offset from the text
40        offset: Vec2,
41        /// Blur radius (0 = hard edge)
42        blur_radius: f32,
43        /// Shadow color
44        color: Color,
45    },
46    /// Outline effect
47    Outline {
48        /// Width of the outline in pixels
49        width: f32,
50        /// Outline color
51        color: Color,
52    },
53    /// Glow effect
54    Glow {
55        /// Radius of the glow
56        radius: f32,
57        /// Glow color
58        color: Color,
59        /// Intensity multiplier (0.0 to 1.0+)
60        intensity: f32,
61    },
62    /// Inner shadow effect
63    InnerShadow {
64        /// Offset from the text
65        offset: Vec2,
66        /// Blur radius
67        blur_radius: f32,
68        /// Shadow color
69        color: Color,
70    },
71}
72
73impl TextEffect {
74    /// Create a new text effect.
75    pub fn new(effect_type: TextEffectType) -> Self {
76        Self {
77            effect_type,
78            enabled: true,
79        }
80    }
81
82    /// Create a drop shadow effect.
83    ///
84    /// # Arguments
85    ///
86    /// * `offset` - Offset from the text (positive Y is down)
87    /// * `color` - Shadow color
88    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    /// Create a drop shadow effect with blur.
97    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    /// Create an outline effect.
106    ///
107    /// # Arguments
108    ///
109    /// * `width` - Width of the outline in pixels
110    /// * `color` - Outline color
111    pub fn outline(width: f32, color: Color) -> Self {
112        Self::new(TextEffectType::Outline { width, color })
113    }
114
115    /// Create a glow effect.
116    ///
117    /// # Arguments
118    ///
119    /// * `radius` - Radius of the glow
120    /// * `color` - Glow color
121    /// * `intensity` - Intensity multiplier (typically 0.5 to 1.0)
122    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    /// Create an inner shadow effect.
131    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    /// Enable or disable the effect.
140    pub fn set_enabled(&mut self, enabled: bool) {
141        self.enabled = enabled;
142    }
143
144    /// Check if the effect is enabled.
145    pub fn is_enabled(&self) -> bool {
146        self.enabled
147    }
148
149    /// Get the effect type.
150    pub fn effect_type(&self) -> &TextEffectType {
151        &self.effect_type
152    }
153
154    /// Update the effect type.
155    pub fn set_effect_type(&mut self, effect_type: TextEffectType) {
156        self.effect_type = effect_type;
157    }
158
159    /// Check if this effect requires multi-pass rendering.
160    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    /// Get the rendering order priority.
168    ///
169    /// Lower values render first (background effects).
170    /// Higher values render last (foreground effects).
171    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/// Effect rendering configuration.
182#[derive(Debug, Clone)]
183pub struct EffectRenderConfig {
184    /// Maximum blur radius for performance
185    pub max_blur_radius: f32,
186    /// Maximum glow radius for performance
187    pub max_glow_radius: f32,
188    /// Number of blur samples (higher = better quality, slower)
189    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    /// Create a low-quality configuration (faster).
204    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    /// Create a medium-quality configuration (balanced).
213    pub fn medium() -> Self {
214        Self::default()
215    }
216
217    /// Create a high-quality configuration (slower).
218    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/// Collection of text effects applied to text.
228#[derive(Debug, Clone, Default)]
229pub struct TextEffects {
230    /// List of effects to apply
231    effects: Vec<TextEffect>,
232}
233
234impl TextEffects {
235    /// Create a new empty effects collection.
236    pub fn new() -> Self {
237        Self {
238            effects: Vec::new(),
239        }
240    }
241
242    /// Add an effect.
243    pub fn add(&mut self, effect: TextEffect) {
244        self.effects.push(effect);
245    }
246
247    /// Remove all effects.
248    pub fn clear(&mut self) {
249        self.effects.clear();
250    }
251
252    /// Get all effects.
253    pub fn effects(&self) -> &[TextEffect] {
254        &self.effects
255    }
256
257    /// Get mutable effects.
258    pub fn effects_mut(&mut self) -> &mut Vec<TextEffect> {
259        &mut self.effects
260    }
261
262    /// Check if any effects are enabled.
263    pub fn has_enabled_effects(&self) -> bool {
264        self.effects.iter().any(|e| e.enabled)
265    }
266
267    /// Get effects sorted by render priority.
268    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    /// Calculate the expanded bounds needed for effects.
275    ///
276    /// Returns (left, top, right, bottom) expansion in pixels.
277    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                    // Inner shadows don't expand bounds
315                }
316            }
317        }
318
319        (left, top, right, bottom)
320    }
321}
322
323/// Builder for creating text effects.
324pub struct TextEffectsBuilder {
325    effects: TextEffects,
326}
327
328impl TextEffectsBuilder {
329    /// Create a new effects builder.
330    pub fn new() -> Self {
331        Self {
332            effects: TextEffects::new(),
333        }
334    }
335
336    /// Add a shadow effect.
337    pub fn shadow(mut self, offset: Vec2, color: Color) -> Self {
338        self.effects.add(TextEffect::shadow(offset, color));
339        self
340    }
341
342    /// Add a blurred shadow effect.
343    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    /// Add an outline effect.
350    pub fn outline(mut self, width: f32, color: Color) -> Self {
351        self.effects.add(TextEffect::outline(width, color));
352        self
353    }
354
355    /// Add a glow effect.
356    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    /// Add an inner shadow effect.
362    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    /// Add a custom effect.
369    pub fn effect(mut self, effect: TextEffect) -> Self {
370        self.effects.add(effect);
371        self
372    }
373
374    /// Build the effects collection.
375    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)); // Priority 3
427        effects.add(TextEffect::shadow(Vec2::ZERO, Color::BLACK)); // Priority 0
428        effects.add(TextEffect::glow(5.0, Color::BLUE, 1.0)); // Priority 2
429
430        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}