hookehuyr

feat(弹幕组件): 优化弹幕循环逻辑并添加预加载功能

重构弹幕组件实现无缝循环播放,避免弹幕突然消失
添加预加载功能使弹幕在页面加载时就开始滚动
优化弹幕间距控制和淡出效果
......@@ -93,6 +93,7 @@ const isPlaying = ref(false)
const danmuId = ref(0)
const trackHeight = ref(110) // 适配最多3行文字显示,确保弹幕之间有足够间距
const animationTimers = ref(new Map()) // 存储动画定时器
const FADE_DURATION = 400 // 弹幕靠近左侧时淡出时长(毫秒)
// 轨道系统
const tracks = reactive(
......@@ -117,6 +118,8 @@ const fetchDanmuData = async () => {
familyIntro: family.note,
avatar: family.avatar_url || defaultAvatar
}))
// 重置弹幕索引,确保从头开始遍历新数据
resetDanmuIndex()
}
} catch (error) {
console.error('获取弹幕数据失败:', error)
......@@ -130,7 +133,7 @@ const getDanmuStyle = (danmu, trackIndex) => {
return {
transform: `translateX(${danmu.x}rpx)`,
transition: danmu.isMoving ? `transform ${danmu.duration}ms linear` : 'none',
transition: danmu.isMoving ? `transform ${danmu.duration}ms linear, opacity ${FADE_DURATION}ms linear` : 'none',
opacity: danmu.opacity || 1,
// 明确设置垂直位置
position: 'absolute',
......@@ -138,13 +141,15 @@ const getDanmuStyle = (danmu, trackIndex) => {
top: `${topPosition}rpx`,
zIndex: 10,
// 添加硬件加速
willChange: 'transform'
willChange: 'transform, opacity'
}
}
// 检查轨道中是否有足够空间放置新弹幕
const checkTrackSpace = (track, danmuWidth = 350) => {
// options.ignoreTime: 跳过时间间隔限制,用于同轨跟随插入,保证无缝
const checkTrackSpace = (track, danmuWidth = 350, options = {}) => {
const now = Date.now()
const ignoreTime = options.ignoreTime === true
// 如果轨道为空,可以直接放置
if (track.danmus.length === 0) {
......@@ -186,9 +191,9 @@ const checkTrackSpace = (track, danmuWidth = 350) => {
// 检查最后一个弹幕的时间间隔 - 更严格的时间控制
const timeSinceLastDanmu = now - track.lastDanmuTime
const minTimeInterval = 2500 // 增加到2.5秒,确保足够的时间间隔
const minTimeInterval = 2500 // 增加到2.5秒,确保足够的时间间隔(正常模式)
return timeSinceLastDanmu >= minTimeInterval
return ignoreTime ? true : timeSinceLastDanmu >= minTimeInterval
}
// 找到可用的轨道 - 随机选择而非按顺序
......@@ -225,12 +230,12 @@ const createDanmu = (data) => {
}
// 添加弹幕到轨道
const addDanmuToTrack = (danmu) => {
const track = findAvailableTrack()
const addDanmuToTrack = (danmu, targetTrack = null, options = {}) => {
const track = targetTrack || findAvailableTrack()
if (!track) return false
// 更严格的最终检查 - 确保轨道真的有足够空间
if (!checkTrackSpace(track)) {
if (!checkTrackSpace(track, 350, options)) {
return false
}
......@@ -284,9 +289,30 @@ const startDanmuAnimation = (danmu, track) => {
// 计算剩余动画时间
const remainingTime = danmu.duration - elapsedTime
// 在动画末尾前触发淡出,避免到最左侧突然消失
const fadeStartDelay = Math.max(0, remainingTime - FADE_DURATION)
setTimeout(() => {
danmu.opacity = 0
}, fadeStartDelay)
// 在本弹幕进入一定进度后,尝试在同一轨道追加下一条,保证无缝滚动
const followProgress = 0.08 // 更早跟随,缩小间隙
const followDelay = Math.max(200, danmu.duration * followProgress)
setTimeout(() => {
if (!isPlaying.value) return
// 仅当轨道有空间时在同轨追加下一条弹幕
if (checkTrackSpace(track, 350, { ignoreTime: true })) {
const nextData = getNextUnusedDanmu()
if (nextData) {
const nextDanmu = createDanmu(nextData)
addDanmuToTrack(nextDanmu, track, { ignoreTime: true })
}
}
}, followDelay)
// 监听动画结束事件,实现循环
const timer = setTimeout(() => {
// 动画结束后,重置弹幕位置到右侧,开始新的循环
// 动画结束后,重置弹幕位置到右侧,开始新的循环(复用元素)
resetDanmuForLoop(danmu, track)
}, remainingTime)
......@@ -297,24 +323,24 @@ const startDanmuAnimation = (danmu, track) => {
const resetDanmuForLoop = (danmu, track) => {
if (!isPlaying.value) return
// 立即停止当前动画
// 停止当前动画并复位到右侧,复用元素,避免删除/新增造成的空档
danmu.isMoving = false
danmu.opacity = 1
// 从轨道中移除这个弹幕,避免重叠检查时的干扰
removeDanmuFromTrack(danmu, track)
// 切换下一条数据,遵循首轮不重复规则
const nextData = getNextUnusedDanmu()
if (nextData) {
danmu.data = nextData
}
// 使用 nextTick 确保 DOM 更新
nextTick(() => {
// 重新创建弹幕数据,避免状态污染
const newDanmuData = { ...danmu.data }
const newDanmu = createDanmu(newDanmuData)
// 复位到右侧并重新计时
danmu.startTime = Date.now()
danmu.x = 750
track.lastDanmuTime = Date.now()
// 延迟重新添加,确保轨道空间检查正确
setTimeout(() => {
if (isPlaying.value) {
addDanmuToTrack(newDanmu)
}
}, 100 + Math.random() * 200) // 随机延迟100-300ms,避免同时重置造成的重叠
// 重新启动动画(无缝继续)
nextTick(() => {
startDanmuAnimation(danmu, track)
})
}
......@@ -335,103 +361,63 @@ const sendDanmu = (data) => {
}
// 初始化满屏弹幕显示
const initFullScreenDanmus = () => {
if (!isPlaying.value) return
const screenWidth = 750 // 屏幕宽度
const danmuWidth = 350 // 弹幕宽度
// 为每个轨道创建初始弹幕,确保轨道间距合理
tracks.forEach((track, trackIndex) => {
// 每个轨道放置2个弹幕,避免过度拥挤
const danmusPerTrack = 2
// 弹幕索引管理
let currentDanmuIndex = 0
let usedDanmuIds = new Set() // 记录已使用的弹幕ID
let isFirstRound = true // 标记是否是第一轮遍历
for (let i = 0; i < danmusPerTrack; i++) {
const danmuData = familyDanmus.value[currentDanmuIndex % familyDanmus.value.length]
currentDanmuIndex++
// 重置弹幕索引和去重记录
const resetDanmuIndex = () => {
currentDanmuIndex = 0
usedDanmuIds.clear()
isFirstRound = true
}
// 计算弹幕持续时间
const duration = (screenWidth + danmuWidth + 100) / (props.danmuSpeed / 1000)
// 计算已经运行的时间,确保弹幕间有足够间距
// 第一个弹幕运行时间较长,第二个弹幕运行时间较短,形成间距
const baseRunTime = i * (duration * 0.4) // 每个弹幕间隔40%的动画时长
const randomOffset = Math.random() * (duration * 0.2) // 添加20%的随机偏移
const randomRunTime = baseRunTime + randomOffset
const startTime = Date.now() - randomRunTime
// 计算初始位置,基于已经运行的时间
const totalDistance = screenWidth + danmuWidth + 100
const progress = Math.min(randomRunTime / duration, 0.9) // 最多运行90%,确保弹幕可见
const initialX = screenWidth - (progress * totalDistance)
const danmu = {
id: `init-${trackIndex}-${i}-${Date.now()}-${Math.random()}`,
data: danmuData,
x: initialX,
duration: duration,
isMoving: true, // 立即开始移动
opacity: 1,
startTime: startTime // 使用计算出的开始时间
// 获取下一个未使用的弹幕数据
const getNextUnusedDanmu = () => {
if (!familyDanmus.value || familyDanmus.value.length === 0) {
return null
}
track.danmus.push(danmu)
// 更新轨道的最后弹幕时间,确保后续弹幕有合理间隔
track.lastDanmuTime = Date.now() - (danmusPerTrack - i - 1) * 1500 // 每个弹幕间隔1.5秒
// 如果是第一轮且还有未使用的弹幕
if (isFirstRound && usedDanmuIds.size < familyDanmus.value.length) {
// 查找下一个未使用的弹幕
while (currentDanmuIndex < familyDanmus.value.length) {
const danmuData = familyDanmus.value[currentDanmuIndex]
const danmuId = danmuData.id
// 立即启动动画,不延迟
nextTick(() => {
startDanmuAnimation(danmu, track)
})
if (!usedDanmuIds.has(danmuId)) {
usedDanmuIds.add(danmuId)
currentDanmuIndex++
return danmuData
}
currentDanmuIndex++
}
})
}
// 当前弹幕索引,用于顺序显示
let currentDanmuIndex = 0
// 发送顺序弹幕 - 根据可用轨道数量同时填充弹幕
const sendSequentialDanmu = () => {
// 获取所有可用的轨道
const availableTracks = tracks.filter(track => checkTrackSpace(track))
if (availableTracks.length === 0) {
return // 没有可用轨道,直接返回
// 第一轮遍历完成
isFirstRound = false
currentDanmuIndex = 0
usedDanmuIds.clear()
}
// 为每个可用轨道同时发送一个弹幕
availableTracks.forEach(track => {
// 按顺序获取弹幕数据
// 第一轮遍历完成后,重新开始循环
if (!isFirstRound) {
const danmuData = familyDanmus.value[currentDanmuIndex % familyDanmus.value.length]
// 发送弹幕到指定轨道
sendDanmuToTrack(danmuData, track)
// 更新索引,循环使用
currentDanmuIndex++
})
}
// 发送弹幕到指定轨道
const sendDanmuToTrack = (danmuData, track) => {
const danmu = {
id: `danmu-${++danmuId.value}-${Date.now()}-${Math.random()}`,
data: danmuData,
x: 750, // 从右侧开始
duration: 0,
isMoving: false,
opacity: 1,
startTime: Date.now()
return danmuData
}
// 添加到轨道
track.danmus.push(danmu)
track.lastDanmuTime = Date.now()
return null
}
// 启动动画
nextTick(() => {
startDanmuAnimation(danmu, track)
})
// 发送顺序弹幕
const sendSequentialDanmu = () => {
// 获取下一个未使用的弹幕数据
const danmuData = getNextUnusedDanmu()
if (danmuData) {
sendDanmu(danmuData)
}
}
// 自动发送弹幕
......@@ -452,16 +438,34 @@ const startAutoSend = () => {
// 动态调整发送间隔,根据可用轨道数量
const availableTrackCount = tracks.filter(track => checkTrackSpace(track)).length
const baseInterval = 1200 // 基础间隔1.2秒
const intervalMultiplier = Math.max(1, (6 - availableTrackCount) * 0.3) // 可用轨道越少,间隔越长
const dynamicInterval = baseInterval * intervalMultiplier + Math.random() * 400
const baseInterval = 800 // 减少基础间隔到0.8秒,让弹幕更密集
const intervalMultiplier = Math.max(1, (6 - availableTrackCount) * 0.2) // 减少间隔倍数
const dynamicInterval = baseInterval * intervalMultiplier + Math.random() * 300
autoSendTimer = setTimeout(sendNext, dynamicInterval)
}
// 立即发送第一条弹幕,不等待
sendNext()
}
// 预加载弹幕,让弹幕在组件显示前就开始滚动
const preloadDanmus = () => {
if (!isPlaying.value) return
// 立即发送多条弹幕到不同轨道,模拟已经在滚动的状态
const preloadCount = Math.min(tracks.length, familyDanmus.value.length)
for (let i = 0; i < preloadCount; i++) {
// 为每个轨道发送一条弹幕,并设置不同的延迟
setTimeout(() => {
if (isPlaying.value) {
sendSequentialDanmu()
}
}, i * 200) // 每条弹幕间隔200ms发送
}
}
const stopAutoSend = () => {
if (autoSendTimer) {
clearTimeout(autoSendTimer)
......@@ -528,15 +532,15 @@ onMounted(async () => {
// 自动开始播放
isPlaying.value = true
// 使用nextTick确保DOM完全渲染后再初始化弹幕
// 使用nextTick确保DOM完全渲染后立即开始弹幕滚动
nextTick(() => {
// 立即初始化满屏弹幕,避免白屏状态
initFullScreenDanmus()
// 先预加载弹幕,让多条弹幕快速出现
preloadDanmus()
// 稍微延迟启动自动发送,让初始弹幕先稳定显示
// 然后启动自动发送,维持持续滚动
setTimeout(() => {
startAutoSend()
}, 1000) // 1秒后开始自动发送新弹幕
}, 1000) // 1秒后开始正常的自动发送节奏
})
})
......
......@@ -124,8 +124,8 @@
</view>
</view>
<!-- 弹幕显示助力榜内容 -->
<view v-if="activeTab === 'support'" class="danmu-section">
<!-- 隐藏的弹幕组件,在页面加载时就开始滚动 -->
<view class="danmu-section" :style="{ visibility: activeTab === 'support' ? 'visible' : 'hidden', position: activeTab === 'support' ? 'static' : 'absolute', top: activeTab === 'support' ? 'auto' : '-9999px' }">
<NativeDanmuComponent
:container-height="700"
:show-controls="true"
......
......@@ -156,8 +156,8 @@
</view>
<!-- 弹幕显示助力榜内容 -->
<view v-if="activeTab === 'support'" class="danmu-section mt-8">
<!-- 隐藏的弹幕组件,在页面加载时就开始滚动 -->
<view class="danmu-section mt-8" :style="{ visibility: activeTab === 'support' ? 'visible' : 'hidden', position: activeTab === 'support' ? 'static' : 'absolute', top: activeTab === 'support' ? 'auto' : '-9999px' }">
<NativeDanmuComponent
:container-height="1200"
:show-controls="true"
......