Animating a Canvas with Phoenix LiveView
Tags: elixir, phoenix, liveview
Published: 2019-09-13 18:02:20

---
title: Animating a Canvas with Phoenix LiveView
published: true
tags: Elixir,Phoenix,LiveView
canonical_url: http://www.petecorey.com/blog/2019/09/02/animating-a-canvas-with-phoenix-liveview/
---

[Phoenix LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html) recently released a new feature called “hooks” that introduces [Javascript interoperability into the LiveView lifecycle](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#module-js-interop-and-client-controlled-dom). Put simply, we can now run arbitrary Javascript every time a DOM node is changed by LiveView! **LiveView hooks are a complete game changer** , and open the doors to a whole new world of applications that can be built with this amazing technology.

As a proof of concept, let’s use LiveView hooks to animate an HTML5 canvas in real time using data provided by the server!

## Getting Set Up

To keep this article short(er), we’ll skip the rigmarole of configuring your application to use LiveView. If you need help with this step, I highly recommend you [check out Sophie DeBenedetto’s thorough walkthrough](https://elixirschool.com/blog/phoenix-live-view/). Be sure to cross reference with [the official documentation](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html), as things are moving quickly in the LiveView world.

Moving forward, let’s assume that you have a bare-bones LiveView component attached to a route that looks something like this:

```
defmodule LiveCanvasWeb.PageLive do
  use Phoenix.LiveView
  
  def render(assigns) do
    ~L"""
    <canvas>
      Canvas is not supported!
    </canvas>
    """
  end
  
  def mount(_session, socket) do
    {:ok, socket}
  end
end
```

We’ll also assume that your `assets/js/app.js` file is creating a LiveView connection:

```
import LiveSocket from "phoenix_live_view";

let liveSocket = new LiveSocket("/live");

liveSocket.connect();
```

Now that we’re on the same page, let’s get started!

## Generating Data to Animate

Before we start animating on the client, we should have some data to animate. We’ll start by storing a numeric value called `i` in our LiveView process’ `assigns`:

```
def mount(_session, socket) do
  {:ok, assign(socket, :i, 0)}
end
```

Next, we’ll increase `i` by instructing our LiveView process to send an `:update` message to itself after a delay of `16` milliseconds:

```
def mount(_session, socket) do
  Process.send_after(self(), :update, 16)
  {:ok, assign(socket, :i, 0)}
end
```

When we handle the `:udpate` message in our process, we’ll schedule another recursive call to `:update` and increment the value of `i` in our socket’s `assigns`:

```
def handle_info(:update, %{assigns: %{i: i}} = socket) do
  Process.send_after(self(), :update, 16)
  {:noreply, assign(socket, :i, i + 0.05)}
end
```

Our LiveView process now has an `i` value that’s slowly increasing by `0.05` approximately sixty times per second.

Now that we have some data to animate, let’s add a `canvas` to our LiveView’s template to hold our animation:

```
def render(assigns) do
  ~L"""
  <canvas data-i="<%= @i %>">
    Canvas is not supported!
  </canvas>
  """
end
```

Notice that we’re associating the value of `i` with our canvas by assigning it to a data attribute on the DOM element. Every time `i` changes in our process’ state, LiveView will update our `canvas` and set the value of `data-i` to the new value of `i`.

This is great, but to render an animation in our `canvas`, we need some way of executing client-side Javascript every time our `canvas` updates. Thankfully, LiveView’s new hook functionality lets us do exactly that!

## Hooking Into LiveView

LiveView hooks lets us execute Javascript [at various points in a DOM node’s lifecycle](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#module-js-interop-and-client-controlled-dom), such as when the node is first `mounted`, when it’s `updated` by LiveView, when it’s `destroyed` and removed from the DOM, and when it becomes `disconnected` or `reconnected` to our Phoenix server.

To hook into LiveView’s client-side lifecycle, we need to create a set of `hooks` and pass them into our `LiveSocket` constructor. Let’s create a hook that initializes our `canvas`’ rendering context when the element mounts, and renders a static circle every time the element updates:

```
let hooks = {
  canvas: {
    mounted() {
      let canvas = this.el;
      let context = canvas.getContext("2d");
      
      Object.assign(this, { canvas, context });
    },
    updated() {
      let { canvas, context } = this;
      
      let halfHeight = canvas.height / 2;
      let halfWidth = canvas.width / 2;
      let smallerHalf = Math.min(halfHeight, halfWidth);
      
      context.clearRect(0, 0, canvas.width, canvas.height);
      context.fillStyle = "rgba(128, 0, 255, 1)";
      context.beginPath();
      context.arc(
        halfWidth,
        halfHeight,
        smallerHalf / 16,
        0,
        2 * Math.PI
      );
      context.fill();
    }
  }
};

let liveSocket = new LiveSocket("/live", { hooks });

liveSocket.connect();
```

Notice that we’re storing a reference to our `canvas` and our newly created rendering `context` on `this`. When LiveView calls our lifecycle callbacks, [`this` points to an instance of a `ViewHook` class](https://github.com/phoenixframework/phoenix_live_view/blob/140cfb3f2924cb6595b9bb2979a1963ac0de6eb8/assets/js/phoenix_live_view.js#L904-L906). A `ViewHook` instance holds references to our provided lifecycle methods, a reference to the current DOM node in `el`, [and various other pieces of data related to the current set of hooks](https://github.com/phoenixframework/phoenix_live_view/blob/140cfb3f2924cb6595b9bb2979a1963ac0de6eb8/assets/js/phoenix_live_view.js#L1100-L1104). As long as we’re careful and we don’t overwrite these fields, we’re safe to store our own data in `this`.

Next, we need to instruct LiveView to attach this new set of `canvas` hooks to our `canvas` DOM element. We can do that with the `phx-hook` attribute:

```
<canvas
  data-i="<%= @i %>"
  phx-hook="canvas"
>
  Canvas is not supported!
</canvas>
```

When our page reloads, we should see our circle rendered gloriously in the center of our canvas.

## Resizing the Canvas

On some displays, our glorious circle may appear to be fuzzy or distorted. This can be fixed by scaling our canvas to match the pixel density of our display. While we’re at it, we might want to resize our canvas to fill the entire available window space.

 ![](http://www.petecorey.com/img/2019-09-02-animating-a-canvas-with-phoenix-liveview/blurry.png)

We can accomplish both of these in our `mounted` callback:

```
mounted() {
  let canvas = this.el;
  let context = canvas.getContext("2d");
  let ratio = getPixelRatio(context);
  
  resize(canvas, ratio);
  
  Object.assign(this, { canvas, context });
}
```

Where `getPixelRatio` is a helper function that determines the ratio of physical pixels in the current device’s screen to “CSS pixels” which are used within the rendering context of our `canvas`:

```
const getPixelRatio = context => {
  var backingStore =
    context.backingStorePixelRatio ||
    context.webkitBackingStorePixelRatio ||
    context.mozBackingStorePixelRatio ||
    context.msBackingStorePixelRatio ||
    context.oBackingStorePixelRatio ||
    context.backingStorePixelRatio ||
    1;
  
  return (window.devicePixelRatio || 1) / backingStore;
};
```

And `resize` is a helper function that modifies the `canvas`’ width and height attributes in order to resize our canvas to fit the current window, while fixing any pixel density issues we may be experiencing:

```
const resize = (canvas, ratio) => {
  canvas.width = window.innerWidth * ratio;
  canvas.height = window.innerHeight * ratio;
  canvas.style.width = `${window.innerWidth}px`;
  canvas.style.height = `${window.innerHeight}px`;
};
```

Unfortunately, our `canvas` doesn’t seem to be able to hold onto these changes. Subsequent calls to our `updated` callback seem to lose our `resize` changes, and the canvas reverts back to its original, blurry self. This is because when LiveView updates our `canvas` DOM node, it resets our `width` and `height` attributes. Not only does this revert our pixel density fix, it also forcefully clears the canvas’ rendering context.

LiveView has a quick fix for getting around this problem. By setting `phx-update` to `"ignore"` on our `canvas` element, we can instruct LiveView to leave our `canvas` element alone after its initial mount.

```
<canvas
  data-i="<%= @i %>"
  phx-hook="canvas" 
  phx-update="ignore"
>
  Canvas is not supported!
</canvas>
```

Now our circle should be rendered crisply in the center of our screen.

 ![](http://www.petecorey.com/img/2019-09-02-animating-a-canvas-with-phoenix-liveview/crisp.png)

## Animating Our Circle

We didn’t go all this way to render a static circle in our `canvas`. Let’s tie everything together and animate our circle based on the ever-changing values of `i` provided by the server!

The first thing we’ll need to do is update our `updated` callback to grab the current value of the `data-i` attribute:

```
let i = JSON.parse(canvas.dataset.i);
```

The value of `canvas.dataset.i` will reflect the contents of our `data-i` attribute. All data attributes are stored as strings, so a call to `JSON.parse` will convert a value of `"0.05"` to its numeric counterpart.

Next, we can update our rendering code to move our circle based on the value of `i`:

```
context.arc(
  halfWidth + (Math.cos(i) * smallerHalf) / 2,
  halfHeight + (Math.sin(i) * smallerHalf) / 2,
  smallerHalf / 16,
  0,
  2 * Math.PI
);
```

That’s it! With those two changes, our circle will rotate around the center of our canvas based entirely on real-time data provided by our server!

 ![](http://www.petecorey.com/img/2019-09-02-animating-a-canvas-with-phoenix-liveview/spinning.png)

## Requesting Animation Frames

Our solution works, but by forcing re-renders on the browser, we’re being bad net citizens. Our client may be forcing re-renders when its tab is out of focus, or it may be re-rendering more than sixty times per second, wasting CPU cycles.

Instead of telling the browser to re-render our `canvas` on every LiveView update, we should invert our control over rendering and [request an animation frame from the browser](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) on every update.

<form action="//east5th.us11.list-manage.com/subscribe/post?u=f8833fd40e2b1b28545d9f382&amp;id=c56ceba687" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" target="_blank">
            <p>I find myself producing lots of ranging content from books and articles, like the one you're reading now, to open source software projects. If you like what I'm doing, <strong>nothing shows your support more than signing up for <a href="#newsletter">my mailing list</a></strong>.</p>
            <div>
<input placeholder="Email address" type="email" value="" name="EMAIL" id="mce-EMAIL"><!--
                                                                                                                                                                                                    --><input type="submit" value="Sign me up!" name="subscribe" id="mc-embedded-subscribe">
            </div>
            <div style="position: absolute; left: -5000px;"><input type="text" name="b_f8833fd40e2b1b28545d9f382_c56ceba687" tabindex="-1" value=""></div>
        </form>

The process for this is straight forward. In our `updated` callback, we’ll wrap our rendering code in a lambda passed into `requestAnimationFrame`. We’ll save the resulting request reference to `this.animationFrameRequest`:

```
this.animationFrameRequest = requestAnimationFrame(() => {
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.beginPath();
  context.arc(
    halfWidth + (Math.cos(i) * smallerHalf) / 2,
    halfHeight + (Math.sin(i) * smallerHalf) / 2,
    smallerHalf / 16,
    0,
    2 * Math.PI
  );
  context.fill();
});
```

It’s conceivable that our LiveView component may update multiple times before our browser is ready to re-render our `canvas`. In those situations, we’ll need to cancel any previously requested animation frames, and re-request a new frame. We can do this by placing a guard just above our call to `requestAnimationFrame`:

```
if (this.animationFrameRequest) {
  cancelAnimationFrame(this.animationFrameRequest);
}
```

With those two changes, our LiveView hooks will now politely request animation frames from the browser, resulting in a smoother experience for everyone involved.

## Taking it Further

Using a canvas to animate a numeric value updated in real-time by a LiveView process running on the server demonstrates the huge potential power of LiveView hooks, but it’s not much to look at.

We can take things further by generating and animating a much larger set of data on the server. [Check out this example project](https://frozen-coast-73544.herokuapp.com/) that simulates over two hundred simple particles, and renders them on the client at approximately sixty frames per second:

 ![](http://www.petecorey.com/img/2019-09-02-animating-a-canvas-with-phoenix-liveview/particles.png)

Is it a good idea to take this approach if your goal is to animate a bunch of particles on the client? **Probably not.** Is it amazing that LiveView gives us the tools to do this? **Absolutely, yes!** Be sure to [check out the entire source for this example on Github](https://github.com/pcorey/live_canvas)!

Hooks have opened the doors to a world of new possibilities for LiveView-based applications. I hope this demonstration has given you a taste of those possibilities, and I hope you’re as eager as I am to explore what we can do with LiveView moving forward.