Tiling & layout
There are several more ways to divide a canvas beyond the square grid. The hexagon and triangle grids are the other two regular tilings, built as Grid siblings. Recursive subdivision gives the uneven panels of a grid painting. A maze is a perfect labyrinth drawn as clean line work. The Apollonian gasket fills a circle with a foam of circles that touch. All of them are geometry rather than draw calls, because each one returns typed cells, contours, or circles. Those values feed the same drawing, boolean, hatching, and SVG paths as everything else. Everything random uses the seeded random, so a seed reproduces the layout. The tilings that never repeat (Penrose, Wang, girih star patterns, the spectre) have their own page.
Contents
hexGrid / HexGrid
hexGrid(columns: Int, rows: Int,
orientation: HexGrid.Orientation = .pointy,
padding: Insets = .zero, gutter: Double = 0) -> HexGrid
This builds a columns × rows honeycomb over the canvas. Use HexGrid(in: someRectangle, …) for a sub-region. A hexagon cannot stretch the way a Grid cell can, so the block keeps its true aspect ratio. The block is sized to fit the padded bounds and centered in them. orientation picks pointy-top or flat-top. With .pointy, each row shifts by half a hex. With .flat, each column shifts. gutter opens a gap between neighbors, and hexSize is the full center-to-corner size of one lattice hex before the gutter shrinks the drawn cell. A TriangleGrid reports itself the same way, as edgeLength and rowHeight.
Each HexGrid.Cell carries its column and row, its center, and its six corners, ready to draw. The corners are also available as a contour. Loop over cells to draw the grid:
let hexes = hexGrid(columns: 12, rows: 10, padding: 40, gutter: 6)
for cell in hexes.cells {
fill(Color(hue: Double(cell.row) / 10, saturation: 0.5, brightness: 0.9))
drawPolygon(cell.corners)
}
Both grids are RandomAccessCollections of their cells, in the same row-major order, so looping the grid itself says it shorter and builds no array along the way:
let hexes = hexGrid(columns: 12, rows: 10, padding: 40)
for cell in hexes { drawPolygon(cell.corners) }
let howMany = hexes.count // columns × rows
let near = hexes.filter { $0.center.distance(to: bounds.center) < 200 }
A hex grid costs you something a square grid gives you for free, since its cells cannot stretch. The hex-native math is what makes that trade worth it, and every cell carries the axial coordinates (q, r) that this math runs on:
distance(from:to:)counts steps between neighbors, so cells at equal distances form true concentric rings. A square grid cannot produce that honeycomb falloff.neighbors(of:)returns the adjacent cells, up to six of them, andring(around:radius:)walks the cells at an exact distance.cell(at: point)picks the exact hexagon under a point, so one call turns a mouse position into a hexagon.cell(q:r:)looks a cell up by its axial address, andcell(column:row:)by its row and column.
let focus = hexes.cell(at: Vector2(mouseX, mouseY)) ?? hexes.cell(column: 6, row: 5)
for cell in hexes.cells {
let rings = Double(hexes.distance(from: focus, to: cell))
fill(Color.mix(.teal, .black, min(1, rings / 6)))
drawPolygon(cell.corners)
}
See the Patterns/HexGrid example (a radiating pulse with mouse picking).
triangleGrid / TriangleGrid
triangleGrid(columns: Int, rows: Int,
padding: Insets = .zero, gutter: Double = 0) -> TriangleGrid
This is the third regular tiling: rows of equilateral triangles that alternate between pointing up and pointing down. Cell (column, row) points up when column + row is even. columns counts the triangles in a row. Each triangle advances half an edge past the one before it, so neighbors share alternating edges. Like the hex grid, the block keeps its true shape and is centered in the padded bounds. gutter shrinks each triangle toward its center, so neighbors separate evenly.
Each TriangleGrid.Cell carries column and row, pointsUp, its center, and its three vertices. neighbors(of:) returns the cells across each edge, up to three of them:
for cell in triangleGrid(columns: 21, rows: 10, padding: 40, gutter: 4).cells {
fill(cell.pointsUp ? .white : .black)
drawPolygon(cell.vertices)
}
See the Patterns/TriangleGrid example (a two-palette weave over the parity).
subdivide / Subdivision
subdivide(in bounds: Rectangle? = nil,
minSize: Double, maxDepth: Int = 6, chance: Double = 1,
fraction: ClosedRange<Double> = 0.3...0.7,
style: Subdivision.Style = .binary) -> [Subdivision.Cell]
This splits a rectangle into leaf panels, recursively. .binary (the default) cuts across the longer side at a random fraction drawn from fraction. That gives the uneven, painterly panels of the classic grid-painting composition. .quad cuts into four equal quadrants instead, which gives the quadtree look. A cell splits while there is room for a cut and while the random draw allows it. It never splits when a cut would leave either side below minSize, and it never splits past maxDepth. The root always splits when it can. Below the root, a cell splits only with probability chance, so chance: 0.7 leaves a mix of large and small panels. Each returned Subdivision.Cell is a frame plus the depth at which it stopped, which is useful for tinting by scale. The randomness comes from the seeded random.
seed(5)
stroke(.black); strokeWeight(8)
for cell in subdivide(minSize: 90, chance: 0.75) {
fill(random(0, 1) < 0.2 ? randomChoice([.red, .yellow, .blue]) : .white)
drawRect(cell.frame)
}
The typed core is Subdivision.cells(in:minSize:maxDepth:chance:fraction:style:using:), which works over any RandomNumberGenerator. See the Patterns/Subdivision example.
maze / Maze
maze(columns: Int, rows: Int,
algorithm: Maze.Algorithm = .backtracker) -> Maze
drawMaze(_ maze: Maze, in bounds: Rectangle? = nil)
This carves a perfect maze and reads it back as geometry. In a perfect maze every cell is reachable, there are no loops, and there is one path between any two cells. The algorithm you choose changes how the maze looks, not only how it is built:
.backtracker(randomized depth-first search) gives long winding corridors and few dead ends..kruskal(randomized Kruskal's over union-find) gives many short dead ends and an even texture across the whole maze..wilson(loop-erased random walks) draws a uniform sample over every possible maze of the grid, so there is no bias.
You can read the maze back as geometry in these ways:
walls(in: rect)returns the line work, ready to stroke. Collinear wall segments are merged into single long runs, which keeps the result clean for stroking, hatching, and plotter SVG.drawMazestrokes it in one call.solution(fromColumn:fromRow:toColumn:toRow:)returns the unique path between two cells.longestPath()returns the maze's diameter, and its two ends make the natural entrance and exit.contour(of:in:)lays a cell path over a rectangle as a polyline through the cell centers.isOpen(_:column:row:)reads a single passage, in aMaze.Directionof.north,.south,.east, or.west, each of which knows itsopposite.
seed(9)
let m = maze(columns: 24, rows: 24)
stroke(.white); strokeWeight(4); strokeCap(.round)
drawMaze(m)
stroke(.orange)
drawPolyline(m.contour(of: m.longestPath(), in: bounds).points)
The classic one-line BASIC maze has a different look: random ╱ and ╲ diagonals, with corridors implied rather than carved. For that look, see the .diagonals tile in Truchet tiling. See the Patterns/Maze example, which shows all three algorithms with the longest path tracing through.
apollonianGasket
apollonianGasket(in circle: Circle, minRadius: Double,
rotation: Double = 0, maxCount: Int = 20_000) -> [Circle]
This fills a circle with the classic fractal foam. Three equal circles touch each other inside the rim. Every three-way gap then gets the one circle that exactly touches all three of its parents, and this repeats down to minRadius. The construction is a closed form with no randomness, because the Descartes circle theorem fixes each new circle. So the same inputs always build the same foam. Circles come back in generation order, so the index doubles as an age for tinting. rotation spins the three seed circles around the center.
let foam = apollonianGasket(in: Circle(center: bounds.center, radius: 480), minRadius: 3)
noStroke()
for (i, c) in foam.enumerated() {
fill(Colormap.viridis.color(at: Double(i) / Double(foam.count)))
drawCircle(c)
}
See the Patterns/Apollonian example.
Related: Geometry (the square Grid these extend, Contour, shape booleans), Truchet tiling (one tile per grid cell, each at a random rotation), Voronoi & Delaunay (cells from scattered points instead of a lattice), Classic curves (the catalog of closed-form curves).