# # Created by Boyd Multerer on August 8, 2018. # Copyright © 2018 Kry10 Industries. All rights reserved. # defmodule Scenic.Clock.Analog do @moduledoc """ A component that runs an analog clock. See the [Components](Scenic.Clock.Components.html#analog_clock/2) module for useage """ use Scenic.Component, has_children: false alias Scenic.Graph alias Scenic.Primitive.Style.Theme # alias Scenic.Component.Input.Dropdown import Scenic.Primitives, only: [ {:circle, 3}, {:line, 3}, {:update_opts, 2} ] # import IEx # analog clock setup @default_radius 10 @two_pi 2 * :math.pi() @back_size_ratio 0.1 @hour_size_ratio -0.6 @minute_size_ratio -0.9 @second_size_ratio -0.9 @tick_ratio 0.08 @min_radius_for_default_ticks 30 @default_theme :dark # -------------------------------------------------------- @doc false @impl Scenic.Component def validate(nil), do: {:ok, nil} def validate(data) do { :error, """ #{IO.ANSI.red()}Invalid Scenic.Clock.Analog Received: #{inspect(data)} #{IO.ANSI.yellow()} """ } end # -------------------------------------------------------- @doc false @impl Scenic.Scene def init(scene, _param, opts) do styles = Keyword.get(opts, :styles, []) # theme is passed in as an inherited style theme = (opts[:theme] || Theme.preset(@default_theme)) |> Theme.normalize() # get and calc the sizes radius = opts[:radius] || @default_radius back_size = radius * @back_size_ratio hour_size = radius * @hour_size_ratio minute_size = radius * @minute_size_ratio second_size = radius * @second_size_ratio thick = cond do radius > 40 -> 2 true -> 1.2 end hour_color = Map.get(theme, :hours, theme.border) minute_color = Map.get(theme, :minutes, theme.border) # set up the main part of the clock graph = Graph.build(styles) |> circle(radius, fill: theme.background, stroke: {thick, theme.border}) |> line({{0, back_size}, {0, hour_size}}, pin: {0, 0}, stroke: {thick, hour_color}, id: :hour_hand ) |> line({{0, back_size}, {0, minute_size}}, pin: {0, 0}, stroke: {thick, minute_color}, id: :minute_hand ) # add the optional second hand if requested graph = case !!opts[:seconds] do true -> second_color = Map.get(theme, :second, theme.border) line( graph, {{0, back_size}, {0, second_size}}, pin: {0, 0}, stroke: {thick, second_color}, id: :second_hand ) false -> graph end # add the tick marks if requested graph = case styles[:ticks] do nil -> radius >= @min_radius_for_default_ticks _ -> !!styles[:ticks] end |> case do true -> angle = @two_pi / 12 tick_size = @tick_ratio * radius Enum.reduce(1..12, graph, fn n, g -> line( g, {{0, radius - tick_size}, {0, radius}}, stroke: {thick, theme.border}, pin: {0, 0}, rotate: n * angle ) end) false -> graph end scene = scene |> assign( graph: graph, timer: nil, last: nil, seconds: !!opts[:seconds] ) |> update_time() # send a message to self to start the clock a fraction of a second # into the future to hopefully line it up closer to when the seconds # actually are. Note that I want it to arrive just slightly after # the one second mark, which is way better than just slighty before. # avoid trunc errors and such that way even if it means the second # timer is one millisecond behind the actual time. {microseconds, _} = Time.utc_now().microsecond Process.send_after(self(), :start_clock, 1001 - trunc(microseconds / 1000)) {:ok, scene} end # -------------------------------------------------------- # should be shortly after the actual one-second mark @doc false @impl GenServer def handle_info(:start_clock, scene) do # start the timer on a one-second interval {:ok, timer} = :timer.send_interval(1000, :tick_tock) scene = scene |> assign(:timer, timer) |> update_time() {:noreply, scene} end # -------------------------------------------------------- def handle_info(:tick_tock, scene) do {:noreply, update_time(scene)} end # -------------------------------------------------------- defp update_time( %{ assigns: %{ graph: graph, seconds: seconds, last: last } } = scene ) do {_, {h, m, s}} = time = :calendar.local_time() base_time = base_time(time, seconds) case base_time != last do true -> # get the hour and minutes as a percent of the circle second_percent = s / 60.0 # get the hour and minutes as a percent of the circle minute_percent = (m + second_percent) / 60.0 hour = cond do h >= 12 -> h - 12 true -> h end hour_percent = (hour + minute_percent) / 12.0 # convert to radians and apply as a rotation matrix # a full circle is 2 radians... graph = graph |> Graph.modify(:hour_hand, &update_opts(&1, r: @two_pi * hour_percent)) |> Graph.modify(:minute_hand, &update_opts(&1, r: @two_pi * minute_percent)) |> Graph.modify(:second_hand, &update_opts(&1, r: @two_pi * second_percent)) scene |> assign(:graph, graph) |> push_graph(graph) _ -> scene end end defp base_time(time, true), do: time defp base_time({d, {h, m, _}}, false), do: {d, {h, m}} end