Search code examples
javascripthtmlcanvas

html 5 canvas LineTo() line color issues


I am drawing five horizontal lines to an HMTL 5 2D canvas:

var canvas_ctx = my_canvas.getContext("2d");
    canvas_ctx.lineWidth = 0.5;
    canvas_ctx.strokeStyle = "black";

    {
        let line_x = 0;
        let line_length = canvas_ctx.width;
        let offset = 5;
        let numLines = 5;
        let numYincrement = 10;
        for (let i=0;i<numLines * numYincrement;i+=numYincrement) {
            //canvas_ctx.beginPath();
            canvas_ctx.moveTo(line_x,i + offset);
            canvas_ctx.lineTo(line_length,i + offset);
            canvas_ctx.stroke();
            //canvas_ctx.closePath();
        }
    }

This should, ideally result in 5 black lines. Instead, the color of the lines seems to fade with each new line (as if it's a gradient!), so that line 5 is gray. If I uncomment canvas_ctx.beginPath(); and canvas_ctx.closePath();, all lines become gray. Why is this happening??


Solution

  • Strokes do overlap from both sides of the coordinates.

    var ctx = c.getContext('2d');
    ctx.strokeStyle="red";
    // draw big
    ctx.scale(30, 30);
    ctx.beginPath();
    ctx.moveTo(5, 0);
    ctx.lineTo(5, 10);
    ctx.stroke();
    
    drawPixelGrid();
    
    
    function drawPixelGrid() {
      // simply renders where the pixel bounds are
      ctx.beginPath();
      // remove the zoom
      ctx.setTransform(1,0,0,1,0,0);
      ctx.strokeStyle = 'gray';
      ctx.lineWidth = 2; // avoid the problem we are demonstrating by using a perfect lineWidth ;-)
    
      for(let y=0; y<=300; y+=30) {
        ctx.moveTo(0, y);
        ctx.lineTo(300, y);
        for(let x=0; x<=300; x+=30) {
          ctx.moveTo(x, 0);
          ctx.lineTo(x, 300);
        }
      }
      ctx.stroke();
    }
    <canvas id="c" height=300></canvas>

    But obviously, a pixel can't be set to two colors at the same time. So browsers apply antialiasing, which will fade your pixel color to an other color, being the result of mixing the background and the foreground color. So for a black stroke over a white or transparent background, this leads to actual gray pixels being rendered. Here I'll keep using red as an example:

    var ctx = c.getContext('2d');
    ctx.strokeStyle="red";
    // first draw as on a 10*10 canvas
    ctx.beginPath();
    ctx.moveTo(5, 0);
    ctx.lineTo(5, 10);
    ctx.stroke();
    
    // zoom it
    ctx.imageSmoothingEnabled = 0;
    ctx.globalCompositeOperation = 'copy';
    ctx.drawImage(c, 0,0,9000,9000);
    
    drawPixelGrid();
    
    // this is not red...
    
    function drawPixelGrid() {
      ctx.globalCompositeOperation = 'source-over';
      ctx.beginPath();
      ctx.setTransform(1,0,0,1,0,0);
      ctx.strokeStyle = 'gray';
      ctx.lineWidth = 2;
    
      for(let y=0; y<=300; y+=30) {
        ctx.moveTo(0, y);
        ctx.lineTo(300, y);
        for(let x=0; x<=300; x+=30) {
          ctx.moveTo(x, 0);
          ctx.lineTo(x, 300);
        }
      }
      ctx.stroke();
    }
    <canvas id="c" height=300></canvas>

    One way to avoid it is generally to apply an offset on your coordinates so that the line extends correctly on pixels boundaries. E.g for a 1px lineWidth, you would apply a 0.5 offset:

    var ctx = c.getContext('2d');
    ctx.strokeStyle="red";
    // first draw as on a 10*10 canvas
    ctx.beginPath();
    ctx.moveTo(5.5, 0); // offset +0.5px
    ctx.lineTo(5.5, 10);
    ctx.stroke();
    
    // zoom it
    ctx.imageSmoothingEnabled = 0;
    ctx.globalCompositeOperation = 'copy';
    ctx.drawImage(c, 0,0,9000,9000);
    
    drawPixelGrid();
    // now we've got a real red
    
    function drawPixelGrid() {
      ctx.globalCompositeOperation = 'source-over';
      ctx.beginPath();
      ctx.setTransform(1,0,0,1,0,0);
      ctx.strokeStyle = 'gray';
      ctx.lineWidth = 2;
    
      for(let y=0; y<=300; y+=30) {
        ctx.moveTo(0, y);
        ctx.lineTo(300, y);
        for(let x=0; x<=300; x+=30) {
          ctx.moveTo(x, 0);
          ctx.lineTo(x, 300);
        }
      }
      ctx.stroke();
    }
    <canvas id="c" height=300></canvas>

    With a lineWidth of 2, you wouldn't need any offset.

    But in your case, you are drawing at 0.5px lineWidth, so no offset will be able to get rid of this antialiasing.

    So if you want perfect color, choose a correct lineWidth.