One Tile Is All It Takes
surt91 invited me to write a guest post and left the topic up to me. I went looking for something that needs almost no code and still earns a second look — and landed on Truchet tiles.
There is exactly one tile. It is a square carrying two quarter-circle arcs, each joining the midpoints of two adjacent edges. That is the entire inventory. The only freedom is how it sits on the grid, and there are exactly two options:
Drop many copies onto a grid and flip a coin for each one. Out of this almost insultingly simple rule falls this:
It gets me every time. Nothing in the rule knows anything about loops, symmetry or closed curves, and yet the arcs reach across the tile boundaries and weave into a tidy fabric. The idea is old: the Dominican friar Sébastien Truchet worked through the patterns a single square produces in all its rotations back in 1704. His original tile was still a square split diagonally into a black and a white half; the smooth arc variant that gives these flowing lines is due to Cyril Stanley Smith, who dug the Truchet tiles back up in 1987.
The whole trick fits into a handful of lines of Python that print an SVG
directly — no numpy, no plotting framework, just a bit of geometry and
random:
import random
def truchet(filename, n=16, s=40, seed=42, stroke_ratio=0.18):
random.seed(seed)
w = h = n * s
r, sw = s / 2, s * stroke_ratio
paths = []
for j in range(n):
for i in range(n):
x, y = i * s, j * s
tm = (x + r, y) # top middle
rm = (x + s, y + r) # right middle
bm = (x + r, y + s) # bottom middle
lm = (x, y + r) # left middle
if random.random() < 0.5:
# arcs hug the top-left and bottom-right corners
arcs = [(tm, lm, 1), (bm, rm, 1)]
else:
# ... or the top-right and bottom-left ones
arcs = [(tm, rm, 0), (lm, bm, 1)]
for (ax, ay), (bx, by), sweep in arcs:
paths.append(f'M{ax:.1f} {ay:.1f} '
f'A{r:.1f} {r:.1f} 0 0 {sweep} {bx:.1f} {by:.1f}')
body = '\n'.join(f' <path d="{d}"/>' for d in paths)
svg = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" '
f'width="{w}" height="{h}">\n'
f'<g fill="none" stroke="#000" stroke-width="{sw:.1f}" '
f'stroke-linecap="round">\n{body}\n</g>\n</svg>\n')
with open(filename, 'w') as f:
f.write(svg)
truchet('truchet.svg')
Per tile we draw two quarter-circle arcs as an SVG path, and the only decision
is the coin flip picking which pair of corners they hug. The 1 or 0 is the
arc’s sweep flag; all it does is make the arc bulge inward instead of outward.
That was the entire algorithm.
Look closely and the tangle turns out not to be a tangle at all: at every edge midpoint exactly two arcs meet — one from each of the two neighbouring tiles. So every stroke has precisely two neighbours, and the whole mess comes apart cleanly into a handful of closed loops. Give each of them its own colour — a short union-find over the edges does the job — and you can see how wildly their lengths differ:
Some loops meander all the way across the image, others have shrivelled down to a
single small circle. Which of the two is decided entirely by the seed.
When I showed surt91 the finished tiling, his first reaction was that it reminded him of percolation. He is right — the random arc tiling is in fact one of the standard ways to draw critical percolation. Because every tile always carries two arcs, the coloured loops from before are nothing but the hulls of percolation clusters. And the fair coin with probability $1/2$ lands, thanks to self-duality, exactly on the critical point $p_c = 1/2$ of the square lattice. That is precisely why loops appear on every scale — at the phase transition the system is scale invariant.
So the loop-size distribution inherits everything we know about critical
percolation. The loops are fractals of dimension $7/4$ — the percolation
counterpart to the $187/96$ that already turned up here for the
Ising model (a German post). The
number of loops enclosing an area larger than $A$ is even known exactly and
universally, namely $\frac{1}{8\pi\sqrt{3}}\,\frac{1}{A}$, a pretty result of
Cardy and Ziff. And the best part, because it confirms surt91’s hunch outright:
bias the coin so that one orientation comes up more often, and you step off the
critical point. The big loops meandering across the image vanish in favour of a
characteristic maximum size — only at exactly $1/2$ does the spectrum reach all
the way to the edge of the picture. In the generator that is a one-line change:
random.random() < p instead of < 0.5.
There is no point in a GitHub repo for any of this; the full code is already up
there in the post. A different number in the seed, and you have your next
wallpaper. Which makes this guest post fit neatly into this blog’s
well-documented fondness for
black-and-white pictures of lines and circles
— a German post, but the pictures need no translation.