Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How draw in high resolution to canvas on Chrome? And why if devicePixelRatio === webkitBackingStorePixelRatio does scaling to 2x improve resolution?

I am trying to draw a 300dpi image to a canvas object but in Chrome it shows in very poor quality. When I used the below code, it did not improve but that was because devicePixelRatio was the same as backingStoreRatio (both were 1).

I then tried to force some ratio changes and found the following:

  • if I changed ratio to be 2 and forced the scaling code to run, then it draws to the canvas in a better resolution.
  • If I changed ratio to anything greater than 2 (e.g. 3, 4, 5, 6, etc) then it had poor resolution!

This was all done on a desktop computer.

How can I ensure the canvas draws with a high resolution?

(Code from: http://www.html5rocks.com/en/tutorials/canvas/hidpi/ )

/**
* Writes an image into a canvas taking into
* account the backing store pixel ratio and
* the device pixel ratio.
*
* @author Paul Lewis
* @param {Object} opts The params for drawing an image to the canvas
*/
function drawImage(opts) {

    if(!opts.canvas) {
        throw("A canvas is required");
    }
    if(!opts.image) {
        throw("Image is required");
    }

    // get the canvas and context
    var canvas = opts.canvas,
    context = canvas.getContext('2d'),
    image = opts.image,

    // now default all the dimension info
    srcx = opts.srcx || 0,
    srcy = opts.srcy || 0,
    srcw = opts.srcw || image.naturalWidth,
    srch = opts.srch || image.naturalHeight,
    desx = opts.desx || srcx,
    desy = opts.desy || srcy,
    desw = opts.desw || srcw,
    desh = opts.desh || srch,
    auto = opts.auto,

    // finally query the various pixel ratios
    devicePixelRatio = window.devicePixelRatio || 1,
    backingStoreRatio = context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio ||
    context.backingStorePixelRatio || 1,    
    ratio = devicePixelRatio / backingStoreRatio;

    // ensure we have a value set for auto.
    // If auto is set to false then we
    // will simply not upscale the canvas
    // and the default behaviour will be maintained
    if (typeof auto === 'undefined') {
        auto = true;
    }

    // upscale the canvas if the two ratios don't match
    if (auto && devicePixelRatio !== backingStoreRatio) {

        var oldWidth = canvas.width;
        var oldHeight = canvas.height;

        canvas.width = oldWidth * ratio;
        canvas.height = oldHeight * ratio;

        canvas.style.width = oldWidth + 'px';
        canvas.style.height = oldHeight + 'px';

        // now scale the context to counter
        // the fact that we've manually scaled
        // our canvas element
        context.scale(ratio, ratio);

    }

    context.drawImage(pic, srcx, srcy, srcw, srch, desx, desy, desw, desh);
}

Making only the below changes results in high resolution canvas images (why?):

    //WE FORCE RATIO TO BE 2
    ratio = 2;

    //WE FORCE IT TO UPSCALE (event though they're equal)
    if (auto && devicePixelRatio === backingStoreRatio) {

If we change the above to be a ratio of 3, it is no longer high resolution!

EDIT: One additional observation - even with the 2x ratio, while it is noticeably better resolution, it's still not as sharp as showing the image in an img tag)

like image 845
Don Rhummy Avatar asked Oct 02 '13 17:10

Don Rhummy


2 Answers

The HTML5Rocks article linked from the question makes things more difficult than they need to be, but it makes the same basic mistake as other resources I have seen (1, 2, 3, 4). Those references give some variation on this formula:

var rect = canvas.getBoundingClientRect();
canvas.width = Math.round (devicePixelRatio * rect.width); // WRONG!

The formula is wrong. A better formula is

var rect = canvas.getBoundingClientRect();
canvas.width = Math.round (devicePixelRatio * rect.right)
             - Math.round (devicePixelRatio * rect.left);

The point is, it doesn't make sense to scale a width or height (i.e., the difference of two positions) by devicePixelRatio. You should only ever scale an absolute position. I can't find a reference for this exact point but I think it is obvious, once you get it.

Proposition.

There is no way to calculate the physical width and height of a rectangle (in device pixels) from its CSS width and height (in device-independent pixels).

Proof.

Suppose you have two elements whose bounding rectangles in device-independent pixels are

{ left:   0, top:  10, right:   8, bottom:  20, width:   8, height:  10 },
{ left:   1, top:  20, right:   9, bottom:  30, width:   8, height:  10 }.

Now assuming devicePixelRatio is 1.4 the elements will cover these device-pixel rectangles:

{ left:   0, top:  14, right:  11, bottom:  28, width:  11, height:  14 },
{ left:   1, top:  28, right:  13, bottom:  42, width:  12, height:  14 },

where left, top, right and bottom have been multiplied by devicePixelRatio and rounded to the nearest integer (using Math.round()).

You will notice that the two rectangles have the same width in device-independent pixels but different widths in device pixels. ▯

Testing.

Here is a code sample for testing. Load it in a browser, then zoom in and out with the mouse. The last canvas should always have sharp lines. The other three will be blurry at some resolutions.

Tested on desktop Firefox, IE, Edge, Chrome and Android Chrome and Firefox. (Note, this doesn't work on JSfiddle because getBoundingClientRect returns incorrect values there.)

<!DOCTYPE html>
<html>
  <head>
    <script>
      function resize() {
        var canvases = document.getElementsByTagName("canvas");
        var i, j;
        for (i = 0; i != canvases.length; ++ i) {
          var canvas = canvases[i];
          var method = canvas.getAttribute("method");
          var dipRect = canvas.getBoundingClientRect();
          var context = canvas.getContext("2d");
          switch (method) {
            case "0":
              // Incorrect:
              canvas.width = devicePixelRatio * dipRect.width;
              canvas.height = devicePixelRatio * dipRect.height;
              break;

            case "1":
              // Incorrect:
              canvas.width = Math.round(devicePixelRatio * dipRect.width);
              canvas.height = Math.round(devicePixelRatio * dipRect.height);
              break;

            case "2":
              // Incorrect:
              canvas.width = Math.floor(devicePixelRatio * dipRect.width);
              canvas.height = Math.floor(devicePixelRatio * dipRect.height);
              break;

            case "3":
              // Correct:
              canvas.width = Math.round(devicePixelRatio * dipRect.right)
                - Math.round(devicePixelRatio * dipRect.left);
              canvas.height = Math.round(devicePixelRatio * dipRect.bottom)
                - Math.round(devicePixelRatio * dipRect.top);
              break;
          }
          console.log("method " + method
            + ", devicePixelRatio " + devicePixelRatio
            + ", client rect (DI px) (" + dipRect.left + ", " + dipRect.top + ")"
            + ", " + dipRect.width + " x " + dipRect.height
            + ", canvas width, height (logical px) " + canvas.width + ", " + canvas.height);

          context.clearRect(0, 0, canvas.width, canvas.height);
          context.fillStyle = "cyan";
          context.fillRect(0, 0, canvas.width, canvas.height);
          context.fillStyle = "black";
          for (j = 0; j != Math.floor (canvas.width / 2); ++ j) {
            context.fillRect(2 * j, 0, 1, canvas.height);
          }
        }
      };
      addEventListener("DOMContentLoaded", resize);
      addEventListener("resize", resize);
    </script>
  </head>
  <body>
    <canvas method="0" style="position: absolute; left: 1px; top: 10px; width: 8px; height: 10px"></canvas>
    <canvas method="1" style="position: absolute; left: 1px; top: 25px; width: 8px; height: 10px"></canvas>
    <canvas method="2" style="position: absolute; left: 1px; top: 40px; width: 8px; height: 10px"></canvas>
    <canvas method="3" style="position: absolute; left: 1px; top: 55px; width: 8px; height: 10px"></canvas>
  </body>
</html>
like image 72
Buster Avatar answered Oct 21 '22 14:10

Buster


One trick you could use is to actually set the canvas width and height to the pixel height and width of the photo then use css to resize the image. Below is a sudo-code example.

canvasElement.width = imgWidth;
canvasElement.height = imgHeight;
canvasElement.getContext("2d").drawImage(ARGS);

Then you can either use width & height setting or a 3d transform.

canvasElement.style.transform = "scale3d(0.5,0.5,0)";

or

canvasElement.style.width = newWidth;
canvasElement.style.height = newWidth * imgHeight / imgWidth;

I suggest you use the 3d transform because when you use 3d transforms the image data of the element gets copied to the GPU so you don't really have to worry about any quality degradation.

I hope this helps!

like image 24
hyphnKnight Avatar answered Oct 21 '22 13:10

hyphnKnight