霓虹贪吃蛇
by SonicGlider99526 lines14.5 KB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>霓虹贪吃蛇</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: radial-gradient(circle at 50% 30%, #111133 0%, #05050f 75%);
font-family: 'Segoe UI', 'Microsoft YaHei', system-ui, sans-serif;
color: #cfefff;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
overflow-x: hidden;
}
.wrap {
text-align: center;
padding: 20px 0 28px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
h1 {
font-size: 34px;
letter-spacing: 8px;
color: #00f5ff;
text-shadow: 0 0 10px #00f5ff, 0 0 26px rgba(0,245,255,.7), 0 0 60px rgba(0,245,255,.35);
margin-bottom: 14px;
font-weight: 700;
}
.hud {
display: flex;
justify-content: space-between;
align-items: center;
width: min(600px, calc(100vw - 24px));
margin: 0 auto 12px;
font-size: 17px;
font-weight: 600;
letter-spacing: 1px;
gap: 8px;
}
.hud .score { color: #00f5ff; text-shadow: 0 0 10px rgba(0,245,255,.9); }
.hud .high { color: #ffd75e; text-shadow: 0 0 10px rgba(255,215,94,.9); }
.hud .speed { color: #b18cff; text-shadow: 0 0 10px rgba(177,140,255,.8); }
.game-container {
position: relative;
margin: 0 auto;
}
canvas {
display: block;
background: #0a0a1a;
border: 2px solid rgba(0,245,255,.4);
border-radius: 12px;
box-shadow: 0 0 20px rgba(0,245,255,.3), 0 0 60px rgba(0,245,255,.12), inset 0 0 40px rgba(0,0,0,.5);
touch-action: none;
}
.overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(4, 4, 18, .72);
border-radius: 12px;
backdrop-filter: blur(3px);
opacity: 1;
transition: opacity .25s ease;
}
.overlay.hidden { opacity: 0; pointer-events: none; }
.panel {
background: rgba(10, 10, 32, .96);
border: 1px solid rgba(255, 0, 128, .55);
border-radius: 14px;
padding: 30px 44px;
box-shadow: 0 0 30px rgba(255,0,128,.35), 0 0 70px rgba(255,0,128,.15);
text-align: center;
max-width: 86vw;
}
.panel h2 {
font-size: 28px;
letter-spacing: 6px;
color: #ff0080;
text-shadow: 0 0 12px #ff0080, 0 0 30px rgba(255,0,128,.85);
margin-bottom: 16px;
}
.panel p {
font-size: 17px;
margin: 10px 0;
color: #cfefff;
letter-spacing: 1px;
}
.panel p span {
color: #00f5ff;
font-weight: 700;
text-shadow: 0 0 10px rgba(0,245,255,.85);
}
#restartBtn {
margin-top: 20px;
padding: 13px 38px;
font-size: 17px;
letter-spacing: 4px;
color: #06121a;
background: linear-gradient(135deg, #00f5ff, #00c8ff);
border: none;
border-radius: 9px;
cursor: pointer;
font-weight: 700;
font-family: inherit;
box-shadow: 0 0 16px rgba(0,245,255,.65);
transition: transform .15s ease, box-shadow .15s ease;
touch-action: manipulation;
}
#restartBtn:hover { transform: translateY(-2px); box-shadow: 0 0 28px rgba(0,245,255,.95); }
#restartBtn:active { transform: translateY(0); }
.dpad {
display: none;
flex-direction: column;
align-items: center;
gap: 8px;
margin: 16px auto 0;
}
.dpad-row { display: flex; gap: 8px; }
.dpad-btn {
width: 62px;
height: 62px;
border-radius: 14px;
background: rgba(0, 245, 255, .08);
border: 1.5px solid rgba(0, 245, 255, .45);
color: #00f5ff;
font-size: 22px;
font-family: inherit;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 12px rgba(0,245,255,.25), inset 0 0 10px rgba(0,245,255,.1);
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
.dpad-btn:active {
background: rgba(0, 245, 255, .3);
box-shadow: 0 0 22px rgba(0,245,255,.6);
}
.tip {
margin-top: 14px;
color: #7f9bbf;
font-size: 14px;
letter-spacing: 2px;
}
.tip-mobile { display: none; }
@media (hover: none), (max-width: 700px) {
.dpad { display: flex; }
.tip-desktop { display: none; }
.tip-mobile { display: inline; }
h1 { font-size: 24px; letter-spacing: 4px; margin-bottom: 10px; }
.hud { font-size: 14px; }
.dpad-btn { width: 60px; height: 60px; font-size: 20px; }
.tip { font-size: 12px; }
}
</style>
</head>
<body>
<div class="wrap">
<h1>霓虹贪吃蛇</h1>
<div class="hud">
<div class="score">分数 <span id="score">0</span></div>
<div class="speed">速度 <span id="speedVal">150ms</span></div>
<div class="high">最高分 <span id="highScore">0</span></div>
</div>
<div class="game-container" id="gameContainer">
<canvas id="game" width="600" height="600"></canvas>
<div id="overlay" class="overlay hidden">
<div class="panel">
<h2>游戏结束</h2>
<p>本次得分:<span id="finalScore">0</span></p>
<p>最高分:<span id="finalHigh">0</span></p>
<button id="restartBtn">重新开始</button>
</div>
</div>
</div>
<div class="dpad" id="dpad">
<div class="dpad-row">
<button class="dpad-btn" data-dir="up">▲</button>
</div>
<div class="dpad-row">
<button class="dpad-btn" data-dir="left">◀</button>
<button class="dpad-btn" data-dir="down">▼</button>
<button class="dpad-btn" data-dir="right">▶</button>
</div>
</div>
<p class="tip"><span class="tip-desktop">使用方向键 ↑ ↓ ← → 控制蛇的移动</span><span class="tip-mobile">滑动屏幕或点击下方按钮控制蛇的移动</span></p>
</div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const container = document.getElementById('gameContainer');
const GRID = 20; // 20 x 20 网格
const START_SPEED = 150;
const MIN_SPEED = 80;
const SPEED_STEP = 10;
const ACCEL_EVERY = 5;
const HIGH_KEY = 'neonSnakeHighScore';
const DIRS = {
up: { x: 0, y: -1 },
down: { x: 0, y: 1 },
left: { x: -1, y: 0 },
right: { x: 1, y: 0 },
};
let SIZE = 600;
let CELL = 30;
let snake = [];
let dir = { x: 1, y: 0 };
let nextDir = null;
let food = null;
let score = 0;
let speed = START_SPEED;
let gameState = 'idle'; // idle | playing | over
let timer = null;
let highScore = parseInt(localStorage.getItem(HIGH_KEY) || '0', 10) || 0;
const $ = (id) => document.getElementById(id);
// ---------- 响应式画布(含手机高分屏适配) ----------
function resizeCanvas() {
const avail = Math.floor(Math.min(window.innerWidth - 24, 600));
const dpr = Math.min(window.devicePixelRatio || 1, 3);
SIZE = avail;
CELL = SIZE / GRID;
canvas.style.width = SIZE + 'px';
canvas.style.height = SIZE + 'px';
canvas.width = Math.round(SIZE * dpr);
canvas.height = Math.round(SIZE * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
container.style.width = SIZE + 'px';
container.style.height = SIZE + 'px';
ctx.fillStyle = '#0a0a1a';
ctx.fillRect(0, 0, SIZE, SIZE);
}
// ---------- 游戏逻辑 ----------
function getSpeed() {
return Math.max(MIN_SPEED, START_SPEED - Math.floor(score / ACCEL_EVERY) * SPEED_STEP);
}
function resetGame() {
clearTimeout(timer);
const cx = Math.floor(GRID / 2);
const cy = Math.floor(GRID / 2);
snake = [
{ x: cx, y: cy },
{ x: cx - 1, y: cy },
{ x: cx - 2, y: cy },
];
dir = { x: 1, y: 0 };
nextDir = null;
score = 0;
speed = getSpeed();
placeFood();
gameState = 'idle';
updateHUD();
ctx.clearRect(0, 0, SIZE, SIZE);
}
function placeFood() {
const free = [];
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
if (!snake.some(s => s.x === x && s.y === y)) free.push({ x, y });
}
}
food = free[Math.floor(Math.random() * free.length)] || { x: 0, y: 0 };
}
function updateHUD() {
$('score').textContent = score;
$('highScore').textContent = highScore;
$('speedVal').textContent = speed + 'ms';
}
function scheduleTick() {
timer = setTimeout(tick, speed);
}
function tick() {
if (gameState !== 'playing') return;
if (nextDir) {
// 禁止 180° 掉头
if (!(nextDir.x === -dir.x && nextDir.y === -dir.y)) {
dir = nextDir;
}
nextDir = null;
}
const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };
// 撞墙
if (head.x < 0 || head.x >= GRID || head.y < 0 || head.y >= GRID) return gameOver();
// 撞自身(蛇尾即将移走,因此不计入碰撞)
if (snake.slice(0, -1).some(s => s.x === head.x && s.y === head.y)) return gameOver();
snake.unshift(head);
if (head.x === food.x && head.y === food.y) {
score++;
speed = getSpeed();
placeFood();
updateHUD();
} else {
snake.pop();
}
scheduleTick();
}
function gameOver() {
gameState = 'over';
clearTimeout(timer);
if (score > highScore) {
highScore = score;
try { localStorage.setItem(HIGH_KEY, String(highScore)); } catch (e) {}
}
$('finalScore').textContent = score;
$('finalHigh').textContent = highScore;
$('overlay').classList.remove('hidden');
updateHUD();
}
$('restartBtn').addEventListener('click', () => {
$('overlay').classList.add('hidden');
resetGame();
gameState = 'playing';
updateHUD();
scheduleTick();
});
// ---------- 方向控制(键盘 / 按钮 / 滑动统一入口) ----------
function setDirection(d) {
if (!d) return;
if (gameState === 'idle') {
dir = d;
gameState = 'playing';
updateHUD();
scheduleTick();
} else if (gameState === 'playing') {
nextDir = d;
}
}
function keyToDir(k) {
switch (k) {
case 'ArrowUp': return DIRS.up;
case 'ArrowDown': return DIRS.down;
case 'ArrowLeft': return DIRS.left;
case 'ArrowRight': return DIRS.right;
default: return null;
}
}
document.addEventListener('keydown', (e) => {
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) e.preventDefault();
setDirection(keyToDir(e.key));
});
// 屏幕方向按钮
document.querySelectorAll('.dpad-btn').forEach(btn => {
btn.addEventListener('touchstart', (e) => {
e.preventDefault();
setDirection(DIRS[btn.dataset.dir]);
}, { passive: false });
btn.addEventListener('mousedown', (e) => {
e.preventDefault();
setDirection(DIRS[btn.dataset.dir]);
});
});
// 在画布上滑动控制方向
let touchStart = null;
canvas.addEventListener('touchstart', (e) => {
const t = e.touches[0];
touchStart = { x: t.clientX, y: t.clientY };
}, { passive: true });
canvas.addEventListener('touchend', (e) => {
if (!touchStart) return;
const t = e.changedTouches[0];
const dx = t.clientX - touchStart.x;
const dy = t.clientY - touchStart.y;
touchStart = null;
const ax = Math.abs(dx), ay = Math.abs(dy);
if (Math.max(ax, ay) < 24) return; // 忽略误触
if (ax > ay) setDirection(dx > 0 ? DIRS.right : DIRS.left);
else setDirection(dy > 0 ? DIRS.down : DIRS.up);
}, { passive: true });
window.addEventListener('resize', resizeCanvas);
// ---------- 渲染 ----------
function lerpColor(a, b, t) {
return [
Math.round(a[0] + (b[0] - a[0]) * t),
Math.round(a[1] + (b[1] - a[1]) * t),
Math.round(a[2] + (b[2] - a[2]) * t),
];
}
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
function drawGrid() {
ctx.strokeStyle = 'rgba(0, 245, 255, 0.05)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i <= GRID; i++) {
const p = Math.floor(i * CELL) + 0.5;
ctx.moveTo(p, 0);
ctx.lineTo(p, SIZE);
ctx.moveTo(0, p);
ctx.lineTo(SIZE, p);
}
ctx.stroke();
}
function drawFood(ts) {
const pulse = (Math.sin(ts / 180) + 1) / 2;
const cx = food.x * CELL + CELL / 2;
const cy = food.y * CELL + CELL / 2;
const r = CELL * 0.32 * (0.72 + 0.28 * pulse);
ctx.save();
ctx.shadowColor = '#ff0080';
ctx.shadowBlur = 16 + 14 * pulse;
ctx.fillStyle = 'rgba(255, 0, 128, ' + (0.7 + 0.3 * pulse).toFixed(2) + ')';
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = 'rgba(255, 255, 255, ' + (0.35 + 0.4 * pulse).toFixed(2) + ')';
ctx.beginPath();
ctx.arc(cx, cy, r * 0.45, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
function drawSnake(ts) {
const n = snake.length;
for (let i = n - 1; i >= 0; i--) {
const seg = snake[i];
const t = n > 1 ? i / (n - 1) : 0;
// 头部亮青色 -> 尾部深蓝的渐变
const c = lerpColor([0, 245, 255], [0, 50, 140], t);
ctx.save();
ctx.shadowColor = 'rgba(0, 245, 255, ' + (0.85 - 0.5 * t).toFixed(2) + ')';
ctx.shadowBlur = 14 - 9 * t;
ctx.fillStyle = 'rgb(' + c[0] + ',' + c[1] + ',' + c[2] + ')';
roundRect(seg.x * CELL + 2.5, seg.y * CELL + 2.5, CELL - 5, CELL - 5, 7);
ctx.fill();
ctx.restore();
}
drawEyes();
}
function drawEyes() {
const head = snake[0];
if (!head) return;
const cx = head.x * CELL + CELL / 2;
const cy = head.y * CELL + CELL / 2;
const px = -dir.y, py = dir.x;
const fwd = CELL * 0.17, side = CELL * 0.17;
ctx.save();
ctx.fillStyle = '#ffffff';
ctx.shadowColor = '#00f5ff';
ctx.shadowBlur = 6;
for (const s of [-1, 1]) {
ctx.beginPath();
ctx.arc(cx + dir.x * fwd + px * side * s, cy + dir.y * fwd + py * side * s, CELL * 0.09, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
function drawHint(ts) {
const a = 0.55 + 0.45 * Math.sin(ts / 300);
ctx.save();
ctx.font = 'bold ' + Math.max(18, CELL * 0.9) + 'px "Segoe UI", "Microsoft YaHei", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.shadowColor = '#00f5ff';
ctx.shadowBlur = 18;
ctx.fillStyle = 'rgba(0, 245, 255, ' + a.toFixed(2) + ')';
ctx.fillText('滑动或按方向键开始', SIZE / 2, SIZE / 2);
ctx.restore();
}
function render(ts) {
// 半透明覆盖实现拖尾残影渐变效果
ctx.fillStyle = 'rgba(10, 10, 26, 0.18)';
ctx.fillRect(0, 0, SIZE, SIZE);
drawGrid();
if (food) drawFood(ts);
if (snake.length) drawSnake(ts);
if (gameState === 'idle') drawHint(ts);
requestAnimationFrame(render);
}
resizeCanvas();
resetGame();
requestAnimationFrame(render);
</script>
</body>
</html>
Game Source: 霓虹贪吃蛇
Creator: SonicGlider99
Libraries: none
Complexity: complex (526 lines, 14.5 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: game-sonicglider99" to link back to the original. Then publish at arcadelab.ai/publish.