hookehuyr

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

- 移除弹幕淡出效果,保持不透明度始终为1
- 引入弹幕宽度测量和动态时长计算,确保速度一致
- 使用常量替换魔法数值,提高代码可维护性
- 优化弹幕位置计算和碰撞检测逻辑
...@@ -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;
......