Ollin

Ten print

tenPrint fills a grid with diagonals, one per cell. A coin flip decides which way each one leans. The diagonals meet at the cells' corners, so the result is not a field of loose marks. It is a tangle of long connected paths, and no two runs of the coin give the same one.

A famous one-line program printed this picture forever on an early home computer. It is about the smallest amount of code anyone has needed to make something worth looking at.

Two panels of the same design: a black maze of diagonals over a 16 by 16 grid, and the same lines redrawn with each joined run in its own color

Contents

tenPrint

tenPrint(in bounds: Rectangle? = nil, columns: Int, rows: Int,
         probability: Double = 0.5) -> TenPrint

drawTenPrint(in bounds: Rectangle? = nil, columns: Int, rows: Int,
             probability: Double = 0.5)

You get a columns × rows field of diagonals over bounds, which is the whole canvas by default. The seeded random flips each cell's coin, so seed(_:) makes the design reproducible. drawTenPrint strokes the joined runs for you.

seed(5)
let maze = tenPrint(columns: 40, rows: 40)
stroke(.white); strokeWeight(6); strokeCap(.round)
for run in maze.runs { drawPolyline(run.points) }

probability biases the coin. At 0.5 you get the balanced tangle. Push it toward 0 or 1 and the field combs into long parallel diagonals, with the odd cell crossing them. That is a different and quieter picture.

The two faces

On TenPrintWhat it gives
linesOne open two-point Contour per cell, the diagonal that cell drew. This is the plain reading, and what the original printed.
runsThe same diagonals joined end to end wherever they meet, so a pen travels a long way before it lifts. Use these to stroke and to plot.
bitsOne bit per cell, row-major, so it zips with grid.cells. false leans forward, true leans back.
bit(column:row:)The bit for a cell. A shorter bits array repeats.
gridThe grid the diagonals are drawn in.

Color each run separately to see what the joining found, because a run is one continuous path through the tangle:

for (index, run) in maze.runs.enumerated() {
    stroke(CosinePalette.rainbow.color(at: (Double(index) * 0.618)
        .truncatingRemainder(dividingBy: 1)))
    drawPolyline(run.points)
}

Bits of your own

You do not have to use the coin. TenPrint(grid:bits:) takes the bits directly, and a shorter array repeats, so a small motif tiles a large field.

let woven = TenPrint(grid: Grid(in: bounds, columns: 30, rows: 30),
                     bits: [true, false, false])

All-false bits are the plainest case worth knowing. Every cell leans the same way, and the runs come back as exactly the anti-diagonals of the grid.

Practical notes

  • The gutter does not apply. A diagonal runs corner to corner of the lattice the cells sit on. Neighbors always meet, whatever the grid's gutter is.
  • strokeCap(.round) is what makes it read as one tangle. Butt caps leave a notch at every corner where two diagonals meet.
  • runs is not the provably shortest set of paths. Every walk starts at a corner where an odd number of diagonals meet. Such a corner has to end some path, and that keeps the count near the fewest possible. What the walks leave behind is taken as loops. Splicing those leftovers back in would shorten the list a little, and that is more machinery than a pen lift is worth.
  • Deterministic. The same seed gives the same design. The runs also come back in the same order every time, so a colored-by-index reading is stable.
  • Plotter-ready. runs are real polylines, so --export-svg writes them as paths.

Related: Hitomezashi (one bit per grid line rather than per cell), Truchet (the same idea with tiles that carry arcs), Ulam spiral (another one-rule picture over a grid), Geometry (the Grid underneath).

↑↓ move ↩ open