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

Analog Clock

analog-clock.livemd

Analog Clock

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

Introduction

The implementation of this clock is split into two components. Once a second, a Ticker genserver sends a timestamp to a ClockFace genserver which then updates a Kino frame using an SVG diagram.

graph LR;
  Ticker-->ClockFace-->Frame;

Note: This clock shows time in GMT (or whatever the system clock is configured to). It does not properly deal with timezones and daylight saving.

Clock Face

defmodule ClockFace do
  use GenServer

  @clock_size 600

  # (client) interface

  def start_link(frame) do
    GenServer.start_link(__MODULE__, frame)
  end
  
  def start(frame) do
    GenServer.start(__MODULE__, frame)
  end

  def set(server, value) do
    GenServer.cast(server, {:set, value})
  end
  
  # callbacks

  @impl true
  def init(state) do
    {:ok, state}
  end

  @impl true
  def handle_cast({:set, timestamp}, frame = state) do
    sec  = timestamp |> rem(60)
    min  = timestamp |> div(60) |> rem(60)
    hour = timestamp |> div(60*60) |> rem(24)
    
    svg =
      {sec, min, hour}
      |> draw()
      |> Kino.Image.new(:svg)
    Kino.Frame.render(frame, svg)
    
    {:noreply, state}
  end

  # helpers

  defp draw({sec, min, hour}) do
    """
    
      
      #{draw_hand(hour, :hours)}
      #{draw_hand(min, :minutes)}
      #{draw_hand(sec, :seconds)}
    
    """
  end

  defp draw_hand(index, type) do
    %{range: range, width: width, length: length, color: color} =
      case type do
        :seconds -> %{range: 60, width: 2, length: 0.8, color: "red"}
        :minutes -> %{range: 60, width: 6, length: 0.7, color: "black"}
        :hours   -> %{range: 12, width: 10, length: 0.5, color: "black"}
      end

    sx = @clock_size/2
    sy = @clock_size/2
    dx = sx + length*@clock_size/2*:math.cos(-:math.pi/2+2*:math.pi/range*index)
    dy = sy + length*@clock_size/2*:math.sin(-:math.pi/2+2*:math.pi/range*index)
    
    """
    
    
    """
  end
end

Ticker

defmodule Ticker do
  use GenServer

  # (client) interface

  def start_link(clock_pid, interval) do
    GenServer.start_link(__MODULE__, {clock_pid, interval})
  end

  def start(clock_pid, interval) do
    GenServer.start(__MODULE__, {clock_pid, interval})
  end

  # callbacks

  @impl true
  def init(state) do
    tick(state)
    {:ok, state}
  end

  @impl true
  def handle_cast({:tick}, state) do
    tick(state)
    {:noreply, state}
  end

  # helpers

  defp tick({clock_pid, interval}) do
    GenServer.cast(self(), {:tick})
    ClockFace.set(clock_pid, System.os_time(:second))
    :timer.sleep(interval)
  end
end

Application Code

Define a frame:

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

Start the clockface process:

{:ok, clockface_pid} = ClockFace.start(frame)

Start the ticker process:

{:ok, ticker_pid} = Ticker.start(clockface_pid, 1000)