feat(training): 新增握力环 2.0 摄像头手势识别页

用前置摄像头 + VisionKit/ROI 管线驱动开合计数,复用跟练舞台与连击粒子,并加固结束收尾避免末段卡死无响应。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-11 18:31:31 +08:00
co-authored by Cursor
parent f6688a740d
commit 729d2a084d
11 changed files with 2740 additions and 27 deletions
@@ -204,13 +204,13 @@ const instance = getCurrentInstance()
// 由父级 prop 信号驱动触发特效(避免跨组件 ref 调用, 小程序端更稳)
const props = defineProps<{
crushSignal?: { type: CrushItemType; nonce: number } | null
crushSignal?: { type: CrushItemType; nonce: number; intensity?: number } | null
}>()
watch(
() => props.crushSignal,
(sig) => {
if (sig) triggerCrush(sig.type)
if (sig) triggerCrush(sig.type, sig.intensity ?? 1)
},
)
@@ -270,12 +270,13 @@ function initCanvas() {
// 触发捏碎特效
// ============================================================
function triggerCrush(itemType: CrushItemType) {
function triggerCrush(itemType: CrushItemType, intensity = 1) {
if (!ctx.value || !canvasNode.value) return
const item = CRUSH_ITEMS[itemType]
if (!item) return
const scale = Math.max(0.45, Math.min(1.15, intensity))
const centerX = canvasWidth.value / 2
const centerY = canvasHeight.value / 2
const maxR = Math.min(canvasWidth.value, canvasHeight.value)
@@ -284,39 +285,44 @@ function triggerCrush(itemType: CrushItemType) {
flashList.value.push({
x: centerX,
y: centerY,
radius: maxR * 0.12,
maxRadius: maxR * 0.62,
radius: maxR * 0.12 * scale,
maxRadius: maxR * 0.62 * scale,
color: item.flashColor,
life: 1.0,
decay: PHYSICS.FLASH_DECAY,
})
// 2. 双层冲击波
// 2. 双层冲击波(低强度时只保留一层)
shockwaves.value.push({
x: centerX, y: centerY,
radius: 12, maxRadius: maxR * 0.62,
radius: 12, maxRadius: maxR * 0.62 * scale,
color: item.shockwaveColor, width: 5,
life: 1.0, decay: PHYSICS.SHOCKWAVE_DECAY,
})
shockwaves.value.push({
x: centerX, y: centerY,
radius: 4, maxRadius: maxR * 0.42,
color: item.shockwaveColor, width: 3,
life: 1.0, decay: PHYSICS.SHOCKWAVE_DECAY * 1.3,
})
if (scale >= 0.8) {
shockwaves.value.push({
x: centerX, y: centerY,
radius: 4, maxRadius: maxR * 0.42 * scale,
color: item.shockwaveColor, width: 3,
life: 1.0, decay: PHYSICS.SHOCKWAVE_DECAY * 1.3,
})
}
const debrisCount = Math.max(8, Math.round(item.debrisCount * scale))
const sparkCount = Math.max(6, Math.round(item.sparkCount * scale))
const sparkleCount = Math.max(2, Math.round(item.sparkleCount * scale))
// 3. 碎片(主体飞溅)
for (let i = 0; i < item.debrisCount; i++) {
const angle = (Math.PI * 2 * i) / item.debrisCount + (Math.random() - 0.5) * 0.7
const speed = item.speedMin + Math.random() * (item.speedMax - item.speedMin)
const size = item.sizeMin + Math.random() * (item.sizeMax - item.sizeMin)
for (let i = 0; i < debrisCount; i++) {
const angle = (Math.PI * 2 * i) / debrisCount + (Math.random() - 0.5) * 0.7
const speed = (item.speedMin + Math.random() * (item.speedMax - item.speedMin)) * (0.85 + scale * 0.15)
const size = (item.sizeMin + Math.random() * (item.sizeMax - item.sizeMin)) * scale
const color = item.colors[Math.floor(Math.random() * item.colors.length)]
const shape = item.shapes[Math.floor(Math.random() * item.shapes.length)]
// confetti(彩纸): 初速带强烈向上偏移 + 水平摇摆下落
const confetti = !!item.confetti
const vy0 = confetti
? Math.sin(angle) * speed - 2.5 // 先向上窜
? Math.sin(angle) * speed - 2.5
: Math.sin(angle) * speed
debrisList.value.push({
@@ -330,18 +336,18 @@ function triggerCrush(itemType: CrushItemType) {
rotation: Math.random() * Math.PI * 2,
rotationSpeed: (Math.random() - 0.5) * (confetti ? 0.5 : 0.3),
life: 1.0,
decay: PHYSICS.DEBRIS_DECAY * (confetti ? 0.7 : 1),
decay: PHYSICS.DEBRIS_DECAY * (confetti ? 0.7 : 1) * (scale < 0.7 ? 1.25 : 1),
gravity: item.gravity,
glow: !confetti, // 彩纸不发光,实色碎片发光
glow: !confetti,
swing: confetti ? 0.6 + Math.random() * 1.2 : 0,
age: Math.random() * Math.PI * 2,
})
}
// 4. 火花(细小高速亮点,带拖尾)
for (let i = 0; i < item.sparkCount; i++) {
// 4. 火花
for (let i = 0; i < sparkCount; i++) {
const angle = Math.random() * Math.PI * 2
const speed = item.speedMax * (0.9 + Math.random() * 0.8)
const speed = item.speedMax * (0.9 + Math.random() * 0.8) * scale
const color = item.sparkColors[Math.floor(Math.random() * item.sparkColors.length)]
sparkList.value.push({
x: centerX,
@@ -359,8 +365,8 @@ function triggerCrush(itemType: CrushItemType) {
}
// 5. 闪烁星光
for (let i = 0; i < item.sparkleCount; i++) {
const r = maxR * (0.08 + Math.random() * 0.32)
for (let i = 0; i < sparkleCount; i++) {
const r = maxR * (0.08 + Math.random() * 0.32) * scale
const a = Math.random() * Math.PI * 2
const color = item.sparkColors[Math.floor(Math.random() * item.sparkColors.length)]
sparkleList.value.push({
@@ -370,7 +376,7 @@ function triggerCrush(itemType: CrushItemType) {
rotation: Math.random() * Math.PI,
spin: (Math.random() - 0.5) * 0.16,
color,
life: 1.0 + Math.random() * 0.4, // 错峰出现
life: 1.0 + Math.random() * 0.4,
decay: PHYSICS.SPARKLE_DECAY * (0.8 + Math.random() * 0.5),
})
}