Skip to content

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>

We count page views to see how Strata is doing. If you're happy with it, we'll also remember which link first brought you here so we know what's working. Your data stays with us — we never share it. Privacy policy