了解如何正确使用canvas画布,以及通过canvas绘制图形及动画。
通过本节,你将学会:
快应用的canvas功能由两部分组成,canvas组件和渲染脚本。
canvas组件中,用于绘制图形的部分,称之为画布。
和其他组件一样,在快应用template中添加即可。同时可为其添加需要的样式。
这里需要注意,与HTML中canvas不同的是:
单独的canvas组件仅仅是一个透明矩形,我们需要通过渲染脚本来进一步操作。
首先通过$element和id来获取canvas组件节点,再通过getContext方法创建canvas绘图上下文。
getContext方法的参数目前仅支持'2d',创建的canvas绘图上下文是一个CanvasRenderingContext2D对象。
在后续脚本中操作该对象即可绘制图形。
完整示例代码如下:
如果你想进入页面即渲染canvas,只能在onShow中获取canvas组件节点,绘制图形。输出效果如图
开始画图之前,需要了解一下画布的坐标系。
如下图所示,坐标系原点为左上角(坐标为(0,0))。所有元素的位置都相对于原点定位。x轴向右递增,y轴向下递增。
canvas绘图的基本绘制方式之一是填充绘制。
填充是指用指定的内容填满所要绘制的图形,最终生成一个实心的图案。
canvas绘图的另一种基本绘制方式是描边绘制。
描边绘制是指,沿着所要绘制的图形边缘,使用指定的内容进行描绘,最终生成的是空心的图案。
如果既要填充又要描边,则需要分别绘制两次完成最终图案。
矩形,是最基础的形状。canvas提供了三种方法绘制矩形:
与绘制矩形的直接绘制不同,绘制路径需要一些额外的步骤。
为此,我们需要了解以下一些基本方法。
开始一条新路径,这是生成路径的第一步操作。
一条路径本质上是由多段子路径(直线、弧形、等等)组成。而每次调用beginPath之后,子路径清空重置,然后就可以重新绘制新的图形。
闭合当前路径。
closePath()不是必须的操作,相当于绘制一条当前位置到路径起始位置的直线子路径。
描边绘制当前路径。
填充绘制当前路径。
当调用fill()时,当前没有闭合的路径会自动闭合,不需要手动调用closePath()函数。调用stroke()时不会自动闭合。
移动笔触。将当前路径绘制的笔触移动到某个坐标点。
相当于绘制一条真正不可见的子路径。通常用于绘制不连续的路径。
调用beginPath()之后,或者canvas刚创建的时候,当前路径为空,第一条路径绘制命令无论实际上是什么,通常都会被视为moveTo。因此,在开始新路径之后建议通过moveTo指定起始位置。
路径绘制命令是实际绘制路径线条的一些命令。包括有:
这里,我们展示一个组合使用的效果,绘制一个快应用的logo。
drawCanvas(){constcanvas=this.$element('newCanvas')constctx=canvas.getContext('2d')constr=20consth=380constp=Math.PIctx.beginPath()ctx.moveTo(r*2,r)ctx.arc(r*2,r*2,r,-p/2,-p,true)ctx.lineTo(r,h-r*2)ctx.arc(r*2,h-r*2,r,p,p/2,true)ctx.lineTo(h-r*2,h-r)ctx.arc(h-r*2,h-r*2,r,p/2,0,true)ctx.lineTo(h-r,r*2)ctx.arc(h-r*2,r*2,r,0,-p/2,true)ctx.closePath()ctx.stroke()consts=60ctx.beginPath()ctx.moveTo(h/2+s,h/2)ctx.arc(h/2,h/2,s,0,-p/2*3,true)ctx.arc(h/2,h/2+s+s/2,s/2,-p/2,p/2,false)ctx.arc(h/2,h/2,s*2,-p/2*3,0,false)ctx.arc(h/2+s+s/2,h/2,s/2,0,p,false)ctx.moveTo(h/2+s*2,h/2+s+s/2)ctx.arc(h/2+s+s/2,h/2+s+s/2,s/2,0,p*2,false)ctx.moveTo(h/2+s/4*3,h/2+s/2)ctx.arc(h/2+s/2,h/2+s/2,s/4,0,p*2,false)ctx.fill()}实现效果如下
通过刚才的例子,我们学会了绘制图形。
但是我们看到,不管是填充还是描边,画出来的都是简单的黑白图形。如果想要指定描绘的内容,画出更丰富的效果应该如何操作呢?
有两个重要的属性可以做到,fillStyle和strokeStyle。顾名思义,分别是为填充和描边指定样式。
在本章节最初的例子里,其实已经看到上色的基本方法,就是直接用颜色作为指定样式。
ctx.fillStyle='rgb(200,0,0)'ctx.fillRect(20,20,200,200)一旦设置了fillStyle或者strokeStyle的值,新值就会成为新绘制的图形的默认值。如果你要给每个图形上不同的颜色,需要画完一种样式的图形后,重新设置fillStyle或strokeStyle的值。
//填充绘制一个矩形,颜色为暗红色ctx.fillStyle='rgb(200,0,0)'ctx.fillRect(20,20,200,200)//描边绘制另一个矩形,边框颜色为半透明蓝色ctx.strokeStyle='rgba(0,0,200,0.5)'ctx.strokeRect(80,80,200,200)canvas的颜色支持各种CSS色彩值。
渐变色对象可以使用createLinearGradient创建线性渐变,然后使用addColorStop上色。
这里要注意的是,渐变色对象的坐标尺寸都是相对画布的。应用了渐变色的图形实际起到的是类似“蒙版”的效果。
线型可设置的项目包括:
顾名思义,线宽就是描边线条的宽度,单位是像素。
这里要注意两点:
线条的宽度会向图形的内部及外部同时延伸,会侵占图形的内部空间。在使用较宽线条时特别需要注意图形内部填充部分是否被过度挤压。常用解决方法可以尝试先描边后填充。
可能会出现的半渲染像素点。例如,绘制一条(1,1)到(1,3),线宽为1px的线段,是在x=1的位置,向左右各延伸0.5px进行绘制。但是由于实际最小绘制单位是一个像素点,那么最终绘制出来的效果将是线宽2px,但是颜色减半的线段,视觉上看就会模糊。常用解决方法,一种是改用偶数的线宽绘制;另一种可以将线段绘制的起始点做适当偏移,例如偏移至(1.5,1)到(1.5,3),左右各延伸0.5px后,正好布满一个像素点,不会出现半像素渲染了。
端点样式决定了线段端点显示的样子。从上至下依次为butt,round和square,其中butt为默认值。
这里要注意的是,round和square会使得线段描绘出来的视觉长度,两端各多出半个线宽,可参考蓝色辅助线。
交点样式决定了图形中两线段连接处所显示的样子。从上至下依次为miter,bevel和round,miter为默认值。
在上图交点样式为miter的展示中,线段的外侧边缘会延伸交汇于一点上。线段直接夹角比较大的,交点不会太远,但当夹角减少时,交点距离会呈指数级增大。
miterLimit属性就是用来设定外延交点与连接点的最大距离,如果交点距离大于此值,交点样式会自动变成了bevel。
drawLineDashCanvas(){constcanvas=this.$element('linedash-canvas')constctx=canvas.getContext('2d')letoffset=0//绘制蚂蚁线setInterval(()=>{offset++if(offset>16){offset=0}ctx.clearRect(0,0,300,300)//设置虚线线段和间隙长度分别为4px2pxctx.setLineDash([4,2])//设置虚线的起始偏移量ctx.lineDashOffset=-offsetctx.strokeRect(10,10,200,200)},20)}运行效果如下
通过学习,我们为刚才绘制的快应用logo添加颜色和样式。
和绘制图形类似,快应用canvas也提供fillText和strokeText两种方法来绘制文字。
可以直接使用符合CSSfont语法的字符串作为文字样式的字体属性。默认值为'10pxsans-serif'。
要注意的是,不同于web,目前快应用还无法引入外部字体文件,对于字体的选择,仅限serif、sans-serif和monosapce。
这两个属性控制了文体相对与绘制定位点的对齐方式。
为了能够在canvas中使用图片,需要使用图像对象来加载图片。
src既可以使用URI来加载本地图片,也使用URL加载网络图片。
为避免图片未加载完成或加载失败导致填充错误,建议在加载成功的回调中进行图片填充操作。
img.onload=()=>{ctx.drawImage(img,0,0)}使用drawImage绘制图片也有3种不同的基本形式,通过不同的参数来控制。
drawImage(image,x,y)其中image是加载的图像对象,x和y是其在目标canvas里的起始坐标。
这种方法会将图片原封不动的绘制在画布上,是最基本的绘制方法。
drawImage(image,x,y,width,height)相对基础方法,多了两个width、height参数,指定了绘制的尺寸。
这种方法会将图片缩放成指定的尺寸后,绘制在画布上。
drawImage(image,sx,sy,sWidth,sHeight,dx,dy,dWidth,dHeight)其中image与基础方法一样,是加载的图像对象。
其它8个参数可以参照下方的图解,前4个是定义图源的切片位置和尺寸,后4个则是定义切片的目标绘制位置和尺寸。
图片不仅仅可以直接绘制在画布中,还可以将图片像渐变色一样,作为绘制图形的样式,在填充和描边绘制中使用。
首先,需要通过createPattern创建图元对象,然后就可以将图元对象作为样式用在图形的绘制中了。
同样,为避免图片未加载完成或加载失败导致填充错误,建议在加载成功的回调中进行操作。
我们不仅可以在已有图形后面再画新图形,还可以用来遮盖指定区域,清除画布中的某些部分(清除区域不仅限于矩形,像clearRect()方法做的那样)以及更多其他操作。
globalCompositeOperation=type
这个属性设定了在画新图形时采用的遮盖策略,其值是一个用于标识不同遮盖方式的字符串。
这是默认设置,并在现有画布上下文之上绘制新图形。
新图形只在与现有画布内容重叠的地方绘制。
新图形只在新图形和目标画布重叠的地方绘制。其他的都是透明的。
在不与现有画布内容重叠的地方绘制新图形。
在现有的画布内容后面绘制新的图形。
现有的画布只保留与新图形重叠的部分,新的图形是在画布内容后面绘制的。
现有的画布内容保持在新图形和现有画布内容重叠的位置。其他的都是透明的。
现有内容保持在新图形不重叠的地方。
两个重叠图形的颜色是通过颜色值相加来确定的。
只显示新图形。
图像中,那些重叠和正常绘制之外的其他地方是透明的。
已有的内容不受影响。
要取消裁切路径的效果,可以绘制一个和画布等大的矩形裁切路径。
//绘制一个红色矩形ctx.fillStyle='rgb(200,0,0)'ctx.fillRect(20,20,200,200)//使用裁切路径绘制一个圆ctx.beginPath()ctx.arc(120,120,120,0,Math.PI*2,true)ctx.clip()//绘制一个蓝色矩形,超出圆形裁切路径之外的部分无法绘制ctx.fillStyle='rgba(0,0,200)'ctx.fillRect(80,80,200,200)运行效果如下
到目前位置,我们所有的绘制,都是基于标准坐标系来绘制的。
标准坐标系的特点是:
现在介绍的变形,就是改变标准坐标系的方法。
for(leti=0;i<6;i++){ctx.fillRect(0,0,40,40)ctx.translate(50,0)}运行效果如图。
可以看到,虽然每次fillRect绘制的参数没有变化,但是因为坐标系变了,最终绘制出来的就是位置不同的图形。
通过前面的学习,我可以看到,每次图形绘制其实都带着非常丰富的状态。
在绘制复杂图形的时候,就会带来重复获取样式的问题。
如何优化呢?
ctx.save()//保存ctx.restore()//恢复canvas状态就是当前所有样式的一个快照。
save和restore方法是用来保存和恢复canvas状态的。
canvas状态存储在栈中,每次save的时候,当前的状态就被推送到栈中保存。
一个canvas状态包括:
你可以调用任意多次save方法。
每一次调用restore方法,上一个保存的状态就从栈中弹出,所有设定都恢复。
ctx.fillRect(20,20,200,200)//使用默认设置,即黑色样式,绘制一个矩形ctx.save()//保存当前黑色样式的状态ctx.fillStyle='#ff0000'//设置一个填充样式,红色ctx.fillRect(30,30,200,200)//使用红色样式绘制一个矩形ctx.save()//保存当前红色样式的状态ctx.fillStyle='#00ff00'//设置一个新的填充样式,绿色ctx.fillRect(40,40,200,200)//使用绿色样式绘制一个矩形ctx.restore()//取出栈顶的红色样式状态,恢复ctx.fillRect(50,50,200,200)//此时状态为红色样式,绘制一个矩形ctx.restore()//取出栈顶的黑色样式状态,恢复ctx.fillRect(60,60,200,200)//此时状态为黑色样式,绘制一个矩形运行效果如下:
之前我们介绍都是静态图像的绘制,接下来介绍动画的绘制方法。
canvas动画的基本原理并不复杂,就是利用setInterval和setTimeout来逐帧的在画布上绘制图形。
在每一帧绘制的过程中,基本遵循以下步骤。
到目前为止,我们尚未深入了解canvas画布真实像素的原理,事实上,你可以直接通过ImageData对象操纵像素数据,直接读取或将数据数组写入该对象中。
在快应用中ImageData对象是一个普通对象,其中存储着canvas对象真实的像素数据,它包含以下几个属性
data属性返回一个Uint8ClampedArray,它可以被使用作为查看初始像素数据。每个像素用4个1bytes值(按照红,绿,蓝和透明值的顺序;这就是"RGBA"格式)来代表。每个颜色值部份用0至255来代表。每个部份被分配到一个在数组内连续的索引,左上角像素的红色部份在数组的索引0位置。像素从左到右被处理,然后往下,遍历整个数组。
Uint8ClampedArray包含高度×宽度×4bytes数据,索引值从0到(高度×宽度×4)-1
例如,要读取图片中位于第50行,第200列的像素的蓝色部份,你会写以下代码:
constblueComponent=imageData.data[50*(imageData.width*4)+200*4+2]你可能用会使用Uint8ClampedArray.length属性来读取像素数组的大小(以bytes为单位):
constmyImageData=ctx.createImageData(width,height)上面代码创建了一个新的具体特定尺寸的ImageData对象。所有像素被预设为透明黑。
你也可以创建一个被anotherImageData对象指定的相同像素的ImageData对象。这个新的对象像素全部被预设为透明黑。这个并非复制了图片数据。
constmyImageData=ctx.getImageData(left,top,width,height)这个方法会返回一个ImageData对象,它代表了画布区域的对象数据,此画布的四个角落分别表示为(left,top),(left+width,top),(left,top+height),以及(left+width,top+height)四个点。这些坐标点被设定为画布坐标空间元素。
你可以用putImageData()方法去对场景进行像素数据的写入。
ctx.putImageData(myImageData,dx,dy)dx和dy参数表示你希望在场景内左上角绘制的像素数据所得到的设备坐标。
例如,为了在场景内左上角绘制myImageData代表的图片,你可以写如下的代码:
setGray(){constcanvas=this.$element('new-canvas')constctx=canvas.getContext('2d')constcanvasW=380constcanvasH=380//得到场景像素数据constimageData=ctx.getImageData(0,0,380,380)constdata=imageData.datafor(leti=0;i了解canvas的特点,现在就可以实现基本组件无法实现的视觉效果。