diff --git a/lib/xebow/animation.ex b/lib/xebow/animation.ex new file mode 100644 index 0000000..572d6d4 --- /dev/null +++ b/lib/xebow/animation.ex @@ -0,0 +1,96 @@ +defmodule Xebow.Animation do + alias Xebow.RGBMatrix + + @callback init_state(pixels :: list(RGBMatrix.pixel())) :: t + @callback next_state(animation :: t) :: t + + @type t :: %__MODULE__{ + type: type, + tick: non_neg_integer, + speed: non_neg_integer, + delay_ms: non_neg_integer, + pixels: list(RGBMatrix.pixel()), + pixel_colors: list(RGBMatrix.pixel_color()) + } + defstruct [:type, :tick, :speed, :delay_ms, :pixels, :pixel_colors] + + # Helpers for implementing animations. + defmacro __using__(_) do + quote do + alias Xebow.Animation + + @behaviour Animation + + @impl true + def init_state(pixels) do + init_state_from_defaults(__MODULE__, pixels) + end + + # Increment the animation state to the next tick. + @spec do_tick(animation :: Animation.t()) :: Animation.t() + defp do_tick(animation) do + %Animation{animation | tick: animation.tick + 1} + end + + # Initialize an `Animation` struct with default values. + # Defaults can be overridden by passing the corresponding keyword as `opts`. + @spec init_state_from_defaults( + animation_type :: Animation.type(), + pixels :: list(RGBMatrix.pixel()), + opts :: list(keyword) + ) :: Animation.t() + defp init_state_from_defaults(animation_type, pixels, opts \\ []) do + %Animation{ + type: animation_type, + tick: opts[:tick] || 0, + speed: opts[:speed] || 100, + delay_ms: opts[:delay_ms] || 17, + pixels: pixels, + pixel_colors: opts[:pixel_colors] || init_pixel_colors(pixels) + } + end + + # Initialize a list of default pixel colors. + # The default sets all pixels to be turned off ("black"). + @spec init_pixel_colors(pixels :: list(RGBMatrix.pixel())) :: list(RGBMatrix.pixel_color()) + defp init_pixel_colors(pixels) do + Enum.map(pixels, fn _pixel -> Chameleon.HSV.new(0, 0, 0) end) + end + + defoverridable init_state: 1 + end + end + + @type type :: + __MODULE__.CycleAll + | __MODULE__.CycleLeftToRight + | __MODULE__.Pinwheel + + @doc """ + Returns a list of the available types of animations. + """ + @spec types :: list(type) + def types do + [ + __MODULE__.CycleAll, + __MODULE__.CycleLeftToRight, + __MODULE__.Pinwheel + ] + end + + @doc """ + Returns an animation set to its initial state. + """ + @spec init_state(animation_type :: type, pixels :: list(RGBMatrix.pixel())) :: t + def init_state(animation_type, pixels) do + animation_type.init_state(pixels) + end + + @doc """ + Returns the next state of an animation based on its current state. + """ + @spec next_state(animation :: t) :: t + def next_state(animation) do + animation.type.next_state(animation) + end +end diff --git a/lib/xebow/animation/cycle_all.ex b/lib/xebow/animation/cycle_all.ex new file mode 100644 index 0000000..a01182f --- /dev/null +++ b/lib/xebow/animation/cycle_all.ex @@ -0,0 +1,27 @@ +defmodule Xebow.Animation.CycleAll do + @moduledoc """ + Cycles hue of all keys. + """ + + alias Chameleon.HSV + + alias Xebow.Animation + + import Xebow.Utils, only: [mod: 2] + + use Animation + + @impl true + def next_state(animation) do + %Animation{tick: tick, speed: speed, pixels: pixels} = animation + time = div(tick * speed, 100) + + hue = mod(time, 360) + color = HSV.new(hue, 100, 100) + + pixel_colors = Enum.map(pixels, fn {_x, _y} -> color end) + + %Animation{animation | pixel_colors: pixel_colors} + |> do_tick() + end +end diff --git a/lib/xebow/animation/cycle_left_to_right.ex b/lib/xebow/animation/cycle_left_to_right.ex new file mode 100644 index 0000000..310398f --- /dev/null +++ b/lib/xebow/animation/cycle_left_to_right.ex @@ -0,0 +1,28 @@ +defmodule Xebow.Animation.CycleLeftToRight do + @moduledoc """ + Cycles hue left to right. + """ + + alias Chameleon.HSV + + alias Xebow.Animation + + import Xebow.Utils, only: [mod: 2] + + use Animation + + @impl true + def next_state(animation) do + %Animation{tick: tick, speed: speed, pixels: pixels} = animation + time = div(tick * speed, 100) + + pixel_colors = + for {x, _y} <- pixels do + hue = mod(x * 10 - time, 360) + HSV.new(hue, 100, 100) + end + + %Animation{animation | pixel_colors: pixel_colors} + |> do_tick() + end +end diff --git a/lib/xebow/animation/pinwheel.ex b/lib/xebow/animation/pinwheel.ex new file mode 100644 index 0000000..c2e1e50 --- /dev/null +++ b/lib/xebow/animation/pinwheel.ex @@ -0,0 +1,43 @@ +defmodule Xebow.Animation.Pinwheel do + @moduledoc """ + Cycles hue in a pinwheel pattern. + """ + + alias Chameleon.HSV + + alias Xebow.Animation + + import Xebow.Utils, only: [mod: 2] + + use Animation + + @center %{ + x: 1, + y: 1.5 + } + + @impl true + def next_state(animation) do + %Animation{tick: tick, speed: speed, pixels: pixels} = animation + time = div(tick * speed, 100) + + pixel_colors = + for {x, y} <- pixels do + dx = x - @center.x + dy = y - @center.y + + hue = mod(atan2_8(dy, dx) + time, 360) + + HSV.new(hue, 100, 100) + end + + %Animation{animation | pixel_colors: pixel_colors} + |> do_tick() + end + + defp atan2_8(x, y) do + atan = :math.atan2(x, y) + + trunc((atan + :math.pi()) * 359 / (2 * :math.pi())) + end +end diff --git a/lib/xebow/rgb_matrix.ex b/lib/xebow/rgb_matrix.ex index acfd43d..5dd0b53 100644 --- a/lib/xebow/rgb_matrix.ex +++ b/lib/xebow/rgb_matrix.ex @@ -2,19 +2,25 @@ defmodule Xebow.RGBMatrix do use GenServer alias Circuits.SPI - alias Xebow.RGBMatrix.Animations + alias Xebow.Animation import Xebow.Utils, only: [mod: 2] defmodule State do - @fields [:spidev, :animation, :animation_state] - @enforce_keys @fields - - defstruct @fields + defstruct [:spidev, :animation] end - @type pixels :: list({non_neg_integer, non_neg_integer}) - @type colors :: list(any) + @type any_color_model :: + Chameleon.Color.RGB.t() + | Chameleon.Color.CMYK.t() + | Chameleon.Color.Hex.t() + | Chameleon.Color.HSL.t() + | Chameleon.Color.HSV.t() + | Chameleon.Color.Keyword.t() + | Chameleon.Color.Pantone.t() + + @type pixel :: {non_neg_integer, non_neg_integer} + @type pixel_color :: any_color_model @spi_device "spidev0.0" @spi_speed_hz 4_000_000 @@ -67,34 +73,28 @@ defmodule Xebow.RGBMatrix do send(self(), :get_next_state) - [initial_animation | _] = Animations.list() + [initial_animation_type | _] = Animation.types() + + state = + %State{spidev: spidev} + |> set_animation(initial_animation_type) - {:ok, - %State{ - spidev: spidev, - animation: initial_animation, - animation_state: initial_animation.init_state() - }} + {:ok, state} end - defp set_animation(state, animation) do - %{ - state - | animation: animation, - animation_state: animation.init_state() - } + defp set_animation(state, animation_type) do + %State{state | animation: Animation.init_state(animation_type, @pixels)} end @impl true def handle_info(:get_next_state, state) do - {colors, delay, new_animation_state} = - state.animation.next_state(@pixels, state.animation_state) + new_animation_state = Animation.next_state(state.animation) - paint(state.spidev, colors) + paint(state.spidev, new_animation_state.pixel_colors) - Process.send_after(self(), :get_next_state, delay) + Process.send_after(self(), :get_next_state, new_animation_state.delay_ms) - {:noreply, %{state | animation_state: new_animation_state}} + {:noreply, %State{state | animation: new_animation_state}} end defp paint(spidev, colors) do @@ -123,22 +123,22 @@ defmodule Xebow.RGBMatrix do end def handle_cast(:next_animation, state) do - animations = Animations.list() - num = Enum.count(animations) - current = Enum.find_index(animations, &(&1 == state.animation)) + animation_types = Animation.types() + num = Enum.count(animation_types) + current = Enum.find_index(animation_types, &(&1 == state.animation.type)) next = mod(current + 1, num) - animation = Enum.at(animations, next) + animation_type = Enum.at(animation_types, next) - {:noreply, set_animation(state, animation)} + {:noreply, set_animation(state, animation_type)} end def handle_cast(:previous_animation, state) do - animations = Animations.list() - num = Enum.count(animations) - current = Enum.find_index(animations, &(&1 == state.animation)) + animation_types = Animation.types() + num = Enum.count(animation_types) + current = Enum.find_index(animation_types, &(&1 == state.animation.type)) previous = mod(current - 1, num) - animation = Enum.at(animations, previous) + animation_type = Enum.at(animation_types, previous) - {:noreply, set_animation(state, animation)} + {:noreply, set_animation(state, animation_type)} end end diff --git a/lib/xebow/rgb_matrix/animation.ex b/lib/xebow/rgb_matrix/animation.ex deleted file mode 100644 index c5397cc..0000000 --- a/lib/xebow/rgb_matrix/animation.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Xebow.RGBMatrix.Animation do - alias Xebow.RGBMatrix - - @callback init_state :: any - @callback next_state( - pixels :: RGBMatrix.pixels(), - state :: any - ) :: {RGBMatrix.colors(), non_neg_integer, any} -end diff --git a/lib/xebow/rgb_matrix/animations.ex b/lib/xebow/rgb_matrix/animations.ex deleted file mode 100644 index d2d8851..0000000 --- a/lib/xebow/rgb_matrix/animations.ex +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Xebow.RGBMatrix.Animations do - alias Xebow.RGBMatrix.Animations - - def list do - [ - Animations.CycleAll, - Animations.CycleLeftToRight, - Animations.Pinwheel - ] - end -end diff --git a/lib/xebow/rgb_matrix/animations/cycle_all.ex b/lib/xebow/rgb_matrix/animations/cycle_all.ex deleted file mode 100644 index 1c28c13..0000000 --- a/lib/xebow/rgb_matrix/animations/cycle_all.ex +++ /dev/null @@ -1,36 +0,0 @@ -defmodule Xebow.RGBMatrix.Animations.CycleAll do - @moduledoc """ - Cycles hue of all keys. - """ - - alias Chameleon.HSV - - alias Xebow.RGBMatrix.Animation - - import Xebow.Utils, only: [mod: 2] - - @behaviour Animation - - @delay_ms 17 - - @impl true - def init_state do - %{ - tick: 0, - speed: 100 - } - end - - @impl true - def next_state(pixels, state) do - %{tick: tick, speed: speed} = state - time = div(tick * speed, 100) - - hue = mod(time, 360) - color = HSV.new(hue, 100, 100) - - colors = Enum.map(pixels, fn {_x, _y} -> color end) - - {colors, @delay_ms, %{state | tick: tick + 1}} - end -end diff --git a/lib/xebow/rgb_matrix/animations/cycle_left_to_right.ex b/lib/xebow/rgb_matrix/animations/cycle_left_to_right.ex deleted file mode 100644 index 08b2f49..0000000 --- a/lib/xebow/rgb_matrix/animations/cycle_left_to_right.ex +++ /dev/null @@ -1,37 +0,0 @@ -defmodule Xebow.RGBMatrix.Animations.CycleLeftToRight do - @moduledoc """ - Cycles hue left to right. - """ - - alias Chameleon.HSV - - alias Xebow.RGBMatrix.Animation - - import Xebow.Utils, only: [mod: 2] - - @behaviour Animation - - @delay_ms 17 - - @impl true - def init_state do - %{ - tick: 0, - speed: 100 - } - end - - @impl true - def next_state(pixels, state) do - %{tick: tick, speed: speed} = state - time = div(tick * speed, 100) - - colors = - for {x, _y} <- pixels do - hue = mod(x * 10 - time, 360) - HSV.new(hue, 100, 100) - end - - {colors, @delay_ms, %{state | tick: tick + 1}} - end -end diff --git a/lib/xebow/rgb_matrix/animations/pinwheel.ex b/lib/xebow/rgb_matrix/animations/pinwheel.ex deleted file mode 100644 index f32470c..0000000 --- a/lib/xebow/rgb_matrix/animations/pinwheel.ex +++ /dev/null @@ -1,51 +0,0 @@ -defmodule Xebow.RGBMatrix.Animations.Pinwheel do - @moduledoc """ - Cycles hue in a pinwheel pattern. - """ - - alias Chameleon.HSV - - alias Xebow.RGBMatrix.Animation - - import Xebow.Utils, only: [mod: 2] - - @behaviour Animation - - @delay_ms 17 - - @impl true - def init_state do - %{ - tick: 0, - speed: 100, - center: %{ - x: 1, - y: 1.5 - } - } - end - - @impl true - def next_state(pixels, state) do - %{tick: tick, speed: speed} = state - time = div(tick * speed, 100) - - colors = - for {x, y} <- pixels do - dx = x - state.center.x - dy = y - state.center.y - - hue = mod(atan2_8(dy, dx) + time, 360) - - HSV.new(hue, 100, 100) - end - - {colors, @delay_ms, %{state | tick: tick + 1}} - end - - defp atan2_8(x, y) do - atan = :math.atan2(x, y) - - trunc((atan + :math.pi()) * 359 / (2 * :math.pi())) - end -end diff --git a/mix.exs b/mix.exs index 4408bbf..6c60fac 100644 --- a/mix.exs +++ b/mix.exs @@ -58,7 +58,10 @@ defmodule Xebow.MixProject do {:shoehorn, "~> 0.6"}, {:ring_logger, "~> 0.8"}, {:toolshed, "~> 0.2"}, - {:chameleon, "~> 2.2"}, + # {:chameleon, "~> 2.2"}, + # Open upstream PR: + # https://github.com/supersimple/chameleon/pull/18 + {:chameleon, github: "amclain/chameleon", ref: "update-spec-for-new"}, {:afk, "~> 0.3"}, {:dialyxir, "~> 1.0.0", only: :dev, runtime: false}, {:ex_doc, "~> 0.21.3", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index 78237be..3ffdb7d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "afk": {:hex, :afk, "0.3.0", "87ca8a98bdb6f5da3fdf96fc091c67655b15d593df38fb191d6324b05488172f", [:mix], [], "hexpm", "f7a1609a6a45221fcf11d5c531046d632e11368a4674c13ee7453e3fb0cc67a8"}, - "chameleon": {:hex, :chameleon, "2.2.0", "30665e3d83a36847dacee5ac3fa1a6ff7f388d14417aa16a7e73871b836a2d3f", [:mix], [], "hexpm", "7df4053da2da521bef95c996f0833587bb46e0f80627d40a2076a3fe2284917b"}, + "chameleon": {:git, "https://github.com/amclain/chameleon.git", "ceadb84dd5a7c0102cbad98c6fe4e1f44975e190", [ref: "update-spec-for-new"]}, "circuits_gpio": {:hex, :circuits_gpio, "0.4.5", "4d5b0f707c425fc56f03086232259f65482a3d1f1cf15335253636d0bb846446", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "b42d28d60a6cfdfb6b21b66ab0b8c5de0ea5a32b390b61d2fe86a2ad8edb90ad"}, "circuits_spi": {:hex, :circuits_spi, "0.1.5", "5f1901c77fb982217a956498ba0d9d0d47cc58ec47731020621b5c95eee296c0", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "134e1ded8e0379e3314a507e80a304cf5ba54edf294e157b7e7ecedcdca30dfd"}, "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"},