An HTML canvas has two sizes:
- The
widthandheightattributes define its drawing buffer. - CSS width and height define its displayed box.
Confusing the two sizes produces blurred output, stretched coordinates, or excessive memory use.
Drawing Buffer and Display Size #
Canvas attributes contain unitless pixel counts:
<canvas width="800" height="400"></canvas>The default drawing buffer is 300 by 150 pixels. CSS can display that buffer at another size:
canvas {
inline-size: 100%;
block-size: auto;
}When the CSS box and drawing buffer have different aspect ratios, the browser
stretches the bitmap. Writing width="800px" is invalid in intent because the
attribute is an integer, not a CSS length.
Changing canvas.width or canvas.height clears the bitmap and resets the
rendering context state, including transforms, clipping regions, styles, and
the current path.
Match the Device Pixel Ratio #
A high-density display maps one CSS pixel to multiple device pixels. Size the backing store from the rendered CSS box:
function resizeCanvas(canvas, context) {
const rect = canvas.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return false;
}
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const width = Math.round(rect.width * dpr);
const height = Math.round(rect.height * dpr);
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width;
canvas.height = height;
}
const scaleX = width / rect.width;
const scaleY = height / rect.height;
context.setTransform(scaleX, 0, 0, scaleY, 0, 0);
return true;
}After the transform, drawing code can use CSS-pixel coordinates while the backing store retains high-density detail. Cap the scale when a large canvas would consume excessive memory: an RGBA buffer needs approximately four bytes per pixel before implementation-specific overhead.
Use setTransform, not a repeated scale call, to establish the transform for
each redraw. Repeatedly multiplying the current transform would otherwise make
the canvas grow on every resize notification. Assigning either dimension
resets the context even when the assigned value equals the current value, so
compare first and redraw after every real assignment.
Respond to Layout Changes #
Canvas size can change because of viewport resizing, container queries, font loading, or surrounding content. Observe the element rather than listening only for a window resize:
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const observer = new ResizeObserver(() => {
if (resizeCanvas(canvas, context)) {
render(context);
}
});
observer.observe(canvas);Avoid a feedback loop in which changing the drawing-buffer attributes changes the CSS size. Give CSS independent control of the display box.
ResizeObserverEntry.devicePixelContentBoxSize can provide the content box in
device pixels without a CSS-pixel rounding step. Support is not universal in
older engines, so production code needs a fallback to contentBoxSize or
getBoundingClientRect().
Coordinate Mapping and Input #
Pointer coordinates arrive in viewport CSS pixels. Map them into the drawing buffer when drawing code uses backing-store coordinates:
const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) * (canvas.width / rect.width);
const y = (event.clientY - rect.top) * (canvas.height / rect.height);CSS transforms can make the displayed geometry more complex than a simple
scale. For transformed interactive canvases, invert the applicable DOMMatrix
rather than using a width ratio. If drawing uses the CSS-pixel transform from
the previous section, subtracting rect.left and rect.top already produces
the logical drawing coordinates; applying the backing-store ratio again would
double-scale the input.
Libraries and Offscreen Rendering #
Graphics libraries can update canvas attributes when a renderer is resized. For example, a library can set the drawing buffer independently from the CSS box. Read the library’s size API before assigning attributes directly because an external assignment can discard renderer state.
OffscreenCanvas has a drawing-buffer size but no CSS box. The main thread
must communicate the desired pixel dimensions to the worker. Transfer images
or rendered frames without assuming that the worker can inspect layout.
After transferControlToOffscreen(), rendering ownership belongs to the
offscreen canvas; requesting another rendering context from the original DOM
canvas is invalid.
Comments
Read or comment on GitHub (sign-in required to comment)