简单实现一个弹幕库

1,705 阅读3分钟

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

github传送门