在我们开发自己的博客时候都会有留言功能,除了传统的类似评论的留言板,还有弹幕形式就像这样的效果,今天就记录一下实现的方法,大家可以去原网站看看小贺的神奇网站 (mxll.xyz),这个也是我的博客,目前也正在完善
大致的思路就是弹幕盒子从右到左移动,溢出屏幕时候销毁盒子就可以了,先上完整代码然后再进行分析。
第一阶段:准备变量
const danmusList = ref<any>([]);
const danmus = ref<any>([]);
const currentDanmuIndex = ref(0);
const danmuContainer = ref<any>(null);
const usInp = ref<String>("");
const num = ref(0);
const player = ref<any>(null);
这里是创建的变量,使用了ts,如果是js去除类型定义就行了并不影响。
danmusList 是获取的后台的弹幕列表。
danmus 是弹幕列表用于渲染,可能有人会问为啥不用danmusList渲染,别忘了弹幕消失后还要删除dom,但是在vue这些框架中只需要更改数据就行了。别忘了那句话数据驱动视图!!
currentDanmuIndex 是弹幕行数的索引,就是在页面上看到的第几行,这个是用于计算的。
currentDanmuIndex 是获取存放弹幕的盒子。
usInp 是输入弹幕。
num 用作结束定时器的判断条件。
player 保存定时器函数。
第二阶段:编写函数
创建一个用于控制弹幕列表的函数,直接往danmus中添加要循环的弹幕的信息,然后定义一个定时器在这个弹幕离开屏幕后删除这个数据就好了,这里说两点setTimeout在执行完毕就会销毁,然后可能还有人会问我怎么知道16秒后我的弹幕就会离开屏幕,这里先卖个关子,在下面会提到。
const addDanmu = (item: any) => {
danmus.value.push(item);
// 等待动画完成后移除弹幕
setTimeout(() => {
const index = danmus.value.findIndex((d: any) => d.id === item.id);
if (index !== -1) {
danmus.value.splice(index, 1);
}
}, 16000);
};
这个函数是用于控制弹幕的轨道,说人话就说屏幕显示几行弹幕,其中minTop 表示第一个弹幕距离顶部的距离,然后lineCount 就表示弹幕的行数,剩下的就是计算每一行弹幕应该距离顶部的距离啦。
const calculateTop = () => {
const minTop = 100;
const lineCount = 15;
const availableHeight = danmuContainer.value.offsetHeight - minTop || 0;
const lineHeight = availableHeight / lineCount;
const lineIndex = currentDanmuIndex.value++ % lineCount;
return lineIndex * lineHeight + minTop;
};
重点来了(敲黑板),这个函数是请求数据然后添加数据,这里要注意吧请求数据换成自己的,然后判断在没有数据时候结束函数(这里其实有bug,最后再说),创建一个定时器添加重置条件,定时器的无限循环的,然后使用 addDanmu函数添加弹幕的信息,注意看addDanmu函数中传的duration这个值,这个是用于弹幕从左到右的时间,之前我说清除弹幕是一个定时器,那个定时器的时间就和这个有关,css动画效果animationDuration表示的是动画运动一个周期的时间,就是一来一回,咱们只需要去不需要回来,所以清除变量的定时器是这个时间的一半就好。最后这个player这个定时器的执行时间越快弹幕距离就越近。
const danmuFn = async () => {
num.value = 0;
const res = await danmuApi(); //注意这里
danmusList.value = res.data; //还有这里
if (danmusList.value.length === 0) return;
player.value = setInterval(() => {
if (num.value >= danmusList.value.length) {
num.value = 0;
}
addDanmu({
id: uuid(),
text: danmusList.value[num.value]["danmu"],
top: calculateTop(),
left: "100vw",
duration: Math.random() * (30 - 27) + 27,
color: danmusList.value[num.value]["act"] ? "#f50" : "#fff",
});
num.value = num.value + 1;
}, 100);
};
这个是点击按钮添加弹幕,这个判断是空就结束,主要是当用户添加弹幕没必要在从新渲染弹幕盒子,直接添加一个数据就好了。
const addDanmuFn = async () => {
if (usInp.value.replace(/\s*/g, "") === '') return
const res = await danmuAddApi({
danmu: usInp.value.replace(/\s*/g, ""),
});
if (res.code) {
addDanmu({
id: uuid(),
text: usInp.value.replace(/\s*/g, ""),
top: calculateTop(),
left: "100vw",
duration: Math.random() * (30 - 28) + 28,
color: "#f50",
});
}
usInp.value = "";
};
最后还有两点,在nuxt中使在onNuxtReady中执行函数,vue中使用onMounted就可以,最后记得组件销毁时候结束结束定时器。
onNuxtReady(() => {
danmuFn();
});
onUnmounted(() => {
clearInterval(player.value);
});
第三阶段:编写样式
js完成之后就要开始html部分以及css部分,都比较简单。
<div id="danmu-container" ref="danmuContainer" class="danmu-container">
<div
v-for="danmu in danmus"
:key="danmu.id"
class="danmu"
:style="{
top: danmu.top + 'px',
left: danmu.left,
animationDuration: danmu.duration + 's',
color: danmu.color,
}"
>
{{ danmu.text }}
</div>
</div>
.danmu-container {
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
.danmu {
position: absolute;
white-space: nowrap;
color: white;
padding: 5px;
user-select: none;
opacity: 0.8;
animation-name: slide;
animation-timing-function: linear;
}
@keyframes slide {
from {
transform: translateX(0);
}
to {
transform: translateX(-(100vw + 100%));
}
}
}
以下是完成的代码,根据自己的需求进行更改即可:需要注意的是里面有一些引入的公共方法还有图片以及css变量等需要根据自己的代码进行更改!
<script setup lang="ts">
import uuid from "~/utils/uuid";
import {danmuApi, danmuAddApi} from "~/service/api/art";
const danmusList = ref<any>([]);
const danmus = ref<any>([]);
const currentDanmuIndex = ref(0);
const danmuContainer = ref<any>(null);
const addDanmu = (item: any) => {
danmus.value.push(item);
// 等待动画完成后移除弹幕
setTimeout(() => {
const index = danmus.value.findIndex((d: any) => d.id === item.id);
if (index !== -1) {
danmus.value.splice(index, 1);
}
}, 16000);
};
const calculateTop = () => {
const minTop = 100;
const lineCount = 15;
const availableHeight = danmuContainer.value.offsetHeight - minTop || 0;
const lineHeight = availableHeight / lineCount;
const lineIndex = currentDanmuIndex.value++ % lineCount;
return lineIndex * lineHeight + minTop;
};
const num = ref(0);
const player = ref<any>(null);
const danmuFn = async () => {
num.value = 0;
const res = await danmuApi();
danmusList.value = res.data;
if (danmusList.value.length <= 50) {
currentDanmuIndex.value = 0;
}
if (danmusList.value.length === 0) return;
player.value = setInterval(() => {
if (num.value >= danmusList.value.length) {
num.value = 0;
}
addDanmu({
id: uuid(),
text: danmusList.value[num.value]["danmu"],
top: calculateTop(),
left: "100vw",
duration: Math.random() * (30 - 27) + 27,
color: danmusList.value[num.value]["act"] ? "#f50" : "#fff",
});
num.value = num.value + 1;
}, 100);
};
onNuxtReady(() => {
danmuFn();
});
onUnmounted(() => {
clearInterval(player.value);
});
const usInp = ref<String>("");
const addDanmuFn = async () => {
if (usInp.value.replace(/\s*/g, "") === '') return
const res = await danmuAddApi({
danmu: usInp.value.replace(/\s*/g, ""),
});
if (res.code) {
addDanmu({
id: uuid(),
text: usInp.value.replace(/\s*/g, ""),
top: calculateTop(),
left: "100vw",
duration: Math.random() * (30 - 28) + 28,
color: "#f50",
});
}
usInp.value = "";
};
</script>
<template>
<div class="bg-img"></div>
<div class="barrage">
<!--顶层发送-->
<div class="bar-input">
<label>
<input type="text" placeholder="发送弹幕(建议10个字以内...)" v-model="usInp"/>
<button type="button" @click="addDanmuFn">发送</button>
</label>
</div>
<!--显示弹幕-->
<div id="danmu-container" ref="danmuContainer" class="danmu-container">
<div
v-for="danmu in danmus"
:key="danmu.id"
class="danmu"
:style="{
top: danmu.top + 'px',
left: danmu.left,
animationDuration: danmu.duration + 's',
color: danmu.color,
}"
>
{{ danmu.text }}
</div>
</div>
</div>
</template>
<style scoped lang="less">
.barrage {
width: 100%;
min-height: calc(100vh);
position: relative;
display: flex;
align-items: center;
justify-content: center;
.bar-input {
label {
display: inline-flex;
flex-direction: column;
gap: 10px;
input {
display: inline-flex;
padding: 5px 10px;
border: none;
outline: none;
background: transparent;
font-size: var(--fs-16);
color: #fff;
border-radius: 10px;
background: rgba(255, 255, 255, 0.1);
&::placeholder {
color: #000;
}
}
button {
padding: 5px 0;
border-radius: 10px;
background: rgba(0, 0, 0, 0.5);
color: #fff3ec;
font-size: var(--fs-16);
cursor: pointer;
}
}
}
.danmu-container {
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
.danmu {
position: absolute;
white-space: nowrap;
color: white;
padding: 5px;
user-select: none;
opacity: 0.8;
animation-name: slide;
animation-timing-function: linear;
}
@keyframes slide {
from {
transform: translateX(0);
}
to {
transform: translateX(-(100vw + 100%));
}
}
}
}
.bg-img {
background-image: url("../assets/images/re4.webp");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
position: fixed;
z-index: -1;
top: 0;
right: 0;
left: 0;
bottom: 0;
}
</style>
结语
之前提到了这个有bug,是在于如果最开始弹幕只有一条,那么整个屏幕的弹幕都会是这一条,后续我进行了优化,打算实现的效果是在弹幕较少的时候,用于存放弹幕数据的数组清空时候再次执行添加弹幕的函数但是效果不佳,最后就在数据库添加了十几条初始的弹幕这样会好看一些。
还有一点就是有的网站弹幕是可以清空并且暂停和从新开始滚动,这个我们已经有开始弹幕的函数了,只用添加有一个点击事件然后清空存放数据的数组即可~
最后欢迎大家评论区留言