Skip to content

Dynamic Workflow (for REPL)

Greg Goltsov edited this page Sep 24, 2021 · 12 revisions

So, you've Installed Quil, played with some of the examples and want to start making your own art. Problem is, you don't just want to make static things, you want to be able to change stuff on the fly. That's one of the reasons you chose to hack with Quil over the standard Processing interface anyway, right. That, and your appreciation of the parens of course.

My default suggestion for the dynamic modification of Quil illustrations is to use an Editor like Emacs. Perhaps even taking advantage of Overtone's excellent Live Programming Emacs Config. But I understand, Emacs isn't everyone's cup of tea. Too many crazy key combinations and all that jazz. Speaking of jazz, I bet some of the greats would have made for awesome Emacs hackers, but I digress...

Let's assume you're hacking with Notepad. I know, I know, that's far too simplistic for your programming needs. But if I can show you how to do it with Notepad, you can do it with whatever swanky tool you happen to be rocking today (and tomorrow).

So, let's get started. First up create a new project...

lein new quil-workflow

Open notepad and edit project.clj. It should look as follows:

(defproject quil-workflow "1.0.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [quil "2.2.1"]])

Now pull in all your deps with lein deps. Great, we're now ready to rock. Open up quil_workflow/core.clj and make it look as follows:

(ns quil-workflow.core
  (:require [quil.core :as q])
  (:require [quil-workflow.dynamic :as dynamic]))
    
(q/defsketch example                
  :title "Oh so many grey circles"
  :setup dynamic/setup           
  :draw dynamic/draw              
  :size [323 200])                

Now, create a new file quil_workflow/dynamic.clj and make it look as follows:

(ns quil-workflow.dynamic
  (:require [quil.core :as q])) 

(defn setup []
  (q/smooth)
  (q/frame-rate 1)
  (q/background 200))

(defn draw []
  (q/stroke (q/random 255))
  (q/stroke-weight (q/random 10))
  (q/fill (q/random 255))

  (let [diam (q/random 100)
        x    (q/random (q/width))
        y    (q/random (q/height))]
    (q/ellipse x y diam diam)))

Notice how we took the example from the README and split it over two files core.clj and dynamic.clj.

Now, in the project root dir fire up a REPL with lein repl and use the core ns:

(use 'quil-workflow.core)

Now you should see pretty grey circles appearing slowly on the screen. Now, let's remove our monochrome glasses and add some brilliant COLOUR! Modify the draw fn in dynamic.clj so that the call to stroke looks as follows:

(q/stroke (q/random 255) (q/random 255) (q/random 255))

Now, on the REPL, reload the dynamic ns:

(use :reload 'quil-workflow.dynamic)

Hey Presto! You should now be seeing coloured circles appear in your Quil illustration. Weep with joy! Now, just start hacking about in dynamic.clj and reloading the ns for an infinite stream of immediate joys.
For more exciting ideas have a look at Functional Mode.

Calling API functions in REPL

The example above used the quil function random to get random coordinates and colors for the circles. Most API functions only work inside sketch functions, i.e. setup and draw. When called in the REPL, you'll get an error like this:

(quil.core/random 10)
NullPointerException   quil.core/random (core.cljc:3027)

In order to call the functions directly in the REPL, they need to be wrapped with the current sketch:

(quil.applet/with-applet quil-workflow.core/example 
  (quil.core/random 10)) 
Pause/Unpause draw loop

The draw function is repeatedly called and redraws the sketch. To pause this loop from within the REPL call:

(quil.applet/with-applet quil-workflow.core/example
  (quil.core/no-loop)

and start-loop

(quil.applet/with-applet quil-workflow.core/example
  (quil.core/start-loop)

to unpause.

Dealing with errors

Because of the repeated calling of draw, any programming mistake leading to error is also repeatedly evaluated. For example changing the draw fn in dynamic.clj so that the call to stroke looks as follows

(q/stroke (q/random 255) (q/random 255) (q/random 255) "oops")

and, on the REPL, reload the ns:

(use :reload 'quil-workflow.dynamic)

produces many java.lang.ClassCastException, because string "oops" cannot be casted to float. To gracefully handle such mistakes, the pause-on-error middleware comes in handy.

An Alternative REPL Workflow

A slightly different approach if you want to heavily do REPL based development, is to use the code below.

The advantage of this is the use of #' or var. It's use ensures that reevaluating the entire file in your editor means that the function definitions get changed directly. So for example if you change the definition of update-state or draw-state, re-evaluating the file means that instead of spawning a new quil sketch window, your existing window changes seamlessly.

Note this isn't perfect, for example, changes to setup or if an exception causes m/pause-on-error to trigger, will often require re-creating the window, which can easily be done by temporarily changing the defonce sketch to def sketch, reevaluating it and then changing it back.

However this provides a pleasant environment for rapid prototyping.

(ns quil-example.core
  (:require [quil.core :as q]
            [quil.middleware :as m]))

(defn setup []
  (q/frame-rate 30)
  (q/color-mode :hsb)
  {:colour 140})

(defn update-state [state]
  {:colour (mod (+ (:colour state) 0.7) 255)})

(defn draw-state [state]
  (q/background (:colour state)))

(defn create-sketch []
  (q/sketch
    :title "Quil Example"
    :size [500 500]
    :setup #'setup
    :update #'update-state
    :draw #'draw-state
    :renderer :p3d
    :features [:keep-on-top]
    :middleware [m/fun-mode m/pause-on-error]))

(defonce sketch (create-sketch))