Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animations refactor #54

Merged
merged 8 commits into from
Jul 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Used by "mix format"
[
inputs: [
"{mix,.formatter}.exs",
"{config,lib,test}/**/*.{ex,exs}",
"dialyzer.ignore.exs"
inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"],
locals_without_parens: [
field: 3
]
]
53 changes: 53 additions & 0 deletions lib/layout.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule Layout do
@moduledoc """
Describes a keyboard layout.
"""
doughsay marked this conversation as resolved.
Show resolved Hide resolved

alias __MODULE__.{Key, LED}

# FIXME: what's wrong with this type?
# @type t :: %__MODULE__{
# keys: [Key.t()],
# leds: [LED.t()],
# leds_by_keys: %{Key.id() => LED.t()},
# keys_by_leds: %{LED.id() => Key.t()}
# }
@type t :: %__MODULE__{}
defstruct [:keys, :leds, :leds_by_keys, :keys_by_leds]

@spec new(keys :: [Key.t()], leds :: [LED.t()]) :: t
def new(keys, leds \\ []) do
doughsay marked this conversation as resolved.
Show resolved Hide resolved
leds_map = Map.new(leds, &{&1.id, &1})

leds_by_keys =
keys
|> Enum.filter(& &1.led)
|> Map.new(&{&1.id, Map.fetch!(leds_map, &1.led)})

keys_by_leds =
keys
|> Enum.filter(& &1.led)
|> Map.new(&{&1.led, &1})

%__MODULE__{
keys: keys,
leds: leds,
leds_by_keys: leds_by_keys,
keys_by_leds: keys_by_leds
}
end

@spec keys(layout :: t) :: [Key.t()]
def keys(layout), do: layout.keys

@spec leds(layout :: t) :: [LED.t()]
def leds(layout), do: layout.leds

@spec led_for_key(layout :: t, Key.id()) :: LED.t() | nil
def led_for_key(%__MODULE__{} = layout, key_id) when is_atom(key_id),
do: Map.get(layout.leds_by_keys, key_id)

@spec key_for_led(layout :: t, LED.id()) :: Key.t() | nil
def key_for_led(%__MODULE__{} = layout, led_id) when is_atom(led_id),
do: Map.get(layout.keys_by_leds, led_id)
end
28 changes: 28 additions & 0 deletions lib/layout/key.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule Layout.Key do
@moduledoc """
Describes a physical key and its location.
"""
doughsay marked this conversation as resolved.
Show resolved Hide resolved

@type id :: atom

@type t :: %__MODULE__{
id: id,
x: float,
y: float,
width: float,
height: float,
doughsay marked this conversation as resolved.
Show resolved Hide resolved
led: atom
doughsay marked this conversation as resolved.
Show resolved Hide resolved
}
defstruct [:id, :x, :y, :width, :height, :led]

def new(id, x, y, opts \\ []) do
%__MODULE__{
id: id,
x: x,
y: y,
width: Keyword.get(opts, :width, 1),
height: Keyword.get(opts, :height, 1),
led: Keyword.get(opts, :led)
}
end
end
22 changes: 22 additions & 0 deletions lib/layout/led.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule Layout.LED do
@moduledoc """
Describes a physical LED location.
"""
doughsay marked this conversation as resolved.
Show resolved Hide resolved

@type id :: atom

@type t :: %__MODULE__{
id: id,
doughsay marked this conversation as resolved.
Show resolved Hide resolved
x: float,
y: float
}
defstruct [:id, :x, :y]

def new(id, x, y) do
%__MODULE__{
id: id,
x: x,
y: y
}
end
end
10 changes: 10 additions & 0 deletions lib/rgb_matrix.ex
Original file line number Diff line number Diff line change
@@ -0,0 +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()
Comment on lines +2 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ (optional)

I wonder if the color model is actually a property of LED. 🤔 Not sure yet.

end
129 changes: 58 additions & 71 deletions lib/rgb_matrix/animation.ex
Original file line number Diff line number Diff line change
@@ -1,109 +1,96 @@
defmodule RGBMatrix.Animation do
@moduledoc """
Provides a data structure and functions to define an RGBMatrix animation.
Provides the behaviour and interface for working with animations.
"""

There are currently two distinct ways to define an animation.
alias Layout.LED
alias RGBMatrix.Animation.Config

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.
@type animation_state :: any

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.
"""
@type t :: %__MODULE__{
type: type,
config: Config.t(),
state: any
}
defstruct [:type, :config, :state]

alias __MODULE__
alias RGBMatrix.Frame
@callback new(leds :: [LED.t()], config :: Config.t()) :: {render_in, animation_state}
@callback render(state :: animation_state, config :: Config.t()) ::
{render_in, [RGBMatrix.any_color_model()], animation_state}
@callback interact(state :: animation_state, config :: Config.t(), led :: LED.t()) ::
{render_in, animation_state}

defmacro __using__(_) do
quote do
alias RGBMatrix.Animation

@behaviour Animation
@behaviour RGBMatrix.Animation
end
end

@callback next_frame(animation :: Animation.t()) :: Frame.t()
@type render_in :: non_neg_integer() | :never | :ignore

@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 ::
@type type ::
__MODULE__.CycleAll
| __MODULE__.CycleLeftToRight
| __MODULE__.HueWave
| __MODULE__.Pinwheel
| __MODULE__.Static
| __MODULE__.RandomSolid
| __MODULE__.RandomKeypresses
| __MODULE__.SolidColor
| __MODULE__.Breathing
| __MODULE__.SolidReactive

@doc """
Returns a list of the available types of animations.
"""
@spec types :: list(animation_type)
@spec types :: [type]
def types do
[
__MODULE__.CycleAll,
__MODULE__.CycleLeftToRight,
__MODULE__.Pinwheel
__MODULE__.HueWave,
__MODULE__.Pinwheel,
__MODULE__.RandomSolid,
__MODULE__.RandomKeypresses,
__MODULE__.SolidColor,
__MODULE__.Breathing,
__MODULE__.SolidReactive
]
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, [])
@doc """
Returns an animation's initial state.
"""
@spec new(animation_type :: type, leds :: [LED.t()]) :: {render_in, t}
def new(animation_type, leds) do
doughsay marked this conversation as resolved.
Show resolved Hide resolved
config_module = Module.concat([animation_type, Config])
animation_config = config_module.new()
{render_in, animation_state} = animation_type.new(leds, animation_config)

%Animation{
animation = %__MODULE__{
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)
config: animation_config,
state: animation_state
}
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}
{render_in, animation}
end

@doc """
Returns the frame count of a given animation,

Note: this function returns :infinite for dynamic animations.
Returns the next state of an animation based on its current state.
"""
@spec frame_count(animation :: Animation.t()) :: non_neg_integer | :infinite
def frame_count(%{loop: :infinite}), do: :infinite
@spec render(animation :: t) :: {render_in, [RGBMatrix.any_color_model()], t}
def render(animation) do
{render_in, colors, animation_state} =
animation.type.render(animation.state, animation.config)

def frame_count(animation), do: length(animation.frames) * animation.loop
{render_in, colors, %{animation | state: animation_state}}
end

@doc """
Returns the expected duration of a given animation.

Note: this function returns :infinite for dynamic animations.
Sends an interaction event to an animation.
"""
@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
@spec interact(animation :: t, led :: LED.t()) :: {render_in, t}
def interact(animation, led) do
{render_in, animation_state} = animation.type.interact(animation.state, animation.config, led)
{render_in, %{animation | state: animation_state}}
end
end
47 changes: 47 additions & 0 deletions lib/rgb_matrix/animation/breathing.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
defmodule RGBMatrix.Animation.Breathing do
@moduledoc """
Single hue brightness cycling.
"""

alias Chameleon.HSV
alias RGBMatrix.Animation

use Animation

defmodule Config do
@moduledoc false
use RGBMatrix.Animation.Config
end

defmodule State do
@moduledoc false
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)

{@delay_ms, colors, %{state | tick: tick + 1}}
end

@impl true
def interact(state, _config, _led) do
{:ignore, state}
end
doughsay marked this conversation as resolved.
Show resolved Hide resolved
end
Loading