Ollin

Aperiodic tilings

This page covers four families of tilings. The tilings in each family either never repeat, or they turn a few local rules into one connected pattern.

  • Penrose tilings, kites and darts, thick and thin rhombs.
  • Wang tiles, edge-matching squares, where aperiodicity was first discovered.
  • Girih star patterns, the polygons-in-contact strapwork of Islamic geometric art.
  • The spectre, the einstein, a single tile that can only ever tile aperiodically.

All four return geometry, not draw calls. Each one returns typed tiles or contours, and those feed the same drawing, boolean, hatching, and SVG paths as every other shape. So all four work as plotter-ready line work too.

Four panels: Penrose kites and darts with colored arcs, Penrose rhombs, a teal star pattern woven over a honeycomb, and curved spectre tiles with a few orange ones

Penrose and spectre generation uses no random numbers, so the same call always lays the same patch. Girih is a pure function of its polygons. Wang tiling uses the seeded random, so a seed reproduces the quilt. The periodic families are on the tiling & layout page. Every polygon on this page also composes with girihPattern, which decorates any edge-to-edge tiling.

Contents

penroseTiling / Penrose

penroseTiling(_ variant: Penrose.Variant = .rhombs,
              in bounds: Rectangle? = nil,
              tileEdge: Double = 48,
              center: Vector2? = nil) -> [Penrose.Tile]

Returns the tiles of a Penrose tiling that covers bounds, which defaults to the whole canvas. There are two variants: .kitesAndDarts is P2, and .rhombs is P3, a thick 72° rhomb and a thin 36° one. The tiles are generated by substitution. A wheel of first-generation tiles at center is deflated until every edge is at most tileEdge long. The result is then merged into whole tiles and clipped to the bounds. center is where the five-fold "sun" sits, and it defaults to the bounds center.

Each Penrose.Tile carries:

MemberMeaning
kind.kite / .dart or .thick / .thin, the per-kind color hook.
points: [Vector2]The four corners. The first is the head of the symmetry axis (the kite's nose, the dart's reflex corner, a rhomb's apex).
shape / contourFillable Shape / closed Contour.
arcs: [Contour]The two matching-rule arcs. Their ends meet across every edge.
for tile in penroseTiling(.rhombs, tileEdge: 36) {
    fill(tile.kind == .thick ? .indigo : .navy)
    drawShape(tile.shape)
}

The arcs are the classic decoration. Each tile carries two circular arcs, and their ends meet each edge at its golden section. That point is the same from both sides of the edge, so the whole tiling reads as a weave of continuous curves. Stroke only the arcs for the line-work look:

noFill(); strokeWeight(3)
for tile in penroseTiling(.kitesAndDarts, tileEdge: 60) {
    stroke(tile.kind == .kite ? .coral : .teal)
    for arc in tile.arcs { drawPolyline(arc.points, closed: false) }
}

The result is deterministic, because no randomness is involved. The same bounds and edge always lay the same tiles. Generation is capped at two million half-tiles. If tileEdge is far below what that cap allows, Ollin clamps it.

wangTiling / WangTile / WangTiling

wangTiling(_ tiles: [WangTile], columns: Int, rows: Int,
           attempts: Int = 20) -> WangTiling?
drawWangTiling(_ tiling: WangTiling, colors: [Color], in bounds: Rectangle? = nil)

Wang tiles are squares with a color on each edge. They are placed without rotation under one rule: touching edges must carry the same color. The colors in WangTile(top:right:bottom:left:weight:) are plain integers. These tiles are where aperiodicity was first found. Creative coding uses the same idea for the opposite purpose, as the classic texture-generation trick. In that use, a seeded scanline fill turns independent random picks into one connected, maze-like pattern.

wangTiling fills a columns × rows grid in scanline order, driven by the seeded random. Each cell is chosen from the tiles that match its west and north neighbors, and the choice is weighted by weight. WangTiling.completeSet(colors: n) builds the full set of n⁴ tiles, which can never dead-end. A hand-built set can dead-end, so the fill is retried up to attempts times. A nil result means no attempt succeeded.

The result carries tiles, columns/rows, row-major indices, tile(column:row:), and tileIndex(column:row:) for the index alone. drawWangTiling renders each cell as four triangles that meet at the center, one per edge in that edge's color (the classic picture). The colors array is indexed by edge color id:

seed(7)
if let quilt = wangTiling(WangTiling.completeSet(colors: 2), columns: 15, rows: 15) {
    drawWangTiling(quilt, colors: [.navy, .orange])
}

For roads, dungeons, or texture patches, read tile(column:row:) and draw your own cell content for each edge color. Neighbors are guaranteed to agree, which is what makes the pieces connect.

girihPattern / Girih

girihPattern(over polygons: [[Vector2]], angle: Double = 54) -> [Contour]
girihPattern(in polygon: [Vector2], angle: Double = 54) -> [Contour]
drawGirih(over polygons: [[Vector2]], angle: Double = 54)

girihPattern builds Islamic star patterns by the polygons-in-contact method, which is Hankin's technique in Kaplan's formulation. Lay any edge-to-edge tiling of polygons. Then start a pair of rays from the midpoint of each edge at the contact angle, in degrees. Grow every ray until it meets another, and the matched pairs become the strapwork strands. Remove the tiling, and the crossings that remain are the star pattern. The strands join seamlessly across tiles because both sides start from the same midpoints.

It works over any polygons: Grid squares, HexGrid cells, Penrose tiles, or the five classic girih tiles. The contact angle is the one main control. As it changes, the same tiling reads spiky, star-like, or woven:

let cells = hexGrid(columns: 9, rows: 8).cells.map(\.corners)
stroke(.white); strokeWeight(3); noFill()
drawGirih(over: cells, angle: 60)

Girih.Tile is the tile set of the medieval decagonal system. The five tiles are .decagon, .pentagon, the elongated .hexagon, .bowtie, and .rhombus. All five share an edge length, so any two compose edge to edge. Each offers unit points, a scaled and placed points(edge:at:rotation:), and the composing form points(onEdge:_:). That last form lays the tile's first edge exactly along a segment. Pass an existing tile's edge in reverse order, and the new tile lands against that edge on the other side:

let decagon = Girih.Tile.decagon.points(edge: 56, at: uv(0.5, 0.5))
var patch = [decagon]
for i in decagon.indices {
    let p = decagon[i], q = decagon[(i + 1) % decagon.count]
    patch.append(Girih.Tile.pentagon.points(onEdge: q, p))   // a flower
}
drawGirih(over: patch, angle: 54)   // 54° is the classic girih angle

The result is pure geometry with no randomness. The strands are open Contours, so you can stroke them, hatch them, thicken them through the shape booleans, or export them.

spectreTiling / Spectre

spectreTiling(in bounds: Rectangle? = nil,
              tileEdge: Double = 26,
              curve: Double = 0) -> [Spectre.Tile]

The spectre is the einstein: one shape that tiles the plane and can never repeat. curve: 0 gives Tile(1,1), the 14-sided polygon with unit edges on the 30° grid. Any nonzero curve bends every edge into the same alternating bump. That removes the tile's mirror symmetry and makes it strictly chiral, so it tiles only without reflections. Useful values run up to about 1, and 0.5 is close to the published look. A patch grows by the substitution system from the discovery paper until it covers bounds, with edges close to tileEdge.

Every tile in a patch is congruent with the others and has the same handedness. Each Spectre.Tile carries:

MemberMeaning
pointsThe outline (14 corners straight, more when curved).
metatileWhich of the nine substitution types the tile descends from, as a Spectre.Metatile: .gamma, .delta, .theta, .lambda, .xi, .pi, .sigma, .phi, or .psi, after the Greek letters the paper names them by. This is the natural coloring key.
isOddWhether the tile is one of the rare mystic partners, which are rotated 30° from every other tile.
shape / contourFillable Shape / closed Contour.
for tile in spectreTiling(tileEdge: 26, curve: 0.5) {
    fill(tile.isOdd ? .tomato : .ivory)
    drawShape(tile.shape)
}

The result is deterministic, so the same call always lays the same patch.


Also see tiling & layout for the periodic families (hex and triangle grids, subdivision, mazes). See Truchet tiles for the related family, where each tile's orientation is seeded, and Wave Function Collapse for constraint solving beyond the scanline.

↑↓ move ↩ open