> Hexagon Grid

 > Projects

Home

Preface

Having recently finished my Master's degree, it is good to look back and reflect on all the pieces that compose this accomplishment. My educational journey began January 2015. This first semester consisted of working on prerequisite courses to begin working on the degree I was seeking - an Associates of Applied Sciences for web development and programming. Motivation for pursuing this was simply to learn how to code. Skill was quickly formed from that motivation.

Fall 2016 had me taking a Web Scripting course. The only scripting programming language I had prior experience with was PHP. This course would introduce Javascript. Web Scripting had us implementing simple things such as client-side form validation or simple canvas animations.

It was during this course that I started and finished a project that I am still proud of to this day. It was the first time I dug real deep to become intimate with a language while applying some other facet of my studies - The trigonometric functions learned through a math course that I was also taking at the time. These functions provided the framework to scaffold atop.

This project went beyond the expectations of a student in the Web Scripting course. It produced something tangible in terms representing data in a dynamic way. It involved implementing a lot of components that would produce more components to build the larger framework.

What is this project? It is a dynamically generated hexagonal grid which can be interacted with via mouse input. Think of the layer on the map of a user-interface for a video game like Civilization. The end result of my efforts cumulated in a JavaScript file getting close to 800 lines of code - something significant for an Alan who only had one semester prior of proper programming!

This page will discuss how this script works. Its components will be discussed on a logical level and not so much at an implementation level. This is partly due to my lack of software engineering experience at the time it was conceived. This writing will also touch on how I would approach these components should I make the script with my current understanding of programming and coding.

To skip explanation, jump to the sandbox.


Javascript: Hexagon Grid

Drawing Hexagons

The assignment which influenced this project was a to find a simple JavaScript web component and include it on a page. This would be an exercise of navigating documentation and an API to massage said plugin into a place; A very simple exercise for someone with a lot of programming experience, but a bit more complex for someone new to development in general.

Unfortunately, there did not exist a plugin that satisfied the needs for the page I was constructing. I was looking for a dynamic hexagon grid to represent information pertaining to a tabletop campaign. The solution to this conundrum? Make my own. This would be done using JavaScript's canvas API.

Luckily I was taking trigonometry at the time. Generating a hexagon takes three key values. The length of any side - S, of the hexagon; and the two sides of the triangle whose hypotenuse is the length of any side which is drawn at an angle. The side which is adjacent to the 30 degree angle of the triangle is denoted as r, and the opposite side is denoted as h. Figure A represents these values.

Figure A - The three trig variables
Figure A - The three trig variables

If one knows the value of S, the values of h and r are as follows:

  • h = (sin(30)*PI/180)*S
  • r = (cos(30)*PI/180)*S

Once these three values are calculated, drawing a hexagon is a matter of finding a starting point and walking around the perimeter using the three variables and enacting a set of canvas draw methods. This is where two more variables come in to represent the initial point of the perimeter of a given hexagon: some X and Y value on the two-dimensional plane. Following Figure A, the X and Y values are initially set to 25. This initial value represents the margin between the canvas tag and the hexagon's left-most point.

After the initialization of the X and Y values, the following logic is used to walk around the perimeter:

  • From the initial vertex, increase the x-coordinate by h and increase the y-coordinate by r. This is the bottom-left vertex.
  • From the bottom-left vertex, increase the x-coordinate's value by s. This is the bottom-right vertex.
  • From the bottom-right vertex, increase X by h and decrease Y by r. This is the middle-right vertex.
  • From the middle-right vertex, decrease X by h and decrease Y by r. This is the top-right vertex.
  • From the top-right vertex, decrease X by S and maintain the value of Y. This is the top left-vertex.
  • From the top-left vertex, decrease X by h and increase Y by r. We are now at the initial vertex.

More logic is needed to handle multiple hexagons to decide where they should lie on a grid. New values need to be established to organize this effort. The hexagons in this script are generated by establishing the amount of columns the grid has. Each subsequent hexagon is set down on a horizontal plane until the amount of hexagons in a row exceeds the amount of columns, wherein the next hexagon is kicked back to the starting x-coordinate and proper calculations are made to set it below the initial hexagon in the previous row. This is done by setting the y-coordinate to 2*r below that very hexagon.

Note that offset needs to be handled too. Consider the every-other nature of the hex-grid. The first hexagon is drawn, then logic needs to be introduced to draw the next hexagon by shifting the corresponding vertices down by r and over by h + s. The third hexagon needs to have the opposite applied to the y-coordinate to bring it back up to the same plane as the original hexagon. One last contingency needs to be made during this process: to check whether or not there is an odd or even number columns. This is relevant when a new row of hexagons starts as the previously drawn hexagon may be shifted by his offset.

Figure B - Vertex Offset

This script was developed during a time before I knew what object oriented design was. A look at the source code will make this apparent. Interestingly, the prototyping nature of JavaScript provided a scaffold for me to implicitly use some of the ideas associated with this paradigm.

Minor effort was taken to refactor the code to allow it to easily be plugged into this very web page. This refactoring essentially encapsulates all the logic into a singular grid object. A grid object in this context is composed of a set of hexagons. Indeed, drilling through the logic of the source script will reveal a quasi-constructor to produce a hexagon:

Here, naivety can be gleaned. The parameter labeled hexV can be seen as a master record of values associated with the grid object in question. This was my way of of maintaining access to these set of values to help make the decisions required for generating the relevant values of a hexagon. Indeed, (before the minor set of refactors mentioned in the paragraph prior), this information was stored as a global. If I wanted to have multiple grids in the same page, multiple copies of the script's .js file would need to be named and the hexV value be renamed to something unique. A clear lack of understanding of what the this keyword means. This was changed through the minor refactor.

The lack of understanding of the this keyword is more obviously realized in the last two statements of the above function. The set of values calculated here are pushed into arrays which maintain the data. Within these arrays it's assumed that the first entry is related to the first hexagon of the grid, the second entry is related to the second hexagon of the grid, etc. Various other methods contained in this grid will operate upon these values and parse through these arrays under such an assumption.

What exactly are these set of values? The call to hex_handler() gains access to the set of values previously discussed. It allows access to the next point of origin for the next hexagon to be placed in a grid. From this point of origin, the set of vertices associated with this new hexagon are computed and returned as an associative array:

This is where the aforementioned walk around the perimeter of a hexagon exists. The initial call to grid_handler essentially primes the X and Y coordinate previously discussed; it contains the logic that knocks into place the vertex offset described in Figure B and then makes adjustments to these values dependent on the amount of columns contained in a grid.

The key takeaway to understanding how this script works is that there exists a master array of data which contains a set of arrays and attributes. The nested arrays contain information relevant to a given layering of a function call. Looking at the function which this recursive pathis instantiated, we see two primary arrays being filled within the master array - hexV.grid and hexV.origin. The center points of each vertex get placed within the origin array. The side vertices get placed into the grid array. These two arrays will have the same length as far as this hex method is concerned. These two arrays will be operated upon concurrently.

In terms of drawing the hexagons to a canvas element, this happens within a method called drawHexes. This method primarily iterates through the associative arrays contained in hexV.grid and enacts the relevant draw method calls from the canvas API; the associative arrays that contain the side vertices for each hexagon within the grid which was determined by both the hex_handler and grid_handler oracles.

This is a bit messy. It's good to look back on old work and reflect how one has progressed in experience and knowledge. Using a strictly object oriented paradigm of programming would encapsulate these ideas much more cleanly. Regardless, I will maintain that this is very advanced for someone new to programming - having only a single month of experience in JavaScript within the classroom setting. The inclusion of code snippets will cease for the remainder of this writing for this reason.

User Interaction

What hass been discussed so far is the general logic to draw a hexagon and from this how to draw a grid of hexagons. This is done by storing vertex information for each hexagon in an array and then parsing through the array for each set of vertices while enacting a set of draw methods from the canvas API.

The fact a grid can adapt to arbitrary size values for the hexagons and column quantities for the grid isn't solely what makes it dynamic. It needs to react to user input. A grid in this context exists to display and return relevant information; the original goal of having a campaign map for a tabletop game would imply that selecting a grid should return some amount of geographic data.

Reaction to input implies a set of event handlers. The term "selecting" should key us into the usage of the mousedown event. What's also at play is a change of rendering whilst navigating the mouse through the grid to a target of for selection. The grid informs the user which hexagon will be selected based on their mouse position, as opposed to what has been selected. This is accomplished through the mousemove event.

For these two events, both event handlers have a general common principal. The event is first associated with the html canvas element in which the grid exists. When the event is fired, the coordinates of the mouse position are retrieved from the event and then normalized with respect to the position of the canvas element and the size of the canvas element as it currently exists in the DOM. These normalized mouse coordinate values are then passed to drawHexes along with a switch condition to allow it to know which event type should be acted on.

Before drawHexes is called in this context, the entire canvas is cleared such that it can be redrawn. In the case where the mousedown event was triggered, the fill color for the hexagon that is being selected will differ from the rest as the canvas is being redrawn. In the case where the mousemove event was triggered, the border of the hexagon in which the mouse is hovering will differ.

Both these cases take advantage of the normalized mouse coordinates being passed. Recall that the master record of a grid contains two arrays of vertex data: one which contains the side vertices of a hexagon and another that contains the points of origin. A method called getselectedHex was developed to leverage the array that houses the points of origin while comparing them to these normalized mouse coordinates.

Figure C - Mouse-over distance formula illustration

What is the comparison being made in getselectedHex? This method runs through the points of origin applying the distance formula between each point and the normalized mouse coordinates whilst maintaining a minimum value along with the point of origin that generated it. After finding the minimum, the relevant point is annotated for the sake of usage in drawHexes; the method now knows which hexagon's point is closest to the mouse cursor.

Thus far it's been established that to draw a hexagon requires a set of vertices. To draw a set of hexagons requires a mechanism to decide where the center point of a hexagon lies. To know which hexagon an entity is closest to requires evaluating the distance formula between each of these points of origin and the entity.* One crucial piece of logic remains: knowing when the cursor is not over any of the hexagons.

Figure C - Mouse-over distance formula illustration

Consider figure C (above). This figure is an instance of the hexagon grid. It has a hexagon count of 1. Whilst mousing over figure C, seven lines are drawn from seven points of origin to the mouse cursor; one line associated with the hexagon's point of origin and the other six associated with some point of origin outside the boundary of a given side of the hexagon. The general algorithm described thus far accounts for comparing points of origins with respect to some instantiated hexagon, but the boundary of the grid itself needs to be considered. If the boundary of the grid is not considered, then moving the mouse onto the white space that exists between a border hexagon and the canvas element will not remove the highlight of the previous hexagon that was hovered over. This is is behavior is shown in figure D (below).

Figure D - Figure C without adjacent points of origin
Figure D - Figure C without adjacent points of origin

The grouping of logic that addresses this issue is contained in a method called calculateAdjacentOrigins. This function exists to insert a set of points into the origin array such that these new points, (which exist beyond the perimeter of the hexagon grid), can be factored for the distance formula calculation. Should the result of this calculation produce a point that exists within this range, the graph will draw correctly by not highlighting any hexagon.

There are a set of three primary cases that calculateAdjacentOrigins operates on. We've seen the first - a grid that is composed of a single hexagon. Here, the six extra vertices pushed to the origin array are calculated, (where the hexagon's point of origin is (x,y)), as follows:

  • Point of origin above the top-most side of the hexagon: (x, y - (2 * r))
  • Point of origin below the bottom-most side of the hexagon: (x, y + (2 * r))
  • Point of origin associated with the top-right side of the hexagon: (x + (2.5 * S), y - r)
  • Point of origin associated with the top-left side of the hexagon: (x - (0.5 * S), y - r)
  • Point of origin associated with the bottom-right side of the hexagon: (x + (2.5 * S), y + r)
  • Point of origin associated with the bottom-left side of the hexagon: (x - (0.5 * S), y + r)

This exposes us to a set of paths that need to be considered whilst building the other primary cases of calculateAdjacentOrigins: A point of origin needs to be considered with respect to a hexagon's side dependent on where it sits along the border. That is if it sits on the border.

The next primary case for calculateAdjacentOrigins is the case in which a hexagon grid consists of a single row of hexagons. That is, if the hexagon count does not exceed the amount of columns in a grid. This case builds an edge case for the first and last hexagon of this singular row. A set of adjacent points of origin need to be put into the grid array that are associated with the first hexagon's top-left and bottom-left side. The same needs to be done for the last hexagon's top-right an bottom-right side. Every hexagon needs to have an adjacent point of origin associated with its top and bottom sides.

The last case of consideration for calculateAdjacentOrigins is for grids that contain many rows. Effort is taken here to ensure no redundant points of origin are generated for hexagons that don't exist on the border of the grid. Special cases are considered in terms deciding whether or not points of origin should be generated with respect to the top or bottom side of a given hexagon, which is dependent on whether or not a hexagon is contained in the first row or the last row.

We now have all the major pieces of logic that allow this hexagon grid to exist. To recap, the major components are hexagon objects with a set of arrays that store the side-vertices of a hexagon and the center points of these hexagons. The center points of would-be hexagons that exist outside the border of a grid are also stored and considered whilst determining whether the mouse is hovering over the grid itself.

Figure E - Select a hex...

Figure E is another instance of the hexagon grid. Contrary to prior figures, it is composed of more than one row of hexagons. Recall the creation of a hexagon object. It accepted a set of parameters, one of which was labeled as name. Selecting a hexagon through figure E will print the selected hexagon's name within the caption of the figure. This should imply that data that may be associated with a hexagon is arbitrary; it is arbitrarily extensible.

Figure E - Select a hex...

Figure E also implicitly informs the usage of another parameter associated with the hex object: type. There are two primary types of hexagons as far as the grid is concerned. Thus far only regular hexagons have been discussed - those which are rendered normally. The other type is a null hex. These hexagons aren't rendered within given pass of the drawHexes method. Their side vertices are ignored. These allow a user to include gaps in the grid, as shown in Figure E.

Sandbox:

Below is a sandbox to play around with the grid to get a good sense of what it's capable of. This will allow one to experience the logic described above. A user can determine an arbitrary column length and hexagon size. They can also add an arbitrary amount of hexagons in addition to adding gaps to the grid.

Grid Controls:











close [x]

Grid Controls:












Concluding notes

I've learned a lot since initially making this script. This hex-grid was developed before a time I even knew what object-oriented programming was. The patterns I employed to solve this task were based on primitive notions that I was only aware of at the time. Going back and reaquainting myself by documenting this old code-base will make it likely that this project will be refactored into a packagable web component sometime in the future.

If curious, one can view the unedited version of the script can happen via this website's repository, specifically here at commit hash 644aa33. Note that the refactoring I've done since is fairly minimal. To track the the updates to this script since this initial commit, one can do so here,