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. Get through the pipes without touching anything.That’s really fucking coolbasic shit ever you could haveClaude make reallythings that Arden was just like that’s cool, but it canit was like it was a fucking planetary system with thank you I wish I had showed it to you becausecool it was like rotating asteroids and an asteroid belt

<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.25, 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>