1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
use super::CanvasDims;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};

pub struct TextRender {
    canvas: HtmlCanvasElement,
    context: CanvasRenderingContext2d,
}

/// Rendered texts dimensions.
///
/// This struct contains the dimensions describing how a list of strings of text
/// has been rendered into a texture.
pub struct TextsDimensions {
    /// Texture coordinates for the bounding box of each string.
    ///
    /// This vector is the concatenation of 4 texture coordinates for each
    /// string of text that has been rendered. These 4 coordinates consist of 8
    /// floats which give the coordinates (in texture coordinates units) for the
    /// bounding box of the corresponding text string in the rendered texture.
    pub texture_coordinates: Vec<f32>,
    /// Width for the bounding box of each string.
    ///
    /// This gives the width of the bounding box of each of the rendered
    /// strings, using screen coordinates.
    pub text_width: f32,
    /// Height for the bounding box of each string.
    ///
    /// This gives the height of the bounding box of each of the rendered
    /// strings, using screen coordinates.
    pub text_height: f32,
}

impl TextRender {
    pub fn new(document: &web_sys::Document) -> Result<TextRender, JsValue> {
        let canvas = document
            .create_element("canvas")?
            .dyn_into::<HtmlCanvasElement>()?;
        let context = canvas
            .get_context("2d")?
            .ok_or("unable to get 2d context")?
            .dyn_into::<CanvasRenderingContext2d>()?;
        Ok(TextRender { canvas, context })
    }

    pub fn canvas(&self) -> &HtmlCanvasElement {
        &self.canvas
    }

    pub fn text_width(&self, text: &str, dims: CanvasDims, height_px: u32) -> Result<f32, JsValue> {
        self.set_font(height_px);
        let width_px = self.context.measure_text(text)?.width();
        let width_relative = 2.0 * width_px as f32 / dims.width as f32;
        Ok(width_relative)
    }

    fn set_font(&self, height_px: u32) {
        self.context.set_font(&format!("bold {height_px}px sans"))
    }

    pub fn render(
        &self,
        texts: &[String],
        dims: CanvasDims,
        height_px: u32,
    ) -> Result<TextsDimensions, JsValue> {
        // Find maximum width over all the texts
        self.set_font(height_px);
        let mut max = None;
        for text in texts.iter() {
            let w = self.context.measure_text(text)?.width();
            max = match (max, w) {
                (Some(z), w) if z >= w => Some(z),
                _ => Some(w),
            };
        }
        let width_px = max.ok_or("no texts specified")?.ceil() as u32;

        // Add some pixels of vertical margin to prevent pieces of texts
        // from showing in the labels for other texts
        let height_px_margin = height_px + 2;
        let margin = 0.5 * (1.0 - height_px as f32 / height_px_margin as f32);

        // Set 2D canvas dimensions to contain all the texts
        let n = (texts.len() as f32 * width_px as f32 / height_px as f32)
            .sqrt()
            .round() as usize;
        let m = (texts.len() + n - 1) / n;
        let total_height_px = height_px_margin * n as u32;
        let total_width_px = width_px * m as u32;
        self.canvas.set_width(total_width_px);
        self.canvas.set_height(total_height_px);

        self.context.set_text_align("center");
        self.context.set_text_baseline("middle");
        // Setting the font again is needed after resizing the canvas.
        self.set_font(height_px);
        self.context
            .clear_rect(0.0, 0.0, total_width_px as f64, total_height_px as f64);
        self.context.set_fill_style(&JsValue::from_str("white"));

        // Render each text and calculate its texture coordinates. Each text
        // gets 4 2D coordinates, given by the corners of its bounding
        // rectangle.
        let mut texture_coords = Vec::with_capacity(8 * texts.len());
        for (j, text) in texts.iter().enumerate() {
            let b = j / n;
            let a = j - b * n;
            self.context.fill_text(
                text,
                (b as f64 + 0.5) * width_px as f64,
                (a as f64 + 0.5) * height_px_margin as f64,
            )?;
            texture_coords.push(b as f32 / m as f32);
            texture_coords.push(((a + 1) as f32 - margin) / n as f32);
            texture_coords.push((b + 1) as f32 / m as f32);
            texture_coords.push(((a + 1) as f32 - margin) / n as f32);
            texture_coords.push(b as f32 / m as f32);
            texture_coords.push((a as f32 + margin) / n as f32);
            texture_coords.push((b + 1) as f32 / m as f32);
            texture_coords.push((a as f32 + margin) / n as f32);
        }

        let width_relative = 2.0 * width_px as f32 / dims.width as f32;
        let height_relative = 2.0 * height_px as f32 / dims.height as f32;
        Ok(TextsDimensions {
            texture_coordinates: texture_coords,
            text_width: width_relative,
            text_height: height_relative,
        })
    }
}