Ollin

GestureMarks

one drawn stroke, heavy at the start and lifting toward the end, made into many small marks laid along it: a cloud of discs, a chain of squares, a spiked band, rows of dashes, dots, and strips, a feather, leaves, a fringe, pixels, facets, lanes, a coil, strands, fragments, a wiggle, and the blob a lasso draws round, plain, spiked, or wobbling. A pen plays the stroke back and nothing behind it moves; drag to draw your own (GestureMark)

Run
swift run --package-path Examples Example-Patterns-GestureMarks
Source
Examples/Patterns/GestureMarks/Sketch.swift
import Ollin

/// Marks built from one gesture: a path, a width at every point of it, and a
/// seed turned into geometry a plotter can draw. One picker across the
/// families of `GestureMark`: a cloud of discs, a chain of rounded squares,
/// a band with spikes; dashes, dots, and strips in rows across the width; a
/// feather, leaves, and a fringe sprouting off both sides; the ribbon as
/// pixels, facets, and lanes; a spiral, strands, fragments, and a wiggle
/// around the path; and the regions a gesture encloses, plain, spiked, and
/// wobbling.
///
/// A hand that is not there draws one long stroke across the sheet, heavy
/// where it starts and lifting toward the end, and the marks follow its
/// taper. The marks are rebuilt from the pen every frame, and nothing behind
/// the pen moves: each piece is placed by its distance from the start.
///
/// Drag to draw your own gesture in the chosen family; `c` clears what you
/// drew. Every piece is a filled outline or a line, so the sheet plots
/// straight to SVG with `--export-svg`.
@main
final class GestureMarks: Sketch {
    enum Family: String, CaseIterable, ParamOption {
        case cloud, chain, spikes, dashes, dots, strips, feather, leaves, fringe
        case pixels, facets, lanes, spiral, strands, fragments, wiggle
        case blob, spikedBlob, wobblyBlob
    }

    @Param(icon: "scribble.variable") var family = Family.cloud
    @Param("Width", 30 ... 180, icon: "lineweight") var weight = 110.0
    @Param("Seed", 1 ... 99, icon: "dice") var seed = 7
    @Param("Pen draws it", icon: "pencil.and.outline") var penDraws = true

    override var loopDuration: Double? { 10 }

    let paper = Color(hex: 0xF3EEE3)
    let ink = Color(hex: 0x1F2B5B)
    let palette = [Color(hex: 0x1F2B5B), Color(hex: 0xC4473A), Color(hex: 0xE0A43A),
                   Color(hex: 0x2F7A78), Color(hex: 0xE07A8F)]

    /// The pointer's gesture in progress, and the ones already drawn.
    var mark = StrokeMark()
    var drawn: [StrokeMark] = []

    override func draw() {
        background(paper)

        // The hand's stroke grows over three quarters of the loop and holds.
        let progress = penDraws ? smoothstep(0, 0.75, loopProgress(over: 10)) : 1
        let (points, widths) = handStroke(seed: seed)
        let count = max(2, Int(progress * Double(points.count)))
        draw(GestureMark(points: Array(points.prefix(count)), widths: Array(widths.prefix(count))))

        for gesture in drawn { draw(GestureMark(gesture, width: weight * 0.6)) }
        if mouseIsPressed { record(into: &mark) }
        if mark.count > 1 { draw(GestureMark(mark, width: weight * 0.6)) }

        // The caption in ink, since the sheet is light.
        noStroke()
        fill(Color(white: 0.4))
        textSize(20)
        textAlign(.center, .bottom)
        drawText("\(family.rawValue)  ·  drag to draw  ·  c clears", width / 2, height - 28)
    }

    override func mouseReleased() {
        if mark.count > 1 { drawn.append(mark) }
        mark = StrokeMark()
    }

    override func keyPressed() {
        if key == "c" { drawn = []; mark = StrokeMark() }
    }

    /// One gesture drawn in the chosen family.
    func draw(_ gesture: GestureMark) {
        withState {
            strokeCap(.round)
            strokeJoin(.round)
            switch family {
            case .cloud: fillOne(gesture.cloud(variation: 0.7, seed: seed).shape, ink)
            case .chain: fill(gesture.chain(seed: seed)) { 3 * ($0.index % 2) }
            case .spikes: fillOne(gesture.spikes(seed: seed).shape, palette[1])
            case .dashes: fill(gesture.dashes(rows: 4, seed: seed)) { $0.row }
            case .dots: fill(gesture.dots(rows: 3, seed: seed)) { $0.row + $0.index }
            case .strips: fill(gesture.strips(rows: 3, seed: seed)) { $0.row + 2 * $0.index }
            case .feather: stroke(gesture.feather(seed: seed), weight: 1.6) { $0.index / 6 }
            case .leaves: fill(gesture.leaves(seed: seed)) { 3 - 2 * $0.row }
            case .fringe: stroke(gesture.fringe(seed: seed), weight: 1.2) { 1 + $0.row }
            case .pixels: fill(gesture.pixels(cell: 12, seed: seed)) { 2 * $0.row }
            case .facets:
                noStroke()
                for piece in gesture.facets(seed: seed) {
                    let fade: Double = 0.12 * Double(piece.row) + 0.05 * Double(piece.index % 3)
                    fill(ink.mixed(with: paper, fade))
                    drawShape(piece.shape)
                }
            case .lanes: fill(gesture.lanes(count: 4, seed: seed)) { $0.row }
            case .spiral: stroke(gesture.spiral(cycle: 0.35), weight: 2.4) { _ in 0 }
            case .strands: stroke(gesture.strands(count: 6, seed: seed), weight: 2) { $0.row }
            case .fragments: stroke(gesture.fragments(density: 1.4, seed: seed), weight: 2.2) { $0.index }
            case .wiggle: stroke(gesture.wiggle(wavelength: 0.7, amplitude: 0.8), weight: 4) { _ in 1 }
            case .blob, .spikedBlob, .wobblyBlob:
                var region = gesture.blob()
                if family == .spikedBlob { region = region.spiked(48, height: 26, seed: seed) }
                if family == .wobblyBlob { region = region.wobbled(by: 18, scale: 90, seed: seed) }
                fillOne(gesture.hull(), palette[2].withAlpha(0.35))
                fillOne(region, palette[4])
                noFill()
                stroke(ink)
                strokeWeight(3)
                drawPolyline(gesture.points)
            }
        }
    }

    /// A whole region in one color.
    func fillOne(_ shape: Shape, _ color: Color) {
        noStroke()
        fill(color)
        drawShape(shape)
    }

    /// Closed pieces filled from the palette by `pick`.
    func fill(_ pieces: [GestureMark.Piece], _ pick: (GestureMark.Piece) -> Int) {
        noStroke()
        for piece in pieces {
            fill(palette[pick(piece) % palette.count])
            drawShape(piece.shape)
        }
    }

    /// Open pieces stroked from the palette by `pick`.
    func stroke(_ pieces: [GestureMark.Piece], weight: Double, _ pick: (GestureMark.Piece) -> Int) {
        noFill()
        strokeWeight(weight)
        for piece in pieces {
            stroke(palette[pick(piece) % palette.count])
            drawPolyline(piece.contour.points)
        }
    }

    /// The hand's stroke: a sweep across the sheet through five points that
    /// the seed nudges, sampled every few units, heavy at the start and
    /// lifting toward the end.
    func handStroke(seed: Int) -> (points: [Vector2], widths: [Double]) {
        randomSeed(seed)
        let route: [Vector2] = [Vector2(150, 600), Vector2(350, 330), Vector2(580, 520), Vector2(760, 790),
                                Vector2(930, 470)]
        let knots: [Vector2] = route.map { (point: Vector2) -> Vector2 in
            point + Vector2(random(-50, 50), random(-70, 70))
        }
        let points = Contour(curveThrough: knots, closed: false).resampled(spacing: 4).points
        let widths: [Double] = points.indices.map { (i: Int) -> Double in
            let u = Double(i) / Double(max(points.count - 1, 1))
            let lift: Double = 1 - 0.7 * u
            let press: Double = 0.35 + 0.65 * smoothstep(0, 0.05, u)
            return weight * lift * press
        }
        return (points, widths)
    }
}

↑↓ move ↩ open