Blog
All posts

Making a whole browser game in one HTML file: procedural art and Web Audio music

How Danphe Dash, a game I built with AI, draws four Nepali landscapes in code, synthesizes folk-inspired music with the Web Audio API and holds 60 fps on phones.

Four Danphe Dash biomes: Terai plains, hills, Himalaya and a Tihar festival night

Danphe Dash is an arcade flyer starring the Danphe, Nepal's national bird, in a Dhaka topi. It has four biomes, a soundtrack, boons and omens, a daily challenge and unlockable skins, and the whole game ships as one HTML file with no image or audio files at all.

That constraint started as a joke and became the design. Here's how the art, the music and the performance work.

To be upfront: I built this game with AI, out of boredom. Read this as a breakdown of how the finished game works, not a claim that I wrote every line by hand.

Procedural art: draw once, scroll forever

Every visual is drawn from shapes on a 2D canvas: the Terai's sal forest and rhino, terraced hills with pagodas and prayer flags, a Himalayan stupa with painted eyes, and a Tihar night lit with diyas and rangoli.

Drawing a detailed landscape every frame would never hold 60 fps on a phone. So each biome draws its parallax layers once into offscreen canvases, at startup. The game loop then only copies those prepared layers at different speeds:

render.jsjavascript
// Build once: each layer is drawn into its own offscreen canvas
const layers = theme.layers.map((draw) => {
  const c = new OffscreenCanvas(TILE_W, H);
  draw(c.getContext("2d"));          // the expensive part, done once
  return { canvas: c, speed: draw.speed };
});

// Every frame: two cheap copies per layer, offset for parallax
for (const { canvas, speed } of layers) {
  const x = -((t * speed) % TILE_W);
  ctx.drawImage(canvas, x, 0);
  ctx.drawImage(canvas, x + TILE_W, 0);  // seamless wrap
}

The tricky part is making each tile wrap seamlessly: anything drawn near the right edge also has to be drawn just past the left edge. Animated details like fluttering prayer flags, diya flames and fireworks are drawn live on top, because they're small.

Music with no audio files

The soundtrack is synthesized live with the Web Audio API. Each biome has its own loop of original pentatonic phrases, with timbres that loosely evoke the madal (hand drum), the bansuri (bamboo flute) and the sarangi (bowed string).

Timing is the hard part. JavaScript timers drift, so notes scheduled with setTimeout slur. The fix is a lookahead scheduler: a timer wakes up often, but every note is scheduled on the audio clock, slightly ahead of time.

music.jsjavascript
const LOOKAHEAD = 0.1; // seconds of notes to schedule ahead

function scheduler() {
  while (nextNoteTime < audio.currentTime + LOOKAHEAD) {
    playStep(step, nextNoteTime);       // scheduled on the audio clock
    nextNoteTime += secondsPerEighth;
    step = (step + 1) % 32;            // 4 bars of 8th notes
  }
}
setInterval(scheduler, 25);

Game design that stays fair

It's a rage game, but punishment is opt-in. Boons (shield, tiny, slow-mo, magnet) are pure reward. Omens (wind, heavy, rush, fog) only trigger if you choose to fly through one. Players forgive hard games; they don't forgive unfair ones.

The daily challenge seeds its random generator from the date, so everyone gets an identical run each day and scores are comparable without a server.

Shipping it

The game is an installable PWA that works offline, with a bilingual English and Nepali interface, haptics and an option to reduce flashing and motion. Because the prototype is a single file with every constant in one place, it also became the exact spec for a Unity 6 port: sprites exported at 2× from the game's own drawing code, seamless audio loops and per-biome metadata.

  • Pre-render anything expensive; copy it every frame.
  • Schedule audio on the audio clock, never on timers.
  • Seed randomness from the date for fair daily challenges.
  • Keep the prototype small enough that it can be the spec.

You can play Danphe Dash in your browser, or read the case study for the full breakdown.