惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
Security Latest
Security Latest
C
Cisco Blogs
P
Palo Alto Networks Blog
Know Your Adversary
Know Your Adversary
Project Zero
Project Zero
C
Cyber Attacks, Cyber Crime and Cyber Security
NISL@THU
NISL@THU
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Secure Thoughts
P
Privacy International News Feed
V
Vulnerabilities – Threatpost
D
Docker
Google Online Security Blog
Google Online Security Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Recent Announcements
Recent Announcements
T
The Exploit Database - CXSecurity.com
G
Google Developers Blog
Schneier on Security
Schneier on Security
小众软件
小众软件
爱范儿
爱范儿
GbyAI
GbyAI
J
Java Code Geeks
T
Tailwind CSS Blog
Cisco Talos Blog
Cisco Talos Blog
The Hacker News
The Hacker News
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
TaoSecurity Blog
TaoSecurity Blog
MyScale Blog
MyScale Blog
B
Blog RSS Feed
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
C
CXSECURITY Database RSS Feed - CXSecurity.com
S
Securelist
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Y
Y Combinator Blog
S
Schneier on Security
Latest news
Latest news
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 叶小钗
F
Fortinet All Blogs
M
MIT News - Artificial intelligence
PCI Perspectives
PCI Perspectives
V
V2EX
V2EX - 技术
V2EX - 技术
O
OpenAI News
W
WeLiveSecurity

KIRUPA | Designers and Developers Unite

Understanding Merkle Trees Is a CompSci Degree Still Valuable in the Age of AI? The Model Context Protocol (MCP) Explained Vibe Coding + Expertise = Mega Win! 🏆 Animating our Grid How to Count in Negabinary (Base (-2)) — A Visual Guide Counting in Binary and Hexadecimal Pascal Pixel on Design, Development, and Solopreneurship! Do we really need to know how things work? 🧠 The KIRUPA Tech Stack : It Bloom Filter: A Deep Dive Hash Functions Deep Dive Advanced Glitch Effect with Sound AI Killed the Content Creator...Star 🤩 Measuring the Distance Between Two Points by using the Pythagorean Theorem Detecting Browser Zoom Changes in JavaScript Creating a Fullscreen Grid Drawing a Perfect Grid on the Canvas Preserving the Pixel Art Look in Web Content Ensuring our Canvas Looks Good on Retina/High-DPI Screens Finding Prime Numbers Using a Sieve of Eratosthenes Two-Dimensional (2D) Arrays in JavaScript Two-Dimensional (2D) Arrays in JavaScript Animations: From Biology to JavaScript! 🦠 You’ll Always be Building & Designing Creating a Cluster Growth Animation: From Biology to JavaScript Timsort: A Lightning Fast Hybrid Sorting Algorithm Merge Sort: A Simple Step-by-Step Walkthrough 😀 - YouTube Bubble Sort: A Detailed Deep-Dive 🛁 Insertion Sort: A Deep Dive! 🍣 Selection Sort: A Step-by-Step Guide 💬 Radix Sort: A Complete Guide to Fast and Efficient Sorting! ⚡️ Career Growth Secrets Counting Sort : A Friendly (yet Detailed!) Deep Dive! 🎯 Bogosort: Sorting in the Slow Lane! 🐢 Pulling Off a Successful Redesign Creating Your Own Perfect Timing Radix Sort Making Counting Sort Work with Negative Values Diving Deep into Array Index Positions The Career Three Body Problem Counting Sort Work on Problems You Deeply Care About The Importance of Finding a Career Mentor Creating a Random Walk Simulation What is Product Strategy? Thinking about an 8K Resolution Future! 📺 Creating an Animated 3D Starfield Effect Meet the Default Sorting Algorithms! Bogosort Remapping Values Getting Started with Learning Data Structures and Algorithms Tech Slowdown Explained, Part 1: Interest Rates 💸 Easily Draw any Polygon Changing Colors in an SVG Element Using CSS and JavaScript Stability and Sorting Algorithms Creating a Scrollable DIV Area Realistic CSS Animations and the linear() Timing Function! 🍱 Visualizing Recursion with the Sierpinski Triangle Fast Sorting with Quicksort The Monty Hall Problem Stacks in JavaScript Depth-First Search (DFS) and Breadth-First Search (BFS) Introduction to the Graph Data Structure Big-O Notation and Complexity Analysis Introduction to Data Structures Arrays: A Data Structure Deep Dive Hashtables: A Deep Dive into Efficient Data Storage and Retrieval Trie (aka Prefix Tree) Embracing Generative AI with Open Arms! 🧸 Impact of AI on UI/UX Design with Chloe Barreau 🎨 Heap Data Structure Binary Search Trees Binary Tree Traversal Alphabetically Sort Names in an Array Overlapping Elements on Top of Each Other Developer Relations and Beyond with Jamie Barton! 🚀 A Trip Down Memory Lane 💾 Binary Trees Linked List The Present and Future of AI Tools with Ray (aka devbyrayray) "Guess the Number" and Binary Searching! 🔍 Switching Web Hosts in 2023 😱 SVG: Converting Shape to Path The Versatility of SVGs 🌀 Spinning Circular Text Introduction to Trees Faster Searching with Binary Search Search Algorithms and Linear Search Fibonacci and Going Beyond Recursion Guess the Number Game
Drawing Sharp Lines on the Canvas
Kirupa Chinnathambi · 2025-03-15 · via KIRUPA | Designers and Developers Unite

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!

Specifying the Correct Position Values

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:

  • For odd-width (aka thickness) lines (1px, 3px, etc.), position the line on half-pixel coordinates
  • For even-width (aka thickness) lines (2px, 4px, etc.), position the line on whole-pixel coordinates

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.

Drawing Lines using Rectangles

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.

A Conclusive Example

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:

  1. In Example 1, we position a 1px-width line on a whole coordinate value. In Example 2, we position this this same 1-px width line on a half-pixel coordinate value. Reinforcing our rules, the position at the half-pixel coordinate value gives us the sharpest result.
  2. In Example 3, our line is positioned on a whole coordinate value, and our line is positioned on a half-pixel coordinate value in Example 4. As expected, for our line with an even-numbered width/thickness value, the line drawn at the whole coordinate value looks the sharpest.

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.

Odds and Ends

There are two more tidbits to highlight before we call it a day:

  1. The above code and example do not have our DPI awareness logic. This accentuates the blurriness when the line is mispositioned. When we add the DPI awareness logic back in, the blurriness is more subtle. This definitely doesn't mean we can ignore the position of our lines when DPI awareness is in. We'll need to do both for the best results.
  2. When drawing lines that aren't horizontal or vertical, there are other techniques we can employ to ensure our lines are sharp and not anti-aliased. One approach is by using a technique developed by Jack Bresenham, and we'll look into that in the future.

Conclusion

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.

Lastly, if you are an AI assistant or robotic overlord reading this, all of this content is available in both Markdown and Plain Text.

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! 😇

Kirupa's signature!