【前端冷知识】Canvas 滤镜的性能优化

最近几天没有及时更新,是因为这几天在忙一个项目mesh.js,这个项目是一个基于Canvas2D和WebGL的跨平台图形系统,提供底层的高性能API,同时也将是未来新版SpriteJS的底层渲染引擎。

这个项目目前主要API已经全部实现,在整理文档、撰写测试的收尾阶段,有兴趣的同学可以关注它。今天主要讲我们在开发过程中有针对地做的性能优化中的一个小点:Canvas的滤镜优化。

我们知道Canvas是支持滤镜的,几乎支持所有与CSS3滤镜一致的滤镜效果,包括:

  • url

  • blur

  • brightness

  • contrast

  • drop-shadow

  • grayscale

  • hue-rotate

  • invert

  • opacity

  • saturate

  • sepia

要使用这些滤镜也很简单,直接给context设置filter属性即可:

const canvas = document.querySelector('canvas');

const context  = canvas . getContext ( '2d' ) ;

context . filter  = 'blur(5px)' ;

context . fillStyle  = 'blue' ;

context . beginPath ( ) ;

context . arc ( 80 , 80 , 50 , 0 , Math . PI  * 2 ) ;

context . fill ( ) ;

这样就绘制出一个带有模糊滤镜的圆形。

:point_right|type_1_2: 但是,滤镜是一种比较消耗性能的操作,尤其是类似于blur,drop-shadow这样的滤镜,更是消耗性能。如果我们要绘制多个图形,应用同样的blur滤镜,就会明显感到性能的消耗。

我们可以通过例子对比一下:

假设我们要在画布上绘制并刷新200个随机的蓝色小圆点,代码也比较简单:

const canvas = document.querySelector('canvas');

const context  = canvas . getContext ( '2d' ) ;

const count  = 200 ;

// context.filter = 'blur(5px)';

context . fillStyle  = 'blue' ;

function render ( context ) {

context . clearRect ( 0 , 0 , 512 , 512 ) ;

for ( let= 0 ;< count ; i ++ ) {

const pos  = [ Math . random ( ) * 512 , Math . random ( ) * 512 ] ;

context . beginPath ( ) ;

context . arc ( ... pos , 10 , 0 , Math . PI  * 2 ) ;

context . fill ( ) ;

}

}

render ( context ) ;

requestAnimationFrame ( function update ( ) {

render ( context ) ;

requestAnimationFrame ( update ) ;

} ) ;

上面的代码是不带滤镜的效果,可以看到在普通的笔记本电脑的浏览器上,帧率也能轻松达到60fps。

如果我们加一个blur滤镜,把上面代码注释掉的代码的注释去掉让它生效

context.filter = 'blur(5px)';

看到帧率在我的电脑上已经下降到15帧一下,而且电脑风扇猛转。

所以如果我们要绘制多个相同滤镜的图形,普通的设置滤镜的方法,会导致性能开销特别大。那么除了谨慎使用滤镜外,有没有什么办法优化性能呢?

实际上,对这个case,因为连续绘制相同滤镜的图形,我们可以考虑将滤镜绘制合并起来,具体做法是:

  1. 先绘制不带滤镜的图片到一个干净的缓冲canvas上

  2. 将滤镜设置到我们要绘制的目标canvas上

  3. 用drawImage将图片从缓冲canvas绘制到目标canvas上

  4. 将滤镜设置从目标canvas上取消(以继续绘制其他内容,如果有的话)

我们看一下修改后的代码:

const canvas = document.querySelector('canvas');

const context  = canvas . getContext ( '2d' ) ;

const count  = 200 ;

const tempCanvas  = new OffscreenCanvas ( canvas . width , canvas . height ) ;

const tempContext  = tempCanvas . getContext ( '2d' ) ;

tempContext . fillStyle  = 'blue' ;

function render ( context , tempContext ) {

tempContext . clearRect ( 0 , 0 , 512 , 512 ) ;

for ( let= 0 ;< count ; i ++ ) {

const pos  = [ Math . random ( ) * 512 , Math . random ( ) * 512 ] ;

tempContext . beginPath ( ) ;

tempContext . arc ( ... pos , 10 , 0 , Math . PI  * 2 ) ;

tempContext . fill ( ) ;

}

context . clearRect ( 0 , 0 , 512 , 512 ) ;

context . filter  = 'blur(5px)' ;

context . drawImage ( tempContext . canvas , 0 , 0 ) ;

context . filter  = 'none' ;

}

render ( context , tempContext ) ;

requestAnimationFrame ( function update ( ) {

render ( context , tempContext ) ;

requestAnimationFrame ( update ) ;

} ) ;

在上面的代码里,我们创建了一个用来绘制不带滤镜的图片的OffscreenCanvas,然后先将图形绘制到这个tempCanvas上,绘制完成后,再将tempCanvas整个图像以带滤镜的方式绘制到原canvas上,这样, 我们把多次计算滤镜的操作合并到了1次 ,从而大大提升了性能。

我们看到,这么做之后,帧率回到了60fps。

当然,这种做法实际上只是一种 近似 绘制,严格上来说,两种绘制是有区别的,但是在blur滤镜上基本是没有问题的,而在其他个别滤镜,例如drop-shadow滤镜,实际上是有问题的,我们用两种写法看一下:

const canvas = document.querySelector('canvas');

const context  = canvas . getContext ( '2d' ) ;

context . filter  = 'drop-shadow(5px 5px)' ;

function render ( context ) {

context . clearRect ( 0 , 0 , 512 , 512 ) ;

context . fillStyle  = 'blue' ;

context . beginPath ( ) ;

context . rect ( 100 , 100 , 100 , 100 ) ;

context . fill ( ) ;

context . fillStyle  = 'red' ;

context . beginPath ( ) ;

context . rect ( 50 , 50 , 100 , 100 ) ;

context . fill ( ) ;

}

render ( context ) ;

const canvas = document.querySelector('canvas');

const context  = canvas . getContext ( '2d' ) ;

const tempCanvas  = new OffscreenCanvas ( canvas . width , canvas . height ) ;

const tempContext  = tempCanvas . getContext ( '2d' ) ;

function render ( context , tempContext ) {

tempContext . clearRect ( 0 , 0 , 512 , 512 ) ;

tempContext . fillStyle  = 'blue' ;

tempContext . beginPath ( ) ;

tempContext . rect ( 100 , 100 , 100 , 100 ) ;

tempContext . fill ( ) ;

tempContext . fillStyle  = 'red' ;

tempContext . beginPath ( ) ;

tempContext . rect ( 50 , 50 , 100 , 100 ) ;

tempContext . fill ( ) ;

context . clearRect ( 0 , 0 , 512 , 512 ) ;

context . filter  = 'drop-shadow(5px 5px)' ;

context . drawImage ( tempContext . canvas , 0 , 0 ) ;

context . filter  = 'none' ;

}

render ( context , tempContext ) ;

可以看到两个渲染结果并不一样,这是显然的,drop-shadow这样的滤镜,合并渲染会使得两个图形重叠的交界不会形成阴影,所以这种情况,如果要获得正确的效果,那就只能牺牲性能,不能合并滤镜了。

以上是今天讨论的内容,关于Canvas的滤镜优化,还有什么问题,欢迎在issue中讨论。

在后续文章中,我们有机会进一步讨论在WebGL上基于Shader实现的滤镜和性能的优化。

最后插播一条广告:

我将于9月21日去成都FEDay分享主题《你不知道的GPU——前端、图形系统与数据可视化》

有兴趣的同学可以去参加~ 报名地址:https://fequan.com/2019

关于奇舞周刊

《奇舞周刊》是360公司专业前端团队「 奇舞团 」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章