easy-barrage
简单实现一个弹幕库
背景
将文字内容以弹幕墙的形式展示在页面上,需要无限循环滚动播放。
在网上查找一番之后找不到满足需求的库,于是自己动手实现。
tips:该库实现的比较简单,针对特定的需求,需要扩展可以在此基础上自行扩展,或者提出issue,一起学习,共同进步。
基本实现原理
1、根据每条弹幕高度和弹幕展示区域的高度计算出弹幕的行数(称为跑道) laneCount
2、定义一个跑道的状态映射 laneStatus 用来表示该跑道当前的状态 空闲(true)or 占用(false)
3、定义一个数组 data 存储弹幕数据;data 每一个元素是一条弹幕
4、定义一个弹幕状态的映射 itemStatusMap 用来表示该弹幕当前的状态 正在展示中(true)or 不在展示中(false)
4、每隔1秒循环一次跑道(laneStatus),如果有空闲状态的跑道再从弹幕数组(data)中取出一条数据开始渲染,从弹幕展示区域右边进入
5、在弹幕开始进入弹幕展示区域时,将该跑道状态置为false,在完全进入弹幕展示区域后,将该跑道状态置为true
6、在弹幕完全滚出弹幕展示区域时,将该弹幕dom节点删除,并将该弹幕状态置为false
附上效果

使用方法
将 src 目录下的 barrage.js 文件复制下来(dist目录下的 barrage.js 为打包后的),通过import方式引入,使用方式如下:
import Barrage from '../src/barrage'
import { barrageData } from './data'
const el = document.getElementsByClassName('container')[0]
const barrage = new Barrage({
container: el, // 必填 弹幕容器
data: barrageData, // 弹幕数据
barrageHeight: 26, // 必填 弹幕的高度 单位px
speed: 3, // 速度参数 可选值为1到5 数值越大速度越快 默认为3
showAvatar: true, // 是否需要显示头像 默认不显示
infinite: true // 是否无限循环 默认是
})
barrage.init()
附上主要源码
class Barrage {
constructor (options) {
const data = Object.assign({}, {
data: [],
barrageHeight: 26,
speed: 3,
showAvatar: false,
infinite: false
}, options)
this.container = data.container
this.data = data.data
// 弹幕状态映射
this.itemStatusMap = new Array(this.data.length).fill(true)
this.itemHeight = data.barrageHeight
this.speed = data.speed * 0.0293
this.showAvatar = data.showAvatar
this.infinite = data.infinite
}
init () {
// 初始化弹幕跑道
this.initLane()
// 弹幕初始化
this.initTimer()
}
initTimer () {
if (this.data.length > 0) {
this.timer = setInterval(() => {
for (let i = 0; i < this.laneCount; i++) {
const lane = this.getLane()
const pointer = this.getOneBarrage()
// 非无限轮播时,弹幕全部滚动结束的情况
if (pointer === -1 && !this.infinite) {
clearInterval(this.timer)
return
}
if (lane > -1 && pointer > -1) {
this.laneStatus[lane] = false
this.initItemDom(pointer, lane)
this.itemStatusMap[pointer] = false
}
}
}, 1000)
}
}
initItemDom (pointer, lane) {
const data = this.data[pointer]
const el = document.createElement('div')
el.classList.add('barrage')
const startLeft = this.containerWidth + Math.floor(Math.random() * 50)
const top = lane * this.itemHeight + lane * this.gap
el.style = `top: ${top}px; left: ${ startLeft }px; height: ${this.itemHeight}px;border-radius: ${this.itemHeight / 2}px`
el.innerHTML = `${this.showAvatar ? '<img class="avatar" src="' + data.avatar + '"/>' : ''}
<p class="content">${data.text}</p>`
this.container.appendChild(el)
const width = el.offsetWidth
const animateTime = Math.ceil((width + startLeft) / this.speed)
this.animate(el, startLeft, -width, animateTime, () => {
if (this.infinite) {
this.itemStatusMap[pointer] = true
}
el.remove()
})
const inScreenTime = animateTime / (width + this.containerWidth) * width
setTimeout(() => {
this.laneStatus[lane] = true
}, inScreenTime)
}
initLane () {
this.containerWidth = this.container.clientWidth
this.containerHeight = this.container.clientHeight
this.laneCount = Math.floor(this.containerHeight / this.itemHeight)
this.laneStatus = new Array(this.laneCount).fill(true)
// 上下弹幕之间的间隙
this.gap = Math.floor(this.containerHeight % this.itemHeight / (this.laneCount - 1))
}
getLane () {
let lane = Math.floor(Math.random() * this.laneCount)
let times = 1
while (times < this.laneCount) {
if (this.laneStatus[lane]) {
return lane
} else {
lane === this.laneCount - 1 ? lane = 0 : lane++
times++
}
}
return -1
}
getOneBarrage () {
return this.itemStatusMap.findIndex(item => item)
}
animate (el, start, des, duration, callback) {
// start 动画初始值
// des 动画结束值
// 动画id
const createTime = () => +new Date()
const startTime = createTime()
function tick () {
const remaining = Math.max(0, startTime + duration - createTime())
const temp = remaining / duration || 0
const percent = 1 - temp
// 最终每次移动的left距离
const leftPos = (des - start) * percent + start
if (1 === percent) {
window.cancelAnimationFrame(frameId)
el.style.left = des + 'px'
callback()
} else {
el.style.left = leftPos + 'px'
window.requestAnimationFrame(tick)
}
}
// 开始执行动画
const frameId = window.requestAnimationFrame(tick)
}
_isDom (el) {
return el && typeof el === 'object' && el.nodeType === 1 && typeof el.nodeName === 'string'
}
_checkSpeed (speed) {
return speed % 1 === 0 && speed >=1 && speed <= 5
}
}
export default Barrage