283 lines
10 KiB
HTML
283 lines
10 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>贪吃蛇动画</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: linear-gradient(135deg, #0a0a0a, #1a1a1a, #2d2d2d);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#snake-canvas {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="snake-canvas"></canvas>
|
||
|
||
<script>
|
||
const canvas = document.getElementById('snake-canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
|
||
// 设置canvas尺寸为窗口大小
|
||
function resizeCanvas() {
|
||
canvas.width = window.innerWidth;
|
||
canvas.height = window.innerHeight;
|
||
}
|
||
|
||
resizeCanvas();
|
||
window.addEventListener('resize', resizeCanvas);
|
||
|
||
class Snake {
|
||
constructor() {
|
||
this.radius = 14; // 蛇的半径(苹果也是14)
|
||
this.path = [
|
||
{ x: canvas.width / 2, y: canvas.height / 2 }
|
||
];
|
||
this.targetLength = 28; // 初始长度为一个苹果直径
|
||
this.currentLength = 28;
|
||
this.speed = 2;
|
||
this.angle = 0;
|
||
this.targetAngle = 0;
|
||
this.hue = 30; // 橙黄色
|
||
this.currentTarget = null; // 当前追逐的苹果
|
||
this.targetStartTime = Date.now(); // 开始追逐当前目标的时间
|
||
this.targetTimeout = 10000; // 10秒超时
|
||
this.timedOut = false; // 是否刚刚超时
|
||
}
|
||
|
||
findNearestApple() {
|
||
const now = Date.now();
|
||
|
||
// 如果当前目标存在且未超时,继续追逐
|
||
if (this.currentTarget && (now - this.targetStartTime) < this.targetTimeout) {
|
||
// 检查当前目标是否还在苹果列表中
|
||
const targetStillExists = apples.some(apple =>
|
||
apple.x === this.currentTarget.x && apple.y === this.currentTarget.y
|
||
);
|
||
|
||
if (targetStillExists) {
|
||
const dx = this.currentTarget.x - this.path[0].x;
|
||
const dy = this.currentTarget.y - this.path[0].y;
|
||
this.targetAngle = Math.atan2(dy, dx);
|
||
this.timedOut = false;
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 超时或目标不存在,寻找新目标
|
||
let targetApple = null;
|
||
let targetDistance = this.timedOut ? -Infinity : Infinity; // 超时后找最远的
|
||
|
||
apples.forEach(apple => {
|
||
const dx = apple.x - this.path[0].x;
|
||
const dy = apple.y - this.path[0].y;
|
||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||
|
||
if (this.timedOut) {
|
||
// 超时了,找最远的苹果
|
||
if (distance > targetDistance) {
|
||
targetDistance = distance;
|
||
targetApple = apple;
|
||
}
|
||
} else {
|
||
// 正常情况,找最近的苹果
|
||
if (distance < targetDistance) {
|
||
targetDistance = distance;
|
||
targetApple = apple;
|
||
}
|
||
}
|
||
});
|
||
|
||
if (targetApple) {
|
||
// 如果切换了目标,重置计时器
|
||
if (!this.currentTarget ||
|
||
this.currentTarget.x !== targetApple.x ||
|
||
this.currentTarget.y !== targetApple.y) {
|
||
this.currentTarget = targetApple;
|
||
this.targetStartTime = now;
|
||
this.timedOut = false; // 重置超时标志
|
||
} else if ((now - this.targetStartTime) >= this.targetTimeout) {
|
||
// 刚好达到超时
|
||
this.timedOut = true;
|
||
}
|
||
|
||
const dx = targetApple.x - this.path[0].x;
|
||
const dy = targetApple.y - this.path[0].y;
|
||
this.targetAngle = Math.atan2(dy, dx);
|
||
}
|
||
}
|
||
|
||
update() {
|
||
// 平滑转向 - 降低转向速度,避免锐角转弯
|
||
let angleDiff = this.targetAngle - this.angle;
|
||
while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
|
||
while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
|
||
|
||
// 限制每帧最大转向角度
|
||
const maxTurnRate = 0.03; // 降低转向速度
|
||
this.angle += angleDiff * maxTurnRate;
|
||
|
||
// 移动头部
|
||
const head = { ...this.path[0] };
|
||
head.x += Math.cos(this.angle) * this.speed;
|
||
head.y += Math.sin(this.angle) * this.speed;
|
||
|
||
// 屏幕边界穿越
|
||
if (head.x < 0) head.x = canvas.width;
|
||
if (head.x > canvas.width) head.x = 0;
|
||
if (head.y < 0) head.y = canvas.height;
|
||
if (head.y > canvas.height) head.y = 0;
|
||
|
||
// 添加新头部
|
||
this.path.unshift(head);
|
||
|
||
// 检查是否吃到苹果
|
||
apples.forEach((apple, index) => {
|
||
const dx = head.x - apple.x;
|
||
const dy = head.y - apple.y;
|
||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||
|
||
if (distance < this.radius + 14) { // 14是苹果半径
|
||
apples[index] = createApple();
|
||
this.targetLength += 28; // 增加一个苹果直径的长度
|
||
// 吃到苹果后清除当前目标,重新寻找
|
||
this.currentTarget = null;
|
||
}
|
||
});
|
||
|
||
// 计算当前路径实际长度
|
||
let pathLength = 0;
|
||
for (let i = 0; i < this.path.length - 1; i++) {
|
||
const dx = this.path[i].x - this.path[i + 1].x;
|
||
const dy = this.path[i].y - this.path[i + 1].y;
|
||
pathLength += Math.sqrt(dx * dx + dy * dy);
|
||
}
|
||
|
||
// 从尾部裁剪多余的路径
|
||
while (this.path.length > 2 && pathLength > this.targetLength) {
|
||
const last = this.path[this.path.length - 1];
|
||
const secondLast = this.path[this.path.length - 2];
|
||
const dx = secondLast.x - last.x;
|
||
const dy = secondLast.y - last.y;
|
||
const segmentLength = Math.sqrt(dx * dx + dy * dy);
|
||
|
||
if (pathLength - segmentLength >= this.targetLength) {
|
||
this.path.pop();
|
||
pathLength -= segmentLength;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
this.currentLength = pathLength;
|
||
}
|
||
|
||
draw() {
|
||
if (this.path.length < 2) return;
|
||
|
||
// 绘制丝带状的蛇身
|
||
ctx.strokeStyle = `hsl(${this.hue}, 70%, 65%)`;
|
||
ctx.lineWidth = this.radius * 2;
|
||
ctx.lineCap = 'round';
|
||
ctx.lineJoin = 'round';
|
||
ctx.shadowBlur = 20;
|
||
ctx.shadowColor = `hsl(${this.hue}, 80%, 55%)`;
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(this.path[0].x, this.path[0].y);
|
||
for (let i = 1; i < this.path.length; i++) {
|
||
ctx.lineTo(this.path[i].x, this.path[i].y);
|
||
}
|
||
ctx.stroke();
|
||
|
||
// 重置阴影
|
||
ctx.shadowBlur = 0;
|
||
}
|
||
}
|
||
|
||
const apples = [];
|
||
|
||
function createApple() {
|
||
const margin = 50;
|
||
return {
|
||
x: margin + Math.random() * (canvas.width - margin * 2),
|
||
y: margin + Math.random() * (canvas.height - margin * 2)
|
||
};
|
||
}
|
||
|
||
// 初始化3个苹果
|
||
for (let i = 0; i < 3; i++) {
|
||
apples.push(createApple());
|
||
}
|
||
|
||
function drawApples() {
|
||
apples.forEach(apple => {
|
||
// 绘制光晕
|
||
const gradient = ctx.createRadialGradient(
|
||
apple.x, apple.y, 0,
|
||
apple.x, apple.y, 28
|
||
);
|
||
gradient.addColorStop(0, `hsla(${snake.hue}, 70%, 65%, 0.5)`);
|
||
gradient.addColorStop(1, 'transparent');
|
||
|
||
ctx.fillStyle = gradient;
|
||
ctx.beginPath();
|
||
ctx.arc(apple.x, apple.y, 28, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
|
||
// 绘制苹果本体
|
||
ctx.fillStyle = `hsl(${snake.hue}, 70%, 65%)`;
|
||
ctx.beginPath();
|
||
ctx.arc(apple.x, apple.y, 14, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
});
|
||
}
|
||
|
||
const snake = new Snake();
|
||
|
||
let lastTime = 0;
|
||
const FPS = 60;
|
||
const frameInterval = 1000 / FPS;
|
||
|
||
function animate(currentTime) {
|
||
requestAnimationFrame(animate);
|
||
|
||
// 限制帧率为60FPS
|
||
const elapsed = currentTime - lastTime;
|
||
if (elapsed < frameInterval) {
|
||
return;
|
||
}
|
||
|
||
lastTime = currentTime - (elapsed % frameInterval);
|
||
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
snake.findNearestApple();
|
||
snake.update();
|
||
drawApples();
|
||
snake.draw();
|
||
}
|
||
|
||
// 开始动画
|
||
animate(0);
|
||
</script>
|
||
</body>
|
||
</html>
|