起因是新项目需要弄弹幕(盒端),于是在参考了各种大神写的弹幕姬源码和拜读各种文章后,写了一个符合盒端硬件的弹幕姬demo。
ps:刚刚开会,弹幕姬被砍掉了。。。世事无常,虽然被砍了,当然demo还是要整理的,不然大好的提升(水文)机会不是浪费了。
技术选择
现在的弹幕姬要么使用canvas,要么使用dom+css3的方式去实现,考虑到兼容性和平台的性能,最终选择了dom+css3的方式去实现功能。
canvas想要保持30帧以上才能较为流畅,也就是一秒需要执行30次js代码才能让弹幕姬看上去不那么掉帧。且不说一秒三十次脚本会不会阻塞其他代码的执行,盒子端的硬件性能也不允许一秒更新三十次位图,不论是cpu和gpu的带宽还是gpu的渲染能力都不支持那么高的更新频率。
概念
-
弹幕轨道
每个弹幕元素作水平直线运动,也就是说:弹幕元素彼此之间在竖直方向没有发生相对运动,因此弹幕在纵向的间距可通过对容器划分「轨道」进行隔离。每一次的插入弹幕容器的过程都是在寻找合适轨道的过程。 -
相同速度和相同时间
弹幕的速度可以是相同的,也有可能运行时间是相同的,当速度是相同时,相同轨道上的弹幕永远不会重合,也就不会发生弹幕重叠现象。
但是当运行时间相同时,较长的弹幕为了在一定时间内跑完全程,其运行速度就会明显高于较短的弹幕,这个时候就要考虑弹幕重叠的问题,在新弹幕选择轨道时,就要判断这条新弹幕在前一条弹幕运行完之前能否将其追上。如果能追上则会发生重叠。
功能清单
要实现弹幕暂停、运行、清屏、插入等功能。
功能实现
因为需要将弹幕姬嵌在react项目中,所以封装了成了react组件。
组件参数初始化
组件挂载时会根据传入的参数初始化内部参数。
1、trackNum:轨道数量。
2、this.tracks:每条轨道的状态。
3、this.bullets:每条轨道中的最后一条dom。
initOpt() {
const { trackHeight } = this.options;
this.targetPos = this.barrage.getBoundingClientRect();
const trackNum = Math.floor(this.targetPos.height / trackHeight);
this.tracks = this.fillArray(trackNum, 'idle');
this.bullets = this.fillArray(trackNum, []);
this.targetW = this.targetPos.width;
}
fillArray(num, content) {
let i = 0;
let tempArr = []
while (i < num) {
tempArr.push(content)
i++
}
return tempArr
}
弹幕插入
首先要选择或者创建容器,在弹幕容器数量已经上限的情况下,优先选择已经运动完的容器,否则就创建一个新的dom做弹幕容器。
getContainer = ({ duration } = {}) => {
let bulletContainer = null, bulletIndex = null;
// 如果容器已经达到上限,从已有容器中寻找已经运动完的容器塞弹幕
// 结束时间比当前时间小则说明该弹幕已经运动完毕
if (this.deleteBullets.length >= 50) {
let nowTime = new Date().getTime();
let index = 0, bulletsEndTime = this.bulletsEndTime, sigleEndTime = null
while (sigleEndTime = bulletsEndTime[index]) {
if (sigleEndTime < nowTime) {
bulletContainer = this.deleteBullets[index];
bulletIndex = index;
break
}
index++
}
} else {
// 里面要改成dom库调度方式
// 创建单条弹幕的容器
bulletContainer = document.createElement('div');
bulletContainer.id = Math.random().toString(36).substring(2);
bulletContainer.classList.add("bullet-item-style")
bulletContainer.style.animationDuration = duration + 's';
bulletContainer.style.webkitAnimationDuration = duration + 's';
}
return { bulletContainer, bulletIndex };
}
容器创建完后,为了知道它的实际长度,会将其插入到一个临时的容器中,然后通过offsetWidth拿到它的计算值。
this.tempContanier.appendChild(bulletContainer);
this.bulletInfo = {
width: bulletContainer.offsetWidth
}
接下来需要去寻找能能使用的轨道。一条轨道是否合适要看
1、轨道中是否有弹幕。
this.tracks.forEach((v, idx) => v === 'idle' && readyIdxs.push(idx))
if (readyIdxs.length) {
const random = getRandom(0, readyIdxs.length - 1)
index = readyIdxs[random];
this.tracks[index] = 'running'
return index;
}
2、其次要看弹幕是匀速还是非匀速的。当匀速时,只要判断轨道中弹幕的最后一条的最右侧是否已经运行过整体容器的最右侧。
当非匀速时,则需要考虑当前弹幕是否能在追上当前轨道的最后一条弹幕。(如果能追上则这条轨道不符合插入条件,因为此时会出现弹幕重叠)
checkTrack(item) {
const itemPos = item.getBoundingClientRect();
// 轨道中最后一个元素尚未完全进入展示区域,直接跳出
if (itemPos.right > this.targetPos.right) {
return false;
}
// 轨道中最后一个元素已完全进去展示区域
// 速度相同,只要初始条件满足即可,不用去关系追及问题
if (this.options.speed) {
if (itemPos.right < this.targetPos.right) return true;
} else {
// 原弹幕速度
const v1 = (this.targetW + itemPos.width) / +item.dataset.duration;
/**
* 新弹幕
* s2:全程距离
* t2:全程时间
* v2:速度
*/
const s2 = this.targetW + this.bulletInfo.width;
const t2 = this.bulletInfo.duration;
const v2 = s2 / t2
if (v2 <= v1) {
return true;
} else {
// 小学时代的追及问题:t = s / v 比较时间:t1, t2
// 原弹幕跑完剩余全程所需要的时间
const t1 = (itemPos.right - this.targetPos.left) / v1;
// 新弹幕头部跑完全程所需要的时间
const t2 = this.targetW / v2;
// console.log('前面的--->', t1, t2, '后面的时间', v1)
if (t2 < t1) {
return false;
}
}
}
return true;
}
dom插入
最后根据挑选好的轨道的序号,计算该条弹幕的top值,这样这条弹幕就算是:这条轨道上的弹幕了。最终将弹幕插入到容器内部。
renderDom = (container, track, bulletIndex) => {
/**
* container:弹幕容器
* track:跑道索引
*/
if (this.isAllPaused) return; // 如果是全部暂停状态,停止push,停止render
container.dataset.track = track + '';
container.style.top = track * this.options.trackHeight + 'px';
this.barrage.appendChild(container);
};
弹幕暂停和启动
使用的是animationPlayState属性,这个属性是paused的时候,就会暂停animation动画,running时就会开启animation动画。需要做兼容。
this.toggleAnimation('running')
this.toggleAnimation('paused')
toggleAnimation(status) {
this.isAllPaused = false
if (status == "paused") {
this.isAllPaused = true
}
this.deleteBullets.forEach((item) => {
item.style.animationPlayState = status;
item.style.webkitAnimationPlayState = status;
})
}
弹幕上限
由于弹幕是用css3的animation实现,其在运行时会生成占用单独渲染通道的并脱离默认图层的复合图层,当弹幕数量过大时,会占用很大的内存资源,并且复合图层的层叠等级过多时,浏览器在合并每一帧的位图时都会花费更多的时间,所以为了性能考虑,需要对弹幕的上限做规定。
弹幕容器的复用
通常情况下,弹幕运动完后,给人的第一感觉就是这个容器已经出去了,可以被销毁。然而如果每次弹幕出容器都选择将弹幕销毁,那么会频繁的更改dom树,在性能上不被允许。
所以demo复用了弹幕容器,每次新弹幕插入到容器时,都会将其运行的结束时间压栈,当容器达到上限后,新的弹幕进来会用当前时间去栈中遍历,寻找一个已经运行完的弹幕容器,用这个弹幕容器运行弹幕。
结语
就只是写了个demo,写的时候甚至不知道具体需求。最近听大佬们开会后发现上面决定不用弹幕了,为啥咱也不知道,后面如果有机会就继续完善吧。