人体汉字识别游戏

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>
相关推荐
wulijuan88866610 小时前
Web Worker
前端·javascript
老朋友此林10 小时前
React Hook原理速通笔记1(useEffect 原理、使用踩坑、渲染周期、依赖项)
javascript·笔记·react.js
克里斯蒂亚诺更新10 小时前
vue3使用pinia替代vuex举例
前端·javascript·vue.js
无限大.11 小时前
为什么游戏需要“加载时间“?——从硬盘读取到内存渲染
网络·人工智能·游戏
冰暮流星11 小时前
javascript赋值运算符
开发语言·javascript·ecmascript
西凉的悲伤11 小时前
html制作太阳系行星运行轨道演示动画
前端·javascript·html·行星运行轨道演示动画
低保和光头哪个先来12 小时前
源码篇 实例方法
前端·javascript·vue.js
TESmart碲视12 小时前
TESmart 推出全新 DP 1.4 双 8K@60Hz KVM 切换游戏扩展坞,助力专业与游戏工作流高效整合
游戏·计算机外设·音视频·kvm切换器·tesmart
你真的可爱呀12 小时前
自定义颜色选择功能
开发语言·前端·javascript
小王和八蛋12 小时前
JS中 escape urlencodeComponent urlencode 区别
前端·javascript