Command Palette

Search for a command to run...

Hectal
Course outline
Topic 3.3Beginner ~21 min · step 3 of 7

Resize and Pixel Ratio

In one line

A canvas has two sizes (its box on the page and its pixels) and the camera has an aspect ratio. You learn to keep all three in step with a ResizeObserver, to cap the pixel ratio at 2, and to survive a container that is 0 pixels tall.

0/7 · 0%

Think of it like this

A photo in a picture frame. The frame (the canvas's box) can be any size; the photo (the canvas's pixels) has its own number of dots. If the photo is only stretched to a wider frame, faces look fat. To keep faces right you must also change the shape of what the camera shoots: the aspect ratio.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

CSS pixel
The unit a web page uses for sizes. On a dense screen one CSS pixel is drawn with 2 or 3 real pixels in each direction.
pixel ratio
Real pixels per CSS pixel: window.devicePixelRatio. renderer.setPixelRatio multiplies the canvas's pixel size by it.
aspect ratio
Width divided by height. camera.aspect must equal the canvas's, or the picture is stretched.
ResizeObserver
A browser object that calls your function when an element's size changes, including once when you start watching.

Step by step

01Give the container a size in CSS

#app { position: fixed; inset: 0; } makes the box fill the window. A box whose size comes from CSS (not from its content) is the starting point; the canvas follows it, not the other way round.

02Write one resize function

Read app.clientWidth and app.clientHeight; if either is 0, return. Otherwise renderer.setSize(width, height), camera.aspect = width / height, camera.updateProjectionMatrix() and, if you draw on demand, renderer.render(scene, camera).

03Watch the box, not the window

new ResizeObserver(resize).observe(app). It calls resize once at the start, so you do not need a separate first call (the project keeps one anyway so the size is right before the first frame).

04Cap the pixel ratio when you create the renderer

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) comes before the first setSize. If the ratio can change (a window dragged to another monitor), call it again inside resize.

Try it yourself

  1. 1

    Try an odd pixel ratio

    In the second example add 2.75 to the list of ratios. The canvas is floor(390 × 2.75) = 1072 by floor(844 × 2.75) = 2321 pixels, 2.49 megapixels, about 7.6 times ratio 1. The cap row shows 780 x 1688 again.

  2. 2

    Shrink a grid cell

    In the first example, change the HTML to <div id="wrap"><div id="app"></div><aside>panel</aside></div> and the CSS to #wrap { display: grid; grid-template-columns: 1fr 100px; width: 600px; height: 200px; } #app { height: 200px; }. At the end of the script add setTimeout(() => { document.querySelector("#wrap").style.width = "300px"; setTimeout(() => console.log("app", app.clientWidth, "| canvas", renderer.domElement.clientWidth), 300); }, 400);. It prints app 500 | canvas 500: the cell did not shrink. Add min-width: 0; to #app and it prints app 200 | canvas 200.

Code & diagrams

A canvas that follows its container New tab

Resize the preview or open the scene in a narrow window: the box stays in proportion because the observer updates the camera as well as the canvas.

idle

Console

Run the scene to see its console.

Expected console output

resize ran: true | canvas fills the box: true
aspect matches the box: true
What a phone costs at each pixel ratio New tab

One 390 × 844 phone page at different ratios. Rows marked `cap` show what `Math.min(ratio, 2)` uses instead. The last line is real: `setPixelRatio(undefined)` returns early.

idle

Console

Run the scene to see its console.
A container with no height New tab

The `naive` columns are what you get from dividing a size by 0. The `guarded` column is what the guard keeps.

idle

Console

Run the scene to see its console.

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

A container whose height is 0

In the first example change the CSS #app { position: fixed; inset: 0; } to #app { height: 0; } (a collapsed panel).

terminal
$ Run
── what you'll see ──
resize ran: false | canvas fills the box: false
aspect matches the box: false
With the guard in place there is no error and no `NaN`: the canvas keeps its default 300 × 150 pixels, and the camera keeps its starting aspect of 1.

Myth vs fact

Myth

window.addEventListener("resize", ...) is enough.

Fact

It fires only when the window changes. A side panel opening, a grid column growing or a tab being shown changes the canvas's box without any window event. ResizeObserver fires for all of them.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    setSize writes canvas.width and canvas.height, which clears the canvas and reallocates buffers. Resizing on every event while a user drags a window edge is expensive, so many apps debounce, or draw straight after the resize as the example does.

  • ▸

    A ResizeObserver callback runs in the browser's rendering step, after layout and before paint. Drawing inside it avoids the one-frame flash of a stretched or empty canvas.

Remember this

  1. 1

    The problem this solves: the Phase 0 stand listened to the window's resize event. A real page puts the canvas in a box beside a panel or in a grid; the window does not change when that box does, and a rotating phone swaps width and height. You must watch the box itself, and handle it being empty.

  2. 2

    Three sizes must agree: the canvas's CSS size (how big it looks), its pixel size (canvas.width, canvas.height) and the camera's aspect. renderer.setSize(w, h) sets the first two (pixels = size × pixel ratio). Then set camera.aspect = w / h and call camera.updateProjectionMatrix(), which rebuilds the projection matrix (Topic 2.5). Forget it and the picture is stretched.

  3. 3

    Pixel ratio is real screen pixels per CSS pixel: 1 on older screens, 2 on most laptops and phones, up to 3 on some. setPixelRatio(r) makes the canvas floor(width × r) by floor(height × r) pixels. A 390 × 844 phone at ratio 3 needs 1170 × 2532: 2.96 million pixels, nine times ratio 1 (second example).

  4. 4

    Cap it at 2: renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)). Past 2 the sharpness is hard to see on a phone, but ratio 3 costs 2.25 times the pixels of ratio 2. It is the cheapest performance setting in 3D on the web (Phase 17 measures it). setPixelRatio(undefined) does nothing.

  5. 5

    **ResizeObserver watches a box.** new ResizeObserver(callback).observe(app) calls back once at the start and after every size change: a window resize, a rotating phone, a panel opening, a grid column growing. Inside, read app.clientWidth and app.clientHeight, resize, and draw a frame at once: setSize clears the canvas, so an on-demand loop would show it blank.

  6. 6

    The zero-size container. A <div> with no height set is as tall as its content, and a hidden tab panel or collapsed accordion gives 0 too. Then width / height is Infinity (or NaN for 0 × 0), the projection matrix fills with NaN and nothing draws (third example). Guard: ignore the callback when width or height is 0 and keep the last good size; give the container a real size in CSS.

  7. 7

    A canvas in a CSS grid can refuse to shrink. A grid column written 1fr cannot get narrower than its content, and the content is the canvas at the size you set. Shrink the grid and the cell stays wide. Add min-width: 0 to the cell that holds the canvas (the tryIt shows both).

  8. 8

    Rotation and orientation need no special code: a rotating phone changes the box's size, and the observer fires. Test it in the browser's device mode or on a real phone: a rotation swaps width and height at once and may report an in-between size first.

Explain it without notes

01

Why must the camera's aspect change when the canvas's box changes, and which call makes the change take effect?

02

Why cap the pixel ratio at 2? Give the megapixel numbers for a 390 × 844 phone.

Practice

01

Make the resize function also redraw only when the size really changed (use the observer's entry).

02

Write a guard that clamps the canvas to at most 2048 pixels on its longer side.

Done when you can

  • Keep canvas size, pixel size and camera aspect in step

  • Use a ResizeObserver on the canvas's container

  • Cap setPixelRatio at 2 and say what it saves

  • Guard against a container that is 0 pixels in either direction