请创建一个视觉精美、交互流畅的进阶版贪吃蛇游戏

请创建一个视觉精美、交互流畅的进阶版贪吃蛇游戏,需包含以下功能和设计要求: 1. 核心玩法: - 采用经典贪吃蛇机制,通过键盘方向键控制蛇的移动(上、下、左、右) - 食物在游戏区域内随机生成,不会与蛇身重叠 - 蛇吃到食物后身体增长,得分增加 2. 特殊机制: - 每吃1个食物,蛇的移动速度轻微增加(最低速度限制为50ms间隔) - 蛇身颜色随得分增加按彩虹色循环变化(红→橙→黄→绿→蓝→靛→紫→红),且身体各段颜色略有差异以增强层次感 3. 视觉设计: - 蛇身:渐变色圆角矩形,带有微妙高光和"呼吸"缩放动画(轻微大小变化),头部有随方向变化的眼睛(带微动效果) - 食物:旋转的星形,带有从亮黄到橙红的径向渐变,同时有闪烁动画和发光效果 - 背景:深色渐变(深蓝到靛蓝),带有缓慢平移的网格线(30px×30px) - 分数显示:位于右上角,半透明背景配合模糊效果,分数变化时有缩放动画 - 游戏区域:带渐变色边框,悬停时有微妙阴影变化 4. 界面元素: - 顶部显示游戏标题("进阶版贪吃蛇"),采用渐变色文字和缓慢脉冲动画 - 底部显示操作说明(方向键控制) - 游戏结束时显示全屏半透明遮罩,包含"游戏结束"标题(渐变色)、最终分数和重新开始按钮(带悬停缩放效果) 5. 技术要求: - 游戏画面不显示滚动条,适配不同屏幕尺寸 - 调整背景网格数量,确保所有文字(标题、说明、分数)在任何设备上都能完整显示 - 支持触摸滑动控制(移动端适配) - 吃到食物时有彩色粒子爆炸效果,增强反馈感 6. 细节优化: - 游戏元素添加适当阴影和发光效果,增强视觉层次 - 界面过渡(如游戏结束画面)使用淡入动画 - 按钮和可交互元素有明确的状态反馈(悬停、点击)

画面优化的进阶版贪吃蛇游戏

V1

创建时间:23:11

产物已经生成完毕,如果你有更多灵感,请与我对话,继续完善或修改应用。

snake-game.html (1-579)

解释这段代码

已完成思考

这段代码实现了一个视觉精美、交互流畅的进阶版贪吃蛇游戏,完全满足了你提出的所有功能和设计要求。下面我将分部分解释代码的结构和功能:

1. 基础结构与资源引入

代码以标准 HTML5 结构编写,包含<head><body>两部分:

  • 引入了Tailwind CSS(用于快速构建响应式 UI)和Font Awesome(提供图标支持)
  • 配置了 Tailwind 自定义主题,定义了游戏专用颜色(如primarysecondary、深色系背景)和字体(font-game

2. 自定义样式与动画(<style>部分)

通过@layer utilities定义了游戏所需的特殊样式和动画,核心包括:

  • 背景效果grid-bg实现带缓慢平移动画的网格背景(30px×30px)
  • 视觉增强text-shadow(文字阴影)、glow(发光效果)、border-gradient(渐变边框)
  • 彩虹色定义rainbow-1rainbow-8共 8 种渐变色,实现蛇身的彩虹循环效果
  • 动画关键帧gridShift(网格平移)、pulse(脉冲缩放)、fadeIn(淡入)等,用于界面元素的动态效果

3. 页面布局(<body>部分)

采用响应式设计,主要包含 4 个核心区域:

  • 标题与分数区:顶部显示渐变色标题(带脉冲动画)和半透明分数面板(带发光效果)
  • 游戏主区域 :中间的canvas元素,用于绘制蛇、食物和粒子效果,容器带有渐变边框和悬停阴影效果
  • 游戏结束界面:全屏半透明遮罩,包含渐变色 "游戏结束" 标题、最终分数和带悬停效果的重新开始按钮
  • 操作说明区:底部显示方向键控制说明和游戏机制提示

4. 游戏核心逻辑(<script>部分)

这部分是游戏的灵魂,实现了所有交互和视觉效果,主要包括:

初始化与配置
  • resizeCanvas():确保游戏画布适配不同屏幕尺寸,无滚动条
  • 定义游戏核心变量:蛇的位置数组(snake)、食物位置(food)、移动方向(direction)、分数(score)、速度(gameSpeed)等
核心功能函数
  • initGame():游戏初始化,设置蛇的初始位置、分数、速度,生成首个食物并启动游戏循环
  • generateFood():随机生成食物,确保不与蛇身重叠
  • drawSnake() :绘制蛇身,实现了:
    • 彩虹渐变色(随分数循环变化)
    • 身体各段颜色差异(增强层次感)
    • 呼吸动画(轻微缩放效果)
    • 头部眼睛(随移动方向变化,带微动效果)
  • drawFood() :绘制旋转的星形食物,带有:
    • 黄到橙红的径向渐变
    • 闪烁动画(大小变化)
    • 发光边框效果
  • gameLoop() :游戏主循环,处理:
    • 蛇的移动逻辑
    • 食物碰撞检测(吃到食物后增长身体、增加分数、提升速度)
    • 边界和自碰撞检测(游戏结束判断)
  • drawParticles():吃到食物时显示彩色粒子爆炸效果,增强反馈
交互控制
  • 键盘控制:通过方向键控制蛇的移动,支持游戏结束后按空格重启
  • 触摸控制:支持移动端滑动操作,通过计算滑动方向判断移动意图

5. 特色机制实现

  • 速度递增 :每吃 1 个食物,速度轻微增加(gameSpeed递减),最低限制为 50ms
  • 颜色循环 :通过currentColorIndex控制蛇身颜色,按彩虹色序循环变化
  • 视觉反馈:分数变化时有缩放动画,吃到食物有粒子效果,按钮有悬停状态反馈

整体而言,代码通过 Canvas 绘图 API 实现游戏核心逻辑,结合 Tailwind CSS 和自定义动画实现了精美的视觉效果,同时兼顾了桌面端和移动端的交互体验,完全符合你提出的进阶版贪吃蛇游戏要求。

分享

除了彩虹色,还可以为蛇身设计哪些独特的颜色变化?

怎样增加游戏的趣味性,比如设置关卡或特殊道具?

如何实现蛇身碰撞检测和游戏结束判定?

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>进阶版贪吃蛇</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#4F46E5',
                        secondary: '#EC4899',
                        dark: '#0F172A',
                        darker: '#020617',
                        'dark-accent': '#1E293B',
                        light: '#F9FAFB'
                    },
                    fontFamily: {
                        game: ['Poppins', 'sans-serif'],
                    },
                }
            }
        }
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }
            .grid-bg {
                background-size: 30px 30px;
                background-image: 
                    linear-gradient(to right, rgba(255,255,255,0.07) 1px, transparent 1px),
                    linear-gradient(to bottom, rgba(255,255,255,0.07) 1px, transparent 1px);
                animation: gridShift 20s linear infinite;
            }
            .text-shadow {
                text-shadow: 0 2px 8px rgba(0,0,0,0.4);
            }
            .glow {
                filter: drop-shadow(0 0 8px rgba(79, 70, 229, 0.5));
            }
            .food-glow {
                filter: drop-shadow(0 0 10px rgba(255, 165, 0, 0.7));
            }
            .bg-game-gradient {
                background: linear-gradient(135deg, #0F172A 0%, #1E293B 100%);
            }
            .border-gradient {
                border-image: linear-gradient(45deg, #4F46E5, #EC4899) 1;
            }
            .rainbow-1 { background: linear-gradient(90deg, #FF5E5E, #FF7D5E); }
            .rainbow-2 { background: linear-gradient(90deg, #FF7D5E, #FFB347); }
            .rainbow-3 { background: linear-gradient(90deg, #FFB347, #FFF275); }
            .rainbow-4 { background: linear-gradient(90deg, #FFF275, #93FF96); }
            .rainbow-5 { background: linear-gradient(90deg, #93FF96, #42D7F5); }
            .rainbow-6 { background: linear-gradient(90deg, #42D7F5, #7D5FFF); }
            .rainbow-7 { background: linear-gradient(90deg, #7D5FFF, #C774E8); }
            .rainbow-8 { background: linear-gradient(90deg, #C774E8, #FF5E5E); }
            
            @keyframes gridShift {
                0% { background-position: 0 0; }
                100% { background-position: 30px 30px; }
            }
            @keyframes pulse {
                0%, 100% { transform: scale(1); }
                50% { transform: scale(1.05); }
            }
            @keyframes fadeIn {
                from { opacity: 0; transform: scale(0.9); }
                to { opacity: 1; transform: scale(1); }
            }
            .animate-fade-in {
                animation: fadeIn 0.5s ease-out forwards;
            }
            .animate-pulse-slow {
                animation: pulse 3s infinite;
            }
        }
    </style>
</head>
<body class="bg-game-gradient font-game text-light min-h-screen flex flex-col items-center justify-start p-4 overflow-hidden pt-4">
    <div class="max-w-3xl w-full flex flex-col h-[calc(100vh-2rem)]">
        <header class="mb-4 flex justify-between items-center flex-shrink-0">
            <h1 class="text-[clamp(1.2rem,4vw,2rem)] font-bold text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary text-shadow animate-pulse-slow">进阶版贪吃蛇</h1>
            <div id="score-display" class="bg-dark/60 backdrop-blur-md px-4 py-2 rounded-lg border border-white/15 shadow-lg glow transition-all duration-300 hover:shadow-primary/20">
                <span class="text-sm font-medium text-white/70">分数:</span>
                <span id="score" class="text-xl font-bold ml-2 transition-all duration-300">0</span>
            </div>
        </header>
        
        <div class="relative bg-dark-accent/80 backdrop-blur-sm rounded-xl overflow-hidden shadow-2xl border-2 border-transparent border-gradient flex-grow mb-4 transition-all duration-500 hover:shadow-primary/10">
            <canvas id="gameCanvas" class="w-full h-full grid-bg"></canvas>
            
            <div id="gameOver" class="absolute inset-0 bg-dark/70 backdrop-blur-md flex flex-col items-center justify-center hidden animate-fade-in">
                <h2 class="text-[clamp(2rem,6vw,3.5rem)] font-bold mb-2 text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-yellow-500 text-shadow">游戏结束!</h2>
                <p class="text-[clamp(1rem,3vw,1.5rem)] mb-6">最终分数: <span id="finalScore" class="font-bold">0</span></p>
                <button id="restartButton" class="px-8 py-3 bg-gradient-to-r from-primary to-secondary rounded-full text-white font-bold shadow-lg hover:shadow-primary/30 hover:scale-105 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-primary/50">
                    <i class="fa fa-refresh mr-2"></i>重新开始
                </button>
            </div>
        </div>
        
        <div class="mt-2 text-center text-white/60 text-sm flex-shrink-0">
            <p class="flex flex-wrap justify-center items-center gap-x-1">使用方向键 
                <i class="fa fa-arrow-up mx-1"></i> 
                <i class="fa fa-arrow-down mx-1"></i> 
                <i class="fa fa-arrow-left mx-1"></i> 
                <i class="fa fa-arrow-right mx-1"></i> 
                控制蛇的移动
            </p>
            <p class="mt-1">每吃一个食物,蛇的速度会增加,颜色也会变化</p>
        </div>
    </div>

    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const canvas = document.getElementById('gameCanvas');
            const ctx = canvas.getContext('2d');
            const scoreDisplay = document.getElementById('score');
            const gameOverScreen = document.getElementById('gameOver');
            const finalScoreDisplay = document.getElementById('finalScore');
            const restartButton = document.getElementById('restartButton');
            const gameContainer = canvas.parentElement;
            
            // 设置Canvas尺寸
            function resizeCanvas() {
                const containerWidth = gameContainer.clientWidth;
                const containerHeight = gameContainer.clientHeight;
                canvas.width = containerWidth;
                canvas.height = containerHeight;
            }
            
            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
            
            // 游戏变量
            let snake = [];
            let food = {};
            let direction = 'right';
            let nextDirection = 'right';
            let score = 0;
            let gameSpeed = 150;
            let gameInterval;
            let isGameOver = false;
            let gridSize;
            let snakeSize;
            let foodSize;
            let rainbowColors = [
                'rainbow-1', 'rainbow-2', 'rainbow-3', 'rainbow-4',
                'rainbow-5', 'rainbow-6', 'rainbow-7', 'rainbow-8'
            ];
            let currentColorIndex = 0;
            let foodBlinkState = 1;
            let foodBlinkRate = 0.02;
            
            // 初始化游戏
            function initGame() {
                gridSize = Math.floor(canvas.width / 25);
                snakeSize = gridSize * 0.8;
                foodSize = gridSize * 0.9;
                
                snake = [
                    {x: 5 * gridSize, y: 5 * gridSize},
                    {x: 4 * gridSize, y: 5 * gridSize},
                    {x: 3 * gridSize, y: 5 * gridSize}
                ];
                direction = 'right';
                nextDirection = 'right';
                score = 0;
                gameSpeed = 150;
                isGameOver = false;
                currentColorIndex = 0;
                
                scoreDisplay.textContent = score;
                gameOverScreen.classList.add('hidden');
                
                generateFood();
                
                if (gameInterval) clearInterval(gameInterval);
                gameInterval = setInterval(gameLoop, gameSpeed);
            }
            
            // 生成食物
            function generateFood() {
                const maxX = Math.floor(canvas.width / gridSize) - 1;
                const maxY = Math.floor(canvas.height / gridSize) - 1;
                
                let overlapping;
                do {
                    overlapping = false;
                    food = {
                        x: Math.floor(Math.random() * maxX) * gridSize,
                        y: Math.floor(Math.random() * maxY) * gridSize,
                        blink: 1,
                        blinkDir: -foodBlinkRate
                    };
                    
                    for (let segment of snake) {
                        if (segment.x === food.x && segment.y === food.y) {
                            overlapping = true;
                            break;
                        }
                    }
                } while (overlapping);
            }
            
            // 绘制蛇(带平滑动画效果)
            function drawSnake() {
                snake.forEach((segment, index) => {
                    // 计算每个段的颜色(略微变化以增强层次感)
                    const colorIndex = (currentColorIndex + Math.floor(index / 3)) % rainbowColors.length;
                    const colorClass = rainbowColors[colorIndex];
                    const colors = getGradientColors(colorClass);
                    
                    // 创建渐变色
                    const gradient = ctx.createLinearGradient(
                        segment.x, segment.y, 
                        segment.x + snakeSize, segment.y + snakeSize
                    );
                    gradient.addColorStop(0, colors.start);
                    gradient.addColorStop(1, colors.end);
                    
                    // 蛇身体轻微缩放动画(呼吸效果)
                    const scale = 1 + Math.sin(Date.now() / 300 + index * 0.5) * 0.02;
                    
                    ctx.save();
                    ctx.translate(segment.x + gridSize/2, segment.y + gridSize/2);
                    ctx.scale(scale, scale);
                    ctx.translate(-(segment.x + gridSize/2), -(segment.y + gridSize/2));
                    
                    // 绘制蛇身体(带圆角)
                    ctx.fillStyle = gradient;
                    ctx.beginPath();
                    ctx.roundRect(
                        segment.x + (gridSize - snakeSize) / 2,
                        segment.y + (gridSize - snakeSize) / 2,
                        snakeSize, snakeSize, snakeSize / 4
                    );
                    ctx.fill();
                    
                    // 添加高光效果
                    ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
                    ctx.beginPath();
                    ctx.roundRect(
                        segment.x + (gridSize - snakeSize) / 2,
                        segment.y + (gridSize - snakeSize) / 2,
                        snakeSize * 0.6, snakeSize * 0.6, snakeSize / 8
                    );
                    ctx.fill();
                    
                    ctx.restore();
                    
                    // 绘制眼睛(只在头部)
                    if (index === 0) {
                        drawEyes(segment);
                    }
                });
            }
            
            // 绘制蛇的眼睛
            function drawEyes(segment) {
                ctx.fillStyle = '#fff';
                
                // 根据方向调整眼睛位置
                let leftEyeX, leftEyeY, rightEyeX, rightEyeY;
                const eyeSize = snakeSize / 8;
                const eyeOffset = Math.sin(Date.now() / 300) * eyeSize * 0.3; // 眼睛动画
                
                switch(direction) {
                    case 'right':
                        leftEyeX = segment.x + snakeSize * 0.7 + eyeOffset;
                        leftEyeY = segment.y + snakeSize * 0.3;
                        rightEyeX = segment.x + snakeSize * 0.7 + eyeOffset;
                        rightEyeY = segment.y + snakeSize * 0.7;
                        break;
                    case 'left':
                        leftEyeX = segment.x + snakeSize * 0.3 - eyeOffset;
                        leftEyeY = segment.y + snakeSize * 0.3;
                        rightEyeX = segment.x + snakeSize * 0.3 - eyeOffset;
                        rightEyeY = segment.y + snakeSize * 0.7;
                        break;
                    case 'up':
                        leftEyeX = segment.x + snakeSize * 0.3;
                        leftEyeY = segment.y + snakeSize * 0.3 - eyeOffset;
                        rightEyeX = segment.x + snakeSize * 0.7;
                        rightEyeY = segment.y + snakeSize * 0.3 - eyeOffset;
                        break;
                    case 'down':
                        leftEyeX = segment.x + snakeSize * 0.3;
                        leftEyeY = segment.y + snakeSize * 0.7 + eyeOffset;
                        rightEyeX = segment.x + snakeSize * 0.7;
                        rightEyeY = segment.y + snakeSize * 0.7 + eyeOffset;
                        break;
                }
                
                // 眼睛白色部分
                ctx.beginPath();
                ctx.arc(leftEyeX, leftEyeY, eyeSize, 0, Math.PI * 2);
                ctx.arc(rightEyeX, rightEyeY, eyeSize, 0, Math.PI * 2);
                ctx.fill();
                
                // 瞳孔(随时间微动)
                const pupilOffsetX = Math.sin(Date.now() / 500) * eyeSize * 0.2;
                const pupilOffsetY = Math.cos(Date.now() / 500) * eyeSize * 0.2;
                
                ctx.fillStyle = '#000';
                ctx.beginPath();
                ctx.arc(leftEyeX + pupilOffsetX, leftEyeY + pupilOffsetY, eyeSize / 2, 0, Math.PI * 2);
                ctx.arc(rightEyeX + pupilOffsetX, rightEyeY + pupilOffsetY, eyeSize / 2, 0, Math.PI * 2);
                ctx.fill();
            }
            
            // 获取渐变颜色
            function getGradientColors(colorClass) {
                const gradients = {
                    'rainbow-1': { start: '#FF5E5E', end: '#FF7D5E' },
                    'rainbow-2': { start: '#FF7D5E', end: '#FFB347' },
                    'rainbow-3': { start: '#FFB347', end: '#FFF275' },
                    'rainbow-4': { start: '#FFF275', end: '#93FF96' },
                    'rainbow-5': { start: '#93FF96', end: '#42D7F5' },
                    'rainbow-6': { start: '#42D7F5', end: '#7D5FFF' },
                    'rainbow-7': { start: '#7D5FFF', end: '#C774E8' },
                    'rainbow-8': { start: '#C774E8', end: '#FF5E5E' }
                };
                
                return gradients[colorClass] || { start: '#4F46E5', end: '#EC4899' };
            }
            
            // 绘制食物(增强动画效果)
            function drawFood() {
                // 更新食物闪烁状态
                food.blink += food.blinkDir;
                if (food.blink <= 0.8 || food.blink >= 1.2) {
                    food.blinkDir *= -1;
                }
                
                ctx.save();
                ctx.translate(food.x + gridSize / 2, food.y + gridSize / 2);
                ctx.rotate(Date.now() / 800); // 稍快的旋转速度
                ctx.scale(food.blink, food.blink); // 闪烁效果
                
                // 绘制星星
                ctx.beginPath();
                ctx.moveTo(0, -foodSize / 2);
                for (let i = 1; i < 10; i++) {
                    const angle = (i * 2 * Math.PI / 10) - (Math.PI / 2);
                    const radius = i % 2 === 0 ? foodSize / 2 : foodSize / 5;
                    ctx.lineTo(Math.cos(angle) * radius, Math.sin(angle) * radius);
                }
                ctx.closePath();
                
                // 创建更丰富的径向渐变
                const gradient = ctx.createRadialGradient(
                    0, 0, 0,
                    0, 0, foodSize / 2
                );
                gradient.addColorStop(0, '#FFEE58');
                gradient.addColorStop(0.5, '#FFC107');
                gradient.addColorStop(1, '#FF8F00');
                
                ctx.fillStyle = gradient;
                ctx.fill();
                
                // 添加更亮的边框和发光效果
                ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
                ctx.lineWidth = 2;
                ctx.stroke();
                
                // 中心高光
                ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
                ctx.beginPath();
                ctx.arc(0, 0, foodSize / 8, 0, Math.PI * 2);
                ctx.fill();
                
                ctx.restore();
            }
            
            // 绘制吃到食物的粒子效果
            function drawParticles(x, y) {
                for (let i = 0; i < 8; i++) {
                    const angle = (i * Math.PI / 4);
                    const distance = gridSize * 0.6;
                    const particleX = x + Math.cos(angle) * distance;
                    const particleY = y + Math.sin(angle) * distance;
                    
                    const colorClass = rainbowColors[i % rainbowColors.length];
                    const colors = getGradientColors(colorClass);
                    
                    ctx.fillStyle = colors.start;
                    ctx.beginPath();
                    ctx.arc(particleX, particleY, gridSize / 8, 0, Math.PI * 2);
                    ctx.fill();
                }
            }
            
            // 检查碰撞
            function checkCollision() {
                const head = {...snake[0]};
                
                if (
                    head.x < 0 || 
                    head.x >= canvas.width || 
                    head.y < 0 || 
                    head.y >= canvas.height
                ) {
                    return true;
                }
                
                for (let i = 1; i < snake.length; i++) {
                    if (head.x === snake[i].x && head.y === snake[i].y) {
                        return true;
                    }
                }
                
                return false;
            }
            
            // 分数增加动画
            function animateScoreIncrease() {
                const scoreElement = document.getElementById('score');
                scoreElement.classList.add('scale-125', 'text-secondary');
                setTimeout(() => {
                    scoreElement.classList.remove('scale-125', 'text-secondary');
                }, 300);
            }
            
            // 游戏主循环
            function gameLoop() {
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                
                direction = nextDirection;
                
                const head = {x: snake[0].x, y: snake[0].y};
                
                switch(direction) {
                    case 'up':
                        head.y -= gridSize;
                        break;
                    case 'down':
                        head.y += gridSize;
                        break;
                    case 'left':
                        head.x -= gridSize;
                        break;
                    case 'right':
                        head.x += gridSize;
                        break;
                }
                
                snake.unshift(head);
                let ateFood = false;
                
                if (head.x === food.x && head.y === food.y) {
                    score += 10;
                    scoreDisplay.textContent = score;
                    animateScoreIncrease();
                    
                    gameSpeed = Math.max(50, gameSpeed - 5);
                    currentColorIndex++;
                    generateFood();
                    clearInterval(gameInterval);
                    gameInterval = setInterval(gameLoop, gameSpeed);
                    ateFood = true;
                } else {
                    snake.pop();
                }
                
                if (checkCollision()) {
                    gameOver();
                    return;
                }
                
                drawSnake();
                drawFood();
                
                // 如果吃到食物,绘制粒子效果
                if (ateFood) {
                    drawParticles(head.x + gridSize/2, head.y + gridSize/2);
                }
            }
            
            // 游戏结束
            function gameOver() {
                isGameOver = true;
                clearInterval(gameInterval);
                finalScoreDisplay.textContent = score;
                gameOverScreen.classList.remove('hidden');
            }
            
            // 键盘控制
            document.addEventListener('keydown', (e) => {
                if (isGameOver && e.key === ' ') {
                    initGame();
                    return;
                }
                
                if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
                    e.preventDefault();
                }
                
                switch(e.key) {
                    case 'ArrowUp':
                        if (direction !== 'down') {
                            nextDirection = 'up';
                        }
                        break;
                    case 'ArrowDown':
                        if (direction !== 'up') {
                            nextDirection = 'down';
                        }
                        break;
                    case 'ArrowLeft':
                        if (direction !== 'right') {
                            nextDirection = 'left';
                        }
                        break;
                    case 'ArrowRight':
                        if (direction !== 'left') {
                            nextDirection = 'right';
                        }
                        break;
                }
            });
            
            // 触摸控制
            let touchStartX = 0;
            let touchStartY = 0;
            
            canvas.addEventListener('touchstart', (e) => {
                touchStartX = e.touches[0].clientX;
                touchStartY = e.touches[0].clientY;
                e.preventDefault();
            }, { passive: false });
            
            canvas.addEventListener('touchmove', (e) => {
                e.preventDefault();
            }, { passive: false });
            
            canvas.addEventListener('touchend', (e) => {
                if (!touchStartX || !touchStartY) return;
                
                const touchEndX = e.changedTouches[0].clientX;
                const touchEndY = e.changedTouches[0].clientY;
                
                const diffX = touchEndX - touchStartX;
                const diffY = touchEndY - touchStartY;
                
                if (Math.abs(diffX) > Math.abs(diffY)) {
                    if (diffX > 0 && direction !== 'left') {
                        nextDirection = 'right';
                    } else if (diffX < 0 && direction !== 'right') {
                        nextDirection = 'left';
                    }
                } else {
                    if (diffY > 0 && direction !== 'up') {
                        nextDirection = 'down';
                    } else if (diffY < 0 && direction !== 'down') {
                        nextDirection = 'up';
                    }
                }
                
                touchStartX = 0;
                touchStartY = 0;
                e.preventDefault();
            }, { passive: false });
            
            // 重新开始按钮
            restartButton.addEventListener('click', initGame);
            
            // 初始化游戏
            initGame();
        });
    </script>
</body>
</html>
html 复制代码
<!DOCTYPE html>
<!-- 声明这是HTML5文档类型,告诉浏览器按照HTML5标准解析页面 -->
<html lang="en">
<!-- html标签:页面根元素,lang="en"表示页面主要语言是英文(可根据需求改为zh-CN) -->
<head>
    <!-- meta标签:设置页面字符编码为UTF-8,确保中文等特殊字符正常显示 -->
    <meta charset="UTF-8">
    <!-- meta标签:适配移动端屏幕,width=device-width表示宽度等于设备宽度,initial-scale=1.0表示初始缩放比例为1(无缩放) -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- title标签:设置浏览器标签页显示的标题 -->
    <title>进阶版贪吃蛇</title>
    <!-- 引入Tailwind CSS框架(CDN方式),用于快速构建响应式、美观的UI,无需手写大量原生CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- 引入Font Awesome图标库(CDN方式),用于获取游戏中的方向键、刷新等图标 -->
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <!-- 配置Tailwind CSS自定义主题,扩展默认样式以适配游戏需求 -->
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        // 自定义游戏专用颜色:primary(主色,蓝紫色)、secondary(辅助色,粉色)
                        primary: '#4F46E5',
                        secondary: '#EC4899',
                        // 深色系背景:dark(深灰蓝)、darker(近黑)、dark-accent(中灰蓝),适合游戏暗色调风格
                        dark: '#0F172A',
                        darker: '#020617',
                        'dark-accent': '#1E293B',
                        // 浅色文本:light(近白),确保在深色背景上清晰可见
                        light: '#F9FAFB'
                    },
                    fontFamily: {
                        // 自定义游戏字体:Poppins(无衬线字体,清晰易读), fallback为系统默认无衬线字体
                        game: ['Poppins', 'sans-serif'],
                    },
                }
            }
        }
    </script>
    <!-- 自定义Tailwind工具类和动画,用于游戏专属样式 -->
    <style type="text/tailwindcss">
        @layer utilities {
            /* 优化性能:内容可见时才渲染,减少不必要的计算 */
            .content-auto {
                content-visibility: auto;
            }
            /* 游戏背景网格:30px×30px网格线,半透明白色,加平移动画 */
            .grid-bg {
                background-size: 30px 30px; /* 网格大小 */
                background-image: 
                    /* 水平网格线:白色半透明(0.07透明度),1px粗 */
                    linear-gradient(to right, rgba(255,255,255,0.07) 1px, transparent 1px),
                    /* 垂直网格线:同上 */
                    linear-gradient(to bottom, rgba(255,255,255,0.07) 1px, transparent 1px);
                animation: gridShift 20s linear infinite; /* 网格20秒匀速平移,无限循环 */
            }
            /* 文字阴影:增强文字立体感,0偏移2px模糊8px黑色半透明 */
            .text-shadow {
                text-shadow: 0 2px 8px rgba(0,0,0,0.4);
            }
            /* 发光效果:给元素加蓝紫色(primary色)发光阴影,增强视觉层次 */
            .glow {
                filter: drop-shadow(0 0 8px rgba(79, 70, 229, 0.5));
            }
            /* 食物发光效果:给食物加橙色发光阴影,突出食物 */
            .food-glow {
                filter: drop-shadow(0 0 10px rgba(255, 165, 0, 0.7));
            }
            /* 游戏背景渐变:从深灰蓝(#0F172A)到中灰蓝(#1E293B)的135度渐变 */
            .bg-game-gradient {
                background: linear-gradient(135deg, #0F172A 0%, #1E293B 100%);
            }
            /* 渐变边框:45度渐变(主色到辅助色),边框宽度1px */
            .border-gradient {
                border-image: linear-gradient(45deg, #4F46E5, #EC4899) 1;
            }
            /* 彩虹色1:从红色(#FF5E5E)到橙红色(#FF7D5E)的水平渐变,用于蛇身颜色循环 */
            .rainbow-1 { background: linear-gradient(90deg, #FF5E5E, #FF7D5E); }
            /* 彩虹色2:从橙红色(#FF7D5E)到橙色(#FFB347)的水平渐变 */
            .rainbow-2 { background: linear-gradient(90deg, #FF7D5E, #FFB347); }
            /* 彩虹色3:从橙色(#FFB347)到黄色(#FFF275)的水平渐变 */
            .rainbow-3 { background: linear-gradient(90deg, #FFB347, #FFF275); }
            /* 彩虹色4:从黄色(#FFF275)到绿色(#93FF96)的水平渐变 */
            .rainbow-4 { background: linear-gradient(90deg, #FFF275, #93FF96); }
            /* 彩虹色5:从绿色(#93FF96)到蓝色(#42D7F5)的水平渐变 */
            .rainbow-5 { background: linear-gradient(90deg, #93FF96, #42D7F5); }
            /* 彩虹色6:从蓝色(#42D7F5)到蓝紫色(#7D5FFF)的水平渐变 */
            .rainbow-6 { background: linear-gradient(90deg, #42D7F5, #7D5FFF); }
            /* 彩虹色7:从蓝紫色(#7D5FFF)到紫色(#C774E8)的水平渐变 */
            .rainbow-7 { background: linear-gradient(90deg, #7D5FFF, #C774E8); }
            /* 彩虹色8:从紫色(#C774E8)到红色(#FF5E5E)的水平渐变,完成彩虹循环 */
            .rainbow-8 { background: linear-gradient(90deg, #C774E8, #FF5E5E); }
            
            /* 网格平移动画:从(0,0)位置移到(30px,30px),和网格大小一致,实现无缝平移 */
            @keyframes gridShift {
                0% { background-position: 0 0; } /* 动画开始:网格在初始位置 */
                100% { background-position: 30px 30px; } /* 动画结束:网格平移30px */
            }
            /* 脉冲动画:元素缩放效果,中间放大到1.05倍,增强视觉活力 */
            @keyframes pulse {
                0%, 100% { transform: scale(1); } /* 开始和结束:原大小 */
                50% { transform: scale(1.05); } /* 中间:放大5% */
            }
            /* 淡入动画:元素从透明(0)和缩小(0.9)到不透明(1)和原大小,用于页面过渡 */
            @keyframes fadeIn {
                from { opacity: 0; transform: scale(0.9); } /* 开始:透明+缩小 */
                to { opacity: 1; transform: scale(1); } /* 结束:不透明+原大小 */
            }
            /* 应用淡入动画:使用fadeIn动画,0.5秒缓出效果,保持最终状态 */
            .animate-fade-in {
                animation: fadeIn 0.5s ease-out forwards;
            }
            /* 应用慢速脉冲动画:使用pulse动画,3秒一次,无限循环 */
            .animate-pulse-slow {
                animation: pulse 3s infinite;
            }
        }
    </style>
</head>
<!-- body:页面内容区域,游戏的所有可视元素都在这里 -->
<body class="bg-game-gradient font-game text-light min-h-screen flex flex-col items-center justify-start p-4 overflow-hidden pt-4">
    <!-- 游戏主容器:最大宽度3xl(适配中等屏幕),宽度100%,flex垂直布局,高度=屏幕高度-2rem(减去上下内边距) -->
    <div class="max-w-3xl w-full flex flex-col h-[calc(100vh-2rem)]">
        <!-- 头部区域:包含标题和分数,底部外边距4px,flex横向布局(两端对齐),不压缩 -->
        <header class="mb-4 flex justify-between items-center flex-shrink-0">
            <!-- 游戏标题:字体大小自适应(1.2rem到2rem),加粗,渐变色文字(主色到辅助色),文字阴影,慢速脉冲动画 -->
            <h1 class="text-[clamp(1.2rem,4vw,2rem)] font-bold text-transparent bg-clip-text bg-gradient-to-r from-primary to-secondary text-shadow animate-pulse-slow">进阶版贪吃蛇</h1>
            <!-- 分数面板:半透明深灰背景(60%透明度),毛玻璃效果,内边距4px 2px,圆角,白色边框(15%透明度),阴影,发光效果,hover时阴影变色(主色20%透明度) -->
            <div id="score-display" class="bg-dark/60 backdrop-blur-md px-4 py-2 rounded-lg border border-white/15 shadow-lg glow transition-all duration-300 hover:shadow-primary/20">
                <!-- 分数标签:小号字体,中等粗细,白色70%透明度 -->
                <span class="text-sm font-medium text-white/70">分数:</span>
                <!-- 分数数值:大号字体,加粗,左边距2px,过渡效果(300ms) -->
                <span id="score" class="text-xl font-bold ml-2 transition-all duration-300">0</span>
            </div>
        </header>
        
        <!-- 游戏容器:相对定位(用于内部绝对定位元素),半透明中灰背景(80%透明度),毛玻璃效果,圆角,溢出隐藏,2px边框(透明,用渐变边框),flex占满剩余空间,底部外边距4px,过渡效果(500ms),hover时阴影变色(主色10%透明度) -->
        <div class="relative bg-dark-accent/80 backdrop-blur-sm rounded-xl overflow-hidden shadow-2xl border-2 border-transparent border-gradient flex-grow mb-4 transition-all duration-500 hover:shadow-primary/10">
            <!-- 游戏画布:宽度100%,高度100%,网格背景 -->
            <canvas id="gameCanvas" class="w-full h-full grid-bg"></canvas>
            
            <!-- 游戏结束遮罩:绝对定位(占满父容器),半透明深灰背景(70%透明度),毛玻璃效果,flex垂直居中,水平居中,默认隐藏,淡入动画 -->
            <div id="gameOver" class="absolute inset-0 bg-dark/70 backdrop-blur-md flex flex-col items-center justify-center hidden animate-fade-in">
                <!-- 游戏结束标题:字体大小自适应(2rem到3.5rem),加粗,底部外边距2px,渐变色文字(红色到黄色),文字阴影 -->
                <h2 class="text-[clamp(2rem,6vw,3.5rem)] font-bold mb-2 text-transparent bg-clip-text bg-gradient-to-r from-red-500 to-yellow-500 text-shadow">游戏结束!</h2>
                <!-- 最终分数文本:字体大小自适应(1rem到1.5rem),底部外边距6px -->
                <p class="text-[clamp(1rem,3vw,1.5rem)] mb-6">最终分数: <span id="finalScore" class="font-bold">0</span></p>
                <!-- 重新开始按钮:内边距8px 3px,渐变背景(主色到辅助色),圆角全圆,白色文字,加粗,阴影,hover时阴影变色(主色30%透明度)+放大5%,过渡效果(300ms),聚焦时无轮廓+2px边框(主色50%透明度) -->
                <button id="restartButton" class="px-8 py-3 bg-gradient-to-r from-primary to-secondary rounded-full text-white font-bold shadow-lg hover:shadow-primary/30 hover:scale-105 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-primary/50">
                    <!-- 刷新图标:左边距2px -->
                    <i class="fa fa-refresh mr-2"></i>重新开始
                </button>
            </div>
        </div>
        
        <!-- 操作说明区:顶部外边距2px,文本居中,白色60%透明度,小号字体,不压缩 -->
        <div class="mt-2 text-center text-white/60 text-sm flex-shrink-0">
            <!-- 控制说明:flex横向布局(换行),水平居中,垂直居中,元素间距1px -->
            <p class="flex flex-wrap justify-center items-center gap-x-1">使用方向键 
                <i class="fa fa-arrow-up mx-1"></i> <!-- 上箭头图标,左右边距1px -->
                <i class="fa fa-arrow-down mx-1"></i> <!-- 下箭头图标,左右边距1px -->
                <i class="fa fa-arrow-left mx-1"></i> <!-- 左箭头图标,左右边距1px -->
                <i class="fa fa-arrow-right mx-1"></i> <!-- 右箭头图标,左右边距1px -->
                控制蛇的移动
            </p>
            <!-- 游戏机制说明:顶部外边距1px -->
            <p class="mt-1">每吃一个食物,蛇的速度会增加,颜色也会变化</p>
        </div>
    </div>

    <!-- JavaScript:游戏核心逻辑,页面加载完成后执行 -->
    <script>
        // 监听页面DOM元素加载完成事件,确保DOM元素都存在后再执行游戏逻辑
        document.addEventListener('DOMContentLoaded', () => {
            // 获取画布元素(游戏绘图区域)
            const canvas = document.getElementById('gameCanvas');
            // 获取画布2D绘图上下文(用于在画布上绘制图形)
            const ctx = canvas.getContext('2d');
            // 获取分数显示元素(实时更新分数)
            const scoreDisplay = document.getElementById('score');
            // 获取游戏结束遮罩元素(游戏结束时显示)
            const gameOverScreen = document.getElementById('gameOver');
            // 获取最终分数显示元素(游戏结束时显示最终分数)
            const finalScoreDisplay = document.getElementById('finalScore');
            // 获取重新开始按钮元素(游戏结束后点击重启)
            const restartButton = document.getElementById('restartButton');
            // 获取游戏容器元素(用于适配画布尺寸)
            const gameContainer = canvas.parentElement;
            
            // 函数:设置画布尺寸(适配游戏容器大小)
            function resizeCanvas() {
                // 获取游戏容器的宽度(作为画布宽度)
                const containerWidth = gameContainer.clientWidth;
                // 获取游戏容器的高度(作为画布高度)
                const containerHeight = gameContainer.clientHeight;
                // 设置画布实际宽度
                canvas.width = containerWidth;
                // 设置画布实际高度
                canvas.height = containerHeight;
            }
            
            // 首次调用:初始化画布尺寸
            resizeCanvas();
            // 监听窗口"大小改变"事件,窗口缩放时重新调整画布尺寸
            window.addEventListener('resize', resizeCanvas);
            
            // 游戏核心变量定义
            let snake = []; // 蛇的身体数组:每个元素是一个对象,包含x(水平坐标)和y(垂直坐标)
            let food = {}; // 食物对象:包含x、y坐标,blink(闪烁状态),blinkDir(闪烁方向)
            let direction = 'right'; // 当前移动方向:初始向右
            let nextDirection = 'right'; // 下一个移动方向:防止快速转向(比如向右时直接向左)
            let score = 0; // 游戏分数:初始0
            let gameSpeed = 150; // 游戏速度(毫秒):初始150ms,值越小越快
            let gameInterval; // 游戏循环定时器:控制游戏帧频
            let isGameOver = false; // 游戏结束标记:初始未结束
            let gridSize; // 网格大小(每个格子的像素数):根据画布宽度计算
            let snakeSize; // 蛇身大小(像素):网格大小的80%(留边距)
            let foodSize; // 食物大小(像素):网格大小的90%(比蛇身略大)
            // 彩虹色数组:存储蛇身循环的颜色类名
            let rainbowColors = [
                'rainbow-1', 'rainbow-2', 'rainbow-3', 'rainbow-4',
                'rainbow-5', 'rainbow-6', 'rainbow-7', 'rainbow-8'
            ];
            let currentColorIndex = 0; // 当前蛇身颜色索引:控制彩虹循环
            let foodBlinkState = 1; // 食物闪烁状态:备用变量(实际用food.blink)
            let foodBlinkRate = 0.02; // 食物闪烁速率:每次帧更新的缩放变化量
            
            // 函数:初始化游戏(重置游戏状态,开始新游戏)
            function initGame() {
                // 计算网格大小:画布宽度 / 25(分成25列,保证格子数量合适),取整数
                gridSize = Math.floor(canvas.width / 25);
                // 蛇身大小 = 网格大小 * 0.8(比网格小,避免占满格子)
                snakeSize = gridSize * 0.8;
                // 食物大小 = 网格大小 * 0.9(比蛇身大,容易识别)
                foodSize = gridSize * 0.9;
                
                // 设置蛇的初始位置:3段身体,水平排列(x从3→4→5,y都是5)
                snake = [
                    {x: 5 * gridSize, y: 5 * gridSize}, // 蛇头:第5列第5行
                    {x: 4 * gridSize, y: 5 * gridSize}, // 蛇身1:第4列第5行
                    {x: 3 * gridSize, y: 5 * gridSize}  // 蛇身2:第3列第5行
                ];
                // 重置移动方向:初始向右
                direction = 'right';
                nextDirection = 'right';
                // 重置分数:0
                score = 0;
                // 重置游戏速度:150ms
                gameSpeed = 150;
                // 重置游戏结束标记:未结束
                isGameOver = false;
                // 重置蛇身颜色索引:从第一个彩虹色开始
                currentColorIndex = 0;
                
                // 更新页面分数显示:设置为0
                scoreDisplay.textContent = score;
                // 隐藏游戏结束遮罩:添加hidden类
                gameOverScreen.classList.add('hidden');
                
                // 生成第一个食物
                generateFood();
                
                // 清除之前的游戏循环(防止多个循环同时运行)
                if (gameInterval) clearInterval(gameInterval);
                // 启动新的游戏循环:每隔gameSpeed毫秒执行一次gameLoop
                gameInterval = setInterval(gameLoop, gameSpeed);
            }
            
            // 函数:生成食物(随机位置,不与蛇身重叠)
            function generateFood() {
                // 计算最大X坐标:画布宽度 / 网格大小 - 1(避免食物超出边界),取整数
                const maxX = Math.floor(canvas.width / gridSize) - 1;
                // 计算最大Y坐标:同上
                const maxY = Math.floor(canvas.height / gridSize) - 1;
                
                let overlapping; // 标记食物是否与蛇身重叠
                do {
                    overlapping = false; // 初始假设不重叠
                    // 生成食物对象:
                    food = {
                        x: Math.floor(Math.random() * maxX) * gridSize, // 随机X坐标(乘以网格大小,对齐格子)
                        y: Math.floor(Math.random() * maxY) * gridSize, // 随机Y坐标
                        blink: 1, // 闪烁初始状态(1=原大小)
                        blinkDir: -foodBlinkRate // 闪烁方向(-0.02=初始缩小)
                    };
                    
                    // 遍历蛇的每一段,检查食物是否与蛇身重叠
                    for (let segment of snake) {
                        // 如果食物坐标和某段蛇身坐标相同,说明重叠
                        if (segment.x === food.x && segment.y === food.y) {
                            overlapping = true; // 标记重叠
                            break; // 退出循环,重新生成
                        }
                    }
                } while (overlapping); // 只要重叠,就重新生成食物
            }
            
            // 函数:绘制蛇(带渐变、呼吸动画、眼睛)
            function drawSnake() {
                // 遍历蛇的每一段身体(index=0是蛇头,index>0是蛇身)
                snake.forEach((segment, index) => {
                    // 计算当前段的颜色索引:(当前颜色索引 + 每3段变一次色)% 彩虹色数量,实现层次感
                    const colorIndex = (currentColorIndex + Math.floor(index / 3)) % rainbowColors.length;
                    // 获取当前段的颜色类名(从彩虹色数组中取)
                    const colorClass = rainbowColors[colorIndex];
                    // 获取该颜色类的渐变起始和结束色(调用getGradientColors函数)
                    const colors = getGradientColors(colorClass);
                    
                    // 创建线性渐变:从蛇段左上角(x,y)到右下角(x+snakeSize,y+snakeSize)
                    const gradient = ctx.createLinearGradient(
                        segment.x, segment.y, 
                        segment.x + snakeSize, segment.y + snakeSize
                    );
                    // 设置渐变起点颜色(colors.start)
                    gradient.addColorStop(0, colors.start);
                    // 设置渐变终点颜色(colors.end)
                    gradient.addColorStop(1, colors.end);
                    
                    // 蛇身呼吸动画:根据时间和索引计算缩放比例(1 ± 2%,sin函数实现循环)
                    const scale = 1 + Math.sin(Date.now() / 300 + index * 0.5) * 0.02;
                    
                    // 保存当前绘图状态(防止后续操作影响其他绘制)
                    ctx.save();
                    // 平移到蛇段中心(为了缩放时居中):蛇段x+网格一半,y+网格一半
                    ctx.translate(segment.x + gridSize/2, segment.y + gridSize/2);
                    // 缩放蛇段:x和y方向都按scale比例缩放
                    ctx.scale(scale, scale);
                    // 平移回原位置(抵消之前的中心平移)
                    ctx.translate(-(segment.x + gridSize/2), -(segment.y + gridSize/2));
                    
                    // 设置蛇身填充色为渐变
                    ctx.fillStyle = gradient;
                    // 开始绘制路径(准备画蛇段)
                    ctx.beginPath();
                    // 绘制圆角矩形(蛇段):
                    // x坐标:蛇段x + (网格大小 - 蛇身大小)/2(居中)
                    // y坐标:同上
                    // 宽度:蛇身大小
                    // 高度:蛇身大小
                    // 圆角半径:蛇身大小/4(圆润效果)
                    ctx.roundRect(
                        segment.x + (gridSize - snakeSize) / 2,
                        segment.y + (gridSize - snakeSize) / 2,
                        snakeSize, snakeSize, snakeSize / 4
                    );
                    // 填充蛇段(用渐变颜色)
                    ctx.fill();
                    
                    // 设置高光填充色:白色半透明(10%透明度,增强立体感)
                    ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
                    // 开始绘制高光路径
                    ctx.beginPath();
                    // 绘制小的圆角矩形(高光):比蛇身小,位置居中
                    ctx.roundRect(
                        segment.x + (gridSize - snakeSize) / 2,
                        segment.y + (gridSize - snakeSize) / 2,
                        snakeSize * 0.6, snakeSize * 0.6, snakeSize / 8
                    );
                    // 填充高光
                    ctx.fill();
                    
                    // 恢复之前保存的绘图状态(结束当前蛇段的特殊效果)
                    ctx.restore();
                    
                    // 如果是蛇头(index=0),绘制眼睛
                    if (index === 0) {
                        drawEyes(segment);
                    }
                });
            }
            
            // 函数:绘制蛇头的眼睛(随移动方向变化,带微动效果)
            function drawEyes(segment) {
                // 设置眼睛填充色:白色
                ctx.fillStyle = '#fff';
                
                // 定义眼睛坐标变量:左右眼的x和y
                let leftEyeX, leftEyeY, rightEyeX, rightEyeY;
                // 眼睛大小:蛇身大小/8(比例合适)
                const eyeSize = snakeSize / 8;
                // 眼睛微动效果:根据时间变化的偏移量(sin函数循环,最大偏移眼睛大小的30%)
                const eyeOffset = Math.sin(Date.now() / 300) * eyeSize * 0.3;
                
                // 根据当前移动方向,设置眼睛位置(确保眼睛朝向移动方向)
                switch(direction) {
                    case 'right': // 向右移动:眼睛在蛇头右侧
                        leftEyeX = segment.x + snakeSize * 0.7 + eyeOffset;
                        leftEyeY = segment.y + snakeSize * 0.3;
                        rightEyeX = segment.x + snakeSize * 0.7 + eyeOffset;
                        rightEyeY = segment.y + snakeSize * 0.7;
                        break;
                    case 'left': // 向左移动:眼睛在蛇头左侧
                        leftEyeX = segment.x + snakeSize * 0.3 - eyeOffset;
                        leftEyeY = segment.y + snakeSize * 0.3;
                        rightEyeX = segment.x + snakeSize * 0.3 - eyeOffset;
                        rightEyeY = segment.y + snakeSize * 0.7;
                        break;
                    case 'up': // 向上移动:眼睛在蛇头上方
                        leftEyeX = segment.x + snakeSize * 0.3;
                        leftEyeY = segment.y + snakeSize * 0.3 - eyeOffset;
                        rightEyeX = segment.x + snakeSize * 0.7;
                        rightEyeY = segment.y + snakeSize * 0.3 - eyeOffset;
                        break;
                    case 'down': // 向下移动:眼睛在蛇头下方
                        leftEyeX = segment.x + snakeSize * 0.3;
                        leftEyeY = segment.y + snakeSize * 0.7 + eyeOffset;
                        rightEyeX = segment.x + snakeSize * 0.7;
                        rightEyeY = segment.y + snakeSize * 0.7 + eyeOffset;
                        break;
                }
                
                // 开始绘制左眼和右眼(白色部分)
                ctx.beginPath();
                // 左眼:圆弧(x,y,半径,起始角度0,结束角度2π(全圆))
                ctx.arc(leftEyeX, leftEyeY, eyeSize, 0, Math.PI * 2);
                // 右眼:同上
                ctx.arc(rightEyeX, rightEyeY, eyeSize, 0, Math.PI * 2);
                // 填充眼睛白色部分
                ctx.fill();
                
                // 瞳孔微动效果:根据时间变化的偏移量(x和y方向,最大偏移眼睛大小的20%)
                const pupilOffsetX = Math.sin(Date.now() / 500) * eyeSize * 0.2;
                const pupilOffsetY = Math.cos(Date.now() / 500) * eyeSize * 0.2;
                
                // 设置瞳孔填充色:黑色
                ctx.fillStyle = '#000';
                // 开始绘制瞳孔
                ctx.beginPath();
                // 左瞳孔:比眼睛小一半(半径eyeSize/2),带偏移
                ctx.arc(leftEyeX + pupilOffsetX, leftEyeY + pupilOffsetY, eyeSize / 2, 0, Math.PI * 2);
                // 右瞳孔:同上
                ctx.arc(rightEyeX + pupilOffsetX, rightEyeY + pupilOffsetY, eyeSize / 2, 0, Math.PI * 2);
                // 填充瞳孔
                ctx.fill();
            }
            
            // 函数:获取彩虹色类对应的渐变颜色(返回起始色和结束色)
            function getGradientColors(colorClass) {
                // 定义颜色映射对象:键是颜色类名,值是{start: 起始色, end: 结束色}
                const gradients = {
                    'rainbow-1': { start: '#FF5E5E', end: '#FF7D5E' },
                    'rainbow-2': { start: '#FF7D5E', end: '#FFB347' },
                    'rainbow-3': { start: '#FFB347', end: '#FFF275' },
                    'rainbow-4': { start: '#FFF275', end: '#93FF96' },
                    'rainbow-5': { start: '#93FF96', end: '#42D7F5' },
                    'rainbow-6': { start: '#42D7F5', end: '#7D5FFF' },
                    'rainbow-7': { start: '#7D5FFF', end: '#C774E8' },
                    'rainbow-8': { start: '#C774E8', end: '#FF5E5E' }
                };
                
                // 返回对应颜色类的渐变值,如果没有找到,默认用主色到辅助色的渐变
                return gradients[colorClass] || { start: '#4F46E5', end: '#EC4899' };
            }
            
            // 函数:绘制食物(带旋转、闪烁、渐变效果)
            function drawFood() {
                // 更新食物闪烁状态:blink += 闪烁方向(blinkDir)
                food.blink += food.blinkDir;
                // 闪烁边界判断:如果blink≤0.8(最小缩放)或≥1.2(最大缩放),反转闪烁方向
                if (food.blink <= 0.8 || food.blink >= 1.2) {
                    food.blinkDir *= -1;
                }
                
                // 保存当前绘图状态
                ctx.save();
                // 平移到食物中心(为了旋转和缩放居中):食物x+网格一半,y+网格一半
                ctx.translate(food.x + gridSize / 2, food.y + gridSize / 2);
                // 旋转食物:根据时间变化(每800毫秒转一圈,Date.now()返回毫秒数)
                ctx.rotate(Date.now() / 800);
                // 缩放食物:根据blink状态(实现闪烁效果)
                ctx.scale(food.blink, food.blink);
                
                // 开始绘制食物路径(星形)
                ctx.beginPath();
                // 起点:顶部(0, -食物大小/2)
                ctx.moveTo(0, -foodSize / 2);
                // 循环10次(绘制5个角的星形)
                for (let i = 1; i < 10; i++) {
                    // 计算每个点的角度:(i*2π/10) - π/2(从顶部开始,顺时针旋转)
                    const angle = (i * 2 * Math.PI / 10) - (Math.PI / 2);
                    // 计算每个点的半径:偶数i(凹点)用小半径,奇数i(凸点)用大半径
                    const radius = i % 2 === 0 ? foodSize / 2 : foodSize / 5;
                    // 绘制线段到当前点:x=cos(angle)*radius,y=sin(angle)*radius
                    ctx.lineTo(Math.cos(angle) * radius, Math.sin(angle) * radius);
                }
                // 闭合路径(连接起点和最后一个点)
                ctx.closePath();
                
                // 创建径向渐变(从中心到边缘,模拟发光效果)
                const gradient = ctx.createRadialGradient(
                    0, 0, 0, // 渐变中心(x,y,半径0)
                    0, 0, foodSize / 2 // 渐变边缘(x,y,半径=食物大小/2)
                );
                // 渐变起点颜色:亮黄色(#FFEE58)
                gradient.addColorStop(0, '#FFEE58');
                // 渐变中间颜色:橙色(#FFC107)
                gradient.addColorStop(0.5, '#FFC107');
                // 渐变终点颜色:深橙色(#FF8F00)
                gradient.addColorStop(1, '#FF8F00');
                
                // 设置食物填充色为径向渐变
                ctx.fillStyle = gradient;
                // 填充食物(星形)
                ctx.fill();
                
                // 设置食物边框色:白色半透明(90%透明度)
                ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
                // 设置边框宽度:2px
                ctx.lineWidth = 2;
                // 绘制食物边框(增强轮廓)
                ctx.stroke();
                
                // 设置食物中心高光色:白色半透明(60%透明度)
                ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
                // 开始绘制中心高光
                ctx.beginPath();
                // 中心高光:小圆(半径=食物大小/8)
                ctx.arc(0, 0, foodSize / 8, 0, Math.PI * 2);
                // 填充高光
                ctx.fill();
                
                // 恢复之前的绘图状态
                ctx.restore();
            }
            
            // 函数:绘制吃到食物的粒子效果(彩色小圆圈,增强反馈)
            function drawParticles(x, y) {
                // 循环8次:生成8个粒子(对应8种彩虹色)
                for (let i = 0; i < 8; i++) {
                    // 计算粒子角度:每45度一个(i*π/4),围成一圈
                    const angle = (i * Math.PI / 4);
                    // 粒子距离中心的距离:网格大小*0.6(合适的扩散范围)
                    const distance = gridSize * 0.6;
                    // 计算粒子x坐标:中心x + cos(angle)*distance
                    const particleX = x + Math.cos(angle) * distance;
                    // 计算粒子y坐标:中心y + sin(angle)*distance
                    const particleY = y + Math.sin(angle) * distance;
                    
                    // 获取粒子颜色类名:i%彩虹色数量(循环使用8种颜色)
                    const colorClass = rainbowColors[i % rainbowColors.length];
                    // 获取该颜色的起始色(粒子用纯色,不用渐变)
                    const colors = getGradientColors(colorClass);
                    
                    // 设置粒子填充色为颜色的起始色
                    ctx.fillStyle = colors.start;
                    // 开始绘制粒子
                    ctx.beginPath();
                    // 粒子:小圆(半径=网格大小/8)
                    ctx.arc(particleX, particleY, gridSize / 8, 0, Math.PI * 2);
                    // 填充粒子
                    ctx.fill();
                }
            }
            
            // 函数:检测碰撞(边界碰撞或自碰撞)
            function checkCollision() {
                // 复制蛇头坐标(避免修改原蛇头数据)
                const head = {...snake[0]};
                
                // 检测边界碰撞:蛇头超出画布范围(左<0,右≥画布宽度,上<0,下≥画布高度)
                if (
                    head.x < 0 || 
                    head.x >= canvas.width || 
                    head.y < 0 || 
                    head.y >= canvas.height
                ) {
                    return true; // 碰撞边界,返回true
                }
                
                // 检测自碰撞:蛇头与身体其他段重叠
                for (let i = 1; i < snake.length; i++) {
                    // 如果蛇头坐标和第i段蛇身坐标相同,说明自撞
                    if (head.x === snake[i].x && head.y === snake[i].y) {
                        return true; // 自碰撞,返回true
                    }
                }
                
                return false; // 无碰撞,返回false
            }
            
            // 函数:分数增加动画(分数变化时放大+变色,增强反馈)
            function animateScoreIncrease() {
                // 获取分数数值元素
                const scoreElement = document.getElementById('score');
                // 添加动画类:放大到125%,文字变辅助色
                scoreElement.classList.add('scale-125', 'text-secondary');
                // 300毫秒后移除动画类(恢复原状态)
                setTimeout(() => {
                    scoreElement.classList.remove('scale-125', 'text-secondary');
                }, 300);
            }
            
            // 函数:游戏主循环(每帧执行,控制蛇移动、碰撞检测、绘制)
            function gameLoop() {
                // 清除画布:从(0,0)到(画布宽度,画布高度),清除所有绘制内容
                ctx.clearRect(0, 0, canvas.width, canvas.height);
                
                // 更新当前方向为下一个方向(防止快速反向)
                direction = nextDirection;
                
                // 复制蛇头坐标(用于计算新蛇头位置)
                const head = {x: snake[0].x, y: snake[0].y};
                
                // 根据当前方向,计算新蛇头位置(移动一个网格大小)
                switch(direction) {
                    case 'up': // 向上:y坐标减少网格大小
                        head.y -= gridSize;
                        break;
                    case 'down': // 向下:y坐标增加网格大小
                        head.y += gridSize;
                        break;
                    case 'left': // 向左:x坐标减少网格大小
                        head.x -= gridSize;
                        break;
                    case 'right': // 向右:x坐标增加网格大小
                        head.x += gridSize;
                        break;
                }
                
                // 在蛇头前面添加新蛇头(实现移动效果)
                snake.unshift(head);
                // 标记是否吃到食物:初始false
                let ateFood = false;
                
                // 检查是否吃到食物:新蛇头坐标和食物坐标相同
                if (head.x === food.x && head.y === food.y) {
                    // 分数增加10分
                    score += 10;
                    // 更新页面分数显示
                    scoreDisplay.textContent = score;
                    // 播放分数增加动画
                    animateScoreIncrease();
                    
                    // 提高游戏速度:当前速度-5ms,最低不低于50ms(避免太快无法操作)
                    gameSpeed = Math.max(50, gameSpeed - 5);
                    // 切换蛇身颜色:颜色索引+1(彩虹循环)
                    currentColorIndex++;
                    // 生成新的食物
                    generateFood();
                    // 清除旧的游戏循环(速度变了,需要重新设置)
                    clearInterval(gameInterval);
                    // 启动新的游戏循环(用新速度)
                    gameInterval = setInterval(gameLoop, gameSpeed);
                    // 标记吃到食物
                    ateFood = true;
                } else {
                    // 没吃到食物:移除蛇尾(保持蛇身长度不变)
                    snake.pop();
                }
                
                // 检测碰撞:如果碰撞(边界或自撞)
                if (checkCollision()) {
                    // 执行游戏结束逻辑
                    gameOver();
                    // 退出当前循环(不再继续绘制)
                    return;
                }
                
                // 绘制蛇
                drawSnake();
                // 绘制食物
                drawFood();
                
                // 如果吃到食物,绘制粒子效果(粒子中心在蛇头中心)
                if (ateFood) {
                    drawParticles(head.x + gridSize/2, head.y + gridSize/2);
                }
            }
            
            // 函数:游戏结束处理
            function gameOver() {
                // 标记游戏结束
                isGameOver = true;
                // 清除游戏循环(停止蛇移动)
                clearInterval(gameInterval);
                // 更新最终分数显示
                finalScoreDisplay.textContent = score;
                // 显示游戏结束遮罩(移除hidden类)
                gameOverScreen.classList.remove('hidden');
            }
            
            // 键盘控制:监听键盘按下事件
            document.addEventListener('keydown', (e) => {
                // 如果游戏结束且按下空格键,重新开始游戏
                if (isGameOver && e.key === ' ') {
                    initGame();
                    return; // 退出事件处理,避免后续方向键逻辑
                }
                
                // 阻止方向键和空格键的默认行为(比如页面滚动)
                if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
                    e.preventDefault();
                }
                
                // 根据按下的方向键,设置下一个移动方向(防止反向)
                switch(e.key) {
                    case 'ArrowUp': // 上箭头:当前方向不是向下时,才能向上
                        if (direction !== 'down') {
                            nextDirection = 'up';
                        }
                        break;
                    case 'ArrowDown': // 下箭头:当前方向不是向上时,才能向下
                        if (direction !== 'up') {
                            nextDirection = 'down';
                        }
                        break;
                    case 'ArrowLeft': // 左箭头:当前方向不是向右时,才能向左
                        if (direction !== 'right') {
                            nextDirection = 'left';
                        }
                        break;
                    case 'ArrowRight': // 右箭头:当前方向不是向左时,才能向右
                        if (direction !== 'left') {
                            nextDirection = 'right';
                        }
                        break;
                }
            });
            
            // 触摸控制:适配移动端,监听触摸事件
            let touchStartX = 0; // 触摸开始时的x坐标
            let touchStartY = 0; // 触摸开始时的y坐标
            
            // 触摸开始事件:记录初始触摸位置
            canvas.addEventListener('touchstart', (e) => {
                // 获取第一个触摸点的x坐标
                touchStartX = e.touches[0].clientX;
                // 获取第一个触摸点的y坐标
                touchStartY = e.touches[0].clientY;
                // 阻止默认行为(比如页面滚动)
                e.preventDefault();
            }, { passive: false }); // passive: false允许preventDefault
            
            // 触摸移动事件:阻止默认行为(避免滚动)
            canvas.addEventListener('touchmove', (e) => {
                e.preventDefault();
            }, { passive: false });
            
            // 触摸结束事件:计算滑动方向,控制蛇移动
            canvas.addEventListener('touchend', (e) => {
                // 如果没有记录初始触摸位置,退出(防止异常)
                if (!touchStartX || !touchStartY) return;
                
                // 获取触摸结束时的x坐标
                const touchEndX = e.changedTouches[0].clientX;
                // 获取触摸结束时的y坐标
                const touchEndY = e.changedTouches[0].clientY;
                
                // 计算x方向滑动距离(结束-开始)
                const diffX = touchEndX - touchStartX;
                // 计算y方向滑动距离(结束-开始)
                const diffY = touchEndY - touchStartY;
                
                // 判断滑动方向:水平滑动距离大于垂直滑动(左右滑动)
                if (Math.abs(diffX) > Math.abs(diffY)) {
                    // 向右滑动(diffX>0)且当前方向不是向左,设置下方向为右
                    if (diffX > 0 && direction !== 'left') {
                        nextDirection = 'right';
                    } 
                    // 向左滑动(diffX<0)且当前方向不是向右,设置下方向为左
                    else if (diffX < 0 && direction !== 'right') {
                        nextDirection = 'left';
                    }
                } else {
                    // 垂直滑动距离大于水平滑动(上下滑动)
                    // 向下滑动(diffY>0)且当前方向不是向上,设置下方向为下
                    if (diffY > 0 && direction !== 'up') {
                        nextDirection = 'down';
                    } 
                    // 向上滑动(diffY<0)且当前方向不是向下,设置下方向为上
                    else if (diffY < 0 && direction !== 'down') {
                        nextDirection = 'up';
                    }
                }
                
                // 重置初始触摸位置(准备下一次触摸)
                touchStartX = 0;
                touchStartY = 0;
                // 阻止默认行为
                e.preventDefault();
            }, { passive: false });
            
            // 重新开始按钮:点击事件,调用initGame重启游戏
            restartButton.addEventListener('click', initGame);
            
            // 初始化游戏:页面加载完成后,启动第一次游戏
            initGame();
        });
    </script>
</body>
</html>
相关推荐
先知后行。2 小时前
线程的创建.销毁
开发语言·c++·算法
时序数据说2 小时前
时序数据库:定义与基本特点
大数据·数据库·物联网·时序数据库
鱼嘻2 小时前
西嘎嘎学习 - C++ 继承 - Day 10
开发语言·c++·学习·算法
源远流长jerry2 小时前
STM32之wifi模块与MQTT模块详解
stm32·单片机·嵌入式硬件
孤廖2 小时前
从 “模板” 到 “场景”,用 C++ 磨透拓扑排序的实战逻辑
开发语言·c++·程序人生·算法·贪心算法·动态规划·学习方法
Hello_Embed2 小时前
STM32HAL 快速入门(二十四):I2C 编程(一)—— 从 OLED 显示初识 I2C 协议
c语言·stm32·单片机·嵌入式硬件·学习
蒋星熠3 小时前
中间件架构设计与实践:构建高性能分布式系统的核心基石
开发语言·数据库·分布式·python·中间件·性能优化·硬件工程
Flash.kkl3 小时前
优先算法——专题十一:字符串
算法
Agatha方艺璇3 小时前
CentOS7 Hive2.3.8 安装图文教程
大数据·数据库