前言
Canvas
作为H5
的的一个核心功能,在包括可视化和在线编辑等功能上发挥了很大的作用。想要熟练的使用它同样也是难度不小,作为一个经验不多的小菜鸡来说,由于工作中也会经常接触到Canvas
,因此这里就总结一些个人会用到的一些小技巧给大家,希望对你有所帮助!
实用技巧
1. 双缓存
其实双缓存是使用Canvas
使用到的比较高频的技巧,主要是用于解决Canvas
绘制时画面闪烁或者说白屏的问题。
原理分析
在学习这个技巧前,我们首先来分析下,为什么Canvas
会出现闪烁或者白屏的问题。我们先看看正常情况下,我们是怎么把图片渲染到Canvas
上去的:
- 获取
Image
对象,并等待它加载完成 - 清除上一帧的画布
- 调用
ctx.drawImage
接口绘制图片 按理说,清除画布和绘制图片都是同步的过程,那么为什么会出现空白或者闪烁的问题呢?其实涉及到了浏览器的渲染机制。
我们知道浏览器有个刷新频率,一般是每秒60
帧,也就是16.7
毫秒重绘一次。那么当我们在清空画布并绘制图片的这个时间间隔内,如果绘制图片的时间超过了16.7
毫秒,那么就会导致浏览器发生了重绘。
此时画布已经清空了,但新的图片还没绘制完成,就造成了视觉上的闪烁或者空白。
那么根据这个流程,我们可以从绘制时间这个角度继续分析,为什么这个过程会超过渲染帧的时间呢?我们看canvas绘制的接口不外乎两个:
drawImage
putImageData
而绘制时间慢的罪魁祸首就在于drawImage
这个接口(附上MDN链接),因为它能够将任意大小的图片在画布上绘制成任意大小。
那么这个任意大小就意味着,Canvas
在绘制图片的过程中必然要对图片进行缩放或者截取,这就导致了效率的下降,也就造成了绘制时间超过了渲染帧。
使用双缓存
那找到了罪魁祸首以后就可以对症下药了,既然渲染过程慢是因为对图片大小缩放和压缩导致的,那我们只要保证大小一致就能确保绘制的速度了。
因此我们通过双缓存的方式来解决这个问题,通过另外创建一个和主画布等大的缓冲Canvas
画布,需要渲染的图片先绘制到缓冲画布上,然后将缓冲画布的数据直接绘制到主画布上,就解决了闪烁的问题。
这里我们直接上代码:
function drawImage(url, mainCanvas) {
//1. 第一步加载图片
const img = new Image();
img.src = url;
img.onload = () => {
//2.第二步,将图片绘制到缓存画布上,缓存画布临时创建和存储起来都可以
const cacheCanvas = document.createElement("canvas");
//画布设置为等大
cacheCanvas.width = mainCanvas.width;
cacheCanvas.height = mainCanvas.height;
//这里是绘制图片的逻辑,自适应或者拉伸取决于自己需要,为了方便这里就拉伸了
cacheCanvas.getContext("2d").drawImage(img, 0, 0, cacheCanvas.width, cacheCanvas.height);
//3.第三步,把缓存画布的内容绘制到主画布上
const mainCtx = mainCanvas.getContext("2d");
//先清空上一帧
mainCtx.clearRect(0, 0, mainCanvas.width, mainCanvas.height);
//绘制画面
mainCtx.drawImage(cacheCanvas, 0, 0);
}
}
2. 线条发散处理
在绘制一些规则的几何图形和线条的时候,可能会发现边缘或者线条明明宽度应该是1px
,但结果却显示成了2px
,而且颜色也更淡。
这其实跟Canvas
绘制线条的规则有关,把屏幕像素点看成一个个网格,那么Canvas
绘制线条时会以网格的交错点作为起点,因此奇数线宽的线条,往往宽度会被拆分到两边的网格之中。
又因为屏幕显示像素最小以1px
为单位,因此两边的像素就会被自动补足发散。
理论结果
实际结果
解决方案
面对这种情况,我们可以做个特殊处理,如果线宽为奇数的情况下,我们就将绘制点偏移0.5px
,最终的结果就不会发散了。
3. 不规则遮罩
有些时候我们可能需要利用Canvas
实现一个遮罩的效果,比如裁剪框、刮刮乐。
对于规则图形,我们可以通过先绘制底部图案,然后调用clearRect
的方法来擦除遮罩区域,但对于不规则的区域的话就没办法实现了。因此对于不规则图案,我们需要用到Canvas
的一个功能来实现遮罩,那就是clip
。对应的接口文档:CanvasRenderingContext2D.clip()
实现思路
实现的思路大致分为四步:
- 绘制作为遮罩的背景(比如纯黑、或者一张图片)
- 绘制需要露出来的形状(比如裁出一个五角星)
- 调用
clip
接口裁出这块区域 - 最后,如果需要镂空的话直接调用
clearRect
擦除画布即可;如果需要在镂空处绘制别的图案,可以在基础上调用绘制方法。
具体代码
function drawMask() {
const cvs = document.createElement("canvas");
const ctx = cvs.getContext("2d");
cvs.width = 500;
cvs.height = 500;
//1. 绘制遮罩背景,这里使用纯黑
ctx.fillStyle = "#000";
ctx.fillRect(0, 0, 500, 500);
//2. 绘制自定义的遮罩形状,这里用三角形比较简单
ctx.beginPath();
ctx.moveTo(250, 100);
ctx.lineTo(375, 300);
ctx.lineTo(125, 300);
ctx.closePath();
//3. 调用clip接口裁出图形
ctx.clip();
//4. 擦除裁出的图形区域,这里只要保证擦除的区域包括裁剪区域即可
ctx.clearRect(0, 0, 500, 500)
}
最终效果
4. Blob和ImageData数据的处理
有些场景下,你可能需要根据图片获取对应的ImageData数据或者Blob数据,传到后端做一些图像处理。这里就介绍下如何通过Canvas
获得这两种数据,并且互相转换
获取ImageData
这里我们可以借助Canvas
自带的getImageData
接口获取ImageData
数据。(Api
的功能介绍直接看MDN就行了 CanvasRenderingContext2D.getImageData())。这里我们直接上代码:
function getImageData(url) {
return new Promise((res, rej) => {
//根据url创建出img对象来
const img = new Image(url);
img.src = url;
img.onload = () => {
//创建临时的canvas元素,用于获取imageData数据
const tempCanvas = document.createElement("canvas");
//将canvas设为和图片等大
tempCanvas.width = img.naturalWidth;
tempCanvas.height = img.naturalHeight;
//绘制图片,并获取imageData数据
const ctx = tempCanvas.getContext("2d");
ctx.drawImage(img, 0, 0);
res(ctx.getImageData(0, 0, tempCanvas.width, tempCanvas.height))
}
img.onerror=(err)=>{
rej(err);
}
})
}
通过将图片绘制到Canvas
上,再调用ctx.getImageData
接口,我们就得到想要的ImageData
数据了。
获取Blob
获取Blob
数据可以借助Canvas
自带的toBlob
接口获取,(同样放上Api
的链接HTMLCanvasElement.toBlob())。同样直接上代码:
function getBlob(url, type, quality) {
return new Promise((res, rej) => {
//根据url创建出img对象来
const img = new Image(url);
img.src = url;
img.onload = () => {
//创建临时的canvas元素,用于获取imageData数据
const tempCanvas = document.createElement("canvas");
//将canvas设为和图片等大
tempCanvas.width = img.naturalWidth;
tempCanvas.height = img.naturalHeight;
//绘制图片,并获取imageData数据
const ctx = tempCanvas.getContext("2d");
ctx.drawImage(img, 0, 0);
tempCanvas.toBlob(res, type, quality);
}
img.onerror = (err) => {
rej(err);
}
})
}
用法基本和获取ImageDta
类似,但最后是通过toBlob
接口获取,同时可以设置数据类型和质量,相当于自带压缩了。
Blob
转ImageData
其实本质上还是借助Canvas
的几个接口,直接上代码:
function blobToImgData(blob) {
const url = URL.createObjectURL(blob);
return getImageData(url);
}
你没看错!借助前面封装好的接口,其实只要两行代码就搞定了。
ImageData
转Blob
这个过程稍微多几步,原理也是一样的:
function imgDataToBlob(imgData, type, quality) {
return new Promise((res) => {
//创建临时的canvas元素
const tempCanvas = document.createElement("canvas");
//将canvas元素设为图片等大
tempCanvas.width = imgData.width;
tempCanvas.height = imgData.height;
//绘制数据
tempCanvas.getContext("2d").putImageData(imgData, 0, 0);
//转成blob数据
tempCanvas.toBlob(res, type, quality);
})
}
5. 局部擦除重绘
当使用Canvas
用于一些绘制图形较多的场景式,比如游戏、或者图形编辑器等等,需要频繁重绘,而每一次重绘都需要对整个画布所有的内容重新绘制,难免会造成很大的浪费。
很多时候其实需要重绘的图形比较独立,只需要重绘本身即可,因此如果能采用局部擦除重绘的方式的话,自然能节省渲染的成本。
不同的渲染情况
我们分析一下可能遇到的不同的渲染情况:
完全独立
这种是最简单的情况,我们只需要擦除掉图形2
自身的区域,然后重绘图形2
即可。
两个独立的图形重叠
当图形2
与图形3
发生了重叠,那我们在重绘图形2
的同时势必要重绘图形3
。 如果只针对这种情况的话,简单点的做法是,直接擦除掉图形2
和图形3
组成的整个包围盒区域,然后全部重绘即可。
多个图形连续重叠
这种情况其实是最头疼的,如果按照上一种情况的做法,大致思路会是这样:
图形2
需要重绘,判断有无图形重叠图形3
和图形2
重叠,因此要重绘图形3
,并判断有无图形和图形3
重叠图形1
和图形3
重叠,因此要重绘图形1
,并判断有无图形和图形1
重叠- 擦除
图形1、2、3
形成的包围盒区域,同时重绘图形1、2、3
可以看到,这样的话当图形数量巨大,且大多重叠的时候,很可能经过了大量的计算以后,最终的结果还是要重绘所有的图形。
因此,我们需要采用另一种方式,借助我们刚刚提到的一个Canvas
相关的接口,clip
来实现。
如何实现局部擦除重绘
我们可以通过clip
接口裁剪出需要重绘的图形区域,将重绘的影响限制到图形自身的区域范围内。
比如这种情况下,我们需要重绘图形2
,那我们要做的其实就这么几步:
- 利用
clip
裁剪出图像2
的绘制区域 - 调用
clearRect
擦除这块区域 - 重绘和这块区域重叠的所有图形
比起之前的做法的优势在于,如果和图形2
重叠的图形上还有重叠的图形,我们就不需要考虑了,只考虑和图形2
重叠的图形即可。
总结
这篇文章主要总结了5个实用的Canvas
使用技巧,要结合实际场景进行使用,相信用得好的话一定会对你有所帮助~
另外,这里举得都是我个人有使用过并觉得比较实用的,如果有错误或者遗漏的地方欢迎指出~ 如果你有别的实用技巧的话也可以分享~
写在最后
- 很感谢你能看到这里,不妨点个赞支持一下,万分感激~!
- 以后会更新更多文章和知识点,感兴趣的话可以关注一波~