Functional Power For Patterning

Functional Programming has many tools and tricks for writing elegant code. Here we see how they can be applied to make interesting patterns.

A round of regular polygons

Let's start with a simple function that makes a ring of n copies of an n-sided regular polygon.

The code for this is simple :

; set up some colours
(def col (p-color 140 220 180))
(def col-f (p-color 190 255 200 100))

; define the function
(defn a-round [n] (clock-rotate n (poly 0 0.5 0.3 n {:stroke col :stroke-weight 3 :fill col-f} )))

; draw it
(a-round 8)

As described previously, we can place this pattern into a grid.

(Note the change in stroke-weight)

(defn a-round [n] (clock-rotate n (poly 0 0.5 0.3 n 
    {:stroke col :stroke-weight 2 :fill col-f} )))

(def final-pattern (grid-layout 4 (repeat (a-round 8))) )

The nature of "repeat"

The meaning of this code should be self-evident. But what about the call to repeat?

Layouts such as grid-layout take not just a single pattern to fill the grid, but a list of them. The algorithm starts by generating a sequence of positions to place each "tile" at. And then runs through the list of positions and the list of patterns and places one pattern at one position. The number of positions is finite, so in Clojure we can pass in an infinite, lazily evaluated list of patterns.

repeat is a function that takes a single item and returns an infinite list of them.

But we can also use cycle to turn finite vector of patterns into an infinite list :

(def final-pattern (grid-layout 4 (cycle [(a-round 4) (a-round 8)])) )

We also note here that the grid is filled column-wise from left to right.

Sequence tricks

Because the grid is filled from a sequence, we can use any functions that operate on the sequence abstraction to generate or process the patterns that feed it.

Here we generate our sequence by mapping a-round to a vector of integers.

(def final-pattern (grid-layout 4 (cycle (map a-round [3 4 5 6]))) )

We can generate the sequence using Clojure's iterate to constantly apply a transformation to an initial pattern. Such as shrinking :

(def final-pattern (grid-layout 4
                  (iterate (partial groups/scale 0.9) (a-round 9)) ))

We can even add random transformations, such as assigning each pattern an arbitrary colour.

; A random colour, and a function to produce a darker variation
(defn rand-col [] (p-color (rand-int 255) (rand-int 255) (rand-int 255) (rand-int 255)))
(defn darker-color [c] (apply p-color (map (partial * 0.7) c)))

; Apply random colour to a group
(defn random-color [p] (let [c (rand-col)] (groups/over-style {:fill c :stroke (darker-color c)} p ) ))

; and slot this in between generating the sequence and the layout
(def final-pattern (grid-layout 4
                                (map random-color (map a-round (cycle [3 4 5 6 7])))))

Maps with multiple arguments

Clojure's map function can actually map a function across multiple lists. (map f xs ys) will call (f x y) for each corresponding element of xs and ys.

We can use this to apply evolving transforms to a stream of evolving patterns. For example, this rotating / shrinking trianlgle.

(def t (stack
        (poly 0 0 0.6 3 {:stroke col :fill col-f :stroke-weight 2})
        (horizontal-line 0 {:stroke col :stroke-weight 2})
        )
  )

(def final-pattern (grid-layout 6 (map (fn [a p] (groups/rotate a p))   ;; the function to be mapped across 
                                       (iterate (partial + 0.15) 0)     ;; a stream of angles, and
                                       (iterate (partial groups/scale 0.97) t)))) ;; a stream of shrinking triangles