


Ensure crisp and sharp lines by understanding the relationship between line position and line thickness/width.
Earlier, we looked at how to draw a perfect grid. We even ensured that the visuals would be crisp on our displays by accounting for DPI. All of this makes for something pretty sweet-looking:
When dealing specifically with horizontal or vertical lines, an added subtle detail becomes important if we want to ensure those lines remain sharp and crisp. In this article, we will look into that subtle detail and more.
Onwards!
For the best and sharpest results when drawing lines, the conventional wisdom is that we should ensure all our lines are positioned on integer values. As it turns out, that isn’t always the case. Whether we position our lines on integer values or not is summarized by the following two rules:
The reason the rules are what they are has to do with how the canvas draws lines. Lines are positioned along the center of their coordinates, not at the edge. Because lines have a thickness, pixels are filled up to the thickness size in both directions perpendicular to the line's direction. For a horizontal line, this can be visualized as follows:
For a vertical line, our visual would be rotated by 90 degrees with the x-position determining where our line is located:
This combination of line position and line thickness determines whether the canvas draws our lines sharply or blurrily.
Now, this all may sound a bit abstract, so let's say that we are drawing a horizontal line at a vertical position of 25 pixels. Our goal is to ensure this line stays sharp and crisp across all line thickness values.
For a 1px-width line at position 25, given how our canvas would draw this line, our line will be positioned at the 25-pixel position. Because it has a thickness of 1 pixel, this line will expand 0.5px on either side of our 25-pixel position:
Because our line spreads outwards by the 0.5px, its ending point will be at 24.5px at the top and 25.5px at the bottom. Because this line visually ends at a half-pixel position, it will appear blurry as our canvas rendering engine tries to round up to the nearest pixel using antialiasing:
Knowing this, what if we position our 1px-width line at a half-pixel coordinate of 25.5? This will look as follows:
Notice that our 1px-width line starts at the 25.5-pixel y-position, and it spreads outward by 0.5px to end on pixel positions 25 and 26. Because no part of this line ends on a half-pixel coordinate, this line will look very sharp. No antialiasing shenanigans are needed. This is why all lines whose widths (or thicknesses) are odd-valued should specify a half-pixel coordinate value for their position.
Things are different for 2px lines (and other even-numbered width/thickness lines). These lines need to be positioned at a whole number coordinate:
Our line starts off at the 25-pixel y-position, and it grows outward by 1 pixel in each direction. The end result works nicely because the center of the line is on a whole pixel value and it spreads outwards by 1px in each direction to end on another pair of whole pixel values. There is nary a half-pixel value in sight, so the line appears very sharp. If we positioned our even-numbered width lines on a half-pixel value, this wouldn't work.
There is another way to draw lines, and that is by using a rectangle. To our eyes, a rectangle that is 1px in height is quite similar to a line that has a 1px thickness. To our canvas rendering engine, there is a big difference. Rectangles aren't drawn center-outward like lines are, so they are consistently predictable.
If you use a rectangle to draw lines, to ensure they look sharp, always make sure to position them on whole pixel values. Check out this example for a simple implementation.
To see all of the above words and visuals put into code, we have a live example (open in new window) that you and I can play with:
What we see are two pairs of canvas examples:
To see how to build this example yourself, create a new HTML document and add the following content to it:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Line Rendering Demo</title>
<style>
body {
font-family: monospace;
line-height: 1.6;
padding: 20px;
max-width: 900px;
margin: 0 auto;
}
.container {
display: flex;
flex-direction: column;
gap: 32px;
padding: 16px;
background-color: #f9fafb;
border-radius: 8px;
}
.header {
font-size: 24px;
font-weight: 800;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
.example {
padding: 8px;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 4px;
}
.example-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
}
canvas {
background-color: white;
border: 1px solid #e5e7eb;
width: 100%;
height: auto;
}
.explanation {
font-size: 14px;
background-color: #eff6ff;
padding: 12px;
border-radius: 4px;
border: 1px solid #dbeafe;
}
.explanation-title {
font-weight: 600;
margin-bottom: 8px;
}
.explanation p {
margin: 0 0 8px 0;
}
.examplehighlight {
background-color: #fff86c;
border-radius: 10px;
padding: 5px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">Canvas Line Rendering Comparison</div>
<div class="grid">
<div class="example">
<div class="example-title">Example 1: 1px-width line at <span class="examplehighlight">y=25</span></div>
<canvas id="canvas1" width="200" height="60"></canvas>
</div>
<div class="example">
<div class="example-title">Example 2: 1px-width line at <span class="examplehighlight">y=25.5</span></div>
<canvas id="canvas2" width="200" height="60"></canvas>
</div>
<div class="example">
<div class="example-title">Example 3: 2px-width line at <span class="examplehighlight">y=25</span></div>
<canvas id="canvas3" width="200" height="60"></canvas>
</div>
<div class="example">
<div class="example-title">Example 4: 2px-width line at <span class="examplehighlight">y=25.5</span></div>
<canvas id="canvas4" width="200" height="60"></canvas>
</div>
</div>
</div>
<script>
// Draw 1px lines at whole coordinates
const canvas1 = document.querySelector('#canvas1');
const ctx1 = canvas1.getContext('2d');
ctx1.beginPath();
ctx1.moveTo(50, 25);
ctx1.lineTo(150, 25);
ctx1.lineWidth = 1;
ctx1.strokeStyle = 'black';
ctx1.stroke();
// Draw 1px lines at half-pixel coordinates
const canvas2 = document.querySelector('#canvas2');
const ctx2 = canvas2.getContext('2d');
ctx2.beginPath();
ctx2.moveTo(50, 25.5);
ctx2.lineTo(150, 25.5);
ctx2.lineWidth = 1;
ctx2.strokeStyle = 'black';
ctx2.stroke();
// Draw 2px lines at whole coordinates
const canvas3 = document.querySelector('#canvas3');
const ctx3 = canvas3.getContext('2d');
ctx3.beginPath();
ctx3.moveTo(50, 25);
ctx3.lineTo(150, 25);
ctx3.lineWidth = 2;
ctx3.strokeStyle = 'black';
ctx3.stroke();
// Draw 2px lines at half-pixel coordinates
const canvas4 = document.querySelector('#canvas4');
const ctx4 = canvas4.getContext('2d');
ctx4.beginPath();
ctx4.moveTo(50, 25.5);
ctx4.lineTo(150, 25.5);
ctx4.lineWidth = 2;
ctx4.strokeStyle = 'black';
ctx4.stroke();
</script>
</body>
</html>
If you look at our code, there is nothing too fancy going on. The basic Line Drawing techniques are highlighted, with the only exotic detail being the vertical position of each line being set to a whole coordinate value or a half-pixel coordinate value depending on the line thickness.
There are two more tidbits to highlight before we call it a day:
Our friendly canvas is an immediate mode system. This means we are responsible for all aspects of getting pixels drawn on the screen. To do this effectively, we must be aware of its various drawing quirks and adjust our code accordingly. What we saw here around ensuring our lines stay sharp by accounting for position and line width/thickness is just one among the many details that we'll become very accustomed to as we get increasingly more proficient in using the canvas.
Just a final word before we wrap up. What you've seen here is freshly baked content without added preservatives, artificial intelligence, ads, and algorithm-driven doodads. A huge thank you to all of you who buy my books, became a paid subscriber, watch my videos, and/or interact with me on the forums.
Your support keeps this site going! 😇
:: Copyright KIRUPA 2025 //--