Learning ClojureScript with P5.js
It’s December and you know what this means. T’is the season to learn new languages! Lately, I have been learning ClojureScript and have been having a blast creative coding with it. I think you should give it a try.
Who this tutorial is for
In order to follow this tutorial, you should be familiar with coding in P5 already. If you are, great! It will be a breeze to follow along. Let’s get started setting up.
Please note that I am not an expert in ClojureScript. Still, I’d like to share this with you.
Setup
I’ve created a P5-CLJS template.
Go ahead and clone the repo to your machine and follow the instructions on the
README.md
. Once the terminal shows Build completed
, you can open
localhost:3000
. You should see a red screen. I recommend using VSCode with the
plugin Calva. With this, we’re now ready to code.
First steps
Our sketch is located in the file src/sketch.cljs
. It looks like this:
(ns sketch
(:require [goog.object :as g]
[p5 :as p5]))
(defn setup[]
(js/createCanvas js/window.innerWidth js/window.innerHeight))
(defn draw[]
(js/background 255 0 0 ))
(doto js/window
(g/set "setup" setup)
(g/set "draw" draw))
It looks a lot like our familiar P5 sketch, with the setup and draw functions. Go ahead and try to change the background of the sketch! You can do so with this line:
(js/background 100 180 180)
Syntax
This line looks a lot different than Javascript. ClojureScript is a dialect of Lisp. You can compile it to Javascript so that you can run it in the browser.
Being a Lisp, everything is an expression and every expression is surrounded by parentheses.
(+ 420 69)
is a valid expression but + (420 69)
is not.
By now you would have noticed, or you may already even know, that the first item within the parentheses is a function. This syntax is called prefix notation. The operation or function which we want to use comes first in an expression. The syntax we are used to, like 1+1 is called infix notation.
JS Interop
You can call global Javascript functions from within Clojure by prefixing
functions with js
followed by a forward slash. For example: (js/console.log "Oi!")
calls console.log("Oi!")
. You’ll find that you don’t need the console
logs, because (println "Oi!")
does the same in ClojureScript.
Here’s an article about JS Interop to find out more about how to call and use JS from within ClojureScript. For us, this would be enough to continue coding.
Rotating Rectangle
I’ve changed my sketch to look like this:
(defn setup []
(js/createCanvas js/window.innerWidth js/window.innerHeight)
(js/rectMode js/CENTER))
(defn draw []
(js/background 0)
(let [x (/ js/width 2)
y (/ js/height 2)]
(js/translate x y)
(js/rotate (* js/frameCount 0.01))
(js/fill 250 0 0)
(js/rect 0 0 100 100)))
You should see a rotating square. Notice that whenever you change and save your
code, the sketch updates immediately! It’s not immediately apparent, but the
state of your program remains. For example, frameCount
continues
incrementing, even when you update your code. Go ahead and print it to the
console to see for yourself. This is a feature about ClojureScript I really
love.
However, if you make changes to the setup function, you’ll have to refresh the page manually. For code that executes only absolutely once (like creating the canvas), put this in setup. For code you’d like to execute once every time you change the sketch, just put it in global scope.
The let
block
let
is a special form. It’s used when you want to have local variables.
There are three parts to let
. First is the let keyword itself, followed by
bindings, and then the expressions you want to run.
(let [a 50
b 100]
(+ a b))
;;yields 150
The bindings are put in square brackets and are variable-value pairs.
Basically, in the example above, a
takes the value of 50 and b
100. Once
the bindings have been defined, you can use the variables in your expressions.
Here is the official documentation for ClojureScript’s
let.
Tubes
Let’s try this:
(defn draw []
(js/background 0)
(js/noStroke)
(doseq [i (range (/ js/width 4))]
(let [x (* i 4)
y ( js/height 2)
time (* js/frameCount 0.05)
theta (+ (* i 0.05) time)
r (* (+ (* (Math/sin theta) 0.5) 0.5) 255)]
(js/fill r)
(js/circle x y (+ r 50)))))
You should see a row of red circles growing towards the left. This is an example on how to use loops in ClojureScript
DoSeq
To do loops, we can use the doseq
special block. Like let, it has three
parts. It starts with the doseq
keyword, followed by bindings in square
brackets and then by the expressions to be evaluated/executed. However, the
bindings here are a bit different. The value of the variable-value pairs needs
to be a sequence or a collection. This simply means an array or a list of
items. In the example above, our collection is (range (/ js/width 4))
, which
basically is an array with numbers from 0 until 1/4th of our screen’s width. Go
ahead and print it out in the console to see what it looks like!
Check out the official docs about DoSeq for more info.
Too many parentheses!
There was a really gnarly line in our last example, when we were calculating the radii of the circles. Three nested parentheses! Luckily, there’s a feature in ClojureScript called threading (not to be confused with concurrency). If you’re familiar with the command line, it’s similar to piping! Let’s take a look at what that looks like.
(-> theta
(Math/sin)
(* 0.5)
(+ 0.5)
(* 255))
The expression starts with ->
, followed by our initial value theta
. This
gets plugged into the next expression (Math/sin)
. The output of this gets
plugged into (* 0.5)
and so on. This, to me, is really beautiful, because you
can easily shuffle, insert and append operations if you need to without
worrying about the parentheses or operation order.
Here’s the official documentation to ClojureScript’s ->.
State
At this point, we’ve been doing everything statelessly. We don’t have any global variable which stores some data that we update. This is because ClojureScript is by default immutable. Every expression returns a new value and never modifies the values or variables you plug into it. This makes it really easy to reason about the logic of our programs.
However, this doesn’t mean that ClojureScript cannot handle state. It does so with something called atoms. Here’s a good article about using state and atoms in ClojureScript.
About our template
We’ve gone this far without really talking about the template that we are using. The build tool that we are using is ShadowCLJS. It basically acts like a code hot reloader. In my opinion, it’s the best build tool to use if you use a lot of NPM packages.
In your sketch, you’ll also see that it starts out with ns sketch
, followed
by a couple of requires. This declares the namespace of sketch for the entire
file and pulls in the dependencies that we need: p5 and goog.object.
Goog.object is a module from Google Closure’s library for working with JS objects. Here’s the API for it. Google Closure library is incidentally a great library with many other modules for the DOM, maths and more.
At the bottom of our sketch file, you’ll see this code block:
(doto js/window
(g/set "setup" setup)
(g/set "draw" draw))
This just sets the global variables of “setup” and “draw” to our setup and draw
functions, which is how P5 works. The syntax may look a bit strange, since it
is using the doto
special form.
(g/set js/window "setup" setup)
(g/set js/window "draw" draw)
This is exactly the same as above. Since js/window
gets called twice, we use
the doto
special form to reduce this. You may later want to add your
keyPressed
, mouseClicked
, windowResized
functions. Using the doto
special form means you don’t have to type js/window
each time.
What next?
This is it for this tutorial! It may seem like we covered only a little, but actually, it’s quite a lot. ClojureScript, being a Lisp, has very simple syntax. This tutorial covers everything you might need to use for basic sketches.
If you enjoyed this tutorial, I recommend going through a more thorough document about the ins and outs of ClojureScript. This online ebook is a great resource.
It’s also worth trying out different tutorials using different build tools. For me though, this can be rather confusing. I stuck to learning about shadow-cljs instead, which has been sufficient for my needs so far.
With #Genuary2023 just around the corner, I challenge you to follow along in ClojureScript!