理解与解决高清屏中使用Canvas绘图出现模糊的问题

介绍

高清屏出现以前,屏幕的一个设备物理像素就是css所定义的一个逻辑像素(估计那个时候还没有设备物理像素和逻辑像素的概念)。例如下面代码就是以此为基础,实现在浏览器中展示一个600x386的Canvas。为了比较,我在Canvas中绘制了一个半径为20px的圆。

<html>  
  <head>
    <title>Canvas demo</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      html, body {
        width: 100%;
      }
      #canvas {
        display: block;
        border: 1px solid red;
        margin: 10px auto 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas" width="600" height="386">Canvas is not supported</canvas>
    <script>
      var canvas = document.getElementById('canvas');
      var ctx = canvas.getContext('2d');
      ctx.beginPath();
      ctx.arc(300, 150, 20, 0, Math.PI*2);
      ctx.fillStyle = "#0095DD";
      ctx.fill();
      ctx.closePath();
    </script>
  </body>
</html>  

但是这段代码如果在现今(9012年)绝大部分电脑的浏览器中打开,你会看到如下图所示的情况。

模糊的圆

我所绘制的圆边缘看起来有些模糊了。为什么会出现这个情况,简单的讲一下:这是因为在Canvas绘制圆时默认是基于设备物理像素进行的,代码里指定圆的半径20px对应20个物理像素;而运行该代码的电脑屏幕属于高清屏(MacBook Pro Chrome浏览器默认设置),要求输入更多像素点以便它能够展示更清晰的内容。由于输入的20个物理像素不符合“更多像素点”的需求,因此显示出来的结果就是模糊的了。

实际上更准确的Canvas绘制流程如下图所示。

Canvas绘制过程

其中有两个关键参数影响了绘制结果:

  • backingStoreRatio 浏览器在Canvas绘制内容到缓存时的比例(这是一个非标准的参数,只有部分浏览器实现了它)。例如代码指定圆的半径20px,如果backingStoreRatio为2,则缓存区中的圆的半径为40px。
  • devicePixelRatio 设备物理像素与逻辑像素的比率。同样大小的屏幕下若该值越大表示能显示的内容细节越多,看起来就越清晰。Windows系统下可以在显示设置中改变这个值。

再回头来看原来的问题。我想绘制一个半径为20px的圆,由于backingStoreRatio为1,所以缓存区也是绘制了一个半径为20px的圆。接着从缓存区向屏幕渲染,由于devicePixelRatio为2,即要求一个半径为40px的圆,缓存区的明显较小导致需要被拉伸显示,从而出现模糊。

解决模糊的方法

将 Canvas 宽高进行放大

放大比例为:devicePixelRatio / webkitBackingStorePixelRatio。方法如下:

var devicePixelRatio = window.devicePixelRatio || 1;  
var backingStoreRatio = context.webkitBackingStorePixelRatio ||  
context.mozBackingStorePixelRatio ||  
context.msBackingStorePixelRatio ||  
context.oBackingStorePixelRatio ||  
context.backingStorePixelRatio || 1;  
var ratio = devicePixelRatio / backingStoreRatio;

// Up scale canvas size
var rect = this.canvas.getBoundingClientRect();  
canvas.width = rect.width * ratio;  
canvas.height = rect.height * ratio;  
通过 CSS 将 Canvas 在页面上显示尺寸缩小为原大小
var rect = this.canvas.getBoundingClientRect();  
// Down scale dom size
canvas.style.width = `${rect.width}px`;  
canvas.style.height = `${rect.height}px`;  
通过CanvasRenderingContext2D.scale()设置绘制时的像素转换
this.ctx.scale(ratio, ratio);  

结果

接下来用上面的方法重新修改代码:

<html>  
  <head>
    <title>Canvas demo</title>
    <style>
      * {
        padding: 0;
        margin: 0;
      }
      html, body {
        width: 100%;
      }
      #canvas {
        display: block;
        border: 1px solid red;
        margin: 10px auto 0;
      }
    </style>
  </head>
  <body>
    <canvas id="canvas" width="600" height="386">Canvas is not supported</canvas>
    <script>
      var canvas = document.getElementById('canvas');
      var ctx = canvas.getContext('2d');

      // Get device pixel ratio
      var devicePixelRatio = window.devicePixelRatio || 1;
      // Get canvas backing store ratio
      var backingStoreRatio = (
        ctx.webkitBackingStorePixelRatio ||
        ctx.mozBackingStorePixelRatio ||
        ctx.msBackingStorePixelRatio ||
        ctx.oBackingStorePixelRatio ||
        ctx.backingStorePixelRatio ||
        1
      );
      // Calculate the scale ratio
      var ratio = devicePixelRatio || backingStoreRatio;
      var rect = canvas.getBoundingClientRect();
      // Up scale canvas size
      canvas.width = rect.width * ratio;
      canvas.height = rect.height * ratio;
      // Down scale dom size
      canvas.style.width = `${rect.width}px`;
      canvas.style.height = `${rect.height}px`;
      // Set scale ratio for drawing
      ctx.scale(ratio, ratio);

      ctx.beginPath();
      ctx.arc(300, 150, 20, 0, Math.PI*2);
      ctx.fillStyle = "#0095DD";
      ctx.fill();
      ctx.closePath();
    </script>
  </body>
</html>  

运行结果如下:

清晰的圆

P.S. 因为博客文章的样式会对上传图片进行拉伸导致效果不明显,这就不在我的考虑范围内了。

ChardLau

继续阅读此作者的更多文章