本文记录了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的正方形块

如果仅仅按照如下代码书写,那么得到的矩形块根本不是正方形,而是个长方形啊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<style>
#myCanvas{
width: 400px;
height: 400px;
border: 1px solid #000;
}
</style>
<canvas id="myCanvas"></canvas>
<script type="text/javascript">
var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(0,0,100,100);
</script>

为什么会这样呢?
在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.jsHTMLCanvasElement

  • ratio = devicePixelRatio \ backingStore
  • 前者是根据ratio, 将canvas的context的主要绘图接口重写
  • 后者是改变canvas的width和style.width
  • 然后两者综合起来,来达到维持canvas清晰度的目的

safari下的webkitBackingStorePixelRatio

BUG复现:仅在safari下报错,但不影响程序的功能,即曲线和正常出,清晰度也符合预期。
报错信息:不赞成在非CanvasRenderingContext2D的对象上使用webkitBackingStorePixelRatio属性

定位问题

因为时间比较紧,没有仔细研究插件代码的具体含义,只是简单定位问题,修复错误。

  1. 猜测,首先想到的是捕获错误,定位确定是不是因为webkitBackingStorePixelRatio而引起的错误。
    方法:尝试在出现webkitBackingStorePixelRatio的地方用try-catch捕获错误
    结果:然并卵,居然没捕获到错误,依然会报相同的错误

  2. 进一步怀疑
    方法:去除webkitBackingStorePixelRatio使用的地方
    结果:报错消失

  3. 确认到底是不是webkitBackingStorePixelRatio而引起的错误
    方法:使用webkitBackingStorePixelRatio的对象是CanvasRenderingContext2D.prototype,分别直接在safari和chorme控制台查找CanvasRenderingContext2D.prototype的属性,都没有找到这个属性;
    接着直接输入CanvasRenderingContext2D.prototype.webkitBackingStorePixelRatio
    结果: safari中报相同的错误,chrome中没有报错。

  4. 再次验证
    方法:在safari控制台输入CanvasRenderingContext2D.webkitBackingStorePixelRatio
    结果:虽然没有获得到值,但是也没有报错。可以肯定确实是CanvasRenderingContext2D.prototype.webkitBackingStorePixelRatio而引起的错误。

  5. 为什么没有捕获到错误,怀疑代码没有执行到try-catch的地方就报错了
    方法:在try-catch的上下加console.log,并打印用webkitBackingStorePixelRatio的变量t最终结果
    结果:输出不符合预期,t居然还有了最终值,报错依然存在。很是奇怪,按预期,报错了应该终止程序,t没有结果。

结果分析

确实是webkitBackingStorePixelRatio这个属性而引起的报错。但是既然不影响程序的正常运行,却又有这个错误,可以理解为是safari自身的问题,它的提示信息不够友好。

修复方法

  1. 通过查阅资料发现webkitBackingStorePixelRatio这个属性也是为了解决canvas清晰度而存在的。
    它决定了浏览器在渲染canvas之前会用几个像素来存储画布信息。不同浏览器的BackingStorePixelRatio可能不同,它和devicePixelRatio共同决定了canvas的清晰度。比如在devicePixelRatio=2的设备上,safari6的webkitBackingStorePixelRatio是2,也就是说一个canvas在safari6上不用做任何处理就是清晰的。而chrome的webkitBackingStorePixelRatio是1,需要对canvas进行缩放才能保证其清晰度。High DPI Canvas

  2. 但是在2013.8之后的chromium升级中,去除了这一属性
    因为此后webkitBackingStorePixelRatio的值始终保持为1,也就是说目前只有safari6.0中的webkitBackingStorePixelRatio是2.

  3. 因此在插件中就直接写定为1就可以了。

  4. 之前提到过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或插件影响了它

综合以上可能的原因,做了如下的验证:

  1. 在最外层js最开始的地方加try-catch,试图捕获错误信息,打印不出来任何错误

  2. 因为canvas为绝对定位,而其父元素又没有设置高度,可能受到影响;为其父元素加高度,加背景色,背景色显示,曲线还是没出来

  3. 给canvas加背景色,然并卵,毫无改变

  4. 打印canvas的宽高,offset().width等信息,可以正常打印,说明canvas这个元素是存在的

  5. 为canvas加较高层级,然并卵

  6. 在body中新追加简单的canvas,同样没显示出来

由此可以确认,不是由于绘制canvas出错,和画曲线无关系。接下来进一步验证

  1. 在纯净的页面中简单画最最简单的canvas,不做任何变换等等,结果出来了。
    由此说明,是其他dom或插件影响了之前canvas的正常绘制。

  2. 去除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的原因了。

修复方法

  1. 初步怀疑是chrome的某个版本内核通用的bug
    通过打印两个2APP的window.navigator.userAgent(一个正常显示,一个非正常显示),居然发现chrome内核版本居然一样,而且除了最后app的信息外,其他的一模一样。

  2. 所以只能是针对特定手机中的特定app做单独的case的处理

  3. 用正则匹配userAgent,/(Coolpad\s8675).*(mso_app\s\(4.1.0.1001\))/i
    如果符合这两个条件,那么就不调用hidpi-canvas插件

总结

  • try-catch是个好方法
    可以捕获错误,避免报错,打印错误信息

    1
    2
    3
    4
    5
    try{
    可能会报错的代码
    }catch(error){
    对错误的处理,比如打印错误信息
    }
  • window.navigator.userAgent是个好东西
    常常用来做hack…

  • 另外还发现在safari下,没有external对象

  • 最后附上解决hidpi下canvas清晰度的代码