前言
弹幕是近年来在各种在线平台上越来越受欢迎的一种互动方式。它以短小的文字消息以及实时显示的形式出现在视频、音乐、直播等内容的播放界面上,允许用户实时在屏幕上发送反馈、评论、表情等内容,与其他观众进行即时交流。弹幕的出现使得观看内容变得更加富有互动性和趣味性。观众们可以通过弹幕与其他观众实时交流,分享自己的看法和心情,对内容进行点赞、吐槽。当然,还可以在弹幕之下向自己喜欢的女孩子表白😍呢~
现在几乎所有的在线媒体平台都提供了弹幕功能,例如Bilibili、YouTube、Twitch、微博等。
像下面这些就是我们经常看到的一些弹幕应用场景:
看到这些五花八门的弹幕,有没有一种自己也想实现的冲动。欸,正好我也有这种冲动,不如咱们就一起来实现一下叭。
既然是咱们自己实现,那肯定得设计得好玩一些,给它随意改颜色,改字体大小。先上一段html代码准备一下。
html
<body>
<div class="wrap">
<h1>听妈妈的话 --周杰伦</h1>
<div class="main">
<canvas id="canvas"></canvas> <!-- 画布 -->
<video src="./mv.mp4" id="video" controls width="720" height="480"></video>
</div>
<div class="content">
<input type="text" id="text">
<input type="button" id="btn" value="发弹幕">
<input type="color" id="color">
<input type="range" id="range" max="40" min="20">
</div>
</div>
</body>
页面初始效果如下:
目标:文本框输入要发送的弹幕内容,调颜色框的颜色,拉字体大小,点发送按钮即可发送对应格式的弹幕。
目标效果:
弹幕实现
我们正在看一个视频,在这之前有其他用户也观看了并且在视频的一些时间点发送了一些弹幕,那么我们现在看到了那个时间点,可以看到那些弹幕从视频右边向左边缓缓划过。所以,我们要先模拟一份历史弹幕信息(有弹幕内容,弹幕颜色,弹幕字体大小,弹幕出现时间)。
js
//模拟历史弹幕数据
const data = [
{ text: '今天你掘金了吗?', color: 'red', fontSize: 22, time: 5 },
{ text: '不听不听,就要恋爱', color: 'green', fontSize: 30, time: 10 },
{ text: '小米露小米露', color: 'red', fontSize: 35, time: 13 },
{ text: '掘金掘金,每日掘金', color: 'pink', fontSize: 50, time: 13 },
{ text: '掘金文章今天你写了吗?', color: '#00a1f5', fontSize: 40, time: 13 },
]
创建了一个barrageManage
构造函数,用来管理弹幕,将画布大小设置为视频大小(正好覆盖),再获取一个2D上下文对象,用于在画布上绘制弹幕。同时,要给弹幕规定默认的颜色,大小,透明度,移动速度等属性,如果用户自己设置了弹幕的属性,则要替换掉默认属性。
js
//管理弹幕
//sendData:用户输入的弹幕属性,默认值为{}
function barrageManage(canvas,video,sendData={}) {
if(!canvas || !video) return //没有画布或视频直接return
this.canvas = canvas
this.video = video
//将画布的宽高设置成和视频宽高一致
this.canvas.width = video.width
this.canvas.height = video.height
//获取一个2D上下文对象
this.ctx = canvas.getContext('2d')
//默认的弹幕属性
const initData = {
color: '#e91e63',
fontSize: 20,
speed: 1.5,
opacity: 0.5,
data: []
}
//将用户自己设置的弹幕属性替换默认属性并挂载到this上
Object.assign(this,initData,sendData)
//1、视频播放,弹幕才出现
this.isPaused = true //播放开关
//2、获取到所有最终被修饰好的弹幕
this.allBarrage = this.data.map((item) => new Barrage(item,this) ) //new Barrage()用来修饰一条弹幕
//3、实现弹幕移动效果,并将移动后的弹幕绘制在画布上
this.render()
}
通过以上代码可以看出,这里面有一个Barrage()
构造函数和render()
函数等待我们实现。
我们先来实现一下render()
方法,因为构造函数和构造函数的原型共用一个this
,所以构造函数barrageManage
里面的this.render()
方法 ,可以写在他的原型(barrageManage.prototype
)上。
js
barrageManage.prototype.render = function(){
//每次移动前清除画布当前内容
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
//调用方法,实现弹幕移动,并将移动后的弹幕绘制在画布上
this.barrageRender()
if(!this.isPaused){ //视频是播放状态
//执行一个移动动画,在下次重绘之前调用指定的回调函数更新动画
requestAnimationFrame(this.render.bind(this)) //每隔17ms执行调用render()方法
}
}
//实现弹幕移动,并将移动后的弹幕绘制在画布上的方法
barrageManage.prototype.barrageRender = function(){
//拿到视频当前播放的时间
const time = this.video.currentTime
//实现每一条弹幕左移
this.allBarrage.forEach(function(barrage) {
//若视频当前时间大于等于弹幕出现时间且弹幕没有从左边出去,弹幕左移
if(time >= barrage.time && !barrage.flag){
if(!barrage.isInit){
barrage.init() //用于弹幕初始化的方法
barrage.isInit = true
}
barrage.x -= barrage.speed
barrage.renderEach() //弹幕绘制方法,用于将每一条弹幕绘制在画布上呈现出来
//弹幕从左边出去了
if(barrage.x <= -barrage.width){
barrage.flag = true
}
}
});
}
//用户发送的弹幕,通过Barrage()修饰后将其添加到allBarrage所有弹幕里面
//因为requestAnimationFrame每17ms会调用render()方法,每次都会重新遍历allBarrage并将其绘制在画布上,所以这实现了新添加的弹幕可以马上被绘制出来。
barrageManage.prototype.add = function(obj){ //添加弹幕
this.allBarrage.push(new Barrage(obj,this))
}
到这里我们只是将弹幕的移动和绘制逻辑先写出来啦,并且还实现了一个可以添加弹幕的方法。
所以接下来我们还需要将上面代码用于弹幕初始化的方法barrage.init()
和用于将每一条弹幕绘制在画布上呈现出来的方法barrage.renderEach()
实现出来,这里我们要知道这两个方法的barrage
只是形参,它代表的是每一条弹幕,它是allBarrage
数组的每一项,而allBarrage
数组的每一项正是通过我们开始还没实现的Barrage()
构造函数创建出来的实例对象,而实例对象会隐式继承其构造函数原型上的方法,所以init()
和renderEach()
这两个方法是不是就可以写在Barrage()
构造函数原型(Barrage.prototype
)上面啦!
好,那现在目标就非常明确了,我们只需要再把Barrage()
构造函数写出来,并且在它的原型上挂init()
和renderEach()
这两个方法就行咯,开始coding!
js
//修饰每一条弹幕
//将barrageManage函数的this传过来,可以拿到它所有的属性
function Barrage(obj,context){ //context == this,barrageManage函数的this
this.text = obj.text //弹幕文本
this.time = obj.time // 弹幕出现时间
this.obj = obj //所有弹幕属性
this.context = context //挂载barrageManage函数里面的this
}
//弹幕初始化
Barrage.prototype.init = function(){
this.color = this.obj.color || this.context.color //优先使用用户设置的颜色,没有则使用默认颜色
this.fontSize = this.obj.fontSize || this.context.fontSize //优先使用用户设置的字体大小,没有则使用默认大小
this.speed = this.context.speed //默认速度
this.opacity = this.context.opacity //默认透明度
//获取弹幕的宽度
const p = document.createElement('p')
p.style.fontSize = this.fontSize + 'px'
p.innerHTML = this.text
document.body.appendChild(p)
this.width = p.offsetWidth //弹幕宽度
document.body.removeChild(p)
//设置弹幕的初始位置 画布最右边任意位置
this.x = this.context.canvas.width
this.y = this.context.canvas.height*Math.random()
//弹幕超出画布上边界
if(this.y < this.fontSize){
this.y = this.fontSize //画布最上端
}
//弹幕超出画布下边界
if(this.y > this.context.canvas.height - this.fontSize){
this.y = this.context.canvas.height - this.fontSize //画布最下端
}
}
//弹幕绘制方法,将每一条弹幕绘制在画布上呈现出来
Barrage.prototype.renderEach = function(){
this.context.ctx.font = `${this.fontSize}px Arial` //字体
this.context.ctx.fillStyle = this.color //颜色
this.context.ctx.fillText(this.text, this.x,this.y) //字体填充
}
写到这里我们就已经把主要的核心代码都已经实现啦~我们现在只需要通过构造函数 barrageManage()
将canvas
画布,video
视频,{data}
历史弹幕作为实参传递进去,实例化出来一个对象,它便能显示继承barrageManage()
构造函数里面所有的属性和方法,以及隐式继承barrageManage()
构造函数原型上的方法。这时,我们只需给视频做个播放监听事件,当视频播放时,这个实例对象只要调用它身上的调用render()
方法,就可以把我们写的所有代码功能都连通起来啦 ~便可以实现弹幕的移动效果了。
当然,还有一个弹幕发送功能,我们需要获取到用户输入的弹幕属性,并且可以通过点击发送弹幕或者键盘按下enter键将它发送出去绘制在屏幕上。这时候我们只需要再写一个send函数,通过它将获取到的弹幕属性作为实参传递到我们刚刚创建的实例对象里面的add()
方法里面,就完成了弹幕发送功能。最后代码如下:
js
const canvas = document.getElementById('canvas')
const video = document.getElementById('video')
const $text = document.getElementById('text')
const btn = document.getElementById('btn')
const $color = document.getElementById('color')
const $range = document.getElementById('range')
//创建一个整理弹幕的实例对象
const barragemanage = new barrageManage(canvas,video,{data})
video.addEventListener('play',()=>{
barragemanage.isPaused = false //将播放状态开关设置为播放
barragemanage.render() //调用render()方法,实现弹幕移动效果,并将移动后的弹幕绘制在画布上
})
function send() {
//获取到用户填的弹幕信息
const time = video.currentTime //用户发送弹幕的时间
const text = $text.value //弹幕文本
const color = $color.value //弹幕颜色
const fontSize = $range.value //弹幕字体大小
const obj = {time, text, color, fontSize} //将用户填的所有弹幕信息放在一个对象里
//添加弹幕
barragemanage.add(obj)
}
btn.addEventListener('click',send) //点击发送弹幕
$text.addEventListener('keyup', function(e) {
if(e.key ==='Enter'){ //键盘敲击enter键也可以发送弹幕
send()
}
})
总结
到这里,我们的代码就全部写完啦~整个逻辑性还是很强的,很多方法都是抽离出来封装成一个函数来调用使用,所以有不懂的地方欢迎各位小伙伴留言评论喔😘😘😘 ~ 同时,本人也是一名大学学生,知识和技术有限,要是文章有不对的地方或者有更好的方法,欢迎各路大神指点❤️ ~
如果觉得文章对您有所帮助的话,麻烦给小博主点点关注,点点赞咯😘,有问题欢迎各位小伙伴评论喔😊~
源码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#canvas{
position: absolute;
}
</style>
</head>
<body>
<div class="wrap">
<h1>听妈妈的话 --周杰伦</h1>
<div class="main">
<canvas id="canvas"></canvas> <!-- 画布 -->
<video src="./mv.mp4" id="video" controls width="720" height="480"></video>
</div>
<div class="content">
<input type="text" id="text">
<input type="button" id="btn" value="发弹幕">
<input type="color" id="color">
<input type="range" id="range" max="40" min="20">
</div>
</div>
</body>
<script src="./2.js"></script>
</html>
js
//模拟历史弹幕数据
const data = [
{ text: '今天你掘金了吗?', color: 'red', fontSize: 22, time: 5 },
{ text: '不听不听,就要恋爱', color: 'green', fontSize: 30, time: 10 },
{ text: '小米露小米露', color: 'red', fontSize: 35, time: 13 },
{ text: '掘金掘金,每日掘金', color: 'pink', fontSize: 50, time: 13 },
{ text: '掘金文章今天你写了吗?', color: '#00a1f5', fontSize: 40, time: 13 },
]
//管理弹幕
//sendData:用户输入的弹幕属性,默认值为{}
function barrageManage(canvas,video,sendData={}) {
if(!canvas || !video) return //没有画布或视频直接return
this.canvas = canvas
this.video = video
//将画布的宽高设置成和视频宽高一致
this.canvas.width = video.width
this.canvas.height = video.height
//获取一个2D上下文对象
this.ctx = canvas.getContext('2d')
//默认的弹幕属性
const initData = {
color: '#e91e63',
fontSize: 20,
speed: 1.5,
opacity: 0.5,
data: []
}
//将用户自己设置的弹幕属性替换默认属性并挂载到this上
Object.assign(this,initData,sendData)
//1、视频播放,弹幕才出现
this.isPaused = true //播放开关
//2、获取到所有最终被修饰好的弹幕
this.allBarrage = this.data.map((item) => new Barrage(item,this) ) //new Barrage()用来修饰一条弹幕
//3、实现弹幕移动效果,并将移动后的弹幕绘制在画布上
this.render()
}
barrageManage.prototype.render = function(){
//每次移动前清除画布当前内容
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
//调用方法,实现弹幕移动,并将移动后的弹幕绘制在画布上
this.barrageRender()
if(!this.isPaused){ //视频是播放状态
//执行一个移动动画,在下次重绘之前调用指定的回调函数更新动画
requestAnimationFrame(this.render.bind(this)) //每隔17ms执行调用render()方法
}
}
//实现弹幕移动,并将移动后的弹幕绘制在画布上的方法
barrageManage.prototype.barrageRender = function(){
//拿到视频当前播放的时间
const time = this.video.currentTime
//实现每一条弹幕左移
this.allBarrage.forEach(function(barrage) {
//若视频当前时间大于等于弹幕出现时间且弹幕没有从左边出去,弹幕左移
if(time >= barrage.time && !barrage.flag){
if(!barrage.isInit){
barrage.init() //用于弹幕初始化的方法
barrage.isInit = true
}
barrage.x -= barrage.speed
barrage.renderEach() //弹幕绘制方法,用于将每一条弹幕绘制在画布上呈现出来
//弹幕从左边出去了
if(barrage.x <= -barrage.width){
barrage.flag = true
}
}
});
}
//用户发送的弹幕,通过Barrage()修饰后将其添加到allBarrage所有弹幕里面
//因为requestAnimationFrame每17ms会调用render()方法,每次都会重新遍历allBarrage并将其绘制在画布上,所以这实现了新添加的弹幕可以马上被绘制出来。
barrageManage.prototype.add = function(obj){ //添加弹幕
this.allBarrage.push(new Barrage(obj,this))
}
//修饰每一条弹幕
//将barrageManage函数的this传过来,可以拿到它所有的属性
function Barrage(obj,context){ //context == this,barrageManage函数的this
this.text = obj.text //弹幕文本
this.time = obj.time // 弹幕出现时间
this.obj = obj //所有弹幕属性
this.context = context //挂载barrageManage函数里面的this
}
//弹幕初始化
Barrage.prototype.init = function(){
this.color = this.obj.color || this.context.color //优先使用用户设置的颜色,没有则使用默认颜色
this.fontSize = this.obj.fontSize || this.context.fontSize //优先使用用户设置的字体大小,没有则使用默认大小
this.speed = this.context.speed //默认速度
this.opacity = this.context.opacity //默认透明度
//获取弹幕的宽度
const p = document.createElement('p')
p.style.fontSize = this.fontSize + 'px'
p.innerHTML = this.text
document.body.appendChild(p)
this.width = p.offsetWidth //弹幕宽度
document.body.removeChild(p)
//设置弹幕的初始位置 画布最右边任意位置
this.x = this.context.canvas.width
this.y = this.context.canvas.height*Math.random()
//弹幕超出画布上边界
if(this.y < this.fontSize){
this.y = this.fontSize //画布最上端
}
//弹幕超出画布下边界
if(this.y > this.context.canvas.height - this.fontSize){
this.y = this.context.canvas.height - this.fontSize //画布最下端
}
}
//弹幕绘制方法,将每一条弹幕绘制在画布上呈现出来
Barrage.prototype.renderEach = function(){
this.context.ctx.font = `${this.fontSize}px Arial` //字体
this.context.ctx.fillStyle = this.color //颜色
this.context.ctx.fillText(this.text, this.x,this.y) //字体填充
}
const canvas = document.getElementById('canvas')
const video = document.getElementById('video')
const $text = document.getElementById('text')
const btn = document.getElementById('btn')
const $color = document.getElementById('color')
const $range = document.getElementById('range')
//创建一个整理弹幕的实例对象
const barragemanage = new barrageManage(canvas,video,{data})
video.addEventListener('play',()=>{
barragemanage.isPaused = false //将播放状态开关设置为播放
barragemanage.render() //调用render()方法,实现弹幕移动效果,并将移动后的弹幕绘制在画布上
})
function send() {
//获取到用户填的弹幕信息
const time = video.currentTime //用户发送弹幕的时间
const text = $text.value //弹幕文本
const color = $color.value //弹幕颜色
const fontSize = $range.value //弹幕字体大小
const obj = {time, text, color, fontSize} //将用户填的所有弹幕信息放在一个对象里
//添加弹幕
barragemanage.add(obj)
}
btn.addEventListener('click',send) //点击发送弹幕
$text.addEventListener('keyup', function(e) {
if(e.key ==='Enter'){ //键盘敲击enter键也可以发送弹幕
send()
}
})