经常刷腾讯视频、爱奇艺、Bilibili的小伙伴,会发现,这些网站都提供了一种"智能防挡弹幕"的功能,效果如下图所示。
那这些视频网站是如何实现的呢?
答案就是依靠 Mask。
什么是 Mask ?
Mask是遮罩/蒙版。Mask 属性允许使用者通过遮罩或者裁切特定区域的图片的方式来隐藏一个元素的部分或者全部可见区域。
接下来主要介绍遮罩/蒙版的原理、实现方式、弹幕应用。
遮罩/蒙版原理
- 遮罩: 有色则可见(任何颜色),无色则不可见,半透明则半透明
- 蒙版: 黑色则不可见,白色则可见,非黑色白色则半透明
可见 | 不可见 | 半透明 | 实现方式 | |
---|---|---|---|---|
遮罩 | rgb(0 ~ 255, 0 ~ 255, 0 ~ 255) | — | rgba(0 ~ 255, 0 ~ 255, 0 ~ 255, 0 ~ 1) | css (mask 属性)canvas (globalCompositeOperation 设置) |
蒙版 | rgb(255, 255, 255) | rgb(0, 0, 0) | rgb(0 ~ 255, 0 ~ 255, 0 ~ 255) | svg (mask 元素) |
其实两者概念上非常容易混淆,但实际最终效果是一样的,仅需记住哪个是无色不可见,哪个是黑色不可见。
遮罩
canvas globalCompositeOperation 属性设置或返回如何将一个源(新的)图像绘制到目标(已有的)的图像上。
源图像 = 您打算放置到画布上的绘图。
目标图像 = 您已经放置在画布上的绘图。
(function(){
var imagecanvas = document.createElement('canvas');
var imagecontext = imagecanvas.getContext('2d');
/* uncomment do see the canvas to debug
document.body.appendChild(imagecanvas);
*/
window.addEventListener('load', function(){
[].forEach.call(document.querySelectorAll('.mask'), function(img){
var newImg = document.createElement('img');
newImg.src = img.src;
newImg.onload = function() {
var width = newImg.width;
var height = newImg.height;
var mask = document.createElement('img');
mask.src = img.getAttribute('data-mask');
mask.onload = function() {
imagecanvas.width = width;
imagecanvas.height = height;
imagecontext.drawImage(mask, 0, 0, width, height);
// source-in 重叠部分显示
imagecontext.globalCompositeOperation = 'source-in';
imagecontext.drawImage(img, 0, 0);
img.src = imagecanvas.toDataURL();
}
}
});
}, false);
})();
蒙版
Svg 可以用 mask 来实现 蒙版
<svg width="300" height="250" viewBox="0 0 300 250" xmlns="http://www.w3.org/2000/svg">
<defs>
<rect id="rect" width="100" height="100" fill="green" />
<!-- 白色的三角形蒙版 -->
<path id="triangle" d="M50 0 L100 100 L0 100 Z" />
<mask id="mask">
<use xlink:href="#triangle" fill="rgb(255,255,255)" />
</mask>
</defs>
<use xlink:href="#rect" x="10" y="10" mask="url(#mask)" />
<!-- 本来是方形,显示出来是绿色的三角形 -->
</svg>
如果 mask 没有设置
fill
,或者设置fill="none"
( 表示无色 ),则不显示;如果有色但非黑白色,则按色值计算透明度;
弹幕应用
前面,我们通过一些案例,简单说明了遮罩、蒙版的作用和概念,相信大家有所理解。
接下来我们将用Canvas globalCompositeOperation(遮罩) 实现一个简易版"智能防挡弹幕"。
页面容器布局
<!-- 弹幕容器 -->
<div class="barrage-container">
<!-- 弹幕列表 -->
<div class="barrage-item">哈哈哈哈</div>
<div class="barrage-item">不错不错</div>
<div class="barrage-item">v5v5v5v5</div>
</div>
<!-- 遮罩容器 -->
<canvas id="canvas"></canvas>
基于AI人脸分割模型人脸坐标实现
function main() {
// 人脸坐标字符串 0:空白 1:人脸
// 256 x 144 的图像规格
const str = `00000000000000111111111111111100011111111111100......11100000000000000111111100000`;
// 人脸识别 width x height
const WIDTH = 256;
const HEIGHT = 144;
function getCanvas(width, height) {
const canvas = document.getElementById('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
}
function doMasking(canvas, pixels) {
const ctx = canvas.getContext('2d');
for (let x = 0; x < WIDTH; x++) {
for (let y = 0; y < HEIGHT; y++) {
const pixelIndex = y * WIDTH + x;
// 如果数据有问题, 抛错
if (pixelIndex >= pixels.length) {
throw new Error('原始数据有问题');
}
const shouldMask = pixels[pixelIndex] === 0;
if (shouldMask) {
ctx.fillRect(x, y, 1, 1);
}
}
}
// ==== 添加遮罩 ====
ctx.globalCompositeOperation = 'source-in';
ctx.fillStyle = 'green';
ctx.fillRect(0, 0, 256, 144);
}
// ==== 主体逻辑 ====
const pixels = str
.split('')
.filter(i => i !== '\n')
.map(i => ~~i);
const canvas = getCanvas(WIDTH, HEIGHT);
doMasking(canvas, pixels);
// ==== 输出图像 ====
const container = document.querySelector('.barrage-container');
const dataUrl = canvas.toDataURL('image/png');
container.style['-webkit-mask-image'] = `url(${dataUrl})`;
container.style['mask-image'] = `url(${dataUrl})`;
}
main();
页面最终效果
要明确的是,使用 mask,不是将弹幕部分给遮挡住,而是利用 mask
,指定弹幕容器之下,哪些部分正常展示,哪些部分透明隐藏。