The board layer I kept rewriting: a hex grid that knows its geometry and nothing else. Axial coordinates in, pixel polygons out, plus the two lookups every board game needs — what’s next to this cell, and how far is that cell. Gameplay data hangs off it; the grid never inspects it.
The demo is the base layer with the bare minimum on top: hover a hex to see its
coordinates and its neighbors ring up, click to move the anchor (the tint is
distance from it), O flips pointy/flat, S swaps the hexagon board for a
rectangular one, +/− resizes.
Reuse
src/hex-grid.js is a framework-free ES module:
createHexGrid({ cells, size, orientation, origin })—sizeis center-to-corner in pixels,orientationis'pointy'or'flat',originis where cell (0, 0) lands.- Cell sets come from
hexagonShape(radius)orrectangleShape(cols, rows); pass your own{q, r}array for an irregular board. - The grid exposes
toPixel,fromPixel,corners(six points, ready to stroke),has,neighbors,within(hex, range),distance, andbounds()for centering the board in a viewport. set(hex, value)/data(hex)is a thin per-cell store — terrain, units, whatever — kept out of the geometry.
Rendering is entirely the caller’s: the demo traces corners() into a canvas path,
but the same points work in SVG, and the axial math ports to DragonRuby unchanged.
Gotchas
fromPixelrounds through cube coordinates, notMath.roundon q and r — rounding the axial pair directly picks the wrong hex near the corners.rectangleShapeoffsets rows, which is the layout you want for'pointy'. Under'flat'it still tiles legally but reads as a sheared slab; offset columns instead if you need a square-looking flat-top board.bounds()walks every corner of every cell — fine at build time, don’t call it per frame on a large board.- The demo bundles its own copy of the module (self-contained by contract); re-copy
it after editing
src/.