5.5 KiB
5.5 KiB
节拍器 P0 修复(Canvas 渲染循环 + 触摸误触)
Goal
修复 TUICallKit-Vue3/training/pages/components/walker-canvas.vue 三个 P0 级问题:
- Vue 2 钩子写错导致 Canvas 渲染循环离开页面后不会停;
- 暂停态仍以满帧 60fps 渲染,老年场景下功耗浪费;
- Canvas 中心 BPM 圆按钮触摸误触(按下后滑出仍触发 toggle)。
What I already know
- 节拍器页面:
TUICallKit-Vue3/training/pages/metronome.vue,父组件用<script setup lang="ts">+onUnmounted,是 Vue 3 写法。 - 律动 Canvas 子组件:
TUICallKit-Vue3/training/pages/components/walker-canvas.vue,用 Options API,全文件唯一一处错用 Vue 2beforeDestroy钩子(第 60 行)——Vue 3 已重命名为beforeUnmount,所以当前 timer 清理代码从未执行。 renderLoop()(143-178 行) 通过setTimeout(fn, 1000/60)自调用,所以清理应该用clearTimeout(this.timer)而不是cancelAnimationFrame(当前清理代码两个 bug 叠加,完全没工作)。onTouchMove(132-134 行) 是空方法;onTouchEnd(135-141 行) 只看coreTargetScale < 1就 emittoggle-play——按下命中圆 → 滑出圆外 → 抬起,仍然触发播放/暂停。- 暂停态: 父组件传入
isPlaying = false时,renderLoop仍每 16ms 重排 → 绘制 →ctx.draw(false)桥接,只是time增长慢一些(fixedDt * 0.2)。 - 不要改
onHide里fg.stop()的行为——之前的 BgAudio 循环长音频方案已被移除,InnerAudio + setTimeout 在小程序后台被强力节流,硬撑反而播得断断续续,所以现在切后台主动停是有意的设计选择,不是 bug。 - 本任务只动 walker-canvas.vue 一个文件,不动
useMetronome.ts和metronome.vue。
Assumptions
- "暂停态完全停渲染"的判定:
isPlaying === false&&shockwaves.length === 0&&|coreScale - coreTargetScale| < 0.001&&|coreVelocity| < 0.001&&waveAmplitudeMultiplier <= 0.11(留 0.01 的浮点容差,因为暂停态衰减下限是 0.1)。 - "touch hit-test" 容差保持现状
coreRadius + 40,不调整大小。 - 触摸 cancel(系统打断,如来电)与 touchend 走同样的"复位 + 不 emit"逻辑(仅当 pressed === true 才 emit,被 cancel 时 pressed 已被 onTouchMove 滑出复位,或直接被 cancel 处理逻辑复位)。
Requirements
R1: 修复生命周期钩子
- 把
beforeDestroy()改名为beforeUnmount()。 - 把里面的
cancelAnimationFrame(this.timer)改为clearTimeout(this.timer)(因为this.timer实际是 setTimeout 返回的 id)。
R2: 暂停态停止渲染循环
- 新增 data 字段
renderRunning: false。 - 抽出
ensureRenderLoop()方法: 若renderRunning === true则直接 return,否则置 true + 调renderLoop()。 renderLoop()末尾根据"是否还需要继续"决定是setTimeout(...)还是renderRunning = false; return:- 继续条件:
isPlaying为真,或 还有 shockwave,或 弹簧物理还没稳,或 waveAmplitudeMultiplier 还没衰减到下限。
- 继续条件:
- 各处外部入口都要调
ensureRenderLoop():initCanvas()首次启动(替换原来的this.renderLoop()直接调用);triggerBeat()末尾;watch(isPlaying)中newVal === true分支;onTouchStart命中 hit box 后(用户按下也要立刻动起来给出按压反馈)。
- 改完后,父组件
isPlaying = false且没动效时,Canvas 不再有定时器在跑。
R3: 触摸命中防误触
- 新增 data 字段
pressed: false。 onTouchStart: 命中 hit box 后置pressed = true+ 维持现有coreTargetScale = 0.9+ 调ensureRenderLoop()。onTouchMove: 拿当前 touch 重新算距离,若超出coreRadius + 40则pressed = false+coreTargetScale = 1(视觉立即复位,弹簧动画把 scale 拉回去)。onTouchEnd/onTouchCancel:- 无条件复位
coreTargetScale = 1; - 仅当
pressed === true时才this.$emit('toggle-play'); - 无条件复位
pressed = false。
- 无条件复位
Acceptance Criteria
- 离开节拍器页面(返回上一页 /
uni.navigateBack)后,Canvas 的渲染循环 timer 被清理,不再产生setTimeout调用(可在renderLoop起始加临时console.count验证,验证完移除)。 - 节拍器停止状态进入稳态后(等动画余波结束 ~1 秒),Canvas 不再有 timer 在跑(同上验证手段);用户按 toggle 启动后,渲染立即恢复且画面流畅。
- 用户按下中心圆 → 手指滑出圆外 → 抬起: 不触发播放/暂停,且 coreScale 视觉上被复位回 1。
- 用户按下中心圆 → 手指原地抬起: 正常触发 toggle-play 切换播放/暂停。
- 用户在播放过程中,触发 triggerBeat 的视觉律动(shockwave、core 脉动)行为不回归。
- 切档位 / 微调 BPM / 切拍号等所有现有交互不回归。
Definition of Done
- 改动范围: 仅
TUICallKit-Vue3/training/pages/components/walker-canvas.vue一个文件。 - 不引入新的 lint warning。
- 在 uniapp 工程的微信小程序端实机测试(或开发者工具)至少跑一遍:启停 / 切档位 / 进出页面 / 拖动滑出。
- 类型检查(若 uniapp 工程有
npm run type-check)通过;无则忽略此项。
Out of Scope
useMetronome.ts内部的调度漂移、CDN 兜底、accentEvery 重置等 P1/P2 项,本次不动。metronome.vue的 onHide 行为(确认为设计选择,非 bug,不改)。- 拍号 UI 文案语义、累计时长、触觉反馈等 UX 增强项。
- 重写为 Composition API / 迁移到 Vue 3
<script setup>:不在范围,只做最小修补。