Skip to content

Instantly share code, notes, and snippets.

@postspectacular
Last active November 29, 2019 15:58
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save postspectacular/5257574 to your computer and use it in GitHub Desktop.
Save postspectacular/5257574 to your computer and use it in GitHub Desktop.
Quick clojure/quil demo to show usage of atoms to manage mutable state, AFAIK quil's architecture doesn't really allow for purely functional approach... even so, with some care one can seriously limit the number points where that mutable state is actually used (here in only 2 places) and the rest remains purely functional! The second version bel…
(ns resonate-2013.agents
(:use quil.core))
(def num-agents 50)
(def num-targets 10)
;;; atom to hold global app state
(def app-state (atom {}))
;;; cheapskate vector ops
(defn mag2
"2d vector magnitude"
[[x y]] (Math/sqrt (+ (* x x) (* y y))))
(defn normalize2
"2d vector normalize"
[v]
(let [m (/ 1.0 (+ (mag2 v) 1e-6))]
(map * v [m m])))
(defn random-point-in-rect
[w h]
[(random w) (random h)])
;;; factory functions ("constructors")
(defn make-target
"Returns a map specifying a random target pos & radius."
[w h]
{:pos (random-point-in-rect w h)
:radius (random 10 20)})
(defn make-agent
"Returns a map specifying a randomly configured agent and
picks a random target from the given list of targets."
[w h targets]
(let [pos (random-point-in-rect w h)]
{:pos pos
:prev pos
:dir [(random -1 1) (random -1 1)]
:speed (random 1 5)
:steer (random 0.025 0.1)
:target (-> targets count random int targets)
:col (repeatedly 3 #(random 255))}))
;;; helpers for updating agents
(defn wrap-coord [x x1 x2 r]
"Wraps `x` on both ends `x1`/`x2` with radius `r`."
(if (< x (- x1 r))
(+ x2 r)
(if (> x (+ x2 r))
(- x1 r)
x)))
(defn steer-towards
"Steers `dir` towards `target` with `steer` strength.
Normalizes both input and result vector(s)."
[target dir steer]
(->> target
(normalize2)
(map #(lerp % %2 steer) dir)
(normalize2)))
(defn update-agent
"If not yet reached target, updates an agent's direction & position.
Returns updated agent state map."
[{:keys [pos dir speed steer] {tpos :pos tradius :radius} :target :as agent}]
(let [delta (map - tpos pos)]
(if (> (mag2 delta) tradius)
(let [dir (steer-towards delta dir steer)
scaled-dir (map * dir [speed speed])
new-pos (map + pos scaled-dir)]
(assoc agent :pos new-pos :prev pos :dir dir))
agent)))
(defn random-targets
"Returns a vector of `n` random targets."
[n w h]
(vec (repeatedly n #(make-target w h))))
(defn random-agents
"Returns a lazyseq of `na` random agents, each with a random
target picked from targets."
[na w h targets]
(repeatedly na #(make-agent w h targets)))
(defn restart-sim
"Clears canvas, replaces the current set of agents & targets with
new random instances."
[]
(background 255)
(let [targets (random-targets num-targets (width) (height))
agents (random-agents num-agents (width) (height) targets)]
(swap! app-state assoc
:targets targets
:agents agents)))
(defn setup
"Initializes viewport and app state map (agents & targets)."
[]
(ellipse-mode :radius)
(no-fill)
(restart-sim))
(defn draw
"Updates all agents, then draws them (and targets too)."
[]
(let [{:keys [agents targets]} @app-state
agents (map update-agent agents)]
(swap! app-state assoc :agents agents)
(doseq [{[x y] :pos [px py] :prev [r g b] :col} agents]
(stroke r g b)
(line px py x y))
(doseq [{[x y] :pos r :radius} targets]
(stroke 0)
(ellipse x y r r))))
(defsketch Agents
:size [640 480]
:title "Quil agents"
:setup setup
:draw draw
:mouse-pressed restart-sim)
;; Faster version with type hints and replacement of
;; map fn calls forvector math with explicit 2d operations
;; More info about type hints:
;; http://dev.clojure.org/display/doc/Documentation+for+1.3+Numerics
(ns resonate-2013.agents
(:use quil.core))
(set! *warn-on-reflection* true)
(def num-agents 5000)
(def num-targets 50)
(def app-state (atom {}))
(defn mag2
"2d vector magnitude"
[[^double x ^double y]] (Math/sqrt (+ (* x x) (* y y))))
(defn normalize2
"2d vector normalize"
[v]
(let [m (/ 1.0 (+ (mag2 v) 1e-6))]
[(* (v 0) m) (* (v 1) m)]))
(defn scale2-n
"Uniform 2d vector scale"
[v ^double s]
[(* s (v 0)) (* s (v 1))])
(defn vop2
"Generic 2d vector operation. Applies f to a b componentwise."
[f a b]
[(f (a 0) (b 0)) (f (a 1) (b 1))])
(defn random-point-in-rect
[w h]
[(random w) (random h)])
(defn make-target
"Returns a map specifying a random target pos & radius."
[w h]
{:pos (random-point-in-rect w h)
:radius (random 10.0 20.0)})
(defn make-agent
"Returns a map specifying a randomly configured agent and
picks a random target from the given list of targets."
[w h targets]
(let [pos (random-point-in-rect w h)]
{:pos pos
:prev pos
:dir [(random -1 1) (random -1 1)]
:speed (random 1 5)
:steer (random 0.025 0.1)
:target (-> targets count random int targets)
:col (repeatedly 3 #(random 255))}))
(defn wrap-coord ^double [^double x ^double x1 ^double x2 ^double r]
"Wraps `x` on both ends `x1`/`x2` with radius `r`."
(if (< x (- x1 r))
(+ x2 r)
(if (> x (+ x2 r))
(- x1 r)
x)))
(defn steer-towards
"Steers `dir` towards `target` with `steer` strength.
Normalizes both input and result vector(s)."
[target dir ^double steer]
(->> target
(normalize2)
(vop2 #(+ % (* (- %2 %) steer)) dir)
(normalize2)))
(defn update-agent
"If not yet reached target, updates an agent's direction & position.
Returns updated agent state map."
[{:keys [pos dir speed steer] {tpos :pos tradius :radius} :target :as agent}]
(let [delta (vop2 - tpos pos)]
(if (> (mag2 delta) tradius)
(let [dir (steer-towards delta dir steer)
scaled-dir (scale2-n dir speed)
new-pos (vop2 + pos scaled-dir)]
(assoc agent :pos new-pos :prev pos :dir dir))
agent)))
(defn random-targets
"Returns a vector of `n` random targets."
[n w h]
(vec (repeatedly n #(make-target w h))))
(defn random-agents
"Returns a lazyseq of `na` random agents, each with a random
target ID between (0 .. `nt`)."
[na w h targets]
(repeatedly na #(make-agent w h targets)))
(defn restart-sim
"Clears canvas, replaces the current set of agents & targets with
new random instances."
[]
(background 255)
(let [targets (random-targets num-targets (width) (height))
agents (random-agents num-agents (width) (height) targets)]
(swap! app-state assoc
:targets targets
:agents agents)))
(defn setup
"Initializes viewport and app state map (agents & targets)."
[]
(ellipse-mode :radius)
(no-fill)
(restart-sim))
(defn draw
"Updates all agents, then draws them (and targets too)."
[]
(let [{:keys [agents targets]} @app-state
;; if num-agents is very large or `update-agent` becomes
;; more complex, it might make sense to switch to from
;; `map` to `pmap` for parallel execution
;; also see: http://clojuredocs.org/clojure_core/clojure.core/pmap
agents (map update-agent agents)]
(swap! app-state assoc :agents agents)
(doseq [{[x y] :pos [px py] :prev [r g b] :col} agents]
(stroke r g b)
(line px py x y))
(stroke 0)
(no-fill)
(doseq [{[x y] :pos r :radius} targets]
(ellipse x y r r))
(fill 255)
(rect 0 0 100 20)
(fill 0)
(text (str (current-frame-rate)) 10 16)))
(defsketch Agents
:size [640 480]
; :renderer :opengl
:title "Quil agents"
:setup setup
:draw draw
:mouse-pressed restart-sim)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment