How Canvas Resizing Affects WebGL Viewport
When you dynamically resize an HTML canvas, the internal WebGL viewport does not automatically update to match the new dimensions. While changing the canvas size alters the underlying drawing buffer, WebGL retains its previously configured coordinate mappings until explicitly told otherwise. This article breaks down the exact mechanics behind canvas resizing in WebGL, explaining why the viewport remains decoupled, the visual artifacts that occur when they fall out of sync, and how to correctly handle dynamic updates in your rendering loop.
Canvas Drawing Buffer vs. Display Size
An HTML <canvas> element operates with two
distinct sets of dimensions:
- Display Size (CSS): The dimensions determined by
CSS styling (
width,height, or flexbox/grid rules), which dictate how much space the element occupies in the browser layout. - Drawing Buffer Size: The actual pixel resolution of
the canvas set via the DOM attributes
canvas.widthandcanvas.height.
If you change the CSS size without updating canvas.width
and canvas.height, the browser scales the existing drawing
buffer like an image, resulting in blurry, pixelated graphics or uneven
stretching. To resize dynamically and maintain crisp rendering, you must
synchronize canvas.width and canvas.height
with the computed CSS pixel size (optionally multiplied by
window.devicePixelRatio for high-DPI screens).
The Decoupling of the WebGL Viewport
When the drawing buffer size changes, WebGL does not automatically adjust its rendering coordinates.
The WebGL viewport is controlled independently using the command:
gl.viewport(x, y, width, height);This function specifies the affine transformation that maps
normalized device coordinates (NDC)—which span from -1 to 1 along the X
and Y axes—to actual window/pixel coordinates in the drawing buffer.
When the WebGL context is initially created, it sets the viewport to
match the canvas's initial width and height.
After that initial setup, WebGL never modifies the viewport state on its
own, regardless of how often the canvas changes size.
Visual Consequences of Mismatched Sizes
If you update canvas.width and
canvas.height without calling
gl.viewport():
- Expanding the Canvas: If the canvas grows larger
than the original viewport setting, WebGL continues drawing strictly
within the pixel bounds defined by the old viewport. Because WebGL's
origin
(0, 0)sits at the bottom-left corner, your rendered scene will appear constrained to a small box in the lower-left corner of the canvas, leaving the rest untouched or cleared. - Shrinking the Canvas: If the canvas becomes smaller than the viewport, geometry mapped to the viewport's dimensions extends beyond the edges of the drawing buffer. This results in severe clipping, where only the lower-left fraction of your intended scene remains visible.
Aspect Ratio and the Projection Matrix
Beyond the viewport mapping, canvas resizing alters the aspect ratio
(width / height). Even if you update
gl.viewport(0, 0, canvas.width, canvas.height), your 3D
objects may still appear squished or horizontally stretched.
This occurs because 3D projection matrices (such as perspective or orthographic matrices) depend directly on the aspect ratio to preserve uniform scaling. When you adjust the canvas dimensions, you must also recalculate the projection matrix with the updated aspect ratio before rendering the next frame.
The Standard Pattern for Dynamic Resizing
To handle dynamic resizing cleanly, inspect the canvas size on each
animation frame or attach a ResizeObserver to the canvas
element.
- Check and Update Buffer: Verify whether
canvas.widthandcanvas.heightmatch the element's client dimensions (scaled bydevicePixelRatio). Update them if they differ. - Update Viewport: Call
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)using the WebGL context's actual buffer dimensions. - Update Projection: Recompute the camera's
projection matrix using the new aspect ratio
(
gl.drawingBufferWidth / gl.drawingBufferHeight). - Draw: Render the frame with the synchronized states.
By synchronizing the drawing buffer, the WebGL viewport, and the camera projection simultaneously, dynamic resizing functions smoothly across varying screen sizes and window adjustments without graphical distortion.