9

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

Pearu
  • 195
  • 2
  • 10
  • Possible duplicate of [Canvas drawings, like lines, are blurry](http://stackoverflow.com/questions/8696631/canvas-drawings-like-lines-are-blurry) – Heretic Monkey Mar 03 '17 at 21:13
  • 2
    I tried experimenting with half pixel values, but it just made them even worse. And I'm not drawing a 1 pixel line, I'm drawing rectangles. – Pearu Mar 03 '17 at 21:14
  • http://stackoverflow.com/q/28057881/215552 Same fix. – Heretic Monkey Mar 03 '17 at 21:19
  • No. Not the same fix. I tried it, word for word, and while the adjusted DOES look a lot better, it still has slightly blurry edges. The same as mine do right now. – Pearu Mar 03 '17 at 21:30
  • You tried all of the answers? That's unbelievably fast coding! – Heretic Monkey Mar 03 '17 at 21:30
  • Your link has one answer. Maybe read it next time? – Pearu Mar 03 '17 at 21:32
  • does [this](https://jsfiddle.net/siam/zyydjL64/1/) look better? – m87 Mar 03 '17 at 21:39
  • No, but thank you for at least trying to answer my question. I zoom in all the way on both Chrome/Firefox and the right and bottom edges are still slightly blurry. – Pearu Mar 03 '17 at 21:47
  • The phone you're trying with has retina display? https://jsfiddle.net/zyydjL64/2/ –  Mar 03 '17 at 22:16
  • @K3N Yes, my mobile device has a retina display. And the bottom/right edges of the rectangle in your fiddle look a lot sharper when scaled up, but they still look slightly blurry when I test it directly on Chrome/Firefox on my laptop screen. – Pearu Mar 03 '17 at 22:29
  • @Pearu you are probably just seeing the interpolated lines (ie. antialiasing) from the upscaling. Not much we can do about that as canvas is a bitmap and not vector. –  Mar 03 '17 at 22:44
  • @K3N I added some screenshots (the first two are on mobile, non-zoomed and zoomed, respectively, and the last one is on a laptop screen, zoomed) for clarity from my side. Is there absolutely nothing that can be done, then? – Pearu Mar 04 '17 at 00:02
  • Some solutions http://stackoverflow.com/a/41776757/3877726, http://stackoverflow.com/a/39951701/3877726, http://stackoverflow.com/a/40916519/3877726 and when you zoom in on any browser you will have blurry edges. This is done on purpose to improve the quality of images. To stop bilinear filtering use CSS rule `image-rendering: pixelated;` – Blindman67 Mar 04 '17 at 07:58

2 Answers2

8

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> 
Pearu
  • 195
  • 2
  • 10
  • I have very small fonts (that need to be small as they are on small objects) that are illegible because of this blur factor when zoomed in. The link to the blog seems to be down. The code seems to involve resizing the canvas. Can't a canvas be set to not scale with the screen, rather show pixel perfect image quality and crop as needed? – user3015682 Jul 05 '20 at 06:48
  • hi the link to the blog isn't working can you please update it – mikasa Feb 23 '23 at 04:59
5

There's also a css-way to do this using the image-rendering property.

canvas {
  /* all four are needed to support the most browsers */
  image-rendering: -moz-crisp-edges;
  image-rendering: -webkit-crisp-edges;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
/* optional: can also be applied to `img` tags */

This is ideal for scaling up pixel-art for example. It preserves the edges and does not blur them when scaling up or zooming in.

For this to work as expected, it's important that your images and shapes are drawn on whole pixels, don't do this: context.rect(1.5, 1.5, 2, 2). While it won't blur, it will draw the half-pixels in different colors. Round to the nearest integer if you calculate positions dynamically.

The vendor-prefix and the repeated property is needed to support all major browsers at this time. Chrome/Webkit only supports pixelated, Firefox only crisp-edges. (mid 2021)

caniuse browser support

MDN tutorial for pixel-art

swift-lynx
  • 3,219
  • 3
  • 26
  • 45