html
复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>人体汉字识别游戏</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #1a237e 0%, #311b92 30%, #4a148c 100%);
color: white;
min-height: 100vh;
overflow-x: hidden;
}
.game-container {
display: grid;
grid-template-columns: 1fr 350px;
gap: 20px;
padding: 20px;
height: 100vh;
max-width: 1400px;
margin: 0 auto;
}
/* 游戏主区域 */
.game-main {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 摄像头区域 */
.camera-area {
flex: 1;
background: rgba(0, 0, 0, 0.5);
border-radius: 15px;
border: 3px solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
#video-canvas {
width: 100%;
height: 100%;
display: block;
transform: scaleX(-1); /* 镜像翻转 */
}
.camera-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
/* 游戏头部 */
.game-header {
background: rgba(0, 0, 0, 0.7);
border-radius: 15px;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
border: 2px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.game-title {
font-size: 2.2rem;
font-weight: bold;
background: linear-gradient(90deg, #00bcd4, #4caf50);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 20px rgba(0, 188, 212, 0.5);
}
.game-info {
display: flex;
gap: 30px;
}
.info-item {
text-align: center;
}
.info-value {
font-size: 2rem;
font-weight: bold;
color: #00e5ff;
text-shadow: 0 0 10px #00e5ff;
}
.info-label {
font-size: 0.9rem;
color: #b3e5fc;
margin-top: 5px;
}
/* 侧边控制面板 */
.control-panel {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 当前汉字区域 */
.character-display {
background: rgba(0, 0, 0, 0.8);
border-radius: 15px;
padding: 25px;
text-align: center;
border: 3px solid rgba(0, 188, 212, 0.4);
box-shadow: 0 0 30px rgba(0, 188, 212, 0.3);
}
.character-title {
font-size: 1.2rem;
color: #80deea;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.character {
font-size: 8rem;
font-weight: bold;
color: #fff;
text-shadow: 0 0 30px #00e5ff;
line-height: 1;
margin: 10px 0;
font-family: 'KaiTi', '楷体', 'STKaiti', serif;
}
.character-pinyin {
font-size: 1.5rem;
color: #b3e5fc;
margin-top: 10px;
}
/* 汉字列表 */
.characters-list {
background: rgba(0, 0, 0, 0.7);
border-radius: 15px;
padding: 20px;
border: 2px solid rgba(255, 215, 0, 0.3);
flex: 1;
overflow-y: auto;
}
.characters-title {
font-size: 1.2rem;
color: #ffd700;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.characters-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.character-item {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 15px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.character-item:hover {
background: rgba(255, 255, 255, 0.2);
transform: translateY(-3px);
}
.character-item.active {
background: rgba(0, 188, 212, 0.3);
border-color: #00bcd4;
box-shadow: 0 0 15px rgba(0, 188, 212, 0.5);
}
.char-item {
font-size: 2.5rem;
font-weight: bold;
font-family: 'KaiTi', '楷体', 'STKaiti', serif;
}
.char-desc {
font-size: 0.8rem;
color: #b3e5fc;
margin-top: 5px;
}
/* 游戏控制 */
.game-controls {
background: rgba(0, 0, 0, 0.7);
border-radius: 15px;
padding: 20px;
border: 2px solid rgba(76, 175, 80, 0.3);
}
.control-title {
font-size: 1.2rem;
color: #4caf50;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.control-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.control-btn {
padding: 12px;
border: none;
border-radius: 10px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.control-btn.start {
background: linear-gradient(135deg, #4caf50, #2e7d32);
color: white;
}
.control-btn.pause {
background: linear-gradient(135deg, #ff9800, #ef6c00);
color: white;
}
.control-btn.next {
background: linear-gradient(135deg, #2196f3, #0d47a1);
color: white;
}
.control-btn.reset {
background: linear-gradient(135deg, #f44336, #b71c1c);
color: white;
}
.control-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
}
.control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
/* 识别状态 */
.recognition-status {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
border-radius: 10px;
padding: 15px;
display: flex;
align-items: center;
gap: 15px;
border: 2px solid rgba(255, 215, 0, 0.4);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
}
.status-indicator {
width: 20px;
height: 20px;
border-radius: 50%;
background: #f44336;
animation: pulse 1.5s infinite;
}
.status-indicator.detecting {
background: #4caf50;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.status-text {
flex: 1;
font-size: 1rem;
}
.match-progress {
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
flex: 1;
}
.match-fill {
height: 100%;
background: linear-gradient(90deg, #4caf50, #8bc34a);
width: 0%;
transition: width 0.3s ease;
}
/* 得分效果 */
.score-effect {
position: fixed;
pointer-events: none;
z-index: 1000;
font-size: 3rem;
font-weight: bold;
color: #ffeb3b;
text-shadow: 0 0 20px #ffeb3b;
animation: floatUp 1.5s ease-out forwards;
}
@keyframes floatUp {
0% {
transform: translateY(0) scale(1);
opacity: 1;
}
100% {
transform: translateY(-100px) scale(1.5);
opacity: 0;
}
}
/* 提示信息 */
.hint-message {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.8);
border-radius: 10px;
padding: 15px;
max-width: 300px;
border-left: 4px solid #00bcd4;
animation: slideIn 0.5s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.hint-title {
color: #00bcd4;
font-weight: bold;
margin-bottom: 5px;
}
.hint-text {
font-size: 0.9rem;
color: #e0f7fa;
}
/* 关节显示样式 */
.joint-info {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
border-radius: 10px;
padding: 15px;
max-width: 200px;
border: 1px solid rgba(255, 215, 0, 0.3);
}
.joint-title {
color: #ffd700;
font-weight: bold;
margin-bottom: 10px;
font-size: 0.9rem;
}
.joint-list {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.joint-tag {
background: rgba(255, 215, 0, 0.2);
color: #ffd700;
padding: 3px 8px;
border-radius: 5px;
font-size: 0.8rem;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.game-container {
grid-template-columns: 1fr;
grid-template-rows: auto 1fr auto;
}
.control-panel {
grid-row: 3;
flex-direction: row;
}
.characters-list {
flex: 1;
}
.character-display {
flex: 0 0 200px;
}
.game-controls {
flex: 0 0 200px;
}
}
@media (max-width: 768px) {
.game-container {
padding: 10px;
gap: 10px;
}
.character {
font-size: 5rem;
}
.info-value {
font-size: 1.5rem;
}
.characters-grid {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
<!-- MediaPipe Pose -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/control_utils/control_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils/drawing_utils.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/pose/pose.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="game-container">
<!-- 游戏主区域 -->
<div class="game-main">
<!-- 游戏头部 -->
<div class="game-header">
<div class="game-title">👤 人体汉字识别游戏</div>
<div class="game-info">
<div class="info-item">
<div class="info-value" id="score">0</div>
<div class="info-label">得分</div>
</div>
<div class="info-item">
<div class="info-value" id="level">1</div>
<div class="info-label">关卡</div>
</div>
<div class="info-item">
<div class="info-value" id="streak">0</div>
<div class="info-label">连击</div>
</div>
<div class="info-item">
<div class="info-value" id="accuracy">0%</div>
<div class="info-label">准确率</div>
</div>
</div>
</div>
<!-- 摄像头区域 -->
<div class="camera-area">
<canvas id="video-canvas"></canvas>
<div class="camera-overlay" id="pose-overlay"></div>
<!-- 识别状态 -->
<div class="recognition-status">
<div class="status-indicator" id="status-indicator"></div>
<div class="status-text" id="status-text">等待摄像头启动...</div>
<div class="match-progress">
<div class="match-fill" id="match-fill"></div>
</div>
</div>
<!-- 提示信息 -->
<div class="hint-message" id="hint-message" style="display: none;">
<div class="hint-title">💡 游戏提示</div>
<div class="hint-text">请用身体摆出当前汉字的形状!</div>
</div>
<!-- 关节信息 -->
<div class="joint-info" id="joint-info" style="display: none;">
<div class="joint-title">🦴 检测到的关节</div>
<div class="joint-list" id="joint-list"></div>
</div>
</div>
</div>
<!-- 侧边控制面板 -->
<div class="control-panel">
<!-- 当前汉字区域 -->
<div class="character-display">
<div class="character-title">
<span>🎯 当前汉字</span>
</div>
<div class="character" id="current-character">大</div>
<div class="character-pinyin" id="current-pinyin">dà</div>
</div>
<!-- 汉字列表 -->
<div class="characters-list">
<div class="characters-title">
<span>📖 可识别的汉字</span>
</div>
<div class="characters-grid" id="characters-grid">
<!-- 汉字列表由JS生成 -->
</div>
</div>
<!-- 游戏控制 -->
<div class="game-controls">
<div class="control-title">
<span>🎮 游戏控制</span>
</div>
<div class="control-buttons">
<button class="control-btn start" id="start-btn">
<span>▶️</span>
<span>开始游戏</span>
</button>
<button class="control-btn pause" id="pause-btn">
<span>⏸️</span>
<span>暂停</span>
</button>
<button class="control-btn next" id="next-btn">
<span>⏭️</span>
<span>下一个</span>
</button>
<button class="control-btn reset" id="reset-btn">
<span>🔄</span>
<span>重新开始</span>
</button>
</div>
</div>
</div>
</div>
<!-- 视频元素(隐藏) -->
<video class="input_video" id="input_video" style="display: none;"></video>
<script>
// ==========================================
// 1. 游戏配置
// ==========================================
const GameConfig = {
// 汉字库 - 人体可表达的汉字
characters: [
{ char: '大', pinyin: 'dà', description: '张开四肢站立', difficulty: 1 },
{ char: '小', pinyin: 'xiǎo', description: '双腿并拢站立', difficulty: 1 },
{ char: '人', pinyin: 'rén', description: '双腿分开站立', difficulty: 1 },
{ char: '十', pinyin: 'shí', description: '双臂平伸站立', difficulty: 2 },
{ char: '土', pinyin: 'tǔ', description: 'T字形站立', difficulty: 2 },
{ char: '工', pinyin: 'gōng', description: '双臂下垂站立', difficulty: 2 },
{ char: '口', pinyin: 'kǒu', description: '双手在头顶合拢', difficulty: 3 },
{ char: '中', pinyin: 'zhōng', description: '中心站立姿势', difficulty: 3 },
{ char: '天', pinyin: 'tiān', description: '头顶双手', difficulty: 3 },
{ char: '木', pinyin: 'mù', description: '树状姿势', difficulty: 3 },
{ char: '火', pinyin: 'huǒ', description: '火焰状姿势', difficulty: 4 },
{ char: '山', pinyin: 'shān', description: '山峰状姿势', difficulty: 4 }
],
// 姿势识别配置
poseDetection: {
minDetectionConfidence: 0.5,
minTrackingConfidence: 0.5,
modelComplexity: 1
},
// 游戏配置
game: {
initialScore: 0,
basePoints: 100,
streakBonus: 50,
matchThreshold: 0.7, // 匹配阈值
recognitionDelay: 2000, // 识别延迟(毫秒)
maxStreak: 10,
levelUpThreshold: 500 // 每500分升一级
}
};
// ==========================================
// 2. 游戏状态管理
// ==========================================
const GameState = {
isRunning: false,
isPaused: false,
currentCharacter: null,
currentPose: null,
score: 0,
level: 1,
streak: 0,
correctCount: 0,
totalAttempts: 0,
lastRecognitionTime: 0,
isRecognizing: false,
poseHistory: [],
detectedJoints: new Set(),
selectedCharacter: null
};
// ==========================================
// 3. 汉字姿势检测器
// ==========================================
class CharacterPoseDetector {
constructor() {
this.poseLandmarks = null;
this.lastDetectionTime = 0;
}
// 计算姿势与汉字的匹配度
matchCharacter(characterChar, landmarks) {
if (!landmarks) return 0;
// 根据不同的汉字,使用不同的匹配算法
switch(characterChar) {
case '大':
return this.matchDa(landmarks);
case '小':
return this.matchXiao(landmarks);
case '人':
return this.matchRen(landmarks);
case '十':
return this.matchShi(landmarks);
case '土':
return this.matchTu(landmarks);
case '工':
return this.matchGong(landmarks);
case '口':
return this.matchKou(landmarks);
case '中':
return this.matchZhong(landmarks);
case '天':
return this.matchTian(landmarks);
case '木':
return this.matchMu(landmarks);
case '火':
return this.matchHuo(landmarks);
case '山':
return this.matchShan(landmarks);
default:
return 0;
}
}
// 计算关键点之间的角度
calculateAngle(pointA, pointB, pointC) {
const AB = Math.sqrt(Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2));
const BC = Math.sqrt(Math.pow(pointB.x - pointC.x, 2) + Math.pow(pointB.y - pointC.y, 2));
const AC = Math.sqrt(Math.pow(pointC.x - pointA.x, 2) + Math.pow(pointC.y - pointA.y, 2));
if (AB === 0 || BC === 0) return 0;
const cosAngle = (Math.pow(AB, 2) + Math.pow(BC, 2) - Math.pow(AC, 2)) / (2 * AB * BC);
return Math.acos(Math.min(Math.max(cosAngle, -1), 1)) * (180 / Math.PI);
}
// 计算关键点之间的距离
calculateDistance(pointA, pointB) {
return Math.sqrt(Math.pow(pointB.x - pointA.x, 2) + Math.pow(pointB.y - pointA.y, 2));
}
// 检查关键点是否在一条直线上
checkCollinear(pointA, pointB, pointC, threshold = 10) {
const area = Math.abs(
pointA.x * (pointB.y - pointC.y) +
pointB.x * (pointC.y - pointA.y) +
pointC.x * (pointA.y - pointB.y)
) / 2;
return area < threshold;
}
// 匹配"大"字 - 张开四肢站立
matchDa(landmarks) {
try {
const leftShoulder = landmarks[11];
const rightShoulder = landmarks[12];
const leftHip = landmarks[23];
const rightHip = landmarks[24];
const leftKnee = landmarks[25];
const rightKnee = landmarks[26];
const leftAnkle = landmarks[27];
const rightAnkle = landmarks[28];
// 检查双腿分开
const hipDistance = this.calculateDistance(leftHip, rightHip);
const shoulderDistance = this.calculateDistance(leftShoulder, rightShoulder);
const legSpread = hipDistance / shoulderDistance;
// 检查手臂伸展(如果可见)
const leftArmAngle = this.calculateAngle(leftShoulder, leftHip, leftAnkle);
const rightArmAngle = this.calculateAngle(rightShoulder, rightHip, rightAnkle);
let score = 0;
// 腿部分开度评分(最佳比例 1.5-2.5)
if (legSpread > 1.2 && legSpread < 3) {
score += 0.4;
}
// 腿部伸直评分
const leftLegStraightness = Math.abs(180 - this.calculateAngle(leftHip, leftKnee, leftAnkle));
const rightLegStraightness = Math.abs(180 - this.calculateAngle(rightHip, rightKnee, rightAnkle));
if (leftLegStraightness < 30 && rightLegStraightness < 30) {
score += 0.3;
}
// 身体直立评分
const bodyAlignment = this.checkCollinear(leftShoulder, leftHip, leftAnkle);
if (bodyAlignment) {
score += 0.3;
}
return Math.min(score, 1);
} catch (e) {
console.error('匹配"大"字时出错:', e);
return 0;
}
}
// 匹配"小"字 - 双腿并拢站立
matchXiao(landmarks) {
try {
const leftShoulder = landmarks[11];
const rightShoulder = landmarks[12];
const leftHip = landmarks[23];
const rightHip = landmarks[24];
const leftAnkle = landmarks[27];
const rightAnkle = landmarks[28];
// 检查双腿并拢
const hipDistance = this.calculateDistance(leftHip, rightHip);
const ankleDistance = this.calculateDistance(leftAnkle, rightAnkle);
let score = 0;
// 腿部和脚踝靠近
if (hipDistance < 0.1 && ankleDistance < 0.1) {
score += 0.6;
}
// 身体直立
const bodyAlignment = this.checkCollinear(leftShoulder, leftHip, leftAnkle);
if (bodyAlignment) {
score += 0.4;
}
return Math.min(score, 1);
} catch (e) {
console.error('匹配"小"字时出错:', e);
return 0;
}
}
// 匹配"人"字 - 双腿分开站立
matchRen(landmarks) {
try {
const leftHip = landmarks[23];
const rightHip = landmarks[24];
const leftAnkle = landmarks[27];
const rightAnkle = landmarks[28];
// 计算腿部分开角度
const angle = this.calculateAngle(leftHip, leftAnkle, rightAnkle);
let score = 0;
// 理想的"人"字角度大约30-60度
if (angle > 20 && angle < 80) {
score = Math.min(angle / 60, 1);
}
return score;
} catch (e) {
console.error('匹配"人"字时出错:', e);
return 0;
}
}
// 匹配"十"字 - 双臂平伸站立
matchShi(landmarks) {
try {
const leftShoulder = landmarks[11];
const rightShoulder = landmarks[12];
const leftElbow = landmarks[13];
const rightElbow = landmarks[14];
const leftWrist = landmarks[15];
const rightWrist = landmarks[16];
const leftHip = landmarks[23];
const rightHip = landmarks[24];
let score = 0;
// 检查双臂水平伸展
const leftArmHorizontal = Math.abs(leftShoulder.y - leftWrist.y) < 0.1;
const rightArmHorizontal = Math.abs(rightShoulder.y - rightWrist.y) < 0.1;
if (leftArmHorizontal && rightArmHorizontal) {
score += 0.5;
}
// 检查手臂伸展程度
const leftArmLength = this.calculateDistance(leftShoulder, leftWrist);
const rightArmLength = this.calculateDistance(rightShoulder, rightWrist);
const shoulderWidth = this.calculateDistance(leftShoulder, rightShoulder);
if (leftArmLength > shoulderWidth * 0.8 && rightArmLength > shoulderWidth * 0.8) {
score += 0.3;
}
// 检查身体直立
const bodyStraight = Math.abs(leftShoulder.x - leftHip.x) < 0.1;
if (bodyStraight) {
score += 0.2;
}
return Math.min(score, 1);
} catch (e) {
console.error('匹配"十"字时出错:', e);
return 0;
}
}
// 匹配"土"字 - T字形站立
matchTu(landmarks) {
try {
const leftShoulder = landmarks[11];
const rightShoulder = landmarks[12];
const leftWrist = landmarks[15];
const rightWrist = landmarks[16];
const leftHip = landmarks[23];
const rightHip = landmarks[24];
let score = 0;
// 检查双臂水平(T字的上横)
const armsHorizontal = Math.abs(leftShoulder.y - rightShoulder.y) < 0.1;
const armsExtended = this.calculateDistance(leftWrist, rightWrist) >
this.calculateDistance(leftShoulder, rightShoulder) * 1.5;
if (armsHorizontal && armsExtended) {
score += 0.6;
}
// 检查身体垂直(T字的竖)
const bodyVertical = Math.abs(leftShoulder.x - leftHip.x) < 0.1;
if (bodyVertical) {
score += 0.4;
}
return Math.min(score, 1);
} catch (e) {
console.error('匹配"土"字时出错:', e);
return 0;
}
}
// 匹配"工"字 - 双臂下垂站立
matchGong(landmarks) {
try {
const leftShoulder = landmarks[11];
const rightShoulder = landmarks[12];
const leftElbow = landmarks[13];
const rightElbow = landmarks[14];
const leftWrist = landmarks[15];
const rightWrist = landmarks[16];
const leftHip = landmarks[23];
const rightHip = landmarks[24];
let score = 0;
// 检查双臂下垂
const leftArmVertical = Math.abs(leftShoulder.x - leftWrist.x) < 0.1;
const rightArmVertical = Math.abs(rightShoulder.x - rightWrist.x) < 0.1;
const leftArmStraight = this.calculateAngle(leftShoulder, leftElbow, leftWrist) > 150;
const rightArmStraight = this.calculateAngle(rightShoulder, rightElbow, rightWrist) > 150;
if (leftArmVertical && rightArmVertical && leftArmStraight && rightArmStraight) {
score += 0.5;
}
// 检查身体直立
const bodyStraight = Math.abs(leftShoulder.x - leftHip.x) < 0.1;
if (bodyStraight) {
score += 0.3;
}
// 检查双腿并拢
const hipsClose = this.calculateDistance(leftHip, rightHip) < 0.15;
if (hipsClose) {
score += 0.2;
}
return Math.min(score, 1);
} catch (e) {
console.error('匹配"工"字时出错:', e);
return 0;
}
}
// 其他汉字匹配方法(简化版)
matchKou(landmarks) {
// "口"字:双手在头顶合拢形成方形
const leftWrist = landmarks[15];
const rightWrist = landmarks[16];
const nose = landmarks[0];
const wristDistance = this.calculateDistance(leftWrist, rightWrist);
const heightAboveHead = nose.y - leftWrist.y;
let score = 0;
if (wristDistance < 0.2 && heightAboveHead > 0.1) {
score = 0.7;
}
return score;
}
matchZhong(landmarks) {
// "中"字:中心对称姿势
return this.matchShi(landmarks) * 0.8;
}
matchTian(landmarks) {
// "天"字:头顶双手
const leftWrist = landmarks[15];
const rightWrist = landmarks[16];
const nose = landmarks[0];
const wristHeight = Math.min(leftWrist.y, rightWrist.y);
let score = 0;
if (wristHeight < nose.y - 0.1) {
score = 0.6;
}
return score;
}
matchMu(landmarks) {
// "木"字:树状姿势,双臂上举
const leftWrist = landmarks[15];
const rightWrist = landmarks[16];
const leftShoulder = landmarks[11];
let score = 0;
if (leftWrist.y < leftShoulder.y && rightWrist.y < leftShoulder.y) {
score = 0.7;
}
return score;
}
matchHuo(landmarks) {
// "火"字:火焰状,不对称姿势
const leftWrist = landmarks[15];
const rightWrist = landmarks[16];
const leftShoulder = landmarks[11];
const rightShoulder = landmarks[12];
const leftArmAngle = this.calculateAngle(leftShoulder, leftWrist, rightShoulder);
const rightArmAngle = this.calculateAngle(rightShoulder, rightWrist, leftShoulder);
let score = 0;
if (Math.abs(leftArmAngle - rightArmAngle) > 30) {
score = 0.6;
}
return score;
}
matchShan(landmarks) {
// "山"字:山峰状,双臂形成三个顶点
const leftWrist = landmarks[15];
const rightWrist = landmarks[16];
const head = landmarks[0];
let score = 0;
const wristHeightDiff = Math.abs(leftWrist.y - rightWrist.y);
const headToWristDiff = Math.abs(head.y - (leftWrist.y + rightWrist.y) / 2);
if (wristHeightDiff > 0.1 && headToWristDiff > 0.2) {
score = 0.5;
}
return score;
}
// 获取检测到的关节信息
getDetectedJoints(landmarks) {
const joints = new Set();
if (!landmarks) return joints;
// MediaPipe Pose的33个关键点
const jointNames = [
'鼻子', '左眼内', '左眼', '左眼外', '右眼内', '右眼', '右眼外',
'左耳', '右耳', '左嘴', '右嘴', '左肩', '右肩', '左肘', '右肘',
'左手腕', '右手腕', '左小指', '右小指', '左食指', '右食指',
'左大拇指', '右大拇指', '左髋', '右髋', '左膝', '右膝',
'左踝', '右踝', '左脚跟', '右脚跟', '左脚尖', '右脚尖'
];
landmarks.forEach((landmark, index) => {
if (landmark.visibility > 0.5) {
joints.add(jointNames[index]);
}
});
return joints;
}
}
// ==========================================
// 4. 游戏主逻辑
// ==========================================
class GameManager {
constructor() {
this.poseDetector = new CharacterPoseDetector();
this.currentCharacterIndex = 0;
this.initializeUI();
this.initializePoseDetection();
this.setupEventListeners();
}
initializeUI() {
// 初始化汉字列表
const charactersGrid = document.getElementById('characters-grid');
charactersGrid.innerHTML = '';
GameConfig.characters.forEach((char, index) => {
const charElement = document.createElement('div');
charElement.className = 'character-item';
charElement.dataset.index = index;
charElement.innerHTML = `
<div class="char-item">${char.char}</div>
<div class="char-desc">${char.description}</div>
`;
charElement.addEventListener('click', () => {
this.selectCharacter(index);
});
charactersGrid.appendChild(charElement);
});
// 设置初始汉字
this.selectCharacter(0);
}
selectCharacter(index) {
// 移除之前的选择
document.querySelectorAll('.character-item').forEach(item => {
item.classList.remove('active');
});
// 设置新的选择
const charElement = document.querySelector(`[data-index="${index}"]`);
charElement.classList.add('active');
GameState.selectedCharacter = GameConfig.characters[index];
this.updateCurrentCharacter(GameConfig.characters[index]);
}
updateCurrentCharacter(character) {
GameState.currentCharacter = character;
document.getElementById('current-character').textContent = character.char;
document.getElementById('current-pinyin').textContent = character.pinyin;
// 显示提示
this.showHint(`请摆出"${character.char}"字的姿势:${character.description}`);
}
updateGameStats() {
document.getElementById('score').textContent = GameState.score;
document.getElementById('level').textContent = GameState.level;
document.getElementById('streak').textContent = GameState.streak;
if (GameState.totalAttempts > 0) {
const accuracy = Math.round((GameState.correctCount / GameState.totalAttempts) * 100);
document.getElementById('accuracy').textContent = `${accuracy}%`;
}
}
showHint(message, duration = 3000) {
const hintElement = document.getElementById('hint-message');
const hintText = document.querySelector('.hint-text');
hintText.textContent = message;
hintElement.style.display = 'block';
if (duration > 0) {
setTimeout(() => {
hintElement.style.display = 'none';
}, duration);
}
}
showScoreEffect(points, x, y) {
const effect = document.createElement('div');
effect.className = 'score-effect';
effect.textContent = `+${points}`;
effect.style.left = `${x}px`;
effect.style.top = `${y}px`;
document.body.appendChild(effect);
setTimeout(() => {
effect.remove();
}, 1500);
}
updateJointInfo(joints) {
const jointList = document.getElementById('joint-list');
const jointInfo = document.getElementById('joint-info');
if (joints.size > 0) {
jointList.innerHTML = '';
joints.forEach(joint => {
const tag = document.createElement('span');
tag.className = 'joint-tag';
tag.textContent = joint;
jointList.appendChild(tag);
});
jointInfo.style.display = 'block';
} else {
jointInfo.style.display = 'none';
}
}
checkPoseMatch(landmarks) {
if (!GameState.isRunning || GameState.isPaused || !GameState.currentCharacter) return;
const now = Date.now();
if (now - GameState.lastRecognitionTime < GameConfig.game.recognitionDelay) return;
// 获取检测到的关节
const detectedJoints = this.poseDetector.getDetectedJoints(landmarks);
GameState.detectedJoints = detectedJoints;
this.updateJointInfo(detectedJoints);
// 计算匹配度
const matchScore = this.poseDetector.matchCharacter(
GameState.currentCharacter.char,
landmarks
);
// 更新匹配进度条
const matchFill = document.getElementById('match-fill');
matchFill.style.width = `${matchScore * 100}%`;
// 更新状态指示器
const statusIndicator = document.getElementById('status-indicator');
const statusText = document.getElementById('status-text');
if (matchScore > 0) {
statusIndicator.className = 'status-indicator detecting';
statusText.textContent = `匹配度: ${Math.round(matchScore * 100)}%`;
// 如果匹配度超过阈值
if (matchScore >= GameConfig.game.matchThreshold) {
this.onPoseMatched(matchScore);
GameState.lastRecognitionTime = now;
}
} else {
statusIndicator.className = 'status-indicator';
statusText.textContent = '请摆出正确的姿势...';
}
}
onPoseMatched(matchScore) {
if (!GameState.isRunning) return;
GameState.totalAttempts++;
GameState.correctCount++;
// 计算得分
const basePoints = GameConfig.game.basePoints;
const streakBonus = GameState.streak * GameConfig.game.streakBonus;
const accuracyBonus = Math.round(matchScore * 50);
const totalPoints = basePoints + streakBonus + accuracyBonus;
// 更新游戏状态
GameState.score += totalPoints;
GameState.streak++;
// 检查升级
if (GameState.score >= GameState.level * GameConfig.game.levelUpThreshold) {
GameState.level++;
}
// 限制连击数
if (GameState.streak > GameConfig.game.maxStreak) {
GameState.streak = GameConfig.game.maxStreak;
}
// 更新UI
this.updateGameStats();
// 显示得分效果
const canvas = document.getElementById('video-canvas');
const x = Math.random() * canvas.clientWidth;
const y = canvas.clientHeight / 2;
this.showScoreEffect(totalPoints, x, y);
// 显示成功消息
this.showHint(`太棒了!成功匹配"${GameState.currentCharacter.char}"字!得分+${totalPoints}`, 2000);
// 自动选择下一个汉字(如果游戏正在运行)
setTimeout(() => {
if (GameState.isRunning && !GameState.isPaused) {
this.nextCharacter();
}
}, 1500);
}
nextCharacter() {
const currentIndex = GameConfig.characters.findIndex(
char => char.char === GameState.currentCharacter.char
);
let nextIndex = (currentIndex + 1) % GameConfig.characters.length;
this.selectCharacter(nextIndex);
// 重置匹配进度
document.getElementById('match-fill').style.width = '0%';
}
startGame() {
GameState.isRunning = true;
GameState.isPaused = false;
document.getElementById('start-btn').disabled = true;
document.getElementById('pause-btn').disabled = false;
this.showHint('游戏开始!请用身体摆出当前汉字的形状。');
// 重置状态指示器
const statusIndicator = document.getElementById('status-indicator');
const statusText = document.getElementById('status-text');
statusIndicator.className = 'status-indicator';
statusText.textContent = '等待姿势识别...';
}
pauseGame() {
GameState.isPaused = !GameState.isPaused;
const pauseBtn = document.getElementById('pause-btn');
pauseBtn.innerHTML = GameState.isPaused ?
'<span>▶️</span><span>继续</span>' :
'<span>⏸️</span><span>暂停</span>';
const statusText = document.getElementById('status-text');
statusText.textContent = GameState.isPaused ? '游戏已暂停' : '等待姿势识别...';
}
resetGame() {
GameState.score = 0;
GameState.level = 1;
GameState.streak = 0;
GameState.correctCount = 0;
GameState.totalAttempts = 0;
this.updateGameStats();
this.selectCharacter(0);
document.getElementById('match-fill').style.width = '0%';
this.showHint('游戏已重置,请重新开始。');
}
setupEventListeners() {
// 开始按钮
document.getElementById('start-btn').addEventListener('click', () => {
this.startGame();
});
// 暂停按钮
document.getElementById('pause-btn').addEventListener('click', () => {
this.pauseGame();
});
// 下一个按钮
document.getElementById('next-btn').addEventListener('click', () => {
if (GameState.isRunning && !GameState.isPaused) {
this.nextCharacter();
}
});
// 重置按钮
document.getElementById('reset-btn').addEventListener('click', () => {
this.resetGame();
});
// 键盘快捷键
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
if (!GameState.isRunning) {
this.startGame();
} else {
this.pauseGame();
}
}
if (e.code === 'ArrowRight' && GameState.isRunning && !GameState.isPaused) {
this.nextCharacter();
}
if (e.code === 'KeyR') {
this.resetGame();
}
// 数字键1-9选择汉字
if (e.code >= 'Digit1' && e.code <= 'Digit9') {
const index = parseInt(e.code[5]) - 1;
if (index < GameConfig.characters.length) {
this.selectCharacter(index);
}
}
});
}
initializePoseDetection() {
const videoElement = document.getElementById('input_video');
const canvasElement = document.getElementById('video-canvas');
const canvasCtx = canvasElement.getContext('2d');
function onResults(results) {
if (!results.poseLandmarks) {
// 没有检测到姿势
const statusText = document.getElementById('status-text');
statusText.textContent = '未检测到人体,请站在摄像头前';
return;
}
// 绘制摄像头画面
canvasCtx.save();
canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
canvasCtx.drawImage(results.image, 0, 0, canvasElement.width, canvasElement.height);
// 绘制姿势关键点和连线
if (results.poseLandmarks) {
if (window.drawConnectors && window.drawLandmarks) {
drawConnectors(canvasCtx, results.poseLandmarks, POSE_CONNECTIONS, {
color: '#00FF00',
lineWidth: 2
});
drawLandmarks(canvasCtx, results.poseLandmarks, {
color: '#FF0000',
lineWidth: 1,
radius: 4
});
}
}
canvasCtx.restore();
// 检查姿势匹配
gameManager.checkPoseMatch(results.poseLandmarks);
}
// 初始化MediaPipe Pose
const pose = new Pose({
locateFile: (file) => {
return `https://cdn.jsdelivr.net/npm/@mediapipe/pose/${file}`;
}
});
pose.setOptions({
modelComplexity: GameConfig.poseDetection.modelComplexity,
smoothLandmarks: true,
minDetectionConfidence: GameConfig.poseDetection.minDetectionConfidence,
minTrackingConfidence: GameConfig.poseDetection.minTrackingConfidence
});
pose.onResults(onResults);
// 启动摄像头
const camera = new Camera(videoElement, {
onFrame: async () => {
await pose.send({image: videoElement});
},
width: 640,
height: 480
});
camera.start().then(() => {
console.log('摄像头已启动,姿势识别准备就绪');
// 更新状态显示
const statusIndicator = document.getElementById('status-indicator');
const statusText = document.getElementById('status-text');
statusIndicator.className = 'status-indicator';
statusText.textContent = '姿势识别已就绪,请开始游戏';
// 显示提示
gameManager.showHint('摄像头已启动,请点击"开始游戏"按钮开始。');
}).catch(error => {
console.error('摄像头启动失败:', error);
const statusText = document.getElementById('status-text');
statusText.textContent = '摄像头启动失败,请检查权限';
statusText.style.color = '#ff4444';
gameManager.showHint('摄像头启动失败,请检查浏览器权限设置。', 5000);
});
}
}
// ==========================================
// 5. 初始化游戏
// ==========================================
let gameManager;
window.addEventListener('DOMContentLoaded', () => {
gameManager = new GameManager();
gameManager.updateGameStats();
});
// 窗口大小调整处理
window.addEventListener('resize', () => {
const canvas = document.getElementById('video-canvas');
if (canvas) {
// 保持摄像头画面的宽高比
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
});
</script>
</body>
</html>