In the last 2 episodes we've built an Etch-a-Sketch application. It's pretty usable, but it's also kind of lame that you have to refresh the page to clear everything out, rather than shaking it. We're going to add a button to shake it and clear everything out. Let's get started.

Project

Graphics Input

We'll start off by just getting a button onto our screen. In earlier version of Elm, you could do this with the graphics package, but now we have to use html buttons. That's not a big problem.

Our buttons send messages. Let's add the message we're going to send:

type Msg
  = KeyboardExtraMsg Keyboard.Extra.Msg
  | Tick Time
  | Shake

We'll handle it in our update with a no-op for now...

update : Msg -> Model -> Model
update msg model =
  case msg of
    -- ...
    Shake ->
      model ! []

Next, let's add a button to the view that emits a Shake message when we click it:

shakeButton : Html Msg
shakeButton =
  Html.button [onClick Shake] [ Html.text "Shake it good" ]

Finally, we need to introduce the button into the view. To do this, we'll wrap the exixting canvas element in a div and make the shakeButton a sibling:


view : Model -> Html Msg
view model =
  div []
    [ collage 800 800
        [ (drawLine model.points) ]
        |> Element.toHtml
    , shakeButton
    ]

If we refresh the view, we should see a button below our collage now:

elm-reactor

OK, so now we need to make that Shake message clear out the list of points:

    Shake ->
      { model | points = [] } ! []

Refresh the page...and now when we click the "Shake it good" button, our drawing is cleared but the cursor position for our Etch-A-Sketch is not affected, of course.

Animation

Of course, that's kind of boring. It's cool that it works and that it took just a few lines of code, but honestly I'd like to see more pizzazz out of a button with such a fantastic label. Let's introduce animation shall we?

To do this, we'll pull in the elm-animation package:

elm package install mgold/elm-animation

We also need to bring in animation-frame:

elm package install elm-lang/animation-frame

To work with elm-animation, you need a clock. We already have one, in the form of our Tick action. We will need the delta in time though, to perform the animations, so we'll use the delta that's provided by AnimationFrame, and we'll wire up our subsriptions with it.

import AnimationFrame

type Msg
  = KeyboardExtraMsg Keyboard.Extra.Msg
  | Tick Time
  | Shake


subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.batch
    [ Sub.map KeyboardExtraMsg Keyboard.Extra.subscriptions
    , AnimationFrame.diffs Tick
    ]

OK, with that everything still works, but now we're ticking between the browser's draw cycles. We also need to add a clock field to the Model to track our elapsed time, and track it on Tick:

type alias Model =
  { points : List Point
  , x : Int
  , y : Int
  , keyboardModel : Keyboard.Extra.Model
  , clock : Time -- <---
  }


init : ( Model, Cmd Msg )
init =
  let
    ( keyboardModel, keyboardCmd ) = Keyboard.Extra.init
  in
    ( { points = [(0, 0)]
      , x = 0
      , y = 0
      , keyboardModel = keyboardModel
      , clock = 0 -- <---
      }
    , Cmd.batch
      [ Cmd.map KeyboardExtraMsg keyboardCmd
      ]
    )


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    -- ...
    Tick dt ->
      let
        {x, y} = Keyboard.Extra.arrows model.keyboardModel
        newX = model.x + x
        newY = model.y + y
        newClock = model.clock + dt
      in
        case (x, y) of
          (0, 0) ->
            { model | clock = newClock }! []
          _ ->
            { model | points = (newX, newY) :: model.points, x = newX, y = newY, clock = newClock } ! []

OK, with that we can refresh, and it all still works but now our Model is tracking elapsed time that we can use for the animations.

Now we'll import elm-animation and add an animation key to our Model that starts out as a static animation, which just means it doesn't change:

import Animation exposing (..)

type alias Model =
  { points : List Point
  , x : Int
  , y : Int
  , keyboardModel : Keyboard.Extra.Model
  , clock : Time
  , animation : Animation
  }

init : ( Model, Cmd Msg )
init =
  let
    ( keyboardModel, keyboardCmd ) = Keyboard.Extra.init
  in
    ( { points = [(0, 0)]
      , x = 0
      , y = 0
      , keyboardModel = keyboardModel
      , clock = 0
      , animation = static 0 -- <---
      }
    , Cmd.batch
      [ Cmd.map KeyboardExtraMsg keyboardCmd
      ]
    )

Alright, let's just refresh to make sure it's valid. Good to go so far.

Next, we need to understand how animation works here. You animate between two numbers, for a given duration, and it produces a function that returns the position that the animation between those two numbers is, given a Time.

Let's go ahead and introduce a default animation that is not static, and thread it into our view, in order to see what I'm talking about:

-- We provide an initial time to seed the animation with
shakeAnimation : Time -> Animation
shakeAnimation t =
  -- We expect to seed the current time when generating this animation
  animation t
  -- Its value will begin at 0
  |> from 0
  -- And it will end up at 360
  |> to 360
  -- Taking 4 seconds to get there.  By default, it follows a sinusoidal easing
  -- function
  |> duration (4 * Time.second)


initialModel : Model
initialModel =
  { points = [(0, 0)]
  , x = 0
  , y = 0
  , arrows = { x = 0, y = 0 }
  , clock = 0
  --, animation = static 0
  , animation = shakeAnimation 0
  }

If we refresh the page, we can see that it all still works. Next we'll use this animation to rotate our drawn path:

-- First, since we haven't looked at rotation before, let's look at rotating the
-- sketch by 30 degrees.
view : Model -> Html Msg
view model =
  div []
    [ collage 800 800
        [ (rotate (degrees 30) (drawLine model.points)) ]
        |> Element.toHtml
    , shakeButton
    ]

If we refresh the screen, we can see that the Form is now rotated by 30 degrees. We can try replacing the 30 with the animation value given the model's current clock and see what happens:

view : Model -> Html Msg
view model =
  let
    angle =
      animate model.clock model.animation
  in
    div []
      [ collage 800 800
          [ (rotate (degrees angle) (drawLine model.points)) ]
          |> Element.toHtml
      , shakeButton
      ]

Alright! Finally we can see animation happening. This is fundamentally how the animations work. Now we'd like to wire this up to the Shake action:

init : ( Model, Cmd Msg )
init =
  let
    ( keyboardModel, keyboardCmd ) = Keyboard.Extra.init
  in
    ( { points = [(0, 0)]
      , x = 0
      , y = 0
      , keyboardModel = keyboardModel
      , clock = 0
      -- We'll start out with a static animation again
      , animation = static 0
      }
    , Cmd.batch
      [ Cmd.map KeyboardExtraMsg keyboardCmd
      ]
    )

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    -- ...
    Shake ->
      { model
      | points = []
      -- And we'll update the animation to our shakeAnimation seeded with our
      -- current clock when the button is pressed.
      , animation = shakeAnimation model.clock
      } ! []

We can try it out...and of course it's terrible, because the points are cleared simultaneously with adding the animation, which means we're animating a blank form. We'll add one more tweak - when we're updating the Tick, we'll check the state of the model's animation. When it's done, we'll clear the points out:

-- We'll also extract the common bits of our model update into a model'
-- function,
update : Action -> Model -> Model
update action model =
  case action of
    -- ...
    -- We'll stop clearing out the points immediately on Shake
    Shake ->
      { model
      | animation = shakeAnimation model.clock
      } ! []

    Tick dt ->
      let
        {x, y} = Keyboard.Extra.arrows model.keyboardModel
        newX = model.x + x
        newY = model.y + y
        newClock = model.clock + dt
        -- We'll return newPoints and a newAnimation from a case statement based
        -- on whether or not we're presently running an animation, using
        -- destructuring and returning a 2-tuple from each branch
        (newPoints, newAnimation) =
          -- Static animations are always done, so if it's a static animation
          -- we'll just return the existing points and animation
          case (model.animation `equals` (static 0)) of
            True ->
              (model.points, model.animation)
            False ->
              -- Otherwise, when the animation's done we'll clear the points and
              -- switch to the static animation again
              case (isDone model.clock model.animation) of
                True -> ([], (static 0))
                -- If it's not done we won't change anything
                False -> (model.points, model.animation)
        -- We'll create an intermediate model that adds our new x and y points
        -- and updates the clock and animation
        model' =
          { model
          | points = (newX, newY) :: newPoints
          , clock = newClock
          , animation = newAnimation
          }
      in
        case (x, y) of
          (0, 0) ->
            model' ! []
          _ ->
            { model'
            | x = newX
            , y = newY
            } ! []

And if we try this out it works. Of course, we've also introduced the problem where our points get appended to even when the keyboard arrows aren't running, so let's fix that again:

    -- ...
    Tick dt ->
      let
        {x, y} = Keyboard.Extra.arrows model.keyboardModel
        newX = model.x + x
        newY = model.y + y
        newClock = model.clock + dt
        -- We'll return newPoints and a newAnimation from a case statement based
        -- on whether or not we're presently running an animation, using
        -- destructuring and returning a 2-tuple from each branch
        (newPoints, newAnimation) =
          -- Static animations are always done, so if it's a static animation
          -- we'll just return the existing points and animation
          case (model.animation `equals` (static 0)) of
            True ->
              (model.points, model.animation)
            False ->
              -- Otherwise, when the animation's done we'll clear the points and
              -- switch to the static animation again
              case (isDone model.clock model.animation) of
                True -> ([], (static 0))
                -- If it's not done we won't change anything
                False -> (model.points, model.animation)

        newPoints' =
          case (x, y) of
            (0, 0) ->
              newPoints
            _ ->
              (newX, newY) :: newPoints
        -- We'll create an intermediate model that adds our new x and y points
        -- and updates the clock and animation
        model' =
          { model
          | points = newPoints'
          , clock = newClock
          , animation = newAnimation
          }
      in
        case (x, y) of
          (0, 0) ->
            model' ! []
          _ ->
            { model'
            | x = newX
            , y = newY
            } ! []

Alright, that was all a little frustrating. We can try it now though, and it's animating nicely. However, it's way too slow. Let's speed it up:

shakeAnimation : Time -> Animation
shakeAnimation t =
  animation t
  |> from 0
  |> to 360
  |> duration (500*Time.millisecond)

Summary

And with that, everything works and we can shake our Etch-A-Sketch. I know this was a bit long, but I figured that introducing solely the button in this episode would have been a bit boring and really wanted to show the animation stuff in a video episode. Next, we'll look at how we can introduce multiple animations. See you soon!

Resources

Code Listing

The final code is as follows:

import Color exposing (..)
import Collage exposing (..)
import Element exposing (..)
import Keyboard
import Html exposing (..)
import Html.Events exposing (onClick)
import Html.App as App
import Keyboard.Extra
import Time exposing (Time, second)
import AnimationFrame
import Animation exposing (..)


type alias Model =
  { points : List Point
  , x : Int
  , y : Int
  , keyboardModel : Keyboard.Extra.Model
  , clock : Time
  , animation : Animation
  }


type alias Point = (Int, Int)


type Msg
  = KeyboardExtraMsg Keyboard.Extra.Msg
  | Tick Time
  | Shake


shakeAnimation : Time -> Animation
shakeAnimation t =
  animation t
  |> from 0
  |> to 360
  |> duration (500*Time.millisecond)


init : ( Model, Cmd Msg )
init =
  let
    ( keyboardModel, keyboardCmd ) = Keyboard.Extra.init
  in
    ( { points = [(0, 0)]
      , x = 0
      , y = 0
      , keyboardModel = keyboardModel
      , clock = 0
      -- We'll start out with a static animation again
      , animation = static 0
      }
    , Cmd.batch
      [ Cmd.map KeyboardExtraMsg keyboardCmd
      ]
    )


shakeButton : Html Msg
shakeButton =
  Html.button [onClick Shake] [ Html.text "Shake it good" ]


view : Model -> Html Msg
view model =
  let
    angle =
      animate model.clock model.animation
  in
    div []
      [ collage 800 800
          [ (rotate (degrees angle) (drawLine model.points)) ]
          |> Element.toHtml
      , shakeButton
      ]


drawLine : List Point -> Form
drawLine points =
  let
    -- Our points are integers, but a path needs a list of floats.  We'll make a
    -- function to turn a 2-tuple of ints into a 2-tuple of floats
    intsToFloats : (Int, Int) -> (Float, Float)
    intsToFloats (x, y) =
      (toFloat x, toFloat y)

    -- Then we'll map our points across that function
    shape = path (List.map intsToFloats points)
  in
    -- Finally, we'll trace that list of points in solid red
    shape
    |> traced (solid red)


update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    KeyboardExtraMsg keyMsg ->
      let
        ( keyboardModel, keyboardCmd ) =
            Keyboard.Extra.update keyMsg model.keyboardModel
      in
        ( { model | keyboardModel = keyboardModel }
        , Cmd.map KeyboardExtraMsg keyboardCmd
        )

    Tick dt ->
      let
        {x, y} = Keyboard.Extra.arrows model.keyboardModel
        newX = model.x + x
        newY = model.y + y
        newClock = model.clock + dt
        -- We'll return newPoints and a newAnimation from a case statement based
        -- on whether or not we're presently running an animation, using
        -- destructuring and returning a 2-tuple from each branch
        (newPoints, newAnimation) =
          -- Static animations are always done, so if it's a static animation
          -- we'll just return the existing points and animation
          case (model.animation `equals` (static 0)) of
            True ->
              (model.points, model.animation)
            False ->
              -- Otherwise, when the animation's done we'll clear the points and
              -- switch to the static animation again
              case (isDone model.clock model.animation) of
                True -> ([], (static 0))
                -- If it's not done we won't change anything
                False -> (model.points, model.animation)

        newPoints' =
          case (x, y) of
            (0, 0) ->
              newPoints
            _ ->
              (newX, newY) :: newPoints
        -- We'll create an intermediate model that adds our new x and y points
        -- and updates the clock and animation
        model' =
          { model
          | points = newPoints'
          , clock = newClock
          , animation = newAnimation
          }
      in
        case (x, y) of
          (0, 0) ->
            model' ! []
          _ ->
            { model'
            | x = newX
            , y = newY
            } ! []

    Shake ->
      { model
      -- We'll update the animation to our shakeAnimation seeded with our
      -- current clock when the button is pressed.
      | animation = shakeAnimation model.clock
      } ! []


main : Program Never
main =
  App.program
    { init = init
    , update = update
    , view = view
    , subscriptions = subscriptions
    }


subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.batch
    [ Sub.map KeyboardExtraMsg Keyboard.Extra.subscriptions
    , AnimationFrame.diffs Tick
    ]