一次关于canvas hidpi的2个bug解决过程
本文记录了2个为了解决hidpi下canvas呈现而引发的不同bug定位和最终解决过程。越来越多的设备屏幕趋于高分辨率,我们称之为hidpi设备,与此同时人们对用户体验的要求也越来越高,然而canvas是一个画布,屏幕上的每个像素块是其绘制的最小单位。这也就决定了凡是能影响其绘制的因素,都会影响最终canvas呈现的效果。
背景
- canvas的width和style.width的关系
- hidpi-canvas-polyfill的简单介绍
canvas的width和style.width的关系
简单来说是这样的
- canvas的width是这个画布的宽,就是要在多大的布上画图。我们后来在canvas上画线,矩形啥的那个单位也是相对于这个画布来看的。它可以理解为把canvas的宽高均分成了多少份。
- canvas的style.width是canvas的物理宽度,也就是相当于画框的宽。
<canvas width="200" height="200"></canvas>
,这里的width和height不能带有单位。如果省略不写,那么canvas默认width=300, height=150
如果canvas的width/height
和其style.width/style.height
不相等,那么会出现图形比例失真的情况。比如:我们想在400*400
的画框里得到一个100*100
的正方形块
如果仅仅按照如下代码书写,那么得到的矩形块根本不是正方形,而是个长方形啊
为什么会这样呢?
在canvas画布计算时,它是这样的一个过程:
- W看到的宽度 = W需要绘制的宽度 / W画布宽 * W画框宽
- 这里我们没有设置画布的宽高,也就是取默认的300*150, 即W画布宽 = 300,W需要绘制的宽度 = 100,W画框宽取CSS或style中设定的宽, W画框宽 = 400px
- W看到的宽度 = 100 / 300 * 400 = 166px
- W看到的高度计算同理
hidpi对canvas的影响
之前提到canvas的最小单位是像素,通常我们是一个像素绘制一个点,但是当devicePixel=2
时,设备会用2个像素来绘制一个canvas上的点,如果按照原始的画布大小,就会看上去线比较粗。
所以通常当devicePixel不为1时,我们会将画布先放大devicePixel倍,并将canvas放大devicePixel倍。
最后,受到canvas画框的限制,画的canvas就清楚了。
注: context.sacle(2, 2)需要在画图之前就设定
hidpi-canvas-polyfill的简单介绍
这两个bug都是因引入插件hidpi-canvas-polyfill而引起的。如这个polyfill的介绍,它是为了帮助我们在不改变自身任何canvas代码的前提下,自动地在任意浏览器和设备上保持canvas的清晰度。
它由两部分JS组成,CanvasRenderingContext2D.js和HTMLCanvasElement
ratio = devicePixelRatio \ backingStore
- 前者是根据ratio, 将canvas的context的主要绘图接口重写
- 后者是改变canvas的width和style.width
- 然后两者综合起来,来达到维持canvas清晰度的目的
safari下的webkitBackingStorePixelRatio
BUG复现:仅在safari下报错,但不影响程序的功能,即曲线和正常出,清晰度也符合预期。
报错信息:不赞成在非CanvasRenderingContext2D的对象上使用webkitBackingStorePixelRatio属性
定位问题
因为时间比较紧,没有仔细研究插件代码的具体含义,只是简单定位问题,修复错误。
猜测,首先想到的是捕获错误,定位确定是不是因为webkitBackingStorePixelRatio而引起的错误。
方法:尝试在出现webkitBackingStorePixelRatio的地方用try-catch捕获错误
结果:然并卵,居然没捕获到错误,依然会报相同的错误进一步怀疑
方法:去除webkitBackingStorePixelRatio使用的地方
结果:报错消失确认到底是不是webkitBackingStorePixelRatio而引起的错误
方法:使用webkitBackingStorePixelRatio的对象是CanvasRenderingContext2D.prototype,分别直接在safari和chorme控制台查找CanvasRenderingContext2D.prototype的属性,都没有找到这个属性;
接着直接输入CanvasRenderingContext2D.prototype.webkitBackingStorePixelRatio
结果: safari中报相同的错误,chrome中没有报错。再次验证
方法:在safari控制台输入CanvasRenderingContext2D.webkitBackingStorePixelRatio
。
结果:虽然没有获得到值,但是也没有报错。可以肯定确实是CanvasRenderingContext2D.prototype.webkitBackingStorePixelRatio而引起的错误。为什么没有捕获到错误,怀疑代码没有执行到try-catch的地方就报错了
方法:在try-catch的上下加console.log,并打印用webkitBackingStorePixelRatio的变量t最终结果
结果:输出不符合预期,t居然还有了最终值,报错依然存在。很是奇怪,按预期,报错了应该终止程序,t没有结果。
结果分析
确实是webkitBackingStorePixelRatio这个属性而引起的报错。但是既然不影响程序的正常运行,却又有这个错误,可以理解为是safari自身的问题,它的提示信息不够友好。
修复方法
通过查阅资料发现webkitBackingStorePixelRatio这个属性也是为了解决canvas清晰度而存在的。
它决定了浏览器在渲染canvas之前会用几个像素来存储画布信息。不同浏览器的BackingStorePixelRatio可能不同,它和devicePixelRatio共同决定了canvas的清晰度。比如在devicePixelRatio=2的设备上,safari6的webkitBackingStorePixelRatio是2,也就是说一个canvas在safari6上不用做任何处理就是清晰的。而chrome的webkitBackingStorePixelRatio是1,需要对canvas进行缩放才能保证其清晰度。High DPI Canvas但是在2013.8之后的chromium升级中,去除了这一属性
因为此后webkitBackingStorePixelRatio的值始终保持为1,也就是说目前只有safari6.0中的webkitBackingStorePixelRatio是2.因此在插件中就直接写定为1就可以了。
之前提到过hidpi-canvas这个插件由两部分组成,一部分是改变画布和画框大小,一部分是重新画一遍图以适应改变大小后的画布
参考High DPI Canvas文章后,去除了CanvasRenderingContext2D.js这部分,改用context.scale(ratio, ratio);
,其中ratio = devicePixelRatio / backingStoreRatio
。
因为CanvasRenderingContext2D是为绘制canvas提供各种接口。其本身也可以通过context = canvas.getContext('2d')
得到。因此保留这个js意义不大。
Coolpad8675中的好搜app下,canvas渲染不出来
BUG复现:canvas处空白
报错信息:没有脚本报错
定位问题
猜测有可能引起问题的原因
- 浏览器对canvas的渲染,可是别人家的canvas渲染一点问题都没有呀
- 可能是canvas的大小没在可显示的范围里,因为之前有遇到canvas的width特别大,style.width正常的,导致canvas显示不正常
- 可能canvas的父元素没显示出来
- 可能是层级问题
- 页面里的其他dom或插件影响了它
综合以上可能的原因,做了如下的验证:
在最外层js最开始的地方加try-catch,试图捕获错误信息,打印不出来任何错误
因为canvas为绝对定位,而其父元素又没有设置高度,可能受到影响;为其父元素加高度,加背景色,背景色显示,曲线还是没出来
给canvas加背景色,然并卵,毫无改变
打印canvas的宽高,offset().width等信息,可以正常打印,说明canvas这个元素是存在的
为canvas加较高层级,然并卵
在body中新追加简单的canvas,同样没显示出来
由此可以确认,不是由于绘制canvas出错,和画曲线无关系。接下来进一步验证
在纯净的页面中简单画最最简单的canvas,不做任何变换等等,结果出来了。
由此说明,是其他dom或插件影响了之前canvas的正常绘制。去除hidpi-canvas插件,曲线出现了
因为在页面中,影响最大的,最有可能的就是hidpi-canvas这个插件。只有它有对后来的canvas做处理。做的主要处理就是改变canvas的width和style.width
进一步验证改变canvas的width和style.width是如何影响canvas绘制的
- 在纯净的页面中,将canvas的width和style.width设置成不同的值,发现在某些值时,canvas是会正常出现的,在另一些值是不出现的。
- 怀疑在当二者比例在某个范围中,是可正常显示,反之不可。通过二分法验证,不断测试最终确定当
width / style.width < 0.78
时,可正常显示。 - 至此,已可以确定出现bug的原因了。
修复方法
初步怀疑是chrome的某个版本内核通用的bug
通过打印两个2APP的window.navigator.userAgent(一个正常显示,一个非正常显示),居然发现chrome内核版本居然一样,而且除了最后app的信息外,其他的一模一样。所以只能是针对特定手机中的特定app做单独的case的处理
用正则匹配userAgent,
/(Coolpad\s8675).*(mso_app\s\(4.1.0.1001\))/i
如果符合这两个条件,那么就不调用hidpi-canvas插件
总结
try-catch是个好方法
可以捕获错误,避免报错,打印错误信息12345try{可能会报错的代码}catch(error){对错误的处理,比如打印错误信息}window.navigator.userAgent是个好东西
常常用来做hack…另外还发现在safari下,没有external对象
最后附上解决hidpi下canvas清晰度的代码