Home Posts Post Search Tag Search

LiveView 31 - Chapter 12: Render Graphics With SVG
Published on: 2026-02-19 Tags: elixir, Blog, Side Project, LiveView, Game Site, Phoenix, SVG, Graphics

Chapter 12: Render Graphics With SVG

SVG will be the core part of how we take text and turn it into images.

Plan the Presentation

We will take a simple version of this code and turn it into smaller parts. We will have a component for each part of the code. We will have a board and pallette components that will be responsible for the board (canvas) and the pentominoes that the player can use respectively.


With that said we also need to be able to render some version of phx-click so that the user can interact with the board and place pieces.


One of the smallest layers will be a point that will be colored squares that will have an {x, y} as well as a width and a color.


Let’s start out with the liveview and go from there.

Define a Skinny GameLive View

# router
live("/game/:puzzle", GameLive)

We now have the route for the new LiveView, let’s now create the file and the module for that new LiveView, pento/lib/pento_web/live/game_live/game_live.ex

defmodule PentoWeb.GameLive do
  use PentoWeb, :live_view
  def mount(_params, _session, socket), do: {:ok, socket}

  def render(assigns) do
    ~H"""
    <section class="mx-auto max-w-4xl px-4 py-8">
    <h1 class="font-heavy text-3xl mb-6">Welcome to Pento!</h1>
    </section>
    """
  end
end

You could now start up the server and check it out.

mix phx.server

Render Points with SVG

We will use SVG (Scalable Vector Graphics) for the .point/1 functions component. This will allow us to not have to define each pixel but to use text to specify the location, size, color, etc of the points.

Build the SVG Point

Add this after the section block in you code.

    <svg viewBox="0 0 100 100">
      <rect x="0" y="0" width="10" height="10" />
    </svg>

This will make a box of size 100x100 and the first 2 are the min size of the box. This is just the viewport of the SVG


The next self closing <rect> is there to define a box of size 10x10 at the location {0, 0}, you might be able to see how this is going to work for the future.

Dynamically Draw Points with SVG <defs>

Lets replace some of the code with the following block

    <svg viewBox="0 0 100 100">
      <defs>
        <rect id="point" width="10" height="10" />
      </defs>
      <use xlink:href="#point" x="0" y="0" fill="blue" />
      <use xlink:href="#point" x="10" y="0" fill="green" />
      <use xlink:href="#point" x="0" y="10" fill="red" />
      <use xlink:href="#point" x="10" y="10" fill="black" />
    </svg>

Look at what we did here, we defined a rect point that we can then add some other values to and then render them in the view-port.

Prepare a Module For Components

Now we want to build some components that we can use to put any number of points in the view-port. Create this file /pento/lib/pento_web/live/game_live/component.ex

defmodule PentoWeb.GameLive.Component do
  use Phoenix.Component
  alias Pento.Game.Pentomino
  import PentoWeb.GameLive.Colors
  @width 10
end

Build the Point Component

defmodule PentoWeb.GameLive.Component do
  use Phoenix.Component
  alias Pento.Game.Pentomino
  import PentoWeb.GameLive.Colors
  @width 10

  attr(:x, :integer, required: true)
  attr(:y, :integer, required: true)
  attr(:fill, :string)
  attr(:name, :string)
  attr(:"phx-click", :string)
  attr(:"phx-value", :string)
  attr(:"phx-target", :any)

  def point(assigns) do
    ~H"""
    <use
      xlink:href="#pento-point"
      x={convert(@x)}
      y={convert(@y)}
      fill={@fill}
      phx-click="pick"
      phx-value-name={@name}
      phx-target="#board-component"
    />
    """
  end

  defp convert(i) do
    (i - 1) * @width + 2 * @width
  end
end

Important things to know about this is that we have the convert/1 in order to make sure that we center the pentomino within the center of the 10x10 rect. Also that we have a value for the fill(color), and then we can set up some click and target events.

Render a Canvas With a Slot

Okay so now that we have the point we can now define the .canvas/1 that will hold all the points. This will have a few responsibilities:

• Define an SVG viewport with a top-level <svg> element.
• Define the reuseable <rect> shape with a <defs> element.
• Provide a component slot we can use to render custom content with the
correct set of points based on the state of our game.

  def canvas(assigns) do
    ~H"""
    <svg viewBox={@view_box}>
      <defs>
        <rect id="pento-point" width="10" height="10" />
      </defs>
      {render_slot(@inner_block)}
    </svg>
    """
  end

What we see here is the view_box just being passed through and then we are going to expect something within the <canvas> tags when we place this within the render. Also notice the def that we are creating here so that we can use the def for everything that is within the <canvas> tag. Okay so now lets test it out.

  def render(assigns) do
    ~H"""
    <section class="mx-auto max-w-4xl px-4 py-8">
      <h1 class="font-heavy text-3xl mb-6">Welcome to Pento!</h1>
        <.canvas view_box="0 0 200 30">
          <.point x={0} y={0} fill="blue" name="a" />
          <.point x={1} y={0} fill="green" name="b" />
          <.point x={0} y={1} fill="red" name="c" />
          <.point x={1} y={1} fill="black" name="d" />
        </.canvas>
    </section>
    """
  end

Okay so we are all set to start to build a board if we wanted we would just need to send the right values for color and the {x, y} but we can do better.

Compose WIth Components

For the next bit we will use the .canvas/1 and the .point/1 to set up the shapes and the board. That is because we can define any sized canvas and then put a point within that canvas.

Render Shapes With Multiple Points

Let’s build a shape.

  attr(:points, :list, required: true)
  attr(:name, :string, required: true)
  attr(:fill, :string, required: true)

  def shape(assigns) do
    ~H"""
    <%= for {x, y} <- @points do %>
      <.point x={x} y={y} fill={@fill} name={@name} />
    <% end %>
    """
  end

See how that allowed us to build off the .point/1 in order to build a set of points and then fill them. Let’s test it out with a new render.

  def render(assigns) do
    ~H"""
    <section class="mx-auto max-w-4xl px-4 py-8">
      <h1 class="font-heavy text-3xl mb-6">Welcome to Pento!</h1>
      <.canvas view_box="0 0 220 70">
        <.shape
          points={ [{3, 2}, {4, 3}, {3, 3}, {4, 2}, {3, 4}] }
          fill="orange"
          name="p"
        />
      </.canvas>
    </section>
    """
  end

Now that is cool right. Using layered components to render a P in the view-port. While we are at it let’s add a Pento to the root-html.heex.

<a href="/">
    <svg viewBox="0 0 55 55" class="h-6">
    <rect x="0" y="0" width="55" height="55" fill="#DDD" />
    <rect x="10" y="5" width="30" height="30" fill="#689042"></rect>
    <rect x="10" y="35" width="15" height="15" fill="#689042"></rect>
    </svg>
</a>
<p class="px-2 text-[0.8125rem] font-heavy leading-6">
    Pentominos
</p>

Build a Pallette from Shapes

Now we want to build a Pallette for all the shapes that we can have within a board size. Let’s build a component for the pallette.

  attr(:shape_names, :list, required: true)
  attr(:completed_shape_names, :list, default: [])

  def palette(assigns) do
    ~H"""
    <div id="pento-palette">
      <svg viewBox="0 0 500 125">
        <defs>
          <rect id="palette-point" width="10" height="10" />
        </defs>
        <%= for shape <- palette_shapes(@shape_names) do %>
          <.palette_shape
            points={shape.points}
            fill={color(shape.color, false, shape.name in @completed_shape_names)}
            name={shape.name}
          />
        <% end %>
      </svg>
    </div>
    """
  end

  attr(:points, :list, required: true)
  attr(:name, :string, required: true)
  attr(:fill, :string, required: true)

  def palette_shape(assigns) do
    ~H"""
    <%= for {x, y} <- @points do %>
      <.palette_point x={x} y={y} fill={@fill} name={@name} />
    <% end %>
    """
  end

  attr(:x, :integer, required: true)
  attr(:y, :integer, required: true)
  attr(:fill, :string, required: true)
  attr(:name, :string, required: true)

  def palette_point(assigns) do
    ~H"""
    <use
      xlink:href="#palette-point"
      x={convert(@x)}
      y={convert(@y)}
      fill={@fill}
      phx-click="pick"
      phx-value-name={@name}
      phx-target="#board-component"
    />
    """
  end

  defp palette_shapes(names) do
    names
    |> Enum.with_index()
    |> Enum.map(&place_pento/1)
  end

  defp place_pento({name, i}) do
    Pentomino.new(name: name, location: location(i))
    |> Pentomino.to_shape()
  end

  defp location(i) do
    x = rem(i, 6) * 4 + 3
    y = div(i, 6) * 5 + 3
    {x, y}
  end

There is a lot to deal with here but in the end we are building a pallette for all the shapes what we are getting when we call the .pallette/1 component. This requires a few levels of components. .pallette_point/1 is very simillar to the .point/1 that we had before but will have a different name to be called later. It will be responsible for taking the location, fill(color), and the name of the shape and rendering it within a box.


That .pallet_point/1 will be called by .pallet_shape/1 and that will need to have all the points for the shape as well as the fill(color) and the name.


That .pallet_shape/1 will be called by .pallet/1 and will need to be fed the shape names and will be responsible for gather all the information about the shapes that we will need.


The helper functions will help us get all the shapes and then place them on the board(give us the {x, y} for the view-port).

Add Helpers for Showing Colors

Okay so now we need to add an other module that will help us take the :atoms for color and turn them into something that the svg can use. Create pento/lib/pento_web/live/game_live/colors.ex

defmodule PentoWeb.GameLive.Colors do
  def color(c), do: color(c, false, false)
  def color(_color, true, _completed), do: "#B86EF0"
  def color(_color, _active, true), do: "#000000"
  def color(:green, _active, _completed), do: "#8BBF57"
  def color(:dark_green, _active, _completed), do: "#689042"
  def color(:light_green, _active, _completed), do: "#C1D6AC"
  def color(:orange, _active, _completed), do: "#B97328"
  def color(:dark_orange, _active, _completed), do: "#8D571E"
  def color(:light_orange, _active, _completed), do: "#F4CCA1"
  def color(:gray, _active, _completed), do: "#848386"
  def color(:dark_gray, _active, _completed), do: "#5A595A"
  def color(:light_gray, _active, _completed), do: "#B1B1B1"
  def color(:blue, _active, _completed), do: "#83C7CE"
  def color(:dark_blue, _active, _completed), do: "#63969B"
  def color(:light_blue, _active, _completed), do: "#B9D7DA"
  def color(:purple, _active, _completed), do: "#240054"
end

Now we can add that to the render.

defmodule PentoWeb.GameLive do
  use PentoWeb, :live_view
  import PentoWeb.GameLive.Component

  def mount(_params, _session, socket), do: {:ok, socket}

  def render(assigns) do
    ~H"""
    <section class="mx-auto max-w-4xl px-4 py-8">
      <h1 class="font-heavy text-3xl mb-6">Welcome to Pento!</h1>
      <.palette shape_names={[:i, :l, :y, :n, :p, :w, :u, :v, :s, :f, :x, :t]} />
    </section>
    """
  end
end

Okay so now we have all the shapes on the pallet, but its still being done with raw values passed to the shape_names. We can do better with the mount and even build the board.

Put it all Together

Let’s first add in the param of the current site we are on with the params, that is being passed thanks to the url.

Render the Board Component

  alias PentoWeb.GameLive.Board

  def mount(%{"puzzle" => puzzle}, _session, socket) do
    {:ok, assign(socket, puzzle: puzzle)}
  end

  def render(assigns) do
    ~H"""
    <section class="mx-auto max-w-4xl px-4 py-8">
      <h1 class="font-heavy text-3xl mb-6">Welcome to Pento!</h1>
      <.live_component module={Board} puzzle={@puzzle} id="board-component" />
    </section>
    """
  end

We will use this later but for now let’s set up the new Board live_component/1, we want this to be a live component because we need it to render as we make changes.

Define the Board Component

Create a new file under the same folder as the rest of the components: pento/lib/pento_web/live/game_live/board.ex

defmodule PentoWeb.GameLive.Board do
  use PentoWeb, :live_component
  alias Pento.Game.{Board, Pentomino}
  import PentoWeb.GameLive.{Colors, Component}

  def update(%{puzzle: puzzle, id: id}, socket) do
    {:ok,
     socket
     |> assign_params(id, puzzle)
     |> assign_board()
     |> assign_shapes()}
  end

  def assign_params(socket, id, puzzle) do
    assign(socket, id: id, puzzle: puzzle)
  end

  def assign_board(%{assigns: %{puzzle: puzzle}} = socket) do
    active = Pentomino.new(name: :p, location: {7, 2})

    completed = [
      Pentomino.new(name: :u, rotation: 270, location: {1, 2}),
      Pentomino.new(name: :v, rotation: 90, location: {4, 2})
    ]

    # atom must exist!
    _puzzles = Board.puzzles()

    board =
      puzzle
      |> String.to_existing_atom()
      |> Board.new()
      |> Map.put(:completed_pentos, completed)
      |> Map.put(:active_pento, active)

    assign(socket, board: board)
  end

  def assign_shapes(%{assigns: %{board: board}} = socket) do
    shapes = Board.to_shapes(board)
    assign(socket, shapes: shapes)
  end
end

Okay so this is a live_component so we need an update and then somethings to deal with all the new information that is being passed with each update. We have hard coded some values for this as we will need to have something to work with as we build the rest, but right now we are setting the active an then some completed pieces to work with.


See the initial update. It takes the params from the current url and the id for the board-component and adds them to the assigns in the socket.


Then we need to assign the board. This will take on an other form later but this is where we would use events to assign an active pentomino from the list below and then so some actions to it. then try and place the pentomino. We are hard coding the pentos atm. Once we have the active and completed when then need to set the board size and then put the tuples into the board and then place that into the socket.assigns.


Then lastly we need to take all the board piece and then assign them shapes.

Render the Board

def render(assigns) do
    ~H"""
    <div id={@id} phx-window-keydown="key" phx-target={@myself}>
      <.canvas view_box="0 0 200 70">
        <%= for shape <- @shapes do %>
          <.shape
            points={shape.points}
            fill={color(shape.color, Board.active?(@board, shape.name), false)}
            name={shape.name}
          />
        <% end %>
      </.canvas>
      <hr />
      <.palette
        shape_names={@board.palette}
        completed_shape_names={Enum.map(@board.completed_pentos, & &1.name)}
      />
    </div>
    """
  end

We did it we have some hard coded game state but we took simple components and used them in sequence to create a board that can be changed with later code.