Skip to main content

blockworx_canvas2d/
fit.rs

1//! The backing store, sized for the display.
2//!
3//! The kernel, the layout and the display list all work in CSS pixels; a
4//! device with more than one device pixel per CSS pixel gets a bigger backing
5//! store and a context scaled to match, so nothing above this ever sees the
6//! difference.
7
8use blockworx_geom::{Rect, Vec2, pos2};
9use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, Window};
10
11/// How many device pixels one CSS pixel is worth. Never zero or negative: a
12/// host that reports one is a host whose canvas would have no pixels.
13#[derive(Clone, Copy, PartialEq, Debug)]
14pub struct DevicePixelRatio(f32);
15
16impl DevicePixelRatio {
17    /// The ratio, floored at 1 — a host reporting less would shrink the
18    /// backing store below the diagram.
19    #[must_use]
20    pub fn new(ratio: f32) -> Self {
21        Self(if ratio.is_finite() && ratio > 1.0 {
22            ratio
23        } else {
24            1.0
25        })
26    }
27
28    /// What the window reports right now. It changes as a tab moves between
29    /// displays or the page is zoomed, so it is read per frame.
30    #[must_use]
31    pub fn of(window: &Window) -> Self {
32        Self::new(window.device_pixel_ratio() as f32)
33    }
34}
35
36impl From<DevicePixelRatio> for f32 {
37    fn from(ratio: DevicePixelRatio) -> Self {
38        ratio.0
39    }
40}
41
42/// Size `canvas`'s backing store for `css_size` at `ratio` and put `ctx` in
43/// CSS pixels. Answers the viewport the display list is drawn in.
44pub fn fit(
45    canvas: &HtmlCanvasElement,
46    ctx: &CanvasRenderingContext2d,
47    css_size: Vec2,
48    ratio: DevicePixelRatio,
49) -> Rect {
50    let scale = f32::from(ratio);
51    let backing = |css: f32| (css * scale).round().max(1.0) as u32;
52    let (width, height) = (backing(css_size.x), backing(css_size.y));
53    // Sizing the backing store clears it even to the size it already has, and
54    // a cleared canvas is a blank frame on screen.
55    if canvas.width() != width || canvas.height() != height {
56        canvas.set_width(width);
57        canvas.set_height(height);
58    }
59    // Sizing the backing store resets the context, so the scale is re-applied
60    // rather than accumulated.
61    let _ = ctx.set_transform(scale.into(), 0.0, 0.0, scale.into(), 0.0, 0.0);
62    Rect::from_min_size(pos2(0.0, 0.0), css_size)
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn a_ratio_below_one_is_taken_as_one() {
71        assert_eq!(f32::from(DevicePixelRatio::new(0.0)), 1.0);
72        assert_eq!(f32::from(DevicePixelRatio::new(-2.0)), 1.0);
73        assert_eq!(f32::from(DevicePixelRatio::new(f32::NAN)), 1.0);
74        assert_eq!(f32::from(DevicePixelRatio::new(2.0)), 2.0);
75    }
76}