A CLI tool that generates hexagonal grid tessellation images. Uses flat-top hexagons on an axial coordinate system with supersampled anti-aliasing.
Runs as a standalone Windows .exe - no Python installation required.
I created hextessellator.exe to generate transparency masks that I use for graphic design projects, like the one below.
As a worked example, the mask above can be created with hextessellator.exe using either one of two workflows.
- Command line arguments.
- Writing and importing a JSON settings file.
I'll show both approaches below. You can use either approach, which ever feels more comfortable.
- πΌοΈ Examples
- β¨οΈ Command Line Arguments Workflow
- π JSON Settings File Workflow
- π Quick Start
- π CLI Reference
- ποΈ Architecture
- π Run from Source
- π¨ Build the Executable
- π Project Structure
- π Mathematical Foundations
- π·οΈ Versioning
- π License
-
Execute the following command in Windows CMD or PowerShell:
hextessellator --width 1600 --height 1000 --circumradius 144 --margin 48 --line_width 0 --color_fill white --color_background black --antialias high --cullThis will generate an image with the default file name,
tessellation.png.
You can use the--fileargument to set your own file name. -
Use your preferred image manipulation program to manually remove unneeded hexagons.
-
Write a JSON settings file, and place it in the same folder as
hextessellator.exe.Note:
Not all the properties in the JSON file below will be used for this example, but I put all the fields in there for the sake of convenience anyway, so you can use the file as a general template.settings-example.json{ "width": 1600, "height": 1000, "circumradius": 144.0, "margin": 48.0, "line_width": 0, "layers": 0, "color_fill": "white", "color_line": "black", "color_background": "black", "antialias": "high", "file": "tessellation.png", "cull": true, "debug": false } -
Execute the following command in Windows CMD or PowerShell:
hextessellator --import_settings settings-example.jsonThis will generate an image with the file name specified by the
filefield,tessellation.pngin this case. -
As in the case of the previous worked example, use your preferred image manipulation program to manually remove unneeded hexagons.
Download hextessellator.exe and run from the Windows Command Prompt:
REM Generate with default image (1024x768, high anti-aliasing)
hextessellator.exe
REM Custom render
hextessellator.exe --width 1920 --height 1080 --circumradius 48 --antialias high --debug
REM Save/load settings
hextessellator.exe --export_settings my_config.json
hextessellator.exe --import_settings my_config.json| Argument | Type | Default | Description |
|---|---|---|---|
--width |
int | 1024 | Image width in pixels |
--height |
int | 768 | Image height in pixels |
--circumradius |
float | 64.0 | Hexagon circumradius R in pixels |
--margin |
float | 16.0 | Gap between hexagon edges in pixels |
--line_width |
int | 8 | Stroke width in pixels (0 = no outline) |
--layers |
int | 0 | Concentric layers (0 = auto-fill canvas) |
--color_fill |
str | grey | Hexagon fill colour |
--color_line |
str | black | Hexagon outline colour |
--color_background |
str | darkgrey | Background colour |
--antialias |
str | high | Anti-alias level: off, low, medium, high |
--file |
str | tessellation.png | Output PNG filename |
--cull |
bool | false | Enable viewport culling |
--debug |
bool | false | Print detailed parameters and statistics |
--export_settings |
str | Export parameters to a JSON file | |
--import_settings |
str | Import parameters from a JSON file |
Colours accept CSS named colours (red, steelblue), hex codes (#FF0000, #F00), and comma-separated RGB (255,128,0).
All application code resides in a single file (src/main.py) with six classes:
Application ...CLI parsing, orchestration, banner, debug output.
ββ SettingsManager ...JSON import/export with CLI-precedence merging.
ββ ColorParser ...CSS names, hex codes, RGB tuples -> (R,G,B).
ββ TessellationRenderer
ββ AxialGrid ...Coordinate system, ring traversal, auto-fill.
ββ HexagonGeometry ...Vertex computation for flat-top hexagons.
Requires Python 3.10+.
REM Clone
git clone https://github.com/rohingosling/hex-grid-tessellator.git
cd hex-grid-tessellator
REM Create and activate a virtual environment
python -m venv .venv
.venv\Scripts\activate
REM Install runtime dependencies
pip install -r requirements.txt
REM Generate (defaults: 1024x768, high anti-aliasing)
python -X utf8 src/main.py --debugOn Windows you can also use the launcher:
run.bat --debugBuilding the standalone Windows .exe needs the dev/build dependencies:
.venv\Scripts\python.exe -m pip install -r venv_requirements.txt
.venv\Scripts\python.exe scripts\build.pybuild.py stamps the version and build date, compiles via PyInstaller using hextessellator.spec, and smoke-tests the result. The executable is output to dist/hextessellator.exe.
hex-grid-tessellator/
+-- src/
| +-- main.py Application (single file, 6 classes)
+-- scripts/
| +-- build.py Build automation (version stamp, PyInstaller, smoke test)
+-- images/ Example output images shown in this README
+-- hextessellator.spec PyInstaller build spec
+-- requirements.txt Runtime dependency (Pillow)
+-- venv_requirements.txt Dev/build dependencies (PyInstaller, pytest)
+-- run.bat Quick-launch script
+-- activate.bat venv activation script
+-- build.bat Build script
+-- README.md This file
+-- LICENSE MIT License
+-- THIRD_PARTY_LICENSES.md Third-party license notices
A regular hexagon is a six-sided polygon with all sides and interior angles equal. The tool uses the flat-top orientation, where the topmost edge is horizontal.
The hexagon is defined by its circumradius R (centre-to-vertex distance). The inradius (apothem) r is the centre-to-edge distance:
The relationship between circumradius and inradius follows directly from the 30-60-90 triangle formed by the centre, a vertex, and the midpoint of an adjacent edge.
For a flat-top hexagon centred at (c_x, c_y), the six vertices are computed by sampling the circumscribed circle at 60Β° intervals:
where k = 0 corresponds to the rightmost vertex at (c_x + R, c_y) and vertices proceed counter-clockwise.
The grid uses the axial coordinate system (also called "trapezoidal" or "skewed") for hexagonal grids, parameterised by two integers (q, r).
For flat-top hexagons with spacing radius R_s, the axial-to-pixel transform is:
The final pixel position is offset to the canvas centre:
where W and H are the canvas dimensions.
The factor 3/2 in the x-component arises because adjacent flat-top hexagons overlap horizontally by R/2 (each hexagon spans 2R horizontally but the centre-to-centre distance is only 3R/2). The q/2 term in the y-component accounts for the vertical stagger of alternating columns.
The layout spacing radius separates the geometric drawing radius from the grid layout radius, creating uniform gaps between hexagons:
where m is the margin (gap width) in pixels. Each hexagon is drawn at circumradius R but positioned on the grid at spacing R_s, producing a uniform margin of m between adjacent edges.
The grid is generated as concentric rings emanating from the origin. Ring d contains all cells at hex distance d from (0, 0).
For d = 0, the ring contains only the origin. For d >= 1, the ring contains exactly 6d cells, enumerated by starting at axial coordinate (-d, 0) and walking d steps along each of six canonical directions:
RING(d):
if d = 0:
return [(0, 0)]
cells <- []
(q, r) <- (-d, 0)
for each (dq, dr) in DIRECTIONS:
repeat d times:
append (q, r) to cells
q <- q + dq
r <- r + dr
return cells
A concentric grid of L layers (layer 1 = origin, layer l >= 2 = ring at distance l - 1) contains:
This is the centred hexagonal number sequence: 1, 7, 19, 37, 61, 91, ...
When the layer count is set to zero, the tool computes the minimum layers needed to cover the canvas:
The +1 buffer ensures edge hexagons overlap the canvas boundary, preventing visible gaps at the periphery.
Hexagon outlines are rendered using a two-pass fill technique that avoids the miter-joint artifacts produced by Pillow's built-in polygon(outline=).
Given circumradius R and stroke width w, the two fill passes use:
TWO_PASS_RENDER(centres, R, w, fill_colour, stroke_colour):
R_outer <- R + w/2
R_inner <- max(R - w/2, 0)
// Pass 1: draw ALL hexagons at R_outer in stroke colour
for each (cx, cy) in centres:
polygon(vertices(cx, cy, R_outer), fill=stroke_colour)
// Pass 2: draw ALL hexagons at R_inner in fill colour
if R_inner > 0:
for each (cx, cy) in centres:
polygon(vertices(cx, cy, R_inner), fill=fill_colour)
The critical detail is that all outer polygons are drawn before any inner polygon. This ensures that the stroke region of one hexagon is never partially occluded by the fill of an adjacent hexagon, producing clean, uniform borders across the entire grid. If the passes were interleaved per-hexagon, overlapping stroke regions would create visible artifacts at the joints between adjacent hexagons.
When w = 0 (no outline), a single pass at circumradius R is used instead.
Hexagons whose geometry extends beyond the canvas boundary are discarded before rendering. The cull test uses an expanded radius to account for the stroke:
A hexagon centred at (c_x, c_y) is retained if and only if all six vertices of the hexagon at radius R_cull fall within the canvas:
This is a conservative test: it discards any hexagon that would partially extend beyond the canvas edge, ensuring no partially-drawn hexagons appear at the boundary.
The tool implements supersampled anti-aliasing (SSAA) by rendering at an integer multiple of the target resolution, then downsampling with a high-quality filter.
| Level | Scale Factor k | Render Resolution |
|---|---|---|
| off | 1x | W x H |
| low | 2x | 2W x 2H |
| medium | 4x | 4W x 4H |
| high | 8x | 8W x 8H |
All geometry (circumradius, margin, line width, canvas dimensions) is scaled by k before rendering:
The tessellation is rendered at the enlarged resolution (W' x H'), then downsampled to (W x H) using a Lanczos filter (sinc-windowed sinc interpolation). This effectively computes a weighted average of k^2 supersamples per output pixel, smoothing polygon edges and reducing aliasing.
The Lanczos resampling kernel is:
where sinc(x) = sin(pi x) / (pi x) and a is the window size (typically 3 for Lanczos-3 as used by Pillow).
This project uses Semantic Versioning:
- MAJOR (X): Breaking changes to CLI arguments or settings JSON format
- MINOR (Y): New features, backwards-compatible
- PATCH (Z): Bug fixes, documentation, non-functional improvements
This project is licensed under the MIT License. Third-party components and their licenses are listed in THIRD_PARTY_LICENSES.md.












