从globalCompositeOperation
到蒙版弹幕
globalCompositeOperation
属性
Canvas 有一个不那么常用的属性globalCompositeOperation
,作用是设置如何将一个源图像绘制到目标图像上。该如何理解?
在使用Canvas绘制图像时,我们可以多次调用ctx.drawImage()
或者是其它绘图函数ctx.fillRect()
等进行绘制。而globalCompositeOperation
属性就指定了当前将要绘制的图像在画布上如果和已绘制的图形重合该怎样显示。该属性有多个可选值:
source-over
默认值,目标图像重合部分将显示在上方(源图像被覆盖)。source-in
目标图像中显示源图像。源图像只显示重合的部分,目标图像透明。source-out
在目标图像之外显示源图像。只会显示非重合的部分,目标图像透明。lighter
显示源图像+目标图像。copy
只显示源图像。xor
亦或。destination-*
例如:当设置属性值为source-over
时,下例将会显示为:1
2
3
4
5
6
7const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 80, 40);
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'green';
ctx.fillRect(20, 20, 80, 40);
可以看到,后绘制的绿色图像(源图像)和先绘制的红色图像(目标图像)发生了重叠,而源图像在层叠上层。
再来看看属性值为source-in
的情况:
源图像和目标图像发生了重合,结果只显示了源图像的重合部分。
蒙版弹幕
不考虑弹幕内容、显示等因素,使用Canvas
实现弹幕就是一个不断擦除和绘制的过程,弹幕本身是绘制在画布上的,和内容(视频、图片等)是分层显示的,并无直接关系。
Canvas
弹幕且不论性能,如果弹幕过多往往会挡住内容本身,体验并不好。B站的弹幕使用了名为蒙版弹幕的技术,这种技术可以让弹幕不遮挡内容主体。这里不讨论B站的蒙版弹幕是如何实现的,先来看看CSS
中一个名叫mask
的属性。
mask
属性用来设置遮罩,那么何为遮罩呢?简单点来说就是使用一张图片来遮住另一张图片,并且如果用于遮罩的图片包含透明的部分,透明部分将会被遮住,非透明部分将会显示为被遮罩图片的内容。
mask
的内容到此为止,是不是和globalCompositeOperation = 'source-in'
很像?其实我认为不是很像。
修改上面的例子,如果将两个图形绘制的区域完全重合,那么设置globalCompositeOperation = 'source-in'
后,不出意外,源图像将会完全覆盖目标图像。
如果我们把目标图像和源图像均换成两张等宽高的图片,那么源图片将会完全遮挡目标图片。
如果目标图像和源图像存在透明区域(RGBA 中 Alpha 为0
的区域),那么源图像会完全遮住目标图像,但是目标图像的透明区域仍然是透明的。
如果反向抠图后,目标图像只有主体是透明的,那么源图像将会覆盖目标图像的非主体区域,主体区域由于是透明的,无能为力。
把目标图像换成蒙版图片,把源图像换成包含弹幕的图像,那么,蒙版图像透明区域不会被覆盖。
此时再把覆盖后的图片渲染在Canvas
上,大功告成。
回顾一下globalCompositeOperation = 'source-in'
的解释:
目标图像中显示源图像。源图像只显示重合的部分,目标图像透明。
目标图像会被完全覆盖,而源图像只显示重合的部分,由于透明区域并不属于目标图像,所以在透明区域并不会显示源图像。
先来看一个例子:
1 | ctx.fillStyle = 'red'; |
在300
*150
的画布上先绘制两个红色的三角形,作为目标图像,此时画布中间的区域是透明的。
1 | ctx.globalCompositeOperation = 'source-in'; |
然后再绘制一个充满画布的矩形覆盖到目标图像上,此时的结果是这样的:
目标图像(两个红色的三角形)已经被完全覆盖了,而透明区域仍然透明。
简易实现
为了实现蒙版弹幕,需要准备:
- 原版图像
- 蒙版图像
- Canvas弹幕
这里原版图像使用下面这张菊花图:
经过抠图(主体变成透明),生成的蒙版图片如下:
弹幕系统的实现不做过多的介绍,这里只关注绘制,和上例的绘制过程一致,首先绘制蒙版图像,再绘制弹幕内容到蒙版上进行覆盖。
1 | drawBarrages() { |
上面的代码是完整绘制一帧的逻辑,跳过绘制弹幕文本的循环,关注整体绘制和层叠逻辑。其中绘制总分为四步,擦除上一帧结果,绘制蒙版(作为目标图像),设置compose
类型为source-in
,绘制弹幕(作为源图像)。
注意到,蒙版图像使用ctx.putImageData
绘制,这表示蒙版图像是ImageData
类型,可以通过ctx.getImageData()
拿到(需先绘制在画布上),这里也可以png
图片作为蒙版图片直接绘制在画布上。
this.ctx.drawImage(this.bgCanvas, 0, 0, this.width, this.height)
用于绘制弹幕,this.bgCanvas
是绘制了弹幕文本的Canvas
画布,这里使用了离屏Canvas 技术,该画布并不会单独绘制在屏幕上。
绘制结果如下图:
可以看到,弹幕并没有遮挡我们的主体(菊花),实现了蒙版弹幕的预期效果。
尾巴
我们的确实现了蒙版弹幕,其主体是使用Canvas绘制蒙版图片和弹幕在同一张画布上,并且设置globalCompositeOperation = 'source-in'
来达到弹幕完全覆盖蒙版的效果。
简易实现的内容主体是一张静态的图片,如果要实现视频蒙版弹幕效果,需要提供每一帧的蒙版图像。在渲染某一帧时,最终的结果使用该帧的蒙版图像和实时弹幕组合而成。
其实蒙版弹幕的关键是提供蒙版图像,对于单个图片还好,我们可以针对这张图片单独制作一张蒙版图像。但是对于视频蒙版弹幕,我们需要逐帧生成蒙版图像,工作量之大可想而知。并且如何生成蒙版图像,如何标注主体是关键中的关键。这一部分理应借助机器学习,进行图像识别和分割。
一个可行的办法是事先针对每个视频,先逐帧生成蒙版图像,在进行流媒体播放的时候同时传递蒙版图像,最终在前端进行组装,完成视频蒙版弹幕。
完。