Ollin

PaintedFills

shapes filled as what they are made of (fillMedium): impasto that rises along its outline and catches the light, paint spread with a knife, a wash that glazes the grid and gouache that covers it, chrome and glass that reflect and bend it, faceted glass, rock, marble, and sand. The blobs take their colors from a palette scheme around one hue (Palette.scheme). Drag a loop to add a blob

Run
swift run --package-path Examples Example-Shapes-PaintedFills
Source
Examples/Shapes/PaintedFills/Sketch.swift
import Ollin

/// Shapes filled as what they are made of: thick paint that rises along its
/// outline and catches the light, paint spread with a knife, a wash that
/// glazes the grid under it and gouache that covers it, chrome that reflects
/// the grid around it, plain and faceted glass that bend it, and rock, marble,
/// and sand. One picker across the ten media, each set with `fillMedium(_:)`
/// beside `fill`.
///
/// The blobs take their colors from a palette scheme built around one hue
/// (`Palette.scheme`): a single hue from deep to light, a wheel whose near half
/// is vivid and far half dark, the vivid wheel, or a pastel or dark cluster.
/// The swatches along the bottom are the palette itself. Turn the hue to move
/// the whole scheme around the wheel.
///
/// The blobs sway in place, so the light plays across their relief and the
/// glass slides over the grid. Drag a loop to add a blob of your own in the
/// next color; `c` clears what you drew.
@main
final class PaintedFills: Sketch {
    enum Medium: String, CaseIterable, ParamOption {
        case impasto, knife, wash, gouache, chrome, glass, facetedGlass, rock, marble, sand

        var fill: FillMedium {
            switch self {
            case .impasto: .impasto
            case .knife: .knife
            case .wash: .wash
            case .gouache: .gouache
            case .chrome: .chrome
            case .glass: .glass
            case .facetedGlass: .facetedGlass
            case .rock: .rock
            case .marble: .marble
            case .sand: .sand
            }
        }
    }

    enum Scheme: String, CaseIterable, ParamOption {
        case monochrome, vividAndDark, vivid, pastel, dark

        var scheme: Palette.Scheme {
            switch self {
            case .monochrome: .monochrome
            case .vividAndDark: .vividAndDark
            case .vivid: .vivid
            case .pastel: .pastel
            case .dark: .dark
            }
        }
    }

    @Param(icon: "paintbrush") var medium = Medium.impasto
    @Param(icon: "swatchpalette") var scheme = Scheme.vividAndDark
    @Param("Hue", 0 ... 1, icon: "circle.lefthalf.filled") var hue = 0.07

    override var loopDuration: Double? { 12 }

    /// The blobs: each outline in its own coordinates around the origin, where
    /// it stands, and its color's place in the palette.
    var blobs: [(shape: Shape, center: Vector2, slot: Int)] = []
    /// The loop under the pointer while it is dragged.
    var loop: [Vector2] = []

    let paper = Color(red: 0.95, green: 0.93, blue: 0.89)
    let ink = Color(red: 0.16, green: 0.17, blue: 0.22)

    override func setup() {
        let places = [Vector2(300, 300), Vector2(770, 280), Vector2(540, 560),
                      Vector2(280, 760), Vector2(790, 760)]
        for (i, place) in places.enumerated() {
            blobs.append((outline(radius: 150 + Double(i % 2) * 25, seed: Double(i) * 1.7), place, i))
        }
    }

    override func draw() {
        let palette = Palette.scheme(scheme.scheme, key: .mostVivid(hue: hue), count: 5)
        background(paper)
        drawGround()

        // Each blob sways around its own middle, by its own phase.
        let t = loopProgress(over: 12) * .tau
        noStroke()
        fillMedium(medium.fill)
        for (i, blob) in blobs.enumerated() {
            withState {
                translate(blob.center + Vector2(0, 10 * sin(t + Double(i))))
                rotate(0.22 * sin(t + Double(i) * 1.3))
                fill(palette[blob.slot])
                drawShape(blob.shape)
            }
        }
        noFillMedium()

        // The loop being dragged, as a line.
        if mouseIsPressed, let last = loop.last, (mouse - last).length > 6 { loop.append(mouse) }
        if loop.count > 1 {
            noFill()
            stroke(ink)
            strokeWeight(3)
            drawPolyline(loop)
        }

        // The palette, flat, and the caption.
        noStroke()
        for i in 0 ..< palette.count {
            fill(palette[i])
            drawRect(40 + Double(i) * 64, height - 96, 56, 40, cornerRadius: 6)
        }
        fill(ink)
        textSize(20)
        textAlign(.left, .bottom)
        drawText("\(medium.rawValue)  ·  \(scheme.rawValue)  ·  drag a loop to add one  ·  c clears",
                 40, height - 24)
    }

    /// The paper's grid, which the wash glazes, the chrome reflects, and the
    /// glass bends.
    func drawGround() {
        noFillMedium()
        stroke(ink)
        strokeWeight(4)
        for x in stride(from: 27.0, to: width, by: 54) { drawLine(x, 0, x, height) }
        strokeWeight(2)
        for y in stride(from: 27.0, to: height, by: 54) { drawLine(0, y, width, y) }
    }

    /// A rounded blob around the origin, its radius wandering by a few slow waves.
    func outline(radius: Double, seed: Double) -> Shape {
        let points = (0 ..< 180).map { i -> Vector2 in
            let a = Double(i) / 180 * .tau
            let r = radius * (1 + 0.13 * sin(3 * a + seed) + 0.07 * sin(5 * a + 2 * seed) + 0.04 * cos(2 * a - seed))
            return Vector2(cos(a), sin(a)) * r
        }
        return Shape(contours: [Contour(points, closed: true)])
    }

    override func mousePressed() {
        loop = [mouse]
    }

    override func mouseReleased() {
        defer { loop = [] }
        guard loop.count > 8 else { return }
        // The loop, closed and smoothed, around its own middle.
        let middle = loop.reduce(Vector2(0, 0), +) / Double(loop.count)
        let shape = Shape(curveThrough: loop.map { $0 - middle })
        blobs.append((shape, middle, blobs.count % 5))
    }

    override func keyPressed() {
        if key == "c" { blobs.removeLast(max(blobs.count - 5, 0)) }
    }
}

↑↓ move ↩ open