---
name: "canvas-tilemap-renderer"
description: "Procedural terrain generation with tile rendering, camera panning, and minimap overlay. Advanced HTML5 Canvas tutorial."
compatibility: "Designed for claude-code, cursor, copilot"
metadata:
  version: "1.0.0"
  runtime:
    language: "html"
    entrypoint: "tilemap.html"
disable-model-invocation: true
---

# Canvas Tilemap Renderer

> **Difficulty:** Advanced | **Runtime:** HTML5 Canvas | **Output:** Interactive tilemap

Teach an AI agent to generate procedural terrain, render it as a colored tile grid, implement camera panning, and display a minimap overlay — all in a single HTML file.

## What You'll Build

A self-contained HTML file featuring:
- Procedural terrain generation using seeded random noise
- Tile rendering with distinct terrain types (grass, water, sand, trees, mountains)
- Arrow-key camera panning across a world larger than the viewport
- A minimap overlay showing the full map with a camera indicator
- Configurable world size and tile palette

## Core Concepts

### 1. Terrain Generation

Use a simple value-noise approach:
1. Create a 2D array of random values (seeded)
2. Smooth with neighbor averaging
3. Map value ranges to terrain types

| Value Range | Terrain | Color |
|------------|---------|-------|
| 0.00–0.30 | Water | `#2563eb` |
| 0.30–0.45 | Sand | `#eab308` |
| 0.45–0.65 | Grass | `#16a34a` |
| 0.65–0.80 | Trees | `#15803d` |
| 0.80–1.00 | Mountain | `#78716c` |

### 2. Camera System

The viewport shows a portion of the world:
```
camera = { x: 0, y: 0 }
visible_cols = floor(canvas.width / TILE_SIZE)
visible_rows = floor(canvas.height / TILE_SIZE)
```

Arrow keys shift the camera. Clamp to world bounds.

### 3. Minimap

A small (150x150) overlay in the corner:
- Draws each tile as a 1-2px dot
- Highlights the camera's current viewport as a white rectangle
- Updates every frame

### 4. Rendering Pipeline

Each frame:
1. Calculate visible tile range from camera position
2. Draw only visible tiles (culling)
3. Draw grid lines (optional)
4. Draw minimap overlay
5. Draw HUD (coordinates, terrain info)

## Instructions

1. Create the canvas and set up keyboard input
2. Generate the world map using seeded noise
3. Implement tile rendering with camera offset
4. Add arrow-key panning with bounds checking
5. Draw the minimap in the top-right corner
6. Display camera coordinates as HUD

## Parameters

| Parameter | Default | Description |
|-----------|---------|-------------|
| `WORLD_W` | 100 | World width in tiles |
| `WORLD_H` | 80 | World height in tiles |
| `TILE_SIZE` | 12 | Tile size in pixels |
| `SEED` | 42 | Random seed for terrain |
| `SMOOTH_PASSES` | 3 | Noise smoothing iterations |

---

## Files

### `tilemap.html`

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Canvas Tilemap Renderer</title>
<style>
  * { margin: 0; padding: 0; }
  body { background: #0a0a0a; overflow: hidden; }
  canvas { display: block; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const WORLD_W = 100, WORLD_H = 80;
const TILE_SIZE = 12;
const SMOOTH_PASSES = 3;
const MINIMAP_SIZE = 150;
const MINIMAP_PAD = 10;

// Seeded PRNG (simple LCG)
let seed = 42;
function seededRandom() {
  seed = (seed * 1664525 + 1013904223) & 0xFFFFFFFF;
  return (seed >>> 0) / 0xFFFFFFFF;
}

const TERRAIN = [
  { max: 0.30, color: '#2563eb', name: 'Water' },
  { max: 0.45, color: '#eab308', name: 'Sand' },
  { max: 0.65, color: '#16a34a', name: 'Grass' },
  { max: 0.80, color: '#15803d', name: 'Trees' },
  { max: 1.00, color: '#78716c', name: 'Mountain' },
];

function getTerrainType(val) {
  for (const t of TERRAIN) {
    if (val <= t.max) return t;
  }
  return TERRAIN[TERRAIN.length - 1];
}

// Generate world
seed = 42;
let world = Array.from({ length: WORLD_H }, () =>
  Array.from({ length: WORLD_W }, () => seededRandom())
);

// Smooth
for (let pass = 0; pass < SMOOTH_PASSES; pass++) {
  const next = world.map((row) => [...row]);
  for (let y = 1; y < WORLD_H - 1; y++) {
    for (let x = 1; x < WORLD_W - 1; x++) {
      let sum = 0, count = 0;
      for (let dy = -1; dy <= 1; dy++) {
        for (let dx = -1; dx <= 1; dx++) {
          sum += world[y + dy][x + dx];
          count++;
        }
      }
      next[y][x] = sum / count;
    }
  }
  world = next;
}

// Camera
const camera = { x: 0, y: 0 };
const keys = {};

document.addEventListener('keydown', (e) => { keys[e.key] = true; });
document.addEventListener('keyup', (e) => { keys[e.key] = false; });

function updateCamera() {
  const speed = 3;
  if (keys['ArrowLeft'] || keys['a']) camera.x -= speed;
  if (keys['ArrowRight'] || keys['d']) camera.x += speed;
  if (keys['ArrowUp'] || keys['w']) camera.y -= speed;
  if (keys['ArrowDown'] || keys['s']) camera.y += speed;

  const maxX = WORLD_W * TILE_SIZE - canvas.width;
  const maxY = WORLD_H * TILE_SIZE - canvas.height;
  camera.x = Math.max(0, Math.min(maxX, camera.x));
  camera.y = Math.max(0, Math.min(maxY, camera.y));
}

function drawTiles() {
  const startCol = Math.floor(camera.x / TILE_SIZE);
  const startRow = Math.floor(camera.y / TILE_SIZE);
  const endCol = Math.min(WORLD_W, startCol + Math.ceil(canvas.width / TILE_SIZE) + 1);
  const endRow = Math.min(WORLD_H, startRow + Math.ceil(canvas.height / TILE_SIZE) + 1);

  for (let r = startRow; r < endRow; r++) {
    for (let c = startCol; c < endCol; c++) {
      const terrain = getTerrainType(world[r][c]);
      const px = c * TILE_SIZE - camera.x;
      const py = r * TILE_SIZE - camera.y;
      ctx.fillStyle = terrain.color;
      ctx.fillRect(px, py, TILE_SIZE, TILE_SIZE);
    }
  }
}

function drawMinimap() {
  const mx = canvas.width - MINIMAP_SIZE - MINIMAP_PAD;
  const my = MINIMAP_PAD;
  const scaleX = MINIMAP_SIZE / WORLD_W;
  const scaleY = MINIMAP_SIZE / WORLD_H;

  // Background
  ctx.fillStyle = 'rgba(0,0,0,0.7)';
  ctx.fillRect(mx - 2, my - 2, MINIMAP_SIZE + 4, MINIMAP_SIZE + 4);

  // Tiles
  for (let r = 0; r < WORLD_H; r++) {
    for (let c = 0; c < WORLD_W; c++) {
      const terrain = getTerrainType(world[r][c]);
      ctx.fillStyle = terrain.color;
      ctx.fillRect(mx + c * scaleX, my + r * scaleY, Math.ceil(scaleX), Math.ceil(scaleY));
    }
  }

  // Camera viewport indicator
  const vx = mx + (camera.x / TILE_SIZE) * scaleX;
  const vy = my + (camera.y / TILE_SIZE) * scaleY;
  const vw = (canvas.width / TILE_SIZE) * scaleX;
  const vh = (canvas.height / TILE_SIZE) * scaleY;
  ctx.strokeStyle = '#fff';
  ctx.lineWidth = 1.5;
  ctx.strokeRect(vx, vy, vw, vh);
}

function drawHUD() {
  const col = Math.floor((camera.x + canvas.width / 2) / TILE_SIZE);
  const row = Math.floor((camera.y + canvas.height / 2) / TILE_SIZE);
  const terrain = (row >= 0 && row < WORLD_H && col >= 0 && col < WORLD_W)
    ? getTerrainType(world[row][col]) : { name: '?' };

  ctx.fillStyle = 'rgba(0,0,0,0.6)';
  ctx.fillRect(8, canvas.height - 32, 280, 24);
  ctx.fillStyle = '#ccc';
  ctx.font = '12px monospace';
  ctx.fillText(`Pos: (${col}, ${row})  Terrain: ${terrain.name}  [Arrow keys to pan]`, 14, canvas.height - 15);
}

function loop() {
  ctx.fillStyle = '#0a0a0a';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  updateCamera();
  drawTiles();
  drawMinimap();
  drawHUD();
  requestAnimationFrame(loop);
}

window.addEventListener('resize', () => {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
});

loop();
</script>
</body>
</html>
```

### `README.md`

```md
# Canvas Tilemap Renderer

A procedurally generated tilemap with camera panning and minimap overlay.

## Running

Open `tilemap.html` in any modern browser. Use arrow keys or WASD to pan the camera across the terrain.

## Terrain Types

| Color | Terrain |
|-------|---------|
| Blue | Water |
| Yellow | Sand |
| Green | Grass |
| Dark Green | Trees |
| Gray | Mountain |

## Customization

Edit the constants in the script:
- `WORLD_W` / `WORLD_H` — world dimensions in tiles
- `TILE_SIZE` — pixel size per tile
- `seed` — change for different terrain layouts
- `SMOOTH_PASSES` — more passes = smoother terrain
```
