Flappy Bird Sandbox
A demo of Strata's interactive HTML blocks: a fully playable Flappy Bird rendered on a canvas and a mermaid diagram of the game's state machine.
Play
Click, tap, or press Space to flap.
<div id="stage" style="display:flex;justify-content:center;padding:8px 0;">
<canvas id="game" style="width:320px;max-width:100%;border-radius:8px;touch-action:manipulation;cursor:pointer;"></canvas>
</div>
<script>
const W = 320, H = 480, GROUND_Y = 400;
const PIPE_W = 52, GAP = 135, SPEED = 2.2, GRAVITY = 0.27, FLAP = -7.2;
const cvs = document.getElementById('game');
const ctx = cvs.getContext('2d');
const dpr = window.devicePixelRatio || 1;
cvs.width = W * dpr;
cvs.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
let state = 'ready'; // ready | play | over
let bird, pipes, score, frames, best = 0, diedAt = 0;
function reset() {
bird = { x: 80, y: 220, vel: 0, r: 12 };
pipes = [];
score = 0;
frames = 0;
state = 'ready';
}
reset();
function flap() {
if (state === 'ready') {
state = 'play';
bird.vel = FLAP;
} else if (state === 'play') {
bird.vel = FLAP;
} else if (state === 'over' && performance.now() - diedAt > 400) {
reset();
}
}
cvs.addEventListener('pointerdown', (e) => { e.preventDefault(); flap(); });
window.addEventListener('keydown', (e) => {
if (e.code === 'Space' || e.code === 'ArrowUp') { e.preventDefault(); flap(); }
});
function die() {
state = 'over';
diedAt = performance.now();
best = Math.max(best, score);
}
function update() {
if (state === 'ready') {
bird.y = 220 + Math.sin(performance.now() / 300) * 8;
return;
}
if (state !== 'play') return;
frames++;
bird.vel += GRAVITY;
bird.y += bird.vel;
if (bird.y - bird.r < 0) { bird.y = bird.r; bird.vel = 0; }
if (bird.y + bird.r > GROUND_Y) { bird.y = GROUND_Y - bird.r; die(); return; }
if (frames % 88 === 1) {
pipes.push({ x: W + 10, top: 60 + Math.random() * (GROUND_Y - GAP - 120), passed: false });
}
for (const p of pipes) {
p.x -= SPEED;
if (!p.passed && p.x + PIPE_W < bird.x) { p.passed = true; score++; }
const inX = bird.x + bird.r > p.x && bird.x - bird.r < p.x + PIPE_W;
const inGap = bird.y - bird.r > p.top && bird.y + bird.r < p.top + GAP;
if (inX && !inGap) { die(); return; }
}
pipes = pipes.filter((p) => p.x > -PIPE_W - 10);
}
function drawPipe(p) {
ctx.fillStyle = '#5ec22e';
ctx.fillRect(p.x, 0, PIPE_W, p.top);
ctx.fillRect(p.x, p.top + GAP, PIPE_W, GROUND_Y - p.top - GAP);
ctx.fillStyle = '#4aa823';
ctx.fillRect(p.x - 3, p.top - 16, PIPE_W + 6, 16);
ctx.fillRect(p.x - 3, p.top + GAP, PIPE_W + 6, 16);
}
function drawBird() {
ctx.save();
ctx.translate(bird.x, bird.y);
ctx.rotate(Math.max(-0.5, Math.min(0.9, bird.vel * 0.06)));
ctx.fillStyle = '#f7d51d';
ctx.beginPath();
ctx.arc(0, 0, bird.r, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#e8a020';
ctx.beginPath();
ctx.ellipse(-3, 3, 6, 4, 0, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.arc(5, -4, 4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#222';
ctx.beginPath();
ctx.arc(6, -4, 2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#f0713a';
ctx.fillRect(9, 0, 8, 4);
ctx.restore();
}
function centerText(text, y, size, fill) {
ctx.font = 'bold ' + size + 'px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.lineWidth = 4;
ctx.strokeStyle = 'rgba(0,0,0,0.45)';
ctx.strokeText(text, W / 2, y);
ctx.fillStyle = fill;
ctx.fillText(text, W / 2, y);
}
function draw() {
ctx.fillStyle = '#70c5ce';
ctx.fillRect(0, 0, W, H);
for (const p of pipes) drawPipe(p);
ctx.fillStyle = '#ded895';
ctx.fillRect(0, GROUND_Y, W, H - GROUND_Y);
ctx.fillStyle = '#9bc25e';
ctx.fillRect(0, GROUND_Y, W, 8);
drawBird();
if (state === 'play') {
centerText(String(score), 70, 42, '#fff');
} else if (state === 'ready') {
centerText('Flappy Bird', 150, 32, '#fff');
centerText('Tap or press Space', 190, 16, '#fff');
} else {
centerText('Game over', 150, 32, '#fff');
centerText('Score ' + score + ' Best ' + best, 190, 18, '#fff');
centerText('Tap to restart', 222, 14, '#fff');
}
}
(function loop() {
update();
draw();
requestAnimationFrame(loop);
})();
</script>
Game state machine
<pre class="mermaid">
stateDiagram-v2
[*] --> Ready
Ready --> Playing: tap / Space
Playing --> Playing: flap
Playing --> GameOver: hit a pipe or the ground
GameOver --> Ready: tap to restart
</pre>
<script src="/sandbox/libs/mermaid.min.js"></script>
<script>
const s = getComputedStyle(document.body);
const t = (name, fb) => (s.getPropertyValue(name) || '').trim() || fb;
mermaid.initialize({
startOnLoad: false,
theme: 'base',
themeVariables: {
primaryColor: t('--color-muted', '#f4f4f5'),
primaryTextColor: t('--color-foreground', '#18181b'),
primaryBorderColor: t('--color-border', '#d4d4d8'),
lineColor: t('--color-muted-foreground', '#71717a'),
textColor: t('--color-foreground', '#18181b'),
background: 'transparent'
}
});
mermaid.run();
</script>