We're going to move away from HTML for a bit and check out Elm's graphics capabilities. Let's get started.

Project

mkdir graphics_playground
cd graphics_playground
elm-package install evancz/elm-graphics
elm-package install elm-lang/html

As an introduction, let's look at a basic example for Graphics:

import Color exposing (..)
import Collage exposing (..)
import Element exposing (..)
import Html exposing (Html)


main : Html msg
main =
  collage 800 800
    [ move (0,-55) blueSquare
    , move (0, 55) redSquare
    ] |> Element.toHtml


blueSquare : Form
blueSquare =
  traced (dashed blue) square


redSquare : Form
redSquare =
  traced (solid red) square


square : Path
square =
  path [ (50,50), (50,-50), (-50,-50), (-50,50), (50,50) ]

We can view it with the reactor:

elm-reactor # in split screen

Let's talk through this.

-- We import a few modules we want to use functions from
import Color exposing (..)
import Collage exposing (..)
import Element exposing (..)
import Html exposing (Html)


-- Our main function produces an `Html msg`, where `msg` is a type variable.  We
-- don't need to specify it since we aren't doing anything with it.
-- We're using Element.toHtml to turn our graphics Element into an Html type.
-- The elemenet is a `Collage`.  It's an 800x800 canvas, containing a list of
-- `Form`s.  We then move each form with the `move` function.
main : Html msg
main =
  collage 800 800
    [ move (0,-55) blueSquare
    , move (0, 55) redSquare
    ] |> Element.toHtml


-- Our bluesquare is a traced form.  `traced` takes two arguments - a
-- `LineStyle` and a `Path`, and produces a `Form`.
-- our LineStyle is `dashed blue`.  `dashed` takes a color and produces a
-- LineStyle.  `LineStyle` is just a record that you can view in the
-- `Graphics.Collage` documentation.  The `dashed` function creates a LineStyle
-- with an `[8, 4]` dashing property - so you get 8 units per dash and 4 units
-- of spacing between dashes.  `blue` is just a function from the `Color`
-- module.
-- Finally, we use the `square` function to define our path.
blueSquare : Form
blueSquare =
  traced (dashed blue) square


-- `redSquare` is a solid red square in red.
redSquare : Form
redSquare =
  traced (solid red) square


-- Finally, the `square` function produces a path that's a 100x100 square.
square : Path
square =
  path [ (50,50), (50,-50), (-50,-50), (-50,50), (50,50) ]

There are some obvious tweaks we can make. We'll make the collage track the user's mouse, but first let's extract the main function into a function that takes an X and Y and moves each of the squares by those values:

main : Html msg
main =
  squares (0, 0)
    |> Element.toHtml


squares : (Float, Float) -> Element
squares (x, y) =
  collage 800 800
    [ move (0 + x,-55 + y) blueSquare
    , move (0 + x, 55 + y) redSquare
    ]

We can refresh the page to make sure we haven't broken anything. Next, we'll bring in a subscription for the mouse, and we'll start to wire up with Html.App.program:

elm-package install elm-lang/mouse
-- We'll import the mouse module.  This gives us a few options for getting
-- subscriptions out of the mouse's position.
import Mouse

-- We'll add a Model
type alias Model =
  { position : Mouse.Position
  }

-- And a Msg type
type Msg
  = MoveMouse Mouse.Position


-- We'll define our initial model
model : Model
model =
  { position =
    { x = 0
    , y = 0
    }
  }


-- updating the position just sets the model's position to the new position.
-- Here we're using the `!` shorthand function.  It is an easy way to produce
-- your 2-tuples of models and commands.  Here's it's equivalent to having a
-- `Cmd.none` in the second position.
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    MoveMouse newPosition ->
      { model | position = newPosition } ! []


-- Our old main becomes our view function
view : Model -> Html Msg
view model =
  squares (model.position.x, model.position.y)
    |> Element.toHtml


-- We modify main to become a `Program`
main : Program Never
main =
  Html.program
    { init = (model, Cmd.none)
    , update = update
    , view = view
    , subscriptions = subscriptions
    }


subscriptions : Model -> Sub Msg
subscriptions model =
  Mouse.moves MoveMouse


squares : (Float, Float) -> Element
squares (x, y) =
  collage 800 800
    [ move (0 + x,-55 + y) blueSquare
    , move (0 + x, 55 + y) redSquare
    ]

Let's run it. It fails because we're passing in Ints but squares expects Floats. We can just change the type signature.

squares : (Int, Int) -> Element
squares (x, y) =
  collage 800 800
    [ move (0 + x,-55 + y) blueSquare
    , move (0 + x, 55 + y) redSquare
    ]

We can refresh...and that's not quite enough. The move function takes in floats as its first argument, but we're returning Ints. We can coerce our Ints into Floats when we add them:

squares : (Int, Int) -> Element
squares (x, y) =
  collage 800 800
    [ move (0 + toFloat(x),-55 + toFloat(y)) blueSquare
    , move (0 + toFloat(x), 55 + toFloat(y)) redSquare
    ]

If we refresh this, we can see that it's a bit funky. Turns out that we need to subtract the y values for it to feel natural, because our coordinate system has x increasing as you go right, and y increasing as you go up, but the mouse coordinates have Y increasing as you go down. This way we correct for the impedance mismatch between the two coordinate systems.

squares : (Int, Int) -> Element
squares (x, y) =
  collage 800 800
    [ move (0 + toFloat(x),-55 - toFloat(y)) blueSquare
    , move (0 + toFloat(x), 55 - toFloat(y)) redSquare
    ]

That sort of works, but why do we have to transform both forms? Let's group them and transform the group:

squares : (Int, Int) -> Element
squares (x, y) =
  let
    theGroup =
      group
        [ move (0,-55) blueSquare
        , move (0, 55) redSquare
        ]

    movedGroup =
        move (toFloat(x), toFloat(-y)) theGroup
  in
    collage 800 800 [ movedGroup ]

Alright, so the origin for the canvas is in the center here. Let's move things to the top left corner before we apply the mouse coordinates:

squares : (Int, Int) -> Element
squares (x, y) =
  let
    theGroup =
      group
        [ move (0,-55) blueSquare
        , move (0, 55) redSquare
        ]

    originGroup =
      move (-400, 400) theGroup

    movedGroup =
        move (toFloat(x), toFloat(-y)) originGroup
  in
    collage 800 800 [ movedGroup ]

Summary

With that, we've got our group centered on the mouse cursor as we move around. There are a few magic numbers floating in there - specifically, the 800 and 400 numbers - that could be better served by being derived from some higher level values about the width and height. However, this is a pretty solid introduction to producing a graphics application. I hope you enjoy fiddling with the Graphics modules for a bit. See you soon!

Resources