Hex Map Generator

An interactive hex map canvas with configurable grid size, pan and zoom, viewport culling, fullscreen mode, terrain generation, line-of-sight simulation, pathfinding, and measurement tools. Built entirely client-side. Visit /playground/hex-map.

Architecture

The engine is entirely client-side JavaScript in a single file (static/js/hex-map.js, ~1500 lines). The server provides:

  1. The template (templates/hex-map.html) — page layout with size controls, canvas, and status bar.
  2. The game script (static/js/hex-map.js) — hex math, rendering, interaction, terrain generation, and game logic helpers.
  3. The route (src/routers/hex_map.py) — GET /playground/hex-map serves the template.

Hex Grid

Orientation

Pointy-top hexagons with a circumradius of 32px. The grid uses odd-r offset coordinates — odd-numbered rows are shifted right by half a hex to form the honeycomb pattern.

Coordinate System

Internally, hexes are stored as {col, row} in odd-r offset coordinates. Pixel positions are calculated directly from offset coordinates:

x = SQRT3 * HEX_SIZE * (col + 0.5 * (row & 1))
y = 1.5 * HEX_SIZE * row

With padding offsets added to centre the grid within the canvas.

Hex Math Helpers

All helpers are exported in the script and documented inline:

Function Purpose
hexCenter(col, row) Returns pixel centre {x, y} of a hex
hexCorners(cx, cy) Returns 6 corner vertices [{x,y},...] for drawing
hexNeighbor(col, row, dir) Returns adjacent hex in direction 0–5 (corrected for odd/even rows)
hexDistance(h1, h2) Cube distance: (|dq| + |dr| + |ds|) / 2
hexInRange(center, radius) All hexes within N steps (hexagonal area)
hexLine(a, b) Bresenham-style line in cube coordinates — all hexes intersected by the line from a to b
hexLOS(obs, target, maxRange) Line-of-sight check with elevation
hexPathAStar(start, end) A* shortest path avoiding blocked hexes

Viewport Culling

Only hexes whose bounding boxes intersect the visible world rect (computed from the canvas transform) are rendered. For a 20×20 grid this is a minor optimisation; for 100×100 grids (~10,000 hexes) it keeps the framerate smooth during pan and zoom.

Rendering Pipeline

The render() function draws in this order:

  1. Canvas background--bs-body-bg
  2. Nebula backdrop (space terrain only) — cached offscreen canvas with layered radial gradients
  3. Hex grid (first pass) — all non-hill hexes: fills (dustbowl floor, blocked, LOS, measure, select, path, observer), then borders at configurable opacity
  4. Hill hexes (second pass) — sand fill, LOS overlays, then per-edge borders (inner edges at 1px, outer edges at 4.5px)
  5. Terrain features — building zones (grey fill), roads (grey fill + connecting line), asteroids/rocks/mountains (polygon fills + outlines)
  6. LOS overlays (final pass) — green tint for visible, red tint for hidden, on top of all terrain
  7. Measure line — dashed line with distance label
  8. Bottom bar — mode buttons, action buttons, range/opacity controls, hex coordinate info

All colours are theme-aware via CSS custom properties except LOS overlays, which use fixed rgba() values for consistency across light and dark themes.

Modes

View Mode

Click a hex to select it (highlighted). Hover updates the hex coordinate display in the bottom bar.

Measure Mode

Click hex A, then hex B. A dashed line is drawn between them with the distance label. The status bar shows the distance in hex steps.

Paint Mode

Click a hex to toggle it as blocked. Blocked hexes show a red tint with a square icon. They are avoided by pathfinding and block line of sight.

LOS Mode

Click a hex to place the observer (blue circle marker). All hexes within the configured range are evaluated:

Result Colour Meaning
Visible Green tint Unobstructed line of sight
Hidden Red tint Line is blocked by terrain
Out of range Unchanged Beyond Range: N setting

Elevation: Hills are height 1, flat hexes are height 0. An observer on a hill can see over flat ground, but other hill hexes at the same height between observer and target will block the view. The LOS algorithm uses height interpolation along the line from observer to target.

Controls: [−] Range: N [+] in the bottom bar adjusts the sight range from 1 to 20.

Path Mode

Click a start hex (green S marker), then an end hex (red E marker). The shortest path avoiding all blocked hexes is calculated using A* and displayed with a connecting line and highlighted hexes.

Toggle [Var:On] / [Var:Off] to randomise the neighbor search order, producing varied paths when multiple equal-cost routes exist.

Terrain Mode

Generates and manages procedural terrain features. Three actions are available when Terrain mode is active:

Button What it does
Clear All Removes all terrain features, unblocks all hexes
Gen Dustbowl Generates organic hills, roads, and building zones
Gen Space Generates asteroid fields with a nebula backdrop

Terrain Types

Dustbowl

A desert-like map with the following features generated procedurally:

Hills: Organic clusters created using probability-based BFS expansion from a random seed. Each hill starts at a centre hex and expands outward with: - Base probability decreasing with distance (max radius 2–5) - A random ridge direction biasing expansion along one axis - Random per-hex noise for irregular edges

Hill hexes are rendered with a sand-coloured fill, thick outer-edge borders (4.5px), and thin inner-edge borders (1px). They are marked as blocked — pathfinding and LOS treat them as obstacles.

Roads: 3–6 entry points selected from the four map edges (biased toward the middle of each edge) and connected via A* pathfinding. Roads avoid hill hexes and are paired between different edges. Rendered as a grey hex fill with a thick grey connecting line, extended to the map edge at entry/exit points.

Building Zones: Grey clusters adjacent to roads, created via probability-based BFS from road-adjacent candidate hexes. Rendered as a subtle grey hex fill (15% opacity). Up to 8 zones per generation, each a minimum of 3 hexes.

Space

Asteroid field with a nebula backdrop:

Asteroids: 40–70 jagged random polygons (5–12 vertices) scattered across the map. Each asteroid has a randomly assigned grey shade stored at generation time (not computed per frame). Rendered as grey fills with a subtle bright edge. Hexes covered by asteroids are blocked.

Nebula Backdrop: Created once per generation with 30–50 coloured blobs (purple, blue, pink) using layered radial gradients on an offscreen canvas. Cached and drawn as a single drawImage per frame. The base fill is rgba(5,5,15,0.5) — dark enough to suggest space while remaining transparent enough to show the theme background.

Interaction

Pan & Zoom

Input Action
Mouse drag Pan
Mouse wheel Zoom (centred on cursor position)
Touch drag Pan
Two-finger pinch Zoom (centred on midpoint)
Zoom range 0.1× to 5×

Zoom centres on the cursor by converting the cursor position to world coordinates, scaling the zoom factor, then converting back to screen coordinates. This keeps the point under the cursor stationary during zoom.

Fullscreen

The fullscreen button (top-right overlay on the canvas) calls wrap.requestFullscreen(). A CSS class hex-fullscreen is added to <html>, which hides the navbar, breadcrumb, and control bar — only the canvas fills the screen. Pressing Escape exits fullscreen and removes the class.

Bottom Bar

All controls are rendered on the canvas in screen space (not DOM elements), ensuring they remain visible and functional in fullscreen mode:

[View][Measure][Paint][LOS][Path][Terrain]  [Clear All] [Gen Dustbowl] [Gen Space]  [Var:Off]  Range:5 [-] [+]  Line:100% [-] [+]  ...  Hex (3, 5)
  • Mode buttons switch the active interaction mode
  • Contextual action buttons appear depending on the mode (terrain actions, variance toggle)
  • Range and opacity controls adjust LOS sight range and hex border opacity (5%–100%)
  • The hex coordinate display shows the current hover hex and mode-specific info

Files

File Role
templates/hex-map.html Game template (layout, controls, canvas, styles)
static/js/hex-map.js Game engine (hex math, rendering, interaction, terrain generation)
src/routers/hex_map.py GET /playground/hex-map
templates/playground.html Playground index page with Hex Map Generator card
src/main.py Router registration