For some sites, user engagement is driven by entertainment value — how diverting the site is. When building such a website, creating a distinctive visual identity is paramount. Your code should aim to enrich the user’s enjoyment on an aesthetic level while fulfilling your client’s design requirements.
One way to do that involves covering a surface with a design composed of individual images. Doing so without any gaps or overlapping of the covering images it is called tiling the plane — the plane being the surface and the tiling being the collection of images that cover it.
My interest in the subject has piqued here and there over the years. A couple weeks ago, I came across a paper titled “Computer Generated Islamic Star Patterns.” The author, Craig Kaplan, has written quite extensively on the subject and I’ll be referring to his publications for inspiration and examples throughout this article.
As it happens, Mr. Kaplan also has a GitHub profile and a library for tiling the plane, called TactileJS. This is basically a utility library dedicated solely to tiling.
According to “Introductory Tiling Theory for Computer Graphics,” there are 93 “tiling types,” — i.e., different ways in which tiles can relate to each other. Of these, 12 are boring because they’re not customizable; the library lets you manipulate the other 81 types.
For these 81 types, a change in one edge of a tile will cause the other edges to change as well — which is, of course, what makes them mathematically interesting, but it’s also why we need a dedicated library to determine how the other edges should change.
In this tutorial, we’ll walk through how to use TactileJS to create visually stunning patterns for your website. You’ll need to connect it to a graphics library to actually show the tilings you define. Tactile provides a demo of how to do this using the JavaScript port of processing.
Kaplan has already demonstrated some use cases for Tactile. I’ve reproduced them here as CodePen examples. They are very thorough and might seem daunting at first, but understanding these examples will help you wrap your mind around the general concept more easily.
The following interactive demo allows you to change the tiling dynamically by playing around with the parameters being sent into Tactile.
This variation of the interactive demo shows support for touch events.
Both of these make use of a utility script called tileinfo.js
, which makes working with Tactile a little less hairy. I will also be using it in some of my demos.
This can be a lot of code to get through to figure out how to do tiling with whatever drawing library you’re using, so Kaplan also created a minimal example.
Although the demos are useful for the pedagogical task of teaching how tiling works, I think they’re still slightly more complicated than they need to be, especially if you just want to generate some quick tiles and draw them.
For this reason, I built a little utility library that provides this functionality, which I’ve taken the liberty of calling TilerTheCreator — because when the universe gives me a perfect opportunity to use a name like that, how could I refuse?
For this example, I’ll use RoughJS to draw the tiles and start us off with the simplest demo I can think of.
RoughJS needs a canvas element to work on, whereas most other graphical libraries need a DOM element to draw in but will create a canvas or svg element as needed.
Our starting HTML will be simple; all we need is a canvas.
<canvas id="roughcanvas" class="roughcanvas"> </canvas>
demo_rough.js
will look like this:
import { TilerTheCreator } from './Tiler_The_Creator.js'; const setCanvas = () => { const roughCanvas = rough.canvas( document.getElementById('roughcanvas') ); const roughJSTiling = new TilerTheCreator({width: window.innerWidth, height: 10, type: 77}); roughJSTiling.readyToTile(); const polygons = roughJSTiling.getPolygonsFromRegion(); polygons.forEach((polygon) => { roughCanvas.polygon( polygon); }) } setCanvas();
The type: 77
tile is a triangle. Changing the height and width will change how many tiles you have.
At any rate, your first tiling will look something like this:
The roughness of the drawing is due to the default settings of Rought.js and has nothing to do with the tiling itself.
This is the simplest tiling API we can make. readyToTile does a few additional things to instantiate your tiles by using the same tiling.fillRegionBounds
function used in the minimal example referenced earlier.
If you want to draw your tiles at different sizes, you can pass in a the scale_factor
property at instantiation time or use the setScale
function that TilerTheCreator exposes.
Try to pass in a property scale_factor: 50
when instantiating your tiling.
const roughJSTiling = new TilerTheCreator({width: window.innerWidth, height: window.innerHeight, scale_factor: 50, type: 77});
You should see a result similar to this:
Obviously, we can draw things other than just tiles with our library — in this case, RoughJS. But as we have polygon information associated with our tiles, we can see how the other things we draw relate to those polygons.
Suppose we want to draw a circle inside our tiles.
The RoughJS code to draw a circle is roughCanvas.circle
(center X of circle, center Y of circle, diameter of circle). This matches the SVG way of defining a circle.
To figure out where our X and Y should be, we might add these functions, for example:
const getX = (polygon) => { return polygon.map(p => p[0]); } const getY = (polygon) => { return polygon.map(p => p[1]); }
Then we’ll add them to our loop through all the polygons.
const polygonX = getX(polygon); const polygonY = getY(polygon); const xmin = Math.min( ...polygonX ); const xmax = Math.max( ...polygonX ); const ymin = Math.min( ...polygonY ); const ymax = Math.max( ...polygonY ); const dx = (xmin+xmax) / 2; const dy = (ymin+ymax) / 2; roughCanvas.circle(dx, dy, 30, {fill: 'blue'});
This should produce the following image.
We can also use information in the polygons to style them using our drawing library’s methods. We won’t get too deep into this, but let’s change slightly how we first instantiate our canvas.
const canvas = document.getElementById('roughcanvas'); const canvasW = canvas.width; const canvasH = canvas.height; const roughCanvas = rough.canvas(canvas);
We can add the following after we draw our circles.
const canvasDivisions = canvasW / 3; const canvasMiddle = canvasDivisions + canvasDivisions; const pointPlacement = (dx < canvasDivisions) ? "start" : (dx < canvasMiddle) ? "middle" : "end"; const styling = {}; styling.fill = (pointPlacement === "middle") ? "#8aea92" : "#80ada0"; styling.hachureAngle = (pointPlacement === "middle") ? 180 : 90; styling.hachureGap = (pointPlacement === "middle") ? 10 : 5;
This way, we know what some basic positions are in our canvas. When we draw our polygons, we can use the styling attribute we made.
roughCanvas.polygon( polygon, styling );
We should have something that looks like this:
Another benefit is that we can mix and match drawing tools while reusing the polygon data. For example, since we’re currently drawing on a canvas, we can reach into the browser’s native canvas APIs instead of depending on a library.
Let’s draw a red star in the center of our circles using the canvas API’s drawImage
function.
First, add some code for a drawing context up by our canvas variable.
const ctx = canvas.getContext('2d');
Next, load the image and put all of your previous polygon manipulations inside the image load event. That way, we have the image to draw when we need it.
const image = new Image(); image.src = 'path to redstar.png'; image.addEventListener('load', () => { //all our normal polygon manipulating code comes in here });
We can now input the following.
ctx.drawImage(image, dx - 12.5, dy - 12.5, 25, 25);
We have to change the x and y coordinates where we start drawing from because, like SVG circles, RoughJS circles are drawn from the x and y out.
Our pattern should look like the following:
Finally, since our tiling solutions are separate from our drawing library, there is nothing to keep us from using multiple tilings inside the same graphic.
Let’s remove our extra drawing code but use the same styling rules we added before. We’ll make two new TilerTheCreator instances and use them to get out some polygons.
Once we have those polygons, we can do two things: draw the two arrays of polygons separately and thus have different rules for how we draw their respective tilings, or simply concatenate them into one array and draw them with the same rules.
Let’s refer to our styled demo from earlier.
We’ll make a new variation of it, but with two different tilings drawn the same way.
Here’s what it should look like:
Our styling is still in there and all polygons are in the same array.
const polygons = roughJSTiling.getPolygonsFromRegion().concat(roughJSTiling2.getPolygonsFromRegion());
So they’re drawn by the same polygon drawing function.
You could also draw the two arrays like this:
The main difference here is that our second array of polygons is drawn.
polygons2.forEach((polygon) => { roughCanvas.polygon( polygon, {fill: 'red'} ); });
If we keep them in two separate tilings, we can also draw some particular type of tiling, such as only drawing every third tiling or placing tiles at a certain position on the canvas.
Check out the examples below for inspiration.
Styling tiles by index:
Do not draw tiles in one array if they fall within the middle of the canvas:
Since the graphics are drawn by JavaScript, we can react to events on our page the same way we would with anything else. For example, we can change a tiling or alter other things in response to an event.
Of course, there are lots of other things you could do with these techniques, such as combining multiple tilings and drawing methods to make kaleidoscopic effects or animations of the tilings. I hope this guide gave you some ideas to help kick off your JavaScript tiling journey.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowReact Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.