Ollin

Hyperbolic tiling

Regular tilings of the hyperbolic plane, drawn in the Poincaré disk. Identical p-sided tiles meet q to a vertex, and the pattern repeats forever toward a circular horizon it never reaches.

On flat paper only three regular tilings exist: triangles, squares, and hexagons. That limit comes from the corners meeting at a vertex, which must add up to a full turn. Hyperbolic space has room for every pair beyond those three, so the tiling exists whenever (sides - 2) * (meeting - 2) > 4, and the disk shows all of it at once. Every tile is the same true size, and only the drawing makes them smaller as they approach the rim.

The call returns geometry, not draw calls. You get typed tiles whose outlines feed the same drawing, boolean, hatching, and SVG paths as everything else. That makes the pattern plotter-ready line work too. Generation is deterministic, with no randomness and no use of time.

Two Poincaré disks side by side. Left: pentagons meeting four to a corner in a crisp ivory-and-indigo curved checkerboard. Right: heptagons meeting three to a corner, ivory at the center deepening to indigo as the tiles shrink toward the circular horizon

Contents

hyperbolicTiling / HyperbolicTiling

hyperbolicTiling(sides: Int = 7,
                 meeting: Int = 3,
                 in bounds: Rectangle? = nil,
                 viewpoint: Vector2 = Vector2(0, 0),
                 minEdge: Double = 3) -> [HyperbolicTiling.Tile]

Returns the tiles of the regular hyperbolic tiling, with sides-sided tiles meeting meeting to a vertex. That pair is the Schläfli symbol {p,q}. The tiles draw in the disk inscribed in bounds, which defaults to the whole canvas. The pair must satisfy (sides - 2) * (meeting - 2) > 4. The classic pairs are {7,3}, {3,7}, {5,4}, {4,5}, {6,4}, and {8,3}.

The construction places the central tile exactly, working from the hyperbolic triangle it decomposes into. It then reflects that tile across its own edges, and keeps reflecting the reflections. Each edge lies on a circle that meets the horizon at right angles, so the reflection is an inversion in that circle. Tiles whose longest edge would draw shorter than minEdge canvas units are dropped, and that is what bounds the otherwise endless tiling.

Each HyperbolicTiling.Tile carries:

MemberMeaning
points: [Vector2]The outline. Its curved geodesic edges are already flattened to short segments.
center: Vector2The tile's hyperbolic center.
depth: IntEdge crossings from the central tile. It is 0 there, 1 for its neighbors, and so on.
parity: Int0 or 1, flipping across every shared edge. Use it to two-color the tiling.
shape / contourFillable Shape / closed Contour.
for tile in hyperbolicTiling(sides: 5, meeting: 4) {
    fill(tile.parity == 0 ? .ivory : .indigo)
    drawShape(tile.shape)
}

The full typed form is HyperbolicTiling.tiles(sides:meeting:in:viewpoint:minEdge:maxDepth:), where maxDepth puts a second limit on the reflection depth.

Coloring

parity alternates across every shared edge. When meeting is even, the two colors close consistently around every vertex, so the whole tiling checkerboards perfectly. When meeting is odd, a perfect two-coloring cannot exist, because an odd ring of tiles surrounds each vertex. The coloring still alternates, and it resolves deterministically where the colors meet. For an odd meeting, depth is the better value to color by:

for tile in hyperbolicTiling(sides: 7, meeting: 3) {
    fill(Color.mix(.ivory, .teal, min(Double(tile.depth) / 6, 1)))
    drawShape(tile.shape)
}

For plain line work, stroke each tile's outline. The flattened points also go straight to the SVG path, so you can plot the result:

noFill()
stroke(.black)
for tile in hyperbolicTiling(sides: 4, meeting: 5, minEdge: 2) {
    drawPolyline(tile.points, closed: true)
}

Panning across the tiling

viewpoint is the point of the hyperbolic plane that is brought to the middle of the disk. You give it in disk units, so its length must stay below 1. Animating it pans the camera across the tiling while the horizon stays put. Tiles grow as they approach the middle and shrink again as they move away, and the picture never runs out.

override func draw() {
    background(.white)
    let t = loopProgress(over: 24) * .tau
    let viewpoint = Vector2(cos(t), sin(t)) * 0.32
    for tile in hyperbolicTiling(sides: 5, meeting: 4, viewpoint: viewpoint) {
        fill(tile.parity == 0 ? .ivory : .indigo)
        drawShape(tile.shape)
    }
}

A closed circuit of viewpoints loops perfectly. The pan is a rigid motion of the hyperbolic plane, so the number of visible tiles stays roughly constant however far it travels.

Run the example with swift run --package-path Examples Example-Patterns-HyperbolicTiling. The periodic tilings are on the tiling & layout page, and the tilings that never repeat are on the aperiodic tilings page.

↑↓ move ↩ open