2120 live cells · running
defmodule PhxDemo.Examples.Life do
@cols 120
@rows 80
@cell 8
def width, do: @cols * @cell
def height, do: @rows * @cell
def cell, do: @cell
def init(density \\ 0.22) do
alive =
for y <- 0..(@rows - 1),
x <- 0..(@cols - 1),
:rand.uniform() < density,
into: MapSet.new() do
{x, y}
end
%{alive: alive}
end
def tick(%{alive: alive} = state) do
counts =
Enum.reduce(alive, %{}, fn {x, y}, acc ->
Enum.reduce(-1..1, acc, fn dx, acc2 ->
Enum.reduce(-1..1, acc2, fn dy, acc3 ->
if dx == 0 and dy == 0 do
acc3
else
nx = x + dx
ny = y + dy
if nx >= 0 and nx < @cols and ny >= 0 and ny < @rows do
Map.update(acc3, {nx, ny}, 1, &(&1 + 1))
else
acc3
end
end
end)
end)
end)
next_alive =
Enum.reduce(counts, MapSet.new(), fn {cell, n}, acc ->
alive_now = MapSet.member?(alive, cell)
if (alive_now and (n == 2 or n == 3)) or (!alive_now and n == 3) do
MapSet.put(acc, cell)
else
acc
end
end)
%{state | alive: next_alive}
end
def toggle(%{alive: alive} = state, x, y) do
if x >= 0 and x < @cols and y >= 0 and y < @rows do
alive =
if MapSet.member?(alive, {x, y}),
do: MapSet.delete(alive, {x, y}),
else: MapSet.put(alive, {x, y})
%{state | alive: alive}
else
state
end
end
def render_background do
Easel.new(width(), height())
|> Easel.set_fill_style("#0b1020")
|> Easel.fill_rect(0, 0, width(), height())
|> Easel.render()
end
def render(%{alive: alive}) do
instances = Enum.map(alive, fn {x, y} -> %{x: x * @cell, y: y * @cell} end)
Easel.new(width(), height())
|> Easel.template(:cell, fn c ->
c
|> Easel.set_fill_style("#7dd3fc")
|> Easel.fill_rect(0, 0, @cell - 1, @cell - 1)
end)
|> Easel.instances(:cell, instances)
|> Easel.render()
end
end
defmodule PhxDemoWeb.LifeLive do
use PhxDemoWeb, :live_view
@interval 80
def mount(_params, _session, socket) do
life = PhxDemo.Examples.life_init() |> Map.put(:running, true)
socket =
socket
|> assign(:life, life)
|> assign(:canvas, PhxDemo.Examples.life_render(life))
|> assign(:background, PhxDemo.Examples.life_render_background())
|> assign(:cell, PhxDemo.Examples.life_cell())
|> Easel.LiveView.animate(
"fg",
:life,
fn life ->
next = if life.running, do: PhxDemo.Examples.life_tick(life), else: life
{PhxDemo.Examples.life_render(next), next}
end,
interval: @interval,
canvas_assign: :canvas
)
{:ok, socket}
end
def handle_info({:easel_tick, id}, socket) do
{:noreply, Easel.LiveView.tick(socket, id)}
end
def handle_event("fg:click", %{"x" => x, "y" => y}, socket) do
cx = div(round(x), socket.assigns.cell)
cy = div(round(y), socket.assigns.cell)
life = PhxDemo.Examples.life_toggle(socket.assigns.life, cx, cy)
{:noreply,
socket
|> assign(:life, life)
|> assign(:canvas, PhxDemo.Examples.life_render(life))}
end
def handle_event("toggle", _, socket) do
{:noreply, update(socket, :life, &Map.update!(&1, :running, fn r -> !r end))}
end
def handle_event("randomize", _, socket) do
running = socket.assigns.life.running
life = PhxDemo.Examples.life_init() |> Map.put(:running, running)
{:noreply,
socket |> assign(:life, life) |> assign(:canvas, PhxDemo.Examples.life_render(life))}
end
def handle_event("clear", _, socket) do
life = %{socket.assigns.life | alive: MapSet.new()}
{:noreply,
socket |> assign(:life, life) |> assign(:canvas, PhxDemo.Examples.life_render(life))}
end
def render(assigns) do
~H"""
<.demo title="Game of Life" code_id="life">
<div class="flex gap-2 mb-3">
<button phx-click="toggle" class="px-3 py-1 border rounded text-sm">
{if @life.running, do: "Pause", else: "Run"}
</button>
<button phx-click="randomize" class="px-3 py-1 border rounded text-sm">Randomize</button>
<button phx-click="clear" class="px-3 py-1 border rounded text-sm">Clear</button>
</div>
<Easel.LiveView.canvas_stack id="life" width={@background.width} height={@background.height}>
<:layer id="bg" ops={@background.ops} />
<:layer id="fg" ops={@canvas.ops} templates={@canvas.templates} on_click />
</Easel.LiveView.canvas_stack>
<p class="text-sm text-gray-500 mt-2">
{MapSet.size(@life.alive)} live cells · {if @life.running, do: "running", else: "paused"}
</p>
</.demo>
"""
end
end