上次项目我们分的是图片网站,原本想的是加一个视频播放的功能嘞,但是奈何我们组就一个后端,三个前端,视频播放并不是我们网站的必要功能,所以就没去实现,但是项目结束了,闲着没事就看了一下弹幕的实现方式,在b站搜到了一个使用canvas实现弹幕的方法,看到人家的代码才知道自己写的代码有多low,人家写的代码是真的将功能分离,后期的可维护性和拓展性大大加强了。这里就来分享一下代码。
html并没有写过多的样式,大概是这样的,video用到的是原生的东西
html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>canvas实现弹幕</title>
<style>
.content {
width: 900px;
margin: 60px auto;
border: 1px solid red;
position: relative;
}
#canvas {
position: absolute;
top: 0;
right: 0;
/* z-index: 10; */
}
.control {
text-align: center;
}
input {
vertical-align: middle;
}
</style>
</head>
<body>
<div class="">
<div class="content">
<canvas id="canvas"></canvas>
<video id="video" src="视频地址" height="auto" controls width="900"></video>
</div>
<div class="control">
// 弹幕内容
<input id="ipt" placeholder="请输入弹幕" />
//弹幕的颜色选择
<input id="colorIpt" value="#cccccc" type="color" />
//弹幕的字体大小选择
<input id="fontIpt" min="20" max="60" value="26" type="range" />
<button id="submit">点击发送</button>
</div>
</div>
</body>
</html>
js(这里我和那个视频上的一样,把实现弹幕的主要功能和调用以及生成假数据的函数分开)
let data = getDate(20)
// 获取数据的函数
function getDate(len) {
let data = []
/* 一个弹幕数据中需要有弹幕的具体值,弹幕的字体大小,弹幕的颜色,它出现在视频中的时间,以及他的速度
*/
for (let i = 0; i < len; i++) {
data.push({
value: `第${i}条弹幕`,
fontSize: 26,
color: 'red',
time: i * 2,
speed: 1
})
}
return data
}
const canvas = document.getElementById('canvas')
const video = document.getElementById('video')
/* Barrages是实现的具体方法,需要传canvas(绘制弹幕的canvas),video(播放视频),数据
*/
barage = new Barrages({
canvas,
video,
data
})
// 播放视频,弹幕播放
video.addEventListener('play', () => {
barage.isPlay = true
barage.render()
})
// 视频暂停,弹幕暂停
video.addEventListener('pause', () => {
barage.pause()
})
/* 声名一个对象,里边包含着弹幕的字体和颜色(这里给其一个默认值),之后修改颜色和字体大小的时候会改变这个对象的值
*/
let option = {
color: '#cccccc',
fontSize: 26
}
// 点击发送,插入数据
submit.addEventListener('click', () => {
option.time = video.currentTime
barage.setBarrage(option)
ipt.value = ''
})
//弹幕内容
ipt.addEventListener('change', (event) => {
option.value = event.target.value;
})
//弹幕颜色
colorIpt.addEventListener('change', (event) => {
option.color = event.target.value;
})
//弹幕字体大小
fontIpt.addEventListener('change', (event) => {
option.fontSize = +event.target.value;
})
具体实现函数
// 渲染弹幕数据的具体函数
class Barrage {
constructor(options, ctx) {
this.ctx = ctx
// 设置是否初始化
this.isInit = false
// 判断是否还需要绘制
this.flag = true
// 归并到this上去
Object.assign(this, options)
}
// 初始化
init() {
this.color = this.color || this.ctx.color
this.fontSize = this.fontSize || this.ctx.fontSize
this.speed = this.speed || this.ctx.speed
// 获取宽度
this.width = this.getWidth()
// 获取宽度
this.x = this.ctx.canvas.width
// 获取高度
this.y = Math.random() * this.ctx.canvas.height
// 判断是否大于canvas的高度减去字体大小(如果大于就等于这个值)
if (this.y > this.ctx.canvas.height - this.fontSize) {
this.y = this.ctx.canvas.height - this.fontSize
}
if (this.y < this.fontSize) {
this.y = this.fontSize
}
/* 上述的两个判断是为了避免弹幕出现在canvas外边(别忘了把字体大小算进去)*/
}
// 获取宽度(通过把弹幕内容放到html元素中来获弹幕的具体宽度)
getWidth() {
// 创建一个标签
let span = document.createElement('span')
span.innerText = this.value
span.style.font = `${this.fontSize}px "Miscrosoft YaHei"`
span.style.position = 'absolute'
span.style.zIndex = -1
//插入到body里边(方便获取宽度)
document.body.appendChild(span)
let width = span.clientWidth
// 删除
document.body.removeChild(span)
return width
}
//将弹幕渲染到canvas上
render() {
//指定字体大小
this.ctx.context.font = `${this.fontSize}px "Miscrosoft YaHei"`
// 指定字体颜色
this.ctx.context.fillStyle = this.color
// 指定绘制内容和绘制位置
this.ctx.context.fillText(this.value, this.x, this.y)
}
}
// 整体的流程控制
class Barrages {
constructor(options) {
//判断是否有canvas和video
if (!options.canvas || !options.video) {
return
}
// 声明一个默认值(避免没有传值时的错误渲染)
const defaultOptions = {
fontSize: 26,
color: 'green',
data: [],
speed: 1
}
// 把这些东西都归并到this上
Object.assign(this, defaultOptions, options)
// 初始化canvas
this.canvas.width = this.video.clientWidth
this.canvas.height = this.video.clientHeight
// 取出canvas上下文(绘制canvas需要获取它的上下文)
this.context = this.canvas.getContext('2d')
// 将每个数据实例化(箭头函数的this)
this.barrages = this.data.map((item) => new Barrage(item, this))
// 是否需要播放弹幕
this.isPlay = true
}
// 清除画布方法
clear() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height)
}
//将新增的数据添加到数组中
setBarrage(data) {
this.data.push(data)
this.barrages.push(new Barrage(data, this))
}
// 渲染方法
render() {
// 清空画布
this.clear()
// 渲染新的画布
this.renderBarrages()
//判断是否需要绘制弹幕
if (this.isPlay) {
// 循环执行渲染函数(需要矫正一下this的值)
requestAnimationFrame(this.render.bind(this))
}
}
// 绘制弹幕方法
renderBarrages() {
// 获取当前视频的时间
const time = this.video.currentTime
// 遍历数据
this.barrages.forEach(item => {
// 遍历所有弹幕数据,通过判断时间来确定其是否应该显示
if (item.time <= time && item.flag) {
// 判断是否应该初始化
if (!item.isInit) {
item.init()
item.isInit = true
}
/* 超出canvas不需要再绘制了原作者这里写的时this,是不对的,因为这里是箭头函数,this的指向的并不是每条弹幕数据的实例,如果是普通函数this的指向则是window,所以这里改成item(弹幕数据的实例对象)
*/
if (item.x < -item.width) {
item.flag = false
}
//改变它的横轴位置
item.x = item.x - item.speed
// 调用其render方法
item.render()
}
})
}
// 暂停方法
pause() {
this.isPlay = false
console.log('pause')
}
}
利用canvas实现的本质就是每次渲染的时候都去初始化canvas,然后根据数据实例化对象的x和y坐标,去绘制到canvas上,虽然我们看着像连续的,但是其背后是重新渲染的。
上边的便是我分享的代码,虽然有点代码搬运工的意思吧,但是看完人家写的代码,还是感觉自己写的太差了,之后要多培养这种将功能分离的思想,和将数据对象化的思想。下周继续努力吧!!!
这段代码的作者b站视频连接