From e6b80981ab801d3c8aec673b4db733d3a4299705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Dos=C3=A9?= Date: Sat, 4 Jul 2020 21:52:17 -0700 Subject: [PATCH] Effects refactor --- lib/layout/key.ex | 4 +- lib/layout/led.ex | 4 +- lib/rgb_matrix.ex | 8 ++ lib/rgb_matrix/animation.ex | 109 --------------- lib/rgb_matrix/animation/cycle_all.ex | 28 ---- .../animation/cycle_left_to_right.ex | 30 ---- lib/rgb_matrix/animation/pinwheel.ex | 45 ------ lib/rgb_matrix/animation/static.ex | 114 --------------- lib/rgb_matrix/effect.ex | 88 ++++++++++++ lib/rgb_matrix/effect/breathing.ex | 45 ++++++ lib/rgb_matrix/effect/config.ex | 83 +++++++++++ lib/rgb_matrix/effect/config/integer.ex | 48 +++++++ lib/rgb_matrix/effect/config/option.ex | 30 ++++ lib/rgb_matrix/effect/cycle_all.ex | 46 ++++++ lib/rgb_matrix/effect/hue_wave.ex | 103 ++++++++++++++ lib/rgb_matrix/effect/pinwheel.ex | 68 +++++++++ lib/rgb_matrix/effect/random_keypresses.ex | 52 +++++++ lib/rgb_matrix/effect/random_solid.ex | 43 ++++++ lib/rgb_matrix/effect/solid_color.ex | 39 ++++++ lib/rgb_matrix/effect/solid_reactive.ex | 113 +++++++++++++++ lib/rgb_matrix/effect/splash.ex | 94 +++++++++++++ lib/rgb_matrix/engine.ex | 131 +++++++++--------- lib/rgb_matrix/frame.ex | 34 ----- lib/rgb_matrix/paintable.ex | 12 +- lib/rgb_matrix/pixel.ex | 27 ---- lib/xebow/application.ex | 5 +- lib/xebow/keyboard.ex | 75 +++++----- lib/xebow/leds.ex | 23 ++- lib/xebow/utils.ex | 42 ------ 29 files changed, 993 insertions(+), 550 deletions(-) delete mode 100644 lib/rgb_matrix/animation.ex delete mode 100644 lib/rgb_matrix/animation/cycle_all.ex delete mode 100644 lib/rgb_matrix/animation/cycle_left_to_right.ex delete mode 100644 lib/rgb_matrix/animation/pinwheel.ex delete mode 100644 lib/rgb_matrix/animation/static.ex create mode 100644 lib/rgb_matrix/effect.ex create mode 100644 lib/rgb_matrix/effect/breathing.ex create mode 100644 lib/rgb_matrix/effect/config.ex create mode 100644 lib/rgb_matrix/effect/config/integer.ex create mode 100644 lib/rgb_matrix/effect/config/option.ex create mode 100644 lib/rgb_matrix/effect/cycle_all.ex create mode 100644 lib/rgb_matrix/effect/hue_wave.ex create mode 100644 lib/rgb_matrix/effect/pinwheel.ex create mode 100644 lib/rgb_matrix/effect/random_keypresses.ex create mode 100644 lib/rgb_matrix/effect/random_solid.ex create mode 100644 lib/rgb_matrix/effect/solid_color.ex create mode 100644 lib/rgb_matrix/effect/solid_reactive.ex create mode 100644 lib/rgb_matrix/effect/splash.ex delete mode 100644 lib/rgb_matrix/frame.ex delete mode 100644 lib/rgb_matrix/pixel.ex delete mode 100644 lib/xebow/utils.ex diff --git a/lib/layout/key.ex b/lib/layout/key.ex index 5bce377..d323886 100644 --- a/lib/layout/key.ex +++ b/lib/layout/key.ex @@ -3,8 +3,10 @@ defmodule Layout.Key do Describes a physical key and its location. """ + @type id :: atom + @type t :: %__MODULE__{ - id: atom, + id: id, x: float, y: float, width: float, diff --git a/lib/layout/led.ex b/lib/layout/led.ex index 18183d2..e06e382 100644 --- a/lib/layout/led.ex +++ b/lib/layout/led.ex @@ -3,8 +3,10 @@ defmodule Layout.LED do Describes a physical LED location. """ + @type id :: atom + @type t :: %__MODULE__{ - id: atom, + id: id, x: float, y: float } diff --git a/lib/rgb_matrix.ex b/lib/rgb_matrix.ex index 074bbfc..53ebaf7 100644 --- a/lib/rgb_matrix.ex +++ b/lib/rgb_matrix.ex @@ -1,2 +1,10 @@ defmodule RGBMatrix do + @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() end diff --git a/lib/rgb_matrix/animation.ex b/lib/rgb_matrix/animation.ex deleted file mode 100644 index cdcedf7..0000000 --- a/lib/rgb_matrix/animation.ex +++ /dev/null @@ -1,109 +0,0 @@ -defmodule RGBMatrix.Animation do - @moduledoc """ - Provides a data structure and functions to define an RGBMatrix animation. - - There are currently two distinct ways to define an animation. - - You may define an animation with a predefined `:frames` field. Each frame will advance every `:delay_ms` milliseconds. - These animations should use the `RGBMatrix.Animation.Static` `:type`. See the moduledocs of that module for - examples. - - Alternatively, you may have a more dynamic animation which generates frames based on the current `:tick` of the - animation. See `RGBMatrix.Animation.{CycleAll, CycleLeftToRight, Pinwheel}` for examples. - """ - - alias __MODULE__ - alias RGBMatrix.Frame - - defmacro __using__(_) do - quote do - alias RGBMatrix.Animation - - @behaviour Animation - end - end - - @callback next_frame(animation :: Animation.t()) :: Frame.t() - - @type t :: %__MODULE__{ - type: animation_type, - tick: non_neg_integer, - speed: non_neg_integer, - loop: non_neg_integer | :infinite, - delay_ms: non_neg_integer, - frames: list(Frame.t()), - next_frame: Frame.t() | nil - } - defstruct [:type, :tick, :speed, :delay_ms, :loop, :next_frame, :frames] - - @type animation_type :: - __MODULE__.CycleAll - | __MODULE__.CycleLeftToRight - | __MODULE__.Pinwheel - | __MODULE__.Static - - @doc """ - Returns a list of the available types of animations. - """ - @spec types :: list(animation_type) - def types do - [ - __MODULE__.CycleAll, - __MODULE__.CycleLeftToRight, - __MODULE__.Pinwheel - ] - end - - @type animation_opt :: - {:type, animation_type} - | {:frames, list} - | {:tick, non_neg_integer} - | {:speed, non_neg_integer} - | {:delay_ms, non_neg_integer} - | {:loop, non_neg_integer | :infinite} - - @spec new(opts :: list(animation_opt)) :: Animation.t() - def new(opts) do - animation_type = Keyword.fetch!(opts, :type) - frames = Keyword.get(opts, :frames, []) - - %Animation{ - type: animation_type, - tick: opts[:tick] || 0, - speed: opts[:speed] || 100, - delay_ms: opts[:delay_ms] || 17, - loop: opts[:loop] || :infinite, - frames: frames, - next_frame: List.first(frames) - } - end - - @doc """ - Updates the state of an animation with the next tick of animation. - """ - @spec next_frame(animation :: Animation.t()) :: Animation.t() - def next_frame(animation) do - next_frame = animation.type.next_frame(animation) - %Animation{animation | next_frame: next_frame, tick: animation.tick + 1} - end - - @doc """ - Returns the frame count of a given animation, - - Note: this function returns :infinite for dynamic animations. - """ - @spec frame_count(animation :: Animation.t()) :: non_neg_integer | :infinite - def frame_count(%{loop: :infinite}), do: :infinite - - def frame_count(animation), do: length(animation.frames) * animation.loop - - @doc """ - Returns the expected duration of a given animation. - - Note: this function returns :infinite for dynamic animations. - """ - @spec duration(animation :: Animation.t()) :: non_neg_integer | :infinite - def duration(%{loop: :infinite}), do: :infinite - - def duration(animation), do: frame_count(animation) * animation.delay_ms -end diff --git a/lib/rgb_matrix/animation/cycle_all.ex b/lib/rgb_matrix/animation/cycle_all.ex deleted file mode 100644 index 11d3d97..0000000 --- a/lib/rgb_matrix/animation/cycle_all.ex +++ /dev/null @@ -1,28 +0,0 @@ -defmodule RGBMatrix.Animation.CycleAll do - @moduledoc """ - Cycles hue of all keys. - """ - - alias Chameleon.HSV - - alias RGBMatrix.{Animation, Frame} - - import RGBMatrix.Utils, only: [mod: 2] - - use Animation - - @impl Animation - def next_frame(animation) do - %Animation{tick: tick, speed: speed} = animation - time = div(tick * speed, 100) - - hue = mod(time, 360) - color = HSV.new(hue, 100, 100) - - # FIXME: no reaching into Xebow namespace - pixels = Xebow.Utils.pixels() - pixel_colors = Enum.map(pixels, fn {_x, _y} -> color end) - - Frame.new(pixels, pixel_colors) - end -end diff --git a/lib/rgb_matrix/animation/cycle_left_to_right.ex b/lib/rgb_matrix/animation/cycle_left_to_right.ex deleted file mode 100644 index 88d91f2..0000000 --- a/lib/rgb_matrix/animation/cycle_left_to_right.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule RGBMatrix.Animation.CycleLeftToRight do - @moduledoc """ - Cycles hue left to right. - """ - - alias Chameleon.HSV - - alias RGBMatrix.{Animation, Frame} - - import RGBMatrix.Utils, only: [mod: 2] - - use Animation - - @impl Animation - def next_frame(animation) do - %Animation{tick: tick, speed: speed} = animation - time = div(tick * speed, 100) - - # FIXME: no reaching into Xebow namespace - pixels = Xebow.Utils.pixels() - - pixel_colors = - for {x, _y} <- pixels do - hue = mod(x * 10 - time, 360) - HSV.new(hue, 100, 100) - end - - Frame.new(pixels, pixel_colors) - end -end diff --git a/lib/rgb_matrix/animation/pinwheel.ex b/lib/rgb_matrix/animation/pinwheel.ex deleted file mode 100644 index 4149c0e..0000000 --- a/lib/rgb_matrix/animation/pinwheel.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule RGBMatrix.Animation.Pinwheel do - @moduledoc """ - Cycles hue in a pinwheel pattern. - """ - - alias Chameleon.HSV - - alias RGBMatrix.{Animation, Frame} - - import RGBMatrix.Utils, only: [mod: 2] - - use Animation - - @center %{ - x: 1, - y: 1.5 - } - - @impl Animation - def next_frame(animation) do - %Animation{tick: tick, speed: speed} = animation - time = div(tick * speed, 100) - - # FIXME: no reaching into Xebow namespace - pixels = Xebow.Utils.pixels() - - 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 - - Frame.new(pixels, pixel_colors) - 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/rgb_matrix/animation/static.ex b/lib/rgb_matrix/animation/static.ex deleted file mode 100644 index d28ae5b..0000000 --- a/lib/rgb_matrix/animation/static.ex +++ /dev/null @@ -1,114 +0,0 @@ -defmodule RGBMatrix.Animation.Static do - @moduledoc """ - Pre-defined animations that run as a one-shot or on a loop. - - The `RGBMatrix.Animation.Static` animation type is used to define animations with a pre-defined set of frames. These - animations can be played continiously or on a loop. This behavior is controlled by the `:loop` field on the animation. - A value of `:infinite` means to play the animation continuously, while a value greater than 0 means to loop the animation - `:loop` times. - - Note that the playback speed of these animations is controlled by the `:delay_ms` field in the animation struct. The - `:speed` field not used when rendering these animations. - - ## Examples - - ### Play a random color on each pixel for 10 frames, continuously. - ``` - gen_map = fn -> - Enum.into(Xebow.Utils.pixels(), %{}, fn pixel -> - {pixel, Chameleon.HSV.new(:random.uniform(360), 100, 100)} - end) - end - - generator = fn -> struct!(RGBMatrix.Frame, pixel_map: gen_map.()) end - frames = Stream.repeatedly(generator) |> Enum.take(10) - - animation = - %RGBMatrix.Animation{ - delay_ms: 100, - frames: frames, - tick: 0, - loop: :infinite, - type: RGBMatrix.Animation.Static - } - - Xebow.LEDs.play_animation(animation) - ``` - - ### Play a pre-defined animation, three times - ``` - frames = [ - %RGBMatrix.Frame{ - pixel_map: %{ - {0, 0} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {0, 1} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {0, 2} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {0, 3} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {1, 0} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {1, 1} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {1, 2} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {1, 3} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {2, 0} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {2, 1} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {2, 2} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {2, 3} => %Chameleon.HSV{h: 100, s: 100, v: 100} - } - }, - %RGBMatrix.Frame{ - pixel_map: %{ - {0, 0} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {0, 1} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {0, 2} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {0, 3} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {1, 0} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {1, 1} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {1, 2} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {1, 3} => %Chameleon.HSV{h: 100, s: 100, v: 100}, - {2, 0} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {2, 1} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {2, 2} => %Chameleon.HSV{h: 0, s: 0, v: 0}, - {2, 3} => %Chameleon.HSV{h: 0, s: 0, v: 0} - } - } - ] - - animation = %RGBMatrix.Animation{ - delay_ms: 200, - frames: frames, - tick: 0, - loop: 3, - type: RGBMatrix.Animation.Static - } - - Xebow.LEDs.play_animation(animation) - ``` - """ - - alias RGBMatrix.Animation - - import RGBMatrix.Utils, only: [mod: 2] - - use Animation - - @impl Animation - def next_frame(%{loop: :infinite} = animation) do - %Animation{frames: frames, tick: tick} = animation - - index = mod(tick, length(frames)) - Enum.at(frames, index) - end - - @impl Animation - def next_frame(animation) do - %Animation{frames: frames, tick: tick, loop: loop} = animation - - all_frames = all_frames(frames, loop) - index = mod(tick, length(all_frames)) - Enum.at(all_frames, index) - end - - defp all_frames(frames, loop) do - List.duplicate(frames, loop) - |> List.flatten() - end -end diff --git a/lib/rgb_matrix/effect.ex b/lib/rgb_matrix/effect.ex new file mode 100644 index 0000000..132041f --- /dev/null +++ b/lib/rgb_matrix/effect.ex @@ -0,0 +1,88 @@ +defmodule RGBMatrix.Effect do + alias Layout.LED + + @callback new(leds :: list(LED.t()), config :: any) :: {render_in, any} + @callback render(state :: any, config :: any) :: + {list(RGBMatrix.any_color_model()), render_in, any} + @callback key_pressed(state :: any, config :: any, led :: LED.t()) :: {render_in, any} + + @type t :: %__MODULE__{ + type: type, + config: any, + state: any + } + defstruct [:type, :config, :state] + + defmacro __using__(_) do + quote do + @behaviour RGBMatrix.Effect + end + end + + @type render_in :: non_neg_integer() | :never | :ignore + + @type type :: + __MODULE__.CycleAll + | __MODULE__.HueWave + | __MODULE__.Pinwheel + | __MODULE__.RandomSolid + | __MODULE__.RandomKeypresses + | __MODULE__.SolidColor + | __MODULE__.Breathing + | __MODULE__.SolidReactive + | __MODULE__.Splash + + @doc """ + Returns a list of the available types of animations. + """ + @spec types :: list(type) + def types do + [ + __MODULE__.CycleAll, + __MODULE__.HueWave, + __MODULE__.Pinwheel, + __MODULE__.RandomSolid, + __MODULE__.RandomKeypresses, + __MODULE__.SolidColor, + __MODULE__.Breathing, + __MODULE__.SolidReactive, + __MODULE__.Splash + ] + end + + @doc """ + Returns an effect's initial state. + """ + @spec new(effect_type :: type, leds :: list(LED.t())) :: {render_in, t} + def new(effect_type, leds) do + config_module = Module.concat([effect_type, Config]) + effect_config = config_module.new() + {render_in, effect_state} = effect_type.new(leds, effect_config) + + effect = %__MODULE__{ + type: effect_type, + config: effect_config, + state: effect_state + } + + {render_in, effect} + end + + @doc """ + Returns the next state of an effect based on its current state. + """ + @spec render(effect :: t) :: {list(RGBMatrix.any_color_model()), render_in, t} + def render(effect) do + {colors, render_in, effect_state} = effect.type.render(effect.state, effect.config) + {colors, render_in, %{effect | state: effect_state}} + end + + @doc """ + Sends a key pressed event to an effect. + """ + @spec key_pressed(effect :: t, led :: LED.t()) :: {render_in, t} + def key_pressed(effect, led) do + {render_in, effect_state} = effect.type.key_pressed(effect.state, effect.config, led) + {render_in, %{effect | state: effect_state}} + end +end diff --git a/lib/rgb_matrix/effect/breathing.ex b/lib/rgb_matrix/effect/breathing.ex new file mode 100644 index 0000000..2df71ba --- /dev/null +++ b/lib/rgb_matrix/effect/breathing.ex @@ -0,0 +1,45 @@ +defmodule RGBMatrix.Effect.Breathing do + @moduledoc """ + Single hue brightness cycling. + """ + + alias Chameleon.HSV + alias RGBMatrix.Effect + + use Effect + + defmodule Config do + use RGBMatrix.Effect.Config + end + + defmodule State do + defstruct [:color, :tick, :speed, :led_ids] + end + + @delay_ms 17 + + @impl true + def new(leds, _config) do + # TODO: configurable base color + color = HSV.new(40, 100, 100) + led_ids = Enum.map(leds, & &1.id) + {0, %State{color: color, tick: 0, speed: 100, led_ids: led_ids}} + end + + @impl true + def render(state, _config) do + %{color: base_color, tick: tick, speed: speed, led_ids: led_ids} = state + + value = trunc(abs(:math.sin(tick * speed / 5_000)) * base_color.v) + color = HSV.new(base_color.h, base_color.s, value) + + colors = Enum.map(led_ids, fn id -> {id, color} end) + + {colors, @delay_ms, %{state | tick: tick + 1}} + end + + @impl true + def key_pressed(state, _config, _led) do + {:ignore, state} + end +end diff --git a/lib/rgb_matrix/effect/config.ex b/lib/rgb_matrix/effect/config.ex new file mode 100644 index 0000000..d7ad6b6 --- /dev/null +++ b/lib/rgb_matrix/effect/config.ex @@ -0,0 +1,83 @@ +defmodule RGBMatrix.Effect.Config do + @callback schema() :: keyword(any) + @callback new(%{optional(atom) => any}) :: struct + @callback update(struct, %{optional(atom) => any}) :: struct + + @types %{ + integer: RGBMatrix.Effect.Config.Integer, + option: RGBMatrix.Effect.Config.Option + } + + defmacro __using__(_) do + quote do + import RGBMatrix.Effect.Config + + Module.register_attribute(__MODULE__, :fields, + accumulate: true, + persist: false + ) + + @before_compile RGBMatrix.Effect.Config + end + end + + defmacro field(name, type, opts \\ []) do + type = Map.fetch!(@types, type) + type_schema = Macro.escape(struct!(type, opts)) + + quote do + @fields {unquote(name), unquote(type_schema)} + end + end + + defmacro __before_compile__(env) do + schema = Module.get_attribute(env.module, :fields) + keys = Keyword.keys(schema) + schema = Macro.escape(schema) + + quote do + @behaviour RGBMatrix.Effect.Config + + @enforce_keys unquote(keys) + defstruct unquote(keys) + + @impl RGBMatrix.Effect.Config + def schema do + unquote(schema) + end + + @impl RGBMatrix.Effect.Config + def new(params \\ %{}) do + schema() + |> Map.new(fn {key, %mod{} = type} -> + value = Map.get(params, key, type.default) + + case mod.validate(type, value) do + :ok -> {key, value} + :error -> value_error!(value, key) + end + end) + |> (&struct!(__MODULE__, &1)).() + end + + @impl RGBMatrix.Effect.Config + def update(config, params) do + schema = schema() + + Enum.reduce(params, config, fn {key, value}, config -> + %mod{} = type = Keyword.fetch!(schema, key) + if mod.validate(type, value) == :error, do: value_error!(value, key) + + Map.put(config, key, value) + end) + end + + defp value_error!(value, key) do + message = + "#{__MODULE__}: value `#{inspect(value)}` is invalid for config option `#{key}`." + + raise ArgumentError, message: message + end + end + end +end diff --git a/lib/rgb_matrix/effect/config/integer.ex b/lib/rgb_matrix/effect/config/integer.ex new file mode 100644 index 0000000..3d206dc --- /dev/null +++ b/lib/rgb_matrix/effect/config/integer.ex @@ -0,0 +1,48 @@ +defmodule RGBMatrix.Effect.Config.Integer do + @enforce_keys [:default, :min, :max] + defstruct [:default, :min, :max, step: 1] + + import RGBMatrix.Utils, only: [mod: 2] + + def validate(integer, value) do + if value >= integer.min && + value <= integer.max && + mod(value, integer.step) == 0 do + :ok + else + :error + end + end + + def cast(integer, bin_value) when is_binary(bin_value) do + case Integer.parse(bin_value) do + {value, ""} -> + cast(integer, value) + + _else -> + case Float.parse(bin_value) do + {value, ""} -> cast(integer, value) + _else -> :error + end + end + end + + def cast(integer, value) when is_float(value) do + int_value = trunc(value) + + if int_value == value do + cast(integer, int_value) + else + :error + end + end + + def cast(integer, value) when is_integer(value) do + case validate(integer, value) do + :ok -> {:ok, value} + :error -> :error + end + end + + def cast(_integer, _value), do: :error +end diff --git a/lib/rgb_matrix/effect/config/option.ex b/lib/rgb_matrix/effect/config/option.ex new file mode 100644 index 0000000..1771348 --- /dev/null +++ b/lib/rgb_matrix/effect/config/option.ex @@ -0,0 +1,30 @@ +defmodule RGBMatrix.Effect.Config.Option do + @enforce_keys [:default, :options] + defstruct [:default, :options] + + def validate(option, value) do + if value in option.options do + :ok + else + :error + end + end + + def cast(option, bin_value) when is_binary(bin_value) do + try do + value = String.to_existing_atom(bin_value) + cast(option, value) + rescue + ArgumentError -> :error + end + end + + def cast(option, value) when is_atom(value) do + case validate(option, value) do + :ok -> {:ok, value} + :error -> :error + end + end + + def cast(_option, _value), do: :error +end diff --git a/lib/rgb_matrix/effect/cycle_all.ex b/lib/rgb_matrix/effect/cycle_all.ex new file mode 100644 index 0000000..9365b13 --- /dev/null +++ b/lib/rgb_matrix/effect/cycle_all.ex @@ -0,0 +1,46 @@ +defmodule RGBMatrix.Effect.CycleAll do + @moduledoc """ + Cycles the hue of all LEDs at the same time. + """ + + alias Chameleon.HSV + alias RGBMatrix.Effect + + use Effect + + import RGBMatrix.Utils, only: [mod: 2] + + defmodule Config do + use RGBMatrix.Effect.Config + end + + defmodule State do + defstruct [:tick, :speed, :led_ids] + end + + @delay_ms 17 + + @impl true + def new(leds, _config) do + led_ids = Enum.map(leds, & &1.id) + {0, %State{tick: 0, speed: 100, led_ids: led_ids}} + end + + @impl true + def render(state, _config) do + %{tick: tick, speed: speed, led_ids: led_ids} = state + + time = div(tick * speed, 100) + hue = mod(time, 360) + color = HSV.new(hue, 100, 100) + + colors = Enum.map(led_ids, fn id -> {id, color} end) + + {colors, @delay_ms, %{state | tick: tick + 1}} + end + + @impl true + def key_pressed(state, _config, _led) do + {:ignore, state} + end +end diff --git a/lib/rgb_matrix/effect/hue_wave.ex b/lib/rgb_matrix/effect/hue_wave.ex new file mode 100644 index 0000000..9d80583 --- /dev/null +++ b/lib/rgb_matrix/effect/hue_wave.ex @@ -0,0 +1,103 @@ +defmodule RGBMatrix.Effect.HueWave do + @moduledoc """ + Creates a wave of shifting hue that moves across the matrix. + """ + + alias Chameleon.HSV + alias Layout.LED + alias RGBMatrix.Effect + + use Effect + + import RGBMatrix.Utils, only: [mod: 2] + + defmodule Config do + use RGBMatrix.Effect.Config + + @doc name: "Speed", + description: """ + Controls the speed at which the wave moves across the matrix. + """ + field(:speed, :integer, default: 4, min: 0, max: 32) + + @doc name: "Width", + description: """ + The rate of change of the wave, higher values means it's more spread out. + """ + field(:width, :integer, default: 20, min: 10, max: 100, step: 10) + + @doc name: "Direction", + description: """ + The direction the wave travels across the matrix. + """ + field(:direction, :option, + default: :right, + options: [ + :right, + :left, + :up, + :down + ] + ) + end + + defmodule State do + defstruct [:tick, :leds, :steps] + end + + @delay_ms 17 + + @impl true + def new(leds, config) do + steps = 360 / config.width + {0, %State{tick: 0, leds: leds, steps: steps}} + end + + @impl true + def render(state, config) do + %{tick: tick, leds: leds, steps: _steps} = state + %{speed: speed, direction: direction} = config + + # TODO: fixme + steps = 360 / config.width + + time = div(tick * speed, 5) + + colors = render_colors(leds, steps, time, direction) + + {colors, @delay_ms, %{state | tick: tick + 1}} + end + + defp render_colors(leds, steps, time, :right) do + for %LED{id: id, x: x} <- leds do + hue = mod(trunc(x * steps) - time, 360) + {id, HSV.new(hue, 100, 100)} + end + end + + defp render_colors(leds, steps, time, :left) do + for %LED{id: id, x: x} <- leds do + hue = mod(trunc(x * steps) + time, 360) + {id, HSV.new(hue, 100, 100)} + end + end + + defp render_colors(leds, steps, time, :up) do + for %LED{id: id, y: y} <- leds do + hue = mod(trunc(y * steps) + time, 360) + {id, HSV.new(hue, 100, 100)} + end + end + + defp render_colors(leds, steps, time, :down) do + for %LED{id: id, y: y} <- leds do + hue = mod(trunc(y * steps) - time, 360) + {id, HSV.new(hue, 100, 100)} + end + end + + @impl true + def key_pressed(state, _config, _led) do + {:ignore, state} + end +end diff --git a/lib/rgb_matrix/effect/pinwheel.ex b/lib/rgb_matrix/effect/pinwheel.ex new file mode 100644 index 0000000..4ef9143 --- /dev/null +++ b/lib/rgb_matrix/effect/pinwheel.ex @@ -0,0 +1,68 @@ +defmodule RGBMatrix.Effect.Pinwheel do + @moduledoc """ + Cycles hue in a pinwheel pattern. + """ + + alias Chameleon.HSV + alias Layout.LED + alias RGBMatrix.Effect + + use Effect + + import RGBMatrix.Utils, only: [mod: 2] + + defmodule Config do + use RGBMatrix.Effect.Config + end + + defmodule State do + defstruct [:tick, :speed, :leds, :center] + end + + @delay_ms 17 + + @impl true + def new(leds, _config) do + {0, %State{tick: 0, speed: 100, leds: leds, center: determine_center(leds)}} + end + + defp determine_center(leds) do + {%{x: min_x}, %{x: max_x}} = Enum.min_max_by(leds, & &1.x) + {%{y: min_y}, %{y: max_y}} = Enum.min_max_by(leds, & &1.y) + + %{ + x: (max_x - min_x) / 2 + min_x, + y: (max_y - min_y) / 2 + min_y + } + end + + @impl true + def render(state, _config) do + %{tick: tick, speed: speed, leds: leds, center: center} = state + + time = div(tick * speed, 100) + + colors = + for %LED{id: id, x: x, y: y} <- leds do + dx = x - center.x + dy = y - center.y + + hue = mod(atan2_8(dy, dx) + time, 360) + + {id, 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 + + @impl true + def key_pressed(state, _config, %LED{x: x, y: y}) do + {:ignore, %{state | center: %{x: x, y: y}}} + end +end diff --git a/lib/rgb_matrix/effect/random_keypresses.ex b/lib/rgb_matrix/effect/random_keypresses.ex new file mode 100644 index 0000000..87ff149 --- /dev/null +++ b/lib/rgb_matrix/effect/random_keypresses.ex @@ -0,0 +1,52 @@ +defmodule RGBMatrix.Effect.RandomKeypresses do + @moduledoc """ + Changes every key pressed to a random color. + """ + + alias Chameleon.HSV + alias RGBMatrix.Effect + + use Effect + + defmodule Config do + use RGBMatrix.Effect.Config + end + + defmodule State do + defstruct [:led_ids, :dirty] + end + + @impl true + def new(leds, _config) do + led_ids = Enum.map(leds, & &1.id) + + {0, + %State{ + led_ids: led_ids, + # NOTE: as to not conflict with possible led ID of `:all` + dirty: {:all} + }} + end + + @impl true + def render(state, _config) do + %{led_ids: led_ids, dirty: dirty} = state + + colors = + case dirty do + {:all} -> Enum.map(led_ids, fn id -> {id, random_color()} end) + id -> [{id, random_color()}] + end + + {colors, :never, state} + end + + defp random_color do + HSV.new((:rand.uniform() * 360) |> trunc(), 100, 100) + end + + @impl true + def key_pressed(state, _config, led) do + {0, %{state | dirty: led.id}} + end +end diff --git a/lib/rgb_matrix/effect/random_solid.ex b/lib/rgb_matrix/effect/random_solid.ex new file mode 100644 index 0000000..8ef619c --- /dev/null +++ b/lib/rgb_matrix/effect/random_solid.ex @@ -0,0 +1,43 @@ +defmodule RGBMatrix.Effect.RandomSolid do + @moduledoc """ + A random solid color fills the entire matrix and changes every key-press. + """ + + alias Chameleon.HSV + alias RGBMatrix.Effect + + use Effect + + defmodule Config do + use RGBMatrix.Effect.Config + end + + defmodule State do + defstruct [:led_ids] + end + + @impl true + def new(leds, _config) do + {0, %State{led_ids: Enum.map(leds, & &1.id)}} + end + + @impl true + def render(state, _config) do + %{led_ids: led_ids} = state + + color = random_color() + + colors = Enum.map(led_ids, fn id -> {id, color} end) + + {colors, :never, state} + end + + defp random_color do + HSV.new((:rand.uniform() * 360) |> trunc(), 100, 100) + end + + @impl true + def key_pressed(state, _config, _led) do + {0, state} + end +end diff --git a/lib/rgb_matrix/effect/solid_color.ex b/lib/rgb_matrix/effect/solid_color.ex new file mode 100644 index 0000000..d8367d8 --- /dev/null +++ b/lib/rgb_matrix/effect/solid_color.ex @@ -0,0 +1,39 @@ +defmodule RGBMatrix.Effect.SolidColor do + @moduledoc """ + All LEDs are a solid color. + """ + + alias Chameleon.HSV + alias RGBMatrix.Effect + + use Effect + + defmodule Config do + use RGBMatrix.Effect.Config + end + + defmodule State do + defstruct [:color, :led_ids] + end + + @impl true + def new(leds, _config) do + # TODO: configurable base color + color = HSV.new(120, 100, 100) + {0, %State{color: color, led_ids: Enum.map(leds, & &1.id)}} + end + + @impl true + def render(state, _config) do + %{color: color, led_ids: led_ids} = state + + colors = Enum.map(led_ids, fn id -> {id, color} end) + + {colors, :never, state} + end + + @impl true + def key_pressed(state, _config, _led) do + {:ignore, state} + end +end diff --git a/lib/rgb_matrix/effect/solid_reactive.ex b/lib/rgb_matrix/effect/solid_reactive.ex new file mode 100644 index 0000000..0936491 --- /dev/null +++ b/lib/rgb_matrix/effect/solid_reactive.ex @@ -0,0 +1,113 @@ +defmodule RGBMatrix.Effect.SolidReactive do + @moduledoc """ + Static single hue, pulses keys hit to shifted hue then fades to current hue. + """ + + alias Chameleon.HSV + alias RGBMatrix.Effect + + use Effect + + import RGBMatrix.Utils, only: [mod: 2] + + defmodule Config do + use RGBMatrix.Effect.Config + + @doc name: "Speed", + description: """ + The speed at which the hue shifts back to base. + """ + field :speed, :integer, default: 4, min: 0, max: 32 + + @doc name: "Distance", + description: """ + The distance that the hue shifts on key-press. + """ + field :distance, :integer, default: 180, min: 0, max: 360, step: 10 + + @doc name: "Direction", + description: """ + The direction (through the color wheel) that the hue shifts on key-press. + """ + field :direction, :option, + default: :random, + options: [ + :random, + :negative, + :positive + ] + end + + defmodule State do + defstruct [:first_render, :paused, :tick, :color, :leds, :hits] + end + + @delay_ms 17 + + @impl true + def new(leds, _config) do + # TODO: configurable base color + color = HSV.new(190, 100, 100) + {0, %State{first_render: true, paused: false, tick: 0, color: color, leds: leds, hits: %{}}} + end + + @impl true + def render(%{first_render: true} = state, _config) do + %{color: color, leds: leds} = state + + colors = Enum.map(leds, &{&1.id, color}) + + {colors, :never, %{state | first_render: false, paused: true}} + end + + def render(%{paused: true} = state, _config), + do: {[], :never, state} + + def render(state, config) do + %{tick: tick, color: color, leds: leds, hits: hits} = state + %{speed: _speed, distance: distance} = config + + {colors, hits} = + Enum.map_reduce(leds, hits, fn led, hits -> + case hits do + %{^led => {hit_tick, direction_modifier}} -> + # TODO: take speed into account + if tick - hit_tick >= distance do + {{led.id, color}, Map.delete(hits, led)} + else + hue = mod(color.h + (tick - hit_tick - distance) * direction_modifier, 360) + {{led.id, HSV.new(hue, color.s, color.v)}, hits} + end + + _else -> + {{led.id, color}, hits} + end + end) + + # FIXME: leaves color 1 away from base + # TODO: we can optimize this by rewriting the above instead of filtering here: + colors = + Enum.filter(colors, fn {_id, this_color} -> + this_color != color + end) + + {colors, @delay_ms, %{state | tick: tick + 1, hits: hits, paused: hits == %{}}} + end + + @impl true + def key_pressed(state, config, led) do + direction = direction_modifier(config.direction) + + render_in = + case state.paused do + true -> 0 + false -> :ignore + end + + {render_in, %{state | paused: false, hits: Map.put(state.hits, led, {state.tick, direction})}} + end + + defp direction_modifier(:random), do: Enum.random([-1, 1]) + defp direction_modifier(:negative), do: -1 + defp direction_modifier(:positive), do: 1 +end diff --git a/lib/rgb_matrix/effect/splash.ex b/lib/rgb_matrix/effect/splash.ex new file mode 100644 index 0000000..d01d419 --- /dev/null +++ b/lib/rgb_matrix/effect/splash.ex @@ -0,0 +1,94 @@ +defmodule RGBMatrix.Effect.Splash do + @moduledoc """ + Full gradient & value pulse away from key hits then fades value out. + """ + + alias Chameleon.HSV + alias RGBMatrix.Effect + + use Effect + + # import RGBMatrix.Utils, only: [mod: 2] + + defmodule Config do + use RGBMatrix.Effect.Config + end + + defmodule State do + defstruct [:tick, :leds, :hits] + end + + @delay_ms 17 + + @impl true + def new(leds, _config) do + {0, %State{tick: 0, leds: leds, hits: %{}}} + end + + @impl true + def render(state, _config) do + %{tick: tick, leds: leds, hits: hits} = state + + {colors, hits} = + Enum.map_reduce(leds, hits, fn led, hits -> + color = HSV.new(0, 100, 0) + + {hits, color} = + Enum.reduce(hits, {hits, color}, fn {hit_led, hit_tick}, {hits, color} -> + dx = led.x - hit_led.x + dy = led.y - hit_led.y + dist = :math.sqrt(dx * dx + dy * dy) + color = effect(color, dist, tick - hit_tick) + + {hits, color} + end) + + {{led.id, color}, hits} + end) + + # for (uint8_t i = led_min; i < led_max; i++) { + # RGB_MATRIX_TEST_LED_FLAGS(); + # HSV hsv = rgb_matrix_config.hsv; + # hsv.v = 0; + # for (uint8_t j = start; j < count; j++) { + # int16_t dx = g_led_config.point[i].x - g_last_hit_tracker.x[j]; + # int16_t dy = g_led_config.point[i].y - g_last_hit_tracker.y[j]; + # uint8_t dist = sqrt16(dx * dx + dy * dy); + # uint16_t tick = scale16by8(g_last_hit_tracker.tick[j], rgb_matrix_config.speed); + # hsv = effect_func(hsv, dx, dy, dist, tick); + # } + # hsv.v = scale8(hsv.v, rgb_matrix_config.hsv.v); + # RGB rgb = hsv_to_rgb(hsv); + # rgb_matrix_set_color(i, rgb.r, rgb.g, rgb.b); + # } + + {colors, @delay_ms, %{state | tick: tick + 1, hits: hits}} + end + + def effect(color, dist, hit_tick) do + # uint16_t effect = tick - dist; + # if (effect > 255) effect = 255; + # hsv.h += effect; + # hsv.v = qadd8(hsv.v, 255 - effect); + # return hsv; + + # effect = trunc(if effect > 360, do: 360, else: effect) + + value = 100 - hit_tick - trunc(dist * 20) + value = if value < 0, do: 0, else: value + + %{color | h: 0, v: value} + + # if dist < 5 do + # HSV.new(0, 100, 100) + # else + # color + # end + end + + @impl true + def key_pressed(state, _config, led) do + # {:ignore, %{state | hits: Map.put(state.hits, led, state.tick)}} + {:ignore, %{state | hits: %{led => state.tick}}} + end +end diff --git a/lib/rgb_matrix/engine.ex b/lib/rgb_matrix/engine.ex index 542e3bc..f192e28 100644 --- a/lib/rgb_matrix/engine.ex +++ b/lib/rgb_matrix/engine.ex @@ -1,17 +1,19 @@ +require Logger + defmodule RGBMatrix.Engine do @moduledoc """ - Renders [`Animation`](`RGBMatrix.Animation`)s and outputs - [`Frame`](`RGBMatrix.Frame`)s to be displayed. + Renders [`Effect`](`RGBMatrix.Effect`)s and outputs colors to be displayed by + [`Paintable`](`RGBMatrix.Paintable`)s. """ use GenServer alias Layout.LED - alias RGBMatrix.Animation + alias RGBMatrix.Effect defmodule State do @moduledoc false - defstruct [:leds, :animation, :paintables] + defstruct [:leds, :effect, :paintables, :last_frame, :timer] end # Client @@ -24,35 +26,26 @@ defmodule RGBMatrix.Engine do This function accepts the following arguments as a tuple: - `leds` - The list of LEDs to be painted on. - - `initial_animation` - The animation that plays when the engine starts. - - `paintables` - A list of modules to output `RGBMatrix.Frame` to that implement - the `RGBMatrix.Paintable` behavior. If you want to register your paintables + - `initial_effect` - The Effect type to initialize and play when the engine + starts. + - `paintables` - A list of modules to output colors to that implement the + `RGBMatrix.Paintable` behavior. If you want to register your paintables dynamically, set this to an empty list `[]`. """ @spec start_link( - {leds :: [LED.t()], initial_animation :: Animation.t(), paintables :: list(module)} + {leds :: [LED.t()], initial_effect_type :: Effect.type(), paintables :: list(module)} ) :: GenServer.on_start() - def start_link({leds, initial_animation, paintables}) do - GenServer.start_link(__MODULE__, {leds, initial_animation, paintables}, name: __MODULE__) + def start_link({leds, initial_effect_type, paintables}) do + GenServer.start_link(__MODULE__, {leds, initial_effect_type, paintables}, name: __MODULE__) end @doc """ - Play the given animation. - - Note that the animation can be played synchronously by passing `:false` for the `:async` option. However, only - looping (animations with `:loop` >= 1) animations may be played this way. This is to ensure that the caller is not - blocked forever. + Sets the given effect as the currently active effect. """ - @spec play_animation(animation :: Animation.t(), opts :: keyword()) :: :ok - def play_animation(animation, opts \\ []) do - async? = Keyword.get(opts, :async, true) - - if async? do - GenServer.cast(__MODULE__, {:play_animation, animation}) - else - GenServer.call(__MODULE__, {:play_animation, animation}) - end + @spec set_effect(effect_type :: Effect.type(), opts :: keyword()) :: :ok + def set_effect(effect_type) do + GenServer.cast(__MODULE__, {:set_effect, effect_type}) end @doc """ @@ -76,16 +69,17 @@ defmodule RGBMatrix.Engine do # Server @impl GenServer - def init({leds, initial_animation, paintables}) do - send(self(), :get_next_frame) + def init({leds, initial_effect_type, paintables}) do + black = Chameleon.HSV.new(0, 0, 0) + frame = Map.new(leds, &{&1.id, black}) - initial_state = %State{leds: leds, paintables: %{}} + initial_state = %State{leds: leds, last_frame: frame, paintables: %{}} state = Enum.reduce(paintables, initial_state, fn paintable, state -> add_paintable(paintable, state) end) - |> set_animation(initial_animation) + |> set_effect(initial_effect_type) {:ok, state} end @@ -100,64 +94,67 @@ defmodule RGBMatrix.Engine do %State{state | paintables: paintables} end - defp set_animation(state, animation) do - %State{state | animation: animation} - end + defp set_effect(state, effect_type) do + {render_in, effect} = Effect.new(effect_type, state.leds) - @impl GenServer - def handle_info(:get_next_frame, state) do - animation = Animation.next_frame(state.animation) + state = schedule_next_render(state, render_in) - state.paintables - |> Map.values() - |> Enum.each(fn paint_fn -> - paint_fn.(animation.next_frame) - end) + %State{state | effect: effect} + end - Process.send_after(self(), :get_next_frame, animation.delay_ms) - {:noreply, set_animation(state, animation)} + defp schedule_next_render(state, :ignore) do + state end - @impl GenServer - def handle_info({:reset_animation, reset_animation}, state) do - {:noreply, set_animation(state, reset_animation)} + defp schedule_next_render(state, :never) do + cancel_timer(state) end - @impl GenServer - def handle_info({:reply, from, reset_animation}, state) do - GenServer.reply(from, :ok) + defp schedule_next_render(state, 0) do + send(self(), :render) + cancel_timer(state) + end - {:noreply, set_animation(state, reset_animation)} + defp schedule_next_render(state, ms) when is_integer(ms) and ms > 0 do + state = cancel_timer(state) + %{state | timer: Process.send_after(self(), :render, ms)} end - @impl GenServer - def handle_cast({:play_animation, %{loop: loop} = animation}, state) - when is_integer(loop) and loop >= 1 do - current_animation = state.animation - expected_duration = Animation.duration(animation) - Process.send_after(self(), {:reset_animation, current_animation}, expected_duration) + defp cancel_timer(%{timer: nil} = state), do: state - {:noreply, set_animation(state, animation)} + defp cancel_timer(state) do + Process.cancel_timer(state.timer) + %{state | timer: nil} end - @impl GenServer - def handle_cast({:play_animation, %{loop: 0} = _animation}, state) do + @impl true + def handle_info(:render, state) do + {new_colors, render_in, effect} = Effect.render(state.effect) + + frame = update_frame(state.last_frame, new_colors) + + state.paintables + |> Map.values() + |> Enum.each(fn paint_fn -> + paint_fn.(frame) + end) + + state = schedule_next_render(state, render_in) + state = %State{state | effect: effect, last_frame: frame} + {:noreply, state} end - @impl GenServer - def handle_cast({:play_animation, animation}, state) do - {:noreply, set_animation(state, animation)} + defp update_frame(frame, new_colors) do + Enum.reduce(new_colors, frame, fn {led_id, color}, frame -> + Map.put(frame, led_id, color) + end) end @impl GenServer - def handle_call({:play_animation, %{loop: loop} = animation}, from, state) - when is_integer(loop) and loop >= 1 do - current_animation = state.animation - duration = Animation.duration(animation) - Process.send_after(self(), {:reply, from, current_animation}, duration) - - {:noreply, set_animation(state, animation)} + def handle_cast({:set_effect, effect_type}, state) do + state = set_effect(state, effect_type) + {:noreply, state} end @impl GenServer diff --git a/lib/rgb_matrix/frame.ex b/lib/rgb_matrix/frame.ex deleted file mode 100644 index 5fcf988..0000000 --- a/lib/rgb_matrix/frame.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule RGBMatrix.Frame do - @moduledoc """ - Provides a data structure and functions for working with animation frames. - - An animation frame is a mapping of pixel coordinates to their corresponding color. An animation can be composed of a - list of frames or each frame can be dynamically generated based on the tick of some animation. - """ - - alias RGBMatrix.Pixel - - @type pixel_map :: %{required(Pixel.t()) => Pixel.color()} - - @type t :: %__MODULE__{ - pixel_map: pixel_map() - } - - defstruct [:pixel_map] - - @spec new(pixels :: list(Pixel.t()), pixel_colors :: list(Pixel.color())) :: - t() - def new(pixels, pixel_colors) do - pixel_map = - Enum.zip(pixels, pixel_colors) - |> Enum.into(%{}) - - %__MODULE__{pixel_map: pixel_map} - end - - @spec solid_color(pixels :: list(Pixel.t()), color :: Pixel.color()) :: t() - def solid_color(pixels, color) do - pixel_colors = List.duplicate(color, length(pixels)) - new(pixels, pixel_colors) - end -end diff --git a/lib/rgb_matrix/paintable.ex b/lib/rgb_matrix/paintable.ex index ad58a3d..12b6e01 100644 --- a/lib/rgb_matrix/paintable.ex +++ b/lib/rgb_matrix/paintable.ex @@ -1,14 +1,18 @@ defmodule RGBMatrix.Paintable do @moduledoc """ - A paintable module controls physical pixels. + A paintable module controls physical LEDs. """ + alias Layout.LED + + @type frame :: %{required(LED.id()) => RGBMatrix.any_color_model()} + @doc """ - Returns a function that can be called to paint the pixels for a given frame. + Returns a function that can be called to paint the LEDs for a given frame. The anonymous function's return value is unused. This callback makes any hardware implementation details opaque to the caller, - while allowing the paintable to retain control of the physical pixels. + while allowing the paintable to retain control of the physical LEDs. """ - @callback get_paint_fn :: (frame :: RGBMatrix.Frame.t() -> any) + @callback get_paint_fn :: (frame -> any) end diff --git a/lib/rgb_matrix/pixel.ex b/lib/rgb_matrix/pixel.ex deleted file mode 100644 index daa912a..0000000 --- a/lib/rgb_matrix/pixel.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule RGBMatrix.Pixel do - @moduledoc """ - A pixel is a unit that has X and Y coordinates and displays a single color. - """ - - @typedoc """ - A tuple containing the X and Y coordinates of the pixel. - """ - @type t :: {x :: non_neg_integer, y :: non_neg_integer} - - @typedoc """ - The color of the pixel, represented as a `Chameleon.Color` color model. - """ - @type color :: any_color_model - - @typedoc """ - Shorthand for any `Chameleon.Color` color model. - """ - @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() -end diff --git a/lib/xebow/application.ex b/lib/xebow/application.ex index 962189d..08f8416 100644 --- a/lib/xebow/application.ex +++ b/lib/xebow/application.ex @@ -6,8 +6,7 @@ defmodule Xebow.Application do use Application @leds Xebow.layout() |> Layout.leds() - @animation_type RGBMatrix.Animation.types() |> List.first() - @animation RGBMatrix.Animation.new(type: @animation_type) + @effect_type RGBMatrix.Effect.types() |> List.first() def start(_type, _args) do # See https://hexdocs.pm/elixir/Supervisor.html @@ -40,7 +39,7 @@ defmodule Xebow.Application do # {Xebow.Worker, arg}, Xebow.HIDGadget, Xebow.LEDs, - {RGBMatrix.Engine, {@leds, @animation, [Xebow.LEDs]}}, + {RGBMatrix.Engine, {@leds, @effect_type, [Xebow.LEDs]}}, Xebow.Keyboard ] end diff --git a/lib/xebow/keyboard.ex b/lib/xebow/keyboard.ex index 189d964..347e004 100644 --- a/lib/xebow/keyboard.ex +++ b/lib/xebow/keyboard.ex @@ -17,7 +17,7 @@ defmodule Xebow.Keyboard do use GenServer alias Circuits.GPIO - alias RGBMatrix.{Animation, Frame} + alias RGBMatrix.Effect # maps the physical GPIO pins to key IDs # TODO: re-number these keys so they map to the keyboard in X/Y natural order, @@ -74,7 +74,7 @@ defmodule Xebow.Keyboard do # Layer 2: %{ k001: AFK.Keycode.MFA.new({__MODULE__, :flash, ["red"]}), - k002: AFK.Keycode.MFA.new({__MODULE__, :previous_animation, []}), + k002: AFK.Keycode.MFA.new({__MODULE__, :previous_effect, []}), k003: AFK.Keycode.Transparent.new(), k004: AFK.Keycode.Transparent.new(), k005: AFK.Keycode.Transparent.new(), @@ -82,7 +82,7 @@ defmodule Xebow.Keyboard do k007: AFK.Keycode.Transparent.new(), k008: AFK.Keycode.Transparent.new(), k009: AFK.Keycode.MFA.new({__MODULE__, :flash, ["green"]}), - k010: AFK.Keycode.MFA.new({__MODULE__, :next_animation, []}), + k010: AFK.Keycode.MFA.new({__MODULE__, :next_effect, []}), k011: AFK.Keycode.Transparent.new(), k012: AFK.Keycode.Transparent.new() } @@ -96,19 +96,19 @@ defmodule Xebow.Keyboard do end @doc """ - Cycle to the next animation + Cycle to the next effect """ - @spec next_animation() :: :ok - def next_animation do - GenServer.cast(__MODULE__, :next_animation) + @spec next_effect() :: :ok + def next_effect do + GenServer.cast(__MODULE__, :next_effect) end @doc """ - Cycle to the previous animation + Cycle to the previous effect """ - @spec previous_animation() :: :ok - def previous_animation do - GenServer.cast(__MODULE__, :previous_animation) + @spec previous_effect() :: :ok + def previous_effect do + GenServer.cast(__MODULE__, :previous_effect) end # Server @@ -136,54 +136,51 @@ defmodule Xebow.Keyboard do poll_timer_ms = 15 :timer.send_interval(poll_timer_ms, self(), :update_pin_values) - animations = - Animation.types() - |> Enum.map(&Animation.new(type: &1)) - - {:ok, - %{ - pins: pins, - keyboard_state: keyboard_state, - hid: hid, - animations: animations, - current_animation_index: 0 - }} + state = %{ + pins: pins, + keyboard_state: keyboard_state, + hid: hid, + effect_types: Effect.types(), + current_effect_index: 0 + } + + {:ok, state} end @impl GenServer - def handle_cast(:next_animation, state) do - next_index = state.current_animation_index + 1 + def handle_cast(:next_effect, state) do + next_index = state.current_effect_index + 1 next_index = - case next_index < Enum.count(state.animations) do + case next_index < Enum.count(state.effect_types) do true -> next_index _ -> 0 end - animation = Enum.at(state.animations, next_index) + effect_type = Enum.at(state.effect_types, next_index) - RGBMatrix.Engine.play_animation(animation) + RGBMatrix.Engine.set_effect(effect_type) - state = %{state | current_animation_index: next_index} + state = %{state | current_effect_index: next_index} {:noreply, state} end @impl GenServer - def handle_cast(:previous_animation, state) do - previous_index = state.current_animation_index - 1 + def handle_cast(:previous_effect, state) do + previous_index = state.current_effect_index - 1 previous_index = case previous_index < 0 do - true -> Enum.count(state.animations) - 1 + true -> Enum.count(state.effect_types) - 1 _ -> previous_index end - animation = Enum.at(state.animations, previous_index) + effect_type = Enum.at(state.effect_types, previous_index) - RGBMatrix.Engine.play_animation(animation) + RGBMatrix.Engine.set_effect(effect_type) - state = %{state | current_animation_index: previous_index} + state = %{state | current_effect_index: previous_index} {:noreply, state} end @@ -228,12 +225,6 @@ defmodule Xebow.Keyboard do # Custom Key Functions def flash(color) do - pixels = Xebow.Utils.pixels() - color = Chameleon.Keyword.new(color) - frame = Frame.solid_color(pixels, color) - - animation = Animation.new(type: Animation.Static, frames: [frame], delay_ms: 250, loop: 1) - - RGBMatrix.Engine.play_animation(animation, async: false) + Logger.info("TODO: flash color #{IO.inspect(color)}") end end diff --git a/lib/xebow/leds.ex b/lib/xebow/leds.ex index 3c19e60..d078e83 100644 --- a/lib/xebow/leds.ex +++ b/lib/xebow/leds.ex @@ -1,3 +1,5 @@ +require Logger + defmodule Xebow.LEDs do @moduledoc """ GenServer that interacts with the SPI device that controls the RGB LEDs on the @@ -22,6 +24,22 @@ defmodule Xebow.LEDs do @spi_speed_hz 4_000_000 @sof <<0, 0, 0, 0>> @eof <<255, 255, 255, 255>> + # This is the hardware order that the LED colors need to be sent to the SPI + # device in. The LED IDs are the ones from `Xebow.layout/0`. + @spi_led_order [ + :l001, + :l004, + :l007, + :l010, + :l002, + :l005, + :l008, + :l011, + :l003, + :l006, + :l009, + :l012 + ] # Client @@ -56,9 +74,8 @@ defmodule Xebow.LEDs do defp paint(spidev, frame) do colors = - frame.pixel_map - |> Enum.sort() - |> Enum.map(fn {_cord, color} -> color end) + @spi_led_order + |> Enum.map(&Map.fetch!(frame, &1)) data = Enum.reduce(colors, @sof, fn color, acc -> diff --git a/lib/xebow/utils.ex b/lib/xebow/utils.ex deleted file mode 100644 index ea34253..0000000 --- a/lib/xebow/utils.ex +++ /dev/null @@ -1,42 +0,0 @@ -defmodule Xebow.Utils do - @moduledoc """ - Shared utility functions that are generally useful. - """ - - @doc """ - Modulo operation that supports negative numbers. - - This is effectively `mod` as it exists in most other languages. Elixir's `rem` - doesn't act the same as other languages for negative numbers. - """ - @spec mod(integer, integer) :: non_neg_integer - def mod(number, modulus) when is_integer(number) and is_integer(modulus) do - case rem(number, modulus) do - remainder when (remainder > 0 and modulus < 0) or (remainder < 0 and modulus > 0) -> - remainder + modulus - - remainder -> - remainder - end - end - - # pixels on the xebow start in upper left corner and count down instead of - # across - @pixels [ - {0, 0}, - {0, 1}, - {0, 2}, - {0, 3}, - {1, 0}, - {1, 1}, - {1, 2}, - {1, 3}, - {2, 0}, - {2, 1}, - {2, 2}, - {2, 3} - ] - - @spec pixels() :: list(RGBMatrix.Pixel.t()) - def pixels, do: @pixels -end