Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Module 6: Colors

elixir-intro/module6.livemd

Module 6: Colors

Mix.install([
  {:kino, "~> 0.14.1"}
])

Preparation

User Interface

Lets first define some slides for choosin integer values between 0 and 255 (both included), and arrange these in a “grid”. We will use them to define a color through its red, green and blue components:

kinos = [
  Kino.Input.range("Red", min: 0, max: 255),
  Kino.Input.range("Green", min: 0, max: 255),
  Kino.Input.range("Blue", min: 0, max: 255),
]
Kino.Layout.grid(kinos)

Next, we create a “frame”. This is a component in which we can place different elements.

frame = Kino.Frame.new() |> Kino.render()
nil

Logic Behind the Scene

The following module implements a GenServer that can receive updates the values indicated by the three sliders that each represent a color channel. A GenServer is a separate process (inside of Elixir) which can be started. Once started, you can send messages to it. Received messages will be processed according to the specified logic, and this may include returning a message. In this particular case, we make sure to draw a square in the above frame. That square is colored accordin to the positions of the sliders. Two comments in the code indicates two types of functions:

  1. Interface functions: Interface functions are those you call from the function you are in when you need the GenServer to do some work for you. This either involves starting an instance of the GenServer or sending a message to a (hopefulle already started) GenServer instance.
  2. Callback functions: When a GenServer receives such a message, a matching callback function will be called (in the process of this GenServer).
defmodule Color do
  use GenServer

  @dim 64

  # interface

  def start_link(widget) do
    color = %{r: 0, g: 0, b: 0}
    GenServer.start_link(__MODULE__, {widget, color})
  end

  def update(pid, channel, value) do
    GenServer.cast(pid, {:update, channel, value})
  end

  # callbacks

  @impl true
  def init({widget,_}=state) do
    Kino.Frame.render(widget, generate_svg(0, 0, 0))
    {:ok, state}
  end

  @impl true
  def handle_cast({:update, channel, value}, {widget, c} = _state) when is_atom(channel) do
    c = Map.put(c, channel, value)
    %{r: r, g: g, b: b} = c
    
    Kino.Frame.render(widget, generate_svg(r, g, b))
    {:noreply, {widget, c}}
  end

  # helpers

  defp generate_svg(r,g,b) do
    """
    
      
    
    """
    |> Kino.Image.new(:svg)
  end
end

Now that we have defined what our Color GenServer is, lets try starting one instance of it! If successful, it will result in a process id (aka, a pid). This is what we need in order to name and address the new Color process. This is needed as we could have more than one.

Starting it, and pattern matching our way to binding a variable to the pid looks like this:

{:ok, pid} = Color.start_link(frame)

Finally, we need to hook up the events generated by the sliders when we adjust them. We use the Kino.listen function for this purpose. The trunc function floors the input and converts it to an integer.

[kino_r, kino_g, kino_b] = kinos

[{:r, kino_r}, {:g, kino_g}, {:b, kino_b}]
|> Enum.map(
  fn {channel, kino} ->
    Kino.listen(kino, fn event -> Color.update(pid, channel, trunc(event.value)) end)
  end
)

Exercise

Scroll to the three sliders (for red, green and blue) and try to adjust them. In my case you have to let go before the action is registered. What happens to the box below?

Click through the above cells to get an understanding of what is going on.

Create a new frame. You can optionally arrange the two frames in a grid. Start a new Color GenServer and bind a variable to its pid. Now extend the code to handle events so that each event causes two things to happen:

  1. It is sent to the first Color pid as before.
  2. It is also sent to the second Color pid, but here the value must be subtracted from $255$. So if the original value is $100$, $255-100=155$ must be sent to this pid.

If you have a good grasp of color theory, you can also create a third frame that shows the complementary color. But it is a bit more complicated, and you have to define a separate GenServer.

Next step …

Interaction, that’s nice! When you are ready, move on here.