Skip to content

Instantly share code, notes, and snippets.

@zorbash
Created December 26, 2021 17:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save zorbash/9ad9ad70335427a655a170c718427370 to your computer and use it in GitHub Desktop.
Save zorbash/9ad9ad70335427a655a170c718427370 to your computer and use it in GitHub Desktop.

Animating Outputs with Kino.animate

The Grid

Mix.install([:kino])

This is an experimentation notebook to try out Kino.animate/3

See livebook-dev/kino#49

The following Life implementation is adapted from this gist.

defmodule Life.Grid do
  defstruct data: nil

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

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

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

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

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

  defp list_to_data(data) do
    data
    |> Enum.map(&List.to_tuple/1)
    |> List.to_tuple()
  end

  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
    end
  end

  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
      1
    end
    |> Enum.sum()
  end
end

Below there are a few interesting patterns.

queen_bee = [
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
  [1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
  [0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
]

pattern = queen_bee

Now, we'll define a module which turns the grid into an SVG string so that we can visualise it.

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"
      end

    """
    <svg viewBox="0 0 #{@cell_size * size} #{@cell_size * size}" xmlns="http://www.w3.org/2000/svg">
      #{cells}
    </svg>
    """
    |> Kino.Image.new(:svg)
  end
end

We'll define a function which returns a randomised 2-dimensional array which will be our grid.

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

We'll display a button, which when pressed it'll generate and preview a few randomised initial configurations for Life.

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

button
widget = Kino.Frame.new() |> Kino.render()

loop = fn f ->
  receive do
    {:randomize, _} ->
      Kino.Frame.render(widget, Life.Svg.render(Life.Grid.new(randomize.(22))))
      f.(f)

    _ ->
      :ok
  end
end

loop.(loop)

We can finally animate Life!

Kino.animate(100, Life.Grid.new(pattern), fn grid ->
  {:cont, Life.Svg.render(grid), Life.Grid.next(grid)}
end)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment