Canvas从"入门"到入门
一、前言
Canvas,英文翻译过来为帆布,对于后端的同学听起来可能会有点陌生,但对于前端亦或是涉及UI的客户端同学而言大概都会脱口而出“画布”,用来画画或做动画的前端标签元素,不过大多数同学真正会使用它的并不多,普遍还是停留在直接使用基于其原理的插件如echart来绘制各种图标、或html2canvas来进行截图,这些都是停留在使用现成API的层面上,当然现在也有越来越多的人初步学会使用其来进行简单的图片 or 截图生成、亦或裁剪图片、绘制简单动画等,所以这节课,就让我们一起重新好好地来认识下Canvas这个标签,了解其各种方法,以及如何去应用这些方法去绘制出各种各样有趣的动画或特效,毕竟自己懂得如何画胜过于只会使用现成API,你说是吧。
二、什么是Canvas
Canvas 中文名称叫“画布”,前端的 canvas 是 html 的一个标签,它可以结合 JavaScript 提供的 canvasApi 来绘制各种各样的图形,目前主要用于绘制 2D 图形,客户端则是所有UI组件的“容器”,我们这节课主要讲前端的Canvas,所以后续就不对客户端的Canvas作相应的介绍了。
PS:当你不直接设置 canvas 的 width 和 height 时,它的默认宽高是 300px 和 150px,而且当通过 CSS 样式设置 canvas 宽高时其只是对canvas的图形进行伸缩(原宽高需直接设置width和height),所以会有变模糊亦或图形被拉伸的情况出现。
三、渲染上下文(getContext)
在认识完Canvas,接下来我们就准备来上手其使用方法了,那如何去调用它的canvasApi方法来绘制图形呢,首先就要认识一个新的对象,叫做渲染上下文,<canvas> 标签会创建一个固定大小的画布,并会公开一个或多个渲染上下文,使用渲染上下文对象中的api方法来绘制和处理要展示的内容,也就是说canvasApi方法是集成在其渲染上下文对象中,由于这节课我们只针对2D绘制,那么获取2D渲染上下文方式为
const canvas = document.getElementById('canvas');
//获得 2d 上下文对象
const ctx = canvas.getContext('2d');
当然像WebGL 则使用了基于 OpenGL 的3D 上下文 canvas.getContext("experimental-webgl"),我们这节课不作过多深入,了解下还有其他渲染上下文对象即可。
四、Canvas坐标原点
位于画布容器的左上角顶点,假如你的画布宽高是300与150,往右为x轴正坐标,往下为y轴正坐标,那么左上角顶点[0,0]即为画布基点,后续调用API方法涉及到横纵坐标时,但是相对于这一位置进行计算定位的。
五、canvasAPI
这里我将介绍几个比较常用的api方法,其他较为少用的方法大家有需要时自行百度即可,网上都有其详细的用法介绍。
1、fillStyle
画笔或填充颜色,后续绘制图形除贴图外都会采用所指定的颜色渲染,默认透明
ctx.fillStyle = 'blue';
2、fillRect
根据指定位置,向左向下按照指定宽高直接帮你拉出(绘制)一个矩形出来,并用fillStyle指定的颜色帮你填充颜色
ctx.fillRect(100, 100, 300, 200)
参数对应分别为x轴坐标,y轴坐标,矩形宽度与矩形高度
这里我用俩个不同宽高的画布绘制了同一个矩形,用不同颜色作区分,可看到
若绘制的图形超过画布的范围,则超出部分会被裁剪掉,So突然间你或许就明白了canvas用来裁剪图片的原理了吧,当然有的人可能不知道图片如何贴进来,这个我们稍后说,继续往下看
3、clearRect
顾名思义,清除指定位置的指定宽高的矩形,跟fillRect绘制是相对的,你可以理解成橡皮擦,以矩形块去擦去不需要的地方
4、strokeRect
stroke翻译过来为一笔的意思,那一笔画出来的矩形长什么样,想象一下,其实就是一个矩形的边框啦,只不过里面并没有填充颜色而已,所以该方法是用来画矩形边框的,使用方法跟fillRect一样
但其实上面都是canvasAPI给你提供的快捷方法来的,canvas将占位以及描绘的方法给你合并成一个了,那你知道其子方法是什么么,以及如何实现相同的绘制逻辑。
那我这里也不墨迹了,直接公布答案就是
rect()、fill()和stroke()方法,rect方法你可以当成矩形选择工具,如果你用过PS的话,即以矩形的方式拉出一个占位框,但在未填充前它并不是实际存在的,在你通过fill方法填充或stroke方法绘制边框后,其才会真正存在于画布中。
5、strokeStyle
同理fillStyle,只不过其用于设置边框颜色
6、lineWidth
可用于调整stroke绘制的边框宽度,在绘制前设置即可
说到这里,你可能会问那除了矩形,还有提供其他图形的API么,有!但是可能是矩形使用比较普遍,亦或是画布本身就是矩形的原因,只有矩形有便捷api,其他的需要分布操作,比如下面要讲到圆形
7、arc
此方法用来进行圆形的占位选择,即跟rect方法一样,在占位后需要通过fill或stroke进行绘制,但它不像矩形有快捷方法,所以你不能使用fillArc或strokeArc,它接收6个参数,按顺序分别为圆心x、y,半径radius,起始和终止弧度 startAngle、 endAngle(注:角度 * π / 180),以及绕哪个时针方向 anticlockwise进行绘制,true为逆时针方向,false为顺时针方向,这里有一个很容易犯错的点要提醒,即弧度0不是从顶部开始而是从右侧开始,如下图
8、moveTo、lineTo、arcTo、quadraticCurveTo和bezierCurveTo
这几个方法都是跟绘制路径相关的,日常中用的比较多的应该是前三个,后俩个方法来绘制二次和三次贝塞尔曲线,这个理解下就好,moveTo的话用来位移你路径的起点,也就是定义你的路径起点,然后通过lineTo绘制直线或arcTo绘制曲线,lineTo接收一个终点坐标,即x和y,用于绘制一条直线,而arcTo会接收俩个点,这里暂且称为ax、ay和bx、by,以及一个圆角大小radius作为参数,首先我们先说下bx和by,它是你想要画的曲线的终点坐标,而ax和ay是你moveTo的起点和终点bx和by的交点,然后arcTo会以此作为圆弧弯曲的顶点,按你指定的圆角大小拉出一个弧线,然后连接你的起点和终点
9、beginPath 和 closePath
在讲完路径方法后这俩其实也是特为重要的俩个方法,大多数情况下大家会在日常代码中比较多的看到这俩成对使用,在绘制路径前以及结束亦或是渲染操作的前后添加,但其实这俩并不是一对配对方法来的,都可以单独使用,beginPath代表接下来要另起一个全新的绘制操作了,比如你想绘制俩条独立的路径线段,又或者你只想填充接下来绘制的图形而已,你就可以用beginPath另起操作,有点像PS的新建图层,这样好理解吧,否则,你下次的路径绘制会从上一次的路径结束点开始又或是你想fill填充一个矩形却把前面绘制的其他图形也一起给填充了。而closePath则会尝试去闭合一条路径,如果起点和终点能够连接起来的话,会将它们进行一个连接,但下一步操作不进行beginPath的话还是会从尾点位置开始的喔务必记得。
好了,在掌握以上几个方法后,现在来到中场休息时间,这里给大家出一个实践练习,尝试做一个俩边圆角为30度的圆角矩形,然后有个小球从左往右滑动的动画,这里给大家15分钟的时间,好好地思考实践下,我相信还是挺有趣的。
OK公布一下我自己的做法,当然我只写了一种而已,其实实现圆角矩形的方式挺多的,而小球的运动方式应该都八九不离十,当然你说你是用曲线arcTo去画的也可以
<canvas id="canvas" width="500" height="500"></canvas><script>var canvas = document.getElementById("canvas");const ctx = canvas.getContext("2d");let tag = true;let x = 125;function drawCircle() {if(x === 275){tag = false} else if(x === 125){tag = true}ctx.clearRect(0, 0, canvas.width, canvas.height);ctx.moveTo(125, 100);ctx.arcTo(300, 100, 300, 125, 30)ctx.arcTo(300, 150, 100, 150, 30)ctx.arcTo(100, 150, 100, 125, 30)ctx.arcTo(100, 100, 300, 100, 30)ctx.stroke()ctx.beginPath()ctx.fillStyle = 'blue'ctx.arc(x, 125, 25, 0, Math.PI * 2, false)ctx.fill()x += tag ? 1 : -1 requestAnimationFrame(drawCircle)}requestAnimationFrame(drawCircle)</script>
10、drawImage
来了,重头api方法来了,相信这个方法应该是除了canvas职业动画师外,普通前端工程师用得最多的方法了吧,因为它能帮我实现图片的裁切以及屏幕截图、模拟手机拍照等多种H5功能,它的功能是在画布中的指定位置以指定大小绘制图片,所以它的参数也分别为绘制的图像资源对象,左上角顶点坐标x、y以及绘制大小width和height
这里就要考考大家个问题,特别是html2canvas截图插件用得比较多的同学
请问,drawImage的图像资源对象可以是哪些对象?
哦豁,我要公布结果了,如果你的答案有除了 canvas、img、video标签外的对象的话,那很遗憾,你已经over了哈哈,canvas的图像资源对象其实只能是任何的 canvas 图像源 ( CanvasImageSource),即HTMLImageElement,HTMLVideoElement,或者 HTMLCanvasElement,其他元素对象你传进去浏览器都会给你报错的。
接着是第二问,请问drawImage能画动图么?
能?不,不能!canvas并不能绘制动图,当你传入一个gif的ImageElement对象时,绘制出来的贴图其实是不会动的,这时有人会问,那为什么canvas可以结合getUserMedia绘制摄像机视频流,来模拟摄像机拍照,这时画布里面的画面是动的,那其实是通过我们的帧动画函数requestAnimationFrame,将我们getUserMedia获取到的视频流一帧一帧的绘制出来,才显示出动图的效果,原理跟我们用canvas绘制帧动画的原理一样,在单个时间点下,又或是一帧,它绘制出来的是一张静态贴图而已。
11、toDataUrl(注意toDataUrl是直接canvas对象调用而不是其上下文context对象调用)
说到drawImage方法,搭配比较多的toDataUrl也就呼之欲出了,它会将我们的画布转换为指定的base64编码后的图片格式,如:image/png、image/jpeg、image/webp,默认是png格式。它接收俩个参数,第一个即你要转换的目标图片格式,第二个则是转换后的图片质量(0-1),默认是0.92来的,正常我们将画布转换为base64编码后,通过其他插件将其转为blob二进制文件就可以将图片文件下载了。
这时有同学会问为啥我设置了质量为1转出来的图片还是很模糊,这也是我们稍后要讲的另一块知识,稍安勿躁,毕竟总不可能这么智能就帮你截出一张2K或4K图你说是吧
那canvas上下文的主要api方法我就大概介绍到这了,当然还有大约一半的api属性和方法我这里没提出来,这些大家感兴趣的话空闲时间可以上网搜搜,都介绍的话可能篇幅就太长太长了。
六、canvas清晰度
说到canvas,特别是日常用它进行截图的时候,大家吐槽最多的无非就是其清晰度,让其堪比手机截图的清晰度肯定是不能的,但也不至于图片的画质像360P的视频画质差不多,其实绝大多数都是出于俩个原因,一个就是我们之前提过的canvas的宽高与css样式的宽高,另一个则是设备像素比devicePixelRatio
1、canvasWidth、canvasHeight与styleWidth、styleHeight
很多人会将canvas画布的宽高与css样式的宽高混淆,首先一张图片绘制进画布时,是基于画布的宽高进行绘制的,比如有个500x500的图片绘制进这个画布
<style> canvas{ width: 300px; height: 300px; } </style><body><canvas width="400px" height="400px"></canvas></body>
则图片会被裁切右边和下边会被裁切掉100px而不是200px,因为其画布容器大小是400而不是300,然后css样式是对整个画布容器进行一个缩放,比如现在300,那最终画布会被缩放至0.75。这也是很多混淆宽高设置而导致最终图片被拉伸或压缩过度导致模糊的原因,当然这是次要原因,接下来则是最最最为主要的原因
2、设备像素比devicePixelRatio
由于canvas绘制的是位图的原因,决定了它绘制的精度固然受设备像素比的影响,那什么是设备像素比,简单的可以理解为设备的屏幕像素可以显示多少个CSS像素,甚至直接就是说放大或缩放了几倍,感兴趣的话下次我可以开一节课来讲这方面的知识,比如设备像素比为1时就是1个屏幕像素对应显示1个CSS像素(宽高1x1),2的话则是1个屏幕像素显示4个css像素(即宽高2x2
),目前情况下我们大多数设备的设备像素比都为2,这个我们可以通过window.devicePixelRatio获取拿到。也就是说当你一张图画在dpr为1时它显示出来的效果是OK的,但是当你画在dpr为2的设备时,由于你没有让绘图跟随倍数放大,相当于在2倍分辨率去显示一张1倍大的图,那必然会导致设备用4个像素去渲染1个CSS像素,那明显就是将你的图片进行了一波拉伸放大,可见地变模糊了。
这里我们来举个例子,比如画一条1px的线,在dpr为2的屏幕上,它会画在俩个设备物理像素中间,但是我们没有显示0.5像素这么一说,所以最后设备会将这俩设备物理像素按机制以附近的像素颜色填满,这也就是为什么我们网页在开发1像素边框时经常被UI设计师说看起来粗了的原因,也是我们canvas图片模糊失真的原因,不单单是被放大,它是根据自身机制来填充的颜色,这也是图片失真重要一点。
所以,另外一个重要的解决方法就是主动将我们的图片按照设备的设备像素比进行放大或缩放,不走设备的缩放填充机制,让其CSS像素比与设备像素比一样,然后再通过样式尺寸对其进行缩放为原尺寸,即可保证图片的最佳质量,当然怎么说都比不上原图的,更何况绘制的尺寸都不一定等于原图尺寸,必然也存在着缩放失真。
七、性能
宏观上来说canvas制作动画等的性能肯定是优于直接用dom+css+js去实现的,这就要涉及到Dom的驻留模式和Canvas的快速模式,由于Dom操作涉及到回流和重绘,在复杂情况下的需要频繁地重新计算布局和样式,性能开销相对较高,而canvas不关心页面的布局和样式,而是在需要时只重绘受影响的部分,相比而已其渲染性能会更为的友好。
但是,当具体到canvas时,不当当只是宏观层面的优化完就万事无忧了,也会有着许多涉及到绘制逻辑影响性能的优化,这也是我以前听说canvas性能很好,但为什么我的页面动画用canvas绘制出来就是那么卡的原因。
1、分层渲染
若涉及到绘制图形比较多的情况,可以将频繁变化的与静态不变的图形分离开,用不同的canvas进行绘制,结合CSS的定位、transform等来复原效果,这样后续只需重绘动态变化的canvas标签即可,减少不必要的绘制操作。
2、避免滥用canvas标签
canvas也像媒体标签(video、audio),并不能滥用,能尽量复用就复用,实例多个会加多对浏览器资源的开销及性能的消耗,卡顿也会油然而生。
3、避免视野之外的绘制
因为其也会消耗性能,当你绘制一张尺寸比画布大的图片,但是超过画布的部分仍会进行绘制计算,所以也会产生不必要的开销,能避免时尽量避免,当然用于裁切图片的情况那就另说啦(这里我借鉴网上一张本地测试的例子给大家看看)
4、受绘制的图形数量及大小的影响
由于canvas绘制是一个一个像素画的,所以只要绘制的时间超过了浏览器渲染一帧的频率,你就会明显感觉到卡顿,所以如果遇到图形数量大或尺寸大的动画,你就好好考虑下是canvas绘制还是做成css帧动画了,这里放几张“知乎——阿飞作者”的测试实践图。
5、减少绘图命令的使用【fill()、storke()等】
尽量在占位多个图形位置后,再一次性调用绘图命令将它们一次性绘制出来,而不是每绘制一个图形立马就调用一次绘图指令,这样也能进一步提升性能,同样还是借用“知乎——阿飞作者”的例子
function drawAnyShape(points) {for(let i=0; i<points.length; i++) {const p1 = points[i]const p2 = i=== points.length - 1 ? points[0] : points[i+1]ctx.fillStyle = 'black'ctx.beginPath()ctx.moveTo(...p1)ctx.lineTo(...p2)ctx.closePath()ctx.stroke()}}
function drawAnyShape2(points) {ctx.beginPath()ctx.moveTo(...points[0])ctx.fillStyle = 'black'for(let i=1; i<points.length; i++) {ctx.lineTo(...points[i])}ctx.closePath()ctx.stroke()}
你会从俩个方法最终运行出来的动画的fps看到很明显的性能提升。
好了,以上就介绍五个比较简单好用的微观、具体到canvas操作上的性能优化,当然还有一些更加深入的优化操作,不过鉴于普通前端开发工程师应该比较少用到,这里就不放出来了,有兴趣的话可以读下这篇知乎文章,总结得很全,特别是对3D前端开发工程师而言。
《你不知道的CANVAS 性能优化几种方式》—— https://zhuanlan.zhihu.com/p/452871005?utm_id=0
八、总结
那以上就是我们日常使用canvas时使用的比较多的一些知识与方法,但也正因为有重新再去理解,才发现自己之前或多或少都存在着一些理解上甚至是使用上的误区,希望这一篇文能真正地让我们对canvas进行一个重新的入门,canvas能绘制的东西很多很多,它其实也是一个深不见底的标签,甚至比video标签更让人探不到底,甚至因为这个标签衍生出了对应的职业,如动画前端开发工程师、3D前端开发工程师等,像近期的浏览器窗口的粒子圆圈交汇动画、以及下面我放出来的烟花爆炸动画等,这些都是通过复杂的绘图指令操作一步步绘制出来,可见其有多生动有趣又高深莫测,笔者后面有时间一定要去好好钻研下,最后以绚丽的烟花愿大家能有所收获,谢谢!
九、参考文献
《canvas中的arcTo()方法》 —— https://www.jianshu.com/p/38f8677f84d1
《你不知道的 canvas(一)》—— https://zhuanlan.zhihu.com/p/581345407
《canvas - 绘制图片,图片变模糊问题解决》—— https://blog.csdn.net/qq_34272760/article/details/123012892
《你不知道的CANVAS 性能优化几种方式》—— https://zhuanlan.zhihu.com/p/452871005?utm_id=0
《用canvas画烟花》—— https://juejin.cn/post/6844903646430232584