Ollin

Classic curves

These are the classic curve builders of generative art. Each one is a pure function of its numbers, so you pass numbers in and get geometry back. All of them are closed forms except the spirolateral, which is a walk. They return ordinary values, either [Vector2] points or a Contour, so everything downstream already works with them. That includes drawPolyline and drawShape, the shape booleans, Chaikin smoothing, hatching, and SVG export for the pen plotter. None of them read random or noise, so the same arguments always produce the same curve. That means a fixed frame renders the same way every time, and an export can be rebuilt from the same arguments.

Nine panels: a sunflower seed spiral, a woven Lissajous figure, a five-petal rose, a squircle holding a pinched four-point star, a looping spirograph curve, a decaying harmonograph tangle, a seven-lobed supershape star, a braided guilloche rosette of wavy rings, and a rough gray polygon with its smoothed version drawn over it
drawPolyline(rose(n: 5, radius: 300).points, closed: true)
drawPolyline(hypotrochoid(ring: 84, wheel: 33, pen: 26).points, closed: true)
for (i, p) in phyllotaxis(count: 600, spacing: 9).enumerated() {
    drawCircle(p.x, p.y, 2 + Double(i) * 0.008)
}

The space-filling curves (Hilbert, Peano, Gosper, and the dragon) live with the L-systems as built-in presets. The related drawing machine for any outline is Fourier epicycles. The clothoid is the curve written as a bend rather than a position. That is what rounds a corner so that the turning is smooth, not only the outline.

Contents

Phyllotaxis: the sunflower scatter

phyllotaxis(count: Int, spacing: Double, angle: Double = .goldenAngle) -> [Vector2]

This is Vogel's model of the sunflower seed head. Point i sits i * angle around the center and spacing * sqrt(i) out from it. So every new seed lands in the gap the earlier ones left, and the disk stays evenly filled at any count. The default angle is Double.goldenAngle (pi * (3 - sqrt(5)), about 137.5°). That is the "most irrational" slice of a turn, which is why the spacing works. Move it by a few hundredths of a degree and the even scatter collapses into spokes. That spoked pattern is itself a classic study.

The points are centered on the origin, so place them with translate. The array index is the seed's age, with the oldest at the center, so it is the natural handle for size and color ramps. Example: Patterns/Phyllotaxis. A pixel-field version of the same pattern lives in the effect generators as Generator.phyllotaxis.

Lissajous figures

lissajous(a: Int, b: Int, phase: Double = .pi / 2,
          width: Double, height: Double? = nil, samples: Int = 512) -> Contour

This is the curve two sine waves make together. A point swings side to side a times while it moves up and down b times. Equal frequencies give a circle, which collapses toward a line as phase goes to 0, while unequal ones weave. width and height are the figure's full extents, and height defaults to width. The returned contour is closed, centered on the origin, and exactly one period long. Shared factors cancel, so a: 2, b: 4 is the same curve as a: 1, b: 2.

Animate phase and the figure moves through its whole family. A grid of figures, with the column frequency against the row frequency, is the classic Lissajous table. Example: Motion/Lissajous.

Rose curves

rose(n: Int, d: Int = 1, radius: Double, samples: Int? = nil) -> Contour

A rose curve draws petals from one polar equation, r = radius * cos(k * theta) with k = n / d. With the default d: 1, an odd n draws n petals and an even n draws 2n. A fractional k (for example n: 7, d: 3) interleaves the petals into woven, open stars. The sampling covers exactly the span that closes the curve once, and that span depends on whether n * d is odd or even. So the geometry contains no retraced lines. Leave samples nil and the density scales with that span.

The contour starts at (radius, 0) and is centered on the origin. A rose has as many symmetry steps as petals, so rotating it by one petal per loop makes a seamless lap. Example: Patterns/Roses.

Superellipse: the squircle family

superellipse(width: Double, height: Double? = nil, n: Double = 4,
             samples: Int = 256) -> Contour

This is the Lamé curve |x/a|^n + |y/b|^n = 1. One exponent, n, covers the whole family between diamond and rectangle. n: 2 is the ellipse. n: 4 is the classic squircle, the shape of app icons and mid-century tabletops. Higher values square up toward the bounding rectangle. n: 1 is the diamond, and lower values pinch inward to a four-point star (n: 2/3 is the astroid). width and height are the full extents, and height defaults to width.

The contour is closed and centered on the origin. The sampling is uniform in the sweep angle, so at extreme exponents the spacing along the outline is uneven. Follow with resampled(spacing:) to get an evenly spaced outline for a plotter. Animate n and the mark moves between round and square. Example: Shapes/Superellipse.

Supershape: the superformula

supershape(radius: Double, m: Double = 7, n1: Double = 0.2,
           n2: Double = 1.7, n3: Double = 1.7, samples: Int = 512) -> Contour

This is the 2D superformula, the same one behind Mesh.supershape: r(θ) = (|cos(mθ/4)|^n2 + |sin(mθ/4)|^n3)^(-1/n1). A few parameters cover star, flower, gear, and organic forms. m sets the symmetry, which is the lobe count. n1/n2/n3 shape the lobes: m: 0, n*: 1 is a circle, a small n1 sharpens them, and unequal n2 and n3 lean the petals. It generalizes the rose and the superellipse into one formula with a few controls.

The contour is closed and centered on the origin. Keep m a whole number. The formula closes in one turn only for an integer m. A fractional m leaves a visible seam where the end meets the start. The formula's own radius reaches at most 1 on the symmetry axes. Between them it is clamped to 4, the same guard the mesh uses. So radius stays a true size. Drive it from time or a @Param. Example: Shapes/Supershape.

Spirograph: hypotrochoids and epitrochoids

hypotrochoid(ring: Int, wheel: Int, pen: Double, samples: Int? = nil) -> Contour
epitrochoid(ring: Int, wheel: Int, pen: Double, samples: Int? = nil) -> Contour

These are the toy gear set. A gear of radius wheel rolls around a fixed gear of radius ring. It rolls inside the ring for the hypotrochoid and outside it for the epitrochoid. The pen sits pen units from the wheel's center. Integer radii stand in for the gears' teeth, which guarantees that the pen eventually lines up with its start again. The sampling covers exactly the wheel / gcd(ring, wheel) laps that close the curve once. The pen distance sets the character of the curve. A distance less than wheel rounds the lobes. A distance equal to wheel gives cusps (the hypocycloid and epicycloid). A larger distance loops the lobes over themselves.

Both return a closed contour centered on the origin. The finished figure repeats every ring / gcd(ring, wheel) lobes around the center, so turning it by one lobe per loop is seamless. Several pen distances from one gear pair stack into the woven rosette the toy draws. For the engraved, many-ring version of that idea, see guilloche. Example: Patterns/Spirograph.

Guilloche: the rose engine

guilloche(rings: Int, innerRadius: Double, outerRadius: Double,
          rosettes: [Rosette], twist: Double = .pi / 90,
          samples: Int? = nil) -> [Contour]

guilloche(rings: Int, innerRadius: Double, outerRadius: Double,
          bumps: Int, amplitude: Double, twist: Double = .pi / 90,
          samples: Int? = nil) -> [Contour]

Rosette(bumps: Int, amplitude: Double, phase: Double = 0)

Guilloche is the engine-turned ornament of watch faces, banknotes, and certificates. The machine behind the look is a lathe whose cams rock the cutter as the piece turns. Each pass cuts one wavy ring, and the piece is turned a little between passes. This function traces the same recipe. It returns rings concentric closed curves from innerRadius out to outerRadius. Each one is r(θ) = base + Σ amplitude·sin(bumps·θ + phase) over the stacked rosettes, and ring k is rotated by k · twist. That small rotation is what weaves neighboring rings into the braided moiré.

Stack a coarse rosette and a fine one, and the fine ripple sits on the coarse wave. That is the layered look the craft is known for. One rosette is common enough to have its own overload. The result is one closed Contour per ring, innermost first, centered on the origin. So you can stroke the line-work, hatch it, and export it to SVG for a pen plotter. Keep bumps a whole number so every ring closes. Example: Patterns/Guilloche.

withState {
    translate(width / 2, height / 2)
    stroke(.white); strokeWeight(1); noFill()
    for ring in guilloche(rings: 36, innerRadius: 90, outerRadius: 320,
                          rosettes: [Rosette(bumps: 8, amplitude: 24),
                                     Rosette(bumps: 40, amplitude: 4)]) {
        drawPolyline(ring.points, closed: true)
    }
}

The harmonograph

struct Harmonograph {
    struct Pendulum {           // amplitude * sin(frequency * tau * t + phase) * exp(-damping * t)
        init(amplitude: Double = 1, frequency: Double, phase: Double = 0, damping: Double = 0)
        var amplitude: Double   // starting half-swing, canvas units
        var frequency: Double   // full swings per unit of time
        var phase: Double       // radians
        var damping: Double     // decay per unit of time; 0.01...0.05 is the classic range
    }
    init(x: [Pendulum], y: [Pendulum])
    var x: [Pendulum]           // summed into the horizontal position
    var y: [Pendulum]           // summed into the vertical

    func point(at t: Double) -> Vector2
    var settleTime: Double
    func contour(duration: Double? = nil, samples: Int? = nil) -> Contour
}

A harmonograph is the Victorian drawing machine whose pen hangs from swinging pendulums. Each pendulum loses a little energy per swing, and each one contributes one damped sine wave. The x list sums into the horizontal position and the y list into the vertical. Two pendulums per axis is the classic instrument, and one per axis draws damped Lissajous figures.

The signature look comes from near-unison detune. Frequencies like 2 against 2.01 make the trace turn slowly. Meanwhile the damping draws each lap inside the last, which weaves the nested web. You can use the machine in two ways. point(at:) with a growing t draws the figure live, so you can show the pen as it moves. contour() computes the whole trace at once. By default it runs through settleTime, the moment when the slowest-decaying pendulum has shrunk to 1% of its starting swing. There is no randomness inside, so the same pendulums always draw the same figure. Take the parameters from the sketch's seeded random and every variation draws a new figure. Example: Motion/Harmonograph.

Spirolaterals: a walk that comes home

spirolateral(order: Int, turn: Double = .pi / 2, step: Double = 1,
             reversed: Set<Int> = [], repeats: Int? = nil,
             maxRepeats: Int = 360) -> Spirolateral

struct Spirolateral {
    var points: [Vector2]       // every corner, in the order it was walked
    var contour: Contour        // the same walk, closed when it comes home
    var run: Contour            // one run on its own
    var repeats: Int            // how many runs were walked
    var closingRepeats: Int?    // how many it takes to come home, nil if never
    var closes: Bool
    var netTurn: Double         // how far one run turns the walker
    var runDisplacement: Vector2 // where one run leaves the walker, from where it started
    var center: Vector2?        // the point the figure turns about
    var length: Double
}

Step one length, turn, step two lengths, turn again, and keep going up to order lengths. Then run the whole sequence again. That is the entire rule. The figures it makes are not obvious from it: pinwheels, square knots, and walks that leave and never return. Frank Odds named and studied them in 1973, and the same walk is a standard turtle-geometry exercise.

Three panels on paper. On the left an orange spiral of seven growing steps. In the middle the same orange run inside a black square knot made of four of them. On the right a walk of eight steps repeated three times, marching off toward the bottom right instead of closing

Whether the walk closes follows from arithmetic. One run turns the walker through netTurn. So run m is run 1 rotated by m times that angle. The runs close into a ring as soon as a whole number of them makes a whole number of turns. At the classic quarter turn that means 4 / gcd(order, 4) runs. The one exception is an order that is a multiple of four. A run of that order comes back facing the way it set off, so every repeat lands further away in the same direction. The walk leaves the page. In that case closes reads false, center is nil, and the walk is drawn open.

turn has to be an exact fraction of a full turn for any of this to happen. An angle swept between two such fractions never closes, so turn is not a good value to animate. Animate the order instead, or the set of reversed steps. A reversed step turns the other way, which changes both the figure and the count.

let figure = spirolateral(order: 7, step: 26)
noFill(); stroke(.white); strokeWeight(2)
drawPolyline(fitted(figure.points, in: bounds.inset(by: 60)), closed: figure.closes)

Fit the points, as above, rather than calling scale(), because the transform would scale the stroke width with the figure. The whole figure is run turned about center, once per repeat. Draw the run in one color over the figure in another and the repeats become visible. Example: Patterns/Spirolateral.

Chaikin smoothing

Contour.smoothed(iterations: Int = 2) -> Contour
Shape.smoothed(iterations: Int = 2) -> Shape

Chaikin's corner cutting rounds a path one pass at a time. Each pass replaces every corner with two points, a quarter and three quarters of the way along its adjoining segments. Two or three passes turn any jagged polyline into a flowing curve, and a few more converge toward a smooth spline. Each pass doubles the point count, and iterations caps at 10. A closed contour rounds all the way around. An open contour keeps its exact endpoints, so a smoothed connector still reaches the same two points. The Shape form smooths every contour and keeps the winding rule.

It pairs with anything that emits raw line-work: a random walk, streamlines, L-system turtle paths, or hand-placed zigzags. Example: Shapes/CornerCutting.

Smoothing that keeps the points

Contour.smoothed(neighbors: Int, weights: SmoothingWeights = .gaussian) -> Contour
Shape.smoothed(neighbors: Int, weights: SmoothingWeights = .gaussian) -> Shape

enum SmoothingWeights { case gaussian, box }

A moving average: each point moves to the average of itself and neighbors points on either side, so the window is 2 * neighbors + 1 points wide. It gives back one point for each point it had, in the same order. That is the difference from corner cutting, which doubles the count every pass. Reach for it when something is keyed to a point's index. That might be a color per point, a phase that travels around a ring, or the matching point on a second ring. A ring that was resampled or traced off a picture loses its facets and its jitter and stays in step with all of those.

let ring = Contour((0 ..< 120).map { i in
    Vector2(angle: Double(i) / 120 * .tau, length: 200 + random(-6, 6))
}, closed: true)
let soft = ring.smoothed(neighbors: 3)                 // the bell, seven points wide
let flat = ring.smoothed(neighbors: 3, weights: .box)  // every point counts the same

.gaussian weighs the window with a bell whose standard deviation is half the window, so the nearer a neighbor is, the more it counts. Over the same window it smooths more gently than .box, the plain average, and shrinks a curve less. A closed contour's window wraps around the seam. An open contour keeps its two end points exactly. Its window reaches past an end by mirroring the curve through that end point. So the points beside an end are smoothed as fully as the rest, and a straight run stays straight. A window that would meet itself around a closed contour, or run past both ends of an open one, stops there. neighbors: 0 returns the contour as it was. The Shape form smooths every contour and keeps the winding rule.

Any average pulls a curve toward the inside of its bends. A circle of 64 points smoothed over three neighbors either side comes back 1.9% smaller under .box and 0.95% smaller under .gaussian. The factor is the same for every point of an evenly spaced circle, so scale about the centroid afterward when the size matters.


Where this comes from

These curves come from Vogel's phyllotaxis model, the classical Lissajous, rose, and trochoid parametric forms, Lamé's superellipse, and Gielis's superformula. The rest come from the rose engine's guilloche, the damped-pendulum harmonograph, Odds's spirolaterals, and Chaikin's corner-cutting algorithm. See ATTRIBUTION.md for the sources.

Go deeper

  • Geometry: Vector2, Contour, resampling, booleans, offsets
  • Fourier epicycles: rebuild any closed outline from spinning circles
  • L-systems: the space-filling curves and branching grammars
  • Export: SVG and PDF vector output, hatching for plotters

↑↓ move ↩ open