Ollin

Pursuit

runners on the corners of a polygon, each running toward the next: the ring shrinks and turns at the same time, the paths are logarithmic spirals, and the caption compares the distance run with the law that predicts it (Pursuit)

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

/// **Pursuit**: put a runner on each corner of a polygon, tell each one to run
/// at the next, and start them together. Nobody travels in a straight line,
/// because every target is moving too, and the paths they leave are
/// logarithmic spirals that meet in the middle.
///
/// The faint web is the chase itself, kept every few steps: each line joins a
/// runner to the one it is running at. The bright lines are the paths.
///
/// The caption is the reason this figure is worth knowing. The distance each
/// runner covers is exactly `radius / sin(chasing * pi / sides)`, worked out
/// centuries before anyone could plot it, and the drawing lands on it. It
/// lands a hair over, and that is honest: a runner covering a fixed distance
/// per step overshoots the turn a little every time. A smaller `stepSize`
/// draws a finer curve and a closer number.
///
/// Try it: `chasing` is how many places around the ring the target sits. At 1
/// it is the classic figure. At half the number of sides everybody runs
/// straight at whoever is opposite, and the spiral disappears.
@main
final class PursuitSketch: Sketch {
    @Param(3 ... 12, icon: "hexagon") var sides = 6.0
    @Param(1 ... 6, icon: "arrow.triangle.turn.up.right.circle") var chasing = 1.0
    @Param(icon: "scribble") var showTheChase = true

    private let paper = Color(hex: 0x11131A)
    private let chalk = Color(hex: 0xF2ECDD)
    private let warm = Color(hex: 0xE0724A)

    private var chase = Pursuit.ring(sides: 3, center: .zero, radius: 1)
    private var asked = (sides: 0, chasing: 0)
    private var ringRadius = 0.0
    private var restingFrames = 0

    override func draw() {
        background(paper)
        if asked != (Int(sides), Int(chasing)) { start() }

        if !chase.isFinished {
            chase.step(20)
        } else if restingFrames > 0 {
            restingFrames -= 1
        } else {
            start()
        }

        if showTheChase {
            stroke(chalk.withAlpha(0.14))
            strokeWeight(1)
            for line in chase.web { drawPolyline(line.points) }
            stroke(warm.withAlpha(0.5))
            for line in chase.links { drawPolyline(line.points) }
        }

        noFill()
        strokeCap(.round)
        strokeWeight(width * 0.004)
        for (index, trail) in chase.trails.enumerated() {
            let along = Double(index) / Double(max(chase.runners.count - 1, 1))
            stroke(Color.mix(chalk, warm, along))
            drawPolyline(trail.points)
        }

        noStroke()
        fill(chalk)
        for runner in chase.runners {
            drawCircle(center: runner.position, radius: width * 0.006)
        }

        caption()
    }

    private func start() {
        let count = Int(sides)
        let ahead = min(Int(chasing), count - 1)
        ringRadius = width * 0.40
        chase = Pursuit.ring(sides: count, center: center, radius: ringRadius,
                             chasing: ahead, turn: -.pi / 2,
                             stepSize: ringRadius / 800)
        chase.recordEvery = 26
        asked = (count, Int(chasing))
        restingFrames = 90
    }

    private func caption() {
        let ahead = chase.runners[0].chases ?? 1
        let law = ringRadius / sin(Double(ahead) * .pi / Double(chase.runners.count))
        let ran = chase.runners[0].distanceTraveled
        fill(chalk.withAlpha(0.75))
        textFont(.system); textSize(width * 0.021); textAlign(.center, .bottom)
        drawText("\(chase.runners.count) runners, each chasing "
                 + (ahead == 1 ? "the next" : "\(ahead) along")
                 + ": ran \(Int(ran.rounded())) of \(Int(law.rounded()))",
                 width / 2, height - width * 0.045)
    }
}

↑↓ move ↩ open