Ollin

PerfectLoop

a sketch that repeats exactly: loopDuration declares the period, looping noise closes the motion, and --export-loop renders one seamless lap

Run
swift run --package-path Examples Example-Motion-PerfectLoop
Source
Examples/Motion/PerfectLoop/Sketch.swift
import Ollin

/// A sketch that repeats exactly, and says so. `loopDuration` declares the
/// period (6 seconds here); every moving part is driven by `loopProgress` and
/// looping `signedNoise`, so the frame one period later is identical to frame
/// zero. That declaration is what powers the perfect-loop export:
///
///     swift run Example-Motion-PerfectLoop --export-loop loop.gif
///
/// renders exactly one lap, and the GIF (or .mp4/.mov) cycles seamlessly.
/// Three nested blobs sample loop noise on a circle, closed in angle by the
/// circular sample path and closed in time by the loop phase; a comet travels
/// the outer edge, one full turn per period, to make the lap visible.
@main
final class PerfectLoop: Sketch {
    let period = 6.0
    override var loopDuration: Double? { period }

    override func setup() {
        seed(11)   // pin the noise field, so every run loops the same lap
    }

    override func draw() {
        background(Color(hex: 0x0E1420))
        let phase = loopProgress(over: period)
        let center = center

        // Nested blobs, each wobbling on its own slice of the looping field.
        let layers: [(scale: Double, color: Color)] = [
            (1.00, Color(hex: 0x27446B)),
            (0.68, Color(hex: 0x4E8098)),
            (0.40, Color(hex: 0xA8D0B0)),
        ]
        noStroke()
        for (l, layer) in layers.enumerated() {
            var points: [Vector2] = []
            for i in 0 ..< 220 {
                let a = Double(i) / 220 * .tau
                points.append(center + Vector2(angle: a, length: edgeRadius(a, layer: l)) * layer.scale)
            }
            fill(layer.color)
            drawPolygon(points)
        }

        // The lap made visible: a comet takes one full turn per period.
        let head = phase * .tau
        for k in stride(from: 24, through: 0, by: -1) {
            let a = head - Double(k) * 0.045
            let p = center + Vector2(angle: a, length: edgeRadius(a, layer: 0))
            let fade = 1 - Double(k) / 25
            fill(Color(hex: 0xFFD98A, alpha: 0.12 + 0.88 * fade * fade))
            drawCircle(center: p, radius: 4 + 12 * fade)
        }
    }

    /// The outer edge of a blob layer at angle `a`: a base radius plus loop
    /// noise sampled on a circle, so the wobble closes in angle and in time.
    func edgeRadius(_ a: Double, layer l: Int) -> Double {
        let wobble = signedNoise(cos(a) * 0.8 + Double(l) * 3.7, sin(a) * 0.8,
                                 loop: loopProgress(over: period) + Double(l) * 0.33,
                                 radius: 1.4)
        return 356 + wobble * 72
    }
}

↑↓ move ↩ open