#
# 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