# TotemBuilder

"a monster made of a circle", built from the pieces the "Circles" chapter takes its opening figure apart into: hands, feet, a tail, eyes, an antenna, a nose, a mouth, a neck, ears, a face, and a body. Each piece has a few forms made the chapter's way, by cutting circles, mirroring, or rotating. Every mark is a disk, a ring, a crescent, or a lens, and at most one piece is red. The seed deals one form of each piece and the proportions, so every seed is a different monster. A pulse runs down the circles of his face and body, he blinks, and his hands wave together. Proof a day's pile with --export-grid.

Run it with `swift run --package-path Examples Example-Recreations-OsamuSato-TotemBuilder`.

```swift
//  Recreation after Osamu Sato - "The Art of Computer Designing: A Black and
//  White Approach" (1993, Graphic-Sha), Chapter 4, "Circles", read from the
//  scan of the book on the Internet Archive: the chapter's opening figure,
//  captioned "This picture was created with circles" (page 59), its pieces
//  laid out one by one (pages 60 and 61: hand, foot and tail, eye, antenna and
//  nose, mouth, neck, ear, face, body), and the three lessons that build them
//  (pages 62 to 67: dividing and combining, symmetry, rotation). A homage,
//  not a reproduction, and not affiliated with or endorsed by the artist.
//  https://archive.org/details/satoArtOfComputerDesigning
//
//  An original Ollin interpretation written from the printed pages. The
//  variants of each piece, their proportions, and the way they are dealt are
//  our own. Nothing was ported: the book shipped a disk of its designs, and
//  none of it was used.

import Foundation
import Ollin

/// "A monster made of a circle" (Osamu Sato, *The Art of Computer Designing*,
/// 1993, Chapter 4). The chapter takes its opening figure apart piece by
/// piece and shows how each one is made from nothing but circles, by one of
/// three moves. A hand, a foot, a tail, and an eye come from dividing circles
/// and combining them: a disk cut by another disk leaves a crescent, and two
/// disks that overlap leave a lens. An antenna, a nose, a mouth, and a neck
/// come from symmetry: half a piece is built and mirrored. An ear, a face,
/// and a body come from rotation: one circle is copied around another. The
/// captions tell the monster's story. "The gaping mouth in his stomach is
/// always hungry for food." And the program that makes his circles "can be
/// rotated to create the shape over and over, resulting in a monster who is
/// hungry his entire body over."
///
/// This sketch builds a new monster from those pieces for every seed. Each
/// piece has a few forms, all made the chapter's way, and the seed deals one
/// of each, with the counts of the rotated circles and the proportions of
/// head, neck, and body. Every mark is a disk, a ring, a crescent (a disk
/// with a disk cut out of it), or a lens (the overlap of two disks), in black
/// or the paper's white, and at most one piece is red. Everything but the
/// tail is built on the right and mirrored to the left. The tail, when there
/// is one, hangs from the middle, the one piece the chapter leaves
/// unmirrored.
///
/// The monster is still hungry. A pulse runs down the circles around his
/// face and body from top to bottom, on both sides at once. He blinks, his
/// hands wave together, his antenna bobs, and his mouth chews. `motion` is
/// how much of that there is (0 holds him still for a print), and the seed is
/// the monster. Proof a day's pile with `--export-grid`.
@main
final class TotemBuilder: Sketch {
    @Param(0 ... 1, icon: "waveform.path") var motion = 0.6

    override var canvasSize: CanvasSize { .size(1000, 1250) }

    private let paper = Color(white: 0.955)
    private let ink = Color(white: 0.06)
    private let red = Color(red: 0.85, green: 0.09, blue: 0.16)

    // MARK: Marks

    /// The only shapes a piece is made of.
    private enum Form {
        case disk(radius: Double)
        case ring(inner: Double, outer: Double)
        /// A disk with a disk cut out of it, the cut toward `angle`.
        case crescent(outer: Double, inner: Double, offset: Double, angle: Double)
        /// The overlap of two equal disks, its tips along `angle`.
        case lens(length: Double, width: Double, angle: Double)
    }

    private enum Ink { case ink, paper, accent }

    private struct Mark {
        var shape: Form
        var center: Vector2
        var ink: Ink

        /// The same mark reflected across the monster's axis.
        var mirrored: Mark {
            var m = self
            m.center = Vector2(-center.x, center.y)
            switch shape {
            case let .crescent(outer, inner, offset, angle):
                m.shape = .crescent(outer: outer, inner: inner, offset: offset, angle: .pi - angle)
            case let .lens(length, width, angle):
                m.shape = .lens(length: length, width: width, angle: .pi - angle)
            case .disk, .ring:
                break
            }
            return m
        }

        var reach: Double {
            switch shape {
            case let .disk(r): return r
            case let .ring(_, outer): return outer
            case let .crescent(outer, _, _, _): return outer
            case let .lens(length, width, _): return max(length, width) / 2
            }
        }
    }

    // MARK: The dealt monster

    /// One monster: a form for every piece, and its proportions.
    private struct Plan {
        var headRadius = 0.7
        var neckLength = 0.2
        var face = 0, faceBeads = 18
        var eyes = 0, nose = 0, ears = 0, antenna = 0, neck = 0
        var body = 0, bodyBumps = 10, bodyBeads = 12, bumpsOffset = false
        var mouth = 0, hands = 0, feet = 0
        var hasTail = false, tailCurlsRight = true
        var shoulder = 1.2      // where the arms leave the body, from the top
        var hip = 2.5           // where the feet sit, from the top
        var accent = 0          // 0 none, then eyes, nose, antenna, mouth, hands, ears
    }

    private var plan = Plan()

    override func setup() {
        noStroke()
        var p = Plan()
        p.headRadius = random(0.6, 0.84)
        p.neckLength = random(0.14, 0.32)
        p.face = Int(random(2))
        p.faceBeads = [14, 18, 22, 26][Int(random(4))]
        p.eyes = Int(random(3))
        p.nose = Int(random(3))
        p.ears = Int(random(3))
        p.antenna = Int(random(3))
        p.neck = Int(random(2))
        p.body = Int(random(2))
        p.bodyBumps = [6, 8, 10, 12][Int(random(4))]
        p.bodyBeads = [10, 12, 14, 16][Int(random(4))]
        p.bumpsOffset = random(1) < 0.5
        p.mouth = Int(random(3))
        p.hands = Int(random(2))
        p.feet = Int(random(2))
        p.hasTail = random(1) < 0.6
        p.tailCurlsRight = random(1) < 0.5
        p.shoulder = random(1.05, 1.45)
        p.hip = random(2.35, 2.7)
        p.accent = Int(random(7))
        plan = p
    }

    override func draw() {
        background(paper)
        let rest = marks(at: 0, amount: 0)
        // Fit the monster at rest to the page, the axis on the page's middle.
        var halfWidth = 0.0, top = Double.infinity, bottom = -Double.infinity
        for m in rest {
            halfWidth = max(halfWidth, abs(m.center.x) + m.reach)
            top = min(top, m.center.y - m.reach)
            bottom = max(bottom, m.center.y + m.reach)
        }
        let fit = min(width * 0.8 / (2 * halfWidth), height * 0.84 / (bottom - top))
        let origin = Vector2(width / 2, height / 2 - fit * (top + bottom) / 2)
        let breathe = 1 + 0.012 * motion * sin(time * 1.3)

        for m in marks(at: time, amount: motion) {
            switch m.ink {
            case .ink: fill(ink)
            case .paper: fill(paper)
            case .accent: fill(red)
            }
            let c = origin + m.center * (fit * breathe)
            let s = fit * breathe
            switch m.shape {
            case let .disk(r):
                drawCircle(c.x, c.y, r * s)
            case let .ring(inner, outer):
                drawRing(c.x, c.y, inner * s, outer * s)
            case let .crescent(outer, inner, offset, angle):
                withState {
                    translate(c.x, c.y)
                    rotate(angle)
                    drawMoon(0, 0, outer * s, inner * s, offset * s)
                }
            case let .lens(length, width, angle):
                withState {
                    translate(c.x, c.y)
                    rotate(angle)
                    drawVesica(0, 0, length * s, width * s)
                }
            }
        }
    }

    // MARK: Building the monster

    /// Every mark of the monster at time `t`, in body units (the body's
    /// radius is 1, its center the origin, y down), back to front. `amount`
    /// scales every motion; at 0 he stands still.
    private func marks(at t: Double, amount: Double) -> [Mark] {
        let p = plan
        var out: [Mark] = []
        /// A piece built on the right and mirrored to the left.
        func pair(_ build: () -> [Mark]) {
            let right = build()
            out += right
            out += right.map(\.mirrored)
        }
        /// A piece on the axis, symmetric by construction.
        func middle(_ build: () -> [Mark]) { out += build() }
        func ink(for piece: Int) -> Ink { p.accent == piece ? .accent : .ink }

        let headY = -(1 + p.neckLength + p.headRadius)
        let hr = p.headRadius
        let blink = amount * blinkAmount(at: t)
        let wave = amount * sin(t * 2.6)
        let bob = amount * sin(t * 3.1)
        let chew = amount * max(0, sin(t * 4.2))

        // The tail, drawn first so the body covers its root.
        if p.hasTail {
            let side = p.tailCurlsRight ? 1.0 : -1.0
            let sway = 0.12 * amount * sin(t * 1.7)
            out += [
                Mark(shape: .crescent(outer: 0.2, inner: 0.19, offset: 0.1, angle: side > 0 ? .pi + sway : sway),
                     center: Vector2(side * 0.08, 1.12), ink: .ink),
                Mark(shape: .crescent(outer: 0.19, inner: 0.18, offset: 0.1, angle: side > 0 ? sway : .pi + sway),
                     center: Vector2(side * -0.04, 1.43), ink: .ink),
                Mark(shape: .disk(radius: 0.075), center: Vector2(side * 0.1, 1.6), ink: .ink),
            ]
        }

        // The feet.
        let hipPoint = Vector2(sin(p.hip), -cos(p.hip)) * 0.96
        pair {
            switch p.feet {
            case 0:
                // A crescent opening up and out, a ball at its toe.
                return [
                    Mark(shape: .crescent(outer: 0.26, inner: 0.24, offset: 0.12, angle: -.pi / 3),
                         center: hipPoint + Vector2(0.08, 0.14), ink: .ink),
                    Mark(shape: .disk(radius: 0.09), center: hipPoint + Vector2(0.3, 0.1), ink: .ink),
                ]
            default:
                // A disk with three toes.
                let foot = hipPoint + Vector2(0.06, 0.18)
                return [Mark(shape: .disk(radius: 0.2), center: foot, ink: .ink)] + (0 ..< 3).map { i in
                    let a = 0.35 + Double(i) * 0.5
                    return Mark(shape: .disk(radius: 0.055), center: foot + Vector2(cos(a), sin(a)) * 0.24, ink: .ink)
                }
            }
        }

        // The hands, raised from the shoulders and waving together.
        let shoulderPoint = Vector2(sin(p.shoulder), -cos(p.shoulder))
        pair {
            let lift = 0.25 * wave
            let knuckle = shoulderPoint * 1.32 + Vector2(0.18, -0.28 - 0.08 * wave)
            var hand: [Mark]
            switch p.hands {
            case 0:
                // A crescent swept up from the shoulder, a knuckle, three bead fingers.
                hand = [
                    Mark(shape: .crescent(outer: 0.36, inner: 0.34, offset: 0.17, angle: -2.4 + lift),
                         center: shoulderPoint * 1.18 + Vector2(0.02, -0.04), ink: ink(for: 5)),
                    Mark(shape: .disk(radius: 0.1), center: knuckle, ink: ink(for: 5)),
                ]
            default:
                // Two crescents closing like a claw on a knuckle.
                hand = [
                    Mark(shape: .crescent(outer: 0.26, inner: 0.25, offset: 0.12, angle: -2.2 + lift),
                         center: knuckle + Vector2(-0.12, 0.2), ink: ink(for: 5)),
                    Mark(shape: .crescent(outer: 0.22, inner: 0.21, offset: 0.1, angle: 2.6 + lift),
                         center: knuckle + Vector2(0.14, 0.02), ink: ink(for: 5)),
                    Mark(shape: .disk(radius: 0.09), center: knuckle, ink: ink(for: 5)),
                ]
            }
            for f in 0 ..< 3 {
                let a = -1.9 + Double(f) * 0.62 + 0.18 * wave
                for b in 1 ... 4 {
                    let at = knuckle + Vector2(cos(a), sin(a)) * (0.07 + Double(b) * 0.075)
                    hand.append(Mark(shape: .disk(radius: 0.042 - Double(b) * 0.004), center: at, ink: ink(for: 5)))
                }
            }
            return hand
        }

        // The body: a disk whose circles are copied around it by rotation.
        middle { body(p, t: t, amount: amount, chew: chew) }

        // The neck: a stem of circles, and what the chapter's neck hangs on it.
        let neckTop = -(1 + p.neckLength) - 0.1, neckBottom = -0.9
        middle {
            (0 ..< 4).map { i in
                let y = neckBottom + (neckTop - neckBottom) * Double(i) / 3
                return Mark(shape: .disk(radius: 0.13), center: Vector2(0, y), ink: .ink)
            }
        }
        let neckY = -(1 + p.neckLength / 2)
        if p.neck == 0 {
            pair {
                [
                    Mark(shape: .disk(radius: 0.12), center: Vector2(0.1, neckY), ink: .ink),
                    Mark(shape: .disk(radius: 0.04), center: Vector2(0.1, neckY), ink: .paper),
                    Mark(shape: .crescent(outer: 0.1, inner: 0.09, offset: 0.06, angle: 0),
                         center: Vector2(0.33, neckY), ink: .ink),
                    Mark(shape: .ring(inner: 0.02, outer: 0.045), center: Vector2(0.31, neckY), ink: .ink),
                ] + (1 ... 3).map { i in
                    Mark(shape: .disk(radius: 0.03 - Double(i) * 0.005),
                         center: Vector2(0.4 + Double(i) * 0.07, neckY), ink: .ink)
                }
            }
        } else {
            pair {
                (1 ... 3).map { i in
                    Mark(shape: .disk(radius: 0.05 - Double(i) * 0.008),
                         center: Vector2(0.16 + Double(i) * 0.1, neckY), ink: .ink)
                }
            }
        }

        // The ears, behind the head.
        pair { ears(p, headY: headY, amount: amount, t: t) }

        // The face: the head's disk or ring, its rotated beads, its window.
        middle { face(p, headY: headY, t: t, amount: amount) }

        // The eyes.
        let onInk = p.face == 0
        let eyeCenter = onInk ? Vector2(0.76 * hr, headY) : Vector2(0.27 * hr, headY - 0.1 * hr)
        let eyeScale = onInk ? 1.2 : 1.1
        pair { eye(p, at: eyeCenter, scale: eyeScale * hr, onInk: onInk, blink: blink) }

        // The nose, on the axis inside the window.
        middle { nose(p, at: Vector2(0, headY + (onInk ? 0.04 : 0.24) * hr), scale: (onInk ? 1.6 : 1.1) * hr) }

        // The antenna, on top of the head.
        middle { antenna(p, top: headY - hr * 1.08, scale: hr, bob: bob) }

        return out
    }

    /// Once every few seconds the eyes close for a moment.
    private func blinkAmount(at t: Double) -> Double {
        let u = (t / 4.7).truncatingRemainder(dividingBy: 1)
        return u > 0.94 ? sin((u - 0.94) / 0.06 * .pi) : 0
    }

    /// The pulse that runs down both sides at once: 0 at rest, up to 1.
    private func pulse(fromTop angle: Double, t: Double, amount: Double) -> Double {
        amount * max(0, sin(t * 3.4 - 2.2 * abs(angle)))
    }

    /// The body: rim bumps, the disk, its ring of beads, and the window with
    /// the mouth in it.
    private func body(_ p: Plan, t: Double, amount: Double, chew: Double) -> [Mark] {
        var m: [Mark] = []
        let bumps = p.bodyBumps
        let start = p.bumpsOffset ? Double.pi / Double(bumps) : 0
        for i in 0 ..< bumps {
            let a = start + 2 * Double.pi * Double(i) / Double(bumps)
            let folded = min(a, 2 * .pi - a)
            let r = 0.1 * (1 + 0.25 * pulse(fromTop: folded, t: t, amount: amount))
            m.append(Mark(shape: .disk(radius: r), center: Vector2(sin(a), -cos(a)) * 1.0, ink: .ink))
        }
        m.append(Mark(shape: .disk(radius: 1), center: .zero, ink: .ink))
        let beads = p.bodyBeads
        let window: Double
        if p.body == 0 {
            // White rings, each with a black dot, copied around.
            for i in 0 ..< beads {
                let a = 2 * Double.pi * Double(i) / Double(beads)
                let folded = min(a, 2 * .pi - a)
                let grow = 1 + 0.2 * pulse(fromTop: folded, t: t, amount: amount)
                let c = Vector2(sin(a), -cos(a)) * 0.76
                m.append(Mark(shape: .disk(radius: 0.085 * grow), center: c, ink: .paper))
                m.append(Mark(shape: .disk(radius: 0.038 * grow), center: c, ink: .ink))
            }
            window = 0.56
        } else {
            // A white band, and a ring of white dots outside it.
            for i in 0 ..< beads {
                let a = 2 * Double.pi * Double(i) / Double(beads)
                let folded = min(a, 2 * .pi - a)
                let grow = 1 + 0.25 * pulse(fromTop: folded, t: t, amount: amount)
                m.append(Mark(shape: .disk(radius: 0.06 * grow), center: Vector2(sin(a), -cos(a)) * 0.82, ink: .paper))
            }
            m.append(Mark(shape: .ring(inner: 0.6, outer: 0.66), center: .zero, ink: .paper))
            window = 0.52
        }
        m.append(Mark(shape: .disk(radius: window), center: .zero, ink: .paper))
        m += mouth(p, window: window, chew: chew)
        return m
    }

    /// The mouth in the stomach.
    private func mouth(_ p: Plan, window w: Double, chew: Double) -> [Mark] {
        let color: Ink = p.accent == 4 ? .accent : .ink
        var m: [Mark] = []
        func both(_ mark: Mark) { m.append(mark); m.append(mark.mirrored) }
        switch p.mouth {
        case 0:
            // Crossed bones at the sides, an arch of beads, and a moustache
            // of two bowls with beads along their rims.
            let bone = Vector2(0.6 * w, -0.1 * w)
            both(Mark(shape: .disk(radius: 0.09 * w), center: bone, ink: color))
            for (dx, dy) in [(1.0, 1.0), (1.0, -1.0), (-1.0, 1.0), (-1.0, -1.0)] {
                both(Mark(shape: .disk(radius: 0.072 * w), center: bone + Vector2(dx, dy) * (0.14 * w), ink: color))
            }
            let dip = 0.07 * w * chew
            m.append(Mark(shape: .disk(radius: 0.11 * w), center: Vector2(0, -0.36 * w + dip), ink: color))
            for i in 1 ... 3 {
                let s = Double(i) / 3
                both(Mark(shape: .disk(radius: (0.11 - 0.012 * Double(i)) * w),
                          center: Vector2(0.13 * w * Double(i), (-0.36 + 0.15 * Double(i)) * w + dip * (1 - s)), ink: color))
            }
            both(Mark(shape: .crescent(outer: 0.3 * w, inner: 0.3 * w, offset: 0.15 * w, angle: -.pi / 2),
                      center: Vector2(0.28 * w, 0.42 * w), ink: color))
            for i in 0 ..< 4 {
                both(Mark(shape: .disk(radius: 0.055 * w),
                          center: Vector2(0.07 * w + Double(i) * 0.14 * w, 0.3 * w), ink: color))
            }
        case 1:
            // One gaping bowl, a bead at each corner.
            m.append(Mark(shape: .crescent(outer: 0.62 * w, inner: 0.6 * w, offset: 0.24 * w + 0.08 * w * chew,
                                           angle: -.pi / 2), center: Vector2(0, 0.06 * w), ink: color))
            both(Mark(shape: .disk(radius: 0.09 * w), center: Vector2(0.58 * w, -0.14 * w), ink: color))
        default:
            // A lens with an eye in it: the stomach looking back.
            m.append(Mark(shape: .lens(length: 1.3 * w, width: (0.52 - 0.2 * chew) * w, angle: 0),
                          center: .zero, ink: color))
            m.append(Mark(shape: .disk(radius: 0.17 * w), center: .zero, ink: .paper))
            m.append(Mark(shape: .disk(radius: 0.08 * w), center: .zero, ink: color))
            both(Mark(shape: .disk(radius: 0.07 * w), center: Vector2(0.76 * w, 0), ink: color))
        }
        return m
    }

    /// One ear, on the right of a head at `headY`.
    private func ears(_ p: Plan, headY: Double, amount: Double, t: Double) -> [Mark] {
        let hr = p.headRadius
        let color: Ink = p.accent == 6 ? .accent : .ink
        switch p.ears {
        case 0:
            // A disk with bumps copied around it, cut from the outside, three
            // dots in the cut.
            let c = Vector2(hr * 1.2, headY)
            var m: [Mark] = []
            for i in 0 ..< 7 {
                let a = Double.pi / 2 + Double(i) / 6 * .pi
                m.append(Mark(shape: .disk(radius: 0.07 * hr), center: c + Vector2(cos(a), sin(a)) * (0.34 * hr), ink: color))
            }
            m.append(Mark(shape: .crescent(outer: 0.36 * hr, inner: 0.3 * hr, offset: 0.28 * hr, angle: 0),
                          center: c, ink: color))
            for i in -1 ... 1 {
                m.append(Mark(shape: .disk(radius: 0.055 * hr),
                              center: c + Vector2(0.26 * hr, Double(i) * 0.16 * hr), ink: color))
            }
            return m
        case 1:
            // A crescent opening up and out, a dot in its bowl.
            let c = Vector2(hr * 1.14, headY - 0.3 * hr)
            let tilt = -Double.pi / 4 - 0.1 * amount * sin(t * 2.1)
            return [
                Mark(shape: .crescent(outer: 0.32 * hr, inner: 0.3 * hr, offset: 0.16 * hr, angle: tilt),
                     center: c, ink: color),
                Mark(shape: .disk(radius: 0.08 * hr), center: c + Vector2(cos(tilt), sin(tilt)) * (0.2 * hr), ink: color),
            ]
        default:
            return []
        }
    }

    /// The head: a black disk with beads around it and a white window, or a
    /// black ring with white-eyed beads around it.
    private func face(_ p: Plan, headY: Double, t: Double, amount: Double) -> [Mark] {
        let hr = p.headRadius
        let c = Vector2(0, headY)
        var m: [Mark] = []
        let n = p.faceBeads
        for i in 0 ..< n {
            let a = 2 * Double.pi * Double(i) / Double(n)
            let folded = min(a, 2 * .pi - a)
            let grow = 1 + 0.3 * pulse(fromTop: folded, t: t, amount: amount)
            let at = c + Vector2(sin(a), -cos(a)) * (hr * (p.face == 0 ? 1.1 : 1.0))
            m.append(Mark(shape: .disk(radius: 0.065 * hr * grow), center: at, ink: .ink))
            if p.face == 1 {
                m.append(Mark(shape: .disk(radius: 0.03 * hr * grow), center: at, ink: .paper))
            }
        }
        if p.face == 0 {
            m.append(Mark(shape: .disk(radius: hr), center: c, ink: .ink))
            m.append(Mark(shape: .disk(radius: 0.52 * hr), center: c, ink: .paper))
        } else {
            m.append(Mark(shape: .ring(inner: 0.56 * hr, outer: hr), center: c, ink: .ink))
            m.append(Mark(shape: .disk(radius: 0.09 * hr), center: c + Vector2(0, -1.13 * hr), ink: .ink))
        }
        return m
    }

    /// One eye, the right one. On a black ground it is drawn in the paper's
    /// white, on a white ground in black.
    private func eye(_ p: Plan, at c: Vector2, scale s: Double, onInk: Bool, blink: Double) -> [Mark] {
        let fore: Ink = p.accent == 1 ? .accent : (onInk ? .paper : .ink)
        let back: Ink = onInk ? .ink : .paper
        let open = 1 - 0.85 * blink
        switch p.eyes {
        case 0:
            // A lens with a white eye in it, a dot at each tip.
            return [
                Mark(shape: .lens(length: 0.36 * s, width: 0.17 * s * open, angle: 0), center: c, ink: fore),
                Mark(shape: .disk(radius: 0.058 * s * open), center: c, ink: back),
                Mark(shape: .disk(radius: 0.026 * s * open), center: c, ink: fore),
                Mark(shape: .disk(radius: 0.035 * s), center: c + Vector2(0.2 * s, 0), ink: fore),
                Mark(shape: .disk(radius: 0.035 * s), center: c - Vector2(0.2 * s, 0), ink: fore),
            ]
        case 1:
            // Three circles, one inside the next.
            return [
                Mark(shape: .disk(radius: 0.13 * s), center: c, ink: fore),
                Mark(shape: .disk(radius: 0.075 * s * open), center: c, ink: back),
                Mark(shape: .disk(radius: 0.034 * s), center: c, ink: fore),
            ]
        default:
            // A crescent facing the nose, a dot in its bowl.
            return [
                Mark(shape: .crescent(outer: 0.13 * s, inner: 0.12 * s, offset: 0.06 * s + 0.05 * s * blink,
                                      angle: .pi), center: c, ink: fore),
                Mark(shape: .disk(radius: 0.04 * s * open), center: c - Vector2(0.05 * s, 0), ink: fore),
            ]
        }
    }

    /// The nose, on the axis.
    private func nose(_ p: Plan, at c: Vector2, scale s: Double) -> [Mark] {
        let color: Ink = p.accent == 2 ? .accent : .ink
        switch p.nose {
        case 0:
            // A gourd of two circles between two crescents facing in.
            let side = Mark(shape: .crescent(outer: 0.11 * s, inner: 0.1 * s, offset: 0.055 * s, angle: 0),
                            center: c + Vector2(-0.24 * s, 0), ink: color)
            return [
                side, side.mirrored,
                Mark(shape: .disk(radius: 0.022 * s), center: c + Vector2(0, -0.16 * s), ink: color),
                Mark(shape: .disk(radius: 0.058 * s), center: c + Vector2(0, -0.07 * s), ink: color),
                Mark(shape: .disk(radius: 0.085 * s), center: c + Vector2(0, 0.06 * s), ink: color),
            ]
        case 1:
            // Beads falling in size.
            return (0 ..< 3).map { i in
                Mark(shape: .disk(radius: (0.035 + 0.022 * Double(i)) * s),
                     center: c + Vector2(0, (-0.14 + Double(i) * 0.11) * s), ink: color)
            }
        default:
            return [Mark(shape: .ring(inner: 0.045 * s, outer: 0.09 * s), center: c, ink: color)]
        }
    }

    /// The antenna, standing on `top`.
    private func antenna(_ p: Plan, top: Double, scale s: Double, bob: Double) -> [Mark] {
        let color: Ink = p.accent == 3 ? .accent : .ink
        var m: [Mark] = []
        func both(_ mark: Mark) { m.append(mark); m.append(mark.mirrored) }
        switch p.antenna {
        case 0:
            // A knob of circles with a row of dots out to each side.
            let s = s * 1.4
            let y = top - 0.1 * s
            m.append(Mark(shape: .disk(radius: 0.1 * s), center: Vector2(0, y), ink: color))
            m.append(Mark(shape: .disk(radius: 0.055 * s), center: Vector2(0, y - 0.13 * s), ink: color))
            m.append(Mark(shape: .disk(radius: 0.06 * s), center: Vector2(0, y + 0.12 * s), ink: color))
            both(Mark(shape: .crescent(outer: 0.09 * s, inner: 0.085 * s, offset: 0.045 * s, angle: .pi),
                      center: Vector2(0.14 * s, y), ink: color))
            for i in 0 ..< 4 {
                let x = (0.3 + Double(i) * 0.13) * s
                let lift = 0.04 * s * bob * Double(i + 1) / 4
                both(Mark(shape: .disk(radius: (0.045 - 0.004 * Double(i)) * s), center: Vector2(x, y - lift), ink: color))
            }
        case 1:
            // A stalk of beads, a ring on top.
            let lean = 0.03 * s * bob
            for i in 0 ..< 3 {
                m.append(Mark(shape: .disk(radius: (0.075 - 0.015 * Double(i)) * s),
                              center: Vector2(0, top - (0.04 + Double(i) * 0.12) * s - lean * Double(i)), ink: color))
            }
            m.append(Mark(shape: .ring(inner: 0.06 * s, outer: 0.12 * s), center: Vector2(0, top - 0.52 * s - 2 * lean), ink: color))
        default:
            // A crown: a crescent opening up, a dot on each horn, an eye in the bowl.
            let c = Vector2(0, top - 0.24 * s)
            m.append(Mark(shape: .crescent(outer: 0.48 * s, inner: 0.46 * s, offset: 0.2 * s, angle: -.pi / 2),
                          center: c, ink: color))
            both(Mark(shape: .disk(radius: 0.075 * s), center: c + Vector2(0.45 * s, -0.2 * s - 0.03 * s * bob), ink: color))
            m.append(Mark(shape: .disk(radius: 0.1 * s), center: c + Vector2(0, -0.08 * s), ink: color))
            m.append(Mark(shape: .disk(radius: 0.05 * s), center: c + Vector2(0, -0.08 * s), ink: .paper))
            m.append(Mark(shape: .disk(radius: 0.022 * s), center: c + Vector2(0, -0.08 * s), ink: color))
        }
        return m
    }
}
```
