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.

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:
// 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.
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.