Ollin

EllipseField

rows of white outlines in two drifting columns: whole drawEllipse rings on the left and chord-closed drawArc crescents on the right, with position, squash, and arc sweep all driven by signedNoise

Run
swift run --package-path Examples Example-Motion-EllipseField
Source
Examples/Motion/EllipseField/Sketch.swift
//  Based on elements of @eaviles's sketch 2023.023. Reworked for Ollin's API.

import Ollin

/// Stacked rows of white outlines in two drifting columns: whole ellipses on
/// the left, chord-closed elliptical arcs on the right. Each row's horizontal
/// position, size, and vertical squash are read from a `signedNoise` field
/// sampled against its row `y` and `time`, so the whole field breathes and
/// wanders without ever repeating. The arc column samples two more slices of
/// the field for where each arc begins and how far it sweeps, so those rings
/// open and close into crescents as the field drifts. The fill matches the
/// background, so each shape's disk hides the ones behind it: overlapping
/// outlines occlude front-to-back instead of all showing through.
@main
final class EllipseField: Sketch {
    override func setup() {
        fill(.black)   // opaque centers, so each shape occludes the ones behind it
        stroke(.white)
    }

    override func draw() {
        background(.black)
        let dim = shortSide / 100
        strokeWeight(dim * 0.25)

        // Rows run from just above the top to just below the bottom, so shapes
        // enter and leave the frame instead of clipping at the edges.
        let margin = 0.125
        let minY = -height * margin
        let maxY = height * (1 + margin)
        let rows = 72.0
        let jump = (maxY - minY) / rows

        var y = minY
        while y <= maxY {
            ring(seed: 1000, offset: -width / 6, y: y, arcs: false)   // left column: ellipses
            ring(seed: 2000, offset:  width / 6, y: y, arcs: true)    // right column: arcs
            y += jump
        }
    }

    /// One shape for a row. `seed` decorrelates the two columns by sampling
    /// different cells of the noise field; `offset` shifts the column
    /// sideways; `arcs` swaps the whole ellipse for a chord-closed arc.
    private func ring(seed: Int, offset: Double, y: Double, arcs: Bool) {
        let t = time * 0.12

        // Horizontal position: a slow wander across the middle band, plus a
        // finer, faster jitter layered on top.
        var n = signedNoise(Double(seed), t * 2, y * 0.002)
        var x = map(n, -1, 1, width * 0.3, width * 0.7) + offset
        n = signedNoise(Double(seed), t * 2, y * 0.02)
        x += map(n, -1, 1, 0, width * 0.025)

        // Size and vertical squash, each from its own slice of the field.
        n = signedNoise(Double(seed + 1), t, -y * 0.0017)
        let rx = map(n, -1, 1, width * 0.05, width * 0.13)
        n = signedNoise(Double(seed + 4), t, y * 0.004)
        let squash = map(n, -1, 1, 0.4, 1.0)

        if arcs {
            // Arc span, from two more slices: where it begins and how far it
            // sweeps.
            n = signedNoise(Double(seed + 2), -t, y * 0.0014)
            let begin = map(n, -1, 1, 0, .tau)
            n = signedNoise(Double(seed + 3), -t, -y * 0.002)
            let sweep = map(n, -1, 1, .pi * 0.25, .tau * 0.85)
            drawArc(x, y, rx, rx * squash, start: begin, stop: begin + sweep, mode: .chord)
        } else {
            drawEllipse(x, y, rx, rx * squash)
        }
    }
}

↑↓ move ↩ open