Clojure2D P(art)icle System

πŸ‘‹πŸ» This is an example of using Clojure2d and a small particle system to make an abstract generative drawing. There are some special considerations for doing this sort of thing in Nextjournal, which we'll cover as we go.

❗️First off, this library normally displays graphics interactively, so we'll need to set the headless property to use it in a notebook context. If we don't do this, the whole thing will fail!

(System/setProperty "java.awt.headless" "true")
0.1s
Clojure
clojure2d

πŸ“š To produce this kind of drawing, we'll need both primitives supporting both drawing and maths. Fortunately, the library we've chosen includes both.

(require '[clojure2d.core :as c]
         '[fastmath.core :as m]
         '[fastmath.random :as r]
         '[fastmath.vector :as v])
14.2s
Clojure
clojure2d

πŸ›  Here we set up a few parameters to control how the output looks. The w and h vars are the width and height of the final drawing in pixels, num-particles is β€” unsurprisingly β€” the number of particles, noise-scale controls the overall vibe of the shapes, while num-steps sets the number of time steps the system will run before saving an image file.

(def w 1000)
(def h 1000)
(def num-particles 50)
(def noise-scale 500)
(def num-steps 800)
0.1s
Clojure
clojure2d

πŸͺ¨ This function returns a new system of particles , which we'll use to figure out where to draw pixels on the screen. It randomly arranges them around the canvas, giving each one a color picked from one of Clojure2D's built-in palettes, and sets up their physical system with initial values.

(defn make-particles [how-many]
  (repeatedly how-many
          #(hash-map
             :color (clojure2d.color/set-alpha (rand-nth (clojure2d.color/palette 1)) 128)
             :angle (r/frand 0 m/PI)
             :speed 0.6
             :direction (v/vec2 0 0)
             :velocity (v/vec2 0 0)
             :position (v/vec2 (r/irand w) (r/irand h)))))
0.1s
Clojure
clojure2d

β˜„οΈ This function evolves the entire particle system one time step, taking the old set of particles and returning a new one. We choose a new angle for each particle using a noise function parameterized on that particle's x and y position, then apply some physics to nudge it in that direction. If the new position is outside of the canvas, we choose a new starting position within bounds.

(defn evolve [particles]
  (mapv (fn [p]
          (let [angle (+ (:angle p)
                         (* (- (r/simplex (+ (/ (.x (:position p)) noise-scale)
                                             (/ (.y (:position p)) noise-scale)))
                                         0.5)
                            0.12))
                direction (v/vec2 (m/cos angle) (m/sin angle))
                velocity (v/mult direction (:speed p))
                position (v/add (:position p) velocity)]
            (assoc p
              :angle angle
              :direction direction
              :velocity velocity
              :position (if (or (> (.x position) w) ;; bounds check!
                                (< (.x position) 0)
                                (> (.y position) h)
                                (< (.y position) 0))
                          (v/vec2 (r/irand w) (r/irand h))
                          position))))
    particles))
0.1s
Clojure
clojure2d

πŸ–Ό Finally, we can draw the output to a canvas and save it. The trick here is to keep drawing the particles' new positions on the same canvas, which gives us curving lines instead of points. It's also worth noting that because we save the output in a special directory called results, Nextjournal automatically detects it and presents it to us inline. (Note that this takes several seconds to compute using the default parameters.)

(c/with-canvas [canvas (c/canvas w h)]
  (c/set-background canvas 18 18 22)
  (doseq [particles (take num-steps (iterate evolve (make-particles num-particles)))]
    (doseq [{:keys [color position]} particles]
      (c/set-color canvas color)
      (c/ellipse canvas (.x position) (.y position) 2.5 2.5)))
  (c/save canvas "/results/headless.jpg"))
6.8s
Clojure
clojure2d

Appendix

Saving the environment for faster boot times.


{:deps {org.clojure/clojure {:mvn/version "1.10.1"}
        clojure2d {:mvn/version "1.4.3"}}}
deps.edn
Extensible Data Notation
clj -Stree
82.4s
clojure2d (Bash)

This sets up the reusable environment.

Runtimes (2)