#05 · June 2, 2026 · ▲ 3,800m · 2 min · code
Rendering mountains in ASCII
How the basecamp on the home page is drawn: a character grid, a little noise, and a fire that is really a heat map.
The scene on the home page is not a video or a sprite sheet. It is a canvas that draws a few thousand characters sixty times a second. Here is how it fits together.
A grid of cells
Everything starts with a grid. Each cell is about 12 pixels wide and 14 pixels tall, and holds exactly one glyph, one color, and one opacity. The renderer never draws lines or shapes, only characters at cell centers.
const cw = clamp(Math.round(width / 125), 8, 12);
const ch = cw + 2;
const cols = Math.ceil(width / cw);
Ridgelines from noise
The mountains are a height per column: a few hand-placed peaks blended with ridged noise for jagged detail. The crest glyph depends on the slope and on where the ridge line falls inside the cell, which is how gentle slopes become _.-' instead of a staircase of slashes.
Below the crest, a cell is snow if it sits above a wobbly snowline, and rock otherwise. Lighting is just the slope: faces that tilt toward the moon get denser glyphs like # and @.
The fire is a heat map
The campfire is the classic "doom fire" trick. A small grid of heat values is seeded hot at the bottom, and each row copies the row beneath it with a random sideways jitter and a little decay. Heat maps to both a glyph ramp and a color ramp:
const FLAME = ".,'^*x%#@";
const glyph = FLAME[Math.floor(heat * FLAME.length)];
Wind leans the flames by biasing that sideways jitter. Click the fire and the decay drops for a second, so the flames jump.
Static versus moving
Mountains do not move, so they are baked once into an offscreen canvas and copied each frame. Only the sky, the birds, the smoke, and the camp are drawn live. That split is the difference between a scene that hums along and one that melts a laptop.