I just read a fantastic and interesting book about how to create digital worlds based on mathematical principles, Nature of code. It inspired me to visualize a simple system with ClojureScript and Canvas.
Here is the result; click around to move the large circle.
The movements are based on a simple model where the small circles are attracted to the large circle, let's call it the attractor from now on.
There are several ways to program this behavior. A tool that fits perfectly for the job is linear algebra, which deals with geometric vectors and matrix calculations.
In short, a vector is an element that has both length and direction. A vector is usually drawn as an arrow.
How is it implemented?
First and foremost, each element we draw must have a position: an x and a y coordinate (we're staying in 2 dimensions, but the principles are the same for 3 dimensions).
One way to represent position is as a vector. This can be a bit confusing to think about since a position doesn't have a direction, but this will make it easier for us when calculating new positions later. In the code it looks like this:
;; x = 2 and y = 3
[2 3]
So this is a vector in a double sense; both a geometric vector and a Clojure vector data structure.
Both the small circles and the attractor have a position. Each of the small circles has a velocity in a given direction, represented as a vector. In addition, they have an acceleration towards the attractor. The acceleration is also a vector.
If we print out the state of the model at any point in the simulation, it looks like this:
@model
=>
{:attractor {:pos [225 225]},
:balls ({:pos [195.95010685999443 169.67465656341605],
:velocity [0.29351709042530344 1.3612519797676996],
:acceleration [0.009194083400046356 0.01776144223966601]}
{:pos [212.66042238266442 213.40288831234903],
:velocity [0.19838276960866433 1.9185125529771718],
:acceleration [0.013601877039455555 0.014662501185116123]}
;; Etc....
The simulation works as follows:
- Change the acceleration of each circle towards the attractor
- Change the speed of each circle based on the new acceleration
- Change the position of each circle based on the speed of the circle
In the source code it looks like this:
(defn update-model [model]
"Updates the model"
(-> model
attract-circles
accelerate-circles
move-circles))
The essence of this is the code for calculating the acceleration for each circle.
;; Constant force with which the attractor accelerates the circles towards itself
(def attractor-acceleration 0.02)
(defn calculate-attraction-force [ball attractor]
(let [force-direction (v/vsub (:pos attractor) (:pos ball)) ;; a)
normalized (v/vnormalize force-direction) ;; b)
with-strength (v/vmult normalized attractor-acceleration)] ;; c)
with-strength))
-
a) First we need to calculate the direction; we get this by subtracting the position of the attractor from the position of the circle (remember that we modeled position as a vector, which pays off now). We're left with a vector pointing from the circle to the attractor.
-
b) Then we normalize it, i.e., make the length of the vector equal to 1.
-
c) We do this so that we can multiply it by the attraction force of the attractor, which in this simulation is a constant. Alternatively, we could have made it relative to the distance, like how gravitational force works.
This is all well and good, but there's a problem. If you don't control the speed of each individual circle, it can grow very large. In the simulation, the speed is limited so that the circles can more quickly adjust their position when the attractor moves.
Finally, it's important to note that the vector mathematics is not optimized; it's just a naive implementation of elementary linear algebra. If you're looking to simulate a significantly larger number of entities, it's worth looking at better libraries.
The source code for the simulation is available on Github.