javascripthtmlhtml5-canvasrenderinglinewidth

Settings canvas linewidth > 1 gives pointy polygons


Problem

In my app I need to draw a lot of polygons fast, for this I am using the following canvas primitives

ctx.beginPath()
ctx.moveTo(points[0], points[1])
ctx.stroke()

Unfortunately when I try to style the width of each line that is being drawn, the lines extend beyond the points I moveTo. This is unexpected, I expected only the thickness of the line being changed, not their endpoints.

Example

I have distilled my problem to the following minimal case. I have created a blue bounding box that should contain the drawn triangle / polygon. This works correctly when the linewidth is set to 1, but as soon as I increase the line width, the polygon exceeds the expected bounding box.

const canvas = document.getElementById('canvas')
canvas.width = 256
canvas.height = 256

// Set a background
const ctx = canvas.getContext('2d')
ctx.fillStyle = "#EEEEEE";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

function drawGrid(ctx, spacing = 10, color = '#ccc') {
  const w = ctx.canvas.width;
  const h = ctx.canvas.height;

  ctx.strokeStyle = color;
  ctx.lineWidth = 1;

  ctx.beginPath();
  // Vertical lines
  for (let x = 0; x <= w; x += spacing) {
    ctx.moveTo(x + 0.5, 0+ 0.5);
    ctx.lineTo(x+ 0.5, h+ 0.5);
  }
  // Horizontal lines
  for (let y = 0; y <= h; y += spacing) {
    ctx.moveTo(0+ 0.5, y+ 0.5);
    ctx.lineTo(w+ 0.5, y+ 0.5);
  }
  ctx.stroke();
}

// Draw a 10x10 grid
drawGrid(ctx, 10, '#ddd');

// Draw expected bounding box with lineWidth 1, this is correct
ctx.strokeStyle = 'blue'
ctx.lineWidth = 1
ctx.strokeRect(100, 100, 12, 26)

// Draw a triangle / polygon that exceeds the bounding box when the lineWidth > 1
const line = [112, 112, 100, 100, 112, 124, 112, 112] // [x1, y1, x2, y2, etc.]

drawPolygon(line, 'red', 5) // Incorrect
// drawPolygon(line, 'orange', 2) // Incorrect
drawPolygon(line, 'green', 1) // Correct


function drawPolygon(points, color, lineWidth) {
  ctx.strokeStyle = color
  ctx.lineWidth = lineWidth

  ctx.beginPath()
  ctx.moveTo(points[0], points[1])
  for (let i = 2; i < points.length; i += 2) {
    ctx.lineTo(points[i], points[i + 1])
  }
  ctx.stroke()

}
<canvas id="canvas" style="transform: scale(2);"></canvas>

Possible cause

It looks like the lineWidth is added to the line end points. I cannot find anything on MDN that would suggest this would happen.

I would like a solution that increases the thickness of the lines, but keeps the polygon inside the bounding box. The solution must be fast as well.


Solution

  • Very much thanks to the knowledge of @C3roe there are 2 possible easy solutions.

    The cause

    Due to the default lineJoining method of the canvas miter the line extends beyond the set points.

    Solutions

    1. Setting the lineJoining method to something else, like the round option, the miter method is not used anymore and due to the different properties of this method it keeps near to the original points.

    2. Setting the miterLimit to a low value, like 1px, prevents the miter method from extending far beyond the original points.

    I like the round option best for aesthetic reasons, but the miterLimitmight be a more correct solution.

    // Source - https://stackoverflow.com/q/79838941
    // Posted by bart, modified by community. See post 'Timeline' for change history
    // Retrieved 2025-12-05, License - CC BY-SA 4.0
    
    const canvas = document.getElementById('canvas')
    canvas.width = 256
    canvas.height = 256
    
    // Set a background
    const ctx = canvas.getContext('2d')
    ctx.fillStyle = "#EEEEEE";
    ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    
    function drawGrid(ctx, spacing = 10, color = '#ccc') {
      const w = ctx.canvas.width;
      const h = ctx.canvas.height;
    
      ctx.strokeStyle = color;
      ctx.lineWidth = 1;
    
      ctx.beginPath();
      // Vertical lines
      for (let x = 0; x <= w; x += spacing) {
        ctx.moveTo(x + 0.5, 0+ 0.5);
        ctx.lineTo(x+ 0.5, h+ 0.5);
      }
      // Horizontal lines
      for (let y = 0; y <= h; y += spacing) {
        ctx.moveTo(0+ 0.5, y+ 0.5);
        ctx.lineTo(w+ 0.5, y+ 0.5);
      }
      ctx.stroke();
    }
    
    // Draw a 10x10 grid
    drawGrid(ctx, 10, '#ddd');
    
    // Draw expected bounding box with lineWidth 1, this is correct
    ctx.strokeStyle = 'blue'
    ctx.lineWidth = 1
    ctx.strokeRect(100, 100, 12, 26)
    
    // Draw a triangle / polygon that exceeds the bounding box when the lineWidth > 1
    const line = [112, 112, 100, 100, 112, 124, 112, 112] // [x1, y1, x2, y2, etc.]
    
    drawPolygon(line, 'red', 5) // Incorrect
    // drawPolygon(line, 'orange', 2) // Incorrect
    drawPolygon(line, 'green', 1) // Correct
    
    
    function drawPolygon(points, color, lineWidth) {
      ctx.strokeStyle = color
      ctx.lineWidth = lineWidth
      ctx.miterLimit = 1;
      
      ctx.beginPath()
      ctx.moveTo(points[0], points[1])
      for (let i = 2; i < points.length; i += 2) {
        ctx.lineTo(points[i], points[i + 1])
      }
      ctx.stroke()
    
    }
    <canvas id="canvas" style="transform: scale(2);"></canvas>