1
//! Canvas renderer for the browser. Uses `plotters-canvas::CanvasBackend`
2
//! and the shared `draw_on` dispatch in `crate::draw`, so a chart
3
//! rendered into a `<canvas>` looks identical to its SVG twin.
4
//!
5
//! Only compiles under feature `canvas`, which is enabled by the
6
//! `nomisync-frontend` WASM crate.
7

            
8
use plotters::prelude::*;
9
use plotters_canvas::CanvasBackend;
10
use wasm_bindgen::JsValue;
11
use web_sys::HtmlCanvasElement;
12

            
13
use crate::draw::draw_on;
14
use crate::spec::ChartSpec;
15

            
16
/// Render `spec` into `canvas` via `plotters-canvas`. Sizes the
17
/// backing canvas to `device_pixel_ratio` so the chart stays crisp on
18
/// `HiDPI` screens; the caller is expected to set the CSS width/height
19
/// to the logical size before calling.
20
///
21
/// # Errors
22
///
23
/// Returns `Err(JsValue)` when the canvas element cannot be wrapped
24
/// (missing 2D context or the shared drawing dispatch fails).
25
pub fn render_canvas(spec: &ChartSpec, canvas: &HtmlCanvasElement) -> Result<(), JsValue> {
26
    apply_dpr(canvas)?;
27

            
28
    let backend = CanvasBackend::with_canvas_object(canvas.clone())
29
        .ok_or_else(|| JsValue::from_str("plotters-canvas: failed to get 2D context"))?;
30
    let root = backend.into_drawing_area();
31

            
32
    draw_on(&root, spec).map_err(|e| JsValue::from_str(&format!("draw failed: {e}")))?;
33
    root.present()
34
        .map_err(|e| JsValue::from_str(&format!("present failed: {e}")))?;
35
    Ok(())
36
}
37

            
38
/// Multiply the backing store by the window's device pixel ratio so
39
/// strokes and text stay crisp. CSS width/height (the logical size)
40
/// must be set by the caller — this function only touches the
41
/// `width`/`height` attributes on the canvas element.
42
fn apply_dpr(canvas: &HtmlCanvasElement) -> Result<(), JsValue> {
43
    let dpr = web_sys::window()
44
        .ok_or_else(|| JsValue::from_str("no window"))?
45
        .device_pixel_ratio();
46
    if dpr <= 0.0 || !dpr.is_finite() {
47
        return Ok(());
48
    }
49

            
50
    let logical_w = f64::from(canvas.client_width().max(1));
51
    let logical_h = f64::from(canvas.client_height().max(1));
52
    let backing_w = (logical_w * dpr).round().max(1.0) as u32;
53
    let backing_h = (logical_h * dpr).round().max(1.0) as u32;
54

            
55
    // Only resize the backing store when the canvas is attached and has
56
    // non-zero CSS dimensions. Otherwise leave the author's width/height
57
    // attributes untouched.
58
    if logical_w > 0.0 && logical_h > 0.0 {
59
        canvas.set_width(backing_w);
60
        canvas.set_height(backing_h);
61
    }
62
    Ok(())
63
}