DEV Community

Cover image for Livebook Animations
Dimitris Zorbas
Dimitris Zorbas

Posted on • Updated on • Originally published at

Livebook Animations

An exciting new feature landed in Livebook (through Kino) which gives
the ability to animate any output.

In the process of experimenting with Brain and its camera, I needed to
quickly sketch out some code and output video in a Livebook notebook.

I thought the following would do the trick:, :jpeg)
|> Kino.render
Enter fullscreen mode Exit fullscreen mode

but it creates a new output cell every time Kino.render/1 is called.

So I posted this issue (kino#48) and implemented a new
widget Kino.ImageDynamic which can be updated with

Then I also implemented a Kino.clear/0
function to dynamically clear any output cell, so that its contents can
be replaced by calling render again.

Thankfully the fruits of this conversation on the issue gave us a more
robust API for animation.


This PR kino#49 and version 0.3.1 of Kino
bring Kino.Frame and the Kino.animate/3 function.

Watch a showcase of the feature below:


With you can start a new
widget which can be updated with Kino.Frame.render/2.

widget = |> tap(&Kino.render/1)

Kino.Frame.render(widget, 1)
Kino.Frame.render(widget, 2)
Kino.Frame.render(widget, 3)
Kino.Frame.render(widget, 4)
Enter fullscreen mode Exit fullscreen mode

animate cells

You'll notice that with the code above we only get a single output
cell which gets updated four times.

With Kino.animate/3 the above can be expressed more concisely:

Kino.animate(50, 1, fn
  i when i in 1..4 -> {:cont, i, i + 1}
  _ -> :halt
Enter fullscreen mode Exit fullscreen mode


Let's put this new API to the test by implementing Life.
To try this on your Livebook instance by importing this notebook.

The implemenation is based on this gist.

defmodule Life.Grid do
  defstruct data: nil

  def new(data) when is_list(data) do
    %Life.Grid{data: list_to_data(data)}

  def size(%Life.Grid{data: data}), do: tuple_size(data)

  def cell_status(grid, x, y) do
    |> elem(y)
    |> elem(x)

  def next(grid) do
    %Life.Grid{grid | data: new_data(size(grid), &next_cell_status(grid, &1, &2))}

  defp new_data(size, fun) do
    for y <- 0..(size - 1) do
      for x <- 0..(size - 1) do
        fun.(x, y)
    |> list_to_data

  defp list_to_data(data) do
    |> List.to_tuple()

  def next_cell_status(grid, x, y) do
    case {cell_status(grid, x, y), alive_neighbours(grid, x, y)} do
      {1, 2} -> 1
      {1, 3} -> 1
      {0, 3} -> 1
      {_, _} -> 0

  defp alive_neighbours(grid, cell_x, cell_y) do
    for x <- (cell_x - 1)..(cell_x + 1),
        y <- (cell_y - 1)..(cell_y + 1),
        x in 0..(size(grid) - 1) and
          y in 0..(size(grid) - 1) and
          (x != cell_x or y != cell_y) and
          cell_status(grid, x, y) == 1 do
    |> Enum.sum()
Enter fullscreen mode Exit fullscreen mode

Then we need a function which returns an SVG string to visualise the

defmodule Life.Svg do
  @cell_size 10

  def render(grid) do
    size = Life.Grid.size(grid)

    cells =
      for y <- 0..(size - 1), x <- 0..(size - 1), into: "" do
        status = Life.Grid.cell_status(grid, x, y)
        fill = if status == 0, do: "#EEE", else: "purple"

        "<rect x=\"#{x * @cell_size}\" y=\"#{y * @cell_size}\" width=\"10\" height=\"10\" fill=\"#{fill}\" />\n"

    <svg viewBox="0 0 #{@cell_size * size} #{@cell_size * size}" xmlns="">
Enter fullscreen mode Exit fullscreen mode

Now we'll add a function to generate random starting configurations.

randomize = fn size ->
  for _ <- 1..size, do: 1..size, fn _ -> Enum.random([0,1]) end
Enter fullscreen mode Exit fullscreen mode

Next, we'll add a button to generate a few configurations and preview them.

button = Kino.Control.button("randomize")
Kino.Control.subscribe(button, :randomize)

Enter fullscreen mode Exit fullscreen mode

When a button is pressed it sends events as messages. We handle them by rendering an SVG.

widget = |> Kino.render()
loop = fn f ->
  receive do
    {:randomize, _} ->
      # Preview the configuration when the button is pressed
      Kino.Frame.render(widget, Life.Svg.render(
    _ -> :ok

Enter fullscreen mode Exit fullscreen mode

Buttons are a new addition to Kino and Livebook released in version 0.4.0.
You can find their docs here.

Having made sure that we can correctly render a grid, we can finally animate it.

Kino.animate(100,, fn grid ->
  {:cont, Life.Svg.render(grid),}
Enter fullscreen mode Exit fullscreen mode

Thanks for reading this post, hope you'll find it useful and make
your notebooks pop with captivating animations.

Top comments (0)