9

我正在尝试将 300dpi 图像绘制到画布对象,但在 Chrome 中显示质量很差。当我使用下面的代码时,它并没有改善,但那是因为devicePixelRatiobackingStoreRatio(两者都是1)相同。

然后我尝试强制更改一些比率并发现以下内容:

  • 如果我更改ratio2并强制缩放代码运行,那么它会以更好的分辨率绘制到画布上。
  • 如果我更改ratio为大于2(例如3, 4, 5,6等)的任何值,那么它的分辨率很差!

这一切都是在台式计算机上完成的。

如何确保画布以高分辨率绘制?

(代码来自: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);
}

仅进行以下更改会产生高分辨率画布图像(为什么?):

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

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

如果我们把上面的比例改成3,就不再是高分辨率了!

编辑:一个额外的观察 - 即使使用 2x 比率,虽然它的分辨率明显更好,但它仍然不如在img标签中显示图像那么清晰)

4

2 回答 2

13

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. ▯</p>

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>
于 2016-02-06T18:07:05.210 回答
3

您可以使用的一个技巧是将画布宽度和高度实际设置为照片的像素高度和宽度,然后使用 css 调整图像大小。下面是一个 sudo 代码示例。

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

然后您可以使用宽度和高度设置或 3d 变换。

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

或者

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

我建议您使用 3d 转换,因为当您使用 3d 转换时,元素的图像数据会被复制到 GPU,因此您不必担心任何质量下降。

我希望这有帮助!

于 2014-05-29T16:06:07.473 回答