| 319 | } |
| 320 | |
| 321 | drawHand(handData, width, height) { |
| 322 | const ctx = this.ctx; |
| 323 | |
| 324 | if (!handData.landmarks) return; |
| 325 | |
| 326 | // 缩放坐标 |
| 327 | const landmarks = handData.landmarks.map(p => |
| 328 | this.scalePoint([p[0], p[1]], width, height) |
| 329 | ); |
| 330 | |
| 331 | // 绘制手部连接线 |
| 332 | ctx.strokeStyle = this.colors.secondary; |
| 333 | ctx.lineWidth = 2; |
| 334 | ctx.globalAlpha = 0.8; |
| 335 | |
| 336 | // MediaPipe手部连接定义 |
| 337 | const connections = [ |
| 338 | [0, 1], [1, 2], [2, 3], [3, 4], // 拇指 |
| 339 | [0, 5], [5, 6], [6, 7], [7, 8], // 食指 |
| 340 | [5, 9], [9, 10], [10, 11], [11, 12], // 中指 |
| 341 | [9, 13], [13, 14], [14, 15], [15, 16], // 无名指 |
| 342 | [13, 17], [17, 18], [18, 19], [19, 20], // 小指 |
| 343 | [0, 17] // 手腕连接 |
| 344 | ]; |
| 345 | |
| 346 | connections.forEach(([start, end]) => { |
| 347 | ctx.beginPath(); |
| 348 | ctx.moveTo(landmarks[start][0], landmarks[start][1]); |
| 349 | ctx.lineTo(landmarks[end][0], landmarks[end][1]); |
| 350 | ctx.stroke(); |
| 351 | }); |
| 352 | |
| 353 | // 绘制关键点 |
| 354 | landmarks.forEach((point, i) => { |
| 355 | ctx.fillStyle = this.colors.secondary; |
| 356 | ctx.beginPath(); |
| 357 | ctx.arc(point[0], point[1], 3, 0, Math.PI * 2); |
| 358 | ctx.fill(); |
| 359 | }); |
| 360 | |
| 361 | ctx.globalAlpha = 1; |
| 362 | |
| 363 | // 绘制握持评分 |
| 364 | if (handData.grasp_score !== undefined) { |
| 365 | ctx.font = this.fonts.body; |
| 366 | ctx.fillStyle = this.colors.text; |
| 367 | ctx.fillText( |
| 368 | `握持评分 Grasp Score: ${handData.grasp_score.toFixed(2)}`, |
| 369 | 20, |
| 370 | 80 |
| 371 | ); |
| 372 | } |
| 373 | } |
| 374 | |
| 375 | drawProgressBars(tracking, width, height) { |
| 376 | const ctx = this.ctx; |