Use vectors in your generative art

May 2021

Quil is a great library to make generative art with. As might be expected for a library that allows for points to be graphed, many functions expect one or more points to be passed to it.

For example, point, vertex, and triangle all take these x and y values. Quad even takes a whopping eight values, representing four points! Working this way, it's easy to do simple things.

(q/point 0 100)

In fact, most example code I've seen works this way. However, it becomes harder to do more complicated things.

(let [original-x 0
      original-y 100
      change-in-x 50
      change-in-y 25]
  (q/line original-x
          original-y
          (+ original-x change-in-x)
          (+ original-y change-in-y)))

It's easy to lose track of which argument is the x value, and which the y. What if instead, we work with vectors?

(defn vector+
  "Add together any nonzero number of vectors."
  [& vectors]
  (reduce #(mapv +
                 %1
                 %2)
          vectors))

(let [original-point [0 100]
      change [50 25]]
  (q/line original-point
          (vector+ original-point change)))

This is easier to understand. There is some complexity in vector+, but this complexity is hidden from the user of the function. The version that requires points as separate x and y values pushes all the complexity to the top level.

Note that this relies on the line function accepting arguments as points. Line is a rare Quil function – it not only accepts its arguments as separate x, y arguments, but also accepts the points as vectors.

If we want to use Quil functions that don't already take arguments this way, we can wrap them. For example, vertex can be wrapped as follows:

(defn vertex
  "A version of quil's vertex that takes a point
   as a single argument."
  [point]
  (apply q/vertex point))

So far we've only discussed simple uses – ones that are easy enough to work with even without using points. Let's look at a more complex algorithm that takes advantage of passing around points: Chaikin curves. This algorithm is a method of taking a series of points and turning it into a smoother curve. You can see the effect below, starting with the algorithm not applied at all on the left shape, one more time on each shape to the right than the previous shape.

Four curves, becoming smoother each time the Chaikin algorithm is applied.

The code to generate this picture is below. The important part is in the function chaikin, which takes a seq of points.

(defn chaikin
  "Generate the ORDER-th level Chaikin curve for the control polygon
  formed by POINTS.

  Note that POINTS is treated as an open polygon; we do not wrap
  around from the front to back.

  Also note that this function is NOT LAZY. Only pass to it points
  you will actually graph.

  More information is here:
  http://graphics.cs.ucdavis.edu/education/CAGDNotes/Chaikins-Algorithm/Chaikins-Algorithm.html"
  [points order]
  (loop [points points
         order order]
    (if (< order 1)
      points
      (recur
        (mapcat (fn [p1 p2]
                  (list (vector-scale (vector+ (vector-scale p1 3)
                                               p2)
                                      0.25)
                        (vector-scale (vector+ p1
                                               (vector-scale p2 3))
                                      0.25)))
                (butlast points)
                (rest points))
        (dec order)))))

(defn chaikin-blogpost-draw []
  (let [control-points [[50 100]
                        [100 400]
                        [200 350]
                        [200 50]
                        [100 200]]
        number-of-offsets 4
        offset-vector [250 0]]
    (q/fill nil)
    (dotimes [offset-number number-of-offsets]
      (q/begin-shape)
      (let [offset (v/scale offset-vector offset-number)]
        (doseq [point (v/chaikin (map (partial v/v+ offset)
                                      control-points)
                                 offset-number)]
          (h/vertex point)))
      (q/end-shape))))

(q/sketch
 :size [1000 500]
 :draw chaikin-blogpost-draw)

Imagine what this code would have to look like if we dealt with x and y coordinates separately! Operating on points lets us do operations that would be quite painful to do if we passed coordinates around separately. So use vectors.

Postscript

It helps that Clojure works very nicely with vectors – we don't have to make a separate class to handle points. We can use a vector, and it's easy to create, introspect, and pass around. Thanks, Clojure!