Search code examples
javascripthtmlcanvasresponsive-design

How do I fix blurry shape edges in HTML5 canvas?


I made a very simple rectangle using the canvas element. However, if the arguments for x and y in fillRect(x, y, width, height) are ANYTHING other than 0 and 0, all of the edges look completely blurry when zoomed in and/or on mobile devices. If x and y ARE 0 and 0, the top and left edges of the rectangle are super defined, even if zoomed in, while the bottom and right edges are blurry. I am rendering this on a 1920x1080 screen using Chrome/Firefox as well as a 750x1334 mobile screen using Safari.

This isn't a problem on desktop when zoomed at 100%, but on mobile devices it looks like crap. And you can clearly see the blurry edges if you zoom in fully on Chrome and Firefox as well as JSFiddle. I'm NOT adjusting width and height on the canvas using CSS. It's done using the canvas attributes and/or JS. The HTML I used to test this on browsers is below.

<!DOCTYPE html>
<html>
    <head>
         <meta charset="utf-8">
         <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    </head>

    <body>
         <canvas id="gameCanvas" width="150" height="150">A game.</canvas>

         <script>
             var canvas = document.getElementById("gameCanvas");
             var ctx = canvas.getContext("2d");

             ctx.fillRect(0, 0, 100, 100);
         </script>
    </body>
</html>

Edit: I'm NOT trying to draw a 1 pixel line. I tried experimenting with half pixel values as well but it made the blurry edges alot worse.

The first two screenshots are from an iPhone 7 screen on Safari, non-zoomed and zoomed, respectively. The last screenshot is on a 1920x1080 laptop screen, zoomed in on Chrome.

enter image description here

enter image description here

test


Solution

  • I figured out what was wrong. It was the device-pixel-ratio property of the device. Anything other than a value of 1 would result in pixelated canvas content. Adjusting the zoom in a browser alters device-pixel-ratio, and some devices come with a high device-pixel-ratio such as retina display iPhones.

    You have to account for this using Javascript. There is no other way. I wrote about this in more detail on my blog, and provide some other sources as well.

    You can see the final result below.

    Responsive canvas using vanilla JavaScript:

    var aWrapper = document.getElementById("aWrapper");
    var canvas = document.getElementById("myCanvas");
    
    //Accesses the 2D rendering context for our canvasdfdf
    var ctx = canvas.getContext("2d");
    
    function setCanvasScalingFactor() {
       return window.devicePixelRatio || 1;
    }
    
    function resizeCanvas() {
        //Gets the devicePixelRatio
        var pixelRatio = setCanvasScalingFactor();
    
        //The viewport is in portrait mode, so var width should be based off viewport WIDTH
        if (window.innerHeight > window.innerWidth) {
            //Makes the canvas 100% of the viewport width
            var width = Math.round(1.0 * window.innerWidth);
        }
      //The viewport is in landscape mode, so var width should be based off viewport HEIGHT
        else {
            //Makes the canvas 100% of the viewport height
            var width = Math.round(1.0 * window.innerHeight);
        }
    
        //This is done in order to maintain the 1:1 aspect ratio, adjust as needed
        var height = width;
    
        //This will be used to downscale the canvas element when devicePixelRatio > 1
        aWrapper.style.width = width + "px";
        aWrapper.style.height = height + "px";
    
        canvas.width = width * pixelRatio;
        canvas.height = height * pixelRatio;
    }
    
    var cascadeFactor = 255;
    var cascadeCoefficient = 1;
    
    function draw() {
      //The number of color block columns and rows
      var columns = 5;
      var rows = 5;
      //The length of each square
      var length = Math.round(canvas.width/columns) - 2;
      
      //Increments or decrements cascadeFactor by 1, based on cascadeCoefficient
      cascadeFactor += cascadeCoefficient;
    
      //Makes sure the canvas is clean at the beginning of a frame
      ctx.clearRect(0, 0, canvas.width, canvas.height);
    
      for (var i = columns; i >= 1; i--) {  
        for (var j = rows; j >= 1; j--) {
          //Where the color magic happens
          ctx.fillStyle = "rgba(" + (j*i*(cascadeFactor-110)) + "," + (i*cascadeFactor) + "," + (j*cascadeFactor) + "," + 0.6 + ")";
            
          ctx.fillRect((length*(i-1)) + ((i-1)*2), (length*(j-1)) + ((j-1)*2), length, length);
        }
      }
      
      if (cascadeFactor > 255 || cascadeFactor < 0) {
        //Resets the color cascade
        cascadeCoefficient = -cascadeCoefficient;
      }
      //Continuously calls draw() again until cancelled
      var aRequest = window.requestAnimationFrame(draw);
    }
    
    window.addEventListener("resize", resizeCanvas, false);
    
    resizeCanvas();
    draw();
    #aWrapper {
        /*Horizontally centers the canvas*/
        margin: 0 auto;
    }
    
    #myCanvas {
        /*This eliminates inconsistent rendering across browsers, canvas is supposed to be a block-level element across all browsers anyway*/
        display: block;
    
        /*myCanvas will inherit its CSS width and style property values from aWrapper*/
        width: 100%;
        height: 100%;
    }
    asdfasdf
    <div id="aWrapper">
        <!--Include some fallback content on the 0.00001% chance your user's browser doesn't support canvas -->
        <canvas id="myCanvas">Fallback content</canvas>
    </div>