请创建一个视觉精美、交互流畅的进阶版贪吃蛇游戏,需包含以下功能和设计要求: 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 自定义主题,定义了游戏专用颜色(如
primary
、secondary
、深色系背景)和字体(font-game
)
2. 自定义样式与动画(<style>
部分)
通过@layer utilities
定义了游戏所需的特殊样式和动画,核心包括:
- 背景效果 :
grid-bg
实现带缓慢平移动画的网格背景(30px×30px) - 视觉增强 :
text-shadow
(文字阴影)、glow
(发光效果)、border-gradient
(渐变边框) - 彩虹色定义 :
rainbow-1
到rainbow-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>
