Honeycomb: hexagon grids in JavaScript

There are plenty of JavaScript libraries offering the tools needed for rendering hexagon grids. But none (that I know of) where you can choose how the grid is rendered, or not render at all (on the server). Most libraries I could find came with canvas rendering built in or had mediocre APIs.

So, yearning for a new hobby project and some time to spare I started Honeycomb.

Basic usage

After installation (npm i -S honeycomb-grid or yarn add honeycomb-grid) you can have a grid in 2 steps:

// 1.  create a Grid factory that uses the default Hex factory:
const Grid = Honeycomb.defineGrid()
// 2a. create a rectangular grid:
Grid.rectangle({ width: 4, height: 4 })
// [
// { x: 0, y: 0 },
// { x: 0, y: 1 },
// { x: 0, y: 2 },
// …
// ]
// 2b. or create a grid from individual hexes:
const Hex = Grid.Hex
Grid(Hex(1, 2), Hex(3, 4))
// [
// { x: 1, y: 2 },
// { x: 3, y: 4 }
// ]

There’s an optional (but very likely) step 0: creating a custom Hex factory:

// 0.  create a Hex factory by extending the default:
const Hex = Honeycomb.extendHex({
size: 30, // default: 1
orientation: 'flat' // default: 'pointy'
})
// 1. create a Grid factory that uses the Hex factory:
const Grid = Honeycomb.defineGrid(Hex)

Rendering

Honeycomb doesn’t care how a grid is rendered. In fact, it’s so indifferent that it’s completely void of functionality to show hexes on a screen. Fortunately, it isn’t very hard. Especially if you use a dedicated rendering library.

Here’s an example of rendering 10,000 hexes with WebGL using PixiJS:

Rendering a 100⨉100 hex grid using PixiJS

If you prefer SVGs (like I do), here’s how you can achieve the same with SVG.js:

Rendering a 100⨉100 hex grid using SVG.js

Grids extend Array.prototype

Most properties/methods of grids are the same as their Array counterpart:

const grid = Grid.rectangle({ width: 4, height: 4 })
grid.length // 16
grid.pop() // { x: 3, y: 3 }
grid.length // 15
grid[4] // { x: 1, y: 0 }

Some Grid methods are augmented. For example: Array#includes always returns false when passed an object literal because it uses strict equality internally. Grid#includes only accepts object literals (in the form of points):

const array = [{ x: 1, y: 0 }]
array.includes({ x: 1, y: 0 }) // false
const grid = Grid(Hex(1, 0))
grid.includes({ x: 1, y: 0 }) // true

Grid methods that mutate

Methods that mutate the grid in-place (Grid#push, Grid#splice and Grid#unshift) only accept valid hexes to prevent “grid corruption” 👮‍.

const grid = Grid()             // []
// this silently fails:
grid.push('invalid hex') // 0 <- the grid's length, which remains 0
grid.includes('invalid hex') // false

Keep in mind that methods that return a new grid (e.g. Grid#map) can create grids with invalid hexes:

const grid = Grid.rectangle({ width: 4, height: 4 })
const newGrid = grid.map(hex => 'invalid hex')
// [
// 'invalid hex',
// 'invalid hex',
// 'invalid hex',
// …
// ]

Be careful with bracket notation

It’s possible to add an invalid hex to a grid by using bracket notation:

const grid = Grid(Hex())
grid[0]                     // { x: 0, y: 0 }
grid[0] = 'invalid hex'
grid[0] // 'invalid hex' ⚠️

Use Grid#get and Grid#set instead:

const grid = Grid(Hex())
grid.get(0)                 // { x: 0, y: 0 }
grid.set(0, 'invalid hex')
grid.get(0) // { x: 0, y: 0 } <- invalid hex is ignored
// Grid#set() also accepts a point:
grid.set({ x: 0, y: 0 }, Hex(-1, 3))
// …as does Grid#get():
grid.get([-1, 3]) // { x: -1, y: 3 }

Point → Hex

Translating a point (pixel) in the grid to the corresponding hex is possible with Grid.pointToHex:

Clicking a point in the grid highlights the corresponding hex

Grid shapes

Honeycomb offers 4 shape methods: rectangle, triangle, hexagon and parallelogram:

Try the 4 grid shape methods

Coordinate systems

The standard coordinate system is a cartesian one. It’s intuitive and easy to reason about. A lot of methods internally use a “cube” coordinate system. See this redblobgames.com blog post for an explanation between the two (he calls the cartesian system “offset coordinates”).

Hexes have getters for each of the cube coordinates q, r and s:

const Hex = Honeycomb.extendHex()
const hex = Hex(3, 4)
hex.q           // 1
hex.r // 4
hex.s // -5
hex.cartesian() // { x: 3, y: 4 }
hex.cube() // { q: 1, r: 4, s: -5 }

There are methods for converting between cartesian and cube:

const Hex = Honeycomb.extendHex()
const hex = Hex()
hex.toCube({ x: 3, y: 4 })      // { q: 1, r: 4, s: -5 }
// Hex#toCartesian doesn't require the s coordinate:
hex.toCartesian({ q: 1, r: 4 }) // { x: 3, y: 4 }

These methods always require coordinates to be passed and don’t work on a hex instance, even though they’re instance methods. This will be fixed in a future release 🙃

The full API documentation is available on Github.

While adding examples to the docs I’ve gained a lot of insights that I hope to apply in the coming months (a full-time job and a 1-year-old aren’t beneficial to a hobby project).

Let me know what you think about Honeycomb by leaving a comment or opening a Github issue if you have any questions or ideas.

Like what you read? Give Abbe Keultjes a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.