复制
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>日落飞鸟</title>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=Fred:wght@400;700&display=swap" rel="stylesheet">
<style>
:root {
--sunset-1: #ff6b6b;
--sunset-2: #feca50;
--sunset-3: #ff9ff3;
--dusk: #36306b;
--night: #1a1746;
--accent: #ffd93b;
--coral: #ff6b6b;
--pipe: #4a902b;
--pipe-dark: #2d5a1a;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
height: 100%;
width: 100%;
overflow: hidden;
background: var(--night);
font-family: 'Fred', sans-serif;
-webkit-user-select: none;
user-select: none;
touch-action: manipulation;
}
#game-wrap {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(180deg, #1a1746 0%, #36306b 40%, #ff6b6b 70%, #feca50 100%);
}
#game-frame {
position: relative;
width: min(100vw, 480px);
height: min(100vh, 720px);
max-height: 100vh;
box-shadow: 0 30px 80px rgba(0,0,0,0.6), inset 0 0 0 1px rgba(255,255,255,0.05);
overflow: hidden;
background: linear-gradient(180deg, #4a3a8e 0%, #ff6b6b 60%, #feca50 100%);
}
canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.overlay {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
pointer-events: none;
z-index: 10;
transition: opacity 0.3s ease;
}
.overlay.hidden {
opacity: 0;
pointer-events: none;
}
.overlay.visible {
opacity: 1;
pointer-events: auto;
}
.panel {
background: rgba(26, 23, 70, 0.85);
border: 3px solid var(--accent);
border-radius: 16px;
padding: 28px 36px;
text-align: center;
color: #fff;
box-shadow: 0 12px 0 rgba(0,0,0,0.3), 0 0 40px rgba(255, 217, 59, 0.4);
backdrop-filter: blur(4px);
transform: translateY(0);
animation: bob 3s ease-in-out infinite;
}
@keyframes bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
.title {
font-family: 'Press Start 2P', monospace;
font-size: 26px;
color: var(--accent);
text-shadow: 3px 3px 0 #b87c0a, 6px 6px 0 rgba(0,0,0,0.3);
letter-spacing: 1px;
margin-bottom: 6px;
line-height: 1.3;
}
.subtitle {
font-size: 14px;
color: #ffe8a8;
margin-bottom: 20px;
font-weight: 700;
}
.tap-icon {
font-size: 42px;
margin: 8px 0;
animation: tap 1.2s ease-in-out infinite;
}
@keyframes tap {
0%, 50% { transform: translateY(0) scale(1); }
20% { transform: translateY(-8px) scale(1.1); }
}
.instructions {
font-size: 13px;
color: #fff;
opacity: 0.85;
margin-top: 10px;
font-weight: 400;
}
.score-row {
display: flex;
gap: 24px;
justify-content: center;
margin: 12px 0 20px;
}
.score-box {
background: rgba(0,0,0,0.4);
border: 2px solid var(--accent);
border-radius: 10px;
padding: 10px 18px;
min-width: 80px;
}
.score-box .label {
font-family: 'Press Start 2p', monospace;
font-size: 9px;
color: var(--accent);
margin-bottom: 4px;
letter-spacing: 1px;
}
.score-box .value {
font-family: 'Press Start 2p', monospace;
font-size: 18px;
color: #fff;
}
.score-box.best .value { color: var(--coral); }
.btn {
font-family: 'Press Start 2p', monospace;
font-size: 12px;
color: var(--night);
background: var(--accent);
border: none;
padding: 12px 22px;
border-radius: 8px;
cursor: pointer;
letter-spacing: 1px;
box-shadow: 0 6px 0 #b87c0a, 0 8px 12px rgba(0,0,0,0.3);
transition: transform 0.1s, box-shadow 0.1s;
pointer-events: auto;
}
.btn:hover { transform: translateY(2px); box-shadow: 0 4px 0 #b87c0a, 0 6px 10px rgba(0,0,0,0.3); }
.btn:active { transform: translateY(6px); box-shadow: 0 0 0 #b87c0a, 0 2px 6px rgba(0,0,0,0.3); }
#hud {
position: absolute;
top: 24px;
left: 50%;
transform: translateX(-50%);
z-index: 5;
font-family: 'Press Start 2P', monospace;
font-size: 36px;
color: #fff;
text-shadow: 3px 3px 0 #b87c0a, 5px 5px 0 rgba(0,0,0,0.5);
pointer-events: none;
transition: opacity 0.3s;
opacity: 0;
}
#hud.visible { opacity: 1; }
#hud .new-best {
font-size: 11px;
color: var(--coral);
text-shadow: 2px 2px 0 #000;
margin-top: 8px;
opacity: 0;
transition: opacity 0.3s;
}
#hud.has-new-best .new-best { opacity: 1; animation: pulse 0.6s ease infinite alternate; }
@keyframes pulse {
from { transform: translateX(-50%) scale(1); }
to { transform: translateX(-50%) scale(1.15); }
}
.medal {
font-family: 'Press Start 2P', monospace;
font-size: 48px;
margin: 10px 0;
text-shadow: 3px 3px 0 #000;
}
.flash {
position: absolute;
inset: 0;
background: #fff;
opacity: 0;
pointer-events: none;
z-index: 8;
}
.footer {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
color: rgba(255,255,255,0.7);
font-weight: 400;
text-align: center;
pointer-events: none;
z-index: 9;
}
</style>
</head>
<body>
<div id="game-wrap">
<div id="game-frame">
<canvas id="canvas"></canvas>
<div id="hud" class="hud">
<div id="hud-score">0</div>
<div class="new-best">新纪录!</div>
</div>
<div id="flash" class="flash"></div>
<!-- 开始界面 -->
<div id="start-screen" class="overlay visible">
<div class="panel">
<div class="title">日落<br>飞行者</div>
<div class="subtitle">— 黄昏飞行 —</div>
<div class="tap-icon">✊</div>
<div class="instructions">轻点 / 空格 / 点击 来拍动翅膀</div>
</div>
</div>
<!-- 游戏结束界面 -->
<div id="end-screen" class="overlay hidden">
<div class="panel">
<div class="title" style="font-size:22px;color:var(--coral);">游戏结束</div>
<div id="medal" class="medal" style="display:none;">★</div>
<div class="score-row">
<div class="score-box">
<div class="label">得分</div>
<div class="value" id="end-score">0</div>
</div>
<div class="score-box best">
<div class="label">最佳</div>
<div class="value" id="end-best">0</div>
</div>
</div>
<button class="btn" id="restart-btn">重试</button>
</div>
</div>
<div class="footer">声音开启 • 轻点起飞</div>
</div>
</div>
<script>
(() => {
// ============ 设置 ============
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const frame = document.getElementById('game-frame');
const hud = document.getElementById('hud');
const hudScore = document.getElementById('hud-score');
const startScreen = document.getElementById('start-screen');
const endScreen = document.getElementById('end-screen');
const endScore = document.getElementById('end-score');
const endBest = document.getElementById('end-best');
const medalEl = document.getElementById('medal');
const flashEl = document.getElementById('flash');
const restartBtn = document.getElementById('restart-btn');
let W = 480, H = 720;
const dpr = window.devicePixelRatio || 1;
function resize() {
const rect = frame.getBoundingClientRect();
W = rect.width;
H = rect.height;
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize);
resize();
// ============ 音频(Web Audio API) ============
let audioCtx = null;
function getAudio() {
if (!audioCtx) {
try { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); }
catch(e) { audioCtx = null; }
}
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
}
function beep(freq, dur=0.1, type='square', vol=0.1, slideTo=null) {
const ac = getAudio();
if (!ac) return;
const osc = ac.createOscillator();
const gain = ac.createGain();
osc.type = type;
osc.frequency.value = freq;
if (slideTo) osc.frequency.linearRampToValueAtTime(slideTo, ac.currentTime + dur);
gain.gain.value = vol;
gain.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + dur);
osc.connect(gain);
gain.connect(ac.destination);
osc.start();
osc.stop(ac.currentTime + dur);
}
const sfx = {
flap: () => beep(420, 0.12, 'square', 0.08, 720),
score: () => { beep(880, 0.08, 'triangle', 0.12); setTimeout(()=>beep(1320, 0.1, 'triangle', 0.1), 60); }
hit: () => { beep(180, 0.25, 'sawtooth', 0.15, 60); }
die: () => { beep(120, 0.5, 'sawtooth', 0.15, 40); }
};
// ============ 游戏状态 ============
const STATE = { READY: 0, PLAYING: 1, DYING: 2, OVER: 3 };
let state = STATE.READY;
let bird, pipes, particles, score, best, frameCount, shake, groundX, clouds, stars;
best = parseInt(localStorage.getItem('sunsetFlierBest') || '0', 10) || 0;
endBest.textContent = best;
// ============ 小鸟 ============
function makeBird() {
return {
x: W * 0.32,
y: H * 0.5,
vy: 0,
radius: 14,
wingPhase: 0,
rotation: 0,
alive: true
};
}
// ============ 管道 ============
const PIPE_W = 68;
const PIPE_GAP = 180;
const PIPE_SPEED = 2.6;
const PIPE_SPACING = 220;
function makePipe(x) {
const minY = 90;
const maxY = H - 90 - PIPE_GAP;
const gapY = minY + Math.random() * (maxY - minY);
return { x, gapY, scored: false, passed: false };
}
function initPipes() {
pipes = [];
let x = W + 80;
for (let i = 0; i < 4; i++) {
pipes.push(makePipe(x));
x += PIPE_SPACING;
}
}
// ============ 粒子 ============
function spawnParticles(x, y, color, count=10, vel=3) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = (Math.random() * 0.6 + 0.4) * vel;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - 1,
life: 1,
size: Math.random() * 4 + 2,
color
});
}
}
// ============ 背景 ============
function initBg() {
clouds = [];
for (let i = 0; i < 4; i++) {
clouds.push({
x: Math.random() * W,
y: 80 + Math.random() * 180,
scale: 0.6 + Math.random() * 0.8,
speed: 0.2 + Math.random() * 0.3
});
}
stars = [];
for (let i = 0; i < 30; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H * 0.4,
r: Math.random() * 1.5 + 0.3,
tw: Math.random() * Math.PI * 2
});
}
}
// ============ 重置 / 开始 ============
function reset() {
bird = makeBird();
pipes = [];
particles = [];
score = 0;
frameCount = 0;
shake = 0;
groundX = 0;
initPipes();
initBg();
hudScore.textContent = '0';
hud.classList.remove('has-new-best');
}
function startGame() {
state = STATE.PLAYING;
startScreen.classList.add('hidden');
startScreen.classList.remove('visible');
hud.classList.add('visible');
bird.vy = -7.2;
sfx.flap();
}
function gameOver() {
state = STATE.DYING;
sfx.hit();
setTimeout(sfx.die, 120);
shake = 18;
spawnParticles(bird.x, bird.y, '#ffd93b', 24, 5);
spawnParticles(bird.x, bird.y, '#ff6b6b', 16, 4);
flashScreen();
bird.alive = false;
const isNewBest = score > best;
if (isNewBest) {
best = score;
localStorage.setItem('sunsetFlierBest', String(best));
}
setTimeout(() => {
state = STATE.OVER;
endScore.textContent = score;
endBest.textContent = best;
const medal = getMedal(score);
if (medal) {
medalEl.style.display = 'block';
medalEl.textContent = medal.symbol;
medalEl.style.color = medal.color;
} else {
medalEl.style.display = 'none';
}
endScreen.classList.remove('hidden');
endScreen.classList.add('visible');
if (isNewBest) {
setTimeout(()=>hud.classList.add('has-new-best'), 0);
}
}, 700);
}
function flashScreen() {
flashEl.style.transition = 'none';
flashEl.style.opacity = '0.9';
requestAnimationFrame(() => {
flashEl.style.transition = 'opacity 0.4s ease-out';
flashEl.style.opacity = '0';
});
}
function getMedal(s) {
if (s >= 30) return { symbol: '★', color: '#ffd93b' };
if (s >= 20) return { symbol: '✦', color: '#c0c0c0' };
if (s >= 10) return { symbol: '✧', color: '#cd7f32' };
if (s >= 5) return { symbol: '•', color: '#ff9ff3' };
return null;
}
function restart() {
reset();
state = STATE.READY;
endScreen.classList.add('hidden');
endScreen.classList.remove('visible');
startScreen.classList.remove('hidden');
startScreen.classList.add('visible');
hud.classList.remove('visible');
hud.classList.remove('has-new-best');
}
// ============ 输入 ============
function flap() {
if (state === STATE.READY) { startGame(); return; }
if (state === STATE.PLAYING) {
bird.vy = -7.2;
bird.wingPhase = 0;
sfx.flap();
spawnParticles(bird.x - 8, bird.y + 6, '#fff8d6', 4, 1.5);
}
if (state === STATE.OVER) {
// 结束界面任意位置点击也会重新开始
// 但我们主要让按钮来处理它
}
}
function handleInput(e) {
if (e.target.closest('button')) return;
e.preventDefault();
flap();
}
frame.addEventListener('mousedown', handleInput);
frame.addEventListener('touchstart', handleInput, { passive: false });
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' || e.code === 'ArrowUp' || e.code === 'Enter') {
e.preventDefault();
if (state === STATE.OVER) restart();
else flap();
}
});
restartBtn.addEventListener('click', (e) => { e.stopPropagation(); restart(); });
// ============ 更新 ============
const GRAVITY = 0.42;
const MAX_FALL = 9;
function update() {
frameCount++;
if (state === STATE.READY) {
// 待机摆动
bird.y = H * 0.5 + Math.sin(frameCount * 0.08) * 14;
bird.wingPhase += 0.2;
bird.rotation = Math.sin(frameCount * 0.08) * 0.05;
}
if (state === STATE.PLAYING) {
bird.vy += GRAVITY;
if (bird.vy > MAX_FALL) bird.vy = MAX_FALL;
bird.y += bird.vy;
bird.wingPhase += 0.4;
bird.rotation = Math.max(-0.4, Math.min(1.2, bird.vy * 0.08));
// 管道
for (const p of pipes) {
p.x -= PIPE_SPEED;
if (!p.scored && p.x + PIPE_W < bird.x) {
p.scored = true;
score++;
hudScore.textContent = score;
sfx.score();
spawnParticles(bird.x, bird.y, '#ffd93b', 8, 2);
}
}
// 移除屏幕外的管道,添加新的
while (pipes.length > 0 && pipes[0].x + PIPE_W < -10) pipes.shift();
const last = pipes[pipes.length - 1];
if (last && last.x < W - PIPE_SPACING) {
pipes.push(makePipe(last.x + PIPE_SPACING));
}
// 碰撞
// 地面与天花板
const groundY = H - 80;
if (bird.y + bird.radius > groundY || bird.y - bird.radius < 0) {
bird.y = Math.max(bird.radius + 1, Math.min(groundY - bird.radius, bird.y));
gameOver();
return;
}
// 管道
for (const p of pipes) {
if (p.x > bird.x + bird.radius || p.x + PIPE_W < bird.x - bird.radius) continue;
const top = p.gapY;
const bottom = p.gapY + PIPE_GAP;
if (bird.y - bird.radius < top || bird.y + bird.radius > bottom) {
// 碰撞
gameOver();
return;
}
}
}
if (state === STATE.DYING) {
bird.vy += GRAVITY * 1.2;
bird.y += bird.vy;
bird.rotation += 0.06;
const groundY = H - 80;
if (bird.y + bird.radius > groundY) {
bird.y = groundY - bird.radius;
bird.vy = 0;
}
}
// 粒子
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.15;
p.life -= 0.02;
if (p.life <= 0) particles.splice(i, 1);
}
// 抖动衰减
if (shake > 0) shake = Math.max(0, shake - 1.2);
// 背景动画
for (const c of clouds) {
c.x -= c.speed;
if (c.x < -100) c.x = W + 100;
}
for (const s of stars) {
s.tw += 0.05;
}
groundX = (groundX - PIPE_SPEED) % 120;
}
// ============ 渲染 ============
function render() {
ctx.save();
// 抖动
if (shake > 0) {
const sx = (Math.random() - 0.5) * shake;
const sy = (Math.random() - 0.5) * shake;
ctx.translate(sx, sy);
}
// 天空渐变
const sky = ctx.createLinearGradient(0, 0, 0, H);
sky.addColorStop(0, '#1a1746');
sky.addColorStop(0.3, '#4a3a8e');
sky.addColorStop(0.55, '#ff6b6b');
sky.addColorStop(0.8, '#feca50');
sky.addColorStop(1, '#ffd93b');
ctx.fillStyle = sky;
ctx.fillRect(-20, -20, W + 40, H + 40);
// 星星
for (const s of stars) {
const a = 0.4 + Math.sin(s.tw) * 0.4;
ctx.fillStyle = `rgba(255,255,255,${a})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fill();
}
// 太阳
const sunX = W * 0.75;
const sunY = H * 0.65;
const sunGlow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, 180);
sunGlow.addColorStop(0, 'rgba(255,240,160,0.7)');
sunGlow.addColorStop(0.4, 'rgba(255,200,120,0.3)');
sunGlow.addColorStop(1, 'rgba(255,200,120,0)');
ctx.fillStyle = sunGlow;
ctx.fillRect(sunX - 200, sunY - 200, 400, 400);
ctx.fillStyle = '#fff6d0';
ctx.beginPath();
ctx.arc(sunX, sunY, 42, 0, Math.PI * 2);
ctx.fill();
// 云朵
for (const c of clouds) {
drawCloud(c.x, c.y, c.scale);
}
// 山脉剪影
ctx.fillStyle = 'rgba(40, 30, 80, 0.7)';
ctx.beginPath();
ctx.moveTo(0, H * 0.72);
const peaks = [
[W * 0.15, H * 0.58],
[W * 0.25, H * 0.68],
[W * 0.4, H * 0.52],
[W * 0.55, H * 0.66],
[W * 0.7, H * 0.6],
[W * 0.85, H * 0.68],
[W, H * 0.62]
];
for (const [px, py] of peaks) ctx.lineTo(px, py);
ctx.lineTo(W, H);
ctx.lineTo(0, H);
ctx.closePath();
ctx.fill();
// 管道
for (const p of pipes) {
drawPipe(p);
}
// 地面
drawGround();
// 小鸟
drawBird();
// 粒子
for (const part of particles) {
ctx.fillStyle = part.color;
ctx.globalAlpha = part.life;
ctx.beginPath();
ctx.arc(part.x, part.y, part.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
ctx.restore();
}
function drawCloud(x, y, scale) {
ctx.fillStyle = 'rgba(255, 230, 200, 0.85)';
const s = scale;
ctx.beginPath();
ctx.arc(x, y, 18 * s, 0, Math.PI * 2);
ctx.arc(x + 18 * s, y + 4 * s, 22 * s, 0, Math.PI * 2);
ctx.arc(x + 38 * s, y, 16 * s, 0, Math.PI * 2);
ctx.arc(x + 18 * s, y - 6 * s, 14 * s, 0, Math.PI * 2);
ctx.fill();
}
function drawPipe(p) {
const x = p.x;
const topY = p.gapY;
const bottomY = p.gapY + PIPE_GAP;
const groundY = H - 80;
// 渐变
const grad = ctx.createLinearGradient(x, 0, x + PIPE_W, 0);
grad.addColorStop(0, '#2d5a1a');
grad.addColorStop(0.15, '#4a902b');
grad.addColorStop(0.5, '#7bc94a');
grad.addColorStop(0.85, '#4a902b');
grad.addColorStop(1, '#2d5a1a');
// 上方管道
ctx.fillStyle = grad;
ctx.fillRect(x, 0, PIPE_W, topY - 18);
// 上方管帽
ctx.fillRect(x - 4, topY - 18, PIPE_W + 8, 18);
// 上方管帽高光
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(x - 4, topY - 18, 14, 18);
// 下方管道
ctx.fillStyle = grad;
ctx.fillRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
// 下方管帽
ctx.fillRect(x - 4, bottomY, PIPE_W + 8, 18);
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.fillRect(x - 4, bottomY, 14, 18);
// 描边
ctx.strokeStyle = '#1c3a0d';
ctx.lineWidth = 2;
ctx.strokeRect(x, 0, PIPE_W, topY - 18);
ctx.strokeRect(x - 4, topY - 18, PIPE_W + 8, 18);
ctx.strokeRect(x, bottomY + 18, PIPE_W, groundY - (bottomY + 18));
ctx.strokeRect(x - 4, bottomY, PIPE_W + 8, 18);
}
function drawGround() {
const groundY = H - 80;
// 沙地/黄昏地面
const g = ctx.createLinearGradient(0, groundY, 0, H);
g.addColorStop(0, '#8b5a2b');
g.addColorStop(0.4, '#5e3a1c');
g.addColorStop(1, '#2d1a0c');
ctx.fillStyle = g;
ctx.fillRect(0, groundY, W, 80);
// 表面线
ctx.strokeStyle = '#1c3a0d';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, groundY);
ctx.lineTo(W, groundY);
ctx.stroke();
// 图案条纹(移动)
ctx.fillStyle = 'rgba(0,0,0,0.15)';
for (let i = -2; i < W / 40 + 2; i++) {
const stripeX = (i * 80 + groundX + 120) % (W + 80) - 40;
ctx.beginPath();
ctx.moveTo(stripeX, groundY);
ctx.lineTo(stripeX + 30, groundY + 80);
ctx.lineTo(stripeX + 50, groundY + 80);
ctx.lineTo(stripeX + 20, groundY);
ctx.closePath();
ctx.fill();
}
// 表面的卵石
ctx.fillStyle = '#3d2412';
for (let i = 0; i < 12; i++) {
const px = (i * 60 + groundX + 60) % (W + 40);
ctx.beginPath();
ctx.arc(px, groundY + 4, 3, 0, Math.PI * 2);
ctx.fill();
}
}
function drawBird() {
const b = bird;
ctx.save();
ctx.translate(b.x, b.y);
ctx.rotate(b.rotation);
// 阴影
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.beginPath();
ctx.ellipse(2, 2, b.radius, b.radius * 0.85, 0, 0, Math.PI * 2);
ctx.fill();
// 身体
const bodyGrad = ctx.createRadialGradient(-4, -4, 2, 0, 0, b.radius + 4);
bodyGrad.addColorStop(0, '#ff9ff3');
bodyGrad.addColorStop(0.5, '#ff6b6b');
bodyGrad.addColorStop(1, '#c0334a');
ctx.fillStyle = bodyGrad;
ctx.beginPath();
ctx.ellipse(0, 0, b.radius, b.radius * 0.92, 0, 0, Math.PI * 2);
ctx.fill();
// 翅膀(拍动)
const wingY = Math.sin(b.wingPhase) * 8;
ctx.fillStyle = '#ffd93b';
ctx.beginPath();
ctx.ellipse(-4, 4 + wingY, 9, 5, -0.4 + wingY * 0.05, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = '#b87c0a';
ctx.lineWidth = 1.5;
ctx.stroke();
// 眼睛
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(8, -4, 5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#1a1746';
ctx.beginPath();
ctx.arc(9.5, -4, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(10, -5, 1, 0, Math.PI * 2);
ctx.fill();
// 喙
ctx.fillStyle = '#ffaa33';
ctx.beginPath();
ctx.moveTo(13, -1);
ctx.lineTo(20, 1);
ctx.lineTo(13, 3);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#b87c0a';
ctx.lineWidth = 1;
ctx.stroke();
// 尾巴
ctx.fillStyle = '#ffd93b';
ctx.beginPath();
ctx.moveTo(-12, -2);
ctx.lineTo(-18, -5);
ctx.lineTo(-16, 0);
ctx.lineTo(-18, 5);
ctx.lineTo(-12, 3);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
}
// ============ 主循环 ============
function loop() {
update();
render();
requestAnimationFrame(loop);
}
reset();
loop();
})();
</script>
</body>
</html>