Amazing animations using the Reader monad
Tags: javascript, webdev, functional
Published: 2020-12-16 19:47:56

---
title: Amazing animations using the Reader monad
published: true
description: A holiday-themed front-end tutorial
tags:
  - javascript
  - webdev
  - functional
cover_image: https://images.unsplash.com/photo-1529973625058-a665431328fb
---

Ho ho ho! :snowman: :santa:

_It's the Most Wonderful Time of the Year_, and to celebrate, I created a small web-arrangement of [Silent Night](https://bit.ly/silent-night-kdv) using PureScript and LilyPond. It can be also be found [here in developer mode](https://klank.dev/?ec&url=https://klank-share.s3.eu-west-1.amazonaws.com/K16079345286771329.purs&klank=https://klank-share.s3.eu-west-1.amazonaws.com/klank16079345297908559.js) . The work uses twenty-four different recordings my wife and I made of Silent Night, blending them together in different combinations and proposing different digital accompaniments depending on your interactions with the site.

In this article, I'd like to show a small example of what I found to be an efficient pattern for making interactive work on [klank.dev](https://klank.dev). The full example will be around [300 lines of PureScript](https://gist.github.com/mikesol/d544cae45b7f22770a00596a002708f2) and will create a small bell symphony where you can click on circles before they disappear. We'll go over three main parts:

- How to update the model using a reader.
- How to write the animation.
- How to write the sound component.

The end result is live on [klank.dev](https://klank.dev/?noterm&nostop&url=https://klank-share.s3.amazonaws.com/K16081476589775373.purs&klank=https://klank-share.s3.amazonaws.com/klank16081476627013691.js) and in developer mode [here](https://klank.dev/?url=https://klank-share.s3.amazonaws.com/K16081476589775373.purs&klank=https://klank-share.s3.amazonaws.com/klank16081476627013691.js).

I hope that, by the end of the article, you'll have enough information to compare [PureScript Drawing](https://github.com/paf31/purescript-drawing) and [PureScript Audio Behaviors](https://github.com/mikesol/purescript-audio-behaviors) to libraries like [EaselJS](https://createjs.com/easeljs) [ToneJS](https://tonejs.github.io/).

# Working with a model

First, let's create a model that keeps track of currently-active visual and audio objects, writing information that will be important for rendering later on.

In imperative languages, two interrelated problems often arise when a model is updated:

- The model's previous state needs to be accessed.
- New information needs to percolate through the model.

Both of these problems can be solved by the `Reader` monad. The reader monad persists a read-only data structure through a computation, allowing arbitrary elements of the computation to access the data.

In the example below, we see how a reader monad allows us to access the current time, the canvas's width and height, information about the mouse and the previous state. Because the data is read-only, there's no danger that we'll change it accidentally. Furthermore, because the data is available through all the functions, there's no need for monster function signatures. We'll use the predefined commands `ask`, which returns the whole read-only environment,and `asks`, which applies a function to the environment before returning it.

In the definitions of `advance`, `accountForClick`, `treatCircle` and `makeCircles`, look at how `ask` and `asks` retrieve only the information we need. Another thing you may notice is that the resulting code looks more declarative. In a way, it resembles a data structure more than code. This is, in my opinion, a good thing. Instead of giving the browser a series of instructions telling it _how_ to do something, we tell PureScript _what_ we want and let lower-level libraries figure out the details.

```haskell
type CircleInfo
  = { direction :: Direction
    , generation :: Int
    , startPos :: Point
    , currentPos :: Point
    , radius :: Number
    , startOpacity :: Number
    , currentOpacity :: Number
    , startTime :: Number
    }

type UpdateEnv
  = { time :: Number
    , mouseDown :: Maybe Point
    , w :: Number
    , h :: Number
    , circs :: List CircleInfo
    }

type UpdateR
  = Reader UpdateEnv

advance :: CircleInfo -> UpdateR CircleInfo
advance circle@{ direction
, generation
, startPos
, currentPos
, startOpacity
, startTime
} = do
  { time, w, h } <- ask
  pure
    $ circle
        { currentPos =
          if generation == 0 then
            currentPos
          else
            { x:
                startPos.x
                  + ((time - startTime) * w * 0.1)
                  * (toNumber (generation + 1))
                  * dirToNumber direction Xc
            , y:
                startPos.y
                  + ((time - startTime) * h * 0.1)
                  * (toNumber (generation + 1))
                  * dirToNumber direction Yc
            }
        , currentOpacity =
          if generation == 0 then
            1.0
          else
            calcSlope startTime
              startOpacity
              (startTime + timeAlive)
              0.0
              time
        }

accountForClick :: CircleInfo -> UpdateR (List CircleInfo)
accountForClick circle = do
  { mouseDown } <- ask
  case mouseDown of
    Nothing -> pure mempty
    Just { x, y }
      | inRadius { x, y } circle -> do
        { time } <- ask
        pure
          $ map
              ( circle
                  { direction = _
                  , generation = circle.generation + 1
                  , startPos = circle.currentPos
                  , startOpacity = circle.currentOpacity * 0.8
                  , radius = circle.radius * 0.8
                  , startTime = time
                  }
              )
              directions
      | otherwise -> pure mempty

treatCircle ::
  CircleInfo ->
  UpdateR (List CircleInfo)
treatCircle circle = do
  { time } <- ask
  if circle.generation /= 0
    && timeAlive
    + circle.startTime
    <= time then
    pure mempty
  else
    append
      <$> (pure <$> advance circle)
      <*> (accountForClick circle)

makeCircles :: UpdateR (List CircleInfo)
makeCircles =
  asks _.circs
    >>= map join
    <<< sequence
    <<< map treatCircle
```

# Creating the visuals

Now that we have an updated list of `CircleInfo`, we can use it to create both visuals. Because the model has already been calculated, the actual drawing is quite short.

```haskell
background :: Number -> Number -> Drawing
background w h =
  filled
    (fillColor $ rgba 0 0 0 1.0)
    (rectangle 0.0 0.0 w h)

circlesToDrawing ::
  Number ->
  Number ->
  List CircleInfo ->
  Drawing
circlesToDrawing w h =
  append (background w h)
    <<< fold
    <<< map go
  where
  go { currentPos: { x, y }
  , currentOpacity
  , radius
  } =
    filled
      (fillColor $ rgba 255 255 255 currentOpacity)
      (circle x y radius)
```

# Creating the audio

Similar to the drawings, the audio is derived completely from the model and is also quite short.

```haskell
toNel :: forall a. Semiring a => List a -> NonEmpty List a
toNel Nil = zero :| Nil

toNel (a : b) = a :| b

directionToPitchOffset :: Direction -> Number
directionToPitchOffset NorthEast = 0.0

directionToPitchOffset NorthWest = 0.25

directionToPitchOffset SouthEast = 0.5

directionToPitchOffset SouthWest = 0.75

circlesToSounds ::
  Number ->
  List CircleInfo ->
  NonEmpty List (AudioUnit D2)
circlesToSounds time = toNel <<< catMaybes <<< map go
  where
  go { startTime, startPos, direction, generation }
    | generation == 0 = Nothing
    | otherwise =
      Just
        $ playBuf_
            ( show startTime
                <> show startPos
                <> show direction
                <> show generation
            )
            "ring" -- the name of the soundfile we'll play
            ( toNumber generation
                + directionToPitchOffset direction
            )
```

# Conclusion

This entire demo clocks in at around 300 lines of code and can be found on [GitHub](https://github.com/mikesol/dev-to-xmas-article) as well as on [klank.dev](https://klank.dev/?noterm&nostop&url=https://klank-share.s3.amazonaws.com/K16081476589775373.purs&klank=https://klank-share.s3.amazonaws.com/klank16081476627013691.js).

The larger piece, [Silent Night](https://bit.ly/silent-night-kdv), uses the same exact patterns on a larger scale. Because individual sections of Silent Night are no more complicated than this smaller example, and because the sections are gated by pattern matching, the execution time is also quite fast and there is no noticeable jank.

I hope that you enjoy playing around with both the shorter example and the larger piece. I find PureScript to be incredibly expressive for making creative work, and I would love to see it gain greater traction amongst visual and sound artists. If you have time over the holidays, try to make your first creation on klank.dev and share it - I'd love to see it!