window.devicePixelRatio counts device pixels per CSS pixel: 2 on most laptops, 3 on many phones, and it changes as the user zooms. A bitmap the size of its CSS box is upscaled and goes soft; allocate that many times more pixels and scale the context to keep drawing in CSS pixels.
<canvas id="soft" width="290" height="44" style="width:290px; height:44px"></canvas>
<canvas id="sharp" style="width:290px; height:44px"></canvas>
<script>
function hidpi(c, r = devicePixelRatio) { // kept in demos/ch02/hidpi.js
c.width = c.clientWidth * r; c.height = c.clientHeight * r;
const ctx = c.getContext('2d'); ctx.scale(r, r); return ctx;
}
for (const ctx of [soft.getContext('2d'), hidpi(sharp)]) {
ctx.fillStyle = '#5b3f99'; ctx.fillRect(4, 4, 26, 36);
ctx.font = '14px Georgia'; ctx.fillText('Patterns of the Deep Web', 38, 20);
ctx.font = '11px sans-serif'; ctx.fillText(`bitmap ${ctx.canvas.width}px wide`, 38, 36);
}
</script>
At the capture's ratio of 2 the left canvas has a quarter of the pixels it needs. Later listings load hidpi.js; re-run it when a ResizeObserver or a matchMedia() query for the ratio reports a change.