Search code examples
javascriptcssreactjshtml5-canvaspixi.js

HTML Canvas does not render straight lines properly


I'm trying to create a grid with a canvas library. I'm using React PixiJS library but I am sure this is not related with PixiJS or React. Because same thing happens when I use standard HTML canvas and pure javascript.

I'm trying to draw a 100x100 grid and I'm doing that with 200 straight lines.

My code to draw 100x100 grid as follows:

    const lineWidth = 0.5;
    const lineColor = 0x000000;
    g.lineStyle(lineWidth, lineColor);

    // Draw horizontal lines
    for (var x = 0; x <= 1000; x += 10) {
        g.moveTo(x, 0);
        g.lineTo(x, 1000); 
    }

    // Draw vertical lines
    for (var y = 0; 1000; y += 10) {
        g.moveTo(0, y);
        g.lineTo(1000, y); // OK
    }

In higher resolution devices it shows grid perfectly as it should but in lower resolutions or sometimes in higher resolutions aswell, it skips some lines. It actually draws it because when I zoom in I can see they are rendered but initially I can't see it.

Below are some examples to explain what does "skipping lines" mean:

example1 example2


Solution

  • To get the best results rendering to the canvas ensure that the canvas resolution matches the canvas size.

    That is the canvas.width and height should match the canvas.style.width and canvas.style.height

    You also need to align the render calls to the pixels. The 2D renderer will anti alias content which can create artifacts if you do not align horizontal and vertical lines to pixels.

    Lastly ensure that the grid spacing in a whole number.

    Display size not matching canvas resolution

    This shows what happens when you do not match the canvas display size to the canvas resolution and do not align the rendering to pixels.

    const ctx = canvas.getContext("2d");
    const styles = {
       thinLine: { lineWidth: 0.25, strokeStyle: "#000" },
      pxLine: { lineWidth: 1, strokeStyle: "#000" },
    };    
    function line(x1,y1,x2,y2) {
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
    }
    function ctxStyle(style) { Object.assign(ctx, style); }
    function drawGrid(spacing) {
        var p = 0;
        const W = canvas.width, H = canvas.height, size = Math.max(W, H);
        ctx.beginPath();
        while (p < size) {
            p <= W && line(p, 0, p, H);
            p <= H && line(0, p, W, p);
            p += spacing;
        }
        ctx.stroke();
    }
    ctxStyle(styles.thinLine);
    drawGrid(1000 / 30);
    ctxStyle(styles.pxLine);
    drawGrid(1000 / 5);
    canvas {
        width: 333px;
        height: 333px;
    }
    <canvas id="canvas" width="1000" height="1000"></camvas>

    Lines not aligned to pixels.

    This example sets the canvas resolution to match the display size which improves the display. However the lines are still inconsistent as there is no effort to align the lines with pixels.

    const ctx = canvas.getContext("2d");
    const styles = {
      thinLine: { lineWidth: 0.25, strokeStyle: "#000" },
      pxLine: { lineWidth: 1, strokeStyle: "#000" },
    };    
    function line(x1,y1,x2,y2) {
        ctx.moveTo(x1, y1);
        ctx.lineTo(x2, y2);
    }
    function ctxStyle(style) { Object.assign(ctx, style); }
    function drawGrid(spacing) {
        var p = 0;
        const W = canvas.width, H = canvas.height, size = Math.max(W, H);
        ctx.beginPath();
        while (p < size) {
            p <= W && line(p, 0, p, H);
            p <= H && line(0, p, W, p);
            p += spacing;
        }
        ctx.stroke();
    }
    ctxStyle(styles.thinLine);
    drawGrid(333 / 30);
    ctxStyle(styles.pxLine);
    drawGrid(333 / 5);
    canvas {
        width: 333px;
        height: 333px;
    }
    <canvas id="canvas" width="333" height="333"></canvas>

    Grid spacing not a whole number

    This example sets the canvas resolution to match the display size and align the lines to pixels.

    Now the lines are consistent, however close inspection will show that not all grid lines are the same distance apart.

    const ctx = canvas.getContext("2d");
    const styles = {
      thinLine: { lineWidth: 0.25, strokeStyle: "#000" },
      pxLine: { lineWidth: 1, strokeStyle: "#000" },
    };    
    function line(x1,y1,x2,y2) {
        ctx.moveTo((x1 | 0) + 0.5, (y1 | 0) + 0.5);
        ctx.lineTo((x2 | 0) + 0.5, (y2 | 0) + 0.5);
    }
    function ctxStyle(style) { Object.assign(ctx, style); }
    function drawGrid(spacing) {
        var p = 0;
        const W = canvas.width, H = canvas.height, size = Math.max(W, H);
        ctx.beginPath();
        while (p < size) {
            p <= W && line(p, 0, p, H);
            p <= H && line(0, p, W, p);
            p += spacing;
        }
        ctx.stroke();
    }
    ctxStyle(styles.thinLine);
    drawGrid(333 / 30);
    ctxStyle(styles.pxLine);
    drawGrid(333 / 5);
    canvas {
        width: 333px;
        height: 333px;
    }
    <canvas id="canvas" width="333" height="333"></canvas>

    Best result

    For the best result ensure that the canvas resolution is divisible by the grid spacing (a whole number (integer)). Ie dividing canvas.width by grid spacing should give you a whole number. The above example had a canvas 333 by 333 and 30 grid lines. The spacing was 11.1px. The example below is 330 by 330 with 30 grid lines, the spacing is 11px

    const ctx = canvas.getContext("2d");
    const styles = {
      thinLine: { lineWidth: 0.25, strokeStyle: "#000" },
      pxLine: { lineWidth: 1, strokeStyle: "#000" },
    };    
    function line(x1,y1,x2,y2) {
        ctx.moveTo((x1 | 0) + 0.5, (y1 | 0) + 0.5);
        ctx.lineTo((x2 | 0) + 0.5, (y2 | 0) + 0.5);
    }
    function ctxStyle(style) { Object.assign(ctx, style); }
    function drawGrid(spacing) {
        var p = 0;
        const W = canvas.width, H = canvas.height, size = Math.max(W, H);
        ctx.beginPath();
        while (p < size) {
            p <= W && line(p, 0, p, H);
            p <= H && line(0, p, W, p);
            p += spacing;
        }
        ctx.stroke();
    }
    ctxStyle(styles.thinLine);
    drawGrid(333 / 30);
    ctxStyle(styles.pxLine);
    drawGrid(333 / 5);
    canvas {
        width: 330px;
        height: 330px;
    }
    <canvas id="canvas" width="330" height="330"></canvas>