Ollin

FormulaParts

a parameter made of several numbers, with one rule per part: a rectangle whose size follows a rule and whose place does not, a dot that reads the rectangle back part by part, a color under a rule, and a pair of ends under a rule (drive(_:x:y:))

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

/// A parameter does not have to hold one number. A point holds two, a color holds
/// four, a pair of ends holds two more. Each part takes its own rule:
/// `drive($eye, x: "…", y: "…")`.
///
/// The frame below breathes because its `width` and `height` are rules, while
/// its `x` and `y` are left alone: a part with no rule stays where the hand put
/// it. The dot reads the frame it sits in, part by part, and the ink is three
/// rules of its own. The listing under the picture prints every rule beside
/// what its part holds this frame.
///
/// See Docs/Helpers/Formula.md.
@main
final class FormulaParts: Sketch {
    @Param(x: 0...1080, y: 0...1080, width: 40...900, height: 40...900)
    var frame = Rectangle(x: 190, y: 120, width: 700, height: 420)
    @Param(x: 0...1080, y: 0...1080) var eye = Vector2(540, 330)
    @Param var ink = Color(red: 0.2, green: 0.4, blue: 0.9, alpha: 1)
    @Param(in: 0...1) var band = 0.2...0.8

    /// The parts in the order the listing shows them, parameter by parameter.
    static let shown = [("frame", ["width", "height"]),
                        ("eye", ["x", "y"]),
                        ("ink", ["red", "green", "blue"]),
                        ("band", ["lower", "upper"])]

    override func setup() {
        drive($frame, width: "620 + sin(time * tau / 7) * 220",
                      height: "360 + cos(time * tau / 5) * 140")
        drive($eye, x: "frame.x + frame.width * (0.5 + sin(time * tau / 4) * 0.42)",
                    y: "frame.y + frame.height * (0.5 + cos(time * tau / 6) * 0.42)")
        drive($ink, red: "0.35 + sin(time * tau / 9) * 0.3",
                    green: "0.28 + sin(time * tau / 11) * 0.22",
                    blue: "0.6 + sin(time * tau / 7) * 0.25")
        drive($band, lower: "0.1 + sin(time * tau / 8) * 0.08",
                     upper: "0.6 + sin(time * tau / 6) * 0.35")
    }

    override func draw() {
        background(.white)
        drawFrame()
        drawEye()
        drawListing()
    }

    // MARK: The picture

    /// The frame, and the part of it the band names, drawn as a filled slab.
    private func drawFrame() {
        let top = frame.y + frame.height * band.lowerBound
        let deep = frame.height * (band.upperBound - band.lowerBound)
        noStroke()
        fill(ink.withAlpha(0.14))
        drawRect(frame.x, top, frame.width, deep)

        noFill()
        stroke(Color(white: 0.75))
        strokeWeight(1.5 * scale)
        drawRect(frame)
    }

    /// The dot, wherever the two rules put it inside the frame.
    private func drawEye() {
        noFill()
        stroke(ink)
        strokeWeight(2 * scale)
        drawLine(frame.x, eye.y, frame.x + frame.width, eye.y)
        drawLine(eye.x, frame.y, eye.x, frame.y + frame.height)

        noStroke()
        fill(ink)
        drawCircle(center: eye, radius: 16 * scale)
    }

    // MARK: The listing

    /// Read the tracks back and print the rule driving each part, so the
    /// picture says what made it.
    private func drawListing() {
        var top = 660.0
        textSize(19 * scale)
        for (name, parts) in Self.shown {
            guard let track = automation?.track(named: name) else { continue }
            for part in parts {
                guard let rule = track.parts[part] else { continue }
                noStroke()
                fill(Color(white: 0.55))
                textAlign(.right, .middle)
                drawText("\(name).\(part)", 250, top)

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

                fill(Color(white: 0.6))
                textAlign(.right, .middle)
                drawText(reading(of: name, part), width - 90, top)
                top += 33
            }
            top += 8
        }
    }

    /// What one part holds this frame, rounded to something readable.
    private func reading(of name: String, _ part: String) -> String {
        guard let track = automation?.track(named: name),
              track.parts[part] != nil else { return "" }
        let value: Double
        switch (name, part) {
        case ("frame", "width"): value = frame.width
        case ("frame", "height"): value = frame.height
        case ("eye", "x"): value = eye.x
        case ("eye", "y"): value = eye.y
        case ("ink", "red"): value = ink.red
        case ("ink", "green"): value = ink.green
        case ("ink", "blue"): value = ink.blue
        case ("band", "lower"): value = band.lowerBound
        case ("band", "upper"): value = band.upperBound
        default: return ""
        }
        return String(format: value < 10 ? "%.3f" : "%.1f", value)
    }
}

↑↓ move ↩ open