惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
博客园 - Franky
V
V2EX
爱范儿
爱范儿
J
Java Code Geeks
小众软件
小众软件
Last Week in AI
Last Week in AI
The Cloudflare Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hugging Face - Blog
Hugging Face - Blog
T
The Blog of Author Tim Ferriss
酷 壳 – CoolShell
酷 壳 – CoolShell
The Register - Security
The Register - Security
GbyAI
GbyAI
Vercel News
Vercel News
Y
Y Combinator Blog
腾讯CDC
F
Fortinet All Blogs
I
InfoQ
N
Netflix TechBlog - Medium
B
Blog RSS Feed
D
Docker
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
量子位
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
The GitHub Blog
The GitHub Blog
Microsoft Security Blog
Microsoft Security Blog
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
Blog — PlanetScale
Blog — PlanetScale
H
Help Net Security
云风的 BLOG
云风的 BLOG
A
About on SuperTechFans
Scott Helme
Scott Helme
T
Tor Project blog
U
Unit 42
Google Online Security Blog
Google Online Security Blog
PCI Perspectives
PCI Perspectives
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
aimingoo的专栏
aimingoo的专栏
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Stack Overflow Blog
Stack Overflow Blog
Security Archives - TechRepublic
Security Archives - TechRepublic

alexwlchan

A Git hook to prevent committing directly to main Describing all my photos I don’t want to repeat repeat myself Rebuilding the computer room What can wonky APIs tell us about the web? Using the Screen Capture API to record a browser window Using Pytester to test my Playwright fixtures Rendering a chat thread in CSS and JavaScript Waiting for website changes in the browser Watching for file changes on macOS Using Playwright to test my static sites Building a basic cache with SQLite HTTP GET requests with the Python standard library Auditing my local Python packages Quietly quantum-resistant blogging Creating a personalised bin calendar Monki Gras 2026 “Prepping Craft” The selfish case for public libraries Dreaming of a ten-year computer Gumdrop, a silly app for messing with my webcam The bare minimum for syncing Git repos Creating Caddyfiles with Cog Swapping gems for tiles Parody posters for made-up movies The Good, the Bad, and the Gutters Using perceptual distance to create better headers The passwords I actually memorise Where I store my multi-factor recovery codes Quick-and-dirty print debugging in Go My favourite books from 2025 Adding a README to S3 buckets with Terraform The palm tree that led to Palmyra
Drawing Truchet tiles in SVG
2025-12-22 · via alexwlchan

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:

AAA

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:

show gridlines

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:

  • The x, y position, and
  • The layer – should it place a full-size tile, or is it a smaller tile subdividing a larger tile

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:

foreground background

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.