├── .formatter.exs ├── .gitignore ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── assets └── images │ ├── .DS_Store │ ├── back │ ├── in.png │ ├── in_out.png │ └── out.png │ ├── bounce │ ├── in.png │ ├── in_out.png │ └── out.png │ ├── circular │ ├── in.png │ ├── in_out.png │ └── out.png │ ├── cubic │ ├── in.png │ ├── in_out.png │ └── out.png │ ├── easings.png │ ├── elastic │ ├── in.png │ ├── in_out.png │ └── out.png │ ├── exponential │ ├── in.png │ ├── in_out.png │ └── out.png │ ├── quadratic │ ├── in.png │ ├── in_out.png │ └── out.png │ ├── quartic │ ├── in.png │ ├── in_out.png │ └── out.png │ ├── quintic │ ├── in.png │ ├── in_out.png │ └── out.png │ └── sine │ ├── in.png │ ├── in_out.png │ └── out.png ├── lib ├── easing.ex └── easing │ └── range.ex ├── mix.exs ├── mix.lock └── test ├── easing └── range_test.exs ├── easing_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | easing-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | /.elixir_ls -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version History 2 | 3 | ## 0.3.1 4 | 5 | Bug fix for Enumberable impl on `Easing.Range` 6 | ## 0.3.0 7 | 8 | Rename `Easing.AnimationRange` to `Easing.Range` 9 | 10 | ## 0.2.1 11 | 12 | Bad release for 0.2.0, didn't actually include the stated changed. 13 | 14 | ## 0.2.0 15 | 16 | Implemented reverse range support for `%Easing.AnimationRange` 17 | Fixed impossible range bug (no impossible ranges, always returns empty list) 18 | 19 | ## 0.1.3 20 | 21 | Bug fix: corrected `Easing.AnimationRange.calculate/2` to properly segment based upon frames within the framerate over the given duration 22 | 23 | ## 0.1.2 24 | * Changed `Easing.AnimationRange.size/1` to calculate an inclusive result 25 | 26 | ## 0.1.1 27 | 28 | * ensure all return values are Float 29 | * ensure that `to_list` and `stream` include the `first` and `last` values precicely 30 | * resolved various Dialyzer warnings 31 | 32 | ## 0.1.0 33 | 34 | * Initial release (Brian Cardarella) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2022 DockYard, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easing 2 | 3 | Easing function calculations 4 | 5 | ![Easing functions visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/easings.png "Easing functions visulizations created by Andrey Sitnik and Ivan Solovev") 6 | 7 | ## Credits 8 | 9 | This library implements all of the easing functions as provided on https://easings.net written by [Andrey Sitnik](https://sitnik.ru/) and [Ivan Solovev](https://solovev.one/) with slight modifications to the mathematical implementations to account for Elixir's immutability. 10 | 11 | ## Description 12 | 13 | Calculates the easing value of a given function and progress of an timing represented by a range between `0..1`. The following easings are availalbe with _in_, _out_, and _in\_out_ varients: 14 | 15 | Note: If you'd like to see visualizations of the easing calculations visit https://easings.net 16 | 17 | * [Linear](https://en.wikipedia.org/wiki/Linear_function) 18 | * [Sine](https://en.wikipedia.org/wiki/Sine_and_cosine) 19 | * [Quadratic](https://en.wikipedia.org/wiki/Quadratic_function) 20 | * [Cubic](https://en.wikipedia.org/wiki/Cubic_function) 21 | * [Quartic](https://en.wikipedia.org/wiki/Quartic_function) 22 | * [Quintic](https://en.wikipedia.org/wiki/Quintic_function) 23 | * [Exponential](https://en.wikipedia.org/wiki/Exponential_function) 24 | * [Circular](https://en.wikipedia.org/wiki/Trigonometric_functions) 25 | * [Back](https://easings.net/#easeInOutBack) 26 | * [Elastic](https://easings.net/#easeInOutElastic) 27 | * [Bounce](https://easings.net/#easeInOutBounce) 28 | 29 | ## Installation 30 | 31 | If [available in Hex](https://hex.pm/docs/publish), the package can be installed 32 | by adding `ease` to your list of dependencies in `mix.exs`: 33 | 34 | ```elixir 35 | def deps do 36 | [ 37 | {:easing, "~> 0.3.1"} 38 | ] 39 | end 40 | ``` 41 | 42 | ## Usage 43 | 44 | ### Calculating a single point 45 | 46 | You can calculate a single point for a function along the progress of a timing: 47 | 48 | ```elixir 49 | iex> Easing.sine_in(0.4) 50 | 0.19098300562505255 51 | ``` 52 | 53 | ### Calculating a list of timing frame values 54 | 55 | However you likely want to calculate the list of values for a given timing. `Easing.to_list/2` will take a `Easing.Range` struct and the easing as either a function reference or a tuple. 56 | 57 | ```elixir 58 | iex> Easing.to_list(%Easing.Range{first: 0, last: 0.5, step: 0.1}, &Easing.bounce_in_out(&1)) 59 | [0.0, 0.030000000000000027, 0.11375000000000002, 0.04499999999999993, 0.3487500000000001, 0.5] 60 | iex> Easing.to_list(%Easing.Range{first: 0, last: 0.5, step: 0.1}, {:bounce, :in_out}) 61 | [0.0, 0.030000000000000027, 0.11375000000000002, 0.04499999999999993, 0.3487500000000001, 0.5] 62 | ``` 63 | 64 | ### Calculating timing frames from a Stream 65 | 66 | In many cases you will generate many timing frame values and it may be most performant to calculate those values lazily. We can easily do this with [Elixir Streams](https://elixir-lang.org/getting-started/enumerables-and-streams.html#streams) 67 | 68 | ```elixir 69 | iex> Easing.stream(%Easing.Range{first: 0, last: 1, step: 0.0001}, &Easing.sine_in/1) |> Enum.take(3) 70 | [0.0, 1.2337005528273437e-8, 4.9348021557982236e-8] 71 | ``` 72 | 73 | `Easing.stream/2` also take a `tuple` similar to `Easing.to_list/2` 74 | 75 | ### Custom easing functions 76 | 77 | There is no need for you to limit to the included easing functions. A custom easing function takes a single value and returns the approximation. 78 | 79 | ```elixir 80 | iex> custom_easing = fn(progress) -> 81 | if progress < 0.5 do 82 | Easing.sine_in(progress) / 2 83 | else 84 | 1 - Easing.sine_in(progress) / 2 85 | end 86 | end 87 | 88 | iex> Easing.to_list(Easing.Range.new(0, 1, 0.1), custom_easing) 89 | [0.0, 0.006155829702431115, 0.024471741852423234, 0.054496737905816106, 90 | 0.09549150281252627, 0.8535533905932737, 0.7938926261462367, 91 | 0.7269952498697734, 0.6545084971874737, 0.5782172325201156, 0.5] 92 | ``` 93 | 94 | ### `Easing.Range` 95 | 96 | `Easing.Range` is a reimplementation of Elixir's `Range` struct but will allow for ranges to be built across and between fractional values 97 | rather than limited to `Integer` values. You can create a new `Easing.Range` struct with either form: 98 | 99 | ```elixir 100 | # struct form 101 | iex> %Easing.Range{first: 0.5, last: 1, step: 0.1} 102 | %Easing.Range{first: 0.5, last: 1, step: 0.1} 103 | 104 | # function form 105 | iex> Easing.Range(0, 0.5, 0.1) 106 | %Easing.Range{first: 0, last: 0.5, step: 0.1} 107 | ``` 108 | 109 | There is also a convenience function: 110 | 111 | ```elixir 112 | iex> Easing.Range.new(0, 1, 0.1) 113 | %Easing.Range{first: 0, last: 1, step: 0.1} 114 | ``` 115 | 116 | You can also calculate the steps from a target frame rate 117 | 118 | ```elixir 119 | iex> duration_in_ms = 1000 120 | iex> fps = 60 121 | iex> Easing.Range.calculate(duaration_in_ms, fps) 122 | %Easing.Range{first: 0, last: 1, step: 1.6666666666666667e-5} 123 | ``` 124 | 125 | ## Authors ## 126 | 127 | * [Brian Cardarella](https://twitter.com/bcardarella) 128 | 129 | [We are very thankful for the many contributors](https://github.com/dockyard/easing/graphs/contributors) 130 | 131 | ## Versioning ## 132 | 133 | This library follows [Semantic Versioning](https://semver.org) 134 | 135 | ## Looking for help with your Elixir project? ## 136 | 137 | [At DockYard we are ready to help you build your next Elixir project](https://dockyard.com/phoenix-consulting). We have a unique expertise 138 | in Elixir and Phoenix development that is unmatched. [Get in touch!](https://dockyard.com/contact/hire-us) 139 | 140 | At DockYard we love Elixir! You can [read our Elixir blog posts](https://dockyard.com/blog/categories/elixir) 141 | 142 | ## Legal ## 143 | 144 | [DockYard](https://dockyard.com/), Inc. © 2022 145 | 146 | [@DockYard](https://twitter.com/DockYard) 147 | 148 | [Licensed under the MIT license](https://www.opensource.org/licenses/mit-license.php) -------------------------------------------------------------------------------- /assets/images/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/.DS_Store -------------------------------------------------------------------------------- /assets/images/back/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/back/in.png -------------------------------------------------------------------------------- /assets/images/back/in_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/back/in_out.png -------------------------------------------------------------------------------- /assets/images/back/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/back/out.png -------------------------------------------------------------------------------- /assets/images/bounce/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/bounce/in.png -------------------------------------------------------------------------------- /assets/images/bounce/in_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/bounce/in_out.png -------------------------------------------------------------------------------- /assets/images/bounce/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/bounce/out.png -------------------------------------------------------------------------------- /assets/images/circular/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/circular/in.png -------------------------------------------------------------------------------- /assets/images/circular/in_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/circular/in_out.png -------------------------------------------------------------------------------- /assets/images/circular/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/circular/out.png -------------------------------------------------------------------------------- /assets/images/cubic/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/cubic/in.png -------------------------------------------------------------------------------- /assets/images/cubic/in_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/cubic/in_out.png -------------------------------------------------------------------------------- /assets/images/cubic/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/cubic/out.png -------------------------------------------------------------------------------- /assets/images/easings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/easings.png -------------------------------------------------------------------------------- /assets/images/elastic/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/elastic/in.png -------------------------------------------------------------------------------- /assets/images/elastic/in_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/elastic/in_out.png -------------------------------------------------------------------------------- /assets/images/elastic/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/elastic/out.png -------------------------------------------------------------------------------- /assets/images/exponential/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/exponential/in.png -------------------------------------------------------------------------------- /assets/images/exponential/in_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/exponential/in_out.png -------------------------------------------------------------------------------- /assets/images/exponential/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/exponential/out.png -------------------------------------------------------------------------------- /assets/images/quadratic/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/quadratic/in.png -------------------------------------------------------------------------------- /assets/images/quadratic/in_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/quadratic/in_out.png -------------------------------------------------------------------------------- /assets/images/quadratic/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/quadratic/out.png -------------------------------------------------------------------------------- /assets/images/quartic/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/quartic/in.png -------------------------------------------------------------------------------- /assets/images/quartic/in_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/quartic/in_out.png -------------------------------------------------------------------------------- /assets/images/quartic/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/quartic/out.png -------------------------------------------------------------------------------- /assets/images/quintic/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/quintic/in.png -------------------------------------------------------------------------------- /assets/images/quintic/in_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/quintic/in_out.png -------------------------------------------------------------------------------- /assets/images/quintic/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/quintic/out.png -------------------------------------------------------------------------------- /assets/images/sine/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/sine/in.png -------------------------------------------------------------------------------- /assets/images/sine/in_out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/sine/in_out.png -------------------------------------------------------------------------------- /assets/images/sine/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DockYard/easing/891eb3c397a332a2140f23db80f0c1f49f56b119/assets/images/sine/out.png -------------------------------------------------------------------------------- /lib/easing.ex: -------------------------------------------------------------------------------- 1 | defmodule Easing do 2 | @moduledoc """ 3 | Easing function calculations 4 | 5 | Cribbed from: https://easings.net/ 6 | """ 7 | 8 | alias Easing.Range 9 | 10 | @type easing_tuple :: {atom(), atom()} 11 | @type range :: %Easing.Range{first: number(), last: number(), step: number()} 12 | @type easing_function :: function() 13 | @type easing_function_or_tuple :: easing_function() | easing_tuple() 14 | @type easings :: [float()] 15 | 16 | 17 | @spec to_list(range(), easing_function_or_tuple()) :: easings() 18 | @doc """ 19 | Generates a list of animation frame values. 20 | 21 | A `Range` is used for the `range` argument. See the example 22 | 23 | ## Examples: 24 | 25 | iex> Easing.to_list(%Easing.Range{first: 0, last: 1, step: 0.1}, &Easing.sine_in(&1)) 26 | [0.0, 0.01231165940486223, 0.04894348370484647, 0.10899347581163221, 0.19098300562505255, 0.2928932188134524, 0.41221474770752675, 0.5460095002604533, 0.6909830056250525, 0.8435655349597688, 0.9999999999999997, 1.0] 27 | 28 | iex> Easing.to_list(%Easing.Range{first: 0, last: 0.5, step: 0.1}, {:bounce, :in_out}) 29 | [0.0, 0.030000000000000027, 0.11375000000000002, 0.04499999999999993, 0.3487500000000001, 0.5] 30 | """ 31 | def to_list(%Range{} = range, easing) do 32 | range 33 | |> stream(easing) 34 | |> Enum.to_list() 35 | end 36 | 37 | @spec stream(range(), easing_function_or_tuple()) :: Enumerable.t() 38 | @doc """ 39 | Generates a stream of animation frame values. 40 | 41 | A `Range` is used for the `range` argument. See the example 42 | 43 | ## Examples: 44 | iex> Easing.stream(%Easing.Range{first: 0, last: 1, step: 0.1}, &Easing.sine_in(&1)) |> Enum.to_list() 45 | [0.0, 0.01231165940486223, 0.04894348370484647, 0.10899347581163221, 0.19098300562505255, 0.2928932188134524, 0.41221474770752675, 0.5460095002604533, 0.6909830056250525, 0.8435655349597688, 0.9999999999999997, 1.0] 46 | 47 | iex> Easing.stream(%Easing.Range{first: 0, last: 0.5, step: 0.1}, {:bounce, :in_out}) |> Enum.to_list() 48 | [0.0, 0.030000000000000027, 0.11375000000000002, 0.04499999999999993, 0.3487500000000001, 0.5] 49 | 50 | """ 51 | def stream(%Range{} = range, easing_function) when is_function(easing_function) do 52 | Stream.map(range, &easing_function.(&1)) 53 | end 54 | def stream(%Range{} = range, easing_tuple) when is_tuple(easing_tuple) do 55 | Stream.map(range, &run(easing_tuple, &1)) 56 | end 57 | 58 | @spec linear_in(float()) :: float() 59 | @doc """ 60 | Linear in easing function 61 | 62 | ## Example 63 | 64 | iex> Easing.linear_in(0.1) 65 | 0.1 66 | """ 67 | def linear_in(progress), do: run({:linear, :in}, progress) 68 | 69 | @spec linear_out(float()) ::float() 70 | @doc """ 71 | Linear out easing function 72 | 73 | ## Example 74 | 75 | iex> Easing.linear_out(0.1) 76 | 0.1 77 | """ 78 | def linear_out(progress), do: run({:linear, :out}, progress) 79 | 80 | @spec linear_in_out(float()) :: float() 81 | @doc """ 82 | Linear in-out easing function 83 | 84 | ## Example 85 | 86 | iex> Easing.linear_in_out(0.1) 87 | 0.1 88 | """ 89 | def linear_in_out(progress), do: run({:linear, :in_out}, progress) 90 | 91 | @spec sine_in(float()) :: float() 92 | @doc """ 93 | Sine in easing function 94 | 95 | ![Sine in easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/sine/in.png "Sine in easing visulizations created by Andrey Sitnik and Ivan Solovev") 96 | 97 | ## Example 98 | 99 | iex> Easing.sine_in(0.1) 100 | 0.01231165940486223 101 | """ 102 | def sine_in(progress), do: run({:sine, :in}, progress) 103 | 104 | @spec sine_out(float()) :: float() 105 | @doc """ 106 | Sine out easing function 107 | 108 | ![Sine out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/sine/out.png "Sine out easing visulizations created by Andrey Sitnik and Ivan Solovev") 109 | 110 | ## Example 111 | 112 | iex> Easing.sine_out(0.1) 113 | 0.15643446504023087 114 | """ 115 | def sine_out(progress), do: run({:sine, :out}, progress) 116 | 117 | @spec sine_in_out(float()) :: float() 118 | @doc """ 119 | Sine in-out easing function 120 | 121 | ![Sine in-out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/sine/in_out.png "Sine in-out easing visulizations created by Andrey Sitnik and Ivan Solovev") 122 | 123 | ## Example 124 | 125 | iex> Easing.sine_in_out(0.1) 126 | 0.024471741852423234 127 | """ 128 | def sine_in_out(progress), do: run({:sine, :in_out}, progress) 129 | 130 | @spec quadratic_in(float()) :: float() 131 | @doc """ 132 | Quadratic in easing function 133 | 134 | ![Quadratic in easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/quadratic/in.png "Quadratic in easing visulizations created by Andrey Sitnik and Ivan Solovev") 135 | 136 | ## Example 137 | 138 | iex> Easing.quadratic_in(0.1) 139 | 0.010000000000000002 140 | """ 141 | def quadratic_in(progress), do: run({:quadratic, :in}, progress) 142 | 143 | @spec quadratic_out(float()) :: float() 144 | @doc """ 145 | Quadratic out easing function 146 | 147 | ![Quadratic out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/quadratic/out.png "Quadratic out easing visulizations created by Andrey Sitnik and Ivan Solovev") 148 | 149 | ## Example 150 | 151 | iex> Easing.quadratic_out(0.1) 152 | 0.18999999999999995 153 | """ 154 | def quadratic_out(progress), do: run({:quadratic, :out}, progress) 155 | 156 | @spec quadratic_in_out(float()) :: float() 157 | @doc """ 158 | Quadratic in-out easing function 159 | 160 | ![Quadratic in-out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/quadratic/in_out.png "Quadratic in-out easing visulizations created by Andrey Sitnik and Ivan Solovev") 161 | 162 | ## Example 163 | 164 | iex> Easing.quadratic_in_out(0.1) 165 | 0.020000000000000004 166 | """ 167 | def quadratic_in_out(progress), do: run({:quadratic, :in_out}, progress) 168 | 169 | @spec cubic_in(float()) :: float() 170 | @doc """ 171 | Cubic in easing function 172 | 173 | ![Cubic in easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/cubic/in.png "Cubic in easing visulizations created by Andrey Sitnik and Ivan Solovev") 174 | 175 | ## Example 176 | 177 | iex> Easing.cubic_in(0.1) 178 | 0.0010000000000000002 179 | """ 180 | def cubic_in(progress), do: run({:cubic, :in}, progress) 181 | 182 | @spec cubic_out(float()) :: float() 183 | @doc """ 184 | Cubic out easing function 185 | 186 | ![Cubic out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/cubic/out.png "Cubic out easing visulizations created by Andrey Sitnik and Ivan Solovev") 187 | 188 | ## Example 189 | 190 | iex> Easing.cubic_out(0.1) 191 | 0.2709999999999999 192 | """ 193 | def cubic_out(progress), do: run({:cubic, :out}, progress) 194 | 195 | @spec cubic_in_out(float()) :: float() 196 | @doc """ 197 | Cubic in-out easing function 198 | 199 | ![Cubic in-out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/cubic/in_out.png "Cubic in-out easing visulizations created by Andrey Sitnik and Ivan Solovev") 200 | 201 | ## Example 202 | 203 | iex> Easing.cubic_in_out(0.1) 204 | 0.004000000000000001 205 | """ 206 | def cubic_in_out(progress), do: run({:cubic, :in_out}, progress) 207 | 208 | @spec quartic_in(float()) :: float() 209 | @doc """ 210 | Quartic in easing function 211 | 212 | ![Quartic in easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/quartic/in.png "Quartic in easing visulizations created by Andrey Sitnik and Ivan Solovev") 213 | 214 | ## Example 215 | 216 | iex> Easing.quartic_in(0.1) 217 | 1.0000000000000002e-4 218 | """ 219 | def quartic_in(progress), do: run({:quartic, :in}, progress) 220 | 221 | @spec quartic_out(float()) :: float() 222 | @doc """ 223 | Quartic out easing function 224 | 225 | ![Quartic out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/quartic/out.png "Quartic out easing visulizations created by Andrey Sitnik and Ivan Solovev") 226 | 227 | ## Example 228 | 229 | iex> Easing.quartic_out(0.1) 230 | 0.3439 231 | """ 232 | def quartic_out(progress), do: run({:quartic, :out}, progress) 233 | 234 | @spec quartic_in_out(float()) :: float() 235 | @doc """ 236 | Quartic in-out easing function 237 | 238 | ![Quartic in-out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/quartic/in_out.png "Quartic in-out easing visulizations created by Andrey Sitnik and Ivan Solovev") 239 | 240 | ## Example 241 | 242 | iex> Easing.quartic_in_out(0.1) 243 | 8.000000000000001e-4 244 | """ 245 | def quartic_in_out(progress), do: run({:quartic, :in_out}, progress) 246 | 247 | @spec quintic_in(float()) :: float() 248 | @doc """ 249 | Quintic in easing function 250 | 251 | ![Quintic in easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/quintic/in.png "Quintic in easing visulizations created by Andrey Sitnik and Ivan Solovev") 252 | 253 | ## Example 254 | 255 | iex> Easing.quintic_in(0.1) 256 | 1.0000000000000003e-5 257 | """ 258 | def quintic_in(progress), do: run({:quintic, :in}, progress) 259 | 260 | @spec quintic_out(float()) :: float() 261 | @doc """ 262 | Quintic out easing function 263 | 264 | ![Quintic out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/quintic/out.png "Quintic out easing visulizations created by Andrey Sitnik and Ivan Solovev") 265 | 266 | ## Example 267 | 268 | iex> Easing.quintic_out(0.1) 269 | 0.40950999999999993 270 | """ 271 | def quintic_out(progress), do: run({:quintic, :out}, progress) 272 | 273 | @spec quintic_in_out(float()) :: float() 274 | @doc """ 275 | Quintic in-out easing function 276 | 277 | ![Quintic in-out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/quintic/in_out.png "Quintic in-out easing visulizations created by Andrey Sitnik and Ivan Solovev") 278 | 279 | ## Example 280 | 281 | iex> Easing.quintic_in_out(0.1) 282 | 1.6000000000000004e-4 283 | """ 284 | def quintic_in_out(progress), do: run({:quintic, :in_out}, progress) 285 | 286 | @spec exponential_in(float()) :: float() 287 | @doc """ 288 | Exponential in easing function 289 | 290 | ![Exponential in easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/exponential/in.png "Expoenential in easing visulizations created by Andrey Sitnik and Ivan Solovev") 291 | 292 | ## Example 293 | 294 | iex> Easing.exponential_in(0.1) 295 | 0.001953125 296 | """ 297 | def exponential_in(progress), do: run({:exponential, :in}, progress) 298 | 299 | @spec exponential_out(float()) :: float() 300 | @doc """ 301 | Exponential out easing function 302 | 303 | ![Exponential out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/exponential/out.png "Expoenential out easing visulizations created by Andrey Sitnik and Ivan Solovev") 304 | 305 | ## Example 306 | 307 | iex> Easing.exponential_out(0.1) 308 | 0.5 309 | """ 310 | def exponential_out(progress), do: run({:exponential, :out}, progress) 311 | 312 | @spec exponential_in_out(float()) :: float() 313 | @doc """ 314 | Exponential in-out easing function 315 | 316 | ![Exponential in-out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/exponential/in_out.png "Expoenential in-out easing visulizations created by Andrey Sitnik and Ivan Solovev") 317 | 318 | ## Example 319 | 320 | iex> Easing.exponential_in_out(0.1) 321 | 0.001953125 322 | """ 323 | def exponential_in_out(progress), do: run({:exponential, :in_out}, progress) 324 | 325 | @spec circular_in(float()) :: float() 326 | @doc """ 327 | Circular in easing function 328 | 329 | ![Circular in easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/circular/in.png "Circular in easing visulizations created by Andrey Sitnik and Ivan Solovev") 330 | 331 | ## Example 332 | 333 | iex> Easing.circular_in(0.1) 334 | 0.005012562893380035 335 | """ 336 | def circular_in(progress), do: run({:circular, :in}, progress) 337 | 338 | @spec circular_out(float()) :: float() 339 | @doc """ 340 | Circular out easing function 341 | 342 | ![Circular out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/circular/out.png "Circular out easing visulizations created by Andrey Sitnik and Ivan Solovev") 343 | 344 | ## Example 345 | 346 | iex> Easing.circular_out(0.1) 347 | 0.4358898943540673 348 | """ 349 | def circular_out(progress), do: run({:circular, :out}, progress) 350 | 351 | @spec circular_in_out(float()) :: float() 352 | @doc """ 353 | Circular in-out easing function 354 | 355 | ![Circular in-out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/circular/in_out.png "Circular in-out easing visulizations created by Andrey Sitnik and Ivan Solovev") 356 | 357 | ## Example 358 | 359 | iex> Easing.circular_in_out(0.1) 360 | 0.010102051443364402 361 | """ 362 | def circular_in_out(progress), do: run({:circular, :in_out}, progress) 363 | 364 | @spec back_in(float()) :: float() 365 | @doc """ 366 | Back in easing function 367 | 368 | ![Back in easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/back/in.png "Back in easing visulizations created by Andrey Sitnik and Ivan Solovev") 369 | 370 | ## Example 371 | 372 | iex> Easing.back_in(0.1) 373 | -0.014314220000000004 374 | """ 375 | def back_in(progress), do: run({:back, :in}, progress) 376 | 377 | @spec back_out(float()) :: float() 378 | @doc """ 379 | Back out easing function 380 | 381 | ![Back out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/back/out.png "Back out easing visulizations created by Andrey Sitnik and Ivan Solovev") 382 | 383 | ## Example 384 | 385 | iex> Easing.back_out(0.1) 386 | 0.40882797999999987 387 | """ 388 | def back_out(progress), do: run({:back, :out}, progress) 389 | 390 | @spec back_in_out(float()) :: float() 391 | @doc """ 392 | Back in-out easing function 393 | 394 | ![Back in-out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/back/in_out.png "Back in-out easing visulizations created by Andrey Sitnik and Ivan Solovev") 395 | 396 | ## Example 397 | 398 | iex> Easing.back_in_out(0.1) 399 | -0.037518552000000004 400 | """ 401 | def back_in_out(progress), do: run({:back, :in_out}, progress) 402 | 403 | @spec elastic_in(float()) :: float() 404 | @doc """ 405 | Elastic in easing function 406 | 407 | ![Elastic in easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/elastic/in.png "Elastic in easing visulizations created by Andrey Sitnik and Ivan Solovev") 408 | 409 | ## Example 410 | 411 | iex> Easing.elastic_in(0.1) 412 | 0.001953125 413 | """ 414 | def elastic_in(progress), do: run({:elastic, :in}, progress) 415 | 416 | @spec elastic_out(float()) :: float() 417 | @doc """ 418 | Elastic out easing function 419 | 420 | ![Elastic out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/elastic/out.png "Elastic out easing visulizations created by Andrey Sitnik and Ivan Solovev") 421 | 422 | ## Example 423 | 424 | iex> Easing.elastic_out(0.1) 425 | 1.25 426 | """ 427 | def elastic_out(progress), do: run({:elastic, :out}, progress) 428 | 429 | @spec elastic_in_out(float()) :: float() 430 | @doc """ 431 | Elastic in-out easing function 432 | 433 | ![Elastic in-out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/elastic/in_out.png "Elastic in-out easing visulizations created by Andrey Sitnik and Ivan Solovev") 434 | 435 | ## Example 436 | 437 | iex> Easing.elastic_in_out(0.1) 438 | 3.39156597005722e-4 439 | """ 440 | def elastic_in_out(progress), do: run({:elastic, :in_out}, progress) 441 | 442 | @spec bounce_in(float()) :: float() 443 | @doc """ 444 | Bounce in easing function 445 | 446 | ![Bounce in easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/bounce/in.png "Bounce in easing visulizations created by Andrey Sitnik and Ivan Solovev") 447 | 448 | ## Example 449 | 450 | iex> Easing.bounce_in(0.1) 451 | 0.01187500000000008 452 | """ 453 | def bounce_in(progress), do: run({:bounce, :in}, progress) 454 | 455 | @spec bounce_out(float()) :: float() 456 | @doc """ 457 | Bounce out easing function 458 | 459 | ![Bounce out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/bounce/out.png "Bounce out easing visulizations created by Andrey Sitnik and Ivan Solovev") 460 | 461 | ## Example 462 | 463 | iex> Easing.bounce_out(0.1) 464 | 0.07562500000000001 465 | """ 466 | def bounce_out(progress), do: run({:bounce, :out}, progress) 467 | 468 | @spec bounce_in_out(float()) :: float() 469 | @doc """ 470 | Bounce in-out easing function 471 | 472 | ![Bounce in-out easing visualizations created by Andrey Sitnik and Ivan Solovev](assets/images/bounce/in_out.png "Bounce in-out easing visulizations created by Andrey Sitnik and Ivan Solovev") 473 | 474 | ## Example 475 | 476 | iex> Easing.bounce_in_out(0.1) 477 | 0.030000000000000027 478 | """ 479 | def bounce_in_out(progress), do: run({:bounce, :in_out}, progress) 480 | 481 | @spec run(easing_function_or_tuple(), float()) :: float() 482 | @doc """ 483 | Easing calculation. Take a tupal of atoms `{direction, type}` and the progress is a value 484 | between 0 - 1 that represents the animation progress. (0 = beginning, 1 = end) 485 | 486 | * directions: `:in`, `:out`, and `:in_out` 487 | * types: `:sine`, `:quadratic`, `:cubic`, `:quartic`, `:quintic`, :`exponential`, `:circular`, `:back`, `:elastic`, `:bounce` 488 | * progress: value between `0` and `1` that represents the % of the animation state. 489 | * options: keyword list 490 | - round: `true` - will round the result up with a precision of 2 491 | """ 492 | def run(easing_function, progress) do 493 | cond do 494 | progress == 0 -> 0.0 495 | progress == 1 -> 1.0 496 | true -> do_run(easing_function, progress) 497 | end 498 | end 499 | 500 | # Linear 501 | 502 | defp do_run({:linear, _direction}, progress), do: progress 503 | 504 | # Sine 505 | 506 | defp do_run({:sine, :in}, progress) do 507 | 1 - :math.cos((progress * :math.pi()) / 2) 508 | end 509 | 510 | defp do_run({:sine, :out}, progress) do 511 | :math.sin((progress * :math.pi()) / 2) 512 | end 513 | 514 | defp do_run({:sine, :in_out}, progress) do 515 | -1 * (:math.cos(:math.pi() * progress) - 1) / 2 516 | end 517 | 518 | # Quadratic 519 | 520 | defp do_run({:quadratic, :in}, progress) do 521 | :math.pow(progress, 2) 522 | end 523 | 524 | defp do_run({:quadratic, :out}, progress) do 525 | 1 - (1 - progress) * (1 - progress) 526 | end 527 | 528 | defp do_run({:quadratic, :in_out}, progress) do 529 | if progress < 0.5 do 530 | 2 * :math.pow(progress, 2) 531 | else 532 | 1 - :math.pow(-2 * progress + 2, 2) / 2 533 | end 534 | end 535 | 536 | # Cubic 537 | 538 | defp do_run({:cubic, :in}, progress) do 539 | :math.pow(progress, 3) 540 | end 541 | 542 | defp do_run({:cubic, :out}, progress) do 543 | 1 - :math.pow(1 - progress, 3) 544 | end 545 | 546 | defp do_run({:cubic, :in_out}, progress) do 547 | if progress < 0.5 do 548 | 4 * :math.pow(progress, 3) 549 | else 550 | 1 - :math.pow(-2 * progress + 2, 3) / 2 551 | end 552 | end 553 | 554 | # Quartic 555 | 556 | defp do_run({:quartic, :in}, progress) do 557 | :math.pow(progress, 4) 558 | end 559 | 560 | defp do_run({:quartic, :out}, progress) do 561 | 1 - :math.pow(1 - progress, 4) 562 | end 563 | 564 | defp do_run({:quartic, :in_out}, progress) do 565 | if progress < 0.5 do 566 | 8 * :math.pow(progress, 4) 567 | else 568 | 1 - :math.pow(-2 * progress + 2, 4) / 2 569 | end 570 | end 571 | 572 | # Quintic 573 | 574 | defp do_run({:quintic, :in}, progress) do 575 | :math.pow(progress, 5) 576 | end 577 | 578 | defp do_run({:quintic, :out}, progress) do 579 | 1 - :math.pow(1 - progress, 5) 580 | end 581 | 582 | defp do_run({:quintic, :in_out}, progress) do 583 | if progress < 0.5 do 584 | 16 * :math.pow(progress, 5) 585 | else 586 | 1 - :math.pow(-2 * progress + 2, 5) / 2 587 | end 588 | end 589 | 590 | # Exponential 591 | 592 | defp do_run({:exponential, :in}, progress) do 593 | :math.pow(2, 10 * progress - 10) 594 | end 595 | 596 | defp do_run({:exponential, :out}, progress) do 597 | 1 - :math.pow(2, -10 * progress) 598 | end 599 | 600 | defp do_run({:exponential, :in_out}, progress) do 601 | cond do 602 | progress < 0.5 -> :math.pow(2, 20 * progress - 10) / 2 603 | true -> (2 - :math.pow(2, -20 * progress + 10)) / 2 604 | end 605 | end 606 | 607 | # Circular 608 | 609 | defp do_run({:circular, :in}, progress) do 610 | 1 - :math.sqrt(1 - :math.pow(progress, 2)) 611 | end 612 | 613 | defp do_run({:circular, :out}, progress) do 614 | :math.sqrt(1 - :math.pow(progress - 1, 2)) 615 | end 616 | 617 | defp do_run({:circular, :in_out}, progress) do 618 | if progress < 0.5 do 619 | (1 - :math.sqrt(1 - :math.pow(2 * progress, 2))) / 2 620 | else 621 | (:math.sqrt(1 - :math.pow(-2 * progress + 2, 2)) + 1) / 2 622 | end 623 | end 624 | 625 | # Back 626 | 627 | defp do_run({:back, :in}, progress) do 628 | c1 = 1.70158 629 | c3 = c1 + 1 630 | 631 | c3 * :math.pow(progress, 3) - c1 * :math.pow(progress, 2) 632 | end 633 | 634 | defp do_run({:back, :out}, progress) do 635 | c1 = 1.70158 636 | c3 = c1 + 1 637 | 638 | 1 + c3 * :math.pow(progress - 1, 3) + c1 * :math.pow(progress - 1, 2) 639 | end 640 | 641 | defp do_run({:back, :in_out}, progress) do 642 | c1 = 1.70158 643 | c2 = c1 * 1.525 644 | 645 | if progress < 0.5 do 646 | (:math.pow(2 * progress, 2) * ((c2 + 1) * 2 * progress - c2)) /2 647 | else 648 | (:math.pow(2 * progress - 2, 2) * ((c2 + 1) * (progress * 2 - 2) + c2) + 2) / 2 649 | end 650 | end 651 | 652 | # Elastic 653 | 654 | defp do_run({:elastic, :in}, progress) do 655 | c4 = (2 * :math.pi()) / 3 656 | 657 | -1 * :math.pow(2, 10 * progress - 10) * :math.sin((progress * 10 - 10.75) * c4) 658 | end 659 | 660 | defp do_run({:elastic, :out}, progress) do 661 | c4 = (2 * :math.pi()) / 3; 662 | 663 | :math.pow(2, -10 * progress) * :math.sin((progress * 10 - 0.75) * c4) + 1 664 | end 665 | 666 | defp do_run({:elastic, :in_out}, progress) do 667 | c5 = (2 * :math.pi()) / 4.5 668 | 669 | cond do 670 | progress < 0.5 -> -1 * (:math.pow(2, 20 * progress - 10) * :math.sin((20 * progress - 11.125) * c5)) / 2 671 | true -> (:math.pow(2, -20 * progress + 10) * :math.sin((20 * progress - 11.125) * c5)) / 2 + 1 672 | end 673 | end 674 | 675 | # Bounce 676 | 677 | defp do_run({:bounce, :in}, progress) do 678 | 1.0 - run({:bounce, :out}, 1.0 - progress) 679 | end 680 | 681 | defp do_run({:bounce, :out}, progress) do 682 | n1 = 7.5625 683 | d1 = 2.75 684 | 685 | cond do 686 | progress < 1 / d1 -> n1 * :math.pow(progress, 2) 687 | progress < 2 / d1 -> 688 | p1 = progress - 1.5 / d1 689 | n1 * :math.pow(p1, 2) + 0.75 690 | progress < 2.5 / d1 -> 691 | p2 = progress - 2.25 / d1 692 | n1 * :math.pow(p2, 2) + 0.9375 693 | true -> 694 | p3 = progress - 2.625 / d1 695 | n1 * :math.pow(p3, 2) + 0.984375 696 | end 697 | end 698 | 699 | defp do_run({:bounce, :in_out}, progress) do 700 | if progress < 0.5 do 701 | (1 - run({:bounce, :out}, 1 - 2 * progress)) / 2 702 | else 703 | (1 + run({:bounce, :out}, 2 * progress - 1)) / 2 704 | end 705 | end 706 | end 707 | -------------------------------------------------------------------------------- /lib/easing/range.ex: -------------------------------------------------------------------------------- 1 | defmodule Easing.Range do 2 | @moduledoc """ 3 | Range struct for Easing 4 | 5 | This struct is basically a reimplementation of Elixir's `Range` struct 6 | but removing the limitations on only working with `Integer` constraints and steps 7 | """ 8 | defstruct first: nil, last: nil, step: nil 9 | 10 | @one_second 1_000 11 | 12 | @type range :: %Easing.Range{first: number(), last: number(), step: number()} 13 | 14 | @spec new(number(), number(), number()) :: range() 15 | @doc """ 16 | Convenience function for creating a new `Easing.Range` struct 17 | 18 | * first: represents the starting % of the range. Value should be: `value >= 0 and < 1` 19 | * last: represents the ending % of the range. Value should be: `value > 0 and <= 1` 20 | * step: value representing what the incremental value is between `first` and `last`. Can represent 21 | """ 22 | def new(first, last, step) do 23 | %__MODULE__{first: first, last: last, step: step} 24 | end 25 | 26 | @spec calculate(integer(), integer()) :: range() 27 | @doc """ 28 | Creates a new `Easing.Range` struct from a desired duration and target fps 29 | 30 | * duration_in_ms - total duration of the animation, only accepts `Integer` 31 | * fps - target frames per second of the animation, only accepts `Integer` 32 | 33 | ## Examples: 34 | iex> Easing.Range.calculate(1000, 1) 35 | %Easing.Range{first: 0, last: 1, step: 1.0} 36 | """ 37 | def calculate(duration_in_ms, fps) when is_integer(duration_in_ms) and is_integer(fps) do 38 | %__MODULE__{first: 0, last: 1, step: (@one_second / fps) / duration_in_ms} 39 | end 40 | def calculate(duration_in_ms, fps) do 41 | raise ArgumentError, "Easing.Range.calculate/2 can only accept values in Integer form " <> 42 | "got: (#{duration_in_ms}, #{fps})" 43 | end 44 | 45 | @spec size(range()) :: integer() 46 | @doc """ 47 | Returns the size of the `Easing.Range` 48 | 49 | Sizes are *inclusive* across a range. So a range from `0` - `1` with a step of `0.1` will have 50 | `11` values, not `10` because the `0` value is included in that result. 51 | 52 | ## Examples: 53 | iex> Easing.Range.calculate(1000, 60) |> Easing.Range.size() 54 | 61 55 | """ 56 | def size(%{__struct__: __MODULE__, first: first, last: last, step: step}) do 57 | (abs(:erlang./(last - first, step)) |> Kernel.trunc()) + 1 58 | end 59 | 60 | 61 | defimpl Enumerable, for: Easing.Range do 62 | def reduce(%{__struct__: Easing.Range, first: first, last: last, step: step}, acc, fun) do 63 | reduce(first, last, acc, fun, step) 64 | end 65 | 66 | defp reduce(_first, _last, {:halt, acc}, _fun, _step) do 67 | {:halted, acc} 68 | end 69 | 70 | defp reduce(first, last, {:suspend, acc}, fun, step) do 71 | {:suspended, acc, &reduce(first, last, &1, fun, step)} 72 | end 73 | 74 | defp reduce(first, last, {:cont, acc}, fun, step) do 75 | cond do 76 | (step > 0 and first > last) or (step < 0 and first < last) -> 77 | acc = case acc do 78 | [[]] = acc -> acc 79 | [] = acc -> acc 80 | _ -> fun.(last, acc) |> elem(1) 81 | end 82 | 83 | {:done, dedup_last(acc)} 84 | true -> 85 | reduce(first + step, last, fun.(first, acc), fun, step) 86 | end 87 | end 88 | 89 | defp dedup_last([acc]) when is_list(acc), do: [dedup_last(acc)] 90 | defp dedup_last([last, last | acc]), do: [last | acc] 91 | defp dedup_last(acc), do: acc 92 | 93 | @spec member?(%Easing.Range{}, any) :: {:ok, boolean} 94 | def member?(%{__struct__: Easing.Range, first: first, last: last, step: step} = range, value) do 95 | cond do 96 | Easing.Range.size(range) == 0 -> 97 | {:ok, false} 98 | 99 | first <= last -> 100 | {:ok, first <= value and value <= last and rem(value - first, step) == 0} 101 | 102 | true -> 103 | {:ok, last <= value and value <= first and rem(value - first, step) == 0} 104 | end 105 | end 106 | 107 | def count(range) do 108 | {:ok, Easing.Range.size(range)} 109 | end 110 | 111 | def slice(%{__struct__: Easing.Range, first: first, step: step} = range) do 112 | {:ok, Easing.Range.size(range), &slice(first + &1 * step, step, &2)} 113 | end 114 | 115 | defp slice(current, _step, 1), do: [current] 116 | defp slice(current, step, remaining), do: [current | slice(current + step, step, remaining - 1)] 117 | end 118 | end 119 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Easing.MixProject do 2 | use Mix.Project 3 | 4 | @version "0.3.1" 5 | @scm_url "https://github.com/DockYard/easing" 6 | 7 | def project do 8 | [ 9 | app: :easing, 10 | version: @version, 11 | elixir: "~> 1.13", 12 | start_permanent: Mix.env() == :prod, 13 | elixirc_paths: elixirc_paths(Mix.env()), 14 | consolidate_protocols: Mix.env() != :test, 15 | package: package(), 16 | description: description(), 17 | source_url: @scm_url, 18 | docs: docs(), 19 | deps: deps() 20 | ] 21 | end 22 | 23 | defp elixirc_paths(:test), do: ["lib", "test/support"] 24 | defp elixirc_paths(_), do: ["lib"] 25 | 26 | # Run "mix help compile.app" to learn about applications. 27 | def application do 28 | [ 29 | extra_applications: [:logger] 30 | ] 31 | end 32 | 33 | def description(), do: "Easing function calculations" 34 | 35 | defp package do 36 | [ 37 | maintainers: ["Brian Cardarella"], 38 | licenses: ["MIT"], 39 | links: %{"GitHub" => @scm_url}, 40 | files: 41 | ~w(lib CHANGELOG.md LICENSE.md mix.exs README.md .formatter.exs) 42 | ] 43 | end 44 | 45 | defp docs do 46 | [ 47 | main: "readme", 48 | source_ref: "v#{@version}", 49 | assets: "assets/", 50 | extras: ["README.md"] 51 | ] 52 | end 53 | 54 | # Run "mix help deps" to learn about dependencies. 55 | defp deps do 56 | [ 57 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} 58 | ] 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, 3 | "ex_doc": {:hex, :ex_doc, "0.28.0", "7eaf526dd8c80ae8c04d52ac8801594426ae322b52a6156cd038f30bafa8226f", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "e55cdadf69a5d1f4cfd8477122ebac5e1fadd433a8c1022dafc5025e48db0131"}, 4 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 5 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 6 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 7 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.1", "264fc6864936b59fedb3ceb89998c64e9bb91945faf1eb115d349b96913cc2ef", [:mix], [], "hexpm", "23c31d0ec38c97bf9adde35bc91bc8e1181ea5202881f48a192f4aa2d2cf4d59"}, 8 | } 9 | -------------------------------------------------------------------------------- /test/easing/range_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Easing.RangeTest do 2 | use ExUnit.Case 3 | doctest Easing.Range 4 | 5 | test "animation range produces full range of values" do 6 | range = %Easing.Range{first: 0, last: 1, step: 0.1} 7 | 8 | list = Enum.to_list(range) 9 | 10 | assert length(list) == 12 11 | assert Enum.at(list, 0) == 0.0 12 | assert Enum.at(list, length(list) - 1) == 1.0 13 | 14 | range = %Easing.Range{first: 0, last: 10, step: 3} 15 | 16 | list = Enum.to_list(range) 17 | 18 | assert list == [0, 3, 6, 9, 10] 19 | end 20 | 21 | test "reverse ranges work with negative steps" do 22 | range = %Easing.Range{first: 10, last: 0, step: -1} 23 | 24 | list = Enum.to_list(range) 25 | 26 | assert list == [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0] 27 | end 28 | 29 | test "impossible ranges result in empty lists" do 30 | range = %Easing.Range{first: 1, last: 0, step: 1} 31 | 32 | list = Enum.to_list(range) 33 | 34 | assert list == [] 35 | 36 | range = %Easing.Range{first: 0, last: 1, step: -1} 37 | 38 | list = Enum.to_list(range) 39 | 40 | assert list == [] 41 | end 42 | 43 | test "range can safely expand impossible ranges" do 44 | range = %Easing.Range{first: 0, last: 5, step: -1} 45 | 46 | result = 47 | range 48 | |> Stream.map(&(&1*2)) 49 | |> Enum.to_list() 50 | 51 | assert result == [] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/easing_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EasingTest do 2 | use ExUnit.Case 3 | doctest Easing 4 | 5 | describe "Linear" do 6 | test "in" do 7 | assert Easing.run({:linear, :in}, 0) == 0 8 | assert Easing.run({:linear, :in}, 0.25) == 0.25 9 | assert Easing.run({:linear, :in}, 0.5) == 0.5 10 | assert Easing.run({:linear, :in}, 0.75) == 0.75 11 | assert Easing.run({:linear, :in}, 1) == 1 12 | end 13 | 14 | test "out" do 15 | assert Easing.run({:linear, :out}, 0) == 0 16 | assert Easing.run({:linear, :out}, 0.25) == 0.25 17 | assert Easing.run({:linear, :out}, 0.5) == 0.5 18 | assert Easing.run({:linear, :out}, 0.75) == 0.75 19 | assert Easing.run({:linear, :out}, 1) == 1 20 | end 21 | 22 | test "in out" do 23 | assert Easing.run({:linear, :in_out}, 0) == 0 24 | assert Easing.run({:linear, :in_out}, 0.25) == 0.25 25 | assert Easing.run({:linear, :in_out}, 0.5) == 0.5 26 | assert Easing.run({:linear, :in_out}, 0.75) == 0.75 27 | assert Easing.run({:linear, :in_out}, 1) == 1 28 | end 29 | end 30 | 31 | describe "Sine" do 32 | test "in" do 33 | assert Easing.run({:sine, :in}, 0) == 0 34 | assert Easing.run({:sine, :in}, 0.25) == 0.07612046748871326 35 | assert Easing.run({:sine, :in}, 0.5) == 0.2928932188134524 36 | assert Easing.run({:sine, :in}, 0.75) == 0.6173165676349102 37 | assert Easing.run({:sine, :in}, 1) == 1 38 | end 39 | 40 | test "out" do 41 | assert Easing.run({:sine, :out}, 0) == 0 42 | assert Easing.run({:sine, :out}, 0.25) == 0.3826834323650898 43 | assert Easing.run({:sine, :out}, 0.5) == 0.7071067811865475 44 | assert Easing.run({:sine, :out}, 0.75) == 0.9238795325112867 45 | assert Easing.run({:sine, :out}, 1) == 1 46 | end 47 | 48 | test "in out" do 49 | assert Easing.run({:sine, :in_out}, 0) == 0 50 | assert Easing.run({:sine, :in_out}, 0.25) == 0.1464466094067262 51 | assert Easing.run({:sine, :in_out}, 0.5) == 0.49999999999999994 52 | assert Easing.run({:sine, :in_out}, 0.75) == 0.8535533905932737 53 | assert Easing.run({:sine, :in_out}, 1) == 1 54 | end 55 | end 56 | 57 | describe "Quadratic" do 58 | test "in" do 59 | assert Easing.run({:quadratic, :in}, 0) == 0 60 | assert Easing.run({:quadratic, :in}, 0.25) == 0.0625 61 | assert Easing.run({:quadratic, :in}, 0.5) == 0.25 62 | assert Easing.run({:quadratic, :in}, 0.75) == 0.5625 63 | assert Easing.run({:quadratic, :in}, 1) == 1 64 | end 65 | 66 | test "out" do 67 | assert Easing.run({:quadratic, :out}, 0) == 0 68 | assert Easing.run({:quadratic, :out}, 0.25) == 0.4375 69 | assert Easing.run({:quadratic, :out}, 0.5) == 0.75 70 | assert Easing.run({:quadratic, :out}, 0.75) == 0.9375 71 | assert Easing.run({:quadratic, :out}, 1) == 1 72 | end 73 | 74 | test "in out" do 75 | assert Easing.run({:quadratic, :in_out}, 0) == 0 76 | assert Easing.run({:quadratic, :in_out}, 0.25) == 0.125 77 | assert Easing.run({:quadratic, :in_out}, 0.5) == 0.5 78 | assert Easing.run({:quadratic, :in_out}, 0.75) == 0.875 79 | assert Easing.run({:quadratic, :in_out}, 1) == 1 80 | end 81 | end 82 | 83 | describe "Cubic" do 84 | test "in" do 85 | assert Easing.run({:cubic, :in}, 0) == 0 86 | assert Easing.run({:cubic, :in}, 0.25) == 0.015625 87 | assert Easing.run({:cubic, :in}, 0.5) == 0.125 88 | assert Easing.run({:cubic, :in}, 0.75) == 0.421875 89 | assert Easing.run({:cubic, :in}, 1) == 1 90 | end 91 | 92 | test "out" do 93 | assert Easing.run({:cubic, :out}, 0) == 0 94 | assert Easing.run({:cubic, :out}, 0.25) == 0.578125 95 | assert Easing.run({:cubic, :out}, 0.5) == 0.875 96 | assert Easing.run({:cubic, :out}, 0.75) == 0.984375 97 | assert Easing.run({:cubic, :out}, 1) == 1 98 | end 99 | 100 | test "in out" do 101 | assert Easing.run({:cubic, :in_out}, 0) == 0 102 | assert Easing.run({:cubic, :in_out}, 0.25) == 0.0625 103 | assert Easing.run({:cubic, :in_out}, 0.5) == 0.5 104 | assert Easing.run({:cubic, :in_out}, 0.75) == 0.9375 105 | assert Easing.run({:cubic, :in_out}, 1) == 1 106 | end 107 | end 108 | 109 | describe "Quartic" do 110 | test "in" do 111 | assert Easing.run({:quartic, :in}, 0) == 0 112 | assert Easing.run({:quartic, :in}, 0.25) == 0.00390625 113 | assert Easing.run({:quartic, :in}, 0.5) == 0.0625 114 | assert Easing.run({:quartic, :in}, 0.75) == 0.31640625 115 | assert Easing.run({:quartic, :in}, 1) == 1 116 | end 117 | 118 | test "out" do 119 | assert Easing.run({:quartic, :out}, 0) == 0 120 | assert Easing.run({:quartic, :out}, 0.25) == 0.68359375 121 | assert Easing.run({:quartic, :out}, 0.5) == 0.9375 122 | assert Easing.run({:quartic, :out}, 0.75) == 0.99609375 123 | assert Easing.run({:quartic, :out}, 1) == 1 124 | end 125 | 126 | test "in out" do 127 | assert Easing.run({:quartic, :in_out}, 0) == 0 128 | assert Easing.run({:quartic, :in_out}, 0.25) == 0.03125 129 | assert Easing.run({:quartic, :in_out}, 0.5) == 0.5 130 | assert Easing.run({:quartic, :in_out}, 0.75) == 0.96875 131 | assert Easing.run({:quartic, :in_out}, 1) == 1 132 | end 133 | end 134 | 135 | describe "Quintic" do 136 | test "in" do 137 | assert Easing.run({:quintic, :in}, 0) == 0 138 | assert Easing.run({:quintic, :in}, 0.25) == 0.0009765625 139 | assert Easing.run({:quintic, :in}, 0.5) == 0.03125 140 | assert Easing.run({:quintic, :in}, 0.75) == 0.2373046875 141 | assert Easing.run({:quintic, :in}, 1) == 1 142 | end 143 | 144 | test "out" do 145 | assert Easing.run({:quintic, :out}, 0) == 0 146 | assert Easing.run({:quintic, :out}, 0.25) == 0.7626953125 147 | assert Easing.run({:quintic, :out}, 0.5) == 0.96875 148 | assert Easing.run({:quintic, :out}, 0.75) == 0.9990234375 149 | assert Easing.run({:quintic, :out}, 1) == 1 150 | end 151 | 152 | test "in out" do 153 | assert Easing.run({:quintic, :in_out}, 0) == 0 154 | assert Easing.run({:quintic, :in_out}, 0.25) == 0.015625 155 | assert Easing.run({:quintic, :in_out}, 0.5) == 0.5 156 | assert Easing.run({:quintic, :in_out}, 0.75) == 0.984375 157 | assert Easing.run({:quintic, :in_out}, 1) == 1 158 | end 159 | end 160 | 161 | describe "Exponential" do 162 | test "in" do 163 | assert Easing.run({:exponential, :in}, 0) == 0 164 | assert Easing.run({:exponential, :in}, 0.25) == 0.005524271728019903 165 | assert Easing.run({:exponential, :in}, 0.5) == 0.03125 166 | assert Easing.run({:exponential, :in}, 0.75) == 0.1767766952966369 167 | assert Easing.run({:exponential, :in}, 1) == 1 168 | end 169 | 170 | test "out" do 171 | assert Easing.run({:exponential, :out}, 0) == 0 172 | assert Easing.run({:exponential, :out}, 0.25) == 0.8232233047033631 173 | assert Easing.run({:exponential, :out}, 0.5) == 0.96875 174 | assert Easing.run({:exponential, :out}, 0.75) == 0.99447572827198 175 | assert Easing.run({:exponential, :out}, 1) == 1 176 | end 177 | 178 | test "in out" do 179 | assert Easing.run({:exponential, :in_out}, 0) == 0 180 | assert Easing.run({:exponential, :in_out}, 0.25) == 0.015625 181 | assert Easing.run({:exponential, :in_out}, 0.5) == 0.5 182 | assert Easing.run({:exponential, :in_out}, 0.75) == 0.984375 183 | assert Easing.run({:exponential, :in_out}, 1) == 1 184 | end 185 | end 186 | 187 | describe "Circular" do 188 | test "in" do 189 | assert Easing.run({:circular, :in}, 0) == 0 190 | assert Easing.run({:circular, :in}, 0.25) == 0.031754163448145745 191 | assert Easing.run({:circular, :in}, 0.5) == 0.1339745962155614 192 | assert Easing.run({:circular, :in}, 0.75) == 0.3385621722338523 193 | assert Easing.run({:circular, :in}, 1) == 1 194 | end 195 | 196 | test "out" do 197 | assert Easing.run({:circular, :out}, 0) == 0 198 | assert Easing.run({:circular, :out}, 0.25) == 0.6614378277661477 199 | assert Easing.run({:circular, :out}, 0.5) == 0.8660254037844386 200 | assert Easing.run({:circular, :out}, 0.75) == 0.9682458365518543 201 | assert Easing.run({:circular, :out}, 1) == 1 202 | end 203 | 204 | test "in out" do 205 | assert Easing.run({:circular, :in_out}, 0) == 0 206 | assert Easing.run({:circular, :in_out}, 0.25) == 0.0669872981077807 207 | assert Easing.run({:circular, :in_out}, 0.5) == 0.5 208 | assert Easing.run({:circular, :in_out}, 0.75) == 0.9330127018922193 209 | assert Easing.run({:circular, :in_out}, 1) == 1 210 | end 211 | end 212 | 213 | describe "Back" do 214 | test "in" do 215 | assert Easing.run({:back, :in}, 0) == 0 216 | assert Easing.run({:back, :in}, 0.25) == -0.06413656250000001 217 | assert Easing.run({:back, :in}, 0.5) == -0.08769750000000004 218 | assert Easing.run({:back, :in}, 0.75) == 0.1825903124999998 219 | assert Easing.run({:back, :in}, 1) == 1 220 | end 221 | 222 | test "out" do 223 | assert Easing.run({:back, :out}, 0) == 0 224 | assert Easing.run({:back, :out}, 0.25) == 0.8174096875000002 225 | assert Easing.run({:back, :out}, 0.5) == 1.0876975 226 | assert Easing.run({:back, :out}, 0.75) == 1.0641365625 227 | assert Easing.run({:back, :out}, 1) == 1 228 | end 229 | 230 | test "in out" do 231 | assert Easing.run({:back, :in_out}, 0) == 0 232 | assert Easing.run({:back, :in_out}, 0.25) == -0.09968184375 233 | assert Easing.run({:back, :in_out}, 0.5) == 0.5 234 | assert Easing.run({:back, :in_out}, 0.75) == 1.09968184375 235 | assert Easing.run({:back, :in_out}, 1) == 1 236 | end 237 | end 238 | 239 | describe "Elastic" do 240 | test "in" do 241 | assert Easing.run({:elastic, :in}, 0) == 0 242 | assert Easing.run({:elastic, :in}, 0.25) == -0.005524271728019903 243 | assert Easing.run({:elastic, :in}, 0.5) == -0.015625000000000045 244 | assert Easing.run({:elastic, :in}, 0.75) == 0.08838834764831832 245 | assert Easing.run({:elastic, :in}, 1) == 1 246 | end 247 | 248 | test "out" do 249 | assert Easing.run({:elastic, :out}, 0) == 0 250 | assert Easing.run({:elastic, :out}, 0.25) == 0.9116116523516816 251 | assert Easing.run({:elastic, :out}, 0.5) == 1.015625 252 | assert Easing.run({:elastic, :out}, 0.75) == 1.00552427172802 253 | assert Easing.run({:elastic, :out}, 1) == 1 254 | end 255 | 256 | test "in out" do 257 | assert Easing.run({:elastic, :in_out}, 0) == 0 258 | assert Easing.run({:elastic, :in_out}, 0.25) == 0.011969444423734044 259 | assert Easing.run({:elastic, :in_out}, 0.5) == 0.5 260 | assert Easing.run({:elastic, :in_out}, 0.75) == 0.988030555576266 261 | assert Easing.run({:elastic, :in_out}, 1) == 1 262 | end 263 | end 264 | 265 | describe "Bounce" do 266 | test "in" do 267 | assert Easing.run({:bounce, :in}, 0) == 0 268 | assert Easing.run({:bounce, :in}, 0.25) == 0.02734375 269 | assert Easing.run({:bounce, :in}, 0.5) == 0.234375 270 | assert Easing.run({:bounce, :in}, 0.75) == 0.52734375 271 | assert Easing.run({:bounce, :in}, 1) == 1 272 | end 273 | 274 | test "out" do 275 | assert Easing.run({:bounce, :out}, 0) == 0 276 | assert Easing.run({:bounce, :out}, 0.25) == 0.47265625 277 | assert Easing.run({:bounce, :out}, 0.5) == 0.765625 278 | assert Easing.run({:bounce, :out}, 0.75) == 0.97265625 279 | assert Easing.run({:bounce, :out}, 1) == 1 280 | end 281 | 282 | test "in out" do 283 | assert Easing.run({:bounce, :in_out}, 0) == 0 284 | assert Easing.run({:bounce, :in_out}, 0.25) == 0.1171875 285 | assert Easing.run({:bounce, :in_out}, 0.5) == 0.5 286 | assert Easing.run({:bounce, :in_out}, 0.75) == 0.8828125 287 | assert Easing.run({:bounce, :in_out}, 1) == 1 288 | end 289 | end 290 | end 291 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------