Ollin

NewtonBasins

Newton's basins: roots placed on the plane, the method run from every pixel, each pixel colored by the root it lands on and shaded by the steps it took; the roots move along slow orbits so the basins pour, relaxation scales the step, and a dark lake is where the method never lands

Run
swift run --package-path Examples Example-Effects-NewtonBasins
Source
Examples/Effects/NewtonBasins/Sketch.swift
import Ollin

/// Newton's basins: a handful of roots placed on the plane, Newton's method run
/// from every pixel, and each pixel colored by the root it lands on. The method
/// is the step every numeric solver takes, z -= p(z) / p'(z), and far from the
/// roots it cannot make up its mind: wherever two basins meet, every other one
/// shows up too, at every scale, so the boundaries are dust where all the
/// colors touch. The roots move on slow orbits here (one of them wanders in and
/// out on a loop of its own), so the basins pour from one arrangement into the
/// next, and `relaxation` scales the step: below 1 the method creeps and the
/// basins fatten, above 1 it overshoots and they spiral and shed islands. A
/// pixel the method never brings home is painted `trapped`, which is what the
/// dark pools are once the dial leaves 1. Everything loops in twelve seconds:
///
/// ```sh
/// swift run Example-Effects-NewtonBasins --export-loop /tmp/newton.gif
/// ```
///
/// Try it: more roots (the palette spreads around them), `shading` at 1 for
/// the contour per step, or `relaxation` held at 1 to see the plain method.
@main
final class NewtonBasins_Example: Sketch {
    private let period = 12.0
    override var loopDuration: Double? { period }

    @Param(2 ... 8, icon: "circle.hexagongrid") var roots = 3
    @Param(0.5 ... 1.8, icon: "figure.walk") var relaxation = 1.0
    @Param(0 ... 1, icon: "circle.lefthalf.filled") var shading = 0.6
    @Param(icon: "smallcircle.filled.circle") var showsRoots = true

    override func draw() {
        let paper = Color(hex: 0x14202B)
        background(paper)
        let lap = loopProgress(over: period)
        let turn = lap * .tau

        // The roots: on a ring that turns once a loop and breathes, with the
        // first one swinging in toward the middle and back out.
        let points = (0 ..< roots).map { i -> Vector2 in
            let a = Double(i) / Double(roots) * .tau + turn * 0.5
            var radius = 0.85 + 0.15 * sin(turn * 2 + Double(i))
            if i == 0 { radius *= 0.55 + 0.45 * cos(turn) }
            return Vector2(cos(a) * radius, sin(a) * radius)
        }
        let field = generate(.newton(roots: points, trapped: paper, shading: shading,
                                     relaxation: relaxation, zoom: 0.9,
                                     iterations: 64, phase: lap))
        drawImage(field.image, 0, 0)

        guard showsRoots else { return }
        // Zoom 0.9 shows 3 / 0.9 units across the shorter side, y up.
        let span = 3.0 / 0.9
        let side = min(width, height)
        noFill()
        stroke(.white)
        strokeWeight(2)
        for p in points {
            drawCircle(width / 2 + p.x / span * side, height / 2 - p.y / span * side, 7)
        }
    }
}

↑↓ move ↩ open