原生JavaScript实现弹幕组件的示例代码


Posted in Javascript onOctober 12, 2020

前言

如今几乎所有的视频网站都有弹幕功能,那么今天我们就自己用原生 JavaScript 封装一个弹幕类。这个类希望有如下属性和实例方法:

属性

  • el容器节点的选择器,容器节点应为绝对定位,设置好宽高
  • height 每条弹幕的高度
  • mode 弹幕模式,half则为一半容器高度,top为三分之一,full为占满
  • speed弹幕划过屏幕的时间
  • gapWidth后一条弹幕与前一条弹幕的距离

方法

  • pushData 添加弹幕元数据
  • addData持续加入弹幕
  • start开始调度弹幕
  • stop停止弹幕
  • restart 重新开始弹幕
  • clearData清空弹幕
  • close关闭
  • open重新显示弹幕

PS:有一些自封装的工具函数就不贴出来了,大概知道意思就好

初始化

引入JavaScript文件之后,我们希望如下使用,先采取默认配置。

let barrage = new Barrage({
  el: '#container'
})

参数初始化:

function Barrage(options) {
  let {
    el,
    height,
    mode,
    speed,
    gapWidth,
  } = options
  this.container = document.querySelector(el)
  this.height = height || 30
  this.speed = speed || 15000 //2000ms
  this.gapWidth = gapWidth || 20
  this.list = []
  this.mode = mode || 'half'
  this.boxSize = getBoxSize(this.container)
  this.perSpeed = Math.round(this.boxSize.width / this.speed)
  this.rows = initRows(this.boxSize, this.mode, this.height)
  this.timeoutFuncs = []
  this.indexs = []
  this.idMap = []
}

先接受好参数然后初始化,下面看看getBoxSize和initRows

function getBoxSize(box) {
  let {
    height,
    width
  } = window.getComputedStyle(box)
  return {
    height: px2num(height),
    width: px2num(width)
  }

  function px2num(str) {
    return Number(str.substring(0, str.indexOf('p')))
  }
}

通过getComputedStyleapi计算出盒子的宽高,这里用来计算容器的宽高,之后也会用到。

function initRows(box, mode, height) {
  let divisor = getDivisor(mode)
  rows = Math.ceil(box.height * divisor / height)
  return rows
}

function getDivisor(mode) {
  let divisor = .5
  switch (mode) {
    case 'half':
      divisor = .5
      break
    case 'top':
      divisor = 1 / 3
      break;
    case 'full':
      divisor = 1;
      break
    default:
      break;
  }
  return divisor
}

根据高度算出弹幕应该有多少行,下面会有地方用到行数。

插入数据

有两种插入数据的方法,一种是添加源数据,一种是持续添加。先来看添加源数据的方法:

this.pushData = function (data) {

  this.initDom()
  if (getType(data) == '[object Object]') {
    //插入单条
    this.pushOne(data)
  }
  if (getType(data) == '[object Array]') {
    //插入多条
    this.pushArr(data)
  }
}
this.initDom = function () {
  if (!document.querySelector(`${el} .barrage-list`)) {
    //注册dom节点
    for (let i = 0; i < this.rows; i++) {
      let div = document.createElement('div')
      div.classList = `barrage-list barrage-list-${i}`
      div.style.height = `${this.boxSize.height*getDivisor(this.mode)/this.rows}px`
      this.container.appendChild(div)
    }
  }
}

this.pushOne = function (data) {
  for (let i = 0; i < this.rows; i++) {
    if (!this.list[i]) this.list[i] = []

  }

  let leastRow = getLeastRow(this.list) //获取弹幕列表中最少的那一列,弹幕列表是一个二维数组
  this.list[leastRow].push(data)
}
this.pushArr = function (data) {
  let list = sliceRowList(this.rows, data)
  list.forEach((item, index) => {
    if (this.list[index]) {
      this.list[index] = this.list[index].concat(...item)
    } else {
      this.list[index] = item
    }
  })
}
//根据行数把一维的弹幕list切分成rows行的二维数组
function sliceRowList(rows, list) {
  let sliceList = [],
    perNum = Math.round(list.length / rows)
  for (let i = 0; i < rows; i++) {
    let arr = []
    if (i == rows - 1) {
      arr = list.slice(i * perNum)
    } else {
      i == 0 ? arr = list.slice(0, perNum) : arr = list.slice(i * perNum, (i + 1) * perNum)
    }
    sliceList.push(arr)
  }
  return sliceList
}

持续加入数据的方法只是调用了添加源数据的方法,并且开始了调度而已

this.addData = function (data) {
  this.pushData(data)
  this.start()
}

发射弹幕

下面来看看发射弹幕的逻辑

this.start = function () {
  //开始调度list
  this.dispatchList(this.list)
}

this.dispatchList = function (list) {
  for (let i = 0; i < list.length; i++) {
    this.dispatchRow(list[i], i)
  }
}

this.dispatchRow = function (row, i) {
  if (!this.indexs[i] && this.indexs[i] !== 0) {
    this.indexs[i] = 0
  }
  //真正的调度从这里开始,用一个实例变量存储好当前调度的下标。
  if (row[this.indexs[i]]) {
    this.dispatchItem(row[this.indexs[i]], i, this.indexs[i])
  }
}
this.dispatchItem = function (item, i) {
  //调度过一次的某条弹幕下一次在调度就不需要了
  if (!item || this.idMap[item.id]) {
    return
  }
  let index = this.indexs[i]
  this.idMap[item.id] = item.id
  let div = document.createElement('div'),
    parent = document.querySelector(`${el} .barrage-list-${i}`),
    width,
    pastTime
  div.innerHTML = item.content
  div.className = 'barrage-item'
  parent.appendChild(div)
  width = getBoxSize(div).width
  div.style = `width:${width}px;display:none`
  pastTime = this.computeTime(width) //计算出下一条弹幕应该出现的时间
  //弹幕飞一会~
  this.run(div)
  if (index > this.list[i].length - 1) {
    return
  }
  let len = this.timeoutFuncs.length
  //记录好定时器,后面清空
  this.timeoutFuncs[len] = setTimeout(() => {
    this.indexs[i] = index + 1
    //递归调用下一条
    this.dispatchItem(this.list[i][index + 1], i, index + 1)
  }, pastTime);
}
//用css动画,整体还是比较流畅的
this.run = function (item) {
  item.classList += ' running'
  item.style.left = "left:100%"
  item.style.display = ''
  item.style.animation = `run ${this.speed/1000}s linear`
  //已完成的打一个标记
  setTimeout(() => {
    item.classList+=' done'
  }, this.speed);
}

//根据弹幕的宽度和gapWth,算出下一条弹幕应该出现的时间
this.computeTime = function (width) {
  let length = width + this.gapWidth
  let time = Math.round(length / this.boxSize.width * this.speed/2)
  return time
}

动画css具体如下

@keyframes run {
  0% {
    left: 100%;
  }

  50% {
    left: 0
  }

  100% {
    left: -100%;
  }
}
.run {
  animation-name: run;
}

其余方法

停止

利用动画的paused属性停止

this.stop = function () {
  let items = document.querySelectorAll(`${el} .barrage-item`);
  [...items].forEach(item => {
    item.className += ' pause'
  })
}
.pause {
  animation-play-state: paused !important;
}

重新开始

移除pause类即可

this.restart = function () {
  let items = document.querySelectorAll(`${el} .barrage-item`);
  [...items].forEach(item => {
    removeClassName(item, 'pause')
  })
}

打开关闭

做一个显示隐藏的逻辑即可

this.close = function () {
  this.container.style.display = 'none'
}
this.open = function () {
  this.container.style.display = ''
}

清理弹幕

this.clearData = function () {
  //清除list
  this.list = []
  //清除dom
  document.querySelector(`${el}`).innerHTML = ''
  //清除timeout
  this.timeoutFuncs.forEach(fun => clearTimeout(fun))
}

最后用一个定时器定时清理过期的弹幕:

setInterval(() => {
  let items = document.querySelectorAll(`${el} .done`);
  [...items].forEach(item=>{
    item.parentNode.removeChild(item)
  })
}, this.speed*5);

最后

感觉这个的实现还是有缺陷的,如果是你设计这么一个类,你会怎么设计呢?

到此这篇关于原生JavaScript实现弹幕组件的示例代码的文章就介绍到这了,更多相关JavaScript 弹幕组件内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木! 

Javascript 相关文章推荐
将HTML自动转为JS代码
Jun 26 Javascript
JQuery 1.3.2以上版本中出现pareseerror错误的解决方法
Jan 11 Javascript
js Array操作的最简短最容易理解方法
Dec 09 Javascript
AngularJS入门教程之链接与图片模板详解
Aug 19 Javascript
knockoutjs动态加载外部的file作为component中的template数据源的实现方法
Sep 01 Javascript
javascript过滤数组重复元素的实现方法
May 03 Javascript
jQuery实现的简单在线计算器功能
May 11 jQuery
jQuery 实时保存页面动态添加的数据的示例
Aug 14 jQuery
Vue在页面右上角实现可悬浮/隐藏的系统菜单
May 04 Javascript
基于JavaScript实现一个简单的Vue
Sep 26 Javascript
利用原生JS实现欢乐水果机小游戏
Apr 23 Javascript
arcgis.js控制地图地体的显示范围超出区域自动弹回(实现思路)
Jan 28 Javascript
分享8个JavaScript库可更好地处理本地存储
Oct 12 #Javascript
浅析我对JS延迟异步脚本的思考
Oct 12 #Javascript
Webpack5正式发布,有哪些新特性
Oct 12 #Javascript
如何利用 JS 脚本实现网页全自动秒杀抢购功能
Oct 12 #Javascript
移动端JS实现拖拽两种方法解析
Oct 12 #Javascript
JavaScript读取本地文件常用方法流程解析
Oct 12 #Javascript
vue实现移动端返回顶部
Oct 12 #Javascript
You might like
人大复印资料处理程序_输入篇
2006/10/09 PHP
一个简单的PHP&amp;MYSQL留言板源码
2020/07/19 PHP
php一些错误处理的方法与技巧总结
2013/08/10 PHP
PHP测试成功的邮件发送案例
2015/10/26 PHP
同一个表单 根据要求递交到不同页面的实现方法小结
2009/08/05 Javascript
Javascript在IE或Firefox下获取鼠标位置的代码
2009/12/18 Javascript
关于this和self的使用说明
2010/08/01 Javascript
jquery根据name属性查找的小例子
2013/11/21 Javascript
js实现数组去重、判断数组以及对象中的内容是否相同
2013/11/29 Javascript
jQuery中next方法用法实例
2015/04/24 Javascript
AngularJS 模块详解及简单实例
2016/07/28 Javascript
jQuery中元素选择器(element)简单用法示例
2018/05/14 jQuery
JavaScript根据json生成html表格的示例代码
2018/10/24 Javascript
详解Python多线程
2016/11/14 Python
Python语言生成水仙花数代码示例
2017/12/18 Python
matplotlib简介,安装和简单实例代码
2017/12/26 Python
Python绘图实现显示中文
2019/12/04 Python
Tensorflow的常用矩阵生成方式
2020/01/04 Python
Python numpy多维数组实现原理详解
2020/03/10 Python
Python3 利用face_recognition实现人脸识别的方法
2020/03/13 Python
记一次django内存异常排查及解决方法
2020/08/07 Python
python matlab库简单用法讲解
2020/12/31 Python
python爬虫scrapy基于CrawlSpider类的全站数据爬取示例解析
2021/02/20 Python
日语专业毕业生自荐信
2013/11/11 职场文书
中英文求职信范文
2014/01/27 职场文书
市场营销大学生职业规划书
2014/02/25 职场文书
大学社团计划书
2014/05/01 职场文书
本溪水洞导游词
2015/02/11 职场文书
微信搭讪开场白
2015/05/28 职场文书
超强台风观后感
2015/06/09 职场文书
黑暗中的舞者观后感
2015/06/18 职场文书
应届生个人的求职(自荐信范文2篇)
2019/08/23 职场文书
创业计划书之冷饮店
2019/09/27 职场文书
Python字符串对齐方法使用(ljust()、rjust()和center())
2021/04/26 Python
简单介绍Python的第三方库yaml
2021/06/18 Python
Li list-style-image 图片垂直居中实现方法
2023/05/21 HTML / CSS