Sharper HTML Canvas
If you’ve used HTML Canvas to display images or draw something before, you may have noticed that the resolution can be a bit lacking. Here I’m going to show you how to stop using the default resolution and make HTML Canvas as sharp as you want it to be.
The basic idea of what to do here is simply to make the canvas bigger than what we can see according to the css styling. The bigger the canvas, the more detail it can contain, and by restricting the width with css, we condense that detail into a smaller area. We then scale the context of the canvas up accordingly so the content remains the correct size.
HTML
Starting with the html, we can create the canvas here, or we can choose to create it in javascript later. Anyway, let’s make a basic slider to adjust the sharpness with.
<!-- <canvas class="canvas" width="360px" height="360px"></canvas> -->
<p>Sharpness
<span class="sharpness">2</span>
</p>
<input type="range" min="1" max="32" value="8" class="slider" id="sharpSlider">
JavaScript
First we’ll set the initial scale of the canvas, and create it if you want to generate the canvas instead of putting it in the html. Set a variable to record the original width and height of the canvas. Set the style width and height to keep the canvas the same size. You could also write this in the CSS file if you prefer that. I prefer writing it in the javascript in case I want to change the size of the canvas dynamically.
let scale = 2;//canvas created in js
let onScreenCVS = document.createElement("canvas");
onScreenCVS.width = 360;
onScreenCVS.height = 360;
document.body.prepend(onScreenCVS);
//canvas created in html
// let onScreenCVS = document.querySelector(".canvas");
let onScreenCTX = onScreenCVS.getContext("2d");
let ocWidth = onScreenCVS.width;
let ocHeight = onScreenCVS.height;
onScreenCVS.style.width = `${ocWidth}px`;
onScreenCVS.style.height = `${ocHeight}px`;
Add an event listener for the slider to adjust sharpness. Here we’ll update the scale variable.
let sharpnessDisplay = document.querySelector(".sharpness");
let sharpSlider = document.querySelector("#sharpSlider");sharpSlider.addEventListener('input', updateSharpness);function updateSharpness() {
scale = sharpSlider.value/4;
sharpnessDisplay.textContent = scale;
window.setTimeout(draw, 1);
}
Finally, our draw function to render the canvas. This is the most important code. Change the canvas width and height with the scale, and scale the context to match. Making adjustments like this will reset the canvas anyway, but it’s good practice to clear the canvas before drawing anything new.
function draw() {
onScreenCVS.width = ocWidth * scale;
onScreenCVS.height = ocHeight * scale; onScreenCTX.scale(scale, scale);
onScreenCTX.clearRect(0,0,ocWidth,ocHeight);
An important thing to keep in mind when drawing on this scaled canvas is that the coordinates remain tied to the styling, so for example to draw something on the far end of the canvas requires you to divide by the scale first.
//coordinates remain analogous to styling, so divide it by the scale to stay within the visible canvas
onScreenCTX.fillStyle = "red";
onScreenCTX.fillRect(
onScreenCVS.width/scale-40,
onScreenCVS.height/scale-40,
30,
30
);
When you stroke a line on a canvas, it draws half the width of the line on either side of the specified coordinates. At the default resolution, if you try to draw half a pixel within a pixel, it will draw the color averaged with the rest of the color inside the pixel. So if you stroke a black line with a width of 1 pixel without accounting for subpixels, you’ll end up drawing a gray line with a width of 2 pixels. I made a series of strokes to help visualize this.
(Important!) When you sharpen the canvas, it allows you to draw subpixels. At 2x sharper, half-width subpixels can be drawn clearly, at 4x, quarter-width subpixels can be drawn clearly. This is why you should choose a power of 2 for your canvas to be scaled at, to make it easier to keep track of what level subpixels can be drawn at.
onScreenCTX.fillStyle = "black";
onScreenCTX.fillRect(50, 50, 40, 40);
onScreenCTX.strokeStyle = "black";
//box outlines
onScreenCTX.beginPath();
onScreenCTX.rect(40, 40, 60, 60);
onScreenCTX.lineWidth = 2;
onScreenCTX.stroke();
onScreenCTX.beginPath();
onScreenCTX.rect(30, 30, 80, 80);
onScreenCTX.lineWidth = 1;
onScreenCTX.stroke();onScreenCTX.beginPath();
onScreenCTX.rect(20, 20, 100, 100);
onScreenCTX.lineWidth = 0.5;
onScreenCTX.stroke();
onScreenCTX.beginPath();
onScreenCTX.rect(10, 10, 120, 120);
onScreenCTX.lineWidth = 0.25;
onScreenCTX.stroke();
//diagonals
onScreenCTX.beginPath();
onScreenCTX.moveTo(140, 10);
onScreenCTX.lineTo(260, 130);
onScreenCTX.lineWidth = 2;
onScreenCTX.stroke();
onScreenCTX.beginPath();
onScreenCTX.moveTo(150, 10);
onScreenCTX.lineTo(270, 130);
onScreenCTX.lineWidth = 1;
onScreenCTX.stroke();
onScreenCTX.beginPath();
onScreenCTX.moveTo(160, 10);
onScreenCTX.lineTo(280, 130);
onScreenCTX.lineWidth = 0.5;
onScreenCTX.stroke();
onScreenCTX.beginPath();
onScreenCTX.moveTo(170, 10);
onScreenCTX.lineTo(290, 130);
onScreenCTX.lineWidth = 0.25;
onScreenCTX.stroke();
//densely packed lines
onScreenCTX.beginPath();
onScreenCTX.moveTo(300, 10);
onScreenCTX.lineTo(300, 130);
onScreenCTX.moveTo(302, 10);
onScreenCTX.lineTo(302, 130);
onScreenCTX.lineWidth = 1;
onScreenCTX.stroke();
onScreenCTX.beginPath();
onScreenCTX.moveTo(304, 10);
onScreenCTX.lineTo(304, 130);
onScreenCTX.moveTo(305, 10);
onScreenCTX.lineTo(305, 130);
onScreenCTX.lineWidth = 0.5;
onScreenCTX.stroke();
onScreenCTX.beginPath();
onScreenCTX.moveTo(306, 10);
onScreenCTX.lineTo(306, 130);
onScreenCTX.moveTo(306.5, 10);
onScreenCTX.lineTo(306.5, 130);
onScreenCTX.lineWidth = 0.25;
onScreenCTX.stroke();
And the image, to help illustrate how sharp is sharp enough. There’s not much point going sharper than your computer can display, after all.
let hummer = new Image();
hummer.src = "https://i.imgur.com/QxEUIOI.jpg";
hummer.onload = () => {
onScreenCTX.drawImage(hummer,10,150,302,200);
}
}