From ef55ba2c3e8ecd822abe1ab82f137d5e0aee4c8c Mon Sep 17 00:00:00 2001 From: maze Date: Fri, 1 Dec 2023 13:07:37 +0100 Subject: [PATCH] better docs for Color.Names better guards around Color.Names better color type naming better color conversion to rgb and colorint fixing a lot of specs simplifying some functions as a consequence fixing some credo issues --- TODO.md | 14 ++++- lib/fledex/color/functions.ex | 3 +- lib/fledex/color/names.ex | 100 +++++++++++++++++++------------- lib/fledex/color/types.ex | 6 +- lib/fledex/color/utils.ex | 31 +++++----- lib/fledex/leds.ex | 71 +++++++++++------------ lib/fledex/leds_driver.ex | 3 +- test/color/color_test.exs | 2 + test/color/color_utils_test.exs | 7 ++- test/leds_test.exs | 5 +- 10 files changed, 136 insertions(+), 106 deletions(-) diff --git a/TODO.md b/TODO.md index 413da92..da7ecf0 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,5 @@ # Intro -Even though this library is published, there are things I still want to do before I consider this -as really done. Here the outstanding tasks that I can think of. +Even though this library is published, there are things I still want to do before I consider this as really done. Here the outstanding tasks that I can think of. # Tasks - [ ] Documentation @@ -15,7 +14,13 @@ as really done. Here the outstanding tasks that I can think of. - [ ] Fledex.Animation.LedAnimator - [ ] Fledex.LedsDriver - [ ] Add type specs (at least for the most important modules) (v0.3) - - [ ] Improve hexdocs + - [ ] Fledex + - [x] Fledex.Leds + - [x] Fledex.Color.Names + - [ ] Fledex.Animation.BaseAnimator + - [ ] Fledex.Animation.LedAnimationManager + - [ ] Fledex.Animation.LedAnimator + - [ ] Fledex.LedsDriver - [ ] Improve hexdocs - [x] add livebooks (v0.3) - [ ] create cheatsheet (v0.3) - [ ] Add documentation on how to connect the LED strip to a RaspberryPi Zero (with and without level shifter).This could be part of the first example (v0.3 & v0.4) @@ -35,6 +40,9 @@ as really done. Here the outstanding tasks that I can think of. - [ ] see the project plan that was planned out with my son, we are not quite there yet - [ ] Connect everything into a supervision tree (to make it more robust) (v0.4) - [ ] Use protocols + - [ ] Drivers + - [x] Conversions `to_rgb`, `to_colorint` <-- decided against it to allow having simple structures (tuple) instead of (module)structs. Protocols don't seem to work with those. + - [ ] ??? animations & components? - [ ] LED-component library - [ ] Create foundation for a led-component-library that enables defining reusable led components. For example both the clock as well as the weather example have a scale it would be easy to define those as components that would make it easier to defining certain aspects (v0.5) - [ ] Increase consumption diff --git a/lib/fledex/color/functions.ex b/lib/fledex/color/functions.ex index e0c3f2d..b2702b8 100644 --- a/lib/fledex/color/functions.ex +++ b/lib/fledex/color/functions.ex @@ -27,8 +27,7 @@ defmodule Fledex.Color.Functions do |> hsv2rgb() end - @spec create_gradient_rgb(pos_integer, Types.rgb, Types.rgb) :: - list(Types.rgb) + @spec create_gradient_rgb(pos_integer, Types.rgb, Types.rgb) :: list(Types.rgb) def create_gradient_rgb(num_leds, {sr, sg, sb} = _start_color, {er, eg, eb} = _end_color) when num_leds > 0 do rdist87 = (er - sr) <<< 7 gdist87 = (eg - sg) <<< 7 diff --git a/lib/fledex/color/names.ex b/lib/fledex/color/names.ex index a77d65e..5025872 100644 --- a/lib/fledex/color/names.ex +++ b/lib/fledex/color/names.ex @@ -21,13 +21,35 @@ defmodule Fledex.Color.Names do """ @external_resource Fledex.Color.LoadUtils.names_file() - import Fledex.Color.Types - alias Fledex.Color.LoadUtils + alias Fledex.Color.Types + + colors = LoadUtils.load_color_file(@external_resource) - @type t :: %{ + @colors colors + @color_names Enum.map(@colors, fn %{name: name} = _colorinfo -> name end) + @typedoc """ + The allowed color names + """ + @type color_names_t :: unquote( + @color_names + |> Enum.map_join(" | ", &inspect/1) + |> Code.string_to_quoted!() + ) + @typedoc """ + The different properties that can be interrogated from a named color + """ + @type color_props_t :: (:all|:index|:name|:decriptive_name|:hex|:rgb|:hsl|:hsv|:spource) + @typedoc """ + The different values that can be returned when interrogating for some named color properties + """ + @type color_vals_t :: Types.color_any() | color_struct_t | String.t + @typedoc """ + The structure of a named color with all it's attributes. + """ + @type color_struct_t :: %{ index: integer, - name: atom, + name: color_names_t, descriptive_name: String.t, hex: Types.colorint, rgb: Types.rgb, @@ -36,39 +58,12 @@ defmodule Fledex.Color.Names do source: String.t } - colors = LoadUtils.load_color_file(@external_resource) - for color <- colors do - name = color.name - @doc """ - See the module docs for `Fledex.Color.Names` for more info - """ - @doc color_name: true - @spec unquote(name)(atom) :: Types.colorint | Types.rgb | Types.hsv | Types.hsl | t - def unquote(name)(what \\ :hex) - def unquote(name)(:all), do: unquote(Macro.escape(color)) - def unquote(name)(:index), do: unquote(Macro.escape(color)).index - # def unquote(name)(:name), do: unquote(Macro.escape(color)).name - def unquote(name)(:rgb), do: unquote(Macro.escape(color)).rgb - def unquote(name)(:hex), do: unquote(Macro.escape(color)).hex - def unquote(name)(:hsv), do: unquote(Macro.escape(color)).hsv - def unquote(name)(:hsl), do: unquote(Macro.escape(color)).hsl - def unquote(name)(:descriptive_name), do: unquote(Macro.escape(color)).descriptive_name - def unquote(name)(:source), do: unquote(Macro.escape(color)).source - end - - @colors colors - @color_names Enum.map(@colors, fn %{name: name} = _colorinfo -> name end) - - quote do - @type t :: unquote_splicing(@color_names) - end - defguard is_color_name(atom) when atom in @color_names @doc """ Get all the data about the predefined colors """ - @spec colors :: list(t) + @spec colors :: list(color_struct_t) def colors do @colors end @@ -76,17 +71,42 @@ defmodule Fledex.Color.Names do @doc """ Get a list of all the predefined color (atom) names. The name can be used to either retrieve the info by calling `info/2` or by calling the function with that - name (see also the [example livebook](3b_fledex_more_about_colors.livemd)) + name (see also the __Color Names__ section nad the [example livebook](3b_fledex_more_about_colors.livemd)) """ - @spec names :: list(atom) - def names do - @color_names - end + @spec names :: list(color_names_t) + def names, do: @color_names @doc """ Retrieve information about the color with the given name """ - @spec info(name :: atom, what :: atom) :: Types.colorint | Types.rgb | Types.hsv | Types.hsl | t - def info(name, what \\ :hex) do - apply(__MODULE__, name, [what]) + @spec info(name :: color_names_t, what :: color_props_t) :: color_vals_t + def info(name, what \\ :hex) + def info(name, what) when is_color_name(name), do: apply(__MODULE__, name, [what]) + def info(_name, _what), do: nil + + @base16 16 + for color <- colors do + name = color.name + {r, g, b} = color.rgb + hex = color.hex + |> Integer.to_string(@base16) + |> String.pad_leading(6, "0") + + @doc """ +
+ + Defines the color rgb(#{r}, #{g}, #{b}). + """ + @doc color_name: true + @spec unquote(name)(color_props_t) :: color_vals_t + def unquote(name)(what \\ :hex) + def unquote(name)(:all), do: unquote(Macro.escape(color)) + def unquote(name)(:index), do: unquote(Macro.escape(color)).index + def unquote(name)(:name), do: unquote(Macro.escape(color)).name + def unquote(name)(:rgb), do: unquote(Macro.escape(color)).rgb + def unquote(name)(:hex), do: unquote(Macro.escape(color)).hex + def unquote(name)(:hsv), do: unquote(Macro.escape(color)).hsv + def unquote(name)(:hsl), do: unquote(Macro.escape(color)).hsl + def unquote(name)(:descriptive_name), do: unquote(Macro.escape(color)).descriptive_name + def unquote(name)(:source), do: unquote(Macro.escape(color)).source end end diff --git a/lib/fledex/color/types.ex b/lib/fledex/color/types.ex index 7737bc0..f6dd46b 100644 --- a/lib/fledex/color/types.ex +++ b/lib/fledex/color/types.ex @@ -2,6 +2,8 @@ defmodule Fledex.Color.Types do @type rgb :: {red :: 0..255, green :: 0..255, blue :: 0..255} @type hsv :: {hue :: 0..255, saturation :: 0..255, value :: 0..255} @type hsl :: {hue :: 0..255, saturation :: 0..255, light :: 0..255} - @type colorint :: 0..0xFFFFFF - @type color :: rgb | colorint | atom | Fledex.Color.Names.t + @type colorint :: 0..0xffffff + + @type color_any :: color() | hsv() | hsl() + @type color :: rgb | colorint | Fledex.Color.Names.color_names_t end diff --git a/lib/fledex/color/utils.ex b/lib/fledex/color/utils.ex index 9d8487f..41f246d 100644 --- a/lib/fledex/color/utils.ex +++ b/lib/fledex/color/utils.ex @@ -6,6 +6,8 @@ defmodule Fledex.Color.Utils do """ import Bitwise + require Fledex.Color.Names + alias Fledex.Color.Names alias Fledex.Color.Types @@ -107,26 +109,19 @@ defmodule Fledex.Color.Utils do end @doc """ - This function combines the subpixels to a single (color) integer value + This function converts a color to a single (color) integer value """ - # TODO: make more generic to accept more color types (define a protocol) - @spec to_colorint(Types.rgb) :: Types.colorint - def to_colorint({r, g, b}) do - (r<<<16) + (g<<<8) + b - end + @spec to_colorint(Types.color) :: Types.colorint + def to_colorint({r, g, b} = _color), do: (r<<<16) + (g<<<8) + b + def to_colorint(color) when is_integer(color), do: color + def to_colorint(color) when Names.is_color_name(color), do: apply(Names, color, [:hex]) @doc """ - This function splits a single (color) integer value into it's rgb components + This function splits a color into it's rgb components """ - # TODO: make more generic to accept more color types (define a protocol) - @spec to_rgb((Types.colorint | atom | Types.rgb | %{rgb: Types.rgb} | %{rgb: Types.colorint})) :: Types.rgb - def to_rgb(rgb) do - case rgb do - %{rgb: {r, g, b}} -> {r, g, b} - %{rgb: x} when is_integer(x) -> to_rgb(x) - x when is_atom(x) -> apply(Names, x, [:rgb]) - x when is_integer(x) -> split_into_subpixels(x) - x -> x - end - end + @spec to_rgb((Types.color | %{rgb: Types.rgb} | %{rgb: Types.colorint})) :: Types.rgb + def to_rgb(%{rgb: x} = _color), do: to_rgb(x) + def to_rgb({r, g, b} = _color), do: {r, g, b} + def to_rgb(color) when Names.is_color_name(color), do: apply(Names, color, [:rgb]) + def to_rgb(color) when is_integer(color), do: split_into_subpixels(color) end diff --git a/lib/fledex/leds.ex b/lib/fledex/leds.ex index bea414f..0bd9145 100644 --- a/lib/fledex/leds.ex +++ b/lib/fledex/leds.ex @@ -15,7 +15,6 @@ defmodule Fledex.Leds do require Logger alias Fledex.Color.Functions - alias Fledex.Color.Names alias Fledex.Color.Types alias Fledex.Color.Utils alias Fledex.LedsDriver @@ -41,8 +40,9 @@ defmodule Fledex.Leds do defdelegate new(count, leds, opts, meta), to: Fledex.Leds, as: :leds @doc """ - Create a new led sequence of length 0. This is rarely useful. - Use the `leds/1` function instead (or change the count by using `set_count/2`) + Create a new led sequence of length 0. + + This is rarely useful. Use the `leds/1` function instead (or change the count by using `set_count/2`) """ @spec leds() :: t def leds do @@ -139,8 +139,10 @@ defmodule Fledex.Leds do count end @doc """ - This function allows to define the server_name and the namespace to which - the led sequence will be send to when the `send/2` function is called. + Define the server_name and the namespace + + This is used when the led sequence is sent to the `LedsDriver` when the + `send/2` function is called. """ @spec set_driver_info(t, namespace :: atom, server_name :: atom) :: t def set_driver_info(%{opts: opts} = leds, namespace, server_name \\ Fledex.LedsDriver) do @@ -148,7 +150,7 @@ defmodule Fledex.Leds do %__MODULE__{leds | opts: opts} end @doc """ - This function defines a rainbow over the leds. The options that can be specified are: + Defines a rainbow over the leds. The options that can be specified are: * `:num_leds`: how many leds should be part of the rainbow (by default all leds) * `:offset`: as from which led we want to start the rainbow (default: 0, no offset) @@ -191,7 +193,8 @@ defmodule Fledex.Leds do * `"num_leds`: Over how many leds the transition should happen. (default: all) * `:offset`: The offset where to start the gardient at (default: 0) """ - def gradient(leds, start_color, end_color, opts \\ []) do + @spec gradient(t, Types.color, Types.color, keyword) :: t + def gradient(%__MODULE__{} = leds, start_color, end_color, opts \\ []) do num_leds = opts[:num_leds] || leds.count offset = opts[:offset] || 0 @@ -239,7 +242,7 @@ defmodule Fledex.Leds do Note: it is possible to use a sub sequence of leds and they all will be added to the sequence. """ - @spec light(t, (Types.colorint | t | atom)) :: t + @spec light(t, Types.color | t) :: t def light(leds, rgb) do do_update(leds, rgb) end @@ -250,7 +253,7 @@ defmodule Fledex.Leds do then the led will be stored, but ignored (but see the description of `set_count/2`). The same note as for `light/2` applies. """ - @spec light(t, (Types.colorint | t | atom), pos_integer) :: t + @spec light(t, Types.color | t, pos_integer) :: t def light(leds, led, offset) when offset > 0 do do_update(leds, led, offset) end @@ -263,8 +266,8 @@ defmodule Fledex.Leds do THe `repeat` needs to be more than 1, otherwise it wouldn't make sense. In adition the same note as for `light/2` applies. """ - @spec light(t, (Types.colorint | t | atom), pos_integer, pos_integer) :: t - def light(leds, led, offset, repeat) when offset > 0 and repeat > 1 do + @spec light(t, Types.color | t, pos_integer, pos_integer) :: t + def light(%__MODULE__{} = leds, led, offset, repeat) when offset > 0 and repeat > 1 do # convert led to a LEDs struct led = case led do led when is_integer(led) -> __MODULE__.leds(1) |> __MODULE__.light(led) @@ -280,22 +283,13 @@ defmodule Fledex.Leds do raise ArgumentError, message: "the offset needs to be > 0 (found: #{offset}) and repeat > 1 (found: #{repeat})" end - @spec do_update(t, (Types.colorint | Types.rgb | atom)) :: t + @spec do_update(t, Types.color | t) :: t defp do_update(%__MODULE__{meta: meta} = leds, rgb) do index = meta[:index] || 1 - index = if index < 1, do: 1, else: index + index = max(index, 1) do_update(leds, rgb, index) end - - @spec do_update(t, Types.colorint, pos_integer) :: t - defp do_update( - %__MODULE__{count: count, leds: leds, opts: opts, meta: meta}, - rgb, - offset - ) when is_integer(rgb) do - __MODULE__.leds(count, Map.put(leds, offset, rgb), opts, %{meta | index: offset + 1}) - end - @spec do_update(t, t, pos_integer) :: t + @spec do_update(t, Types.color | t, pos_integer) :: t defp do_update( %__MODULE__{count: count1, leds: leds1, opts: opts1, meta: meta1}, %__MODULE__{count: count2, leds: leds2}, @@ -306,20 +300,19 @@ defmodule Fledex.Leds do leds = Map.merge(leds1, remapped_new_leds) __MODULE__.leds(count1, leds, opts1, %{meta1 | index: offset + count2}) end - @spec do_update(t, atom, pos_integer) :: t - defp do_update(leds, atom, offset) when is_atom(atom) do - color_int = apply(Names, atom, [:hex]) - do_update(leds, color_int, offset) - end - @spec do_update(t, Types.rgb, pos_integer) :: t - defp do_update(leds, {_r, _g, _b} = rgb, offset) do - color_int = Utils.to_colorint(rgb) - do_update(leds, color_int, offset) + defp do_update( + %__MODULE__{count: count, leds: leds, opts: opts, meta: meta}, + color, + offset + ) do + color_int = Utils.to_colorint(color) + __MODULE__.leds(count, Map.put(leds, offset, color_int), opts, %{meta | index: offset + 1}) end defp do_update(leds, led, offset) do raise ArgumentError, message: "unknown data #{inspect leds}, #{inspect led}, #{inspect offset}" end + @spec remap_leds(%{pos_integer => Types.colorint}, pos_integer) :: %{pos_integer => Types.colorint} defp remap_leds(leds, offset) do Map.new(Enum.map(leds, fn {key, value} -> index = offset + key - 1 @@ -344,7 +337,7 @@ defmodule Fledex.Leds do Note: Only the leds that are inside the `count` will be added to the list. """ - @spec to_list(t) :: list[integer] + @spec to_list(t) :: list(Types.colorint) def to_list(%__MODULE__{count: count, leds: _leds, opts: _opts, meta: _meta} = leds) when count > 0 do Enum.reduce(1..count, [], fn index, acc -> acc ++ [get_light(leds, index)] @@ -360,10 +353,14 @@ defmodule Fledex.Leds do Convert the sequence of leds to a markdown representation. The #{@block} will be used to represent the leds and they will be colored in - the appropriate color. The opts are currently not used (but are planned to - be used for potential color correction) + the appropriate color. It then looks something like this: + #{@block} + #{@block} + #{@block} + The opts are currently not used, but are planned to + be used for potential color correction (similar to `Fledex.LedStripDriver.KinoDriver`) """ - @spec to_markdown(Fledex.Leds.t, map) :: String.t + @spec to_markdown(t, map) :: String.t def to_markdown(leds, _opts \\ %{}) do leds |> Fledex.Leds.to_list() @@ -461,7 +458,7 @@ defmodule Fledex.Leds do @doc delegate_to: {Kino.Reader, :to_livebook, 1} @impl true - @spec to_livebook(Fledex.Leds.t) :: map() + @spec to_livebook(Fledex.Leds.t) :: map def to_livebook(leds) do md_kino = Kino.Markdown.new(Leds.to_markdown(leds)) i_kino = Kino.Inspect.new(leds) diff --git a/lib/fledex/leds_driver.ex b/lib/fledex/leds_driver.ex index d50f290..f8405eb 100644 --- a/lib/fledex/leds_driver.ex +++ b/lib/fledex/leds_driver.ex @@ -346,8 +346,7 @@ defmodule Fledex.LedsDriver do |> Utils.to_colorint() end - @spec apply_merge_strategy(list(Types.colorint()), atom) :: - Types.rgb() + @spec apply_merge_strategy(list(Types.colorint()), atom) :: Types.rgb() def apply_merge_strategy(rgb, merge_strategy) do case merge_strategy do :avg -> Utils.avg(rgb) diff --git a/test/color/color_test.exs b/test/color/color_test.exs index c883ff5..2cca44f 100644 --- a/test/color/color_test.exs +++ b/test/color/color_test.exs @@ -105,12 +105,14 @@ defmodule Fledex.Color.ColorTest do assert 14_235_678 == Names.info(:vermilion2) assert 14_235_678 == Names.info(:vermilion2, :hex) assert {216, 56, 30} == Names.info(:vermilion2, :rgb) + assert :vermilion2 == Names.info(:vermilion2, :name) assert {5, 193, 122} == Names.info(:vermilion2, :hsl) assert {5, 219, 216} == Names.info(:vermilion2, :hsv) assert 828 == Names.info(:vermilion2, :index) assert "Vermilion2" == Names.info(:vermilion2, :descriptive_name) assert "" == Names.info(:vermilion2, :source) assert "Crayola" == Names.info(:absolute_zero, :source) + assert nil == Names.info(:non_existing_color_name, :hex) assert :vermilion2 in Names.names() end diff --git a/test/color/color_utils_test.exs b/test/color/color_utils_test.exs index 7a62469..ce3429f 100644 --- a/test/color/color_utils_test.exs +++ b/test/color/color_utils_test.exs @@ -37,7 +37,12 @@ defmodule Fledex.Color.UtilsTest do assert Utils.nscale8({128, 128, 128}, Utils.frac8(32, 85), false) == {48, 48, 48} assert Utils.nscale8(Utils.to_colorint({128, 128, 128}), Utils.frac8(32, 85)) == 3_223_857 end - test "convert to subpixels" do + test "convert to_colorint" do + assert Utils.to_colorint(0xffeedd) == 0xffeedd + assert Utils.to_colorint({0xff, 0xee, 0xdd}) == 0xffeedd + assert Utils.to_colorint(:red) == 0xff0000 + end + test "convert to_rgb" do assert Utils.to_rgb(%{rgb: 0x123456}) == {0x12, 0x34, 0x56} assert Utils.to_rgb(%{rgb: {0x12, 0x34, 0x56}}) == {0x12, 0x34, 0x56} assert Utils.to_rgb(:red) == {0xff, 0x00, 0x00} diff --git a/test/leds_test.exs b/test/leds_test.exs index 1bc9d5c..6692f9f 100644 --- a/test/leds_test.exs +++ b/test/leds_test.exs @@ -447,7 +447,10 @@ defmodule Fledex.LedsTestKino do %{ type: :terminal_text, # the field order is not stable :-( Thus we don't compare the actual text - # text: ~s(%Fledex.Leds{\n \e[34mcount:\e[0m \e[34m3\e[0m,\n \e[34mleds:\e[0m %{\e[34m1\e[0m => \e[34m16711680\e[0m, \e[34m2\e[0m => \e[34m65280\e[0m, \e[34m3\e[0m => \e[34m255\e[0m},\n \e[34mopts:\e[0m %{\e[34mnamespace:\e[0m \e[35mnil\e[0m, \e[34mserver_name:\e[0m \e[35mnil\e[0m},\n \e[34mmeta:\e[0m %{\e[34mindex:\e[0m \e[34m4\e[0m}\n}), + # text: ~s(%Fledex.Leds{\n \e[34mcount:\e[0m \e[34m3\e[0m,\n \e[34mleds:\e[0m %{\e[34m1\e[0m) <> + # ~s( => \e[34m16711680\e[0m, \e[34m2\e[0m => \e[34m65280\e[0m, \e[34m3\e[0m => \e[34m255\e[0m}) <> + # ~s(,\n \e[34mopts:\e[0m %{\e[34mnamespace:\e[0m \e[35mnil\e[0m, \e[34mserver_name:\e[0m) <> + # ~s( \e[35mnil\e[0m},\n \e[34mmeta:\e[0m %{\e[34mindex:\e[0m \e[34m4\e[0m}\n}), chunk: false }], type: :tabs