Skip to content

How to embed Quil in another program

Nikita Beloglazov edited this page Jul 7, 2018 · 1 revision

Quil is a library for creating interactive drawing and animations. To create minimal sketch you need to define draw function and pass it to defsketch to start the sketch. If you want to update state you can do it either inside draw function or even better use functional mode. Both these approaches assume that all you code is written as part of Quil sketch lifecycle. But often this is not the case. Sometimes you need to add animation to an existing program and don't want to change your program to fit Quil's conventions. Examples:

  • Visualization for an algorithm or state machine.
  • Visualization of data coming from external sources, for example from internet.
  • UI of a multiplayer game where game server is a separate machine and Quil is used as game client.

This can be achieved with existing Quil functionality. To do that we are going to introduce a mutable state. The main part of the program is going to modify the state while sketch is going to draw it. The state will be passed to the sketch initialization.

As an example let\s implement a progress bar. State of the progress bar is a number between 0 and 100 inclusive. It is passed as atom. Sketch's responsibility is to constantly (re)draw the progress bar. Here is how sketch code looks (very high level, full implementation is at the end of this article):

(defn draw [progress]
  ; draw progress bar where 'progress' is number in [0, 100]
)

(defn start-sketch [state]
  (q/sketch
    :host "host"
    :size [500 500]
    :draw #(draw @state)))

Note that start-sketch function takes an atom and creates a new sketch. We pass anonymous function as :draw which dereferences state on each redrawing and calls draw function with the current state value.

In this simple example state contains a number. But generally the state can contain a map or some complex structure representing whatever needs to be drawn. Note that the sketch code is separated from the main part of the program where state is modified. It makes it easier to reason about: "business" logic can be developed and tested separately from "UI". For example when working on a sketch you might start by providing static atom:

(start-sketch (atom 42))

And hook up the main part of the program once you satisfied with UI.

Adding user input

Until now we discussed only the case where sketch needs to visualize a state of the main program. But Quil also allows user to interact with the sketch: sketch can react to user keyboard clicks, mouse movements and similar. It's quite easy to extend our approach to allow the main program to get user feedback from the sketch: the main program should pass listeners to be used by sketch.

Let's assume that for some odd reason we want to reset progress bar to 0 when user clicks on it. For that the main is going to pass a function that resets the progress bar and start-sketch is going to use it as a mouse click listener:

(defn draw [progress]
  ; draw progress bar where 'progress' is number in [0, 100]
)

(defn start-sketch [state reset-progress-bar]
  (q/sketch
    :host "host"
    :size [500 500]
    :draw #(draw @state)
    :mouse-clicked reset-progress-bar))

Sketch can do more than just using passed function as listener. It can process user input to all provided function with some arguments. For example your main program wants to show user a set of 4 images and ask to choose one by typing number 1, 2, 3 or 4 on keybard. In that case start-sketch might look like the following:

(defn start-sketch [state image-selected-fn]
  (q/sketch
    :host "host"
    :size [500 500]
    :draw #(draw-images @state)
    :key-pressed #(condp (q/key-as-keywoard)
                    :1 (image-selected-fn 1)
                    :2 (image-selected-fn 2)
                    :3 (image-selected-fn 3)
                    :4 (image-selected-fn 4)
                    (show-error-to-user))))

Full code

Here is the full example of the progress bar. Cljs is used so that you can play with it online: link.

(ns progress-bar
  (:require [quil.core :as q :include-macros true]))

(def bar-height 50)
(def bar-width 400)

(defn draw [state]
  ; Clear the sketch by filling it with white color.
  (q/background 255)
  (q/no-stroke)
  (q/with-translation [50 125]
    ; Set color to light-gray and draw outline.
    (q/fill 230)
    (q/rect 0 0 bar-width bar-height)
    ; Set color to light-blue and draw bar.
    (q/fill 81 172 229)
    (q/rect 0 0 (* bar-width state 0.01) bar-height)
    ; Set color to white and draw text.
    (q/fill 255)
    (q/text-align :center :center)
    (q/text-size 20)
    (q/text (str state "%")
            (* bar-width state 0.01 0.5)
            (/ bar-height 2))))

(defn start-sketch [state reset-progress-bar]
  (q/sketch
    :host "host"
    :size [500 300]
    :draw #(draw @state)
    :mouse-clicked reset-progress-bar))

; Returns random nubmer representing amount of time in milliseconds
; to wait between updating progress bar.
(defn rand-time-to-wait []
  (rand-nth [30 50 200]))

(defn progress-bar-iteration [state]
  ; Reset state to 0 once it reaches 100. Otherwise just increment it.
  (swap! state #(if (= % 100) 0 (inc %)))
  ; Schedule next iteration using random delay.
  (js/setTimeout (partial progress-bar-iteration state)
                 (rand-time-to-wait)))

(defn start-progress-bar []
  (let [state (atom 0)
        reset-progress-bar #(reset! state 0 )]
    (progress-bar-iteration state)
    (start-sketch state reset-progress-bar)))

(start-progress-bar)