Mouse Chasing in Elm (Beginners)
Continuing upon my last post, I’ll be showing how basic Signal functionality can be used to update a data model in Elm.
Signals are a non-obvious concept to grasp at first glance. The Reactivity guide contains good in-depth instruction about how Signals work, but spending a little time playing with them can really help grasp how they work.
We’ll walk through a simple example to generate a snake-line line that follows the mouse. To see what we’re going to build, you can open up the demo here. (Note that this only works with a mouse, not with touch events. Exercise: Extend this example to support touch events using Elm’s Touch module.)
Let’s explore!
Intro to Signals
The idea behind signals in Elm is that all information flows one-way through the architecture. You define a data model, which flows into a view function, which knows how to generate a view from the data model. But, how do you update the data model? You capture a Signal that flows into an update function that knows how to update the data model. Each time the data model is updated, it will automatically trigger a new flow into the view function.
(This is a bit of an over-simplification. I do recommend that readers review the Reactivity guide to get a full in-depth understanding.)
In our demo, we’re going to capture updates to mouse signals in order to draw a line path that will follow the mouse.
Set Up and Run
(First, make sure you have elm installed.)
I’ve published the code in this post to a github repo.
You can run the example as-is with the following commands (I’ve included the command output for reference):
> git clone https://github.com/stormont/elm-mouse-chaser.git Cloning into 'elm-mouse-chaser'... remote: Counting objects: 11, done. remote: Compressing objects: 100% (9/9), done. remote: Total 11 (delta 0), reused 7 (delta 0), pack-reused 0 Unpacking objects: 100% (11/11), done. Checking connectivity... done. > cd elm-mouse-chaser > git checkout 2015.10.27.01 > elm-make src/Main.elm --output=demo.html Some new packages are needed. Here is the upgrade plan. Install: elm-lang/core 2.1.0 Do you approve of this plan? (y/n) y Downloading elm-lang/core Packages configured successfully! Success! Compiled 31 modules. Successfully generated demo.html
At this point, you’ll have generated a demo.html file, which you can open in your browser and observe the result. (This is the same output I linked to from the beginning of the post.)
Architecture
Following the standard Elm Architecture, we’ll develop four segments to our code:
- The data model
- The update function
- The view function
- The Signals
Before we start, we’ll define some simple module imports to reference. At the top of the file, define these imports:
import Color exposing (..) import Graphics.Collage exposing (..) import Graphics.Element exposing (..) import Mouse import Window
These are all modules defined in the core Elm package, so you don’t need to install any additional packages to reference these.
Also, as we move along, pretty much every line of code has an inline comment that describes what that line is meant for. These may be descriptive enough that you don’t need to keep reading this post!
Now, let’s dig in to our data model!
The Data Model
Our data model is really simple:
type alias MouseX = Int type alias MouseY = Int type alias Point = (MouseX, MouseY) -- Mouse position (x,y). type alias Model = List Point -- A list of points.
Type aliases in Elm allow you to use new names to reference the same data types. You can use the name on the left-hand side of the assignment interchangeably with the definition found on the right-hand side. They’re not required by any means, but can be a nice, descriptive shorthand.
In the definition above, we define both MouseX and MouseY as Ints. This isn’t necessary at all (and you won’t see these beyond the Point definition), but it helps show what the two different elements of the Point tuple type are meant to represent.
Our actual Model is just a List of Points.
The Update Function
Our update function is also pretty simple:
update : Point -> Model -> Model update p model = let numPoints = 12 -- Number of points to retain. points = p :: model -- Prepend the latest Point (AKA, "cons"). in List.take numPoints points -- Update the path (limiting path length).
First, we define update as a function that expects two arguments, a Point and a Model (which, recall, is actually just a List of Points). The final parameter specifies that we return a new Model from the function.
The let segment allows us to define variables that we can use later within the in segment, which is our actual function implementation. Here, we define numPoints as 12; this is an arbitrary number of trailing line segments that we want to draw chasing the mouse. We also define points as our new point prepended on to our previous model. The funny (::) operator is called “cons”, which is short for “construct”. Don’t let the funny name confuse you; it’s just a simple list prepend operation.
The in segment simply defines our new model. We just say that we’re going to take the first numPoints elements from our points list. Recall, numPoints equals 12 and we prepend new elements to the head of the list, so we’re taking the 12 most recently added elements of the list to return as our new model.
The View Function
Our view function gets a bit more complex. We’re going to build it up from the bottom with some helper functions:
lineStyle : LineStyle lineStyle = { defaultLine -- Extend the "default" definition. | width <- 10.0 -- Line width. , color <- blue -- Assign the color. , cap <- Round -- The shape of the end points. , join <- Smooth -- The shape of the joints. }
Here, we’re defining a LineStyle to use for drawing our line. The curly braces are how Elm defines record types. I won’t go in-depth about records, but you can read more about them here. If you’re unfamiliar with them, just try to follow along.
First, we use the defaultLine function to generate a “default” specification for our LineStyle record. The | operator basically says “Take all of the comma-delimited definitions on the right-hand side and apply them as updates to the record on the left-hand side”. You can update existing record fields or even extend a record with new field definitions! Each of the field names are defined on the left of the <- operator, with the assigned value on the right. This is a simple example, but you can use more complex things like expressions on the right side of the operator.
The function ultimately returns our updated LineStyle record, which we use to draw our line segments:
drawLineSegments : (Int,Int) -> List Point -> Form drawLineSegments (w,h) points = List.map (\(x,y) -> (toFloat x, toFloat -y)) points -- Convert the mouse points to -- "model" coordinates. |> path -- Build a path from the points. |> traced lineStyle -- Trace the line with defined form. |> move (-(toFloat w) / 2, (toFloat h) / 2) -- Move drawing from middle to upper -- left ("screen" coordinates).
We define drawLineSegments as a function that takes an (Int, Int) tuple to define the size of our canvas, a List of Points that represent the Points in our Model, and returns a Form, which is just a definition for a kind of shape object. Technically, the (Int, Int) tuple is the same definition as a Point, but I’ve retained the name to highlight a different intent: This parameter is used to define the size of our canvas, which will just be the full window in this case (but more on that later).
Inside our function, it begins with a List.map to shift all of our points values to model coordinates. This is because the coordinate system of a collage is the center of the resulting Form, but window (and mouse) coordinates begin in the upper left hand corner as (0, 0). We have to use the toFloat function to convert the mouse coordinates, defined as Ints, into floating point values. Elm doesn’t allow implicit type conversions between numbers or any other types, so we have to explicitly convert values when we need them as a different type.
The next line uses the |> operator to take the result of the List.map and pipes it into the Graphics.Collage.path function to generate a Path type from the result (the angle indicates the direction of data flow). The Elm documentation isn’t particularly clear as to the purpose of the Path type, so I have to skim over this a bit; if I had to guess, I’d say it’s a representation of a set of points where the sequence is considered important, whereas a List can typically be considered to be a selection of non-determinant choices.
The important thing is that we need an intermediate Path type to send to the traced function (again, piping through a |> operator). This takes the lineStyle, which we defined above, and we use that style to draw along the Path points. This generates a Form object.
Lastly, we pipe this again using |> to the move function to correctly orient the final Form. I’ll admit that I didn’t spend a lot of time thinking through the required coordinate transformations throughout the drawLineSegments function; I played with it using values that seemed to make sense, until the representation came out right.
Now that we’ve defined a Form to draw our line segments, we’ll render this into an HTML canvas element:
view : (Int,Int) -> Model -> Element view (w,h) model = collage w h [ drawLineSegments (w,h) model ] -- Draw the path.
Here, we finally define our view function. It has the same type declaration as our drawLineSegments function, except that we return an HTML Element type, rather than a graphic Form.
We use collage to generate an HTML canvas, again matching the provided dimensions (which we’ll later match to the full window dimensions). We provide a List of Forms to the collage function, which in this case has just a single element: The output of our drawLineSegments function.
This HTML canvas element is what Elm will actually draw to the screen.
The Signals
As I mentioned near the beginning, Signals are ways of sending updates to our data models. In this case, we’ll simply trap mouse signals and send them to our update function. (Again, I leave it as an exercise to the reader to extend this to support Touch events!)
mousePositions : Signal Model mousePositions = Signal.foldp -- Fold each signal into an accumulated model... update -- ... through the update function. [] -- Start with an empty list. Mouse.position -- Updates given by mouse position changes.
Our mousePositions function will capture mouse signals and call update. We do this by using the Signal.foldp function, which can be interpreted as aggregating (or folding) values into a past-dependent function. We simply give foldp the function we want to call (update), an initial state (in this case, an empty list, []), and a Signal source for updates (in this case, Mouse.position). The type of the Signal source must match the first parameter type of the past-dependent function (update), while the type of the initial state must match the second parameter of update.
Mouse.position works by trapping every update to the position of the mouse and firing a Signal into our mousePositions function, which itself channels it through the foldp function.
Lastly, we tie all of the above together by defining the required main function:
main : Signal Element main = Signal.map2 -- Map two signals together... view -- ... through the view function. Window.dimensions -- Use updates to the window dimensions as the first signal. mousePositions -- Use updates to mouse positions as the second signal.
Elm requires a main function to be defined, which itself just emits a Signal of HTML Elements to the internal Elm architecture. We use Signal.map2 to capture two different Signals and map them into our view function.
The first Signal we capture is Window.dimensions, which will capture modifications to the size of the browser window. It will also fire once at the beginning of our program with the starting size of the window.
The second Signal we capture is our mousePositions function that we previously defined.
Conclusion
With that, we’re done! If you run the demo, you’ll see a blue trail of line segments that will follow your mouse cursor around.
Signals are core component of Elm architecture. They contain a surprising amount of power and are used prolifically.
Hopefully, you found this post fun and instructive! I plan on posting a few more extensions and variations upon the code presented here, as it serves as a good testing ground. Please feel free to experiment with the code given here and to modify it to explore for yourself! Also, you should find that the full source code can be copy and pasted right into Elm’s online editor, which is a great test bed for experimentation.
2 comments