78 lines
5.5 KiB
Markdown
78 lines
5.5 KiB
Markdown
# 节拍器 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`)。
|
|
- **不要**改 `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>`:不在范围,只做最小修补。
|