Ollin

ShapeGrammar

a window frame, one rule, and a lattice that divides itself: a cell becomes two cells, split by one straight line between two of its edges, and the rule is applied over and over (ShapeGrammar)

Run
swift run --package-path Examples Example-Patterns-ShapeGrammar
Source
Examples/Patterns/ShapeGrammar/Sketch.swift
import Ollin

/// **Shape grammar**: a window frame, one rule, and a lattice that cuts itself.
///
/// The rule is a single sentence. *A cell becomes two cells, cut apart by one
/// straight line drawn between two of its edges, with the two parts about equal
/// in area.* Nothing says which edges, or where on them, so the rule stands for
/// every lattice it could make rather than for this one.
///
/// Watch it build. Each sweep offers every cell to the rule, and a cell too
/// small to be worth cutting is left alone, which is what brings the design to
/// a stop. Traditional ice-ray lattices were cut this way for real, one fitted
/// stick at a time: divide the area into large and equal spots, then keep
/// dividing until the pieces are the size you wanted.
///
/// The second rule is what turns the cells into bars. It pulls each cell's
/// outline inward by the same distance all the way round, and the ring left
/// over is the wood.
///
/// Try it: `balance` is how uneven a cut may leave the two parts. Hold it near
/// zero and every cut halves the area, which reads as regular and machined.
/// Open it up and the cuts go where they like, which is where the cracked-ice
/// look comes from.
@main
final class ShapeGrammarSketch: Sketch {
    @Param(1_500 ... 40_000, icon: "square.grid.3x3") var cellSize = 7_000.0
    @Param(0 ... 0.7, icon: "scalemass") var balance = 0.3
    @Param(0 ... 16, icon: "square.dashed") var barWidth = 6.0

    private let paper = Color(hex: 0x121016)
    private let wood = Color(hex: 0x8A5A34)
    private let sky = Color(hex: 0xD9CFAF)

    private var frame = Rectangle(x: 0, y: 0, width: 1, height: 1)
    private var grammar = ShapeGrammar(start: ShapeGrammar.Piece("cell", Rectangle(x: 0, y: 0, width: 1, height: 1)),
                                       rules: [])
    private var cells: [ShapeGrammar.Piece] = []
    private var source = SplitMix64(seed: 1)
    private var asked = (cell: 0.0, balance: 0.0)
    private var run = 0
    private var restingFrames = 0

    override func setup() {
        frame = Rectangle(center: center, width: width * 0.82, height: height * 0.82)
        start()
    }

    override func draw() {
        background(paper)
        if asked != (cellSize, balance) { start() }

        // One sweep every few frames, so the rule can be watched working.
        if frameCount % 6 == 0 {
            let grown = grammar.step(cells, using: &source)
            if grown.count == cells.count {
                restingFrames -= 1
                if restingFrames < 0 { run += 1; start() }
            } else {
                cells = grown
            }
        }

        // The bars are what is left of each cell once its middle is taken out.
        // The second rule carries no weight, so it is only reached for a cell
        // with no room for a full bar, which would otherwise stay solid wood.
        let panes = ShapeGrammar(start: cells,
                                 rules: [.inset("cell", by: barWidth, into: "pane"),
                                         .inset("cell", by: barWidth * 0.3, into: "pane",
                                                weight: 0)])
            .run(generations: 1, seed: 0)

        noStroke()
        fill(wood)
        drawRect(corner: frame.topLeft, width: frame.width, height: frame.height)
        fill(sky)
        for pane in panes where pane.label == "pane" { drawPolygon(pane.corners) }

        caption()
    }

    private func start() {
        source = SplitMix64(seed: UInt64(run &* 977 &+ 13))
        grammar = ShapeGrammar.iceRay(in: frame, minArea: cellSize, balance: balance)
        cells = grammar.start
        asked = (cellSize, balance)
        restingFrames = 40
    }

    private func caption() {
        let corners = cells.map(\.corners.count)
        let threes = corners.filter { $0 == 3 }.count
        let fours = corners.filter { $0 == 4 }.count
        let fives = corners.filter { $0 == 5 }.count
        fill(sky.withAlpha(0.8))
        textFont(.system); textSize(width * 0.019); textAlign(.center, .bottom)
        drawText("\(cells.count) cells: \(threes) triangles, \(fours) quadrilaterals, \(fives) pentagons",
                 width / 2, height - width * 0.045)
    }
}

↑↓ move ↩ open