阅读 287

canvas学习重点和“坑点”分享(一)

前言

本文主要总结一下学习 canvas 过程中个人的一些心得,希望对想要学习 canvas,或者正在学习 canvas 的小伙伴提供一些思路。文章主要按照时间顺序推进。

踩的那些“坑”

一、用属性设置 canvas 宽高

按照我们平时的开发习惯,一般就用css对canvas设置宽高,下图就是用css设置宽300px,高200px下画的一个圆arc(100, 70, 50, 0, Math.PI * 2) 。结果呈现出来的是一个椭圆形。

原因是因为canvas 的绘制是先以属性宽高(未设置默认为300*150)绘制,绘制完后再放大到css设置的样式宽高,所以相当于绘制的图形被缩放了。会有失真和比例不对的问题。
所以需要使用属性对 canvas 设置宽高<canvas width="600" height="300"></canvas>
或者用 js 设置canvas.width=600;canvas.height=300;
宽高不要写单位,html 中不管你写的什么单位,最终都以 px 为单位,而 js 中写上非px单位宽高会变为 0。

二、绘制的路径需调用beginPath方法清除

路径一旦生成,在调用beginPath方法前,几乎就不会被清除。

1.不会因为调用 stroke 而清除已绘制路径

    ctx.lineWidth =20;
    ctx.strokeStyle = "rgba(0,255,0,0.5)"
    ctx.moveTo(50, 70)
    ctx.lineTo(350, 70);
    ctx.stroke();
    ctx.lineWidth = 2;
    ctx.strokeStyle = "black"
    ctx.moveTo(50, 100)
    ctx.lineTo(350, 100);
    ctx.stroke();
复制代码

想要画两条水平线,最开始的时候很自然地写了以上代码。而实际效果如下图

可以看到第二次stroke时,黑色线条在两处地方都绘制了。因为调用stroke后,stroke的路径并没有被清除。也就是说路径并不是一次性的。所以再次调用 stroke 就画了之前生成的所有路径。这点比较容易被忽视,因为很多时候绘制样式不变,重复绘制不容易看出效果。

2.clearRect 不能清除路径

clearRect 是清除画布某区域的常用方法,但是它是不能清除路径的,在你调用 clearRect 后再调用 stroke 方法,之前的路径依然会被绘制。

3.canvas.width = canvas.width 可以清除路径

canvas.width = canvas.width 是一种清空画布的技巧。实践发现,实际上这个方法相当于重置了整个canvas,不仅画布上已绘制内容被清空,路径被清除。而且绘图环境context上设置的属性都被重置为默认值了。
列这个方法想要说明肯定有其他方法可以清除路径,但并不是常规方法。

三、lineWidth 是奇数时画出的直线异常

1.问题

当线宽为奇数特别是 1px 时画出的直线颜色和宽度都不正常

2.分析

根本原因是 canvas 最小绘制单位是 1px,以及 canvas 画线方式决定。canvas 在画线时是以给定路径为中心,像两边延伸,各自延伸线宽的一半。如下图所示


绘制(3,1) 到 (3,5),lineWidth 为 1px 的直线时,先以 x 坐标 3 为中心,两边各自画 0.5 px,即图深蓝色部分。因为 canvas 最小绘制单位是 1 px,而当前绘制的是 2.5-3.5 区域,所以 2-2.5 区域也必须要被绘制,结果就是 2-3 像素的区域会以给的颜色值平均分摊,视觉上颜色就变淡了。3-4区域 同理。所以最终的宽度也由 1 px变成了 2 px。

为什么线宽为 1 px时特别明显?首先是因为 1 px变到 2 px很直观,线宽越宽增加 1 px感官上越不明显,第二是因为画 1 px时整个线条的颜色值都变淡了,而画 3 px或者更宽的线条时,只有边缘的半像素扩大并变淡了,中间的线条颜色还是设置的颜色。

3.解决方法

根据上面的分析,解决办法就是画这样的直线时平移 0.5 像素,如下图所示。即(3.5,1) 画至(3.5,5)。实际绘制的是横坐标 3-4 像素的垂直线。有个简便的操作可以直接 translate 0.5像素,就不用关心具体的坐标值了。

重点功能总结

这部分主要总结下canvas中比较重要的API,使用较多或者对解决问题比较有帮助。

一、canvas中的变形

变形在 canvas 中是一个很实用的功能。有translate、rotate、scale三种类型。canvas中的变形要理解它并不是去改变已绘制内容。而是对canvas的坐标系统进行了变换。MDN上有一句话说的很好,这里分享下:

canvas 中的图像一旦绘制出来,它就一直保持那样了,除非清除后重绘或者直接在原有区域进行覆盖。

1.canvas中的坐标系统

canvas中默认的坐标系统:原点在画布左上角,水平方向x轴向右为正方向,竖直方向y轴向下为正方向。在默认坐标系统中绘制一个矩形ctx.fillRect(20,20,100,100)呈现的效果如下图

这时进行一个translate变形。ctx.translate(120,120)。即把坐标系统向右向下平移120像素。如果不绘制图形,那么canvas看上去没有任何改变。因为只是改变坐标系统的位置,对于已经在canvas上绘制好的图形毫无影响。translate后再执行一遍绘制矩形的代码ctx.fillRect(20,20,100,100) 现在的canvas如下图所示

虽然两次绘制矩形的参数完全一致,但是矩形的起点坐标点(20,20)对于两次绘制来说在canvas中的位置并不相同。因为坐标系统改变了。

2.变形一般配合save和restore使用

save 会保存 cavans 的所有状态,这个所有状态大致分为:变形、样式属性、裁剪路径。裁剪路径后面单独记录。理解时可以认为调用一次save就相当于把canvas当前状态push进一个数组,而调用restore就是pop出数组最后一个状态供你使用。遵循后进先出原则。
当你使用了多个变形,调用一次 restore 就直接恢复了,而不用一个一个去设置回初始值。使用起来非常方便。

3.多种变形配合使用

rotate和scale很多时候是需要和translate一起使用的。比如下面这个钟表动画。

这个动画中三个指针是动态的,即需要一直重绘。他们的实现思路:根据当前时刻,分别计算出时分秒指针对于12点钟方向,即竖直向上方向的偏差角度。然后使用rotate变换将坐标系统旋转相应的偏差角度。这时去绘制,只需从钟表圆心画至12点方向即可,在canvas上呈现出来的就是正确的偏移角度了。
那么在这个过程中,在使用rotate时就需要先把坐标系统translate至钟表圆心,否则它会以画布左上角为变换原点进行旋转。显然是不对的。

二、clip方法和isPointInPath方法

1.裁剪路径 clip

当调用clip方法后,canvas上的绘制都只针对裁剪后的区域生效,需要注意的是clip裁剪的是绘图环境最后一次绘制的路径。默认裁剪整块画布。这里举一个应用场景,比方说要绘制下方这个圆形网格:


如果不使用clip方法要画出这个图形,可能需要进行复杂的计算。
运用 clip 后就变得非常简单了:先绘制一个圆路径调用 clip,然后就可以放心大胆的画网格了,直接针对整个画布绘制网格即可,最终只有被裁剪区域即圆形区域才会被绘制,注意,调用 clip 前后必须使用 save 和 restore,否则就变不回去了。

再举一个clip的应用场景,canvas中清除某块区域一般是用clearRect方法,但是clearRect只能清除矩形区域。如果想清除非矩形区域呢?下面是一个简单的绘制工具,就做了一个圆形的橡皮擦

同样是用clip实现的,即在清除前先arc圆形橡皮擦路径,再调用clip方法后使用clearRect清除,只要clearRect矩形够大能够覆盖圆形橡皮擦,最终被清除的就是橡皮擦内的内容。

2.isPointInPath方法

isPointInPath(x,y) 是判断传入点是否在绘制路径中,也是最后一次绘制路径。这个方法在交互的时候使用的会比较多。下面同样是一个使用场景

图中展示了绘制二次贝塞尔曲线的二种情况,一种就是拖动了小球即控制点,那么相应地就绘制贝塞尔曲线。如果没有点击到小球,那么就是去另一个地方绘制贝塞尔曲线。这里就需要去判断鼠标是否在小球内 mouseDown 了。这个时候就使用isPointInPath 方法,注意小球路径必须为调用方法前的最后绘制路径。

像圆形这种规则图形,不使用上面两种方法或许还可自行计算。比如鼠标是否在小球内按下,也可通过鼠标位置到小球圆心的距离是否大于小球半径判断。但是如果是复杂图形,不规则图形,很多时候就非常难计算了。

三、合成图形 globalCompositeOperation

globalCompositeOperation 属性就是设置已绘图形和新绘图形的合成方式。默认很明显地就是新图形覆盖老图形,新图形的层级比较高,默认值 source-over。那肯定也可以设置新绘制图形在老图形之下。这时候 globalCompositeOperation 的值为 destination-over。 使用场景就是这个例子中的选中效果,给框框增加了一个阴影。设置阴影需要绘制一个同样大小的矩形,那么正常情况新绘制的矩形就会覆盖原有的矩形,所以在这里设置 globalCompositeOperation 为 destination-over,这样新矩形的内容在原有矩形下方被盖住,而只有我需要的阴影呈现了出来。

globalCompositeOperation 还有一些常用的值可以用来组合出一些新图形,比如下面就是用 globalCompositeOperation 的 source-out 合成的月亮。source-out 就是说新图形 out 部分即不重叠部分被绘制。 默认值 source-out

globalCompositeOperation的值很多,就不一一罗列了。这个属性感觉上是很有用的,但是目前为止个人对它的使用场景了解的还不够。所以后面学习中如果有遇到会再补充进来。

性能

最后关于 canvas 性能,说下个人的看法。如果你没有频繁重绘,那么大部分情况不需太关注性能。绘制一次对现代浏览器来说问题是不大的。要关注的是频繁重绘部分。

频繁重绘时最重要的就是只重绘必须重绘的。比方说之前的那个时钟动画,除了三个指针外其他东西都是静止的,静止的东西绘制一次就够了。这个时候可以使用多层画布,即静态部分用一个canvas放在下方,动态部分用另外一个canvas叠在上方。 再比如上面clip中讲到的橡皮擦实例,这个绘制工具的背景就是用了一个底层canvas,因为背景是永远不需要改变的。这样在橡皮擦擦拭的时候,就不用担心把背景也清除了。

结语

本文是个人对canvas第一阶段学习的一个小结。如果文中内容有错误或者不准确的地方欢迎大家指出。最后,文中绘制实例的源码在这,有兴趣的小伙伴可以看看: github.com/Yuchaocheng…

文章分类
前端
文章标签