Joel.pm The Blog

Script: Tesselating hexagons of random shades for a background


UPDATE: hexagons.js has been superceded by the massively superior tess.js

This post is in 3 acts. Act 1 is How it came to be. Act 2 (which might be the only one you're after) is How to use it. Act 3 is the nitty-gritty How it works. Experts in canvas tags or good code might want to either avoid that one to save yourself, or read it and laugh at me.

How it came to be

I recently redesigned my website, which is fun. Generally, I get to about 80% done, and my free time runs out, so I just push it in that almost-done state. Then, once more free time comes around, I have gone off my old design, and so I start again.

For this new design, I decided that, on the homepage, I wanted one of those big full-page intro slides of sorts, where you scroll down and then see the rest of the page. This needed to have some sort of repeating background. So initially I jumped into mspaint and created a dodgy tessellating pattern in white and grey as a placeholder (because I wanted to get the rest of the site done).

When I came back to it, my mind was on hexagons. I wanted them light grey, and to be randomly coloured. I started playing in Hexels, which is a painting tool that's very simple in its idea, but can create some awesome looking pixel art. I am not an artist. Also, when I made a reasonably-large random hexagon area to tessellate, it was causing problems when trying to make a pattern that tessellates properly.

So, I decided to write code to create it live, randomly every time. Good idea

...server-side, in PHP. BAD idea.

Now, I was more than aware that this would be an issue, but I am currently more comfortable in PHP than in JavaScript (something I'd like to rectify) so I decided to initially write it on PHP anyway. I'd rather not share that code, because it's bad. The JS code that I switched to is slightly less bad, and that's what this post is about.

How to use it

If you'd like an element to have a hexy background just like the one I have in the header of this site, with any range of shades (not neccessarily the light grey I'm using) then listen in!

First, download hexagons.js from my site and put it on your own. Please don't hotlink it from my site. It's CC-BY-SA 4.0, meaning you're free to share it the same way that I do, or modify it, as long as you keep the comment at the top mentioning me as the author and mentioning the license.

Then add this as a script tag inside of the head of your site. Something like this (you may want to change the url if you have it in a different folder):

<script src="/js/hexagons.js"></script>

Then, at the bottom of your <body> tag, add the folowing one line of JavaScript:

<script>
FillHexBoxes();
</script>

hexagons.js will now run. But you have no elements that are filled with it. So you need to tell hexagons.js which elements to use. This involves adding the class hexbox to the element. You also need to add three 'data attributes'. Let me show you an example, using my own homepage's header screen.

<div class="hero-container hexbox" data-min-grey="230" data-max-grey="250" data-hex-size="15">
    <!-- Optional content of the div -->
</div>

I already have one class, hero-container, which is assigning things in CSS. Obviously, you can use whatever classes you like to size your div boxes. The crucial thing is that the hexbox class is added. Additionally, there are three data attributes. data-min-grey and data-max-grey are the minimum and maximum shade allowed, between 0 (black) and 255 (white). data-hex-size is the size, in pixels of one of the six sides of a hexagon. Play around with all three of these attributes until you find the one you like.

That's all for getting it working! Now comes the gory details on how it works.

How it works

So I'm going to build up from the inner-most function, which is also top-to-bottom in the code. We start with this function:

function Hexagon(x, y, size, min, max)
{
    // Starting the path at (x, y)
    var pathCode = '<path d="M' + x + " " + y;
    // Adding the five remaining points on the hexagon
    pathCode += " L" + (0.5 * size + x) + " " + (-0.866 * size + y);
    pathCode += " L" + (1.5 * size + x) + " " + (-0.866 * size + y);
    pathCode += " L" + (2 * size + x) + " " + y;
    pathCode += " L" + (1.5 * size + x) + " " + (0.866 * size + y);
    pathCode += " L" + (0.5 * size + x) + " " + (0.866 * size + y);
    // Closing the path and beginning the fill style
    pathCode += ' Z" style="fill:rgb(';
    // Randomly choose a number between min and max
    var rand = Math.floor((Math.random() * (max - min)) + min);
    // Create a colour with the same random value for R G and B, making grey.
    pathCode += rand + "," + rand + "," + rand + ');" />';
    // The tag has been closed, the string is done. Return it.
    return pathCode;
}

This just creates a hexagon, within a path tag. This is using older SVG 1.1 syntax because it's what I recognised and didn't want to learn the newer syntax. For those who don't know, SVGs use XML to create a vector image, and a path tag just contains a list of points (and optionally some styles). You put particular letters between the sets of points, for example M begins a path, then L will continue the path with a simple point, and the path can be ended with Z. Different letters are used for fancy bezier curves and whatnot, but I don't need those. The Hexagon function is given x and y, corresponding to the location of the hexagon (namely it's left-middle point) a size for one side of the shape, and a min and max value for the colour. A combo of math functions returns a random integer between min and max, and that integer is used for all three parts of the colour (red, green, blue) to create a grey. this is used in a 'fill' style for the path. This path tag is returned as a string to the function that called it, which is the following function:

function DrawHexagons(width,height,hexSize,minShade,maxShade) {
    // Standard ruberic needed at the top of an SVG
    var svgCode = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="' + height + '" width="' + width + '">';
    // The offset var gets moved to and fro between each row to create a tessellation.
    var offset = 0;
    // For each row of hexagons required
    for (var y = 0; y <= (height + hexSize); y += 0.866 * hexSize) {
        // Swap the offset between 0 and the amount required to fit snug between two higher hexagons
        offset == 0 ? offset = hexSize * 1.5 : offset = 0;
        // For each hexagon on the row
        for (var x = 0; x <= width + 3 * hexSize; x += 3 * hexSize) {
            // Add the path tag that creates this hexagon to the SVG
            svgCode += Hexagon(x - offset, y, hexSize, minShade, maxShade);
        }
    }
    // Close the SVG, return the markup.
    svgCode += '</svg>'
    return svgCode;
}

The above function creates an entire SVG. Let's first go over the parameters. The width and height of the div that needs filling are passed in. hexSize is the size of one side of a hexagon. Note that this is not the entire length/width of the shape but just the length of one of the six sides. minShade and maxShade have been mentioned in the Hexagon function.

The first (very long) line opens the SVG and adds a load of tags that are needed for the SVG to work properly. the SVG tag also contains the height and width of the image, which are output directly from the function parameters.

Rows of hexagons The code creates hexagons in rows. For example, the image is a pattern generated from a custom version of the code, where each row is a single colour. We can see that the rows each contain only four or so hexagons in this pattern, because the rows above and below them interlink in a way, as they are offset slightly in comparison. Thus, every other row needs to be offset by a certain value. This is the offset variable defined before the iteration. Each row, the offset variable is swapped between 0, and 1.5 times the side-length (this amount is required to get the row offset perfect).

Then the iteration begins. The y value is set in the first for (meaining a row is created as the y will remain constant and x will change). A maximum of (height + hexSize) is set, because if height alone was used, there may be some empty gaps at the bottom of the image. An incrementer of 0.866 * hexSize is used because this is half the height of a hexagon, meaning the rows will fit together perfectly. (0.866 is an approximation of cos(30), but as it is also used in the Hexagons function, the fit is technically perfect but the hexagons' shape is what is negligibly off). The first thing set in the iteration is the aforementioned swapping of the offset value.

Then another nested iteration is required for each hexagon in a row. This is much the same setup as the first iteration, except for a slightly larger margin in terms of when to stop (due to hexagons in a row being quite spaced apart) and a larger increment (for the same reason). In this very core of the function, is a call to Hexagon to add the path tag to the SVG. The x value is adjusted by the offset when passed. Outside of the two iterations, the closing svg tag is added, and the whole SVG is returned.

If you're just after an SVG and fancy a slightly more manual approach, you can just call DrawHexagons and it will give you the SVG you seek. However, a more fancy approach is created, in the form of the FillHexBoxes function.

function FillHexBoxes() {
    // List all of the elements with the hexbox tag, iterate through them
    var list = document.getElementsByClassName('hexbox');
    for (var i = 0; i < list.length; i++) {
        // Grab the data values for min and max shade, and shape size.
        var minGrey = parseInt(list[i].dataset.minGrey);
        var maxGrey = parseInt(list[i].dataset.maxGrey);
        var hexSize = parseInt(list[i].dataset.hexSize);
        // Create the SVG required
        var svgData = DrawHexagons(list[i].offsetWidth,list[i].offsetHeight,hexSize,minGrey,maxGrey);
        // Put the SVG into a data: constructor, and set it as the background with a url()
        list[i].style.backgroundImage = 'url(\'data:image/svg+xml;utf8,' + svgData + '\')';
        list[i].style.backgroundRepeat = "repeat";
    }
}

First, this grabs any element that has the hexbox class into a list. These are all the elements we want to add our hexagons to. It then iterates through them. (I didn't use a foreach because someone on stackoverflow said that it would be a bad idea and doesn't work in some browsers. I didn't check if that's the case, but it probably is).

First, it grabs the data attributes that are set on each element (and parses them into integers). If they've set them wrong, or not at all, it'll probably break. I'm not going to fix that.

It then uses the DrawHexagons function to grab the SVG required and store it as svgData, using .offsetWidth and .offsetHeight on the element to find out the dimensions required, and using the three data attributes.

We need to set this SVG as the background of the element. This is actually best done using those fancy data: formats. Now, usually this involves taking an image and converting it to base64 and throwing what looks like gobbledygook into your precious CSS. With SVGs, however, you can just throw it in how it is. All you need is data:image/svg+xml;utf8, so it knows what it is and how it's encoded, and then the entirety of the SVG can be included. Now, wrap this whole thing in a url() container and set it as the background-image style of the element using .style.backgroundImage in JavaScript. The script also sets background-repeat: repeat because right now, it does not re-generate the hexagons if the element changes size. If the element gets smaller, that's fine, but if it gets bigger after generation then it'll still repeat due to that tag, but it will be a clear cut that looks unsightly. This could be fixed by making the SVG properly tessellate and I'll likely work on that. For now, one could set a trigger on the element getting bigger that re-runs FillHexBoxes if one needs this feature.

I hope to add some extra options using those data attributes soon, to throw a bit of colour into the mix. But I didn't need it, so I didn't rush to it. Enjoy your grey hexagons.


Last post: Script: Tesselating hexagons of random shades for a background