geng_font/
lib.rs

1use batbox_color::*;
2use batbox_la::*;
3use batbox_num::*;
4use geng_camera::*;
5use serde::{Deserialize, Serialize};
6use std::cell::RefCell;
7use std::collections::{HashMap, HashSet};
8use std::rc::Rc;
9use ugli::Ugli;
10
11#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
12pub enum DistanceMode {
13    Euclid,
14    Max,
15}
16
17#[derive(Clone)]
18pub struct Options {
19    pub pixel_size: f32,
20    pub max_distance: f32,
21    pub antialias: bool,
22    pub distance_mode: DistanceMode,
23    // TODO: specify a set of glyphs
24}
25
26impl Default for Options {
27    fn default() -> Self {
28        Self {
29            pixel_size: 64.0,
30            max_distance: 0.25,
31            antialias: true,
32            distance_mode: DistanceMode::Euclid,
33        }
34    }
35}
36
37#[derive(Debug, Clone, ugli::Vertex)]
38pub struct GlyphInstance {
39    pub i_pos: vec2<f32>,
40    pub i_size: vec2<f32>,
41    pub i_uv_pos: vec2<f32>,
42    pub i_uv_size: vec2<f32>,
43}
44
45#[derive(Debug)]
46struct GlyphMetrics {
47    uv: Aabb2<f32>,
48    pos: Aabb2<f32>,
49}
50
51#[derive(Debug)]
52struct Glyph {
53    metrics: Option<GlyphMetrics>,
54    advance_x: f32,
55}
56
57pub struct Font {
58    ugli: Ugli,
59    sdf_program: Rc<ugli::Program>,
60    program: Rc<ugli::Program>,
61    glyphs: HashMap<char, Glyph>,
62    atlas: ugli::Texture,
63    max_distance: f32,
64    ascender: f32,
65    descender: f32,
66    line_gap: f32,
67}
68
69impl Font {
70    pub fn default(ugli: &Ugli) -> Self {
71        Self::new(ugli, include_bytes!("default.ttf"), &Options::default()).unwrap()
72    }
73    pub fn new(ugli: &Ugli, data: &[u8], options: &Options) -> anyhow::Result<Self> {
74        let shader_lib = geng_shader::Library::new(ugli, options.antialias, None);
75        let face = ttf_parser::Face::parse(data, 0)?;
76        struct RawGlyph {
77            id: ttf_parser::GlyphId,
78            code_point: char,
79            bounding_box: Option<Aabb2<f32>>,
80        }
81        let unit_scale = 1.0 / (face.ascender() - face.descender()) as f32;
82        let scale = options.pixel_size * unit_scale;
83        let mut raw_glyphs = Vec::new();
84        let mut found = HashSet::new();
85        for subtable in face.tables().cmap.unwrap().subtables {
86            if !subtable.is_unicode() {
87                continue;
88            }
89            subtable.codepoints(|code_point| {
90                let id = match subtable.glyph_index(code_point) {
91                    Some(id) => id,
92                    None => return,
93                };
94                let code_point = match char::from_u32(code_point) {
95                    Some(code_point) => code_point,
96                    None => return,
97                };
98                if found.contains(&code_point) {
99                    return;
100                }
101                found.insert(code_point);
102                let bounding_box = face.glyph_bounding_box(id).map(|rect| {
103                    Aabb2 {
104                        min: vec2(rect.x_min, rect.y_min),
105                        max: vec2(rect.x_max, rect.y_max),
106                    }
107                    .map(|x| x as f32 * scale)
108                });
109                raw_glyphs.push(RawGlyph {
110                    id,
111                    code_point,
112                    bounding_box,
113                })
114            });
115        }
116        raw_glyphs.sort_unstable_by_key(|glyph| {
117            glyph
118                .bounding_box
119                .map_or(0, |bb| -bb.height().ceil() as i32)
120        });
121        let mut glyphs: HashMap<char, Glyph> = HashMap::with_capacity(raw_glyphs.len());
122        let mut width = 0;
123        let mut x = 0;
124        let mut y = 0;
125        let mut row_height = 0;
126        let renderable_glyphs: Vec<&RawGlyph> = raw_glyphs
127            .iter()
128            .filter(|g| g.bounding_box.is_some())
129            .collect();
130        for glyph in &raw_glyphs {
131            if glyph.bounding_box.is_none() {
132                glyphs.insert(
133                    glyph.code_point,
134                    Glyph {
135                        metrics: None,
136                        advance_x: face.glyph_hor_advance(glyph.id).unwrap_or(0) as f32
137                            * unit_scale,
138                    },
139                );
140            }
141        }
142        for (i, glyph) in renderable_glyphs.iter().enumerate() {
143            let glyph_pos = glyph
144                .bounding_box
145                .unwrap()
146                .extend_uniform(options.max_distance * options.pixel_size);
147            let glyph_size = glyph_pos.size().map(|x| x.ceil() as usize);
148            if (y == 0 && i * i >= renderable_glyphs.len())
149                || (y > 0 && x > 0 && x + glyph_size.x > width)
150            {
151                x = 0;
152                y += row_height;
153                row_height = 0;
154            }
155            let uv = Aabb2::point(vec2(x, y)).extend_positive(glyph_size);
156            x = uv.max.x;
157            row_height = row_height.max(uv.height());
158            width = width.max(x);
159            glyphs.insert(
160                glyph.code_point,
161                Glyph {
162                    metrics: Some(GlyphMetrics {
163                        uv: uv.map(|x| x as f32),
164                        pos: glyph_pos.map(|x| x / options.pixel_size),
165                    }),
166                    advance_x: face.glyph_hor_advance(glyph.id).unwrap_or(0) as f32 * unit_scale,
167                },
168            );
169        }
170        let height = y + row_height;
171        let atlas_size = vec2(width, height);
172        for glyph in glyphs.values_mut() {
173            if let Some(metrics) = &mut glyph.metrics {
174                metrics.uv = metrics.uv.map_bounds(|b| b / atlas_size.map(|x| x as f32));
175            }
176        }
177        let mut atlas = ugli::Texture::new_uninitialized(ugli, atlas_size);
178        {
179            let mut depth_buffer = ugli::Renderbuffer::new(ugli, atlas_size);
180            let mut framebuffer = ugli::Framebuffer::new(
181                ugli,
182                ugli::ColorAttachment::Texture(&mut atlas),
183                ugli::DepthAttachment::RenderbufferWithStencil(&mut depth_buffer),
184            );
185            let framebuffer = &mut framebuffer;
186            ugli::clear(
187                framebuffer,
188                Some(Rgba::TRANSPARENT_BLACK),
189                Some(1.0),
190                Some(0),
191            );
192
193            #[derive(ugli::Vertex, Copy, Clone)]
194            struct Vertex {
195                a_pos: vec2<f32>,
196                a_dist_pos: vec2<f32>,
197            }
198            struct Builder {
199                distance_mesh: Vec<Vertex>,
200                stencil_mesh: Vec<Vertex>,
201                pos: vec2<f32>,
202                contour_start: vec2<f32>,
203                scale: f32,
204                offset: vec2<f32>,
205                options: Options,
206            }
207            impl Builder {
208                fn new_glyph_at(&mut self, offset: vec2<f32>) {
209                    self.offset = offset;
210                }
211                fn add_triangle_fan(&mut self, mid: Vertex, vs: impl IntoIterator<Item = Vertex>) {
212                    use itertools::Itertools;
213                    for (a, b) in vs.into_iter().tuple_windows() {
214                        self.distance_mesh.push(mid);
215                        self.distance_mesh.push(a);
216                        self.distance_mesh.push(b);
217                    }
218                }
219                fn add_triangle_fan2(&mut self, vs: impl IntoIterator<Item = Vertex>) {
220                    let mut vs = vs.into_iter();
221                    let first = vs.next().unwrap();
222                    self.add_triangle_fan(first, vs);
223                }
224                fn add_line(&mut self, a: vec2<f32>, b: vec2<f32>) {
225                    let radius = self.options.max_distance * self.options.pixel_size;
226                    self.stencil_mesh.push(Vertex {
227                        a_pos: self.offset,
228                        a_dist_pos: vec2::ZERO,
229                    });
230                    self.stencil_mesh.push(Vertex {
231                        a_pos: a,
232                        a_dist_pos: vec2::ZERO,
233                    });
234                    self.stencil_mesh.push(Vertex {
235                        a_pos: b,
236                        a_dist_pos: vec2::ZERO,
237                    });
238                    let unit_quad = Aabb2::point(vec2::ZERO).extend_uniform(1.0);
239                    let a_quad = Aabb2::point(a).extend_uniform(radius);
240                    let b_quad = Aabb2::point(b).extend_uniform(radius);
241                    self.add_triangle_fan2(
242                        itertools::izip![a_quad.corners(), unit_quad.corners()]
243                            .map(|(a_pos, a_dist_pos)| Vertex { a_pos, a_dist_pos }),
244                    );
245                    self.add_triangle_fan2(
246                        itertools::izip![b_quad.corners(), unit_quad.corners()]
247                            .map(|(a_pos, a_dist_pos)| Vertex { a_pos, a_dist_pos }),
248                    );
249                    let n = (b - a).rotate_90().normalize_or_zero() * radius;
250                    self.add_triangle_fan2([
251                        Vertex {
252                            a_pos: a + n,
253                            a_dist_pos: vec2(0.0, 1.0),
254                        },
255                        Vertex {
256                            a_pos: b + n,
257                            a_dist_pos: vec2(0.0, 1.0),
258                        },
259                        Vertex {
260                            a_pos: b - n,
261                            a_dist_pos: vec2(0.0, -1.0),
262                        },
263                        Vertex {
264                            a_pos: a - n,
265                            a_dist_pos: vec2(0.0, -1.0),
266                        },
267                    ]);
268                }
269            }
270            fn quad_bezier(p0: vec2<f32>, p1: vec2<f32>, p2: vec2<f32>, t: f32) -> vec2<f32> {
271                (1.0 - t).sqr() * p0 + 2.0 * (1.0 - t) * t * p1 + t.sqr() * p2
272            }
273            fn cubic_bezier(
274                p0: vec2<f32>,
275                p1: vec2<f32>,
276                p2: vec2<f32>,
277                p3: vec2<f32>,
278                t: f32,
279            ) -> vec2<f32> {
280                (1.0 - t) * quad_bezier(p0, p1, p2, t) + t * quad_bezier(p1, p2, p3, t)
281            }
282            const N: usize = 10;
283            impl ttf_parser::OutlineBuilder for Builder {
284                fn move_to(&mut self, x: f32, y: f32) {
285                    self.contour_start = vec2(x, y);
286                    self.pos = vec2(x, y) * self.scale + self.offset;
287                }
288                fn line_to(&mut self, x: f32, y: f32) {
289                    let a = self.pos;
290                    self.pos = vec2(x, y) * self.scale + self.offset;
291                    let b = self.pos;
292                    self.add_line(a, b);
293                }
294                fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
295                    // TODO proper math stuff
296                    let p0 = self.pos;
297                    let p1 = vec2(x1, y1) * self.scale + self.offset;
298                    let p2 = vec2(x, y) * self.scale + self.offset;
299                    for i in 1..=N {
300                        let t = i as f32 / N as f32;
301                        let p = quad_bezier(p0, p1, p2, t);
302                        self.add_line(self.pos, p);
303                        self.pos = p;
304                    }
305                }
306                fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
307                    // TODO proper math stuff
308                    let p0 = self.pos;
309                    let p1 = vec2(x1, y1) * self.scale + self.offset;
310                    let p2 = vec2(x2, y2) * self.scale + self.offset;
311                    let p3 = vec2(x, y) * self.scale + self.offset;
312                    for i in 1..=N {
313                        let t = i as f32 / N as f32;
314                        let p = cubic_bezier(p0, p1, p2, p3, t);
315                        self.add_line(self.pos, p);
316                        self.pos = p;
317                    }
318                }
319                fn close(&mut self) {
320                    self.line_to(self.contour_start.x, self.contour_start.y);
321                }
322            }
323            let mut builder = Builder {
324                distance_mesh: vec![],
325                stencil_mesh: vec![],
326                pos: vec2::ZERO,
327                contour_start: vec2::ZERO,
328                scale,
329                offset: vec2::ZERO,
330                options: options.clone(),
331            };
332            for glyph in &raw_glyphs {
333                if glyph.bounding_box.is_none() {
334                    continue;
335                }
336                builder.new_glyph_at(
337                    (glyphs[&glyph.code_point]
338                        .metrics
339                        .as_ref()
340                        .unwrap()
341                        .uv
342                        .bottom_left()
343                        * atlas_size.map(|x| x as f32))
344                    .map(|x| x + options.max_distance * options.pixel_size)
345                        - glyph.bounding_box.unwrap().bottom_left(),
346                );
347                face.outline_glyph(glyph.id, &mut builder);
348            }
349            let line_shader = shader_lib
350                .compile(match options.distance_mode {
351                    DistanceMode::Euclid => include_str!("ttf_line_euclid.glsl"),
352                    DistanceMode::Max => include_str!("ttf_line_max.glsl"),
353                })
354                .unwrap();
355            let white_shader = shader_lib.compile(include_str!("white.glsl")).unwrap();
356            ugli::draw(
357                framebuffer,
358                &line_shader,
359                ugli::DrawMode::Triangles,
360                &ugli::VertexBuffer::new_static(ugli, builder.stencil_mesh),
361                ugli::uniforms! {
362                    u_framebuffer_size: framebuffer.size(),
363                },
364                ugli::DrawParameters {
365                    stencil_mode: Some(ugli::StencilMode {
366                        back_face: ugli::FaceStencilMode {
367                            test: ugli::StencilTest {
368                                condition: ugli::Condition::Always,
369                                reference: 0,
370                                mask: 0,
371                            },
372                            op: ugli::StencilOp::always(ugli::StencilOpFunc::IncrementWrap),
373                        },
374                        front_face: ugli::FaceStencilMode {
375                            test: ugli::StencilTest {
376                                condition: ugli::Condition::Always,
377                                reference: 0,
378                                mask: 0,
379                            },
380                            op: ugli::StencilOp::always(ugli::StencilOpFunc::DecrementWrap),
381                        },
382                    }),
383                    write_color: false,
384                    ..Default::default()
385                },
386            );
387            ugli::draw(
388                framebuffer,
389                &line_shader,
390                ugli::DrawMode::Triangles,
391                &ugli::VertexBuffer::new_static(ugli, builder.distance_mesh),
392                ugli::uniforms! {
393                    u_framebuffer_size: framebuffer.size(),
394                },
395                ugli::DrawParameters {
396                    blend_mode: Some(ugli::BlendMode::combined(ugli::ChannelBlendMode {
397                        src_factor: ugli::BlendFactor::One,
398                        dst_factor: ugli::BlendFactor::One,
399                        equation: ugli::BlendEquation::Max,
400                    })),
401                    // depth_func: Some(ugli::DepthFunc::Less),
402                    ..Default::default()
403                },
404            );
405            ugli::draw(
406                framebuffer,
407                &white_shader,
408                ugli::DrawMode::TriangleFan,
409                &ugli::VertexBuffer::new_static(
410                    ugli,
411                    Aabb2::point(vec2(0, 0))
412                        .extend_positive(framebuffer.size())
413                        .corners()
414                        .into_iter()
415                        .map(|p| Vertex {
416                            a_pos: p.map(|x| x as f32),
417                            a_dist_pos: vec2::ZERO,
418                        })
419                        .collect(),
420                ),
421                ugli::uniforms! {
422                    u_framebuffer_size: framebuffer.size(),
423                },
424                ugli::DrawParameters {
425                    stencil_mode: Some(ugli::StencilMode::always(ugli::FaceStencilMode {
426                        test: ugli::StencilTest {
427                            condition: ugli::Condition::NotEqual,
428                            reference: 0,
429                            mask: 0xff,
430                        },
431                        op: ugli::StencilOp::always(ugli::StencilOpFunc::Keep),
432                    })),
433                    blend_mode: Some(ugli::BlendMode::combined(ugli::ChannelBlendMode {
434                        src_factor: ugli::BlendFactor::OneMinusDstColor,
435                        dst_factor: ugli::BlendFactor::Zero,
436                        equation: ugli::BlendEquation::Add,
437                    })),
438                    ..Default::default()
439                },
440            );
441        }
442        thread_local! { pub static SHADERS: RefCell<Option<[Rc<ugli::Program>; 2]>> = Default::default(); };
443        let [program, sdf_program] = SHADERS.with(|shaders| {
444            fn map<T, R>(a: &[T; 2], f: impl Fn(&T) -> R) -> [R; 2] {
445                let [a, b] = a;
446                [f(a), f(b)]
447            }
448            map(
449                shaders.borrow_mut().get_or_insert_with(|| {
450                    [
451                        shader_lib.compile(include_str!("shader.glsl")).unwrap(),
452                        shader_lib.compile(include_str!("sdf.glsl")).unwrap(),
453                    ]
454                    .map(Rc::new)
455                }),
456                |shader| Rc::clone(shader),
457            )
458        });
459        Ok(Self {
460            ugli: ugli.clone(),
461            program,
462            sdf_program,
463            glyphs,
464            atlas,
465            max_distance: options.max_distance,
466            ascender: face.ascender() as f32 * unit_scale,
467            descender: face.descender() as f32 * unit_scale,
468            line_gap: face.line_gap() as f32 * unit_scale,
469        })
470    }
471
472    pub fn max_distance(&self) -> f32 {
473        self.max_distance
474    }
475
476    pub fn ascender(&self) -> f32 {
477        self.ascender
478    }
479
480    pub fn descender(&self) -> f32 {
481        self.descender
482    }
483
484    pub fn line_gap(&self) -> f32 {
485        self.line_gap
486    }
487
488    pub fn measure(&self, text: &str, align: vec2<TextAlign>) -> Option<Aabb2<f32>> {
489        self.draw_with(text, align, |glyphs, _| {
490            if glyphs.is_empty() {
491                return None;
492            }
493            Some(
494                Aabb2::points_bounding_box(
495                    glyphs
496                        .iter()
497                        .flat_map(|glyph| [glyph.i_pos, glyph.i_pos + glyph.i_size]),
498                )
499                .unwrap()
500                .extend_uniform(-self.max_distance),
501            )
502        })
503    }
504
505    pub fn advance(&self, text: &str) -> f32 {
506        let mut x = 0.0;
507        for glyph in text.chars().filter_map(move |c| self.glyphs.get(&c)) {
508            // TODO: kerning
509            x += glyph.advance_x;
510        }
511        x
512    }
513
514    pub fn draw_with<R>(
515        &self,
516        text: &str,
517        align: vec2<TextAlign>,
518        f: impl FnOnce(&[GlyphInstance], &ugli::Texture) -> R,
519    ) -> R {
520        let mut vs = Vec::<GlyphInstance>::new();
521        let mut pos = vec2::ZERO;
522        let mut size_x: f32 = 0.0;
523        let mut line_width: f32 = 0.0;
524        let mut line_start = 0;
525        for c in text.chars() {
526            if c == '\n' {
527                for v in &mut vs[line_start..] {
528                    v.i_pos.x -= line_width * align.x.0;
529                }
530                pos.x = 0.0;
531                pos.y -= 1.0;
532                line_width = 0.0;
533                line_start = vs.len();
534                continue;
535            }
536            let Some(glyph) = self.glyphs.get(&c) else {
537                continue;
538            };
539            // TODO: kerning
540            if let Some(metrics) = &glyph.metrics {
541                let instance = GlyphInstance {
542                    i_pos: pos + metrics.pos.bottom_left(),
543                    i_size: metrics.pos.size(),
544                    i_uv_pos: metrics.uv.bottom_left(),
545                    i_uv_size: metrics.uv.size(),
546                };
547                line_width =
548                    line_width.max(instance.i_pos.x + instance.i_size.x - self.max_distance);
549                size_x = size_x.max(line_width);
550                vs.push(instance);
551            }
552            pos.x += glyph.advance_x;
553        }
554        for v in &mut vs[line_start..] {
555            v.i_pos.x -= line_width * align.x.0;
556        }
557        for v in &mut vs {
558            v.i_pos.y += -pos.y * (1.0 - align.y.0) - align.y.0;
559        }
560        f(&vs, &self.atlas)
561    }
562
563    pub fn draw(
564        &self,
565        framebuffer: &mut ugli::Framebuffer,
566        camera: &(impl AbstractCamera2d + ?Sized),
567        text: &str,
568        align: vec2<TextAlign>,
569        transform: mat3<f32>,
570        color: Rgba<f32>,
571    ) {
572        self.draw_with_outline(
573            framebuffer,
574            camera,
575            text,
576            align,
577            transform,
578            color,
579            0.0,
580            Rgba { a: 0.0, ..color },
581        );
582    }
583
584    #[allow(clippy::too_many_arguments)]
585    pub fn draw_with_outline(
586        &self,
587        framebuffer: &mut ugli::Framebuffer,
588        camera: &(impl AbstractCamera2d + ?Sized),
589        text: &str,
590        align: vec2<TextAlign>,
591        transform: mat3<f32>,
592        color: Rgba<f32>,
593        outline_size: f32,
594        outline_color: Rgba<f32>,
595    ) {
596        self.draw_with(text, align, |glyphs, texture| {
597            let framebuffer_size = framebuffer.size();
598            ugli::draw(
599                framebuffer,
600                &self.program,
601                ugli::DrawMode::TriangleFan,
602                // TODO: don't create VBs each time
603                ugli::instanced(
604                    &ugli::VertexBuffer::new_dynamic(
605                        &self.ugli,
606                        Aabb2::point(vec2::ZERO)
607                            .extend_positive(vec2(1.0, 1.0))
608                            .corners()
609                            .into_iter()
610                            .map(|v| Vertex { a_pos: v, a_vt: v })
611                            .collect(),
612                    ),
613                    &ugli::VertexBuffer::new_dynamic(&self.ugli, glyphs.to_vec()),
614                ),
615                (
616                    ugli::uniforms! {
617                        u_texture: texture,
618                        u_model_matrix: transform,
619                        u_color: color,
620                        u_outline_dist: outline_size / self.max_distance,
621                        u_outline_color: outline_color,
622                    },
623                    camera.uniforms(framebuffer_size.map(|x| x as f32)),
624                ),
625                ugli::DrawParameters {
626                    depth_func: None,
627                    blend_mode: Some(ugli::BlendMode::straight_alpha()),
628                    ..Default::default()
629                },
630            );
631        });
632    }
633
634    pub fn create_text_sdf(
635        &self,
636        text: &str,
637        line_align: TextAlign,
638        pixel_size: f32,
639    ) -> Option<ugli::Texture> {
640        let align = vec2(line_align, TextAlign::BOTTOM);
641        let aabb = self.measure(text, align)?;
642        let texture_size = (vec2(
643            aabb.width() + 2.0 * self.max_distance(),
644            text.chars().filter(|c| *c == '\n').count() as f32 + 1.0 + 2.0 * self.max_distance(),
645        ) * pixel_size)
646            .map(|x| x.ceil() as usize);
647        let mut texture = ugli::Texture::new_uninitialized(&self.ugli, texture_size);
648        let framebuffer = &mut ugli::Framebuffer::new_color(
649            &self.ugli,
650            ugli::ColorAttachment::Texture(&mut texture),
651        );
652        ugli::clear(framebuffer, Some(Rgba::TRANSPARENT_BLACK), None, None);
653        self.draw_with(text, align, |glyphs, atlas| {
654            ugli::draw(
655                framebuffer,
656                &self.sdf_program,
657                ugli::DrawMode::TriangleFan,
658                ugli::instanced(
659                    &ugli::VertexBuffer::new_dynamic(
660                        &self.ugli,
661                        Aabb2::point(vec2::ZERO)
662                            .extend_positive(vec2(1.0, 1.0))
663                            .corners()
664                            .into_iter()
665                            .map(|v| Vertex { a_pos: v, a_vt: v })
666                            .collect(),
667                    ),
668                    &ugli::VertexBuffer::new_dynamic(&self.ugli, glyphs.to_vec()),
669                ),
670                ugli::uniforms! {
671                    u_texture: atlas,
672                    u_matrix: mat3::ortho(aabb.extend_uniform(self.max_distance())),
673                },
674                ugli::DrawParameters {
675                    blend_mode: Some(ugli::BlendMode::combined(ugli::ChannelBlendMode {
676                        src_factor: ugli::BlendFactor::One,
677                        dst_factor: ugli::BlendFactor::One,
678                        equation: ugli::BlendEquation::Max,
679                    })),
680                    ..Default::default()
681                },
682            );
683        });
684        Some(texture)
685    }
686}
687
688#[derive(ugli::Vertex, Debug)]
689pub struct Vertex {
690    pub a_pos: vec2<f32>,
691    pub a_vt: vec2<f32>,
692}
693
694#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
695pub struct TextAlign(pub f32);
696
697impl TextAlign {
698    pub const LEFT: Self = Self(0.0);
699    pub const BOTTOM: Self = Self(0.0);
700    pub const TOP: Self = Self(1.0);
701    pub const CENTER: Self = Self(0.5);
702    pub const RIGHT: Self = Self(1.0);
703}