Iterated Function Systems
Jan 31, 2017 · 5 minute read clojureinteractiveAbove you can see a static image generated using this web page. Below you should see a picture of a tree that your browser just generated using the code snippet below it. You can increase the value for iterations
and the tree will repaint. Go ahead, give it a try.
(def tree {:colour-1 [23 2 10]
:colour-2 [58 173 75]
:iterations 40000
:transformations [[0.195 -0.488 0.344 0.443 0.4431 0.2453]
[0.462 0.414 -0.252 0.361 0.2511 0.5692]
[-0.058 -0.07 0.453 -0.111 0.5976 0.0969]
[-0.035 0.07 -0.469 -0.022 0.4884 0.5069]
[-0.637 0 0 0.501 0.8562 0.2513]]})
(draw-ifs tree "canvas-tree")
You can see a preview of the transformations on the left. Let’s try again, this time with a snowflake:
(def snowflake {:colour-1 [225 255 255]
:colour-2 [0 29 30]
:iterations 40000
:transformations [[0.75 0 0 0.75 0.125 0.125]
[0.5 0.5 -0.5 0.5 0 0.5]
[0.25 0 0 0.25 0 0.75]
[0.25 0 0 0.25 0.75 0.75]
[0.25 0 0 0.25 0 0]
[0.25 0 0 0.25 0.75 0]]})
(draw-ifs snowflake "canvas-snowflake")
Perhaps not the best choice of colours, but I’m sure you can fix that. They’re RGB. Here’s a couple more examples:
(def weed {:colour-1 [2 2 0]
:colour-2 [154 189 40]
:iterations 40000
:transformations [[0.5 0 0 0.75 0.2 0]
[0.25 0.1 -0.2 0.3 0.2 0.5]
[0.25 -0.1 0.2 0.3 0.5 0.4]
[0.2 0 0 0.3 0.4 0.55]]})
(draw-ifs weed "canvas-weed")
(def pine-tree {:colour-1 [3 2 10]
:colour-2 [58 173 75]
:iterations 40000
:transformations [[0.25 0 0 0.9 0.375 0]
[0.65 0 0 0.75 0.175 0.25]
[0 -0.5 0.25 0 0.5 0.2]
[0 0.5 -0.25 0 0.5 0.45]]})
(draw-ifs pine-tree "canvas-pine-tree")
If you play around a bit things can get weird:
(def weird {:colour-1 [215 122 126]
:colour-2 [0 2 8]
:iterations 40000
:transformations [[0.5 0.557 -0.357 0.1 0.0951 0.5893]
[0.1 0.157 -0.557 0.4 0.4413 0.5893]
[-0.2 0.257 -0.547 0.3 0.2313 0.5893]
[0.7 0.517 -0.547 0.4 0.0952 0.9893]]})
(draw-ifs weird "canvas-weird")
But how does it work? Well, it is using a technique for drawing fractals known as Iterated Functions Systems.
You can find all the code required for this page to work below. Let’s go through it step by step. First a helper function to perform a transformation on a point:
(defn transform [transformation point]
(let [[a b c d e f] transformation
[x y] point]
[(+ e
(+ (* a x)
(* b y)))
(+ f
(+ (* c x)
(* d y)))]))
and another helper function to calculate \$\log_y x\$ which will be used to determine the colour of a pixel:
(defn log [x y]
(/ (.log js/Math x)
(.log js/Math y)))
Next we have another function that is used to display a preview of the transformations on a canvas:
(defn draw-transformations [transformations canvas]
(let [canvas (js/document.getElementById canvas)
ctx (.getContext canvas "2d")
width (.-width canvas)
height (.-height canvas)
clear (.clearRect ctx 0 0 width height)
n (count transformations)]
(.rect ctx 0 0 width height)
(.stroke ctx)
(doseq [[[a b c d e f] colour]
(map vector transformations
(cycle (map #(+ 100 (int (/ 125 %))) (range n))))]
(.setTransform ctx a b c d (* e width) (* f height))
(set! (.-fillStyle ctx) (str "rgb(" colour "," colour "," colour ")"))
(.fillRect ctx 0 0 width height))
(.setTransform ctx 1 0 0 1 0 0)))
And finally the draw-ifs
function that renders the fractal using the parameters provided:
(defn draw-ifs [{:keys [iterations transformations colour-1 colour-2]}
canvas]
(draw-transformations transformations (str canvas "-transformations"))
(let [canvas (js/document.getElementById canvas)
ctx (.getContext canvas "2d")
width (.-width canvas)
height (.-height canvas)
clear (.clearRect ctx 0 0 width height)
image (.createImageData ctx width height)
points (drop 100
(persistent!
(reduce (fn [points i]
(conj! points
(transform (rand-nth transformations)
(nth points i))))
(transient [[1 1]])
(range iterations))))
max-x (apply max (map first points))
max-y (apply max (map second points))
mapped (frequencies
(map (fn [[x y]]
[(int (* width (/ x max-x)))
(int (* height (/ y max-y)))])
points))
max-v (apply max (map second mapped))
pixel-count (* width height)
pixel-data (persistent!
(reduce (fn [pixels [[x y] v]]
(let [r1 (log v max-v)
r2 (- 1 r1)]
(conj! pixels
(into [(* 4 (+ x (- pixel-count (* width y))))]
(map #(+ (* r1 %1) (* r2 %2)) colour-1 colour-2)))))
(transient [])
mapped))]
(doseq [[i r g b] pixel-data]
(aset image.data i r)
(aset image.data (+ i 1) g)
(aset image.data (+ i 2) b)
(aset image.data (+ i 3) 255))
(.putImageData ctx image 0 0)))
The interactive code snippets in this article are powered by KLIPSE.