Skip to content

Canvas绘制图片模糊问题?

在日常业务中,总能遇到使用 canvas 绘制图片的场景,今天就聊一聊 canvas 绘制图片且出现模糊不清的问题?

案例代码

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>canvas</title>
    <style>
      .container {
        display: flex;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div>
        <p>原图:</p>
        <img
          style="width: 300px"
          src="https://c-ssl.duitang.com/uploads/blog/202107/13/20210713121231_46508.jpeg"
          alt=""
        />
      </div>
      <div>
        <p>未优化:</p>
        <canvas id="canvas1" width="300"></canvas>
      </div>
      <div>
        <p>优化后:</p>
        <canvas id="canvas2" width="300"></canvas>
      </div>
    </div>

    <script>
      function loadImage() {
        return new Promise((resolve, reject) => {
          const img = new Image()
          img.src =
            'https://c-ssl.duitang.com/uploads/blog/202107/13/20210713121231_46508.jpeg'
          img.onload = () => {
            resolve(img)
          }

          img.onerror = (e) => {
            reject(e)
          }
        })
      }

      function drawImage(canvas, canvasCtx, img) {
        const imgScale = img.width / img.height
        canvas.style.width = canvas.width + 'px'
        canvas.style.height = canvas.width / imgScale + 'px'
        canvas.width = canvas.width
        canvas.height = canvas.width / imgScale
        canvasCtx.drawImage(
          img,
          0,
          0,
          img.width,
          img.height,
          0,
          0,
          canvas.width,
          canvas.height,
        )
      }

      async function render() {
        const canvas1 = document.getElementById('canvas1')
        const ctx1 = canvas1.getContext('2d')
        const img = await loadImage()

        drawImage(canvas1, ctx1, img)
      }

      render()
    </script>
  </body>
</html>

效果图如👇🏻

(图片是网上百度的,如果图片出现跨域问题,可以通过添加属性 img.setAttribute('crossOrigin', 'anonymous') 来解决)canvas 未优化效果图

对比原图,没有优化的 canvas 绘制确实有明显清晰度下降。

🤔为什么会出现这种情况呢

canvas 渲染出的东西本质是一张位图。高dpi显示设备意味着每平方英寸有更多的像素,如在 Retina 屏幕上相当于使用两个像素点来渲染一个像素,相当于图片被放大了一倍,因此绘制出来的东西会变得模糊。

在解决前,先了解一下像素问题,已了解可跳过

像素问题解析

(有错误可邮件我!!!)

  1. 物理像素表示屏幕上有多少个发光点(颗粒),逻辑像素(也叫设备独立像素)表示屏幕展示物体的视觉尺寸是多大。
  2. 为什么需要逻辑像素?因为物理像素仅仅表示像素的个数,并没有规定实际的尺寸,如果说一个矩形要用10个物理像素来渲染,那么不同的(物理)像素密度和排列方式展示出来的效果也就不一样(像素密度越大图形越小);因此为了保持各设备展示一样产生了逻辑像素。

比如:某屏幕的物理像素是 375x667,那么表示该屏幕每行有375个颗粒和每列有667个颗粒组成;假设这个屏幕的逻辑像素也是 375x667,那么1个逻辑像素等于1个物理像素;假设屏幕的物理像素是 750x1334,逻辑像素依旧是 375x667,这时1个逻辑像素等于2个物理像素。这也就是屏幕的 dpr 概念,dpr = 物理像素 / 设备独立像素。

在前端有两个很热门的问题

  1. 为什么手机的设计稿大多数是2倍图
  2. 为什么设置 1px 手机看起来比设计稿粗

第一个问题:因为现在大多数手机的 dpr 都是为2,使用2倍图会在设备中显示更加精细,就好比使用一行100个物理像素的屏幕和一行50个物理像素的屏幕,同时渲染黑色到白色的渐变,明显100个物理像素的屏幕会过渡得更加自然。(记住一个物理像素只能像素一种颜色,不能说一半黑一半灰)

第二个问题:首先提一个要点,在屏幕没有缩放的情况下,1css像素等于1个逻辑像素,也就是说等于2个物理像素;(场景设计稿提供的是2倍图750x1334,设备逻辑像素375x667),设计稿中的1px相对等于1个逻辑像素,如果缩放到设备中显示,相当于每一条边进行等比缩放,所以实际需要设备中的0.5个逻辑像素显示,但你已经使用 1px(css像素),那么当然会比设计稿看起来粗。 其实这个问题个人觉得使用dpr来解析并不惬当,只是一个简单的缩放问题而已,即拿一个750px的图在375px的设备上显示,整体的设计稿缩小了,但写1px时你没去转换导致的。

解决1px问题,网上有很多,这里就不重复说了。

不少人第一时间应该会想到使用0.5px解决,确实在ios可以解决,但在android中兼容不太好。

解决思路

canvas 存在一个 backingStorePixelRatio 属性,不过现在已经废弃。转而使用在浏览器 window 对象下的 devicePixelRatio 属性,它表示浏览器的设备像素比,也就是 dpr。

举例:使用一张 100x100 像素大小的图片,去Retina 屏幕上实际会占据 200×200 像素的空间,相当于图片被放大了一倍,因此图片会变得模糊。 (图片并没有 dpr 概念,图片的参数 100x100像素,即表示 100x100像素尺寸,也表示 100x 100个像素点;Retina 屏幕 的dpr 是2,显示 100x100 像素尺寸(逻辑像素),因此需要使用 2个物理像素点去渲染 1个 像素点)

而在 canvas 中的 style.widthstyle.height 是设置画布的实际渲染大小;canvas.widthcanvas.height 设置的是画布的大小。

当两者都设定相同数值时,在 dpr 为 1 的屏幕下是没有问题的,而在 dpr 大于 1 的屏幕下,就出现 canvas 被放大的问题;或许你把 canvas.width 比作图片像素,canvas style 比作设备像素 就比较好理解了。

所以我们可以通过 devicePixelRatio 属性来动态设置 canvas 画布大小,来实现 dpr 为 1 的时候,使用 1 倍图,dpr 为 2 时使用 2 倍图,以此类推。

🗒️ 完整代码展示

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>canvas</title>
    <style>
      .container {
        display: flex;
      }
    </style>
  </head>
  <body>
    <div class="container">
      <div>
        <p>原图:</p>
        <img
          style="width: 300px"
          src="https://c-ssl.duitang.com/uploads/blog/202107/13/20210713121231_46508.jpeg"
          alt=""
        />
      </div>
      <div>
        <p>未优化:</p>
        <canvas id="canvas1" width="300"></canvas>
      </div>
      <div>
        <p>优化后:</p>
        <canvas id="canvas2" width="300"></canvas>
      </div>
    </div>

    <script>
      function loadImage() {
        return new Promise((resolve, reject) => {
          const img = new Image()
          img.src =
            'https://c-ssl.duitang.com/uploads/blog/202107/13/20210713121231_46508.jpeg'
          img.onload = () => {
            resolve(img)
          }

          img.onerror = (e) => {
            reject(e)
          }
        })
      }

      function drawImage(canvas, canvasCtx, img, ratio = 1) {
        const imgScale = img.width / img.height
        canvasCtx.scale(ratio, ratio)
        canvas.style.width = canvas.width + 'px'
        canvas.style.height = canvas.width / imgScale + 'px'
        canvas.width = canvas.width * ratio
        canvas.height = canvas.width / imgScale
        canvasCtx.drawImage(
          img,
          0,
          0,
          img.width,
          img.height,
          0,
          0,
          canvas.width,
          canvas.height,
        )
      }

      async function render() {
        const pxRatio = window.devicePixelRatio || 1

        const canvas1 = document.getElementById('canvas1')
        const ctx1 = canvas1.getContext('2d')
        const canvas2 = document.getElementById('canvas2')
        const ctx2 = canvas2.getContext('2d')
        const img = await loadImage()

        drawImage(canvas1, ctx1, img, 1)
        drawImage(canvas2, ctx2, img, pxRatio)
      }

      render()
    </script>
  </body>
</html>

🥳🥳🥳 效果图canvas 优化后效果图

Released under the MIT License.