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:
- The template (
templates/hex-map.html) — page layout with size controls, canvas, and status bar. - The game script (
static/js/hex-map.js) — hex math, rendering, interaction, terrain generation, and game logic helpers. - The route (
src/routers/hex_map.py) —GET /playground/hex-mapserves 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:
- Canvas background —
--bs-body-bg - Nebula backdrop (space terrain only) — cached offscreen canvas with layered radial gradients
- Hex grid (first pass) — all non-hill hexes: fills (dustbowl floor, blocked, LOS, measure, select, path, observer), then borders at configurable opacity
- Hill hexes (second pass) — sand fill, LOS overlays, then per-edge borders (inner edges at 1px, outer edges at 4.5px)
- Terrain features — building zones (grey fill), roads (grey fill + connecting line), asteroids/rocks/mountains (polygon fills + outlines)
- LOS overlays (final pass) — green tint for visible, red tint for hidden, on top of all terrain
- Measure line — dashed line with distance label
- 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 |