Canvas 绘图模糊问题解析
初次使用 canvas 来绘图的小伙伴可能遇到过绘制出来的图像模糊不清的情况
我们先一起来绘制一个矩形
<body>
<style>
canvas {background: #eee;}
</style>
<canvas id="canvas" width="200" height="200"></canvas>
<script>
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.strokeStyle = '#000'
ctx.lineWidth = 1
ctx.strokeRect(10, 10, 100, 100)
</script>
</body>
效果图如下:
我们明明设置的线条宽度是 1px,为什么会导致绘制出来的效果是 2px 甚至看上去更粗呢?
要解决这点,我们需要先搞清楚两个概念:
- 设备像素比
- canvas 的 css 宽高与上下文宽高
设备像素比
这个概念相信很多小伙伴都有所了解,举个简单的栗子,在 iPhone3G 时代,屏幕宽度是 320px,其宽度上的物理像素也是 320px;而到了 4s 时代,屏幕宽度依然是 320px,但是宽度上的物理像素却变成了 640px,是宽度的两倍
屏幕宽度没变,物理像素却增加了,所以为了屏幕显示的内容不改变,原先需要一个像素绘制的点,现在会用两个像素来绘制
为了表示这种屏幕的特性,浏览器全局对象下就有了这样一个属性——devicePixelRatio
设备像素比,它的计算方式是 物理像素 / 屏幕宽度的像素,所以 3G 的设备像素比为 1 , 4s 为 2,而现在 iPhone 的 plus 型号手机的设备像素比为 3,甚至部分出现了比值为 4 的安卓设备
// 获取设备像素比
window.devicePixelRatio
回到 canvas 上的问题上,当我们想要绘制一条 1px 的线时,由于当前浏览器的设备像素比是 2,所以实际上是通过 2 个像素点来绘制的,但是即便是2个像素绘制的线条也不应该出现模糊的问题,而现在确实模糊了,这又是为什么呢?
canvas 的 css 宽高与上下文宽高
在开头的代码中,canvas 标签是下面这样的:
<canvas id="canvas" width="200" height="200"></canvas>
canvas 标签中的 width
和 height
属性并不是 css 中的宽高,而是 canvas 绘图上下文(绘图区域)的宽高,当不设置 canvas 的 css 宽高时,canvas 会将 width
和 height
的值作为 css 宽高,而 css 宽高使元素在页面上的可见尺寸
但是 canvas 的上下文宽高略奇怪,它可不管像素比是 1 是 2 还是 3,它就是会将整个 canvas 绘图区域塞进 css 宽高中并且填满,绘图的时候会将绘制的图形的宽高按照塞进 css 时宽与高的缩放比率分别进行缩放(所以如果缩放比率不同,就会导致绘制的图形变形)
但是上面这些都不是导致模糊的真正原因,下面这个才是捣乱的元凶:
canvas 绘图时,会从两个物理像素的中间位置开始绘制并向两边扩散 0.5 个物理像素。当设备像素比为 1 时,一个 1px 的线条实际上占据了两个物理像素(每个像素实际上只占一半),由于不存在 0.5 个像素,所以这两个像素本来不应该被绘制的部分也被绘制了,于是 1 物理像素的线条变成了 2 物理像素,视觉上就造成了模糊
解决绘图模糊的方法
上面说了那么多弯弯绕,可能理解起来比较晕,但是解决的方法通过上面的内容却已经诞生了:
首先分别声明 canvas 的 css 宽高和上下文宽高,同时上下文宽高应该是 css 宽高的 devicePixelRatio 倍。在上面的例子中就是:
// devicePixelRatio = 2
<style>
canvas {
width: 200px;
height: 200px;
}
</style>
<canvas id="canvas" width="400" height="400"></canvas>
此时绘制一个与上面例子中相同的矩形,会是下面的效果:
虽然是 100px * 100px 的矩形,但是由于绘制在 400px * 400px 的区域上,而画布又等比缩放在 200px * 200px 的 canvas 标签内,导致视觉上这个矩形只有 50px * 50px,绘制的线条宽度 1px 变成了 0.5 px,但因为无法绘制 0.5px 所以依然是通过 2 个物理像素绘制
下一步,我们需要将 canvas 的绘图区域扩大一倍(因为 devicePixelRatio = 2),这样才能让视觉上的效果正常:
ctx.scale(2, 2)
ctx.strokeStyle = '#000'
ctx.lineWidth = 1
ctx.strokeRect(10, 10, 100, 100)
效果如下:
此时原先 0.5px 的线条变成了 1px,依然通过 2 个物理像素绘制,所以虽然扩大了一倍,但是边框宽度并不会改变
需要注意的是,要在绘制之前先放大绘图区域,否则无效(不要忘了 canvas 的绘制是基于上下文状态的)
到这里,我们就已经顺利解决了 canvas 绘图模糊的问题啦!如果你还没能理解,或许自己动手写一遍是很好的选择哦!
扫码关注前端周记公众号