A Billiards Simulator in Elm

The screen shot on the left is taken from a prototype of a billiard simulator written in Elm. (See GitHub for the source code.) Two balls, one red, one blue, are set in motion, with the display updating every 20 milliseconds. When a ball collides with a wall, it bounces back according to the rule Angle of Reflection equals Angle of Incidence — just like light rays. This is how ideal “point” billiards behave, but real billiards are more complicated because a ball which approaches a wall at other than 90 degrees will acquire spin. For now, our model ignores this pesky detail. It also ignores the fact that the two balls may collide, changing their trajectories. Planning ahead, however, we compute and display the distance between the two balls every 20 milliseconds. If we can detect collisions, we can act upon them. Also (but just for fun), we will make the screen flash yellow when the two balls are too close. Too close is defined as the distance between centers is less than or equal to two radii. Our first idea was to display a COLLISION message in this case. But since it was displayed for just 20 milliseconds, it was almost unnoticeable. The screen flash, which is “ON” for the same length of time, is, however, quite visible. Pop quiz: why is this?
We will describe the operation of the simulator and improvements to it in several parts. In Part I, which you are now reading, we give an overview of the code (see below). The improvements will be mostly in the nature of making the model more realistic, taking into account factors such as (i) friction, (ii) collisions, (iii) spin. Pop quiz: which factor is the easiest to implement?
Related posts: Simulating Brownian Motion in Elm
About developing with Elm
I am doing these little projects to better learn Elm — and to get in shape for the Elm Europe conference in Paris, June 8–9. I’m beginning to get a feel for this language, and have to say that the developer experience is Fantastic! In the Billiards project, I went through several refactorings, changing core data structures and moving code into new modules. No drama, no sweat! My work flow was to make the changes, work though the consequences on my own, then compile and fix the things I had forgotten to do, repeating this last step until there were no error messages. There was only one case in which the app did not behave as I expected after a successful build.
About the code
I ended up splitting the code into several modules for this project. The main code is in Billiards.elm
(233 lines of code in this file, 456 in the project). Billiards
calls on four other modules: Geometry
, Graph
, Physics
, and XColor
. A few notes …
Billiards
The model is defined by this data structure:
type alias Model =
{ simulatorState : SimulatorState
, count : Int
, x_max : Float
, y_max : Float
, particles : Particles
, graphMap : Graph.GraphMap
, message : String
, info : String
}
Most important is particles
, which looks like this:
type alias Particles =
{ a : Particle, b : Particle }
where a Particle
is as described below in the Physics
module. Our billiards system is thus a fixed-particle model, with two particles a
and b
.
The heart of the program is then given by the render
and update
functions. The render
function displays the current state of the simulator. For this it calls renderParticles model
, where renderParticles
calls renderParticle
, which is defined like this:
renderParticle : Model -> Particle -> S.Svg msgrenderParticle model particle =
Graph.drawCircle model.graphMap particle.circle
Here particle.circle
is a Circle
, an object defined in the Graph
module defined below.
XColor
The XColor
module is used by Geometry
to define the color of Geometry objects
such as Circle
and Rect
. It makes the definition
type alias XColor =
{ r : Int, g : Int, b : Int, a : Float }
and defines various colors, e.g.,
blueColor =
XColor 0 0 255 1.0
There is also a function rgba : XColor -> String
which is needed to use XColor
records with the Svg
module.
Geometry
The Geometry
module is mostly a set of type aliases — for Points
, Rectangles
, Circles
, etc. For example,
type alias Circle =
{ center : Point, radius : Float, fillColor : XColor,
strokeColor : XColor }
Here is a typical function:
distance : Point -> Point -> Float
Thus, if a
and b
are Points
, then it may happen that distance a b
is 42.
Physics
The Physics module sets up type aliases such as
type alias Vector =
{ x : Float, y : Float }
and
type alias Particle =
{ circle : Geometry.Circle
, velocity : Vector
, mass : Float
}
Among the functions defined in this module are distance : Particle -> Particle -> Float
and rotate : Float -> Vector -> Vector
. Thus rotate 1.2 v
will yield as value the vector v
rotated counterclockwise through an angle of 1.2 radians.
Graphics
The Graphics
module is facilitates graphing objects defined in Cartesian coordinates. These must be translated into screen coordinates and put in a form that can be consumed by the Svg
module. The core data structure is
type alias GraphMap =
{ sourceRect : Rect
, targetRect : Rect
}
Defining a GraphMap
is all that is needed to set up the map from Cartesian to screen coordinates. Then one can say, for example, Graph.drawCircle graphMap circle
to draw a circle, where the latter is a Circle
as defined above.