项目官网地址:fly-barrage.netlify.app/; 👑🐋🎉如果感觉项目还不错的话,还请点下 star 🌟🌟🌟。
Gitee:gitee.com/fei_fei27/f...(Gitee 官方推荐项目);
Github:github.com/feiafei27/f...;
今天和大家说说顶部、底部弹幕的设计与实现,这块的逻辑相对简单些,为了方便,这里把顶部、底部弹幕统一叫固定弹幕。
1:固定弹幕 left 的计算
固定弹幕从开始渲染到渲染结束,它的 left 属性都是不变的,因为固定弹幕是居中的,所以它的算法如下所示:
typescript
export default class FixedBarrage extends BaseBarrage {
constructor(fixedBarrageOptions: FixedBarrageOptions, barrageRenderer: BarrageRenderer) {
this.calcFixedBarrageLeft();
}
/**
* 计算固定弹幕的 left 属性
*/
calcFixedBarrageLeft() {
this.left = (this.br.canvasSize.width - this.width) / 2;
}
}
使用 Canvas 的宽减去固定弹幕的宽,然后除以 2 即可。
2:固定弹幕 top 的计算
这里我把整个 Canvas 分成上下等高的两部分,上半部分 [0, mid] 只能渲染顶部弹幕,下半部分 [mid, full] 只能渲染底部弹幕。
2-1:重要属性
在源码中,设计了一个专门的类用于固定弹幕的布局计算。
typescript
/**
* 用于处理顶部弹幕和底部弹幕的布局问题
*/
export default class FixedBarrageLayout {
// 用于维护当前渲染的顶部弹幕(从上往下排序)
topRenderBarrages: FixedBarrage[] = [];
// 用于维护当前渲染的底部弹幕(从下往上排序)
bottomRenderBarrages: FixedBarrage[] = [];
}
这个类中最重要的就是 topRenderBarrages 和 bottomRenderBarrages 两个属性,这两个属性是数组类型的,用于维护当前正在渲染的固定弹幕。
2-2:重要方法
接下来说说这个 class 中最重要的两个方法,getRenderFixedBarrages 和 insertFixedBarrage,这两个方法围绕着 topRenderBarrages 和 bottomRenderBarrages 两个数组进行一系列的计算处理。
typescript
/**
- 获取当前应该渲染的固定弹幕
- @param allFixedBarrages 所有的固定弹幕数组
- @param time 视频播放的时间点
*/
getRenderFixedBarrages(allFixedBarrages: FixedBarrage[], time: number): FixedBarrage[] {
// 1:能够渲染的顶部弹幕和底部弹幕
const nextTopRenderBarrages = allFixedBarrages
.filter(barrage => barrage.barrageType === 'top' && time >= barrage.time && time <= barrage.endTime);
const nextBottomRenderBarrages = allFixedBarrages
.filter(barrage => barrage.barrageType === 'bottom' && time >= barrage.time && time <= barrage.endTime);
// 2:移除接下来不用渲染的弹幕
this.topRenderBarrages = this.topRenderBarrages
.filter(barrage => nextTopRenderBarrages.includes(barrage));
this.bottomRenderBarrages = this.bottomRenderBarrages
.filter(barrage => nextBottomRenderBarrages.includes(barrage));
// 3:获取新增的需要渲染的弹幕,之前已经渲染的不用处理,因为之前已经计算过 top 属性了
const newTopRenderBarrages = nextTopRenderBarrages
.filter(barrage => !this.topRenderBarrages.includes(barrage));
const newBottomRenderBarrages = nextBottomRenderBarrages
.filter(barrage => !this.bottomRenderBarrages.includes(barrage));
// 4:遍历新增的弹幕,看能不能插入,能插入的话,计算 top
newTopRenderBarrages.forEach(newTopBarrage => {
this.insertFixedBarrage(newTopBarrage);
});
newBottomRenderBarrages.forEach(newBottomBarrage => {
this.insertFixedBarrage(newBottomBarrage);
});
return [...this.topRenderBarrages, ...this.bottomRenderBarrages];
}
这个方法的作用就是:获取当前应该渲染的固定弹幕,它的计算步骤如下所示:
- 1:先获取能够渲染的顶部弹幕和底部弹幕,这里根据固定弹幕的 barrageType 和 time 计算出当前可以被渲染出的顶部弹幕和底部弹幕;
- 2:移除接下来不用渲染的弹幕,之前渲染的固定弹幕,是保存在 topRenderBarrages 和 bottomRenderBarrages 中的,此时新的 time,之前渲染的弹幕在新的 time 有可能已经不需要渲染了,所以这里需要把不需要渲染的弹幕从数组中移除掉;
- 3:获取新增的需要渲染的弹幕,之前已经渲染的不用处理,因为之前已经计算过 top 属性并且已经维护在数组中了;
- 4:遍历新增的弹幕,看能不能插入,能插入的话,计算 top,能不能插入的逻辑在 insertFixedBarrage 方法中;
- 5:上面的逻辑都执行完了后,topRenderBarrages 和 bottomRenderBarrages 数组中的固定弹幕就是当前应该被渲染出来的弹幕,将他们组合成一个新的数组 return 出去即可,接下来看看 insertFixedBarrage 方法的逻辑;
insertFixedBarrage 方法的源码如下所示:
typescript
/**
* 封装通用的工具方法
* @param barrage 弹幕实例
*/
insertFixedBarrage(barrage: FixedBarrage) {
// 标识 barrage 是否插入成功
let inserted = false;
if (barrage.barrageType === 'top') {
// 如果当前没有渲染的顶部弹幕的话,判断 topRange 能不能插进去
if (this.topRenderBarrages.length === 0) {
if (this.topRangeLength >= barrage.height) {
barrage.top = this.topRange[0];
this.topRenderBarrages.push(barrage);
inserted = true;
}
} else {
// 判断顶部弹幕的间隙能不能插入,遍历存在的顶部弹幕
for (let i = 0; i < this.topRenderBarrages.length; i++) {
const currentTopBarrage = this.topRenderBarrages[i];
// 如果当前是第一个的话,判断第一个上面能否插入
if (i === 0) {
if (currentTopBarrage.top - this.topRange[0] >= barrage.height) {
barrage.top = this.topRange[0];
this.topRenderBarrages.unshift(barrage);
inserted = true;
break;
}
}
// 判断下面能不能插入
// 先计算下面间隙的高度
const belowGapHeight = (i === this.topRenderBarrages.length - 1)
? (this.topRange[1] - currentTopBarrage.top - currentTopBarrage.height)
: (this.topRenderBarrages[i + 1].top - currentTopBarrage.top - currentTopBarrage.height);
if (belowGapHeight >= barrage.height) {
barrage.top = currentTopBarrage.top + currentTopBarrage.height;
this.topRenderBarrages.splice(i + 1, 0, barrage);
inserted = true;
break;
}
}
}
} else {
// 底部弹幕的计算,和顶部弹幕的逻辑一致,所以这里忽略掉
}
}
这块的逻辑很简单,就是遍历相邻固定弹幕之间的间隙,判断这些间隙能不能插入新的固定弹幕,如果能插入的话,便计算对应 top 并维护到数组中,逻辑图如下所示: