# 实现原理
主要通过面向对象的方式进行实现。
# 弹幕不重叠
弹幕库的实现核心基本都是围绕着如何保证弹幕不重叠展开的。在 Bullet-Chat-Everywhere 中,我们选择的方案如下:
- 将浏览器窗口的高度均分为八等份,每一份都是一条通道。每条通道维护一个 occupied 变量,表示当前通道能否加入新的弹幕。
- 设置一个等待栈(因为要先进后出),当有新的弹幕出现时,先把弹幕加入等待栈。每一轮渲染,根据等待栈和通道的占用情况决定通道是否可以加入新的元素,另外还要判断当前通道是否在允许的通道之中。若开启顶部弹幕,则允许 [0, 1];若开启中部弹幕,则允许 [2, 3, 4, 5];若开启底部弹幕,则允许 [6, 7]。这样就可以灵活地进行弹幕位置的更改,如果需要进行拓展也很方便。
// 比较对象:每条通道的最后一个元素 & 等待栈的栈顶元素
// 如果最后一个元素满足 x 坐标 + 元素宽度 + 弹幕间隔 <= 浏览器窗口宽度,则说明当前通道可以加入新的弹幕
if (
i === channel.length - 1 &&
this.waitStack.length &&
window.innerWidth >=
channel[i].x + channel[i].width + BARRAGE_PADDING
) {
// 修改通道占用情况
this.channelPositions.occupied[index] = true;
// 从等待栈中取出元素塞入通道中
const shiftBarrage = this.waitStack.shift() as Barrage;
channel.push(shiftBarrage);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 弹幕绘制
我们先定义一个整体弹幕的绘制函数,这个函数负责所有弹幕的绘制,通过渲染函数递归调用 requestAnimationFrame()
来确保渲染的高效。注意此处是递归调用,而且需要绑定 this。
private render() {
// 先清空画布
this.clearCanvas();
// 执行弹幕的渲染
this.renderPerTime();
// 判断是否暂停
if (!this.isPaused) {
requestAnimationFrame(this.render.bind(this));
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
有了整体的渲染函数,我们就要处理单条弹幕数据的渲染。主要原理是使用 canvas 的 fillText(content, x, y)
函数进行弹幕的绘制,通过 clearRect(x, y, width, height)
进行画布清除。
// 绘制
this.context.fillText(channel[i].content, channel[i].x, channel[i].y);
1
2
2
// 清除
private clearCanvas() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
1
2
3
4
2
3
4
每次渲染完成还需要进行弹幕的移动,即减小弹幕的 x 坐标。当弹幕已经离开窗口之后,大多数情况下我们不能直接将弹幕从存储中删除,因为我们需要支持用户可能的时间回退操作,比如说将视频进度条拉回去,比如说像本项目这样进行弹幕的重播。此时我们可以标记一个 outOfWindow 属性,初始化为 false,如果弹幕离开了窗口就将其标记为 true,渲染时进行判断,若 outOfWindow 为 true 则不进行渲染。
← 插件使用说明