Matrix Rain

Code

Matrix module · lib/phx_demo/examples/matrix.ex
defmodule PhxDemo.Examples.Matrix do
  @width 800
  @height 600
  @font_size 14
  @cols div(@width, @font_size)
  @chars ~c"abcdefghijklmnopqrstuvwxyz0123456789@#$%^&*(){}[]|;:<>?アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン"

  def init do
    columns =
      for col <- 0..(@cols - 1), into: %{} do
        drops = for _ <- 1..Enum.random(1..3), do: new_drop(col)
        {col, drops}
      end

    %{columns: columns, tick: 0}
  end

  def tick(%{columns: columns, tick: tick} = state) do
    max_rows = div(@height, @font_size)

    columns =
      Map.new(columns, fn {col, drops} ->
        drops = Enum.map(drops, &%{&1 | row: &1.row + &1.speed})

        drops =
          Enum.map(drops, fn drop ->
            if drop.row - drop.length > max_rows, do: new_drop(col), else: drop
          end)

        drops =
          if :rand.uniform() < 0.3 do
            Enum.map(drops, fn drop ->
              idx = :rand.uniform(length(drop.chars)) - 1
              %{drop | chars: List.replace_at(drop.chars, idx, random_char())}
            end)
          else
            drops
          end

        {col, drops}
      end)

    %{state | columns: columns, tick: tick + 1}
  end

  def render(%{columns: columns}) do
    max_rows = div(@height, @font_size)

    canvas =
      Easel.new(@width, @height)
      |> Easel.set_fill_style("rgba(0, 0, 0, 0.85)")
      |> Easel.fill_rect(0, 0, @width, @height)
      |> Easel.set_font("#{@font_size}px monospace")
      |> Easel.set_text_baseline("top")
      |> Easel.set_text_align("center")

    Enum.reduce(columns, canvas, fn {col, drops}, c0 ->
      x = col * @font_size + @font_size / 2

      Enum.reduce(drops, c0, fn drop, c1 ->
        head_row = trunc(drop.row)

        Enum.reduce(0..(drop.length - 1), c1, fn i, c2 ->
          row = head_row - i

          if row >= 0 and row < max_rows do
            y = row * @font_size
            char_idx = rem(abs(row), length(drop.chars))
            char = Enum.at(drop.chars, char_idx)

            {color, alpha} =
              if i == 0 do
                {"#ffffff", 1.0}
              else
                brightness = 1.0 - i / drop.length
                g = round(255 * brightness)
                {"rgb(0, #{g}, 0)", max(0.1, brightness)}
              end

            c2
            |> Easel.save()
            |> Easel.set_global_alpha(alpha)
            |> Easel.set_fill_style(color)
            |> Easel.fill_text(char, x, y)
            |> Easel.restore()
          else
            c2
          end
        end)
      end)
    end)
    |> Easel.render()
  end

  defp new_drop(_col) do
    %{
      row: -Enum.random(0..30),
      speed: Enum.random(1..3) / 1.0,
      length: Enum.random(8..25),
      chars: for(_ <- 1..40, do: random_char())
    }
  end

  defp random_char do
    idx = :rand.uniform(length(@chars)) - 1
    @chars |> Enum.at(idx) |> List.wrap() |> List.to_string()
  end
end
Matrix LiveView · lib/phx_demo_web/live/matrix_live.ex
defmodule PhxDemoWeb.MatrixLive do
  use PhxDemoWeb, :live_view

  @width 800
  @height 600

  def mount(_params, _session, socket) do
    state = PhxDemo.Examples.matrix_init()
    canvas = PhxDemo.Examples.matrix_render(state)

    background =
      Easel.new(@width, @height)
      |> Easel.set_fill_style("#000000")
      |> Easel.fill_rect(0, 0, @width, @height)
      |> Easel.render()

    socket =
      socket
      |> assign(:state, state)
      |> assign(:canvas, canvas)
      |> assign(:background, background)
      |> Easel.LiveView.animate(
        "fg",
        :state,
        fn state ->
          new_state = PhxDemo.Examples.matrix_tick(state)
          canvas = PhxDemo.Examples.matrix_render(new_state)
          {canvas, new_state}
        end,
        interval: 50,
        canvas_assign: :canvas
      )

    {:ok, socket}
  end

  def handle_info({:easel_tick, id}, socket) do
    {:noreply, Easel.LiveView.tick(socket, id)}
  end

  def render(assigns) do
    ~H"""
    <.demo title="Matrix Rain" code_id="matrix">
      <Easel.LiveView.canvas_stack id="matrix" width={@background.width} height={@background.height}>
        <:layer id="bg" ops={@background.ops} />
        <:layer id="fg" ops={@canvas.ops} />
      </Easel.LiveView.canvas_stack>
    </.demo>
    """
  end
end