Ollin

Formula

six parameters driven by rules typed as text rather than written in Swift, and the sketch prints the rule behind each one under the ring (drive, Formula)

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

/// A parameter does not have to be a number. It can be a rule, typed as text and
/// worked out every frame: `drive($radius, "190 + sin(time * tau / 6) * 80")`.
///
/// The five parameters below are all driven that way, and the sketch prints the very
/// text that drives them under the ring, beside what each one holds right now.
/// `wobble` reads the sketch's own noise field, so `noiseSeed()` reproduces it.
/// `edge` is worked out from `radius`, which lands on the same frame, because a
/// parameter named by another is always set first.
///
/// A formula is a track like any other, so it travels in the same file
/// (`--automation`) and the same `loops` and `speed` shape it. See
/// Docs/Helpers/Formula.md.
@main
final class Formulas: Sketch {
    @Param(80...460) var radius = 200.0
    @Param(3...48) var count = 12
    @Param(0...360) var spin = 0.0
    @Param(-80...80) var wobble = 0.0
    @Param(2...40) var edge = 6.0
    @Param var filled = true

    /// The parameters in the order the listing shows them.
    static let shown = ["radius", "count", "spin", "wobble", "edge", "filled"]

    override func setup() {
        noiseSeed(11)
        drive($radius, "190 + sin(time * tau / 6) * 80")
        drive($count, "8 + round(sin(time * tau / 12) * 5)")
        drive($spin, "time * 24")
        drive($wobble, "signedNoise(time * 0.6) * 70")
        drive($edge, "radius / 22")
        drive($filled, "time % 6 < 3")
    }

    override func draw() {
        background(.white)
        drawRing()
        drawListing()
    }

    // MARK: The ring

    private func drawRing() {
        let center = Vector2(width / 2, 380)
        let ring = radius + wobble

        noFill()
        stroke(Color(white: 0.9))
        strokeWeight(1.5 * scale)
        drawCircle(center: center, radius: ring)

        for i in 0..<count {
            let angle = (Double(i) / Double(count)) * .tau + spin * .pi / 180
            let at = center + Vector2(angle: angle) * ring
            if filled {
                noStroke()
                fill(Color.coral)
            } else {
                noFill()
                stroke(Color(hex: 0x2E6BE6))
                strokeWeight(edge / 3)
            }
            drawCircle(center: at, radius: edge)
        }
    }

    // MARK: The listing

    /// Read the tracks back and print the text that drives each parameter, so the
    /// picture says what made it.
    private func drawListing() {
        var top = 800.0
        textSize(21 * scale)
        for name in Self.shown {
            guard let track = automation?.track(named: name),
                  let formula = track.formula else { continue }
            noStroke()
            fill(Color(white: 0.55))
            textAlign(.right, .middle)
            drawText(name, 250, top)

            fill(Color(white: 0.2))
            textAlign(.left, .middle)
            drawText(formula.source, 274, top)

            fill(Color(white: 0.6))
            textAlign(.right, .middle)
            drawText(reading(of: name), width - 140, top)
            top += 42
        }
    }

    /// What the parameter holds this frame, rounded to something readable.
    private func reading(of name: String) -> String {
        switch name {
        case "radius": return String(format: "%.1f", radius)
        case "count": return "\(count)"
        case "spin": return String(format: "%.1f", spin)
        case "wobble": return String(format: "%.2f", wobble)
        case "edge": return String(format: "%.2f", edge)
        case "filled": return filled ? "on" : "off"
        default: return ""
        }
    }
}

↑↓ move ↩ open