dancing-dots
This commit is contained in:
parent
3ab71444f5
commit
5203753bd5
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
"angelikatyborska"
|
||||||
|
],
|
||||||
|
"contributors": [
|
||||||
|
"jiegillet"
|
||||||
|
],
|
||||||
|
"files": {
|
||||||
|
"solution": [
|
||||||
|
"lib/dancing_dots/animation.ex"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"test/dancing_dots/animation_test.exs"
|
||||||
|
],
|
||||||
|
"exemplar": [
|
||||||
|
".meta/exemplar.ex"
|
||||||
|
],
|
||||||
|
"editor": [
|
||||||
|
"lib/dancing_dots/dot.ex",
|
||||||
|
"lib/dancing_dots/dot_group.ex"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"language_versions": ">=1.10",
|
||||||
|
"blurb": "Learn about behaviours by writing animations for dot-based generative art."
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
{"track":"elixir","exercise":"dancing-dots","id":"536c8ec8f63e47379fca44df961ca8e8","url":"https://exercism.org/tracks/elixir/exercises/dancing-dots","handle":"negrienko","is_requester":true,"auto_approve":false}
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Used by "mix format"
|
||||||
|
[
|
||||||
|
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||||
|
]
|
|
@ -0,0 +1,24 @@
|
||||||
|
# The directory Mix will write compiled artifacts to.
|
||||||
|
/_build/
|
||||||
|
|
||||||
|
# If you run "mix test --cover", coverage assets end up here.
|
||||||
|
/cover/
|
||||||
|
|
||||||
|
# The directory Mix downloads your dependencies sources to.
|
||||||
|
/deps/
|
||||||
|
|
||||||
|
# Where third-party dependencies like ExDoc output generated docs.
|
||||||
|
/doc/
|
||||||
|
|
||||||
|
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||||
|
/.fetch
|
||||||
|
|
||||||
|
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||||
|
erl_crash.dump
|
||||||
|
|
||||||
|
# Also ignore archive artifacts (built via "mix archive.build").
|
||||||
|
*.ez
|
||||||
|
|
||||||
|
# Ignore package tarball (built via "mix hex.build").
|
||||||
|
numbers-*.tar
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Help
|
||||||
|
|
||||||
|
## Running the tests
|
||||||
|
|
||||||
|
From the terminal, change to the base directory of the exercise then execute the tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mix test
|
||||||
|
```
|
||||||
|
|
||||||
|
This will execute the test file found in the `test` subfolder -- a file ending in `_test.exs`
|
||||||
|
|
||||||
|
Documentation:
|
||||||
|
|
||||||
|
* [`mix test` - Elixir's test execution tool](https://hexdocs.pm/mix/Mix.Tasks.Test.html)
|
||||||
|
* [`ExUnit` - Elixir's unit test library](https://hexdocs.pm/ex_unit/ExUnit.html)
|
||||||
|
|
||||||
|
## Pending tests
|
||||||
|
|
||||||
|
In test suites of practice exercises, all but the first test have been tagged to be skipped.
|
||||||
|
|
||||||
|
Once you get a test passing, you can unskip the next one by commenting out the relevant `@tag :pending` with a `#` symbol.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# @tag :pending
|
||||||
|
test "shouting" do
|
||||||
|
assert Bob.hey("WATCH OUT!") == "Whoa, chill out!"
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
If you wish to run all tests at once, you can include all skipped test by using the `--include` flag on the `mix test` command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ mix test --include pending
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, you can enable all the tests by commenting out the `ExUnit.configure` line in the file `test/test_helper.exs`.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# ExUnit.configure(exclude: :pending, trace: true)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful `mix test` options
|
||||||
|
|
||||||
|
* `test/<FILE>.exs:LINENUM` - runs only a single test, the test from `<FILE>.exs` whose definition is on line `LINENUM`
|
||||||
|
* `--failed` - runs only tests that failed the last time they ran
|
||||||
|
* `--max-failures` - the suite stops evaluating tests when this number of test failures
|
||||||
|
is reached
|
||||||
|
* `--seed 0` - disables randomization so the tests in a single file will always be ran
|
||||||
|
in the same order they were defined in
|
||||||
|
|
||||||
|
## Submitting your solution
|
||||||
|
|
||||||
|
You can submit your solution using the `exercism submit lib/dancing_dots/animation.ex` command.
|
||||||
|
This command will upload your solution to the Exercism website and print the solution page's URL.
|
||||||
|
|
||||||
|
It's possible to submit an incomplete solution which allows you to:
|
||||||
|
|
||||||
|
- See how others have completed the exercise
|
||||||
|
- Request help from a mentor
|
||||||
|
|
||||||
|
## Need to get help?
|
||||||
|
|
||||||
|
If you'd like help solving the exercise, check the following pages:
|
||||||
|
|
||||||
|
- The [Elixir track's documentation](https://exercism.org/docs/tracks/elixir)
|
||||||
|
- The [Elixir track's programming category on the forum](https://forum.exercism.org/c/programming/elixir)
|
||||||
|
- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
|
||||||
|
- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
|
||||||
|
|
||||||
|
Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
|
||||||
|
|
||||||
|
If you're stuck on something, it may help to look at some of the [available resources](https://exercism.org/docs/tracks/elixir/resources) out there where answers might be found.
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Hints
|
||||||
|
|
||||||
|
## General
|
||||||
|
|
||||||
|
- Read about behaviours in the official [Getting Started guide][getting-started-behaviours].
|
||||||
|
- Read about behaviours on [elixirschool.com][elixir-school-behaviours].
|
||||||
|
- Read about behaviours in the [documentation][doc-behaviours].
|
||||||
|
- Read about `use` in the official [Getting Started guide][getting-started-use].
|
||||||
|
- Read about `use` in the [documentation][doc-use].
|
||||||
|
|
||||||
|
## 1. Define the animation behaviour
|
||||||
|
|
||||||
|
- Use the `@callback` module attribute to define the desired functions.
|
||||||
|
- Each callback must specify the function name, list of arguments (their types) and the return value (its type).
|
||||||
|
- Use the given custom types `dot`, `opts`, `error`, and `frame_number` in the callbacks' definitions.
|
||||||
|
- Refresh your knowledge of [typespecs][typespec] to help with defining callbacks.
|
||||||
|
|
||||||
|
## 2. Provide a default implementation of the `init/1` callback
|
||||||
|
|
||||||
|
- Define a `__using__/1` macro in the `DancingDots.Animation` module.
|
||||||
|
- The macros' argument can be ignored.
|
||||||
|
- The macro must return a [quoted expression][quote].
|
||||||
|
- In the quoted expression, use `@behaviour` so that calling `use DancingDots.Animation` sets `DancingDots.Animation` as the using module's behaviour.
|
||||||
|
- In the quoted expression, implement the `init/1` function.
|
||||||
|
- The default implementation of the `init/1` function should wrap the given `opts` argument in `:ok` tuple.
|
||||||
|
- There is [a macro][defoverridable] that can mark a function as overridable.
|
||||||
|
|
||||||
|
## 3. Implement the `Flicker` animation
|
||||||
|
|
||||||
|
- Make use of `DancingDots.Animation` `__using__/1` macro by calling [this one special macro][doc-use] in the `DancingDots.Flicker` module.
|
||||||
|
- You do not need to implement the `init/1` function. Its default implementation is enough.
|
||||||
|
- You need to implement the `handle_frame/3` function.
|
||||||
|
- To detect "every 4th frame", you can check if the [remainder][rem] when dividing it by 4 is equal to 0.
|
||||||
|
|
||||||
|
## 4. Implement the `Zoom` animation
|
||||||
|
|
||||||
|
- Make use of `DancingDots.Animation` `__using__/1` macro by calling [this one special macro][doc-use] in the `DancingDots.Zoom` module.
|
||||||
|
- You need to implement both the `init/1` function and the `handle_frame/3` function.
|
||||||
|
- Use the [`Keyword`][keyword] module to work with the options keyword list.
|
||||||
|
- There is [a built-in guard][is_number] for checking if a value is a number.
|
||||||
|
|
||||||
|
[getting-started-behaviours]: https://hexdocs.pm/elixir/typespecs.html#behaviours
|
||||||
|
[doc-behaviours]: https://hexdocs.pm/elixir/typespecs.html#behaviours
|
||||||
|
[elixir-school-behaviours]: https://elixirschool.com/en/lessons/advanced/behaviours
|
||||||
|
[doc-use]: https://hexdocs.pm/elixir/Kernel.html#use/2
|
||||||
|
[getting-started-use]: https://hexdocs.pm/elixir/alias-require-and-import.html#use
|
||||||
|
[typespec]: https://hexdocs.pm/elixir/typespecs.html
|
||||||
|
[defoverridable]: https://hexdocs.pm/elixir/Kernel.html#defoverridable/1
|
||||||
|
[quote]: https://hexdocs.pm/elixir/Kernel.SpecialForms.html#quote/2
|
||||||
|
[rem]: https://hexdocs.pm/elixir/Kernel.html#rem/2
|
||||||
|
[is_number]: https://hexdocs.pm/elixir/Kernel.html#is_number/1
|
||||||
|
[keyword]: https://hexdocs.pm/elixir/Keyword.html
|
|
@ -0,0 +1,198 @@
|
||||||
|
# Dancing Dots
|
||||||
|
|
||||||
|
Welcome to Dancing Dots on Exercism's Elixir Track.
|
||||||
|
If you need help running the tests or submitting your code, check out `HELP.md`.
|
||||||
|
If you get stuck on the exercise, check out `HINTS.md`, but try and solve it without using those first :)
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
The `use` macro allows us to quickly extend our module with functionality provided by another module. When we `use` a module, that module can inject code into our module - it can for example define functions, `import` or `alias` other modules, or set module attributes.
|
||||||
|
|
||||||
|
If you ever looked at the test files of some of the Elixir exercises here on Exercism, you most likely noticed that they all start with `use ExUnit.Case`. This single line of code is what makes the macros `test` and `assert` available in the test module.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule LasagnaTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
|
||||||
|
test "expected minutes in oven" do
|
||||||
|
assert Lasagna.expected_minutes_in_oven() === 40
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### `__using__/1` macro
|
||||||
|
|
||||||
|
What exactly happens when you `use` a module is dictated by that module's `__using__/1` macro. It takes one argument, a keyword list with options, and it returns a [quoted expression][concept-ast]. The code in this quoted expression is inserted into our module when calling `use`.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule ExUnit.Case do
|
||||||
|
defmacro __using__(opts) do
|
||||||
|
# some real-life ExUnit code omitted here
|
||||||
|
quote do
|
||||||
|
import ExUnit.Assertions
|
||||||
|
import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
The options can be given as a second argument when calling `use`, e.g. `use ExUnit.Case, async: true`. When not given explicitly, they default to an empty list.
|
||||||
|
|
||||||
|
## Behaviours
|
||||||
|
|
||||||
|
Behaviours allow us to define interfaces (sets of functions and macros) in a _behaviour module_ that can be later implemented by different _callback modules_. Thanks to the shared interface, those callback modules can be used interchangeably.
|
||||||
|
|
||||||
|
~~~~exercism/note
|
||||||
|
Note the British spelling of "behaviours".
|
||||||
|
~~~~
|
||||||
|
|
||||||
|
### Defining behaviours
|
||||||
|
|
||||||
|
To define a behaviour, we need to create a new module and specify a list of functions that are part of the desired interface. Each function needs to be defined using the `@callback` module attribute. The syntax is identical to a [function typespec][concept-typespecs] (`@spec`). We need to specify a function name, a list of argument types, and all the possible return types.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Countable do
|
||||||
|
@callback count(collection :: any) :: pos_integer
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementing behaviours
|
||||||
|
|
||||||
|
To add an existing behaviour to our module (create a callback module) we use the `@behaviour` module attribute. Its value should be the name of the behaviour module that we're adding.
|
||||||
|
|
||||||
|
Then, we need to define all the functions (callbacks) that are required by that behaviour module. If we're implementing somebody else's behaviour, like Elixir's built-in `Access` or `GenServer` behaviours, we would find the list of all the behaviour's callbacks in the documentation on [hexdocs.pm][hexdocs].
|
||||||
|
|
||||||
|
A callback module is not limited to implementing only the functions that are part of its behaviour. It is also possible for a single module to implement multiple behaviours.
|
||||||
|
|
||||||
|
To mark which function comes from which behaviour, we should use the module attribute `@impl` before each function. Its value should be the name of the behaviour module that defines this callback.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule BookCollection do
|
||||||
|
@behaviour Countable
|
||||||
|
|
||||||
|
defstruct [:list, :owner]
|
||||||
|
|
||||||
|
@impl Countable
|
||||||
|
def count(collection) do
|
||||||
|
Enum.count(collection.list)
|
||||||
|
end
|
||||||
|
|
||||||
|
def mark_as_read(collection, book) do
|
||||||
|
# other function unrelated to the Countable behaviour
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default callback implementations
|
||||||
|
|
||||||
|
When defining a behaviour, it is possible to provide a default implementation of a callback. This implementation should be defined in the quoted expression of the `__using__/1` macro. To make it possible for users of the behaviour module to override the default implementation, call the `defoverridable/1` macro after the function implementation. It accepts a keyword list of function names as keys and function arities as values.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Countable do
|
||||||
|
@callback count(collection :: any) :: pos_integer
|
||||||
|
|
||||||
|
defmacro __using__(_) do
|
||||||
|
quote do
|
||||||
|
@behaviour Countable
|
||||||
|
def count(collection), do: Enum.count(collection)
|
||||||
|
defoverridable count: 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that defining functions inside of `__using__/1` is discouraged for any other purpose than defining default callback implementations, but you can always define functions in another module and import them in the `__using__/1` macro.
|
||||||
|
|
||||||
|
[concept-ast]: https://exercism.org/tracks/elixir/concepts/ast
|
||||||
|
[concept-typespecs]: https://exercism.org/tracks/elixir/concepts/typespecs
|
||||||
|
[hexdocs]: https://hexdocs.pm
|
||||||
|
|
||||||
|
## Instructions
|
||||||
|
|
||||||
|
Your friend, an aspiring artist, reached out to you with a project idea. Let's combine his visual creativity with your technical expertise. It's time to dabble in [generative art][generative-art]!
|
||||||
|
|
||||||
|
Constraints help creativity and shorten project deadlines, so you've both agreed to limit your masterpiece to a single shape - the circle. But there's going to be many circles. And they can move around! You'll call it... dancing dots.
|
||||||
|
|
||||||
|
Your friend will definitely want to come up with new elaborate movements for the dots, so you'll start coding by creating an architecture that will allow you to later define new animations easily.
|
||||||
|
|
||||||
|
## 1. Define the animation behaviour
|
||||||
|
|
||||||
|
Each animation module needs to implement two callbacks: `init/1` and `handle_frame/3`. Define them in the `Animation` module.
|
||||||
|
|
||||||
|
Define the `init/1` callback. It should take one argument of type `opts` and return either an `{:ok, opts}` tuple or `{:error, error}` tuple. Implementations of this callback will check if the given options are valid for this particular type of animation.
|
||||||
|
|
||||||
|
Define the `handle_frame/3` callbacks. It should take three arguments - the dot, a frame number, and options. It should always return a dot. Implementations of this callback will modify the dot's attributes based on the current frame number and the animation's options.
|
||||||
|
|
||||||
|
## 2. Provide a default implementation of the `init/1` callback
|
||||||
|
|
||||||
|
The `Animation` behaviour should be easy to incorporate into other modules by calling `use DancingDots.Animation`.
|
||||||
|
|
||||||
|
To make that happen, implement the `__using__` macro in the `Animation` module so that it sets the `Animation` module as the other module's behaviour. It should also provide a default implementation of the `init/1` callback. The default implementation of `init/1` should return the given options unchanged.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule MyCustomAnimation do
|
||||||
|
use DancingDots.Animation
|
||||||
|
end
|
||||||
|
|
||||||
|
MyCustomAnimation.init(some_option: true)
|
||||||
|
# => {:ok, [some_option: true]}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Implement the `Flicker` animation
|
||||||
|
|
||||||
|
Use the `Animation` behaviour to implement a flickering animation.
|
||||||
|
|
||||||
|
It should use the default `init/1` callback because it doesn't take any options.
|
||||||
|
|
||||||
|
Implement the `handle_frame/3` callback, which handles a single frame. If the frame number is a multiple of four, the function should return the dot with half of its original opacity. In other frames, it should return the dot unchanged.
|
||||||
|
|
||||||
|
Frames are counted from `1`. The dot passed to `handle_frame/3` is always the dot in its original state, not in the state from the previous frame.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}
|
||||||
|
|
||||||
|
DancingDots.Flicker.handle_frame(dot, 1, [])
|
||||||
|
# => %DancingDots.Dot{opacity: 1, radius: 24, x: 100, y: 100}
|
||||||
|
|
||||||
|
DancingDots.Flicker.handle_frame(dot, 4, [])
|
||||||
|
# => %DancingDots.Dot{opacity: 0.5, radius: 24, x: 100, y: 100}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Implement the `Zoom` animation
|
||||||
|
|
||||||
|
Use the `Animation` behaviour to implement a zooming animation.
|
||||||
|
|
||||||
|
This animation takes one option - velocity. Velocity can be any number. If it's negative, the dot gets zoomed out instead of zoomed in.
|
||||||
|
|
||||||
|
Implement the `init/1` callback. It should validate that the passed options is a keyword list with a `:velocity` key. The value of velocity must be a number. If it's not a number, return the error `"The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}"`.
|
||||||
|
|
||||||
|
Implement the `handle_frame/3` callback. It should return the dot with its radius increased by the current frame number, minus one, times velocity.
|
||||||
|
|
||||||
|
Frames are counted from `1`. The dot passed to `handle_frame/3` is always the dot in its original state, not in the state from the previous frame.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
DancingDots.Zoom.init(velocity: nil)
|
||||||
|
# => {:error, "The :velocity option is required, and its value must be a number. Got: nil"}
|
||||||
|
|
||||||
|
dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}
|
||||||
|
|
||||||
|
DancingDots.Zoom.handle_frame(dot, 1, velocity: 10)
|
||||||
|
# => %DancingDots.Dot{radius: 24, opacity: 1, x: 100, y: 100}
|
||||||
|
|
||||||
|
DancingDots.Zoom.handle_frame(dot, 2, velocity: 10)
|
||||||
|
# => %DancingDots.Dot{radius: 34, opacity: 1, x: 100, y: 100}
|
||||||
|
```
|
||||||
|
|
||||||
|
[generative-art]: https://en.wikipedia.org/wiki/Generative_art
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
### Created by
|
||||||
|
|
||||||
|
- @angelikatyborska
|
||||||
|
|
||||||
|
### Contributed to by
|
||||||
|
|
||||||
|
- @jiegillet
|
|
@ -0,0 +1,49 @@
|
||||||
|
defmodule DancingDots.Animation do
|
||||||
|
@type dot :: DancingDots.Dot.t()
|
||||||
|
@type opts :: keyword
|
||||||
|
@type error :: any
|
||||||
|
@type frame_number :: pos_integer
|
||||||
|
|
||||||
|
@callback init(opts) :: {:ok, opts} | {:error, error}
|
||||||
|
@callback handle_frame(dot, frame_number, opts) :: dot
|
||||||
|
|
||||||
|
defmacro __using__(_) do
|
||||||
|
quote do
|
||||||
|
@behaviour DancingDots.Animation
|
||||||
|
def init(opts), do: {:ok, opts}
|
||||||
|
defoverridable init: 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule DancingDots.Flicker do
|
||||||
|
use DancingDots.Animation
|
||||||
|
|
||||||
|
@impl DancingDots.Animation
|
||||||
|
def handle_frame(%DancingDots.Dot{opacity: opacity} = dot, frame_number, _opts)
|
||||||
|
when rem(frame_number, 4) == 0,
|
||||||
|
do: %{dot | opacity: opacity / 2}
|
||||||
|
|
||||||
|
@impl DancingDots.Animation
|
||||||
|
def handle_frame(dot, _frame_number, _opts), do: dot
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule DancingDots.Zoom do
|
||||||
|
use DancingDots.Animation
|
||||||
|
|
||||||
|
@impl DancingDots.Animation
|
||||||
|
def init(opts) do
|
||||||
|
case Keyword.get(opts, :velocity) do
|
||||||
|
velocity when is_number(velocity) ->
|
||||||
|
{:ok, opts}
|
||||||
|
|
||||||
|
velocity ->
|
||||||
|
{:error,
|
||||||
|
"The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl DancingDots.Animation
|
||||||
|
def handle_frame(%DancingDots.Dot{radius: radius} = dot, frame_number, opts),
|
||||||
|
do: %{dot | radius: radius + (frame_number - 1) * Keyword.get(opts, :velocity)}
|
||||||
|
end
|
|
@ -0,0 +1,4 @@
|
||||||
|
defmodule DancingDots.Dot do
|
||||||
|
defstruct [:x, :y, :radius, :opacity]
|
||||||
|
@type t :: %__MODULE__{}
|
||||||
|
end
|
|
@ -0,0 +1,61 @@
|
||||||
|
defmodule DancingDots.DotGroup do
|
||||||
|
# This module is an example of how behaviours can be used in practice.
|
||||||
|
# You don't need to read it to solve this exercise.
|
||||||
|
# It's here for the curious :)
|
||||||
|
|
||||||
|
@moduledoc """
|
||||||
|
Allows applying a list of one or more animations to a group of one or more dots.
|
||||||
|
"""
|
||||||
|
|
||||||
|
defstruct [:dots, :animations_with_opts]
|
||||||
|
@type t :: %__MODULE__{}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a new dot group with given dots and an empty list of animations.
|
||||||
|
"""
|
||||||
|
@spec new([DancingDots.Dot.t()]) :: t()
|
||||||
|
def new(dots) do
|
||||||
|
%__MODULE__{
|
||||||
|
dots: dots,
|
||||||
|
animations_with_opts: []
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Validates the given animation module with its given options and adds it to the group.
|
||||||
|
"""
|
||||||
|
@spec add_animation(t(), module, DancingDots.Animation.opts()) ::
|
||||||
|
{:ok, t()} | {:error, DancingDots.Animation.error()}
|
||||||
|
def add_animation(dot_group, animation_module, opts) do
|
||||||
|
# using Animation's init/1 callback
|
||||||
|
init_result = animation_module.init(opts)
|
||||||
|
|
||||||
|
case init_result do
|
||||||
|
{:ok, opts} ->
|
||||||
|
animations_with_opts = [{animation_module, opts} | dot_group.animations_with_opts]
|
||||||
|
dot_group = %{dot_group | animations_with_opts: animations_with_opts}
|
||||||
|
{:ok, dot_group}
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
{:error, error}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Applies the list of animations to all the dots.
|
||||||
|
"""
|
||||||
|
@spec render_dots(t(), DancingDots.Animation.frame_number()) :: [DancingDots.Dot.t()]
|
||||||
|
def render_dots(dot_group, frame_number) do
|
||||||
|
%{
|
||||||
|
dots: dots,
|
||||||
|
animations_with_opts: animations_with_opts
|
||||||
|
} = dot_group
|
||||||
|
|
||||||
|
Enum.map(dots, fn dot ->
|
||||||
|
Enum.reduce(animations_with_opts, dot, fn {animation_module, opts}, acc ->
|
||||||
|
# using Animation's handle_frame/3 callback
|
||||||
|
animation_module.handle_frame(acc, frame_number, opts)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule DancingDots.MixProject do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :dancing_dots,
|
||||||
|
version: "0.1.0",
|
||||||
|
# elixir: "~> 1.10",
|
||||||
|
start_permanent: Mix.env() == :prod,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help compile.app" to learn about applications.
|
||||||
|
def application do
|
||||||
|
[
|
||||||
|
extra_applications: [:logger]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help deps" to learn about dependencies.
|
||||||
|
defp deps do
|
||||||
|
[
|
||||||
|
# {:dep_from_hexpm, "~> 0.3.0"},
|
||||||
|
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,221 @@
|
||||||
|
defmodule DancingDots.AnimationTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
import ExUnit.CaptureIO
|
||||||
|
|
||||||
|
describe "Animation module" do
|
||||||
|
@tag task_id: 1
|
||||||
|
test "defines two required callbacks, init/1 and handle_frame/3" do
|
||||||
|
assert DancingDots.Animation.behaviour_info(:callbacks) == [{:init, 1}, {:handle_frame, 3}]
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 1
|
||||||
|
test "defines no optional callbacks" do
|
||||||
|
assert DancingDots.Animation.behaviour_info(:optional_callbacks) == []
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "defines a __using__ macro" do
|
||||||
|
Code.ensure_loaded(DancingDots.Animation)
|
||||||
|
assert macro_exported?(DancingDots.Animation, :__using__, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "__using__ provides a default implementation of init/1" do
|
||||||
|
defmodule TestAnimation1 do
|
||||||
|
use DancingDots.Animation
|
||||||
|
def handle_frame(dot, _frame_number, _opts), do: dot
|
||||||
|
end
|
||||||
|
|
||||||
|
assert function_exported?(TestAnimation1, :init, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "the default implementation of init/1 returns opts unchanged" do
|
||||||
|
defmodule TestAnimation2 do
|
||||||
|
use DancingDots.Animation
|
||||||
|
def handle_frame(dot, _frame_number, _opts), do: dot
|
||||||
|
end
|
||||||
|
|
||||||
|
assert TestAnimation2.init([]) == {:ok, []}
|
||||||
|
assert TestAnimation2.init(:anything) == {:ok, :anything}
|
||||||
|
assert TestAnimation2.init({1, 2, 3}) == {:ok, {1, 2, 3}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "the default implementation of init/1 can be overridden" do
|
||||||
|
defmodule TestAnimation3 do
|
||||||
|
use DancingDots.Animation
|
||||||
|
def init(_), do: {:ok, []}
|
||||||
|
def handle_frame(dot, _frame_number, _opts), do: dot
|
||||||
|
end
|
||||||
|
|
||||||
|
assert TestAnimation3.init(3) == {:ok, []}
|
||||||
|
assert TestAnimation3.init(:anything) == {:ok, []}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "__using__ does not provide a default implementation of handle_frame/3" do
|
||||||
|
capture_io(:stderr, fn ->
|
||||||
|
Code.compile_quoted do
|
||||||
|
quote do
|
||||||
|
defmodule TestAnimation4 do
|
||||||
|
use DancingDots.Animation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
refute function_exported?(TestAnimation4, :handle_frame, 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 2
|
||||||
|
test "__using__ sets Animation as the behaviour, emitting compilation warnings about missing callbacks" do
|
||||||
|
compilation_warnings =
|
||||||
|
capture_io(:stderr, fn ->
|
||||||
|
Code.compile_quoted do
|
||||||
|
quote do
|
||||||
|
defmodule TestAnimation5 do
|
||||||
|
use DancingDots.Animation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert compilation_warnings =~
|
||||||
|
"function handle_frame/3 required by behaviour DancingDots.Animation is not implemented"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Flicker module" do
|
||||||
|
@tag task_id: 3
|
||||||
|
test "implements Animation behaviour" do
|
||||||
|
Code.ensure_loaded(DancingDots.Flicker)
|
||||||
|
assert function_exported?(DancingDots.Flicker, :init, 1)
|
||||||
|
assert function_exported?(DancingDots.Flicker, :handle_frame, 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "uses the default init/1 implementation" do
|
||||||
|
assert DancingDots.Flicker.init([]) == {:ok, []}
|
||||||
|
assert DancingDots.Flicker.init(:anything) == {:ok, :anything}
|
||||||
|
assert DancingDots.Flicker.init({1, 2, 3}) == {:ok, {1, 2, 3}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "every 4th frame, handle_frame/3 sets the dot's opacity to half the original value" do
|
||||||
|
dot1 = %DancingDots.Dot{x: 10, y: 30, radius: 4, opacity: 1}
|
||||||
|
dot2 = %DancingDots.Dot{x: 0, y: 0, radius: 10, opacity: 0.6}
|
||||||
|
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot1, 1, []) == %{dot1 | opacity: 1}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot1, 2, []) == %{dot1 | opacity: 1}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot1, 3, []) == %{dot1 | opacity: 1}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot1, 4, []) == %{dot1 | opacity: 0.5}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot1, 5, []) == %{dot1 | opacity: 1}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot1, 6, []) == %{dot1 | opacity: 1}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot1, 7, []) == %{dot1 | opacity: 1}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot1, 8, []) == %{dot1 | opacity: 0.5}
|
||||||
|
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot2, 13, []) == %{dot2 | opacity: 0.6}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot2, 14, []) == %{dot2 | opacity: 0.6}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot2, 15, []) == %{dot2 | opacity: 0.6}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot2, 16, []) == %{dot2 | opacity: 0.3}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot2, 17, []) == %{dot2 | opacity: 0.6}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot2, 18, []) == %{dot2 | opacity: 0.6}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot2, 19, []) == %{dot2 | opacity: 0.6}
|
||||||
|
assert DancingDots.Flicker.handle_frame(dot2, 20, []) == %{dot2 | opacity: 0.3}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 3
|
||||||
|
test "can be used in a dot group" do
|
||||||
|
dot1 = %DancingDots.Dot{x: 44, y: 44, radius: 2, opacity: 0.8}
|
||||||
|
dot2 = %DancingDots.Dot{x: 0, y: 0, radius: 3, opacity: 0.5}
|
||||||
|
|
||||||
|
{:ok, dot_group} =
|
||||||
|
DancingDots.DotGroup.new([dot1, dot2])
|
||||||
|
|> DancingDots.DotGroup.add_animation(DancingDots.Flicker, [])
|
||||||
|
|
||||||
|
assert dot_group ==
|
||||||
|
%DancingDots.DotGroup{
|
||||||
|
animations_with_opts: [{DancingDots.Flicker, []}],
|
||||||
|
dots: [dot1, dot2]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert DancingDots.DotGroup.render_dots(dot_group, 4) == [
|
||||||
|
%{dot1 | opacity: 0.4},
|
||||||
|
%{dot2 | opacity: 0.25}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Zoom module" do
|
||||||
|
@tag task_id: 4
|
||||||
|
test "implements Animation behaviour" do
|
||||||
|
Code.ensure_loaded(DancingDots.Zoom)
|
||||||
|
assert function_exported?(DancingDots.Zoom, :init, 1)
|
||||||
|
assert function_exported?(DancingDots.Zoom, :handle_frame, 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "has a custom init/1 implementation that checks that a valid velocity was passed as an option" do
|
||||||
|
assert DancingDots.Zoom.init(velocity: 10) == {:ok, [velocity: 10]}
|
||||||
|
|
||||||
|
assert DancingDots.Zoom.init([]) ==
|
||||||
|
{:error,
|
||||||
|
"The :velocity option is required, and its value must be a number. Got: nil"}
|
||||||
|
|
||||||
|
assert DancingDots.Zoom.init(velocity: "7") ==
|
||||||
|
{:error,
|
||||||
|
"The :velocity option is required, and its value must be a number. Got: \"7\""}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "the first frame, handle_frame/3 returns the dot unchanged" do
|
||||||
|
dot = %DancingDots.Dot{x: 90, y: 90, radius: 100, opacity: 0.5}
|
||||||
|
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 1, velocity: 3) == dot
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 1, velocity: 30) == dot
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 1, velocity: -7) == dot
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "every frame after the first, handle_frame/3 grows the dot's radius by the given positive velocity" do
|
||||||
|
dot = %DancingDots.Dot{x: 90, y: 90, radius: 100, opacity: 0.5}
|
||||||
|
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 1, velocity: 3) == %{dot | radius: 100}
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 2, velocity: 3) == %{dot | radius: 103}
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 3, velocity: 3) == %{dot | radius: 106}
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 101, velocity: 3) == %{dot | radius: 400}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "every frame after the first, handle_frame/3 shrinks the dot's radius by the given negative velocity" do
|
||||||
|
dot = %DancingDots.Dot{x: 100, y: 0, radius: 400, opacity: 0.9}
|
||||||
|
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 1, velocity: -1) == %{dot | radius: 400}
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 2, velocity: -1) == %{dot | radius: 399}
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 3, velocity: -1) == %{dot | radius: 398}
|
||||||
|
assert DancingDots.Zoom.handle_frame(dot, 101, velocity: -1) == %{dot | radius: 300}
|
||||||
|
end
|
||||||
|
|
||||||
|
@tag task_id: 4
|
||||||
|
test "can be used in a dot group" do
|
||||||
|
dot1 = %DancingDots.Dot{x: 0, y: 0, radius: 100, opacity: 0.3}
|
||||||
|
dot2 = %DancingDots.Dot{x: 0, y: 0, radius: 150, opacity: 0.3}
|
||||||
|
|
||||||
|
{:ok, dot_group} =
|
||||||
|
DancingDots.DotGroup.new([dot1, dot2])
|
||||||
|
|> DancingDots.DotGroup.add_animation(DancingDots.Zoom, velocity: 10)
|
||||||
|
|
||||||
|
assert dot_group ==
|
||||||
|
%DancingDots.DotGroup{
|
||||||
|
animations_with_opts: [{DancingDots.Zoom, [velocity: 10]}],
|
||||||
|
dots: [dot1, dot2]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert DancingDots.DotGroup.render_dots(dot_group, 50) == [
|
||||||
|
%{dot1 | radius: 590},
|
||||||
|
%{dot2 | radius: 640}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,2 @@
|
||||||
|
ExUnit.start()
|
||||||
|
ExUnit.configure(exclude: :pending, trace: true, seed: 0)
|
Loading…
Reference in New Issue