最近做年终活动,许愿的时候希望可以以弹幕的形式出现。
以下一步步,说明弹幕逻辑。本文的弹幕较为简单,重在说明弹幕逻辑。
显示一条弹幕
先写段静态的文字。
超简单:
但是一行通常不止一个弹幕,肯定不能block,所以这里的bullet-item需要处理下,这里我用定位的方式(主要为了之后的弹道,定位更方便)。
<!-- App.vue -->
<template lang="pug">
div#app
div.bullet-wrap
div.bullet-item 减肥成功!
</template>
<style>
body {
margin: 0;
}
.bullet-wrap {
height: 375px;
background-color: #eee;
position: relative;
}
.bullet-item {
position: absolute;
}
</style>
让文字在左边屏幕外
让文字在左边屏幕外,表面看起来消失的样子
.bullet-item {
transform: translateX(-100%);
}
让文字在右边屏幕外
让文字在右边屏幕外,表面看起来消失的样子
.bullet-item {
transform: translateX(100vw);
}
让文字从右边屏幕外到左边屏幕外
让文字从右边屏幕外到左边屏幕外,表面看起来从左边诞生,从右边离去
.bullet-item {
position: absolute;
animation: rightToLeft 7s linear both;
}
@keyframes rightToLeft {
0% {
transform: translate(100vw);
}
100% {
transform: translate(-100%);
}
}
多行显示
上面一条弹幕的感觉已经有了。
接下来,看看多行弹幕。
弹幕的的行,说好听点是弹道,一般将弹幕区域分成好几个弹道,就像跑道一样,每个运动员有自己的跑道。
两个弹道
两个弹道的逻辑,其实是在于,弹道靠top控制位置,不同的弹道top不一样
div.bullet-wrap
div.bullet-item(data-line="1") 减肥成功!
div.bullet-item(data-line="2") 没有bug!
.bullet-item[data-line="1"] {
top: 0;
}
.bullet-item[data-line="2"] {
top: 75px;
}
多个弹道,错峰运行
多个弹道,其实就是将上面改下,这么先设置5个弹道。
但因为动画是同时开启的,所以看起来,怪怪的。
错峰的逻辑如下,以下也是本文的核心:
首先分成两个集合
- 等待集合:将要显示的弹幕集合
- 显示集合:显示在屏幕上的弹幕集合,这也是页面循环的集合,出现在这个集合里,弹幕就开始滚动
显示弹幕的逻辑
- 先确定弹道,接下来的弹幕就出现在这个弹道里
- 从等待集合里取出第一个
- 设置此弹幕的弹道
- 此弹幕放进显示集合里,此弹幕开始滚动
隔一段时间执行,上续操作,弹幕就源源不断。
<template lang="pug">
div#app
div.bullet-wrap
div.bullet-item(v-for="item in showingBullets" :key="item.id" :data-line="item.line") {{item.name}}
</template>
<script>
const getUUID = () => Math.random() + Math.random();
export default {
data() {
return {
// 将要显示的弹幕队列
waitBullets: [
{ id: getUUID(), name: "一场说走就走的旅行", line: 0 },
{ id: getUUID(), name: "结束单身汪", line: 0 },
{ id: getUUID(), name: "明年暴瘦10斤", line: 0 },
{ id: getUUID(), name: "多陪伴父母", line: 0 },
{ id: getUUID(), name: "赚到1个亿,买别墅", line: 0 }
],
showingBullets: [],
lines: 5,
currentLine: 1
};
},
mounted() {
this.showNextBullet();
setInterval(this.showNextBullet, 700);
},
methods: {
showNextBullet() {
if (!this.waitBullets.length) {
return;
}
// 先确定弹道,跟上一个弹道错开即可
this.currentLine = (this.currentLine % this.lines) + 1;
// 从等待集合里取出第一个
const currentBullet = this.waitBullets.shift();
// 设置弹幕的弹道
currentBullet.line = this.currentLine;
// 弹幕放进显示集合里,弹幕开始滚动
this.showingBullets.push(currentBullet);
}
}
};
</script>
<style>
html,
body {
min-height: 100%;
overflow: hidden;
}
body {
margin: 0;
}
.bullet-wrap {
height: 375px;
background-color: #eee;
position: relative;
}
.bullet-item {
position: absolute;
animation: rightToleft 7s linear both;
}
.bullet-item[data-line="1"] {
top: 0;
}
.bullet-item[data-line="2"] {
top: 75px;
}
.bullet-item[data-line="3"] {
top: 150px;
}
.bullet-item[data-line="4"] {
top: 225px;
}
.bullet-item[data-line="5"] {
top: 300px;
}
@keyframes rightToleft {
0% {
transform: translate(100vw);
}
100% {
transform: translate(-100%);
}
}
</style>
发送新弹幕
发送新弹幕,其实没啥,送到等待队列里即可。
clickSend() {
if (!this.newBullet) { return; }
const newBullet = { id: getUUID(), name: this.newBullet, line: 0 };
this.waitBullets.push(newBullet);
}
div.input-wrap
input(v-model.trim="newBullet" type='text' maxlength='12' placeholder='来说点什么')
button.btn(@click="clickSend") 发送
优化
- 弹幕从屏幕上消失之后,需要从显示队列里移除
- 组件销毁前,清除定时器
- 想无限循环弹幕列表,需要在等待队列推出第一个的时候,顺手将这个再塞到等待队列里
<template lang="pug">
div#app
div.bullet-wrap
div.bullet-item(v-for="item in showingBullets" @animationend='removeBullet' :key="item.id" :data-line="item.line") {{item.name}}
div.input-wrap
input(v-model.trim="newBullet" type='text' maxlength='12' placeholder='来说点什么')
button.btn(@click="clickSend") 发送
</template>
<script>
const getUUID = () => Math.random() + Math.random();
export default {
data() {
return {
// 将要显示的弹幕队列
waitBullets: [
{ id: getUUID(), name: "一场说走就走的旅行", isWished: false, line: 0 },
{ id: getUUID(), name: "结束单身汪", isWished: false, line: 0 },
{ id: getUUID(), name: "明年暴瘦10斤", isWished: false, line: 0 },
{ id: getUUID(), name: "多陪伴父母", isWished: false, line: 0 },
{ id: getUUID(), name: "赚到1个亿,买别墅", isWished: false, line: 0 }
],
showingBullets: [],
lines: 5,
currentLine: 1,
newBullet: "",
isInfinite: true
};
},
mounted() {
this.showNextBullet();
const timer = setInterval(this.showNextBullet, 700);
// 组件销毁前,清除定时器
this.$once("hook:beforeDestroy", () => {
clearInterval(timer);
});
},
methods: {
showNextBullet() {
if (!this.waitBullets.length) {
return;
}
// 先确定弹道,跟上一个弹道错开即可
this.currentLine = (this.currentLine % this.lines) + 1;
// 从等待集合里取出第一个
const currentBullet = this.waitBullets.shift();
// 想要无限循环的话
this.isInfinite &&
this.waitBullets.push({
id: getUUID(),
name: currentBullet.name,
isWished: false,
line: 0
});
// 设置弹幕的弹道
currentBullet.line = this.currentLine;
// 弹幕放进显示集合里,弹幕开始滚动
this.showingBullets.push(currentBullet);
},
clickSend() {
if (!this.newBullet) {
return;
}
const newBullet = {
id: getUUID(),
name: this.newBullet,
isWished: false,
line: 0
};
this.waitBullets.push(newBullet);
},
removeBullet() {
this.showingBullets.shift();
console.log(this.showingBullets);
}
}
};
</script>
<style>
html,
body {
min-height: 100%;
overflow: hidden;
}
body {
margin: 0;
}
.bullet-wrap {
height: 375px;
background-color: #eee;
position: relative;
}
.bullet-item {
position: absolute;
animation: rightToleft 7s linear both;
}
.bullet-item[data-line="1"] {
top: 0;
}
.bullet-item[data-line="2"] {
top: 75px;
}
.bullet-item[data-line="3"] {
top: 150px;
}
.bullet-item[data-line="4"] {
top: 225px;
}
.bullet-item[data-line="5"] {
top: 300px;
}
@keyframes rightToleft {
0% {
transform: translate(100vw);
}
100% {
transform: translate(-100%);
}
}
</style>
不足
这种只适合简单的弹幕,复杂的话,最好封装弹幕类,有空我在研究研究。
后来产品新增,一进来页面就希望满屏弹幕,想了想,在显示集合里,配置了各自的初始位置和动画时间,因为逻辑不复杂,就不贴代码了。
有兴趣的可以看看代码。