Ollin

Rivers

the drainage of a landscape, computed rather than drawn: water runs to the steepest neighbor, the flow through a cell is the sum of every cell upstream of it, and the network above a threshold is stroked by Strahler order over the contour map it came from (Drainage)

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

/// **Rivers**: the drainage of a landscape, worked out rather than drawn.
///
/// Nothing here decides where a river should go. Water on any cell runs to
/// whichever neighbor is steepest downhill, and a cell joins the network once
/// enough ground drains through it. The branching comes out of the ground
/// itself, which is why it looks like branching you have seen.
///
/// The lines thicken by Strahler's order rather than by flow. A headwater is
/// one. Two of the same order meeting make the next one up, and an unequal pair
/// keeps the larger, so the thickness counts how much of the branching upstream
/// is behind a reach.
///
/// Try it: `minFlow` is the smallest catchment you are willing to call a
/// river, in cells. Take it down and a fine tracery fills every crease of the
/// terrain. Take it up and only a few trunks are left. `weathered` runs the
/// rain erosion first, and the network sharpens because the valleys it carved
/// are the ones the water then finds.
@main
final class RiversSketch: Sketch {
    @Param(20 ... 900, icon: "drop") var minFlow = 140.0
    @Param(1 ... 99, icon: "dice") var landSeed = 7.0
    @Param(icon: "cloud.rain") var weathered = true
    @Param(icon: "square.grid.3x3") var showBasins = false

    private let paper = Color(hex: 0x10131A)
    private let chalk = Color(hex: 0xF2ECDD)
    private let water = Color(hex: 0x6FA8DC)
    private let warm = Color(hex: 0xE0724A)

    private var land = Heightfield(columns: 2, rows: 2)
    private var drainage: Drainage?
    private var contours: [Contour] = []
    private var built = ""

    private var map: Rectangle {
        Rectangle(center: Vector2(width / 2, height * 0.47),
                  width: width * 0.86, height: width * 0.86)
    }

    override func draw() {
        background(paper)
        rebuildIfNeeded()
        guard let drainage else { return }

        // The ground as a contour map, so the network reads against the shape
        // it came out of, and the whole picture stays lines a pen could draw.
        noFill()
        stroke(chalk.withAlpha(showBasins ? 0.08 : 0.16))
        strokeWeight(1)
        for line in contours { drawPolyline(line.points, closed: line.isClosed) }

        if showBasins { drawBasins(drainage) }

        noFill()
        strokeCap(.round)
        strokeJoin(.round)
        for river in drainage.rivers(minFlow: minFlow, in: map) {
            guard river.points.count >= 2 else { continue }
            stroke(Color.mix(water, chalk, min(1, Double(river.order - 1) / 4))
                .withAlpha(0.55 + 0.1 * Double(min(river.order, 4))))
            strokeWeight(0.7 + Double(river.order) * 0.9)
            drawPolyline(river.points)
        }

        drawCaption(drainage)
    }

    // MARK: - Building it

    private func rebuildIfNeeded() {
        let key = "\(landSeed.rounded())|\(weathered)"
        guard key != built else { return }
        built = key

        let seed = Int(landSeed.rounded())
        var field = Heightfield.diamondSquare(size: 257, roughness: 0.55, seed: seed)
        if weathered {
            field = field.eroded(.hydraulic(drops: 50_000), seed: seed)
                .eroded(.thermal(talus: 0.012, iterations: 30))
        }
        land = field
        drainage = field.drainage()
        let box = map
        contours = isolines(at: (1 ... 13).map { Double($0) / 14 }, in: box, resolution: 220) {
            field.value(u: box.uv(of: $0).x, v: box.uv(of: $0).y)
        }.flatMap { $0 }
    }

    /// Each basin in its own tone, so where one river's ground ends and the
    /// next one's begins can be seen at all.
    private func drawBasins(_ drainage: Drainage) {
        let cell = map.width / Double(drainage.columns)
        noStroke()
        for y in stride(from: 0, to: drainage.rows, by: 2) {
            for x in stride(from: 0, to: drainage.columns, by: 2) {
                let which = drainage.basin(x, y)
                guard which >= 0 else { continue }
                // Hue runs 0 to 1, stepped by the golden ratio so neighboring
                // basins land far apart in color.
                fill(Color(hue: Double(which) * 0.61803, saturation: 0.45,
                           brightness: 0.5, alpha: 0.5))
                let point = drainage.point(x, y, in: map)
                drawRect(corner: point, width: cell * 2, height: cell * 2)
            }
        }
    }

    private func drawCaption(_ drainage: Drainage) {
        let reaches = drainage.rivers(minFlow: minFlow, in: map).count
        let biggest = drainage.flow.max() ?? 0
        textSize(17)
        textAlign(.center)
        fill(chalk.withAlpha(0.55))
        drawText("\(reaches) reaches over \(Int(minFlow)) cells, in \(drainage.outlets.count) basins",
                 width / 2, height * 0.955)
        fill(warm.withAlpha(0.75))
        drawText("the largest river carries \(Int(biggest)) of the \(drainage.columns * drainage.rows)",
                 width / 2, height * 0.925)
    }
}

↑↓ move ↩ open