Ollin

Kolam

one line launched at 45 degrees between a field of dots, bouncing off the edges until it closes: rows by columns dots make exactly gcd(rows, columns) loops, so 7 by 5 is one unbroken loop and 6 by 4 is two

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

/// **Kolam and sona**: one line launched between a field of dots at 45 degrees,
/// bouncing off the edges until it comes back to where it began. The count is
/// exact: a field of `rows` by `columns` dots closes into gcd(rows, columns)
/// loops, so 7 by 5 is a single unbroken line and 6 by 4 is two.
///
/// A wall placed between two neighboring dots turns the line there too, and each
/// one either cuts a loop in two or joins two into one. The walls here are held
/// still while the field breathes, so you can watch the count change as the
/// drawing reorganizes.
///
/// Try it: set `columns` and `rows` to two numbers with a common factor and
/// count the loops, or empty `walls` and see the plain field underneath.
@main
final class Kolam_Example: Sketch {
    @Param(3 ... 12, icon: "arrow.left.and.right") var columns = 7.0
    @Param(3 ... 12, icon: "arrow.up.and.down") var rows = 5.0
    @Param(0 ... 4, icon: "scissors") var rounding = 3.0
    @Param(icon: "square.grid.3x3") var showDots = true

    /// Four walls, held still: they are what steers the line.
    let walls: [Kolam.Mirror] = [.rightOf(column: 2, row: 1), .below(column: 3, row: 2),
                                 .rightOf(column: 4, row: 3), .below(column: 1, row: 0)]

    let paper = Color(hex: 0x1A1410)
    let chalk = Color(hex: 0xF3E7D3)

    override func draw() {
        background(paper)

        let field = bounds.inset(by: .all(width * 0.12))
        let design = kolam(in: field, columns: Int(columns), rows: Int(rows),
                           mirrors: walls.filter { $0.column < Int(columns) && $0.row < Int(rows) })

        if showDots {
            noStroke()
            fill(chalk.withAlpha(0.45))
            for dot in design.dots { drawCircle(center: dot, radius: 3.5) }
        }

        noFill()
        strokeCap(.round)
        strokeJoin(.round)
        for (index, loop) in design.loops.enumerated() {
            // Each loop gets its own tone, so the count is readable at a glance.
            let tone = design.loops.count == 1 ? chalk
                : Color.mix(chalk, Color(hex: 0xE0724A),
                            Double(index) / Double(design.loops.count - 1))
            stroke(tone)
            strokeWeight(width * 0.008)
            drawPolyline(loop.smoothed(iterations: Int(rounding)).points, closed: true)
        }

        noStroke()
        fill(chalk.withAlpha(0.7))
        textFont(.system); textSize(width * 0.022); textAlign(.center, .bottom)
        let count = design.loops.count
        drawText("\(Int(columns)) by \(Int(rows)): \(count) \(count == 1 ? "line" : "lines")",
                 width / 2, height - width * 0.045)
    }
}

↑↓ move ↩ open