use std::sync::Arc;
use egui::{
Align2, Color32, Pos2, Shape, Vec2,
emath::TSTransform,
epaint::{Galley, TextShape},
};
#[derive(Clone, Copy, Debug)]
pub struct TextShadow {
pub offset: Vec2,
pub color: Color32,
}
#[derive(Clone, Copy, Debug)]
pub struct Glow {
pub color: Color32,
pub radius: f32,
pub intensity: f32,
}
#[derive(Clone, Copy, Debug)]
pub struct GlowQuality {
pub samples: u32,
}
impl Default for GlowQuality {
fn default() -> Self {
Self { samples: 64 }
}
}
#[derive(Clone, Default, Debug)]
pub struct TextEffects {
pub shadows: Vec<TextShadow>,
pub outline: Option<(f32, Color32)>,
pub glow: Option<Glow>,
pub glow_quality: GlowQuality,
pub scale: Option<(f32, Align2)>,
}
impl TextEffects {
pub fn is_empty(&self) -> bool {
self.shadows.is_empty()
&& self.outline.is_none()
&& self.glow.is_none()
&& self.scale.is_none()
}
}
pub fn paint_text_effects(
ui: &mut egui::Ui,
pos: Pos2,
galley: Arc<Galley>,
fallback_color: Color32,
effects: &TextEffects,
response_rect: egui::Rect,
) {
let layer_id = ui.layer_id();
let scale_start = ui.painter().add(Shape::Noop);
if let Some(glow) = effects.glow {
paint_glow(ui, pos, galley.clone(), glow, effects.glow_quality.samples);
}
for s in &effects.shadows {
stamp(ui, pos + s.offset, galley.clone(), fallback_color, s.color);
}
if let Some((width, color)) = effects.outline {
for offset in outline_offsets(width) {
stamp(ui, pos + offset, galley.clone(), fallback_color, color);
}
}
ui.painter()
.add(Shape::Text(TextShape::new(pos, galley, fallback_color)));
let scale_end = ui.painter().add(Shape::Noop);
if let Some((factor, pivot_align)) = effects.scale {
let pivot = pivot_align.pos_in_rect(&response_rect);
let t = TSTransform::from_translation(pivot.to_vec2())
* TSTransform::from_scaling(factor)
* TSTransform::from_translation(-pivot.to_vec2());
ui.ctx().graphics_mut(|g| {
g.entry(layer_id).transform_range(scale_start, scale_end, t);
});
}
}
fn stamp(
ui: &mut egui::Ui,
pos: Pos2,
galley: Arc<Galley>,
fallback_color: Color32,
color: Color32,
) {
ui.painter().add(Shape::Text(
TextShape::new(pos, galley, fallback_color).with_override_text_color(color),
));
}
fn paint_glow(ui: &mut egui::Ui, pos: Pos2, galley: Arc<Galley>, glow: Glow, samples: u32) {
if glow.intensity <= 0.0 || glow.radius <= 0.0 {
return;
}
const GOLDEN_ANGLE: f32 = 2.399_963_2; const STRENGTH: f32 = 0.5;
const REF_RADIUS: f32 = 8.0; const REF_SAMPLES: f32 = 64.0; const MAX_SAMPLES: u32 = 384;
let base = samples.max(1) as f32;
let density_scale = (glow.radius / REF_RADIUS).powi(2).clamp(0.25, 6.0);
let n = ((base * density_scale).round() as u32).clamp(8, MAX_SAMPLES);
let intensity = glow.intensity.clamp(0.0, 1.0);
let alpha_norm = STRENGTH * intensity * (REF_SAMPLES / base);
for i in 0..n {
let frac = (i as f32 + 0.5) / n as f32;
let r_norm = frac.sqrt();
let r = glow.radius * r_norm;
let theta = i as f32 * GOLDEN_ANGLE;
let off = Vec2::new(theta.cos() * r, theta.sin() * r);
let edge = 1.0 - r_norm * r_norm;
let a = edge * edge * alpha_norm;
if a <= 0.002 {
continue;
}
let color = Color32::from_rgba_premultiplied(
(glow.color.r() as f32 * a) as u8,
(glow.color.g() as f32 * a) as u8,
(glow.color.b() as f32 * a) as u8,
(glow.color.a() as f32 * a) as u8,
);
stamp(ui, pos + off, galley.clone(), Color32::TRANSPARENT, color);
}
}
fn outline_offsets(width: f32) -> [Vec2; 8] {
let d = width;
let d45 = width * std::f32::consts::FRAC_1_SQRT_2;
[
Vec2::new(d, 0.0),
Vec2::new(-d, 0.0),
Vec2::new(0.0, d),
Vec2::new(0.0, -d),
Vec2::new(d45, d45),
Vec2::new(-d45, d45),
Vec2::new(d45, -d45),
Vec2::new(-d45, -d45),
]
}