移动端canvas绘图模糊问题
为了移动端兼容性,我们的一个项目需要一个前端将PDF转换为可以在移动端页面直接查看的界面。为了更简单的解决方案,我们使用 pdf.js 插件,它可以将 pdf 转换为画布并将其绘制在页面上。但测试过程中发现,手机浏览器中绘制的内容非常模糊(如下图)。经分析,发现是移动端高清屏幕导致的。 解决问题后,以书面形式描述调查的原因和结果
在解释问题之前,应该先了解一点关于手机屏幕和cavans的知识,以方便后期研究。您可以滚动到末尾直接查看结果。
关于屏幕的一些基本知识
- 物理像素(DP)
物理像素也称为设备像素。我们经常听说手机的分辨率就是物理像素。比如iPhone 7的物理分辨率为750*1334,屏幕是由像素组成的,也就是说屏幕水平方向有750个像素,垂直方向有1334个像素。 iPhone3GS是3.5英寸。 iPhone4的物理分辨率是640*980,而3gs只有320*480。如果我们按照真实布局画一张宽度为320像素的图片,那么只有一半会包含iPhone4上的内容,而剩下的将是半空的。为了避免这个问题,我们引入了逻辑像素,将两台手机的逻辑像素都设置为320像素,方便绘图
- 设备像素比(DPR)
顶级设备归根结底独立像素是为了便于计算。我们统一了设备的逻辑像素,但是每个逻辑像素所代表的物理像素是未定义的。为了在不缩放的情况下确定物理像素和逻辑像素的比例,我们引入理解设备像素比(DPR)的概念
设备像素比 = 设备像素 / 逻辑像素 DPR = DP / DIP 复制代码
上面讲了很多理论,但这里放一张图来解释一下
可以从上图可以看出,同等大小的逻辑像素下,高分辨率显示的物理像素更多。在普通显示器上,1个逻辑像素对应1个物理像素,而在dpr=2的高清显示器上,1个逻辑像素由4个物理像素组成。这也是高分辨率屏幕更灵敏的原因。
关于canvas的一些基础知识
- canvas绘制位图
这是每个了解canvas的人都应该知道的知识点,也是我们下面要分析的问题的核心。
位图的解释我们稍后再说。我们只需要知道canvas绘制的图片是位图即可。
- 画布宽度和高度属性
画布宽度和高度属性对于初学者来说很容易出错。这两个属性经常与 CSS 中的宽度和高度属性混淆。
例如我们有如下代码(1):
<canvas width="600" height="300" style="width: 300px; height: 150px"></canvas> 复制代码
- 样式宽度和高度表示界面上canvas元素占用的宽度和高度,是样式宽度和高度
- 宽度和height 属性 代表实际画布像素的宽度和高度
如果你还不明白,你可以将其理解为以下代码(2):
<!-- logo.png的像素为600 * 300 --> <img style="width: 300px; height: 150px" /> 复制代码
默认画布宽度和高度为 300 * 150,请设置。添加css后,画布会根据css设置的宽高进行调整(注意不是裁剪),这个和img标签是一样的
上面的代码(1)其实可以用更通俗的方式来解释。方式是 1 个逻辑像素实际上填充有 2 个画布像素。
歧义产生原因的初步探讨
上面是一些必要的背景知识的介绍,下面开始正式的探索。
首先我们提到使用canvas来绘制图片是位图,我们平时使用的jpg、png也是位图。那么什么是位图呢?
位图也称为图像图或光栅图。它通过记录图像中各点的颜色、深度等信息来存储和显示图像。更具体地说,您可以将位图视为一个大谜题。这个拼图有无数块,每块代表一个颜色像素。 理论上,1 位像素对应 1 个物理像素。但是,如果您使用像苹果视网膜显示屏这样的高分辨率显示屏来查看图像呢?
假设我们有以下代码,将在iphone4视网膜屏幕上显示:
<canvas width="320" height="150" style="width: 320px; height: 150px"></canvas>
复制代码
iphone4本身的物理像素是640 * 980,与设备无关的像素是320 * 480,实际上是1个css像素由4个物理像素组成。画布像素为 320 * 150,其 CSS 像素为 320 * 150,这意味着 1 个 CSS 像素将包含 1 个画布元素。像这样转换,在视网膜显示下,1个画布像素(或1个位图)将填充4个物理像素。由于位图的一个点无法进一步划分,因此只能接受最接近的颜色,从而导致图像模糊。
如果您还有疑问,下图可以说明位图如何在视网膜显示器上填充:
上图的左侧是正常的显示规则。可以看出有4位。图像为 16 像素,右侧高分辨率屏幕为 16 像素。由于无法裁剪像素,因此颜色会发生变化。
不过,有一点没有解释清楚,所以图像取的是最接近的颜色,而不是直接取原始值。这也是造成模糊的罪魁祸首。
幕后黑手---平滑技术
以下是我的一位老同学向我解释的。我们刚刚说过,位图的每个元素实际上都是单个颜色像素。现在假设我们需要在普通屏幕上绘制数字“0”,css大小为4px * 4px,dpr为1。那么我们绘制的内容应该如下所示,其中1代表黑色像素,0代表白色像素。
我们可以看到,当 dpr 相对较小时,我们的“0”模式更加明显。如果我们的 css 大小保持不变,但更改为在视网膜显示屏上绘制图像,会产生什么效果?羊毛面料?
我们知道,在视网膜显示下,1个CSS像素代表4个物理像素。如果我们不做任何处理,直接对着上面的矩阵进行编辑,展开矩阵,我们会发现我们的图案在视网膜显示下有锯齿感。很明显,图像明显缺少一丝平滑度。
如果我们把图片稍微改一下,就改成下图
图片立马就柔和了,但是本来应该有4个0的地方,现在却出现了3个1和1个0。这实际上就是所谓的图像平滑过程。为了消除锯齿感,我们改变了原来的颜色。在填充多种颜色的图像中,图像合并成近似颜色,使它们看起来更自然。这也解释了为什么顶部颜色填充不使用原始颜色,而是使用近似颜色。
原因总结
经过上述解释,我们得出以下结论。随着移动终端的普及,高分辨率屏幕基本普及,1个css像素实际上代表了4个或更多的物理像素。但是,由于我们的代码问题,我们的 1 个 CSS 像素和 1 个画布像素是相同的,要求 1 个画布像素实际上填充有 4 个或更多物理像素。为了保证图像处理顺利,填充 剩余的物理像素使用原始颜色的近似值,从而导致图像模糊。
解决方案
一旦了解了问题的原因,解决它就很容易了。解决问题最重要的是均衡一个canvas像素和一个物理像素
对于较新版本的浏览器 devicePixelRatio属性挂在window对象下面,也就是上面提到的dpr,
canvas元素挂时CSS宽度和高度设置好了,我们就可以做到了
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
let dpr = window.devicePixelRatio; // 假设dpr为2
// 获取css的宽高
let { width: cssWidth, height: cssHeight } = canvas.getBoundingClientRect();
// 根据dpr,扩大canvas画布的像素,使1个canvas像素和1个物理像素相等
canvas.width = dpr * cssWidth;
canvas.height = dpr * cssHeight;
// 由于画布扩大,canvas的坐标系也跟着扩大,如果按照原先的坐标系绘图内容会缩小
// 所以需要将绘制比例放大
ctx.scale(dpr,dpr);
复制代码
经验总结
很多时候我们发现,一旦解决了一个问题,你不应该只专注于解决问题,而是需要深入了解问题产生的原因问题,才能取得更好的进步。
作者:carbrokers
链接:https://juejin.im/post/5cbdda7bf265da036504fb46
来源:掘金
版权归作者所有。商业转载请联系作者获得许可。非商业转载请注明来源。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。