一.知识准备
1.动画
动画——我们的眼睛具有“视觉暂留”的特性,当我们看到一幅画或一个物体后,在0.34秒内不会消失,当我们在0.34秒内切换画时,就会有一种流畅地动画效果,整个画面看起来像是在动的,所以小时候的小人书,快速翻动时就感觉整个画面像是动了起来,其实它们就是由一张张静态的图画快速切换形成的动画效果
帧——就是影像动画中最小单位的单幅影像画面。 一帧就是一副静止的画面 动画是由多个静止的话面构成的
帧率——就是在1秒钟时间里传输的图片的张数,也可以理解为图形处理器每秒钟能够刷新几次,通常用FPS(Frames Per Second)表示,我们在玩游戏的时候说的掉帧,就是因为帧率过低所造成的画面出现停滞
弹幕效果——我们的弹幕效果就是每一帧通过不断擦除canvas重新绘制canvas来达到滑动的目的
2.requestAnimationFrame
requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画,也就是说根据显示器的刷新频率来决定一秒内重绘的次数,一般显示器都是为60Hz 或 75Hz,也就是说一秒内会执行60或75次函数,我们将传入一个函数,不断的对canvas进行清空与绘制
仅从动画角度对比requestAnimationFrame与setTimeout
优点
1.requestAnimationFrame紧跟着浏览器刷新率,不会出现过渡绘制(绘制速度太快,超过浏览器刷新率,出现丢帧),或绘制速度太慢造成的卡顿,而setTimeout和setInterva都不太准确,一旦设置的时间不能和浏览器刷新时间保持一致,就容易出现过渡绘制与卡顿
2.如果页面不是激活状态下的话,动画会自动暂停,有效节省了CPU开销(我们在绘制弹幕时切换到其他页面弹幕会暂停,直到你切换回页面才会继续绘制),而setTimeout会一直在后台运行
缺点:
1.兼容性比不上setTimeout和setInterva
3.canvas篇
初始化canvas
//获取canvas对象
this.canvas = document.getElementById(canvas)
//获得 2d 上下文对象
this.context = this.canvas.getContext("2d")
//获取canvas容器的宽高,Canvas类似于位图,一般根据手机的屏幕倍数展示,将容器宽高乘以2以适配两倍屏,可自定义
this.canvasHeight = canvasHeight * 2
this.canvasWidth = canvasWidth * 2
//设置canvas的宽高
this.canvas.width = this.canvasWidth
this.canvas.height = this.canvasHeight
canvas绘制文字
//设置文字颜色
this.context.fillStyle = '#008B8B'
//设置文字字体字号
this.context.font = `20px STheiti, SimHei`
//绘制文字位置
this.context.fillText(绘制的文本,填充文本的起点横坐标(X轴), 填充文本的起点纵坐标(Y轴),文本占据的最大宽度)
例:this.context.fillText(`拿来把你`, 500, 400)
我们会通过不断修改X轴坐标达到弹幕滑动效果,也可对弹幕速度进行控制
canvas清除内容
//清空canvas画布内容
this.context.clearRect(矩形起点的 x 轴坐标,矩形起点的 y 轴坐标, 矩形的宽度, 矩形的高度)
this.context.clearRect(0, 0, this.canvasWidth , this.canvasHeight)
例:this.context.clearRect(0, 0, 500, 400)
每一帧都会擦除整个屏幕重新绘制所有弹幕
canvas测量文字宽度
通过measureText()函数我们可以测量文字的宽度
let width = this.context.measureText(value).width
作用:我们会通过文字宽度做两件事情,1.判断文字是否完整展示在画面上了 2.判断文字是否已经完整滑动出画面
二.弹幕实现
本例子用的是vue哦,在barrage.vue中引入barrage.js,实例化Barrage类,该类可以直接放在项目中使用,需要传入参数 barrageEffect.Barrage(canvas的ID,canvas容器的宽,canvas容器的高,弹幕行数,字体大小)
barrage.vue
<template>
<div class="barrage">
<img src="https://images.pexels.com/photos/2286895/pexels-photo-2286895.jpeg?auto=compress&cs=tinysrgb&dpr=2&w=500" alt="">
<canvas id="myCanvas">
</canvas>
</div>
</template>
<script>
import barrageEffect from './barrage'
export default {
data () {
return {
imgUrl:'',
canvasWidth:0,
canvasHeight:0,
context:null,
mobile:0,
barrage:null,
}
},
methods: {
init(){
const fontSize = 20
const highwayAmount = 4
this.barrage = new barrageEffect.Barrage('myCanvas',this.canvasWidth,this.canvasHeight,highwayAmount,fontSize)
//可通过addTest方法不断添加弹幕
this.barrage.addTest(['风景好美呀','绝了绝了','好想去','安排安排','醒醒醒醒,你没钱去'])
setTimeout(()=>{
this.barrage.addTest(['年轻人','耗子尾子','耗耗反思','这是你该做的梦吗','醒醒醒醒,老实搬砖','再做梦别怪我捶你'])
},500)
},
setCanvas(){
let canvasStyle = document.getElementById('myCanvas')
this.canvasWidth = canvasStyle.offsetWidth
this.canvasHeight =canvasStyle.offsetHeight
this.init()
}
},
computed:{
},
mounted(){
this.setCanvas()
}
};
</script>
<style lang="scss" scoped>
.barrage{
width: 100vw;
height: 100vh;
position: relative;
#myCanvas{
position: absolute;
z-index: 2;
width: 565px;
height: 377px;
top: 0;
left: 0;
}
img{
width: 565px;
height: 377px;
}
}
</style>
barrage.js
class Barrage{
constructor(canvas,canvasWidth,canvasHeight,highwayAmount,fontSize){
//获取canvas对象
this.canvas = document.getElementById(canvas)
//获得 2d 上下文对象
this.context = this.canvas.getContext("2d")
//获取canvas容器的宽高,Canvas类似于位图,一般根据手机的屏幕倍数展示,将容器宽高乘以2以适配两倍屏,可自定义
this.canvasHeight = canvasHeight * 2
this.canvasWidth = canvasWidth * 2
//设置canvas的宽高
this.canvas.width = this.canvasWidth
this.canvas.height = this.canvasHeight
//存储正在发送的弹幕
this.barrageList = [];
//存储待发送的弹幕
this.textList = []
//初始化字体大小
this.fontSize = fontSize
this.context.font = `${this.fontSize}px STheiti, SimHei`
this.highwayAmount = []
//初始化弹幕的highwayAmount,将弹幕划分成类似一条条公路,防止弹幕在Y轴重叠,可根据实际情况调整数量
this.initTop(highwayAmount)
//是否绘画完成标志
this.draw = false
}
initTop(highwayAmount){
//最多存在弹幕行数
const maxHighwayAmount = parseInt(this.canvasHeight/(this.fontSize+20))
//如果传入的行数大于最大行数,取最大行数
maxHighwayAmount<highwayAmount ? highwayAmount = maxHighwayAmount : ''
//highwayAmount弹幕行数
for(let i =1;i<=highwayAmount;i++){
this.highwayAmount.push(((this.fontSize+20)*i))
}
}
drawBarrage(){
//清空canvas画布内容
this.context.clearRect(0, 0, this.canvasWidth , this.canvasHeight)
//提前清除无用的弹幕,保证绘制弹幕时所有弹幕都是存在于页面上的,如果边绘制弹幕边清除数据,因为数组下标的改变会引起弹幕闪烁
for(let i=0;i<this.barrageList.length;i++){
//当弹幕的left(距离canvas左边的位置)+width弹幕自身宽度<=0时,说明弹幕已出屏幕,从this.barrageList中剔除
this.barrageList[i].left + this.barrageList[i].width <=0 ? this.barrageList.splice(i,1) : ''
}
//绘制弹幕
this.barrageList.forEach((val)=>{
//设置弹幕颜色
this.context.fillStyle = val.color
//绘制弹幕位置
this.context.fillText(`${val.value} `, val.left, val.top)
//occupation为占用top标志,当弹幕的left(距离canvas左边的位置)+width弹幕自身宽度<屏幕宽度时,说明弹幕已完全展示于屏幕中,可以释放占用的top并插入新值,consumeText函数作用见下文
val.occupation && val.left + val.width <= this.canvasWidth ? this.consumeText(val) : ''
//控制弹幕速度
val.left-=2
})
//this.barrageList为0,说明已无弹幕
this.barrageList.length == 0 ? this.draw=false : requestAnimationFrame(this.drawBarrage.bind(this))
}
consumeText(val){
//将占用标志置为false,防止多次执行val.occupation && val.left + val.width <= this.canvasWidth ? this.consumeText(val) : ''
val.occupation =false
//释放top值,向this.highwayAmount返回占用的top值,延迟0.5s执行,防止弹幕粘黏
setTimeout(()=>{
this.highwayAmount.push(val.top)
//检查是否有待发送的弹幕,如果有,向this.barrageList插入新值
if(this.textList.length!=0){
this.barrageList.push(this.initTest(this.textList.splice(0,1)[0]))
}
},500)
}
addTest(textList){
//判断是否处于绘制状态
if(this.draw){
//判断是否有空余的highwayAmount可以使用
if(this.highwayAmount.length != 0){
let barrageList = textList.splice(0,this.highwayAmount.length)
for(const val of barrageList){
this.barrageList.push(this.initTest(val))
}
}
this.textList.push(...textList)
}else{
this.textList.push(...textList)
//将绘制状态置为true
this.draw = true
this.runBarrage()
}
}
initTest(text){
//初始化弹幕对象信息
let value = text
let color = ['#E0FFFF','#FFE1FF','#FFB5C5','#F0FFF0','#BFEFFF','#63B8FF','#FFFFFF']
let barrageTest ={
value,//弹幕值
top:this.highwayAmount.splice(0,1)[0],
left:this.canvasWidth,//弹幕起点
color:color[Math.floor(Math.random()*6)],//弹幕随机颜色
offset:Math.ceil(Math.random()* 3),//弹幕速度
width:Math.ceil(this.context.measureText(value).width),//获取该弹幕占用的宽度
occupation:true,//占用top状态
}
return barrageTest
}
runBarrage(){
//根据弹幕top数初始化第一次展示的数据
this.textList.splice(0,this.highwayAmount.length).forEach((val)=>{
this.barrageList.push(this.initTest(val))
})
//开始绘制
this.drawBarrage()
}
}
export default{
Barrage
}
三.类函数解析与弹幕重叠优化
1.initTop(highwayAmount)函数(控制Y轴弹幕不重叠)
接受一个参数(highwayAmount),表示生成的弹幕行数,控制弹幕在Y轴上不重叠,将canvas的Y轴看成一条条公路,根据字体大小分配出每条公路的宽度坐标,最终根据highwayAmount参数,获取相应数量不重叠的公路Y轴坐标
计算最多存在的弹幕数
通过canvas的高度/文字大小可以得出最多的弹幕行数,为了让行数之间保持距离,多加了20px parseInt(this.canvasHeight/(this.fontSize+20))
canvas绘制文字x,y坐标是按文字左下角计算,如果高度为0会出现如下情况
所以我们储存公路坐标时需要剔除掉0,所以i从1开始
for(let i =1;i<=highwayAmount;i++){
this.highwayAmount.push(((this.fontSize+20)*i))
}
2.initTest(text)函数
生成一个个弹幕对象储存弹幕对象信息
top:this.highwayAmount.splice(0,1)[0],//返回一个top值,并将该top值从this.highwayAmount剔除,为什么要这么干呢,假设我们有1行弹幕,第一个弹幕没完全展示完成时该行不能输出弹幕,否则会导致弹幕在X轴上重叠,
例:如下图
所以我们将值从this.highwayAmount剔除,防止被赋值给其他弹幕对象,如何判断弹幕已完全展示可插入其他弹幕呢,详见drawBarrage()函数
occupation:true,//占用top状态,drawBarrage()需要用到
3.drawBarrage()函数
根据this.highwayAmount数组获取弹幕展示行数,初始化第一批展示的数据,并调用this.drawBarrage()函数开始绘制
4.addTest(textList)函数
它作为绘制弹幕的入口
根据this.draw判断弹幕是否处于绘制状态,如果弹幕不处于绘制状态,将所有数据添加到this.textList中,并调用this.runBarrage()方法初始化弹幕数据进行绘制
如果弹幕处于绘制状态检查是否有多余的top可以使用,如果有top可以使用,直接将数据添加至this.barrageList中进行绘制,其他数据添加至this.textList中等待绘制
5.drawBarrage()函数
每一帧执行的函数,控制弹幕移动,添加,删除弹幕数据
1.会通过this.context.clearRect(0, 0, this.canvasWidth , this.canvasHeight)清除画布重新绘制弹幕与弹幕位置
2.根据弹幕距canvas的左边距离与自身宽度清除绘制完成的弹幕
3.根据每个弹幕对象绘制弹幕,并根据根据弹幕距canvas的左边距离与自身宽度判断是否已完整显示在了canvas中
如果是,则调用consumeText(val)函数,将弹幕对象占用的top,释放到this.highwayAmount中,并查看this.textList是否有待发送弹幕,如果有,将其添加至this.barrageList中
三.结语
首次发表文章,文中错误之处多多指正啊