Files
zyt/.trellis/tasks/05-27-metronome-p0/prd.md
T

5.5 KiB

节拍器 P0 修复(Canvas 渲染循环 + 触摸误触)

Goal

修复 TUICallKit-Vue3/training/pages/components/walker-canvas.vue 三个 P0 级问题:

  1. Vue 2 钩子写错导致 Canvas 渲染循环离开页面后不会停;
  2. 暂停态仍以满帧 60fps 渲染,老年场景下功耗浪费;
  3. 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 2 beforeDestroy 钩子(第 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 就 emit toggle-play——按下命中圆 → 滑出圆外 → 抬起,仍然触发播放/暂停
  • 暂停态: 父组件传入 isPlaying = false 时,renderLoop 仍每 16ms 重排 → 绘制 → ctx.draw(false) 桥接,只是 time 增长慢一些(fixedDt * 0.2)。
  • 不要onHidefg.stop() 的行为——之前的 BgAudio 循环长音频方案已被移除,InnerAudio + setTimeout 在小程序后台被强力节流,硬撑反而播得断断续续,所以现在切后台主动停是有意的设计选择,不是 bug。
  • 本任务只动 walker-canvas.vue 一个文件,不动 useMetronome.tsmetronome.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 + 40pressed = 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>:不在范围,只做最小修补。