Chapter 8. Circles
The next example, Circles.elm, is a program which maintains state. Initially, the program only shows an empty square. However, after you click inside it, a colorful circle is created and starts moving inside the square. After each subsequent click, another circle is created. Before continuing, take a look at the working program (Circles.html), to have an idea of how it works.
The code is divided into three modules:
- CirclesModel
- CirclesView
- Circles
We start our analysis with the CirclesModule module, defined in the CirclesModel.elm, which starts with the usual module declaration followed by imports and three type declarations:
File CirclesModel.elm (fragment): module CirclesModel where import Color exposing (Color, rgb) import Time exposing (Time) import Random exposing (generate, int, initialSeed) type alias Position = { x: Int, y: Int } type alias CircleSpec = { radius: Int, xv: Int, yv: Int, col: Color, creationTime: Time } type alias Circle = { position: Position, circleSpec: CircleSpec }
The module defines three data types, that we will use in our program. Their definitions start with the type alias keywords followed by the type name. The type alias statement creates a type alias, that is, the type on the right hand side of the equals sign acquires a new name.
All of the data types are records. A record is a data structure consisting of one or more values. Each value in a record has a name and a type. For example, Position is an alias for a record type consisting of two values, x and y, both of type Int.
We can create record values by providing field names followed by the equal sign and the field values, separated by commas and enclosed in curly braces. Here is an example:
> myRecord = { x = round 1.1, y = round 4.9 } { x = 1, y = 5 } : { x : Int, y : Int }
We can reference record fields by appending a dot and the field name to the record name. The following function converts our record to a string:
> import String exposing (concat) > showRecord rec = concat [toString rec.x, " ", toString rec.y] <function> : { c | y : b, x : a } -> String > showRecord myRecord "1 5" : String
We can also pattern match on record fields. We do it by providing a pattern consisting of field names separated by commas and enclosed in curly braces. Here is another version of the showRecord function, which uses pattern matching in the let expression:
> showRecord rec = let {x,y} = rec in concat [toString x, " ", toString y] <function> : { c | x : a, y : b } -> String > showRecord myRecord "1 5" : String
Pattern matching can also be used directly in the function parameters, as will be shown in a moment.
Going back to our data types, the CircleSpec records contain data specifying a circle: its radius, its vertical and horizontal velocity (xv and yv), its color and the time when it was created. The Circle data type contains the circle specification (CircleSpec) and its position.
The makeCircleSpec function creates a new CircleSpec record given a Time value.
File CirclesModel.elm (fragment): makeCircleSpec : Time -> CircleSpec makeCircleSpec time = let seed1 = initialSeed (round time) (radius,seed2) = generate (int 10 30) seed1 (xv,seed3) = generate (int 10 50) seed2 (yv,seed4) = generate (int 10 50) seed3 (r,seed5) = generate (int 10 220) seed4 (g,seed6) = generate (int 10 220) seed5 (b,_) = generate (int 10 220) seed6 in { radius = radius , xv = xv , yv = yv , col = rgb r g b , creationTime = time }
The makeCircleSpec function is using several functions from the Random module, which contains functions related to generating random values. The function generates random values using the generate function, which has the following type:
generate : Generator a -> Seed -> (a, Seed)
Its first argument is a generator, which produces random values of a certain type a. The makeCircleSpec function uses generators created by the int function, which takes two Int arguments defining a range of values and returns a generator of Int values from that range.
int : Int -> Int -> Generator Int
Besides a generator, the generator function needs a Seed value to produce a result. A Seed value can be created by the initialSeed function, which takes an Int value.
initialSeed : Int -> Seed
The generate function returns a random value produced by the generator and a new seed. If we try to call the generate function with the same generator and seed, we will always get the same return value (the REPL output is a little verbose, showing the internals of the seed):
> import Random exposing (..) > generate (int 10 18) (initialSeed 12345) (12,{ next = <function>, range = <function>, split = <function>, state = State 494012844 40692 }) : ( Int, Random.Seed ) > generate (int 10 18) (initialSeed 12345) (12,{ next = <function>, range = <function>, split = <function>, state = State 494012844 40692 }) : ( Int, Random.Seed )
That is not very useful if we really need randomness. We can, however, use the seed returned by generate as the input seed in the next call to generate:
> result = generate (int 10 18) (initialSeed 12345) (12,{ next = <function>, range = <function>, split = <function>, state = State 494012844 40692 }) : ( Int, Random.Seed ) > generate (int 10 18) (snd result) (18,{ next = <function>, range = <function>, split = <function>, state = State 1991225964 1655838864 }) : ( Int, Random.Seed )
The makeCircleSpec function uses a time value (recall that Time is an alias of Float) to get the initial seed. It then calls generate several times in order to calculate the circle specification elements, passing previously calculated seeds as input to the subsequent generate calls. In order to calculate the circle color, it uses the rgb function, which takes three Int values representing the primary colors: red, green and blue.
> import CirclesModel exposing (..) > makeCircleSpec 12345.0 { col = RGBA 34 33 99 1, creationTime = 12345, radius = 18, xv = 10, yv = 34 } : CirclesModel.CircleSpec
We will now turn our attention to the CirclesView module. It is defined in the CirclesView.elm file and it starts as usual with the module declaration and a list of imports.
File CirclesView.elm: module CirclesView where import CirclesModel exposing (Circle, CircleSpec, Position) import Color exposing (black, red, green) import Graphics.Collage exposing (circle, collage, filled, move, outlined, rect, solid) import Graphics.Element exposing (layers) import List exposing (map)
The boundingBox function draws a square of a given width and height. The outlined function draws the square border using the line specification provided as its first argument. In our case we want a solid black line. The solid function takes a color and returns a LineStyle value representing the line style to be used.
File CirclesView.elm (fragment): boundingBox w h = collage w h [ outlined (solid black) <| rect (toFloat w) (toFloat h), outlined (solid black) <| rect (toFloat (w-2)) (toFloat (h-2)) ]
The drawCircle function draws a circle. It takes as arguments the width and height of the bounding box, and the information about the circle, of type Circle. The third argument is not specified in the usual way — as an indentifier. Instead, a pattern is used, which enumerates the member names of the Circle data type. The members are separated by a comma and enclosed in curly braces. This is pattern matching in action again. The function body can directly reference the members of the Circle record passed as the function argument. Although the drawCircle function enumerates all members of the CircleSpec data type in the pattern, in general we do not have to enumerate all of them if not all of them are used in the function body.
File CirclesView.elm (fragment): drawCircle w h {position, circleSpec} = filled circleSpec.col (circle (toFloat circleSpec.radius)) |> move (toFloat position.x - (toFloat w)/2, (toFloat h)/2 - toFloat position.y)
The circle position is adjusted, because we want to specify the position using the same coordinate system that is used by Elm to represent the mouse clicks positions. That coordinate system has its origin (the point with the (0,0) coordinates) in the left-upper corner. The x coordinates increase to the right, and the y coordinates increase downwards. However, the coordinate system used for drawing forms in a collage is different: the origin is in the middle and the coordinates increase when going to the right and upwards.
The drawCircles function draws circles from a given list by mapping the drawCircle function over the list. The view function draws the complete view by drawing the bounding box first and the circles on top of it. To draw two (or more) elements on top of each other, the layers function from the Graphics.Element module is used.
File CirclesView.elm (fragment): drawCircles w h circles = collage w h (map (drawCircle w h) circles) view w h circles = layers [ boundingBox w h, drawCircles w h circles ]
The main function let us test the view function with sample data. Notice, that since the drawing functions do not use all of the fields from the records defined in CirclesModel, we do not need to provide all of them when constructing the sample data. With our test data, we should see a red circle in the left-upper corner, and a green one in the center of the bounding box. You can verify whether that is what really happens here: CirclesView.html.
File CirclesView.elm (fragment): main = view 400 400 [ { circleSpec = { col = red, radius = 26 }, position = { x = 0, y = 0 } }, { circleSpec = { col = green, radius = 43 }, position = { x = 200, y = 200 } } ]
The Circles module defines the signals used in our program. Here is its beginning:
File Circles.elm (fragment): module Circles where import CirclesModel exposing (..) import CirclesView exposing (..) import List exposing ((::), map) import Mouse import Signal exposing (Signal, (<~), (~), filter, foldp, sampleOn) import Time exposing (Time, fps, timestamp)
The module creates several signals and combines them together. The following figure presents the relations between the individual signals.
The first signal from the Circles module is created by the clockSignal function. It periodically outputs a timestamp. The rate of events is established by the fps function (fps means frames per second) from the Time module.
File Circles.elm (fragment): clockSignal : Signal Time clockSignal = fst <~ timestamp (fps 50)
The signal created by the clickPositionsSignal function outputs the mouse pointer position on every click.
File Circles.elm (fragment): clickPositionsSignal : Signal (Int, Int) clickPositionsSignal = sampleOn Mouse.clicks Mouse.position
The inBoxClickPositionsSignal function takes the width and height as arguments and creates a signal, that filters the events from the clickPositionsSignal to only output those that represent positions inside the bounding box of the given width and height. The filter function from the standard Signal module is used for filtering the events.
File Circles.elm (fragment): inBoxClickPositionsSignal : Int -> Int -> Signal (Int, Int) inBoxClickPositionsSignal w h = let positionInBox pos = fst pos <= w && snd pos <= h in filter positionInBox (0, 0) clickPositionsSignal
The creationTimeSignal function produces a signal representing the creation times of the circles. It is created by sampling (using the sampleOn function) the signal produced by the clockSignal function on the events carried on by the signal from the inBoxClickPositionsSignal function.
File Circles.elm (fragment): creationTimeSignal : Int -> Int -> Signal Time creationTimeSignal w h = sampleOn (inBoxClickPositionsSignal w h) clockSignal
The newCircleSpecSignal function combines several signals to produce a signal representing the specifications of the new circles.
File Circles.elm (fragment): newCircleSpecSignal : Int -> Int -> Signal CircleSpec newCircleSpecSignal w h = makeCircleSpec <~ creationTimeSignal w h
A full circle representation consists of its specification and its position. The newCircleSignal function produces a signal representing circles by combining the newCircleSpecSignal (representing the specifications) and the inBoxClickPositionsSignal (representing the positions — the initial position of a circle is the position of where the user clicked inside the bounding box) signals.
File Circles.elm (fragment): newCircleSignal : Int -> Int -> Signal Circle newCircleSignal w h = let makeCircle (x,y) spec = { position = { x = x, y = y }, circleSpec = spec } in makeCircle <~ inBoxClickPositionsSignal w h ~ newCircleSpecSignal w h
We have arrived to a moment when we want to maintain state in our program. We want the program to remeber the circles created each time the user clicked inside the bounding box. After each such click, we want to add the new circle to a list of previous circles (our list is empty at the beginning). New circles are represented by a signal of type Signal Circle. We now want to have a signal of type Signal (List Circle), which contains a list of all circles created so far.
We can use the foldp function from the Signal module to maintain state in Elm programs. The signature of that function looks as follows:
foldp : (a -> b -> b) -> b -> Signal a -> Signal b
It takes three arguments. It transforms a signal of values of some type a (given as the third argument) into a signal of values of some type b. The initial value of the output signal is provided as the second argument. The first argument is a function that takes two arguments — one of type a and one of type b — and returns a value of type b. Let’s call that function a transformation function. For each event of the input signal (the signal of values of type a) the transformation function is called with the value of the new signal, and the current value of the output signal as arguments. The result of calling the transformation function becomes the new value of the output signal. It will also be the value given as the second argument in the subsequent call to the transformation function — whenever a new event from the input signal is going to be processed. Thus the b values represent the state that is maintained by the foldp function and also the results emitted by the signal produced by that function.
The allCirclesSpecSignal function uses foldp to maintain a list of circles — which is our state. The initial value of the state is the empty list, and this is the value of the second argument given to the foldp function. The input signal provided as the third argument is produced by the newCircleSignal. The first argument is simply a function that appends the new circle to the current list of circles. We can thus use the :: operator as the first argument to our foldp call:
File Circles.elm (fragment): allCirclesSpecSignal : Int -> Int -> Signal (List Circle) allCirclesSpecSignal w h = foldp (::) [] (newCircleSignal w h)
Since :: is an operator — its name is not alphanumeric, but it contains other symbols — we have to enclose its name in parenthesis. Otherwise the Elm compiler would complain.
Having a list of circles is not enough for our purposes. We want the circles to move on the screen. We thus have to update their coordinates as a function of time. We will use the computeCoordinate function to calculate the circle coordinates — each of the x and y coordinates separately. The computeCoordinate function takes four arguments:
- the coordinate of the starting position
- the size of the box inside which the circle is to move
- the circle velocity
- the time that passed since the circle was created
It computes the distance the circle has travelled since its creation. It then calculates the coordinate based on that distance. If the distance divided by the box size is even, the circle should move to the right or downwards and the coordinate is taken as the distance modulo the box size. If it is odd, the circle should move to the left or upwards, and the coordinate is the box size minus the distance modulo the box size.
File Circles.elm (fragment): computeCoordinate : Int -> Int -> Float -> Float -> Int computeCoordinate startingPointCoordinate boxSize velocity time = let distance = startingPointCoordinate + round(velocity * time / 1000) distanceMod = distance % boxSize distanceDiv = distance // boxSize in if (distanceDiv % 2 == 0) then distanceMod else boxSize - distanceMod
The positionedCircle function transforms a Circle value representing its initial state, into a new Circle value with its position updated.
File Circles.elm (fragment): positionedCircle : Int -> Int -> Float -> Circle -> Circle positionedCircle w h time circle = let {position, circleSpec} = circle {radius, xv, yv, creationTime} = circleSpec relativeTime = time - creationTime boxSizeX = w - radius*2 boxSizeY = h - radius*2 x = radius + computeCoordinate (position.x-radius) boxSizeX (toFloat xv) relativeTime y = radius + computeCoordinate (position.y-radius) boxSizeY (toFloat yv) relativeTime in { position = { x=x, y=y }, circleSpec = circleSpec }
We use two different techniques to access individual members of the Circle record. The first line after the let keyword use pattern matching to extract individual members of the circle. The second line also uses pattern matching to extract the members of the circleSpec value. We do not perform the similar extraction for the position members. Instead, the x and y coordinates are accessed using the dot notattion. We calculate the circle position using the computeCoordinate function separately for x and y coordinates. The size of the box inside which the circle is to move is calculated as the widht or height of the bounding box minus twice the circle radius. We substract the radius, because the position represents the center of the circle, but we want the circle to change its direction as soon as its boundary touches the border of the bounding box. Thus the real bounding box for the center of a given circle is smaller than the visible bounding box by twice the circle radius — we substract the radius once for each side of the box. (There is a small problem with this design, which the implementation silently ignores. What happens when the user clicks near the border of the bounding box? The center of the circle, after taking the modulo operation, may not be in the same place where the user clicked.)
The positionedCircles function maps the positionedCircle function partially applied with the appropriate parameters, through a list of Circle values.
File Circles.elm (fragment): positionedCircles : Int -> Int -> Float -> List Circle -> List Circle positionedCircles w h time circles = map (positionedCircle w h time) circles
Notice how we have used the positionedCircle function. That function takes a Circle as its last argument. The map function requires a one-argument function and in our case the argument should have the type of Circle. By partially applying all but last arguments of the positionedCircle function, we obtain a one-argument function suitable to be passed as the first argument to map.
We can now use positionedCircles to create a signal of circles whose positions change while time passes. This is the job of the circlesSignal function, which combines the signals produced by clockSignal and allCirclesSpecSignal.
File Circles.elm (fragment): circlesSignal : Int -> Int -> Signal (List Circle) circlesSignal w h = positionedCircles w h <~ clockSignal ~ allCirclesSpecSignal w h
The main function uses the values of the circlesSignal as input to the view function from the CirclesView module, producing the main program signal, that is rendered by Elm’s runtime.
File Circles.elm (fragment): main = let main' w h = view w h <~ circlesSignal w h in main' 400 400
So far we have used signals from the Mouse module to get mouse related input. The next chapter presents an alternative way of processing mouse events.