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")
π 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])
π 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)
πͺ¨ 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)))))
βοΈ 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))
πΌ 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"))
Appendix
Saving the environment for faster boot times.
{:deps {org.clojure/clojure {:mvn/version "1.10.1"}
clojure2d {:mvn/version "1.4.3"}}}
clj -Stree
This sets up the reusable environment.