perf(弹幕组件): 优化弹幕动画性能并移除淡出效果
- 移除弹幕淡出效果,保持不透明度始终为1 - 引入弹幕宽度测量和动态时长计算,确保速度一致 - 使用常量替换魔法数值,提高代码可维护性 - 优化弹幕位置计算和碰撞检测逻辑
Showing
1 changed file
with
74 additions
and
45 deletions
| ... | @@ -13,13 +13,14 @@ | ... | @@ -13,13 +13,14 @@ |
| 13 | :style="{ top: trackIndex * trackHeight + 'rpx', height: trackHeight + 'rpx' }" | 13 | :style="{ top: trackIndex * trackHeight + 'rpx', height: trackHeight + 'rpx' }" |
| 14 | > | 14 | > |
| 15 | <!-- 轨道中的弹幕 --> | 15 | <!-- 轨道中的弹幕 --> |
| 16 | - <view | 16 | + <view |
| 17 | - v-for="danmu in track.danmus" | 17 | + v-for="danmu in track.danmus" |
| 18 | - :key="danmu.id" | 18 | + :key="danmu.id" |
| 19 | - class="danmu-item" | 19 | + class="danmu-item" |
| 20 | - :style="getDanmuStyle(danmu, trackIndex)" | 20 | + :id="'danmu-' + danmu.id" |
| 21 | - @tap="handleDanmuClick(danmu)" | 21 | + :style="getDanmuStyle(danmu, trackIndex)" |
| 22 | - > | 22 | + @tap="handleDanmuClick(danmu)" |
| 23 | + > | ||
| 23 | <!-- 弹幕内容 --> | 24 | <!-- 弹幕内容 --> |
| 24 | <view class="danmu-content"> | 25 | <view class="danmu-content"> |
| 25 | <!-- 头像 --> | 26 | <!-- 头像 --> |
| ... | @@ -76,7 +77,7 @@ const props = defineProps({ | ... | @@ -76,7 +77,7 @@ const props = defineProps({ |
| 76 | // 弹幕速度 (rpx/s) | 77 | // 弹幕速度 (rpx/s) |
| 77 | danmuSpeed: { | 78 | danmuSpeed: { |
| 78 | type: Number, | 79 | type: Number, |
| 79 | - default: 150 // 提高默认速度,让弹幕移动更流畅 | 80 | + default: 200 // 提高默认速度,让弹幕移动更流畅 |
| 80 | }, | 81 | }, |
| 81 | // 轨道数量 | 82 | // 轨道数量 |
| 82 | trackCount: { | 83 | trackCount: { |
| ... | @@ -93,7 +94,41 @@ const isPlaying = ref(false) | ... | @@ -93,7 +94,41 @@ const isPlaying = ref(false) |
| 93 | const danmuId = ref(0) | 94 | const danmuId = ref(0) |
| 94 | const trackHeight = ref(110) // 适配最多3行文字显示,确保弹幕之间有足够间距 | 95 | const trackHeight = ref(110) // 适配最多3行文字显示,确保弹幕之间有足够间距 |
| 95 | const animationTimers = ref(new Map()) // 存储动画定时器 | 96 | const animationTimers = ref(new Map()) // 存储动画定时器 |
| 96 | -const FADE_DURATION = 400 // 弹幕靠近左侧时淡出时长(毫秒) | 97 | +// 统一移动相关常量,避免魔法数不一致 |
| 98 | +const CONTAINER_WIDTH = 750 | ||
| 99 | +const DANMU_WIDTH = 350 | ||
| 100 | +const EXTRA_DISTANCE = 120 // 额外移动距离,确保完全移出视图 | ||
| 101 | +// 关闭边缘淡出,保持在视图内始终不改变透明度 | ||
| 102 | +const FADE_DURATION = 0 | ||
| 103 | +const SYSTEM_INFO = Taro.getSystemInfoSync() | ||
| 104 | +const WINDOW_WIDTH = SYSTEM_INFO.windowWidth | ||
| 105 | + | ||
| 106 | +// px 转 rpx | ||
| 107 | +const toRpx = (px) => (px * CONTAINER_WIDTH) / WINDOW_WIDTH | ||
| 108 | +// 获取弹幕宽度(rpx) | ||
| 109 | +const getDanmuWidth = (danmu) => { | ||
| 110 | + return danmu?.widthRpx && danmu.widthRpx > 0 ? danmu.widthRpx : DANMU_WIDTH | ||
| 111 | +} | ||
| 112 | +// 重新计算时长,保证速度一致 | ||
| 113 | +const recalculateDuration = (danmu) => { | ||
| 114 | + const width = getDanmuWidth(danmu) | ||
| 115 | + const totalDistance = CONTAINER_WIDTH + width + EXTRA_DISTANCE | ||
| 116 | + danmu.duration = (totalDistance / (props.danmuSpeed * 0.6)) * 1000 | ||
| 117 | +} | ||
| 118 | +// 测量弹幕真实宽度(px),存为 rpx | ||
| 119 | +const measureDanmuWidth = (danmu) => new Promise((resolve) => { | ||
| 120 | + try { | ||
| 121 | + const selector = `#danmu-${danmu.id} .danmu-content` | ||
| 122 | + Taro.createSelectorQuery().select(selector).boundingClientRect(rect => { | ||
| 123 | + if (rect && rect.width) { | ||
| 124 | + danmu.widthRpx = toRpx(rect.width) | ||
| 125 | + } | ||
| 126 | + resolve(getDanmuWidth(danmu)) | ||
| 127 | + }).exec() | ||
| 128 | + } catch (e) { | ||
| 129 | + resolve(getDanmuWidth(danmu)) | ||
| 130 | + } | ||
| 131 | +}) | ||
| 97 | 132 | ||
| 98 | // 轨道系统 | 133 | // 轨道系统 |
| 99 | const tracks = reactive( | 134 | const tracks = reactive( |
| ... | @@ -133,7 +168,7 @@ const getDanmuStyle = (danmu, trackIndex) => { | ... | @@ -133,7 +168,7 @@ const getDanmuStyle = (danmu, trackIndex) => { |
| 133 | 168 | ||
| 134 | return { | 169 | return { |
| 135 | transform: `translateX(${danmu.x}rpx)`, | 170 | transform: `translateX(${danmu.x}rpx)`, |
| 136 | - transition: danmu.isMoving ? `transform ${danmu.duration}ms linear, opacity ${FADE_DURATION}ms linear` : 'none', | 171 | + transition: danmu.isMoving ? `transform ${(danmu.remainingDuration ?? danmu.duration)}ms linear` : 'none', |
| 137 | opacity: danmu.opacity || 1, | 172 | opacity: danmu.opacity || 1, |
| 138 | // 明确设置垂直位置 | 173 | // 明确设置垂直位置 |
| 139 | position: 'absolute', | 174 | position: 'absolute', |
| ... | @@ -141,13 +176,13 @@ const getDanmuStyle = (danmu, trackIndex) => { | ... | @@ -141,13 +176,13 @@ const getDanmuStyle = (danmu, trackIndex) => { |
| 141 | top: `${topPosition}rpx`, | 176 | top: `${topPosition}rpx`, |
| 142 | zIndex: 10, | 177 | zIndex: 10, |
| 143 | // 添加硬件加速 | 178 | // 添加硬件加速 |
| 144 | - willChange: 'transform, opacity' | 179 | + willChange: 'transform' |
| 145 | } | 180 | } |
| 146 | } | 181 | } |
| 147 | 182 | ||
| 148 | // 检查轨道中是否有足够空间放置新弹幕 | 183 | // 检查轨道中是否有足够空间放置新弹幕 |
| 149 | // options.ignoreTime: 跳过时间间隔限制,用于同轨跟随插入,保证无缝 | 184 | // options.ignoreTime: 跳过时间间隔限制,用于同轨跟随插入,保证无缝 |
| 150 | -const checkTrackSpace = (track, danmuWidth = 350, options = {}) => { | 185 | +const checkTrackSpace = (track, danmuWidth = DANMU_WIDTH, options = {}) => { |
| 151 | const now = Date.now() | 186 | const now = Date.now() |
| 152 | const ignoreTime = options.ignoreTime === true | 187 | const ignoreTime = options.ignoreTime === true |
| 153 | 188 | ||
| ... | @@ -159,12 +194,13 @@ const checkTrackSpace = (track, danmuWidth = 350, options = {}) => { | ... | @@ -159,12 +194,13 @@ const checkTrackSpace = (track, danmuWidth = 350, options = {}) => { |
| 159 | // 清理已经完全移出屏幕的弹幕 | 194 | // 清理已经完全移出屏幕的弹幕 |
| 160 | track.danmus = track.danmus.filter(danmu => { | 195 | track.danmus = track.danmus.filter(danmu => { |
| 161 | const elapsedTime = now - danmu.startTime | 196 | const elapsedTime = now - danmu.startTime |
| 162 | - const totalDistance = 750 + danmuWidth + 100 | 197 | + const existingWidth = getDanmuWidth(danmu) |
| 198 | + const totalDistance = CONTAINER_WIDTH + existingWidth + EXTRA_DISTANCE | ||
| 163 | const moveDistance = (elapsedTime / danmu.duration) * totalDistance | 199 | const moveDistance = (elapsedTime / danmu.duration) * totalDistance |
| 164 | - const currentX = 750 - moveDistance | 200 | + const currentX = CONTAINER_WIDTH - moveDistance |
| 165 | 201 | ||
| 166 | // 如果弹幕已经完全移出屏幕左侧,则移除 | 202 | // 如果弹幕已经完全移出屏幕左侧,则移除 |
| 167 | - return currentX > -danmuWidth | 203 | + return currentX > -existingWidth |
| 168 | }) | 204 | }) |
| 169 | 205 | ||
| 170 | // 重新检查轨道是否为空 | 206 | // 重新检查轨道是否为空 |
| ... | @@ -175,16 +211,17 @@ const checkTrackSpace = (track, danmuWidth = 350, options = {}) => { | ... | @@ -175,16 +211,17 @@ const checkTrackSpace = (track, danmuWidth = 350, options = {}) => { |
| 175 | // 检查所有弹幕的位置,确保新弹幕不会与任何现有弹幕重叠 | 211 | // 检查所有弹幕的位置,确保新弹幕不会与任何现有弹幕重叠 |
| 176 | for (const existingDanmu of track.danmus) { | 212 | for (const existingDanmu of track.danmus) { |
| 177 | const elapsedTime = now - existingDanmu.startTime | 213 | const elapsedTime = now - existingDanmu.startTime |
| 178 | - const totalDistance = 750 + danmuWidth + 100 | 214 | + const existingWidth = getDanmuWidth(existingDanmu) |
| 215 | + const totalDistance = CONTAINER_WIDTH + existingWidth + EXTRA_DISTANCE | ||
| 179 | const moveDistance = Math.min((elapsedTime / existingDanmu.duration) * totalDistance, totalDistance) | 216 | const moveDistance = Math.min((elapsedTime / existingDanmu.duration) * totalDistance, totalDistance) |
| 180 | - const existingDanmuX = 750 - moveDistance | 217 | + const existingDanmuX = CONTAINER_WIDTH - moveDistance |
| 181 | 218 | ||
| 182 | // 计算弹幕的右边界位置 | 219 | // 计算弹幕的右边界位置 |
| 183 | - const existingDanmuRightEdge = existingDanmuX + danmuWidth | 220 | + const existingDanmuRightEdge = existingDanmuX + existingWidth |
| 184 | 221 | ||
| 185 | // 如果现有弹幕的右边界还在屏幕内或刚出屏幕,则需要更严格的间距检查 | 222 | // 如果现有弹幕的右边界还在屏幕内或刚出屏幕,则需要更严格的间距检查 |
| 186 | const safeDistance = danmuWidth * 0.8 // 增加到80%的弹幕宽度作为安全距离 | 223 | const safeDistance = danmuWidth * 0.8 // 增加到80%的弹幕宽度作为安全距离 |
| 187 | - if (existingDanmuRightEdge > (750 - safeDistance)) { | 224 | + if (existingDanmuRightEdge > (CONTAINER_WIDTH - safeDistance)) { |
| 188 | return false | 225 | return false |
| 189 | } | 226 | } |
| 190 | } | 227 | } |
| ... | @@ -214,9 +251,9 @@ const findAvailableTrack = () => { | ... | @@ -214,9 +251,9 @@ const findAvailableTrack = () => { |
| 214 | // 创建弹幕对象 | 251 | // 创建弹幕对象 |
| 215 | const createDanmu = (data) => { | 252 | const createDanmu = (data) => { |
| 216 | const id = `danmu_${danmuId.value++}` | 253 | const id = `danmu_${danmuId.value++}` |
| 217 | - const containerWidth = 750 // 小程序默认宽度 | 254 | + const containerWidth = CONTAINER_WIDTH // 小程序默认宽度 |
| 218 | - const danmuWidth = 350 // 弹幕宽度 | 255 | + const danmuWidth = DANMU_WIDTH // 弹幕宽度 |
| 219 | - const extraDistance = 100 // 额外移动距离,确保完全消失 | 256 | + const extraDistance = EXTRA_DISTANCE // 额外移动距离,确保完全消失 |
| 220 | 257 | ||
| 221 | return { | 258 | return { |
| 222 | id, | 259 | id, |
| ... | @@ -235,7 +272,7 @@ const addDanmuToTrack = (danmu, targetTrack = null, options = {}) => { | ... | @@ -235,7 +272,7 @@ const addDanmuToTrack = (danmu, targetTrack = null, options = {}) => { |
| 235 | if (!track) return false | 272 | if (!track) return false |
| 236 | 273 | ||
| 237 | // 更严格的最终检查 - 确保轨道真的有足够空间 | 274 | // 更严格的最终检查 - 确保轨道真的有足够空间 |
| 238 | - if (!checkTrackSpace(track, 350, options)) { | 275 | + if (!checkTrackSpace(track, DANMU_WIDTH, options)) { |
| 239 | return false | 276 | return false |
| 240 | } | 277 | } |
| 241 | 278 | ||
| ... | @@ -243,7 +280,11 @@ const addDanmuToTrack = (danmu, targetTrack = null, options = {}) => { | ... | @@ -243,7 +280,11 @@ const addDanmuToTrack = (danmu, targetTrack = null, options = {}) => { |
| 243 | track.lastDanmuTime = Date.now() | 280 | track.lastDanmuTime = Date.now() |
| 244 | 281 | ||
| 245 | // 启动弹幕动画 | 282 | // 启动弹幕动画 |
| 246 | - nextTick(() => { | 283 | + nextTick(async () => { |
| 284 | + await measureDanmuWidth(danmu) | ||
| 285 | + recalculateDuration(danmu) | ||
| 286 | + // 重置起始时间,保证时长重新计算后动画正确 | ||
| 287 | + danmu.startTime = Date.now() | ||
| 247 | startDanmuAnimation(danmu, track) | 288 | startDanmuAnimation(danmu, track) |
| 248 | }) | 289 | }) |
| 249 | 290 | ||
| ... | @@ -266,7 +307,8 @@ const startDanmuAnimation = (danmu, track) => { | ... | @@ -266,7 +307,8 @@ const startDanmuAnimation = (danmu, track) => { |
| 266 | // 计算当前弹幕应该在的位置(基于已经运行的时间) | 307 | // 计算当前弹幕应该在的位置(基于已经运行的时间) |
| 267 | const now = Date.now() | 308 | const now = Date.now() |
| 268 | const elapsedTime = now - danmu.startTime | 309 | const elapsedTime = now - danmu.startTime |
| 269 | - const totalDistance = 750 + 350 + 100 // 总移动距离 | 310 | + const width = getDanmuWidth(danmu) |
| 311 | + const totalDistance = CONTAINER_WIDTH + width + EXTRA_DISTANCE // 总移动距离 | ||
| 270 | 312 | ||
| 271 | // 如果弹幕已经运行超过预定时间,直接重置 | 313 | // 如果弹幕已经运行超过预定时间,直接重置 |
| 272 | if (elapsedTime >= danmu.duration) { | 314 | if (elapsedTime >= danmu.duration) { |
| ... | @@ -275,7 +317,7 @@ const startDanmuAnimation = (danmu, track) => { | ... | @@ -275,7 +317,7 @@ const startDanmuAnimation = (danmu, track) => { |
| 275 | } | 317 | } |
| 276 | 318 | ||
| 277 | const currentProgress = elapsedTime / danmu.duration | 319 | const currentProgress = elapsedTime / danmu.duration |
| 278 | - const currentPosition = 750 - (currentProgress * totalDistance) | 320 | + const currentPosition = CONTAINER_WIDTH - (currentProgress * totalDistance) |
| 279 | 321 | ||
| 280 | // 设置当前位置 | 322 | // 设置当前位置 |
| 281 | danmu.x = currentPosition | 323 | danmu.x = currentPosition |
| ... | @@ -283,17 +325,12 @@ const startDanmuAnimation = (danmu, track) => { | ... | @@ -283,17 +325,12 @@ const startDanmuAnimation = (danmu, track) => { |
| 283 | // 使用 nextTick 确保 DOM 更新后再启动动画 | 325 | // 使用 nextTick 确保 DOM 更新后再启动动画 |
| 284 | nextTick(() => { | 326 | nextTick(() => { |
| 285 | // 移动到左侧屏幕外,确保弹幕完全消失 | 327 | // 移动到左侧屏幕外,确保弹幕完全消失 |
| 286 | - danmu.x = -(450) // 移动距离 = 弹幕宽度(350) + 额外距离(100) | 328 | + danmu.x = -(width + EXTRA_DISTANCE) |
| 287 | }) | 329 | }) |
| 288 | 330 | ||
| 289 | // 计算剩余动画时间 | 331 | // 计算剩余动画时间 |
| 290 | const remainingTime = danmu.duration - elapsedTime | 332 | const remainingTime = danmu.duration - elapsedTime |
| 291 | - | 333 | + danmu.remainingDuration = remainingTime |
| 292 | - // 在动画末尾前触发淡出,避免到最左侧突然消失 | ||
| 293 | - const fadeStartDelay = Math.max(0, remainingTime - FADE_DURATION) | ||
| 294 | - setTimeout(() => { | ||
| 295 | - danmu.opacity = 0 | ||
| 296 | - }, fadeStartDelay) | ||
| 297 | 334 | ||
| 298 | // 在本弹幕进入一定进度后,尝试在同一轨道追加下一条,保证无缝滚动 | 335 | // 在本弹幕进入一定进度后,尝试在同一轨道追加下一条,保证无缝滚动 |
| 299 | const followProgress = 0.08 // 更早跟随,缩小间隙 | 336 | const followProgress = 0.08 // 更早跟随,缩小间隙 |
| ... | @@ -301,7 +338,7 @@ const startDanmuAnimation = (danmu, track) => { | ... | @@ -301,7 +338,7 @@ const startDanmuAnimation = (danmu, track) => { |
| 301 | setTimeout(() => { | 338 | setTimeout(() => { |
| 302 | if (!isPlaying.value) return | 339 | if (!isPlaying.value) return |
| 303 | // 仅当轨道有空间时在同轨追加下一条弹幕 | 340 | // 仅当轨道有空间时在同轨追加下一条弹幕 |
| 304 | - if (checkTrackSpace(track, 350, { ignoreTime: true })) { | 341 | + if (checkTrackSpace(track, DANMU_WIDTH, { ignoreTime: true })) { |
| 305 | const nextData = getNextUnusedDanmu() | 342 | const nextData = getNextUnusedDanmu() |
| 306 | if (nextData) { | 343 | if (nextData) { |
| 307 | const nextDanmu = createDanmu(nextData) | 344 | const nextDanmu = createDanmu(nextData) |
| ... | @@ -335,7 +372,7 @@ const resetDanmuForLoop = (danmu, track) => { | ... | @@ -335,7 +372,7 @@ const resetDanmuForLoop = (danmu, track) => { |
| 335 | 372 | ||
| 336 | // 复位到右侧并重新计时 | 373 | // 复位到右侧并重新计时 |
| 337 | danmu.startTime = Date.now() | 374 | danmu.startTime = Date.now() |
| 338 | - danmu.x = 750 | 375 | + danmu.x = CONTAINER_WIDTH |
| 339 | track.lastDanmuTime = Date.now() | 376 | track.lastDanmuTime = Date.now() |
| 340 | 377 | ||
| 341 | // 重新启动动画(无缝继续) | 378 | // 重新启动动画(无缝继续) |
| ... | @@ -455,7 +492,7 @@ const preloadDanmus = () => { | ... | @@ -455,7 +492,7 @@ const preloadDanmus = () => { |
| 455 | 492 | ||
| 456 | // 立即发送多条弹幕到不同轨道,模拟已经在滚动的状态 | 493 | // 立即发送多条弹幕到不同轨道,模拟已经在滚动的状态 |
| 457 | const preloadCount = Math.min(tracks.length, familyDanmus.value.length) | 494 | const preloadCount = Math.min(tracks.length, familyDanmus.value.length) |
| 458 | - | 495 | + |
| 459 | for (let i = 0; i < preloadCount; i++) { | 496 | for (let i = 0; i < preloadCount; i++) { |
| 460 | // 为每个轨道发送一条弹幕,并设置不同的延迟 | 497 | // 为每个轨道发送一条弹幕,并设置不同的延迟 |
| 461 | setTimeout(() => { | 498 | setTimeout(() => { |
| ... | @@ -536,7 +573,7 @@ onMounted(async () => { | ... | @@ -536,7 +573,7 @@ onMounted(async () => { |
| 536 | nextTick(() => { | 573 | nextTick(() => { |
| 537 | // 先预加载弹幕,让多条弹幕快速出现 | 574 | // 先预加载弹幕,让多条弹幕快速出现 |
| 538 | preloadDanmus() | 575 | preloadDanmus() |
| 539 | - | 576 | + |
| 540 | // 然后启动自动发送,维持持续滚动 | 577 | // 然后启动自动发送,维持持续滚动 |
| 541 | setTimeout(() => { | 578 | setTimeout(() => { |
| 542 | startAutoSend() | 579 | startAutoSend() |
| ... | @@ -572,14 +609,6 @@ onUnmounted(() => { | ... | @@ -572,14 +609,6 @@ onUnmounted(() => { |
| 572 | pointer-events: none; | 609 | pointer-events: none; |
| 573 | } | 610 | } |
| 574 | 611 | ||
| 575 | -.danmu-item { | ||
| 576 | - position: absolute; | ||
| 577 | - top: 50%; | ||
| 578 | - transform: translateY(-50%); | ||
| 579 | - pointer-events: auto; | ||
| 580 | - z-index: 10; | ||
| 581 | - right: 0; /* 从右侧开始 */ | ||
| 582 | -} | ||
| 583 | 612 | ||
| 584 | .danmu-content { | 613 | .danmu-content { |
| 585 | display: flex; | 614 | display: flex; | ... | ... |
-
Please register or login to post a comment