Canvas 2D 概述
撰写时间:2024-03-30
修订时间:2024-04-14
概述
Canvas 2D是HTML 5中用以在canvas上绘制图形的技术。
在Canvas 2D技术出来之前,一般通过在SVG实现在网页中绘制图形与文本的目的。SVG虽是声明式的,绘图较方便,但若要绘制较为丰富的图形与文本,需要做较多的设置工作。而Canvas 2D主要通过API来绘图,具有较大的灵活性。并且Canvas 2D的API很简练,上手很快。因此,现有一些开源软件使用Canvas 2D技术来自动生成SVG图像,从而实现强强联合,在各自领域内更有效地发挥巨大作用。
基本用法
先从一个canvas元素上取得其绘图环境,然后再绘制图形。
在下面的一些代码中,可能不显示下面的语句:
这两行语句只是不显示,但均会在后台得到执行。
Canvas标签是行内元素
canvas标签是行内元素(inline element)。
上面两个标签,因为它们均为行内元素,因此都排在同一行上。
一般情况下,行内元素没有宽度与高度。上面的span标签虽已通过CSS为其设置了具体的宽度与高度,但对其行内元素不起作用。
但canvas元素是较为特殊的元素。它虽显示为行内元素(与span排在一行),却是可以有宽度与高度的属性。因此,这是一个inline-block元素。默认情况下,其宽度与高度分别为300px × 150px。
CSS及JavaScript的冲突
在涉及到canvas时,有一个很不容易觉察的问题。
不指定Canvas宽高时的默认值
JS修改Canvas宽高值
下面根据devicePixelRatio属性值改变:
代码只修改了canvas的width及height属性值,但canvas的clientWidth及clientHeight属性值均发生了变化!这种情况在Safari中出现,但Chrome中未改变。
显式地指定Canvas的初始默认值
现在,在canvas的CSS设置中为其显式地设置其width及height属性值。
clientWidth及clientHeight属性值没有变化,仅是width及height属性值发生了变化。
也即,在应用devicePixelRatio时,是否显式地设置canvas的CSS的width及height属性值,将导致出现不同的结果!
哪种逻辑正确?canvas在浏览器中的大小应由CSS来设置,我们通过JavaScript代码来设置其width及height的值,目的仅是为了改变清晰度(下节解释),而不应连锁地改变canvas的clientWidth及clientHeight属性值。因此,应通过CSS显式地设置canvas的width及height值。
解决Canvas图像模糊的问题
通过CSS将canvas的宽度设置为600px,将高度设置为400px,将导致图像变模糊了。
在console面板中观察clientWidth, clientHeight, width及height的值。CSS的设置将导致canvas的clientWidth属性值变为600px,clientHeight属性值变为400px,但其width的值仍为300px,height的值仍为150px。而canvas的绘图环境ctx只受width及height影响,与clientWidth与clientHeight无关。这样,我们在300px * 150px的范围内绘制图像,其结果最终被拉伸映射至600px * 400px的范围,图像就变得模糊了。
修改上面的CSS宽度值或高度值再重新运行,您会发现,CSS所设置的值越大,映射拉伸越厉害,图像就越模糊。
解决的方法是,在应用CSS样式后,我们应将canvas的width属性值及height属性值与其clientWidth及clientHeight值同步,再调用canvas的getContext方法。
这样就可确保canvas的内部绘图环境ctx的空间大小与canvas在浏览器客户端上的大小完全保持一致,就不会出现图像拉伸问题,从而确保图像清晰。
在高分辨率的设备上使用Canvas
何为高分辨率设备
在Web环境中,全局对象window有一个名为devicePixelRatio的属性,表示硬件设备可用多少个物理像素来绘制一个CSS像素。
可用下面的代码来查看其值:
或者,在线检测:window.devicePixelRatio = 。
上面的结果是动态检测的结果,不是手工打上去的。因此,不同的设备看到的数值都不一样。值为1,为标准设备。值为2或3,则为高分辨率设备。在购买新设备时,先使用该设备访问本网页,便可知道该显示设备是否高分辨率的显示设备,然后再决定是否购买。
当devicePixelRatio的值为2时,表示您的显示硬件设备可用2个硬件像素来绘制一个CSS像素。使用更多的硬件像素来体现一个CSS像素,将使得图像更加清晰、精细。这就是为何它们被称为高分辨率的原因。Apple公司还为这种设备专门取了一个名字:Retina设备
。好消息是,现在千元以上的手机,大多都具备了这种高分辨率的显示设备。而台式电脑,可能有相当大的一部分仅是标准设备。但,不好说,最好在线检测。
标准设备与高清设备的比较
CSS不会根据devicePixelRatio的值自动改变clientWidth及clientHeight的值。因此,在高分辨率设备上,我们需要将这两个值分别乘以devicePixelRatio的值。
上面,第一个canvas只使用了原始大小,从而造成高分辨率设备的浪费。而第二个canvas则先将devicePixelRatio的值分别乘以clientWidth及clientHeight的值后,再分别赋值于width及height属性。
第2个canvas的尺寸变大了,但字体却保持不变,也会导致浪费了canvas的丰富的空间。加大第二个canvas上的字体:
字体变大了,但这回却不存在图像从小被拉伸至较大的情况,因此图像依旧十分清晰。这就是高分辨率的好处:如果我们保留原来的字体大小,则可在相同区域内绘制更多图像;如果我们选择占用更多空间,则图像变得更大,但又保持了高度清晰。
统一应对标准设备与高清设备
统一应对的原理
对于特定的绘制代码,若要在标准设备与高清设备中实现相同的大小,只需将相应的参数都乘以devicePixelRatio即可。
上面代码的最核心部分:
知晓了原理,但如何让各个类型为数值的参数自动地与devicePixelRatio相乘?
共有三种方法。第一种方法是使用JavaScript中的Proxy技术。第二种方法是通过prototype来重新定义相关方法。第三种方法是通过scale变换。
使用Proxy技术
现在,变量ctx实际上已经是CanvasRenderingContext2D的一个代理,当我们调用CanvasRenderingContext2D的任意方法时,我们在代理中都会检查其参数是否有数值类型,若有,则与devicePixelRatio相乘后再返回其值。
同样,对于设置字体大小的代码:
我们通过正则表达式将其中的数字与devicePixelRatio相乘:
这样就保证了无论是图形还是文本,其大小都根据devicePixelRatio自动进行了变换。
效果如下:
左边的canvas为标清,右边的为高清。两者使用的代码完全一致,大小完全一致,外观完全一致。但右边的canvas在高分辨率设备下看着更舒服。
使用prototype重新定义
CSS将canvas的大小定义为300px × 150px。上面两行代码:
按照CSS所声明的像素来调用这两个方法,渲染效果为全部描边及填充(留边10px)。但实际上,在高分辨率显示设备下,正如Console所显示的,canvas的大小已经实际变为600px × 300px。说明上面两行代码中的参数已自动进行了转换。
核心代码为:
第一,代码:
先将原来的方法留存起来。
第二,利用prototype重新定义方法:
因为我们准备同时重定义strokeRect, fillRect等多个方法,故使用不定长参数...args
来接收不同的参数。
第三,通过Array的map方法,将这些参数中的数值逐个都与devicePixelRatio相乘,然后再通过_innerMethod.apply
方法来调用原来留存的方法。不能直接调用,否则会造成无限嵌套调用而死锁。因为map方法返回数组,故调用了apply方法而不是call方法。
第四,因为每次调用时,都与devicePixelRatio相乘,当我们重复按下Run按钮时,将导致累积地相乘。因此,只能进行一次转换。故此,在第一次转换时,我们通过为各个方法设置一个isRedefined属性来记录是否已经进行重新定义。
第五,如果已经重新定义,则不再重定义:
与使用Proxy的方式相比,使用prototype来重新定义的方式更简便,代码更好维护。
需提出的是,不仅CanvasRenderingContext2D的相关方法需重新定义,Path2D的各个方法也需如此重新定义。
使用Scale变换
第三种方法最简单,通过调用ctx的scale方法即可。
监听devicePixelRatio的变化
当我们放大、缩小网页,或是将移动设备竖放或横放时,将导致devicePixelRatio的值也会发生变化。我们可以使用matchMedia方法来获取相应的媒介查询,并在此基础上监听其值的变化。
上面代码的要点在于每次监听的媒介都不一样。例如,在一些标准设备上,devicePixelRatio的初始值为1。这时我们应监听(resolution: 1dppx)
。而当其值改变为2后,我们应重新监听(resolution: 2dppx)
,如此等等。因此上面的代码的addEventListener都有一个{once: true}
的选项,用完即弃,然后再重新监听新值。下面是drawOnCanvas的代码:
查看单页应用的效果。
在下面几种情况下可导致devicePixelRatio发生变化:
- 缩放页面字体大小时
- 从竖屏与横屏状态相互切换时
- 在自响应模式下切换不同的模拟设备时
无论devicePixelRatio如何变化,也无论是否有高分辨率设备,字体大小所占比例均完全一样。
注意上面的代码,并没有清屏的代码,但却获得了自动清屏并重绘的效果,并且毫无闪烁之感。这应该是各浏览器均自动实现了离屏缓冲区的结果。
小结
本站的geoGrp使用用户自定义坐标系则是以另一种方式完美地解决了此问题。但作为Canvas 2D的参考文档,应尽可能如实地使用原有的APIs。
由于Canvas 2D的规范尚未对如何使用高分辨率设备作为规定,导致各浏览器对此没有直接的技术支持,从而导致我们走了许多弯路。由此可见Web标准的重要性。
微软的IE当初与Netscape争得何比激烈,并成功地将Netscape驱离了市场。但为何获胜后最终还是退出了市场?原因是其太狂妄了,在获胜后居功自傲,完全漠视Web标准,最终被市场无情抛弃。而Netscape却转型为Mozilla,至今成长为浏览器领域中的翘楚。
三十年河东,三十年河西,世事难料。唯有不断进取,独善其身,才是王者之道。