Skip to main content

proof_engine/glyph/
sdf_atlas.rs

1//! SDF Font Atlas — GPU-ready signed distance field texture with per-glyph metrics.
2//!
3//! The `SdfAtlas` wraps the output of `sdf_generator` and provides:
4//!   - Lookup of UV rects and metrics by character
5//!   - Configurable SDF spread and generation font size
6//!   - Methods for computing screen-space smoothing at render time
7//!   - Support for both single-channel SDF and multi-channel MSDF
8
9use std::collections::HashMap;
10use glam::Vec2;
11
12use super::sdf_generator::{SdfConfig, SdfGlyphMetric, SdfAtlasData, generate_sdf_atlas};
13
14// ── SdfAtlas ─────────────────────────────────────────────────────────────────
15
16/// A font atlas using Signed Distance Fields for resolution-independent rendering.
17///
18/// Each pixel in the atlas stores the signed distance to the nearest glyph edge:
19///   - 128 (0.5 normalized) = exactly on the edge
20///   - 255 (1.0 normalized) = deep inside the glyph
21///   - 0   (0.0 normalized) = far outside the glyph
22///
23/// The fragment shader thresholds this distance to produce a crisp edge at any
24/// resolution, rotation, or scale.
25pub struct SdfAtlas {
26    /// R8 pixel data (each pixel = distance value).
27    pub pixels: Vec<u8>,
28    /// Atlas dimensions.
29    pub width: u32,
30    pub height: u32,
31    /// Number of channels: 1 for SDF, 3 for MSDF.
32    pub channels: u32,
33    /// Per-glyph metrics and UV coordinates.
34    pub glyph_metrics: HashMap<char, SdfGlyphMetric>,
35    /// How many output pixels the distance field extends (typically 8.0).
36    pub sdf_spread: f32,
37    /// The font size at which the SDF was generated (typically 64px).
38    pub font_size_px: f32,
39}
40
41impl SdfAtlas {
42    /// Build an SDF atlas with default configuration.
43    pub fn build() -> Self {
44        Self::build_with_config(&SdfConfig::default())
45    }
46
47    /// Build an SDF atlas with custom configuration.
48    pub fn build_with_config(config: &SdfConfig) -> Self {
49        let data = generate_sdf_atlas(config);
50        Self::from_atlas_data(data)
51    }
52
53    /// Build from pre-computed atlas data.
54    pub fn from_atlas_data(data: SdfAtlasData) -> Self {
55        Self {
56            pixels: data.pixels,
57            width: data.width,
58            height: data.height,
59            channels: data.channels,
60            glyph_metrics: data.metrics,
61            sdf_spread: data.spread,
62            font_size_px: data.font_size_px,
63        }
64    }
65
66    /// Get the metric for a character, falling back to '?' then a default.
67    pub fn metric_for(&self, ch: char) -> SdfGlyphMetric {
68        self.glyph_metrics
69            .get(&ch)
70            .or_else(|| self.glyph_metrics.get(&'?'))
71            .copied()
72            .unwrap_or(SdfGlyphMetric {
73                uv_rect: [0.0, 0.0, 0.01, 0.01],
74                size: Vec2::new(1.0, 1.0),
75                bearing: Vec2::ZERO,
76                advance: self.font_size_px * 0.6,
77            })
78    }
79
80    /// Get the UV rect as [u_min, v_min, u_max, v_max].
81    pub fn uv_for(&self, ch: char) -> [f32; 4] {
82        self.metric_for(ch).uv_rect
83    }
84
85    /// Get the UV offset (top-left corner) as [u, v].
86    pub fn uv_offset(&self, ch: char) -> [f32; 2] {
87        let uv = self.uv_for(ch);
88        [uv[0], uv[1]]
89    }
90
91    /// Get the UV size as [u_width, v_height].
92    pub fn uv_size(&self, ch: char) -> [f32; 2] {
93        let uv = self.uv_for(ch);
94        [uv[2] - uv[0], uv[3] - uv[1]]
95    }
96
97    /// Compute the smoothing factor for SDF rendering based on the glyph's
98    /// screen-space size.
99    ///
100    /// At large scales, the smoothing is very small → razor-sharp edges.
101    /// At small scales, the smoothing is larger → anti-aliased edges.
102    ///
103    /// `screen_px_per_unit` is how many screen pixels one world unit occupies.
104    /// `glyph_scale` is the scale multiplier on the glyph.
105    pub fn compute_smoothing(&self, screen_px_per_unit: f32, glyph_scale: f32) -> f32 {
106        let effective_px = screen_px_per_unit * glyph_scale;
107        if effective_px <= 0.0 {
108            return 0.1;
109        }
110        // The SDF spread covers `sdf_spread` pixels in the atlas texture.
111        // At render time, one atlas texel covers (font_size_px / effective_px) screen pixels.
112        // Smoothing should be approximately 1.0 / (effective_px * sdf_spread / font_size_px).
113        let texels_per_screen_px = self.font_size_px / effective_px;
114        (texels_per_screen_px / self.sdf_spread).clamp(0.001, 0.25)
115    }
116
117    /// Compute the threshold for SDF rendering.
118    ///
119    /// Default is 0.5 (on the edge).
120    /// Lower values make glyphs bolder, higher values make them thinner.
121    pub fn threshold(&self) -> f32 {
122        0.5
123    }
124
125    /// Compute the threshold for a bold variant.
126    pub fn bold_threshold(&self) -> f32 {
127        0.45
128    }
129
130    /// Compute the outline range for a given outline width (in SDF-space units).
131    ///
132    /// Returns (inner_threshold, outer_threshold) for the outline smoothstep.
133    pub fn outline_range(&self, outline_width: f32) -> (f32, f32) {
134        let inner = 0.5;
135        let outer = (0.5 - outline_width / self.sdf_spread).max(0.05);
136        (inner, outer)
137    }
138
139    /// Compute the shadow UV offset for a drop shadow effect.
140    ///
141    /// `shadow_offset` is in screen pixels, `glyph_scale` is the current scale.
142    pub fn shadow_uv_offset(&self, shadow_offset: Vec2, glyph_scale: f32) -> Vec2 {
143        // Convert screen-pixel offset to UV-space offset in the atlas.
144        let scale_factor = glyph_scale * self.font_size_px;
145        if scale_factor <= 0.0 {
146            return Vec2::ZERO;
147        }
148        Vec2::new(
149            shadow_offset.x / (self.width as f32 * scale_factor / self.font_size_px),
150            shadow_offset.y / (self.height as f32 * scale_factor / self.font_size_px),
151        )
152    }
153
154    /// Total number of glyphs in the atlas.
155    pub fn glyph_count(&self) -> usize {
156        self.glyph_metrics.len()
157    }
158
159    /// Check if a character is available in the atlas.
160    pub fn has_char(&self, ch: char) -> bool {
161        self.glyph_metrics.contains_key(&ch)
162    }
163
164    /// Measure the width of a string in world units at a given scale.
165    pub fn measure_string_width(&self, text: &str, scale: f32) -> f32 {
166        let scale_factor = scale / self.font_size_px;
167        text.chars()
168            .map(|ch| self.metric_for(ch).advance * scale_factor)
169            .sum()
170    }
171
172    /// Measure the height of a single line of text in world units at a given scale.
173    pub fn line_height(&self, scale: f32) -> f32 {
174        scale
175    }
176}
177
178// ── SDF Effect Parameters ───────────────────────────────────────────────────
179
180/// Per-glyph SDF rendering effects that can be applied in the fragment shader.
181#[derive(Clone, Debug)]
182pub struct SdfEffects {
183    /// Whether to render an outline.
184    pub outline: bool,
185    /// Outline color (RGBA).
186    pub outline_color: glam::Vec4,
187    /// Outline width in SDF-space units (0.0 to ~0.2).
188    pub outline_width: f32,
189
190    /// Whether to render a drop shadow.
191    pub shadow: bool,
192    /// Shadow color (RGBA).
193    pub shadow_color: glam::Vec4,
194    /// Shadow offset in screen pixels.
195    pub shadow_offset: Vec2,
196    /// Shadow softness (blur radius in SDF-space, 0.0 to ~0.2).
197    pub shadow_softness: f32,
198
199    /// Whether to use the distance field as a glow source.
200    pub glow: bool,
201    /// Glow color (RGBA).
202    pub glow_color: glam::Vec4,
203    /// Glow radius in SDF-space (how far from edge the glow extends).
204    pub glow_radius: f32,
205
206    /// Bold mode: shifts the threshold to make glyphs thicker.
207    pub bold: bool,
208
209    /// UV distortion effects: wave, shake, glitch.
210    pub wave_amplitude: f32,
211    pub wave_frequency: f32,
212    pub shake_amount: f32,
213    pub glitch_intensity: f32,
214}
215
216impl Default for SdfEffects {
217    fn default() -> Self {
218        Self {
219            outline: false,
220            outline_color: glam::Vec4::new(0.0, 0.0, 0.0, 1.0),
221            outline_width: 0.1,
222            shadow: false,
223            shadow_color: glam::Vec4::new(0.0, 0.0, 0.0, 0.6),
224            shadow_offset: Vec2::new(2.0, -2.0),
225            shadow_softness: 0.05,
226            glow: false,
227            glow_color: glam::Vec4::new(1.0, 1.0, 1.0, 0.5),
228            glow_radius: 0.3,
229            bold: false,
230            wave_amplitude: 0.0,
231            wave_frequency: 0.0,
232            shake_amount: 0.0,
233            glitch_intensity: 0.0,
234        }
235    }
236}
237
238impl SdfEffects {
239    /// No effects — plain SDF rendering.
240    pub fn none() -> Self {
241        Self::default()
242    }
243
244    /// Outline only.
245    pub fn outline(color: glam::Vec4, width: f32) -> Self {
246        Self {
247            outline: true,
248            outline_color: color,
249            outline_width: width,
250            ..Self::default()
251        }
252    }
253
254    /// Drop shadow only.
255    pub fn shadow(color: glam::Vec4, offset: Vec2, softness: f32) -> Self {
256        Self {
257            shadow: true,
258            shadow_color: color,
259            shadow_offset: offset,
260            shadow_softness: softness,
261            ..Self::default()
262        }
263    }
264
265    /// Glow only.
266    pub fn glow(color: glam::Vec4, radius: f32) -> Self {
267        Self {
268            glow: true,
269            glow_color: color,
270            glow_radius: radius,
271            ..Self::default()
272        }
273    }
274
275    /// Bold text.
276    pub fn bold() -> Self {
277        Self {
278            bold: true,
279            ..Self::default()
280        }
281    }
282
283    /// Wavy text animation.
284    pub fn wave(amplitude: f32, frequency: f32) -> Self {
285        Self {
286            wave_amplitude: amplitude,
287            wave_frequency: frequency,
288            ..Self::default()
289        }
290    }
291
292    /// Glitch effect.
293    pub fn glitch(intensity: f32) -> Self {
294        Self {
295            glitch_intensity: intensity,
296            ..Self::default()
297        }
298    }
299}
300
301// ── Tests ───────────────────────────────────────────────────────────────────
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn smoothing_inversely_proportional_to_scale() {
309        let atlas = SdfAtlas {
310            pixels: vec![],
311            width: 512,
312            height: 512,
313            channels: 1,
314            glyph_metrics: HashMap::new(),
315            sdf_spread: 8.0,
316            font_size_px: 64.0,
317        };
318
319        let small = atlas.compute_smoothing(10.0, 1.0);
320        let large = atlas.compute_smoothing(100.0, 1.0);
321        assert!(small > large, "Small scale should have more smoothing: {} vs {}", small, large);
322    }
323
324    #[test]
325    fn outline_range_valid() {
326        let atlas = SdfAtlas {
327            pixels: vec![],
328            width: 512,
329            height: 512,
330            channels: 1,
331            glyph_metrics: HashMap::new(),
332            sdf_spread: 8.0,
333            font_size_px: 64.0,
334        };
335
336        let (inner, outer) = atlas.outline_range(1.0);
337        assert!(inner > outer, "Inner threshold should be > outer: {} vs {}", inner, outer);
338    }
339
340    #[test]
341    fn measure_string_empty() {
342        let atlas = SdfAtlas {
343            pixels: vec![],
344            width: 512,
345            height: 512,
346            channels: 1,
347            glyph_metrics: HashMap::new(),
348            sdf_spread: 8.0,
349            font_size_px: 64.0,
350        };
351        assert_eq!(atlas.measure_string_width("", 1.0), 0.0);
352    }
353
354    #[test]
355    fn sdf_effects_defaults() {
356        let fx = SdfEffects::default();
357        assert!(!fx.outline);
358        assert!(!fx.shadow);
359        assert!(!fx.glow);
360        assert!(!fx.bold);
361    }
362}