Add Flappy Bird game - HTML5 Canvas implementation

This commit is contained in:
Flappy Dev 2026-06-06 14:42:47 +00:00
commit e1c8e971fd
2 changed files with 316 additions and 0 deletions

279
game.js Normal file
View file

@ -0,0 +1,279 @@
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const W = canvas.width;
const H = canvas.height;
// --- Constants ---
const GRAVITY = 0.45;
const FLAP_FORCE = -8.5;
const PIPE_SPEED = 2.8;
const PIPE_WIDTH = 60;
const PIPE_GAP = 160;
const PIPE_INTERVAL = 90; // frames between pipes
const BIRD_X = 90;
const BIRD_R = 18; // collision radius (slightly smaller than visual)
// --- Palette ---
const SKY_TOP = '#4ec0e4';
const SKY_BOT = '#87ceeb';
const GROUND_COL = '#ded895';
const PIPE_COL = '#4caf50';
const PIPE_DARK = '#388e3c';
const BIRD_BODY = '#ffd700';
const BIRD_WING = '#ffb300';
const BIRD_EYE = '#fff';
const BIRD_PUPIL = '#222';
const BIRD_BEAK = '#ff8c00';
// --- State ---
let bird, pipes, score, frame, state, bestScore;
// states: 'idle' | 'playing' | 'dead'
function init() {
bird = { y: H / 2, vy: 0, angle: 0, wingPhase: 0 };
pipes = [];
score = 0;
frame = 0;
state = 'idle';
bestScore = bestScore || 0;
}
// --- Input ---
function flap() {
if (state === 'idle') { state = 'playing'; }
if (state === 'playing') {
bird.vy = FLAP_FORCE;
bird.wingPhase = 0;
}
if (state === 'dead') { init(); }
}
document.addEventListener('keydown', e => { if (e.code === 'Space') { e.preventDefault(); flap(); }});
canvas.addEventListener('click', flap);
canvas.addEventListener('touchstart', e => { e.preventDefault(); flap(); }, { passive: false });
// --- Pipe helpers ---
function spawnPipe() {
const minTop = 80;
const maxTop = H - 120 - PIPE_GAP;
const topH = Math.floor(Math.random() * (maxTop - minTop + 1)) + minTop;
pipes.push({ x: W + 10, topH, passed: false });
}
// --- Collision ---
function circleRect(cx, cy, r, rx, ry, rw, rh) {
const nearX = Math.max(rx, Math.min(cx, rx + rw));
const nearY = Math.max(ry, Math.min(cy, ry + rh));
const dx = cx - nearX, dy = cy - nearY;
return dx * dx + dy * dy < r * r;
}
function checkCollision() {
const GROUND_Y = H - 80;
if (bird.y + BIRD_R >= GROUND_Y) return true;
if (bird.y - BIRD_R <= 0) return true;
for (const p of pipes) {
const cap = 6; // pipe cap extra width
if (
circleRect(BIRD_X, bird.y, BIRD_R, p.x - cap, 0, PIPE_WIDTH + cap * 2, p.topH) ||
circleRect(BIRD_X, bird.y, BIRD_R, p.x - cap, p.topH + PIPE_GAP, PIPE_WIDTH + cap * 2, H)
) return true;
}
return false;
}
// --- Draw helpers ---
function drawSky() {
const grad = ctx.createLinearGradient(0, 0, 0, H);
grad.addColorStop(0, SKY_TOP);
grad.addColorStop(1, SKY_BOT);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, W, H);
}
function drawGround() {
const gy = H - 80;
ctx.fillStyle = GROUND_COL;
ctx.fillRect(0, gy, W, 80);
ctx.fillStyle = '#8bc34a';
ctx.fillRect(0, gy, W, 12);
}
function drawPipe(p) {
const capH = 24, capW = PIPE_WIDTH + 12;
const capX = p.x - 6;
// bottom pipe
const botY = p.topH + PIPE_GAP;
ctx.fillStyle = PIPE_COL;
ctx.fillRect(p.x, botY, PIPE_WIDTH, H - botY);
ctx.fillStyle = PIPE_DARK;
ctx.fillRect(p.x, botY, 6, H - botY);
// bottom cap
ctx.fillStyle = PIPE_COL;
ctx.fillRect(capX, botY, capW, capH);
ctx.fillStyle = PIPE_DARK;
ctx.fillRect(capX, botY, 6, capH);
// top pipe
ctx.fillStyle = PIPE_COL;
ctx.fillRect(p.x, 0, PIPE_WIDTH, p.topH);
ctx.fillStyle = PIPE_DARK;
ctx.fillRect(p.x, 0, 6, p.topH);
// top cap
ctx.fillStyle = PIPE_COL;
ctx.fillRect(capX, p.topH - capH, capW, capH);
ctx.fillStyle = PIPE_DARK;
ctx.fillRect(capX, p.topH - capH, 6, capH);
}
function drawBird() {
ctx.save();
ctx.translate(BIRD_X, bird.y);
const angle = Math.min(Math.max(bird.vy * 3, -30), 80) * Math.PI / 180;
ctx.rotate(angle);
// wing
const wingY = 4 + Math.sin(bird.wingPhase) * 8;
ctx.fillStyle = BIRD_WING;
ctx.beginPath();
ctx.ellipse(-4, wingY, 10, 6, -0.4, 0, Math.PI * 2);
ctx.fill();
// body
ctx.fillStyle = BIRD_BODY;
ctx.beginPath();
ctx.ellipse(0, 0, 22, 18, 0, 0, Math.PI * 2);
ctx.fill();
// white belly
ctx.fillStyle = '#fff8dc';
ctx.beginPath();
ctx.ellipse(4, 4, 12, 9, 0.3, 0, Math.PI * 2);
ctx.fill();
// eye
ctx.fillStyle = BIRD_EYE;
ctx.beginPath();
ctx.arc(9, -5, 7, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = BIRD_PUPIL;
ctx.beginPath();
ctx.arc(11, -5, 4, 0, Math.PI * 2);
ctx.fill();
// shine
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(12, -7, 1.5, 0, Math.PI * 2);
ctx.fill();
// beak
ctx.fillStyle = BIRD_BEAK;
ctx.beginPath();
ctx.moveTo(18, -2);
ctx.lineTo(28, 2);
ctx.lineTo(18, 6);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawScore() {
ctx.fillStyle = '#fff';
ctx.strokeStyle = '#333';
ctx.lineWidth = 4;
ctx.font = 'bold 42px Segoe UI, Arial';
ctx.textAlign = 'center';
ctx.strokeText(score, W / 2, 70);
ctx.fillText(score, W / 2, 70);
}
function drawOverlay(title, sub) {
// panel
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.beginPath();
ctx.roundRect(W / 2 - 140, H / 2 - 120, 280, 240, 16);
ctx.fill();
ctx.textAlign = 'center';
ctx.fillStyle = '#ffd700';
ctx.font = 'bold 36px Segoe UI, Arial';
ctx.fillText(title, W / 2, H / 2 - 68);
if (state === 'dead') {
ctx.fillStyle = '#fff';
ctx.font = '20px Segoe UI, Arial';
ctx.fillText(`Score: ${score}`, W / 2, H / 2 - 20);
ctx.fillText(`Best: ${bestScore}`, W / 2, H / 2 + 14);
}
ctx.fillStyle = '#87ceeb';
ctx.font = '18px Segoe UI, Arial';
ctx.fillText(sub, W / 2, H / 2 + 60);
ctx.fillStyle = 'rgba(255,255,255,0.15)';
ctx.beginPath();
ctx.roundRect(W / 2 - 100, H / 2 + 74, 200, 42, 10);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.font = 'bold 16px Segoe UI, Arial';
ctx.fillText('Space / Click / Tap', W / 2, H / 2 + 100);
}
// --- Main loop ---
function update() {
if (state === 'playing') {
bird.vy += GRAVITY;
bird.y += bird.vy;
bird.wingPhase += 0.25;
// Spawn pipes
if (frame % PIPE_INTERVAL === 0) spawnPipe();
// Move pipes & score
for (const p of pipes) {
p.x -= PIPE_SPEED;
if (!p.passed && p.x + PIPE_WIDTH < BIRD_X) {
p.passed = true;
score++;
if (score > bestScore) bestScore = score;
}
}
// Remove off-screen pipes
pipes = pipes.filter(p => p.x + PIPE_WIDTH + 20 > 0);
if (checkCollision()) { state = 'dead'; }
frame++;
}
if (state === 'idle') {
bird.y = H / 2 + Math.sin(frame * 0.05) * 10;
frame++;
}
}
function render() {
drawSky();
pipes.forEach(drawPipe);
drawGround();
drawBird();
if (state === 'playing' || state === 'dead') drawScore();
if (state === 'idle') drawOverlay('Flappy Bird', 'Get ready!');
if (state === 'dead') drawOverlay('Game Over', 'Try again?');
}
function loop() {
update();
render();
requestAnimationFrame(loop);
}
init();
loop();

37
index.html Normal file
View file

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flappy Bird</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #1a1a2e;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
font-family: 'Segoe UI', sans-serif;
color: #fff;
}
canvas {
border: 3px solid #16213e;
border-radius: 8px;
box-shadow: 0 0 40px rgba(0,200,255,0.2);
display: block;
}
#info {
margin-top: 12px;
font-size: 14px;
opacity: 0.6;
}
</style>
</head>
<body>
<canvas id="gameCanvas" width="400" height="600"></canvas>
<div id="info">Space / Click / Tap to flap</div>
<script src="game.js"></script>
</body>
</html>