Ollin

HommageAPaulKlee

the bands, their buckled edges, the rolled choice for every quadrilateral, and the circles, after Nake's 13/9/65 Nr. 2. An original Ollin interpretation written from the work and from Nake's own list of its chance decisions. sheetSeed is the whole piece and every seed is a new sheet; buckling, density, and circles are dials on the rolls.

Run
swift run --package-path Examples Example-Recreations-FriederNake-HommageAPaulKlee
Source
Examples/Recreations/FriederNake/HommageAPaulKlee/Sketch.swift
//  Recreation after Frieder Nake - "13/9/65 Nr. 2" (1965), the plotter drawing
//  usually called "Hommage à Paul Klee": horizontal bands whose edges buckle
//  across the sheet, with vertical lines, triangles, and circles set into them
//  by chance. A homage, not a reproduction, and not affiliated with or
//  endorsed by the artist.
//  http://dada.compart-bremen.de/item/artwork/414
//
//  An original Ollin interpretation written from the work itself and from
//  Nake's own list of the drawing's random decisions (Programm-Information
//  PI-21, quoted in the compArt database). No source was ported: the piece
//  was drawn on a Zuse Graphomat Z64 from a machine-language program for the
//  ER56 that is not the program here.

import Ollin

/// "13/9/65 Nr. 2" (Frieder Nake, 1965): the sheet is cut into horizontal
/// bands of unequal width, and the line between one band and the next bends
/// from vertex to vertex on its way across, never crossing its neighbors. Each
/// band is a row of quadrilaterals, and for every one of them the program
/// rolled a decision: leave it empty, fill it with vertical lines, or set
/// triangles in it. Then it threw a handful of circles over the whole sheet.
/// That is the entire drawing. Nake listed the chance decisions himself: the
/// widths of the bands at the left edge, the buckling of their edges, the
/// choice for each quadrilateral, how many signs it gets and where, and the
/// number, place, and size of the circles.
///
/// He named it after Paul Klee's *Hauptweg und Nebenwege* (1929), for the way
/// that painting sets a rhythm of horizontals against verticals, and it is not
/// a simulation of Klee: the painting has no circles, and its bands run the
/// other way. The drawing is one of the most reproduced images of the first
/// computer art, shown in Stuttgart in November 1965 beside Georg Nees's
/// plates.
///
/// Two things are worth noticing. The picture has two scales at once, a
/// composition of bands you read from across the room and a texture of signs
/// you read up close, and both came out of the same short list of rolls. And
/// the vertical lines are the only thing that varies in *amount*: a dense cell
/// reads as a dark patch, a sparse one as a pale one, so the sheet has tone
/// though every mark is the same black line.
///
/// Try it: `sheetSeed` is the whole piece, and every seed is a different
/// sheet from the same rules. `buckling` is how far the band edges may wander
/// (0 rules them straight), `density` scales how many signs a filled cell
/// gets, and `circles` is how many are thrown. The lines export as strokes, so
/// `--export-svg` gives the plotter drawing back.
@main
final class HommageAPaulKlee: Sketch {
    @Param(4 ... 14, icon: "rectangle.split.3x1") var bands = 8.0
    @Param(0 ... 1, icon: "waveform.path") var buckling = 0.6
    @Param(0 ... 2, icon: "line.3.horizontal") var density = 1.0
    @Param(0 ... 20, icon: "circle") var circles = 9.0
    @Param(1 ... 999, icon: "dice") var sheetSeed = 13.0

    override func draw() {
        background(Color(hex: 0xF4F1EA))
        noFill()
        stroke(.black)
        strokeWeight(1.5)
        randomSeed(Int(sheetSeed.rounded()))

        // The sheet: a square with a margin, like the 50 cm plate.
        let side = min(width, height) * 0.86
        let sheet = Rectangle(center: center, width: side, height: side)

        // 1. The band widths, rolled at the left edge.
        let count = Int(bands.rounded())
        let weights = (0 ..< count).map { _ in random(0.35, 1.65) }
        let total = weights.reduce(0, +)
        var bases: [Double] = [sheet.y]
        for w in weights { bases.append(bases[bases.count - 1] + w / total * side) }

        // 2. The edges buckle from vertex to vertex on the way across, each
        //    kept to a share of the band above and the band below so no two
        //    ever cross. Every edge has its own stations, so the kinks never
        //    line up down the sheet.
        var edges: [[Vector2]] = []
        for i in 0 ... count {
            let stations = Int(random(6, 11))
            let above = i > 0 ? bases[i] - bases[i - 1] : 0
            let below = i < count ? bases[i + 1] - bases[i] : 0
            let up = -0.45 * above * buckling, down = 0.45 * below * buckling
            var points: [Vector2] = []
            for k in 0 ... stations {
                var x = sheet.x + Double(k) / Double(stations) * side
                if k > 0, k < stations { x += random(-0.3, 0.3) * side / Double(stations) }
                let y = bases[i] + (up < down ? random(up, down) : 0)
                points.append(Vector2(x, y))
            }
            edges.append(points)
            drawPolyline(points)
        }

        // 3. Each quadrilateral of a band, cut at its upper edge's stations,
        //    rolls its own decision: empty, vertical lines, or triangles, then
        //    how many and where.
        for i in 0 ..< count {
            let upper = edges[i], lower = edges[i + 1]
            for k in 0 ..< upper.count - 1 {
                let x0 = upper[k].x, x1 = upper[k + 1].x
                let choice = random(0, 1)
                if choice < 0.42 { continue }
                if choice < 0.82 {
                    let lines = Int(random(1, 24) * density)
                    for _ in 0 ..< lines {
                        let x = random(x0, x1)
                        drawLine(Vector2(x, height(of: upper, at: x)),
                                 Vector2(x, height(of: lower, at: x)))
                    }
                } else {
                    let signs = Int(random(1, 6) * density)
                    for _ in 0 ..< signs {
                        let x = random(x0, x1)
                        let top = height(of: upper, at: x), bottom = height(of: lower, at: x)
                        let room = bottom - top
                        let size = random(0.15, 0.5) * room
                        let base = bottom - random(0, room - size)
                        drawTriangle(Vector2(x - size / 2, base), Vector2(x + size / 2, base),
                                     Vector2(x, base - size))
                    }
                }
            }
        }

        // 4. The circles: where, and how big.
        for _ in 0 ..< Int(circles.rounded()) {
            let radius = random(0.012, 0.075) * side
            drawCircle(random(sheet.x + radius, sheet.x + side - radius),
                       random(sheet.y + radius, sheet.y + side - radius), radius)
        }
    }

    /// Where a buckled edge sits at `x`, read off its vertices.
    private func height(of edge: [Vector2], at x: Double) -> Double {
        guard let first = edge.first, let last = edge.last else { return 0 }
        if x <= first.x { return first.y }
        for k in 1 ..< edge.count where x <= edge[k].x {
            let a = edge[k - 1], b = edge[k]
            let t = b.x > a.x ? (x - a.x) / (b.x - a.x) : 0
            return a.y + (b.y - a.y) * t
        }
        return last.y
    }
}

↑↓ move ↩ open