hookehuyr

perf(弹幕组件): 优化弹幕动画性能并移除淡出效果

- 移除弹幕淡出效果,保持不透明度始终为1
- 引入弹幕宽度测量和动态时长计算,确保速度一致
- 使用常量替换魔法数值,提高代码可维护性
- 优化弹幕位置计算和碰撞检测逻辑
......@@ -17,6 +17,7 @@
v-for="danmu in track.danmus"
:key="danmu.id"
class="danmu-item"
:id="'danmu-' + danmu.id"
:style="getDanmuStyle(danmu, trackIndex)"
@tap="handleDanmuClick(danmu)"
>
......@@ -76,7 +77,7 @@ const props = defineProps({
// 弹幕速度 (rpx/s)
danmuSpeed: {
type: Number,
default: 150 // 提高默认速度,让弹幕移动更流畅
default: 200 // 提高默认速度,让弹幕移动更流畅
},
// 轨道数量
trackCount: {
......@@ -93,7 +94,41 @@ const isPlaying = ref(false)
const danmuId = ref(0)
const trackHeight = ref(110) // 适配最多3行文字显示,确保弹幕之间有足够间距
const animationTimers = ref(new Map()) // 存储动画定时器
const FADE_DURATION = 400 // 弹幕靠近左侧时淡出时长(毫秒)
// 统一移动相关常量,避免魔法数不一致
const CONTAINER_WIDTH = 750
const DANMU_WIDTH = 350
const EXTRA_DISTANCE = 120 // 额外移动距离,确保完全移出视图
// 关闭边缘淡出,保持在视图内始终不改变透明度
const FADE_DURATION = 0
const SYSTEM_INFO = Taro.getSystemInfoSync()
const WINDOW_WIDTH = SYSTEM_INFO.windowWidth
// px 转 rpx
const toRpx = (px) => (px * CONTAINER_WIDTH) / WINDOW_WIDTH
// 获取弹幕宽度(rpx)
const getDanmuWidth = (danmu) => {
return danmu?.widthRpx && danmu.widthRpx > 0 ? danmu.widthRpx : DANMU_WIDTH
}
// 重新计算时长,保证速度一致
const recalculateDuration = (danmu) => {
const width = getDanmuWidth(danmu)
const totalDistance = CONTAINER_WIDTH + width + EXTRA_DISTANCE
danmu.duration = (totalDistance / (props.danmuSpeed * 0.6)) * 1000
}
// 测量弹幕真实宽度(px),存为 rpx
const measureDanmuWidth = (danmu) => new Promise((resolve) => {
try {
const selector = `#danmu-${danmu.id} .danmu-content`
Taro.createSelectorQuery().select(selector).boundingClientRect(rect => {
if (rect && rect.width) {
danmu.widthRpx = toRpx(rect.width)
}
resolve(getDanmuWidth(danmu))
}).exec()
} catch (e) {
resolve(getDanmuWidth(danmu))
}
})
// 轨道系统
const tracks = reactive(
......@@ -133,7 +168,7 @@ const getDanmuStyle = (danmu, trackIndex) => {
return {
transform: `translateX(${danmu.x}rpx)`,
transition: danmu.isMoving ? `transform ${danmu.duration}ms linear, opacity ${FADE_DURATION}ms linear` : 'none',
transition: danmu.isMoving ? `transform ${(danmu.remainingDuration ?? danmu.duration)}ms linear` : 'none',
opacity: danmu.opacity || 1,
// 明确设置垂直位置
position: 'absolute',
......@@ -141,13 +176,13 @@ const getDanmuStyle = (danmu, trackIndex) => {
top: `${topPosition}rpx`,
zIndex: 10,
// 添加硬件加速
willChange: 'transform, opacity'
willChange: 'transform'
}
}
// 检查轨道中是否有足够空间放置新弹幕
// options.ignoreTime: 跳过时间间隔限制,用于同轨跟随插入,保证无缝
const checkTrackSpace = (track, danmuWidth = 350, options = {}) => {
const checkTrackSpace = (track, danmuWidth = DANMU_WIDTH, options = {}) => {
const now = Date.now()
const ignoreTime = options.ignoreTime === true
......@@ -159,12 +194,13 @@ const checkTrackSpace = (track, danmuWidth = 350, options = {}) => {
// 清理已经完全移出屏幕的弹幕
track.danmus = track.danmus.filter(danmu => {
const elapsedTime = now - danmu.startTime
const totalDistance = 750 + danmuWidth + 100
const existingWidth = getDanmuWidth(danmu)
const totalDistance = CONTAINER_WIDTH + existingWidth + EXTRA_DISTANCE
const moveDistance = (elapsedTime / danmu.duration) * totalDistance
const currentX = 750 - moveDistance
const currentX = CONTAINER_WIDTH - moveDistance
// 如果弹幕已经完全移出屏幕左侧,则移除
return currentX > -danmuWidth
return currentX > -existingWidth
})
// 重新检查轨道是否为空
......@@ -175,16 +211,17 @@ const checkTrackSpace = (track, danmuWidth = 350, options = {}) => {
// 检查所有弹幕的位置,确保新弹幕不会与任何现有弹幕重叠
for (const existingDanmu of track.danmus) {
const elapsedTime = now - existingDanmu.startTime
const totalDistance = 750 + danmuWidth + 100
const existingWidth = getDanmuWidth(existingDanmu)
const totalDistance = CONTAINER_WIDTH + existingWidth + EXTRA_DISTANCE
const moveDistance = Math.min((elapsedTime / existingDanmu.duration) * totalDistance, totalDistance)
const existingDanmuX = 750 - moveDistance
const existingDanmuX = CONTAINER_WIDTH - moveDistance
// 计算弹幕的右边界位置
const existingDanmuRightEdge = existingDanmuX + danmuWidth
const existingDanmuRightEdge = existingDanmuX + existingWidth
// 如果现有弹幕的右边界还在屏幕内或刚出屏幕,则需要更严格的间距检查
const safeDistance = danmuWidth * 0.8 // 增加到80%的弹幕宽度作为安全距离
if (existingDanmuRightEdge > (750 - safeDistance)) {
if (existingDanmuRightEdge > (CONTAINER_WIDTH - safeDistance)) {
return false
}
}
......@@ -214,9 +251,9 @@ const findAvailableTrack = () => {
// 创建弹幕对象
const createDanmu = (data) => {
const id = `danmu_${danmuId.value++}`
const containerWidth = 750 // 小程序默认宽度
const danmuWidth = 350 // 弹幕宽度
const extraDistance = 100 // 额外移动距离,确保完全消失
const containerWidth = CONTAINER_WIDTH // 小程序默认宽度
const danmuWidth = DANMU_WIDTH // 弹幕宽度
const extraDistance = EXTRA_DISTANCE // 额外移动距离,确保完全消失
return {
id,
......@@ -235,7 +272,7 @@ const addDanmuToTrack = (danmu, targetTrack = null, options = {}) => {
if (!track) return false
// 更严格的最终检查 - 确保轨道真的有足够空间
if (!checkTrackSpace(track, 350, options)) {
if (!checkTrackSpace(track, DANMU_WIDTH, options)) {
return false
}
......@@ -243,7 +280,11 @@ const addDanmuToTrack = (danmu, targetTrack = null, options = {}) => {
track.lastDanmuTime = Date.now()
// 启动弹幕动画
nextTick(() => {
nextTick(async () => {
await measureDanmuWidth(danmu)
recalculateDuration(danmu)
// 重置起始时间,保证时长重新计算后动画正确
danmu.startTime = Date.now()
startDanmuAnimation(danmu, track)
})
......@@ -266,7 +307,8 @@ const startDanmuAnimation = (danmu, track) => {
// 计算当前弹幕应该在的位置(基于已经运行的时间)
const now = Date.now()
const elapsedTime = now - danmu.startTime
const totalDistance = 750 + 350 + 100 // 总移动距离
const width = getDanmuWidth(danmu)
const totalDistance = CONTAINER_WIDTH + width + EXTRA_DISTANCE // 总移动距离
// 如果弹幕已经运行超过预定时间,直接重置
if (elapsedTime >= danmu.duration) {
......@@ -275,7 +317,7 @@ const startDanmuAnimation = (danmu, track) => {
}
const currentProgress = elapsedTime / danmu.duration
const currentPosition = 750 - (currentProgress * totalDistance)
const currentPosition = CONTAINER_WIDTH - (currentProgress * totalDistance)
// 设置当前位置
danmu.x = currentPosition
......@@ -283,17 +325,12 @@ const startDanmuAnimation = (danmu, track) => {
// 使用 nextTick 确保 DOM 更新后再启动动画
nextTick(() => {
// 移动到左侧屏幕外,确保弹幕完全消失
danmu.x = -(450) // 移动距离 = 弹幕宽度(350) + 额外距离(100)
danmu.x = -(width + EXTRA_DISTANCE)
})
// 计算剩余动画时间
const remainingTime = danmu.duration - elapsedTime
// 在动画末尾前触发淡出,避免到最左侧突然消失
const fadeStartDelay = Math.max(0, remainingTime - FADE_DURATION)
setTimeout(() => {
danmu.opacity = 0
}, fadeStartDelay)
danmu.remainingDuration = remainingTime
// 在本弹幕进入一定进度后,尝试在同一轨道追加下一条,保证无缝滚动
const followProgress = 0.08 // 更早跟随,缩小间隙
......@@ -301,7 +338,7 @@ const startDanmuAnimation = (danmu, track) => {
setTimeout(() => {
if (!isPlaying.value) return
// 仅当轨道有空间时在同轨追加下一条弹幕
if (checkTrackSpace(track, 350, { ignoreTime: true })) {
if (checkTrackSpace(track, DANMU_WIDTH, { ignoreTime: true })) {
const nextData = getNextUnusedDanmu()
if (nextData) {
const nextDanmu = createDanmu(nextData)
......@@ -335,7 +372,7 @@ const resetDanmuForLoop = (danmu, track) => {
// 复位到右侧并重新计时
danmu.startTime = Date.now()
danmu.x = 750
danmu.x = CONTAINER_WIDTH
track.lastDanmuTime = Date.now()
// 重新启动动画(无缝继续)
......@@ -572,14 +609,6 @@ onUnmounted(() => {
pointer-events: none;
}
.danmu-item {
position: absolute;
top: 50%;
transform: translateY(-50%);
pointer-events: auto;
z-index: 10;
right: 0; /* 从右侧开始 */
}
.danmu-content {
display: flex;
......