maia_wasm/render/engine/
text.rs

1use super::CanvasDims;
2use wasm_bindgen::prelude::*;
3use wasm_bindgen::JsCast;
4use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
5
6pub struct TextRender {
7    canvas: HtmlCanvasElement,
8    context: CanvasRenderingContext2d,
9}
10
11/// Rendered texts dimensions.
12///
13/// This struct contains the dimensions describing how a list of strings of text
14/// has been rendered into a texture.
15pub struct TextsDimensions {
16    /// Texture coordinates for the bounding box of each string.
17    ///
18    /// This vector is the concatenation of 4 texture coordinates for each
19    /// string of text that has been rendered. These 4 coordinates consist of 8
20    /// floats which give the coordinates (in texture coordinates units) for the
21    /// bounding box of the corresponding text string in the rendered texture.
22    pub texture_coordinates: Vec<f32>,
23    /// Width for the bounding box of each string.
24    ///
25    /// This gives the width of the bounding box of each of the rendered
26    /// strings, using screen coordinates.
27    pub text_width: f32,
28    /// Height for the bounding box of each string.
29    ///
30    /// This gives the height of the bounding box of each of the rendered
31    /// strings, using screen coordinates.
32    pub text_height: f32,
33}
34
35impl TextRender {
36    pub fn new(document: &web_sys::Document) -> Result<TextRender, JsValue> {
37        let canvas = document
38            .create_element("canvas")?
39            .dyn_into::<HtmlCanvasElement>()?;
40        let context = canvas
41            .get_context("2d")?
42            .ok_or("unable to get 2d context")?
43            .dyn_into::<CanvasRenderingContext2d>()?;
44        Ok(TextRender { canvas, context })
45    }
46
47    pub fn canvas(&self) -> &HtmlCanvasElement {
48        &self.canvas
49    }
50
51    pub fn text_width(&self, text: &str, dims: CanvasDims, height_px: u32) -> Result<f32, JsValue> {
52        self.set_font(height_px);
53        let width_px = self.context.measure_text(text)?.width();
54        let width_relative = 2.0 * width_px as f32 / dims.width as f32;
55        Ok(width_relative)
56    }
57
58    fn set_font(&self, height_px: u32) {
59        self.context.set_font(&format!("bold {height_px}px sans"))
60    }
61
62    pub fn render(
63        &self,
64        texts: &[String],
65        dims: CanvasDims,
66        height_px: u32,
67    ) -> Result<TextsDimensions, JsValue> {
68        // Find maximum width over all the texts
69        self.set_font(height_px);
70        let mut max = None;
71        for text in texts.iter() {
72            let w = self.context.measure_text(text)?.width();
73            max = match (max, w) {
74                (Some(z), w) if z >= w => Some(z),
75                _ => Some(w),
76            };
77        }
78        let width_px = max.ok_or("no texts specified")?.ceil() as u32;
79
80        // Add some pixels of vertical margin to prevent pieces of texts
81        // from showing in the labels for other texts
82        let height_px_margin = height_px + 2;
83        let margin = 0.5 * (1.0 - height_px as f32 / height_px_margin as f32);
84
85        // Set 2D canvas dimensions to contain all the texts
86        let n = (texts.len() as f32 * width_px as f32 / height_px as f32)
87            .sqrt()
88            .round() as usize;
89        let m = (texts.len() + n - 1) / n;
90        let total_height_px = height_px_margin * n as u32;
91        let total_width_px = width_px * m as u32;
92        self.canvas.set_width(total_width_px);
93        self.canvas.set_height(total_height_px);
94
95        self.context.set_text_align("center");
96        self.context.set_text_baseline("middle");
97        // Setting the font again is needed after resizing the canvas.
98        self.set_font(height_px);
99        self.context
100            .clear_rect(0.0, 0.0, total_width_px as f64, total_height_px as f64);
101        self.context.set_fill_style_str("white");
102
103        // Render each text and calculate its texture coordinates. Each text
104        // gets 4 2D coordinates, given by the corners of its bounding
105        // rectangle.
106        let mut texture_coords = Vec::with_capacity(8 * texts.len());
107        for (j, text) in texts.iter().enumerate() {
108            let b = j / n;
109            let a = j - b * n;
110            self.context.fill_text(
111                text,
112                (b as f64 + 0.5) * width_px as f64,
113                (a as f64 + 0.5) * height_px_margin as f64,
114            )?;
115            texture_coords.push(b as f32 / m as f32);
116            texture_coords.push(((a + 1) as f32 - margin) / n as f32);
117            texture_coords.push((b + 1) as f32 / m as f32);
118            texture_coords.push(((a + 1) as f32 - margin) / n as f32);
119            texture_coords.push(b as f32 / m as f32);
120            texture_coords.push((a as f32 + margin) / n as f32);
121            texture_coords.push((b + 1) as f32 / m as f32);
122            texture_coords.push((a as f32 + margin) / n as f32);
123        }
124
125        let width_relative = 2.0 * width_px as f32 / dims.width as f32;
126        let height_relative = 2.0 * height_px as f32 / dims.height as f32;
127        Ok(TextsDimensions {
128            texture_coordinates: texture_coords,
129            text_width: width_relative,
130            text_height: height_relative,
131        })
132    }
133}