Skip to main content

blockworx_canvas2d/
glyphs.rs

1//! The diagram's text engine: one typeface, laid out and inked from its own
2//! bytes.
3//!
4//! The layout a run is measured by and the outlines its glyphs are filled
5//! from are one value, built from one [`FontChoice`], so the diagram cannot
6//! draw a face the recorder did not measure — the contract the exporters
7//! stand on, kept by construction rather than by agreement between two
8//! arguments.
9
10use std::cell::RefCell;
11use std::collections::HashMap;
12
13use blockworx_geom::WorldPx;
14use blockworx_paint::text::{GlyphId, Layout, TextLayout};
15use blockworx_paint::{Font, FontChoice};
16use blockworx_text::{Metrics, Outlines, PathCommand, Shaper};
17use web_sys::Path2d;
18
19/// A typeface as the canvas draws it: the [`Shaper`] the kernel is called
20/// with, and a [`Path2d`] per glyph traced from the same face's outlines.
21pub struct Glyphs {
22    shaper: Shaper,
23    outlines: Outlines,
24    /// Glyphs already traced into the browser's own path type. `None` where
25    /// the glyph draws nothing, so a space is looked up rather than retraced.
26    inked: RefCell<HashMap<GlyphId, Option<Path2d>>>,
27}
28
29impl Glyphs {
30    #[must_use]
31    pub fn new(typeface: FontChoice) -> Self {
32        Self {
33            shaper: Shaper::new(typeface),
34            outlines: Outlines::new(typeface),
35            inked: RefCell::default(),
36        }
37    }
38
39    /// The vertical metrics both halves are stated in.
40    #[must_use]
41    pub fn metrics(&self) -> Metrics {
42        self.shaper.metrics()
43    }
44
45    /// The glyph a face's character map files `chr` under — the identity a
46    /// layout that carries none falls back to.
47    #[must_use]
48    pub fn glyph_index(&self, chr: char) -> Option<GlyphId> {
49        self.outlines.glyph_index(chr)
50    }
51
52    /// `id`'s ink as a path in font units, Y up, or `None` where the glyph
53    /// draws nothing.
54    #[must_use]
55    pub fn path(&self, id: GlyphId) -> Option<Path2d> {
56        if let Some(inked) = self.inked.borrow().get(&id) {
57            return inked.clone();
58        }
59        let path = trace(&self.outlines.outline(id));
60        self.inked.borrow_mut().insert(id, path.clone());
61        path
62    }
63}
64
65impl TextLayout for Glyphs {
66    fn typeface(&self) -> FontChoice {
67        self.shaper.typeface()
68    }
69
70    fn layout(&self, text: &str, font: &Font, wrap: WorldPx) -> Layout {
71        self.shaper.layout(text, font, wrap)
72    }
73}
74
75fn trace(outline: &[PathCommand]) -> Option<Path2d> {
76    if outline.is_empty() {
77        return None;
78    }
79    let path = Path2d::new().ok()?;
80    for command in outline {
81        match *command {
82            PathCommand::MoveTo(to) => path.move_to(to.x.into(), to.y.into()),
83            PathCommand::LineTo(to) => path.line_to(to.x.into(), to.y.into()),
84            PathCommand::QuadTo(control, to) => path.quadratic_curve_to(
85                control.x.into(),
86                control.y.into(),
87                to.x.into(),
88                to.y.into(),
89            ),
90            PathCommand::CurveTo(first, second, to) => path.bezier_curve_to(
91                first.x.into(),
92                first.y.into(),
93                second.x.into(),
94                second.y.into(),
95                to.x.into(),
96                to.y.into(),
97            ),
98            PathCommand::Close => path.close_path(),
99        }
100    }
101    Some(path)
102}