目录
1.准备元素与样式
<div class="container"></div>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
width: 100vw;
height: 100vh;
background-image: url(https://pic4.zhimg.com/v2-17ec8cd47fa9fcf0b2d1094c5741c7d7_r.jpg);
background-repeat: no-repeat;
background-size: cover;
position: relative;
overflow: hidden;
}
.bullet {
position: absolute;
top: 0;
z-index: 100;
color: #fff;
display: flex;
align-items: center;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 25px;
padding-right: 10px;
white-space: nowrap;
cursor: pointer;
}
.bullet:hover {
animation-play-state: paused;
box-shadow: 0px 0px 15px #fff;
transition: all 0.5s;
}
.nickname {
margin-left: 5px;
margin-right: 5px;
font-weight: 700;
}
.bullet-waiting {
left: 100%;
}
.bullet-done {
left: 0%;
transform: translateX(-100%);
}
.bullet-running {
animation: run 5s linear;
}
@keyframes run {
from {
left: 100%;
transform: translateX(0);
}
to {
left: 0%;
transform: translateX(-100%);
}
}
.bullet img {
width: 40px;
height: 40px;
border-radius: 50%;
}
2.渲染弹幕
// 弹幕数据
const bulletData = [
{ id: 1,
nickname: '小王',
content: '你cscscscscscscscscscsc好',
avatar: "https://q2.itc.cn/q_70/images03/20240320/3df8c75302424a05864e62215a548e28.jpeg",
status: "waiting"
},
{ id: 2,
nickname: '小李',
content: '你好cscscscscscscc666',
avatar: "https://q4.itc.cn/q_70/images03/20240320/50c0867b3ae84ee88082ac4cef647a4d.jpeg",
status: "waiting"
},
{ id: 3,
nickname: '小红',
content: 'Arecc you ok ?',
avatar: "https://q0.itc.cn/q_70/images03/20240320/688db9e4a3fd44fabdaa1e5d1a6f07c4.jpeg",
status: "waiting"
},
{ id: 4,
nickname: '小张',
content: '你cscscscsc好xasxaxax',
avatar: "https://q1.itc.cn/q_70/images03/20240320/24386ce5c85b4742a9a74d2e57f498ea.jpeg",
status: "waiting"
},
{ id: 5,
nickname: '小刘',
content: '你好发达按时吃撒参数擦拭擦拭擦擦',
avatar: "https://q8.itc.cn/q_70/images03/20240320/cef5cfa10c0b42348dabf346d8c7eedb.jpeg",
status: "waiting"
},
{ id: 6,
nickname: '小王',
content: '你好',
avatar: "https://q2.itc.cn/q_70/images03/20240320/3df8c75302424a05864e62215a548e28.jpeg",
status: "waiting"
},
{ id: 7,
nickname: '小李',
content: '你好666',
avatar: "https://q4.itc.cn/q_70/images03/20240320/50c0867b3ae84ee88082ac4cef647a4d.jpeg",
status: "waiting"
},
{ id: 8,
nickname: '小红',
content: 'Are you ok ?',
avatar: "https://q0.itc.cn/q_70/images03/20240320/688db9e4a3fd44fabdaa1e5d1a6f07c4.jpeg",
status: "waiting"
},
{ id: 9,
nickname: '小张',
content: '你好xasxaxax',
avatar: "https://q1.itc.cn/q_70/images03/20240320/24386ce5c85b4742a9a74d2e57f498ea.jpeg",
status: "waiting"
},
{ id: 10,
nickname: '小刘',
content: '你好发达按时吃撒参数擦拭擦拭擦擦',
avatar: "https://q8.itc.cn/q_70/images03/20240320/cef5cfa10c0b42348dabf346d8c7eedb.jpeg",
status: "waiting"
},
{ id: 11,
nickname: '小王',
content: '你好',
avatar: "https://q2.itc.cn/q_70/images03/20240320/3df8c75302424a05864e62215a548e28.jpeg",
status: "waiting"
},
{ id: 12,
nickname: '小李',
content: '你好666',
avatar: "https://q4.itc.cn/q_70/images03/20240320/50c0867b3ae84ee88082ac4cef647a4d.jpeg",
status: "waiting"
},
{ id: 13,
nickname: '小红',
content: 'Are you ok ?',
avatar: "https://q0.itc.cn/q_70/images03/20240320/688db9e4a3fd44fabdaa1e5d1a6f07c4.jpeg",
status: "waiting"
},
{ id: 14,
nickname: '小张',
content: '你好xasxaxax',
avatar: "https://q1.itc.cn/q_70/images03/20240320/24386ce5c85b4742a9a74d2e57f498ea.jpeg",
status: "waiting"
},
{ id: 15,
nickname: '小刘',
content: '你好发达按时吃撒参数擦拭擦拭擦擦',
avatar: "https://q8.itc.cn/q_70/images03/20240320/cef5cfa10c0b42348dabf346d8c7eedb.jpeg",
status: "waiting"
},
{ id: 16,
nickname: '小王',
content: '你好',
avatar: "https://q2.itc.cn/q_70/images03/20240320/3df8c75302424a05864e62215a548e28.jpeg",
status: "waiting"
},
{ id: 17,
nickname: '小李',
content: '你好666',
avatar: "https://q4.itc.cn/q_70/images03/20240320/50c0867b3ae84ee88082ac4cef647a4d.jpeg",
status: "waiting"
},
{ id: 18,
nickname: '小红',
content: 'Are you ok ?',
avatar: "https://q0.itc.cn/q_70/images03/20240320/688db9e4a3fd44fabdaa1e5d1a6f07c4.jpeg",
status: "waiting"
},
{ id: 19,
nickname: '小张',
content: '你好xasxaxax',
avatar: "https://q1.itc.cn/q_70/images03/20240320/24386ce5c85b4742a9a74d2e57f498ea.jpeg",
status: "waiting"
},
{ id: 20,
nickname: '小刘',
content: '你好发达按时吃撒参数擦拭擦拭擦擦',
avatar: "https://q8.itc.cn/q_70/images03/20240320/cef5cfa10c0b42348dabf346d8c7eedb.jpeg",
status: "waiting"
}
]
// 添加弹幕
function createMessage() {
const container = document.querySelector(".container")
bulletData.forEach(message => {
container.innerHTML += `
<div
id="${message.id}"
class="bullet-${message.status} bullet"
>
<div class="avatar">
<img src="${message.avatar}" alt="" />
</div>
<div class="nickname">${ message.nickname }:</div>
<div class="content">${ message.content }</div>
</div>`
})
}
createMessage()
3.开启弹幕运动
这里使用的css关键帧动画,为每个bullet元素添加对应的类名,来控制其对应的显示状态
// 开启弹幕运动
function startMessage(item) {
const bullet = document.getElementById(item.id)
bullet.classList.remove("bullet-waiting")
bullet.classList.add("bullet-running")
// 监听元素的动画结束事件
bullet.addEventListener("animationend", () => {
bullet.classList.remove("bullet-running")
bullet.classList.add("bullet-done")
// 一秒后重新添加弹幕
setTimeout(() => {
bullet.classList.remove("bullet-done")
bullet.classList.add("bullet-waiting")
startMessage(item)
})
})
}
这个函数可以开启磨一个弹幕的运动,由于页面上的弹幕众多,我们首次渲染是需要分次开启运动状态。
4.生成轨道,控制弹幕纵向位置
这里为了分清楚一次性最多添加多少弹幕以及弹幕的纵向位置,我们生成一个轨道数组,用于存放当前页面的轨道信息,每一项保存轨道位置和当前轨道状态。
// 每条轨道高度
const trackHeight = 70
// 轨道数据
const trackData = []
// 生成轨道
function createTrack() {
const container = document.querySelector(".container")
// 计算轨道数量
const trackCount = Math.floor(container.clientHeight / trackHeight)
// 生成轨道数据
for(let i = 0 ; i < trackCount ; i++) {
trackData.push({
offset: i,
disabled: false
})
}
}
createTrack()
我们再编写一个为元素分配轨道的函数,其中每次分配后将本次轨道禁用,一定时间后重新开启轨道,这样在首次渲染时就不会有多个弹幕挤在同一轨道了。
// 分配轨道
function assignTrack(item) {
// 获取可用轨道
const availableTrack = trackData.filter(track => !track.disabled)
// 随机获取一个可用轨道
console.log(availableTrack);
const index = Math.floor(Math.random() * availableTrack.length)
const track = availableTrack[index]
// 分配轨道
item.track = track
// 禁用轨道
track.disabled = true
setTimeout(() => {
track.disabled = false
},100)
}
我们在开启弹幕的函数中调用分配轨道函数
// 开启弹幕运动
function startMessage(item) {
// 分配轨道
assignTrack(item)
const bullet = document.getElementById(item.id)
bullet.classList.remove("bullet-waiting")
bullet.classList.add("bullet-running")
bullet.style.top = `${item.track.offset * trackHeight}px`
// 监听元素的动画结束事件
bullet.addEventListener("animationend", () => {
bullet.classList.remove("bullet-running")
bullet.classList.add("bullet-done")
// 一秒后重新添加弹幕
setTimeout(() => {
bullet.classList.remove("bullet-done")
bullet.classList.add("bullet-waiting")
startMessage(item)
})
})
}
5.分批渲染
由于弹幕数量原因,我们通过一个函数来分批渲染,每次渲染最大数量为当前轨道数。
// 分批渲染
function startAllMessage() {
// 批量添加弹幕
for(let i = 0 ; i < trackData.length; i++) {
const start = i * trackData.length
const end = start + trackData.length
const data = bulletData.slice(start,end)
setTimeout(() => {
data.forEach((item) => startMessage(item))
}, 1000 * i)
}
}
startAllMessage()
6.控制每条轨道分配弹幕的间隔
比如当第一批弹幕渲染之后,由于弹幕的长度不一,轨道禁止使用的时长也是不同的。因此我们为了下一次渲染弹幕优先寻找已经完全空闲的轨道,我们需要动态计算当前轨道禁用的时长。我们假设当前弹幕运动时间为5s(后续会动态改变每个弹幕的运动时间,以此来改变速度),弹幕的运动速度=(页面宽度 + 弹幕本身宽度)/ 运动时间,所以弹幕全完进入页面耗时=弹幕自身长度 / 运动速度,以此我们计算出了弹幕全完进入页面所需要的时间,这这段时间内此弹幕都是用的轨道需要开启禁用状态。
代码如下:
// 分配轨道
function assignTrack(item) {
// 获取可用轨道
const availableTrack = trackData.filter(track => !track.disabled)
// 随机获取一个可用轨道
console.log(availableTrack);
const index = Math.floor(Math.random() * availableTrack.length)
const track = availableTrack[index]
// 分配轨道
item.track = track
// 计算弹幕进入页面所用时间
const w1 = document.querySelector(".container").clientWidth
const w2 = document.getElementById(item.id).clientWidth
const time = (w2 / (w1 +w2))*5*1000
// 禁用轨道
track.disabled = true
setTimeout(() => {
track.disabled = false
},time )
}
7.设置每个弹幕的颜色与时间
通过随机数生成每个弹幕元素的动画时间,使用hsl通过一个随机角度值来随机生成弹幕颜色。
// 生成弹幕随机运动时间
item.duration = Math.floor(Math.random() * 15) + 10
bullet.style.animationDuration = `${item.duration}s`
// 生成随机颜色
const deg = Math.floor(Math.random() * 360)
bullet.style.color = `hsl(${deg}, 100%, 50%)`
总结:
完整代码:
<!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>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.container {
width: 100vw;
height: 100vh;
background-image: url(https://pic4.zhimg.com/v2-17ec8cd47fa9fcf0b2d1094c5741c7d7_r.jpg);
background-repeat: no-repeat;
background-size: cover;
position: relative;
overflow: hidden;
}
.bullet {
position: absolute;
top: 0;
z-index: 100;
color: #fff;
display: flex;
align-items: center;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 25px;
padding-right: 10px;
white-space: nowrap;
cursor: pointer;
}
.bullet:hover {
animation-play-state: paused;
box-shadow: 0px 0px 15px #fff;
transition: all 0.5s;
}
.nickname {
margin-left: 5px;
margin-right: 5px;
font-weight: 700;
}
.bullet-waiting {
left: 100%;
}
.bullet-done {
left: 0%;
transform: translateX(-100%);
}
.bullet-running {
animation: run 5s linear;
}
@keyframes run {
from {
left: 100%;
transform: translateX(0);
}
to {
left: 0%;
transform: translateX(-100%);
}
}
.bullet img {
width: 40px;
height: 40px;
border-radius: 50%;
}
</style>
</head>
<body>
<div class="container"></div>
<script>
// 弹幕数据
const bulletData = [
{ id: 1,
nickname: '小王',
content: '你cscscscscscscscscscsc好',
avatar: "https://q2.itc.cn/q_70/images03/20240320/3df8c75302424a05864e62215a548e28.jpeg",
status: "waiting"
},
{ id: 2,
nickname: '小李',
content: '你好cscscscscscscc666',
avatar: "https://q4.itc.cn/q_70/images03/20240320/50c0867b3ae84ee88082ac4cef647a4d.jpeg",
status: "waiting"
},
{ id: 3,
nickname: '小红',
content: 'Arecc you ok ?',
avatar: "https://q0.itc.cn/q_70/images03/20240320/688db9e4a3fd44fabdaa1e5d1a6f07c4.jpeg",
status: "waiting"
},
{ id: 4,
nickname: '小张',
content: '你cscscscsc好xasxaxax',
avatar: "https://q1.itc.cn/q_70/images03/20240320/24386ce5c85b4742a9a74d2e57f498ea.jpeg",
status: "waiting"
},
{ id: 5,
nickname: '小刘',
content: '你好发达按时吃撒参数擦拭擦拭擦擦',
avatar: "https://q8.itc.cn/q_70/images03/20240320/cef5cfa10c0b42348dabf346d8c7eedb.jpeg",
status: "waiting"
},
{ id: 6,
nickname: '小王',
content: '你好',
avatar: "https://q2.itc.cn/q_70/images03/20240320/3df8c75302424a05864e62215a548e28.jpeg",
status: "waiting"
},
{ id: 7,
nickname: '小李',
content: '你好666',
avatar: "https://q4.itc.cn/q_70/images03/20240320/50c0867b3ae84ee88082ac4cef647a4d.jpeg",
status: "waiting"
},
{ id: 8,
nickname: '小红',
content: 'Are you ok ?',
avatar: "https://q0.itc.cn/q_70/images03/20240320/688db9e4a3fd44fabdaa1e5d1a6f07c4.jpeg",
status: "waiting"
},
{ id: 9,
nickname: '小张',
content: '你好xasxaxax',
avatar: "https://q1.itc.cn/q_70/images03/20240320/24386ce5c85b4742a9a74d2e57f498ea.jpeg",
status: "waiting"
},
{ id: 10,
nickname: '小刘',
content: '你好发达按时吃撒参数擦拭擦拭擦擦',
avatar: "https://q8.itc.cn/q_70/images03/20240320/cef5cfa10c0b42348dabf346d8c7eedb.jpeg",
status: "waiting"
},
{ id: 11,
nickname: '小王',
content: '你好',
avatar: "https://q2.itc.cn/q_70/images03/20240320/3df8c75302424a05864e62215a548e28.jpeg",
status: "waiting"
},
{ id: 12,
nickname: '小李',
content: '你好666',
avatar: "https://q4.itc.cn/q_70/images03/20240320/50c0867b3ae84ee88082ac4cef647a4d.jpeg",
status: "waiting"
},
{ id: 13,
nickname: '小红',
content: 'Are you ok ?',
avatar: "https://q0.itc.cn/q_70/images03/20240320/688db9e4a3fd44fabdaa1e5d1a6f07c4.jpeg",
status: "waiting"
},
{ id: 14,
nickname: '小张',
content: '你好xasxaxax',
avatar: "https://q1.itc.cn/q_70/images03/20240320/24386ce5c85b4742a9a74d2e57f498ea.jpeg",
status: "waiting"
},
{ id: 15,
nickname: '小刘',
content: '你好发达按时吃撒参数擦拭擦拭擦擦',
avatar: "https://q8.itc.cn/q_70/images03/20240320/cef5cfa10c0b42348dabf346d8c7eedb.jpeg",
status: "waiting"
},
{ id: 16,
nickname: '小王',
content: '你好',
avatar: "https://q2.itc.cn/q_70/images03/20240320/3df8c75302424a05864e62215a548e28.jpeg",
status: "waiting"
},
{ id: 17,
nickname: '小李',
content: '你好666',
avatar: "https://q4.itc.cn/q_70/images03/20240320/50c0867b3ae84ee88082ac4cef647a4d.jpeg",
status: "waiting"
},
{ id: 18,
nickname: '小红',
content: 'Are you ok ?',
avatar: "https://q0.itc.cn/q_70/images03/20240320/688db9e4a3fd44fabdaa1e5d1a6f07c4.jpeg",
status: "waiting"
},
{ id: 19,
nickname: '小张',
content: '你好xasxaxax',
avatar: "https://q1.itc.cn/q_70/images03/20240320/24386ce5c85b4742a9a74d2e57f498ea.jpeg",
status: "waiting"
},
{ id: 20,
nickname: '小刘',
content: '你好发达按时吃撒参数擦拭擦拭擦擦',
avatar: "https://q8.itc.cn/q_70/images03/20240320/cef5cfa10c0b42348dabf346d8c7eedb.jpeg",
status: "waiting"
}
]
// 每条轨道高度
const trackHeight = 70
// 轨道数据
const trackData = []
// 生成轨道
function createTrack() {
const container = document.querySelector(".container")
// 计算轨道数量
const trackCount = Math.floor(container.clientHeight / trackHeight)
// 生成轨道数据
for(let i = 0 ; i < trackCount ; i++) {
trackData.push({
offset: i,
disabled: false
})
}
}
createTrack()
// 分配轨道
function assignTrack(item) {
// 获取可用轨道
const availableTrack = trackData.filter(track => !track.disabled)
// 随机获取一个可用轨道
console.log(availableTrack);
const index = Math.floor(Math.random() * availableTrack.length)
const track = availableTrack[index]
// 分配轨道
item.track = track
// 计算弹幕进入页面所用时间
const w1 = document.querySelector(".container").clientWidth
const w2 = document.getElementById(item.id).clientWidth
const time = (w2 / (w1 +w2))*5*1000
// 禁用轨道
track.disabled = true
setTimeout(() => {
track.disabled = false
},time )
}
// 添加弹幕
function createMessage() {
const container = document.querySelector(".container")
bulletData.forEach(message => {
container.innerHTML += `
<div
id="${message.id}"
class="bullet-${message.status} bullet"
>
<div class="avatar">
<img src="${message.avatar}" alt="" />
</div>
<div class="nickname">${ message.nickname }:</div>
<div class="content">${ message.content }</div>
</div>`
})
}
createMessage()
// 开启弹幕运动
function startMessage(item) {
// 分配轨道
assignTrack(item)
const bullet = document.getElementById(item.id)
bullet.classList.remove("bullet-waiting")
bullet.classList.add("bullet-running")
bullet.style.top = `${item.track.offset * trackHeight}px`
// 生成弹幕随机运动时间
item.duration = Math.floor(Math.random() * 15) + 10
bullet.style.animationDuration = `${item.duration}s`
// 生成随机颜色
const deg = Math.floor(Math.random() * 360)
bullet.style.color = `hsl(${deg}, 100%, 50%)`
// 监听元素的动画结束事件
bullet.addEventListener("animationend", () => {
bullet.classList.remove("bullet-running")
bullet.classList.add("bullet-done")
// 一秒后重新添加弹幕
setTimeout(() => {
bullet.classList.remove("bullet-done")
bullet.classList.add("bullet-waiting")
startMessage(item)
})
})
}
// 分批渲染
function startAllMessage() {
// 批量添加弹幕
for(let i = 0 ; i < trackData.length; i++) {
const start = i * trackData.length
const end = start + trackData.length
const data = bulletData.slice(start,end)
setTimeout(() => {
data.forEach((item) => startMessage(item))
}, 1000 * i)
}
}
startAllMessage()
</script>
</body>
</html>