Skip to main content

Drawing Truchet tiles in SVG

I recently read Ned Batchelder’s post about Truchet tiles, which are square tiles that make nice patterns when you tile them on the plane. I was experimenting with alternative headers for this site, and I thought maybe I’d use Truchet tiles. I decided to scrap those plans, but I still had fun drawing some pretty pictures.

One of the simplest Truchet tiles is a square made of two colours:

These can be arranged in a regular pattern, but they also look nice when arranged randomly:

The tiles that really caught my eye were Christopher Carlson’s. He created a collection of “winged tiles” that can be arranged with multiple sizes in the same grid. A tile can be overlaid with four smaller tiles with inverted colours and extra wings, and the pattern still looks seamless.

He defined fifteen tiles, which are seven distinct patterns and then various rotations:

The important thing to notice here is that every tile only really “owns” the red square in the middle. When laid down, you add the “wings” that extend outside the tile – this is what allows smaller tiles to seamlessly flow into the larger pattern.

Here’s an example of a Carlson Truchet tiling:

Conceptually, we’re giving the computer a bag of tiles, letting it pull tiles out at random, and watching what happens when it places them on the page.

In this post, I’ll explain how to do this: filling the bag of tiles with parametric SVGs, then placing them randomly at different sizes. I’m assuming you’re familiar with SVG and JavaScript, but I’ll explain the geometry as we go.

Filling the bag of tiles

Although Carlson’s set has fifteen different tiles, they’re made of just four primitives, which I call the base, the slash, the wedge, and the bar.

The first step is to write SVG definitions for each of these primitives that we can reuse.

Whenever I’m doing this sort of generative art, I like to define it parametrically – writing a template that takes inputs I can change, so I can always see the relationship between the inputs and the result, and I can tweak the settings later. There are lots of templating tools; I’m going to write pseudo-code rather than focus on one in particular.

For these primitives, there are two variables, which I call the inner radius and outer radius. The outer radius is the radius of the larger wings on the corner of the tile, while the inner radius is the radius of the foreground components on the middle of each edge. For the slash, the wedge, and the bar, the inner radius is half the width of the shape where it meets the edge of the tile.

This diagram shows the two variables, plus two variables I compute in the template:

outer radiusinner radiustile sizepadding

Here’s the template for these primitives:

<!-- What's the length of one side of the tile, in the red dashed area?
     tileSize = (innerR + outerR) * 2 -->

<!-- How far is the tile offset from the edge of the symbol/path?
     padding = max(innerR, outerR) -->

<symbol id="base">
  <!--
    For the background, draw a square that fills the whole tile, then
    four circles on each of the corners.
    -->
  <g class="background">
    <rect x="{{ padding }}" y="{{ padding }}" width="{{ tileSize }}" height="{{ tileSize }}"/>
    <circle cx="{{ padding }}"            cy="{{ padding }}"            r="{{ outerR }}"/>
    <circle cx="{{ padding + tileSize }}" cy="{{ padding }}"            r="{{ outerR }}"/>
    <circle cx="{{ padding }}"            cy="{{ padding + tileSize }}" r="{{ outerR }}"/>
    <circle cx="{{ padding + tileSize }}" cy="{{ padding + tileSize }}" r="{{ outerR }}"/>
  </g>
  <!--
    For the foreground, draw four circles on the middle of each tile edge.
    -->
  <g class="foreground">
    <circle cx="{{ padding }}"            cy="{{ tileSize / 2 }}"       r="{{ innerR }}"/>
    <circle cx="{{ padding + tileSize }}" cy="{{ tileSize / 2 }}"       r="{{ innerR }}"/>
    <circle cx="{{ tileSize / 2 }}"       cy="{{ padding }}"            r="{{ innerR }}"/>
    <circle cx="{{ tileSize / 2 }}"       cy="{{ padding + tileSize }}" r="{{ innerR }}"/>
  </g>
</symbol>

<!--
  Slash:
    - Move to the top edge, left-hand vertex of the slash
    - Line to the top edge, right-hand vertex
    - Smaller arc to left egde, upper vertex
    - Line down to left edge, lower vertex
    - Larger arc back to the start
-->
<path
  id="slash"
  d="M {{ padding + outerR }} {{ padding }}
     l {{ 2 * innerR }} 0
     a {{ outerR }} {{ outerR }} 0 0 0 {{ outerR }} {{ outerR }}
     l 0 {{ 2 * innerR }}
     a {{ innerR*2 + outerR }} {{ innerR*2 + outerR }} 0 0 1 {{ -innerR*2 - outerR }} {{ -innerR*2 - outerR }}"/>

<!--
  wedge:
    - Move to the top edge, left-hand vertex of the slash
    - Line to the top edge, right-hand vertex
    - Smaller arc to left egde, upper vertex
    - Line to centre of the tile
    - Line back to the start
-->
<path
  id="wedge"
  d="M {{ padding + outerR }} {{ padding }}
     l {{ 2 * innerR }} 0
     a {{ outerR }} {{ outerR }} 0 0 0 {{ outerR }} {{ outerR }}
     l {{ 0 }} {{ 2 * innerR }}
     l {{ -innerR*2 - outerR }} 0"/>

<!--
  Bar: horizontal rectangle that spans the tile width and is the same height
  as a circle on the centre of an edge.
  -->
<rect
  id="bar"
  x="{{ padding }}" y="{{ padding + outerR }}"
  width="{{ tileSize }}"
  height="{{ 2 * innerR }}"/>

The foreground/background classes are defined in CSS, so I can choose the colour of each.

This template is more verbose than the rendered SVG, but I can see all the geometric expressions – I find this far more readable than a file full of numbers. This also allows easy experimentation – I can change an input, re-render the template, and instantly see the new result.

I can then compose the tiles by referencing these primitive shapes with a <use> element. For example, the “T” tile is made of a base and two wedge shapes:

<!-- The centre of rotation is the centre of the whole tile, including padding.
     centreRotation = outerR + innerR -->

<symbol id="carlsonT">
  <use href="#base"/>
  <use href="#wedge" class="foreground"/>
  <use href="#wedge" class="foreground" transform="rotate(90 {{ centreRotation }} {{ centreRotation }})"/>
</symbol>

After this, I write a similar <symbol> definition for all the other tiles, plus inverted versions that swap the background and foreground.

Now we have a bag full of tiles, let’s tell the computer how to place them.

Placing the tiles on the page

Suppose the computer has drawn a tile from the bag. To place it on the page, it needs to know:

From these two properties, it can work out everything else – in particular, whether to invert the tile, and how large to scale it.

The procedure is straightforward: get the position of all the tiles in a layer, then decide if any of those tiles are going to be subdivided into smaller tiles. Use those to position the next layer, and repeat. Continue until the next layer is empty, or you hit the maximum number of layers you want.

Here’s an implementation of that procedure in JavaScript:

function getTilePositions({
  columns,
  rows,
  tileSize,
  maxLayers,
  subdivideChance,
}) {
  let tiles = [];
  
  // Draw layer 1 of tiles, which is a full-sized tile for
  // every row and column.
  for (i = 0; i < columns; i++) {
    for (j = 0; j < rows; j++) {
      tiles.push({ x: i * tileSize, y: j * tileSize, layer: 1 });
    }
  }
  
  // Now go through each layer up to maxLayers, and decide which
  // tiles from the previous layer to subdivide into four smaller tiles.
  for (layer = 2; layer <= maxLayers; layer++) {
    let previousLayer = tiles.filter(t => t.layer === layer - 1);
    
    // The size of tiles halves with each layer.
    // On layer 2, the tiles are 1/2 the size of the top layer.
    // On layer 3, the tiles are 1/4 the size of the top layer.
    // And so on.
    let layerTileSize = tileSize * (0.5 ** (layer - 1));
    
    previousLayer.forEach(tile => {
      if (Math.random() < subdivideChance) {
        tiles.push(
          { layer, x: tile.x,                 y: tile.y                 },
          { layer, x: tile.x + layerTileSize, y: tile.y                 },
          { layer, x: tile.x,                 y: tile.y + layerTileSize },
          { layer, x: tile.x + layerTileSize, y: tile.y + layerTileSize },
        )
      }
    })
  }
  
  return tiles;
}

Once we know the positions, we can lay them out in our SVG element.

We need to make sure we scale down smaller tiles to fit, and adjust the position – remember each Carlson tile only “owns” the red square in the middle, and the wings are meant to spill out of the tile area. Here’s the code:

function drawTruchetTiles(svg, tileTypes, tilePositions, padding) {
  tilePositions.forEach(c => {
    // We need to invert the tiles every time we subdivide, so we use
    // the inverted tiles on even-numbered layers.
    let tileName = c.layer % 2 === 0
      ? tileTypes[Math.floor(Math.random() * tileTypes.length)] + "-inverted"
      : tileTypes[Math.floor(Math.random() * tileTypes.length)];
      
    // The full-sized tiles are on layer 1, and every layer below
    // that halves the tile size.
    const scale = 0.5 ** (c.layer - 1);
    
    // We don't want to draw a tile exactly at (x, y) because that
    // would include the wings -- we add negative padding to offset.
    //
    // At layer 1, adjustment = padding
    // At layer 2, adjustment = padding * 1/2
    // At layer 3, adjustment = padding * 1/2 + padding * 1/4
    //
    const adjustment = -padding * Math.pow(0.5, c.layer - 1);

    svg.innerHTML += `
      <use
        href="${tileName}"
        x="${c.x / scale}"
        y="${c.y / scale}"
        transform="translate(${adjustment} ${adjustment}) scale(${scale})"/>`;
  });
}

The padding was fiddly and took me a while to work out, but now it works fine. The tricky bits are another reason I like defining my SVGs parametrically – it forces me to really understand what’s going on, rather than tweaking values until I get something that looks correct.

Demo

Here’s a drawing that uses this code to draw Carlson truchet tiles:

It was generated by your browser when you loaded the page, and there are so many possible combinations that it’s a unique image.

If you want a different picture, reload the page, or tell the computer to draw some new tiles.

These pictures put me in mind of an alien language – something I’d expect to see etched on the wall in a sci-fi movie. I can imagine eyes, tentacles, roads, and warnings left by a long-gone civilisation.

It’s fun, but not really the tone I want for this site – I’ve scrapped my plan to use Truchet tiles as header images. I’ll save them for something else, and in the meantime, I had a lot of fun.