This is a story of Clara who attended a ClojureBridge workshop recently.
At the workshop, she learned what Clojure is and how to write Clojure code.
That really impressed her, "How functional!"
Also, Clara met an interesting drawing tool, Quil, which is written in Clojure.
When Clara learned how to use Quil, she thought, "I want to create something cool with Quil!"
Here's how Clara developed her own Quil application.
Clara considered where to start. Then, a small light turned on in her mind, "a white snowflake on a blue background looks nice." What she wanted to know was how to make a background blue and put a snowflake on it. Clara already learned how to find the way. It was:
- Go to the API documentation website
- Google it
So, Clara went to the Quil API web site, http://quil.info/api, and found the Loading and Displaying section. Then, she found the background-image function.
Then, she googled and found a StackOverflow question, Load/display image in clojure with quil, which looked like what she needed.
Those gave her enough information to accomplish step 1.
At the ClojureBridge workshop, Clara went though the Quil app
tutorial,
Making Your First Program with Quil,
so she already had the drawing
Clojure project. She decided to use
the same project for her own app.
Clara added a new file under src/drawing
directory with the name
practice.clj
. At this point, her directory structure looks like the
below:
| LICENSE
| README.md
| project.clj
| src
| | drawing
| | | core.clj
| | | lines.clj
| | | practice.clj
First, Clojure source code has a namespace declaration, so Clara
copy-pasted ns
from the top of her lines.clj
file. But, she
changed the name from drawing.lines
to drawing.practice
, because
her new file has the name practice
.
At this moment, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]))
Basic Quil code has setup
and draw
functions, along with a
defsketch
macro, which defines the app. Following these Quil rules,
Clara added those things into her practice.clj
.
Now, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]))
(defn setup [])
(defn draw [])
(q/defsketch practice
:title "Clara's Quil practice"
:size [1000 1000]
:setup setup
:draw draw
)
Looking at the Quil API and the StackOverflow question, Clara learned
that where to put her image files was important. She created a new
directory, images
, under the top drawing
directory, and she and
put two images there.
Now, her directory structure looks like the one below:
| LICENSE
| README.md
| project.clj
| src
| | drawing
| | | core.clj
| | | lines.clj
| | | practice.clj
| images
| | blue_background.png
| | white_flake.png
Since the images were ready, it was time to code using the Quil API.
Clara added a few lines of code to the setup
and draw
functions in
practice.clj
to load and draw two images. She was careful when
writing the image filenames because it should reflect the actual
directory structure.
At this moment, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]))
(def flake (ref nil)) ;; reference to snowflake image
(def background (ref nil)) ;; reference to blue background image
(defn setup []
;; loading two images
(dosync
(ref-set flake (q/load-image "images/white_flake.png"))
(ref-set background (q/load-image "images/blue_background.png"))))
(defn draw []
;; drawing blue background and a snowflake on it
(q/background-image @background)
(q/image @flake 400 10))
(q/defsketch practice
:title "Clara's Quil practice"
:size [1000 1000]
:setup setup
:draw draw)
When Clara ran this code, she saw this image:
Woohoo! She made it!
Clara was satisfied with the image of the white snowflake on the blue background. However, that was boring.
Next, she wanted to move the snowflake like it was falling down. This needed further Quil API study and googling.
Moving some pieces in the image is called "animation". She learned that Quil had two choices: a legacy, simple way and a new style using a framework.
Her choice was the new framework style, since the coding looked simple.
Clara read the document
Functional mode (fun mode)
and added two lines to her practice.clj
:
[quil.middleware :as m]
in thens
form:middleware [m/fun-mode]
in theq/defsketch
form
At this point, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(def flake (ref nil)) ;; reference to snowflake image
(def background (ref nil)) ;; reference to blue background image
(defn setup []
;; loading two images
(dosync
(ref-set flake (q/load-image "images/white_flake.png"))
(ref-set background (q/load-image "images/blue_background.png"))))
(defn draw []
;; drawing blue background and a snowflake on it
(q/background-image @background)
(q/image @flake 400 10))
(q/defsketch practice
:title "Clara's Quil practice"
:size [1000 1000]
:setup setup
:draw draw
:middleware [m/fun-mode])
"Well," Clara thought, "What does 'moving the snowflake like it was falling down' mean in terms of programming?"
To draw the snowflake, she used Quil's
image` function, described in
the API:
image.
The x and y parameters were 400 and 10 from the upper-left corner, which was the position she set to draw the snowflake. To make it fall down, the y parameter should be increased as time goes by.
In terms of programming, 'moving the snowflake like it was falling down' means:
- Set the initial state--for example,
(x, y) = (400, 10)
- Draw the background first, then the snowflake
- Update the state - increase the
y
parameter--for example,(x, y) = (400, 11)
- Draw the background again first, then the snowflake
- Repeat 2 and 3, increasing the
y
parameter.
In her application, "changing state" includes only the y
parameter. How could she increment the y
value by one?
Yes, Clojure has the inc
function. This is the function she used.
Here's what she did:
- Add a new function
update
which will increment they
parameter by one - Change the
draw
function to take the argumentstate
- Change
q/image
function's parameter to see thestate
- Add the
update
function in theq/defsketch
form
At this point, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(def flake (ref nil)) ;; reference to snowflake image
(def background (ref nil)) ;; reference to blue background image
(defn setup []
;; loading two images
(dosync
(ref-set flake (q/load-image "images/white_flake.png"))
(ref-set background (q/load-image "images/blue_background.png")))
(q/smooth)
(q/frame-rate 60)
10)
(defn update [state]
;; updating y parameter by one
(inc state))
(defn draw [state]
;; drawing blue background and a snowflake on it
(q/background-image @background)
(q/image @flake 400 state))
(q/defsketch practice
:title "Clara's Quil practice"
:size [1000 1000]
:setup setup
:update update
:draw draw
:middleware [m/fun-mode])
When Clara ran this code--hey! She saw that the snowflake was falling down.
Clara has a nice Quil app. But, once the snowflake went down beyond the bottom line, that was it. Only a blue background remained. So, she wanted it to repeat again and again.
In other words: if the snowflake reaches the bottom, it should come back to the top. Then, it should fall down again.
In terms of programming, what does that mean?
If the state
(y
parameter) is greater than the height of the
image, state
will go back to 0
. Otherwise, the state
will be
incremented by one.
OK, Clara learned if
, which is used for flow control, at the
ClojureBridge workshop:
Flow Control.
So, she used if
to make the snowflake go back to the top in the
update function.
At this point, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(def flake (ref nil)) ;; reference to snowflake image
(def background (ref nil)) ;; reference to blue background image
(defn setup []
;; loading two images
(dosync
(ref-set flake (q/load-image "images/white_flake.png"))
(ref-set background (q/load-image "images/blue_background.png")))
(q/smooth)
(q/frame-rate 60)
10)
(defn update [state]
(if (>= state (q/height)) ;; state is greater than or equal to image height?
0 ;; true - get it back to the 0 (top)
(inc state) ;; false - increment y parameter by one
))
(defn draw [state]
;; drawing blue background and a snowflake on it
(q/background-image @background)
(q/image @flake 400 state))
(q/defsketch practice
:title "Clara's Quil practice"
:size [1000 1000]
:setup setup
:update update
:draw draw
:middleware [m/fun-mode])
Clara saw the snowflake appear from the top after it went down below the bottom line.
Looking at the snowflake fall down many times is nice. But, Clara wanted to see more snowflakes falling down: one or more on the left half, as well as one or more on the right half.
Again, she needed to think in terms of programming.
This would be "draw mutiple images with the different x
parameters."
The easiest way to do that is to copy-paste (q/image @flake 400 state)
mutiple times with the different x
parameters. For example,
(q/image @flake 150 state)
(q/image @flake 400 state)
(q/image @flake 650 state)
But, for Clara, this did not look nice; she learned a lot about Clojure and wanted to use what she knew.
First, she thought about how to keep multiple x
parameters. She
remembered there was a vector
in the
Data Structures
section, which looked a good fit in this case.
Here's what she did to add more snowflakes:
- Add a vector which has multiple
x
parameters withdef
. - Draw snowflakes as many times as the number of
x
-params usingdoseq
.
- See, doseq for more info.
At this point, practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(def flake (ref nil)) ;; reference to snowflake image
(def background (ref nil)) ;; reference to blue background image
(def x-params [100 400 700]) ;; x parameters for three snowflakes
(defn setup []
;; loading two images
(dosync
(ref-set flake (q/load-image "images/white_flake.png"))
(ref-set background (q/load-image "images/blue_background.png")))
(q/smooth)
(q/frame-rate 60)
10)
(defn update [state]
(if (>= state (q/height)) ;; state is greater than or equal to image height?
0 ;; true - get it back to the 0 (top)
(inc state) ;; false - increment y parameter by one
))
(defn draw [state]
;; drawing blue background and mutiple snowflakes on it
(q/background-image @background)
(doseq [x x-params]
(q/image @flake x state)))
(q/defsketch practice
:title "Clara's Quil practice"
:size [1000 1000]
:setup setup
:update update
:draw draw
:middleware [m/fun-mode])
Yeah! Clara saw three snowflakes that kept falling down.
So far, so good.
But, Clara felt something was not quite right. All three snowflakes fell down simultaneously, which does not look very natural. So, she wanted to make them fall down at different rates.
Using programming terms, the problem here is that all three snowflakes
share the same y
parameter.
Adding multiple y
values would solve the problem--but how?
As she used vector
for the x parameters, the vector
is a good data
structure for y
parameters as well. But, not just the height; Clara
wanted to change the speed of each snowflake falling down.
So, she looked at the ClojureBridge curriculum page
More Data Structures
and found the Maps
section. Then, she changed the state
from a
single value to a vector of 3 maps.
[{:y 10 :speed 1} {:y 300 :speed 5} {:y 100 :speed 3}]
It was a nice data structure.
However, her update
function was not very simple anymore. She needed
to update all y
values in the three maps.
She had already learned how to get and update the values in the
map. There was an assoc
function, which would change the value in a
map, that appeared in
More Functions. (See
http://clojuredocs.org/clojure.core/assoc
for more info)
To iterate over all elements in a vector, Clojure has a few
functions. But, be careful! The update
function must return the
updated state.
So, she used for
to update the state like this:
(for [p state]
(if (>= (:y p) (q/height))
(assoc p :y 0)
(assoc p :y (+ (:y p) (:speed p))))
)
Another challenge was the draw
function.
Clara found a couple of ways to repeat something in Clojure. Among
them, she chose dotimes
and nth
to repeatedly draw images; the
nth
function is the one in
More Functions
In this case, she knew there were exactly 3 snowflakes, so she changed the code to draw 3 snowflakes as shown below:
(dotimes [n 3]
(q/image @flake (nth x-params n) (:y (nth state n))))
At this point, her entire practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(def flake (ref nil)) ;; reference to snowflake image
(def background (ref nil)) ;; reference to blue background image
(def x-params [100 400 700]) ;; x parameters for three snowflakes
(defn setup []
;; loading two images
(dosync
(ref-set flake (q/load-image "images/white_flake.png"))
(ref-set background (q/load-image "images/blue_background.png")))
(q/smooth)
(q/frame-rate 60)
[{:y 10 :speed 1} {:y 300 :speed 5} {:y 100 :speed 3}])
(defn update [state]
(for [p state]
(if (>= (:y p) (q/height)) ;; y is greater than or equal to image height?
(assoc p :y 0) ;; true - get it back to the 0 (top)
(assoc p :y (+ (:y p) (:speed p)))) ;; false - increment y parameter by one
))
(defn draw [state]
;; drawing blue background and mutiple snowflakes on it
(q/background-image @background)
(dotimes [n 3]
(q/image @flake (nth x-params n) (:y (nth state n)))))
(q/defsketch practice
:title "Clara's Quil practice"
:size [1000 1000]
:setup setup
:update update
:draw draw
:middleware [m/fun-mode])
When she ran the code above, three snowflakes kept falling down at different speeds. It looked more natural.
Clara looked at her practice.clj
, thinking her code got longer.
When she looked at her code from top to bottom again, she thought
"x-params
may be part of the state." So, she changed her state
to
have the x parameter also, like
[{:x 100 :y 10 :speed 1} {:x 400 :y 300 :speed 5} {:x 700 :y 100 :speed 3}]
.
She found that this new format was easy to maintain the state of each snowflake.
Now, Clara also needed to change the part that drew the snowflakes.
At first, she wrote it like this:
(dotimes [n 3]
(q/image @flake (:x (nth state n)) (:y (nth state n))))
But, the exact the same thing, (nth state n)
, appeared twice.
"Is there anything to avoid repetition?" she thought. She went to
Clojure Cheat Sheet to figure out the
dotimes
syntax. Then, she clicked on dotimes
in the "Macros,
Loops" section and saw the syntax described in
dotimes.
It says (dotimes bindings & body)
.
Clara remembered that she learned let
in
Flow Control,
which was "bindings". So, she rewrote it using let
like this:
(dotimes [n 3]
(let [snowflake (nth state n)]
(q/image @flake (:x snowflake) (:y snowflake))))
It looked nice and Clojure-ish.
At this moment, her entire practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(def flake (ref nil)) ;; reference to snowflake image
(def background (ref nil)) ;; reference to blue background image
(defn setup []
;; loading two images
(dosync
(ref-set flake (q/load-image "images/white_flake.png"))
(ref-set background (q/load-image "images/blue_background.png")))
(q/smooth)
(q/frame-rate 60)
[{:x 100 :y 10 :speed 1} {:x 400 :y 300 :speed 5} {:x 700 :y 100 :speed 3}])
(defn update [state]
(for [p state]
(if (>= (:y p) (q/height)) ;; y is greater than or equal to image height?
(assoc p :y 0) ;; true - get it back to the 0 (top)
(assoc p :y (+ (:y p) (:speed p)))) ;; false - add a value of speed
))
(defn draw [state]
;; drawing blue background and mutiple snowflakes on it
(q/background-image @background)
(dotimes [n 3]
(let [snowflake (nth state n)]
(q/image @flake (:x snowflake) (:y snowflake)))))
(q/defsketch practice
:title "Clara's Quil practice"
:size [1000 1000]
:setup setup
:update update
:draw draw
:middleware [m/fun-mode])
She saw the exact same result as the previous code, but her code looked nicer. This sort of work is often called "refactoring".
Clara was getting familiar with Clojure coding. Her Quil app was getting much better, as well!
However, looking at the snowflakes falling down, she thought she could swing them left and the right as they fall down. Right now, all of the snowflakes were falling straight down.
In programming terms, the x
parameter should both increase and
decrease when the value is updated. This means that the update
function should update the x
parameters as well as the y
parameters.
To write this feature, she changed the initial state to this:
[{:x 100 :swing 10 :y 10 :speed 8}
{:x 400 :swing 5 :y 300 :speed 11}
{:x 700 :swing 8 :y 100 :speed 9}]
The map got a new :swing
key, which holds a range of left and right
from a current position.
This means that the updated x
parameter will have a value between
the current x
parameter + swing
and the current x
parameter -
swing
. For example, the first snowflake's next x
parameter will be
between 100 + 10
and 100 - 10
.
To update the x
parameter by a random value between some range, she
needed to do something not just using the existing clojure functions.
She found rand-int
from Clojure Cheat Sheet, but it only
returned between 0
and a specified value.
Googling led her to this clojure code (from https://github.com/sjl/roul/blob/master/src/roul/random.clj ):
(defn rand-int
"Return a random int between start (inclusive) and end (exclusive).
start defaults to 0
"
([end] (clojure.core/rand-int end))
([start end] (+ start (clojure.core/rand-int (- end start)))))
This was the random generation function that she wanted. But, she wanted a bit more.
When the value goes to less than 0
, it should take the value of the
image width, so that the snowflake will appear from the
right. Likewise, when the value goes more than the image width, it
should have value 0
so that the snowflake will appear from the left.
She couldn't use if
anymore here, since if
takes only one
predicate (comparison). Instead of if
, she used cond
and wrote an
update-x
funcion.
(defn update-x
[x swing]
(let [start (- x swing)
end (+ x swing)
new-x (+ start (rand-int (- end start)))]
(cond
(> 0 new-x) (q/width)
(< (q/width) new-x) 0
:else new-x )))
This update-x
function hinted to her that she could refactor the
update function and write an update-y
function. The below is the
update-y
function.
(defn update-y
[y speed]
(if (>= y (q/height))
0
(+ y speed)))
Lastly, she rewrote the update
function.
She could still use assoc
, but it would be like this:
(assoc (assoc p :y (update-y (:y p) (:speed p))) :x (update-x (:x p) (:swing p)))
She remembered that there was another function for maps. It was
merge
, which was appeared in
More Functions.
Using merge
, her update
function turned into this:
(defn update [state]
(for [p state]
(merge p {:x (update-x (:x p) (:swing p)) :y (update-y (:y p) (:speed p))})))
At this point, her entire practice.clj
looks like this:
(ns drawing.practice
(:require [quil.core :as q]
[quil.middleware :as m]))
(def flake (ref nil)) ;; reference to snowflake image
(def background (ref nil)) ;; reference to blue background image
(defn setup []
;; loading two images
(dosync
(ref-set flake (q/load-image "images/white_flake.png"))
(ref-set background (q/load-image "images/blue_background.png")))
(q/smooth)
(q/frame-rate 30)
[{:x 100 :swing 10 :y 10 :speed 8}
{:x 400 :swing 5 :y 300 :speed 11}
{:x 700 :swing 8 :y 100 :speed 9}])
(defn update-x
[x swing]
(let [start (- x swing)
end (+ x swing)
new-x (+ start (rand-int (- end start)))]
(cond
(> 0 new-x) (q/width)
(< (q/width) new-x) 0
:else new-x )))
(defn update-y
[y speed]
(if (>= y (q/height)) ;; y is greater than or equal to image height?
0 ;; true - get it back to the 0 (top)
(+ y speed))) ;; false - add a value of speed
(defn update [state]
(for [p state]
(merge p {:x (update-x (:x p) (:swing p)) :y (update-y (:y p) (:speed p))})))
(defn draw [state]
;; drawing blue background and mutiple snowflakes on it
(q/background-image @background)
(dotimes [n 3]
(let [snowflake (nth state n)]
(q/image @flake (:x snowflake) (:y snowflake)))))
(q/defsketch practice
:title "Clara's Quil practice"
:size [1000 1000]
:setup setup
:update update
:draw draw
:middleware [m/fun-mode])
(frame-rate has been changed to 30)
When Clara ran this code, she saw snowflakes were falling down, swinging left and right randomly.
Even though there were a couple of problems as well as room for more refactoring, Clara was satified with her app. Moreover, she started thinking about her next, more advanced app in Clojure!
The End.