lib/easing.ex

defmodule Easing do
  @moduledoc """
  Easing function calculations

  Cribbed from: https://easings.net/
  """

  alias Easing.Range

  @type easing_tuple :: {atom(), atom()}
  @type range :: %Easing.Range{first: number(), last: number(), step: number()}
  @type easing_function :: function()
  @type easing_function_or_tuple :: easing_function() | easing_tuple()
  @type easings :: [float()]


  @spec to_list(range(), easing_function_or_tuple()) :: easings()
  @doc """
  Generates a list of animation frame values.

  A `Range` is used for the `range` argument. See the example

  ## Examples:

      iex> Easing.to_list(%Easing.Range{first: 0, last: 1, step: 0.1}, &Easing.sine_in(&1))
      [0.0, 0.01231165940486223, 0.04894348370484647, 0.10899347581163221, 0.19098300562505255, 0.2928932188134524, 0.41221474770752675, 0.5460095002604533, 0.6909830056250525, 0.8435655349597688, 1.0]

      iex> Easing.to_list(%Easing.Range{first: 0, last: 0.5, step: 0.1}, {:bounce, :in_out})
      [0.0, 0.030000000000000027, 0.11375000000000002, 0.04499999999999993, 0.3487500000000001, 0.5]
  """
  def to_list(%Range{} = range, easing) do
    range
    |> stream(easing)
    |> Enum.to_list()
  end

  @spec stream(range(), easing_function_or_tuple()) :: Enumerable.t()
  @doc """
  Generates a stream of animation frame values.

  A `Range` is used for the `range` argument. See the example

  ## Examples:
      iex> Easing.stream(%Easing.Range{first: 0, last: 1, step: 0.1}, &Easing.sine_in(&1)) |> Enum.to_list()
      [0.0, 0.01231165940486223, 0.04894348370484647, 0.10899347581163221, 0.19098300562505255, 0.2928932188134524, 0.41221474770752675, 0.5460095002604533, 0.6909830056250525, 0.8435655349597688, 1.0]

      iex> Easing.stream(%Easing.Range{first: 0, last: 0.5, step: 0.1}, {:bounce, :in_out}) |> Enum.to_list()
      [0.0, 0.030000000000000027, 0.11375000000000002, 0.04499999999999993, 0.3487500000000001, 0.5]

  """
  def stream(%Range{} = range, easing_function) when is_function(easing_function) do
    Stream.map(range, &easing_function.(&1))
  end
  def stream(%Range{} = range, easing_tuple) when is_tuple(easing_tuple) do
    Stream.map(range, &run(easing_tuple, &1))
  end

  @spec linear_in(float()) :: float()
  @doc """
  Linear in easing function

  ## Example

      iex> Easing.linear_in(0.1)
      0.1
  """
  def linear_in(progress), do: run({:linear, :in}, progress)

  @spec linear_out(float()) ::float()
  @doc """
  Linear out easing function

  ## Example

      iex> Easing.linear_out(0.1)
      0.1
  """
  def linear_out(progress), do: run({:linear, :out}, progress)

  @spec linear_in_out(float()) :: float()
  @doc """
  Linear in-out easing function

  ## Example

      iex> Easing.linear_in_out(0.1)
      0.1
  """
  def linear_in_out(progress), do: run({:linear, :in_out}, progress)

  @spec sine_in(float()) :: float()
  @doc """
  Sine in easing function

  ![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")

  ## Example

      iex> Easing.sine_in(0.1)
      0.01231165940486223
  """
  def sine_in(progress), do: run({:sine, :in}, progress)

  @spec sine_out(float()) :: float()
  @doc """
  Sine out easing function

  ![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")

  ## Example

      iex> Easing.sine_out(0.1)
      0.15643446504023087
  """
  def sine_out(progress), do: run({:sine, :out}, progress)

  @spec sine_in_out(float()) :: float()
  @doc """
  Sine in-out easing function

  ![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")

  ## Example

      iex> Easing.sine_in_out(0.1)
      0.024471741852423234
  """
  def sine_in_out(progress), do: run({:sine, :in_out}, progress)

  @spec quadratic_in(float()) :: float()
  @doc """
  Quadratic in easing function

  ![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")

  ## Example

      iex> Easing.quadratic_in(0.1)
      0.010000000000000002
  """
  def quadratic_in(progress), do: run({:quadratic, :in}, progress)

  @spec quadratic_out(float()) :: float()
  @doc """
  Quadratic out easing function

  ![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")

  ## Example

      iex> Easing.quadratic_out(0.1)
      0.18999999999999995
  """
  def quadratic_out(progress), do: run({:quadratic, :out}, progress)

  @spec quadratic_in_out(float()) :: float()
  @doc """
  Quadratic in-out easing function

  ![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")

  ## Example

      iex> Easing.quadratic_in_out(0.1)
      0.020000000000000004
  """
  def quadratic_in_out(progress), do: run({:quadratic, :in_out}, progress)

  @spec cubic_in(float()) :: float()
  @doc """
  Cubic in easing function

  ![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")

  ## Example

      iex> Easing.cubic_in(0.1)
      0.0010000000000000002
  """
  def cubic_in(progress), do: run({:cubic, :in}, progress)

  @spec cubic_out(float()) :: float()
  @doc """
  Cubic out easing function

  ![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")

  ## Example

      iex> Easing.cubic_out(0.1)
      0.2709999999999999
  """
  def cubic_out(progress), do: run({:cubic, :out}, progress)

  @spec cubic_in_out(float()) :: float()
  @doc """
  Cubic in-out easing function

  ![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")

  ## Example

      iex> Easing.cubic_in_out(0.1)
      0.004000000000000001
  """
  def cubic_in_out(progress), do: run({:cubic, :in_out}, progress)

  @spec quartic_in(float()) :: float()
  @doc """
  Quartic in easing function

  ![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")

  ## Example

      iex> Easing.quartic_in(0.1)
      1.0000000000000002e-4
  """
  def quartic_in(progress), do: run({:quartic, :in}, progress)

  @spec quartic_out(float()) :: float()
  @doc """
  Quartic out easing function

  ![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")

  ## Example

      iex> Easing.quartic_out(0.1)
      0.3439
  """
  def quartic_out(progress), do: run({:quartic, :out}, progress)

  @spec quartic_in_out(float()) :: float()
  @doc """
  Quartic in-out easing function

  ![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")

  ## Example

      iex> Easing.quartic_in_out(0.1)
      8.000000000000001e-4
  """
  def quartic_in_out(progress), do: run({:quartic, :in_out}, progress)

  @spec quintic_in(float()) :: float()
  @doc """
  Quintic in easing function

  ![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")

  ## Example

      iex> Easing.quintic_in(0.1)
      1.0000000000000003e-5
  """
  def quintic_in(progress), do: run({:quintic, :in}, progress)

  @spec quintic_out(float()) :: float()
  @doc """
  Quintic out easing function

  ![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")

  ## Example

      iex> Easing.quintic_out(0.1)
      0.40950999999999993
  """
  def quintic_out(progress), do: run({:quintic, :out}, progress)

  @spec quintic_in_out(float()) :: float()
  @doc """
  Quintic in-out easing function

  ![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")

  ## Example

      iex> Easing.quintic_in_out(0.1)
      1.6000000000000004e-4
  """
  def quintic_in_out(progress), do: run({:quintic, :in_out}, progress)

  @spec exponential_in(float()) :: float()
  @doc """
  Exponential in easing function

  ![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")

  ## Example

      iex> Easing.exponential_in(0.1)
      0.001953125
  """
  def exponential_in(progress), do: run({:exponential, :in}, progress)

  @spec exponential_out(float()) :: float()
  @doc """
  Exponential out easing function

  ![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")

  ## Example

      iex> Easing.exponential_out(0.1)
      0.5
  """
  def exponential_out(progress), do: run({:exponential, :out}, progress)

  @spec exponential_in_out(float()) :: float()
  @doc """
  Exponential in-out easing function

  ![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")

  ## Example

      iex> Easing.exponential_in_out(0.1)
      0.001953125
  """
  def exponential_in_out(progress), do: run({:exponential, :in_out}, progress)

  @spec circular_in(float()) :: float()
  @doc """
  Circular in easing function

  ![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")

  ## Example

      iex> Easing.circular_in(0.1)
      0.005012562893380035
  """
  def circular_in(progress), do: run({:circular, :in}, progress)

  @spec circular_out(float()) :: float()
  @doc """
  Circular out easing function

  ![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")

  ## Example

      iex> Easing.circular_out(0.1)
      0.4358898943540673
  """
  def circular_out(progress), do: run({:circular, :out}, progress)

  @spec circular_in_out(float()) :: float()
  @doc """
  Circular in-out easing function

  ![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")

  ## Example

      iex> Easing.circular_in_out(0.1)
      0.010102051443364402
  """
  def circular_in_out(progress), do: run({:circular, :in_out}, progress)

  @spec back_in(float()) :: float()
  @doc """
  Back in easing function

  ![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")

  ## Example

      iex> Easing.back_in(0.1)
      -0.014314220000000004
  """
  def back_in(progress), do: run({:back, :in}, progress)

  @spec back_out(float()) :: float()
  @doc """
  Back out easing function

  ![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")

  ## Example

      iex> Easing.back_out(0.1)
      0.40882797999999987
  """
  def back_out(progress), do: run({:back, :out}, progress)

  @spec back_in_out(float()) :: float()
  @doc """
  Back in-out easing function

  ![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")

  ## Example

      iex> Easing.back_in_out(0.1)
      -0.037518552000000004
  """
  def back_in_out(progress), do: run({:back, :in_out}, progress)

  @spec elastic_in(float()) :: float()
  @doc """
  Elastic in easing function

  ![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")

  ## Example

      iex> Easing.elastic_in(0.1)
      0.001953125
  """
  def elastic_in(progress), do: run({:elastic, :in}, progress)

  @spec elastic_out(float()) :: float()
  @doc """
  Elastic out easing function

  ![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")

  ## Example

      iex> Easing.elastic_out(0.1)
      1.25
  """
  def elastic_out(progress), do: run({:elastic, :out}, progress)

  @spec elastic_in_out(float()) :: float()
  @doc """
  Elastic in-out easing function

  ![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")

  ## Example

      iex> Easing.elastic_in_out(0.1)
      3.39156597005722e-4
  """
  def elastic_in_out(progress), do: run({:elastic, :in_out}, progress)

  @spec bounce_in(float()) :: float()
  @doc """
  Bounce in easing function

  ![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")

  ## Example

      iex> Easing.bounce_in(0.1)
      0.01187500000000008
  """
  def bounce_in(progress), do: run({:bounce, :in}, progress)

  @spec bounce_out(float()) :: float()
  @doc """
  Bounce out easing function

  ![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")

  ## Example

      iex> Easing.bounce_out(0.1)
      0.07562500000000001
  """
  def bounce_out(progress), do: run({:bounce, :out}, progress)

  @spec bounce_in_out(float()) :: float()
  @doc """
  Bounce in-out easing function

  ![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")

  ## Example

      iex> Easing.bounce_in_out(0.1)
      0.030000000000000027
  """
  def bounce_in_out(progress), do: run({:bounce, :in_out}, progress)

  @spec run(easing_function_or_tuple(), float()) :: float()
  @doc """
  Easing calculation. Take a tupal of atoms `{direction, type}` and the progress is a value
  between 0 - 1 that represents the animation progress. (0 = beginning, 1 = end)

  * directions: `:in`, `:out`, and `:in_out`
  * types: `:sine`, `:quadratic`, `:cubic`, `:quartic`, `:quintic`, :`exponential`, `:circular`, `:back`, `:elastic`, `:bounce`
  * progress: value between `0` and `1` that represents the % of the animation state.
  * options: keyword list
    - round: `true` - will round the result up with a precision of 2
  """
  def run(easing_function, progress) do
    cond do
      progress == 0 -> 0.0
      progress == 1 -> 1.0
      true -> do_run(easing_function, progress)
    end
  end

  # Linear

  defp do_run({:linear, _direction}, progress), do: progress

  # Sine

  defp do_run({:sine, :in}, progress) do
    1 - :math.cos((progress * :math.pi()) / 2)
  end

  defp do_run({:sine, :out}, progress) do
    :math.sin((progress * :math.pi()) / 2)
  end

  defp do_run({:sine, :in_out}, progress) do
    -1 * (:math.cos(:math.pi() * progress) - 1) / 2
  end

  # Quadratic

  defp do_run({:quadratic, :in}, progress) do
    :math.pow(progress, 2)
  end

  defp do_run({:quadratic, :out}, progress) do
    1 - (1 - progress) * (1 - progress)
  end

  defp do_run({:quadratic, :in_out}, progress) do
    if progress < 0.5 do
      2 * :math.pow(progress, 2)
    else
      1 - :math.pow(-2 * progress + 2, 2) / 2
    end
  end

  # Cubic

  defp do_run({:cubic, :in}, progress) do
    :math.pow(progress, 3)
  end

  defp do_run({:cubic, :out}, progress) do
    1 - :math.pow(1 - progress, 3)
  end

  defp do_run({:cubic, :in_out}, progress) do
    if progress < 0.5 do
      4 * :math.pow(progress, 3)
    else
      1 - :math.pow(-2 * progress + 2, 3) / 2
    end
  end

  # Quartic

  defp do_run({:quartic, :in}, progress) do
    :math.pow(progress, 4)
  end

  defp do_run({:quartic, :out}, progress) do
    1 - :math.pow(1 - progress, 4)
  end

  defp do_run({:quartic, :in_out}, progress) do
    if progress < 0.5 do
      8 * :math.pow(progress, 4)
    else
      1 - :math.pow(-2 * progress + 2, 4) / 2
    end
  end

  # Quintic

  defp do_run({:quintic, :in}, progress) do
    :math.pow(progress, 5)
  end

  defp do_run({:quintic, :out}, progress) do
    1 - :math.pow(1 - progress, 5)
  end

  defp do_run({:quintic, :in_out}, progress) do
    if progress < 0.5 do
      16 * :math.pow(progress, 5)
    else
      1 - :math.pow(-2 * progress + 2, 5) / 2
    end
  end

  # Exponential

  defp do_run({:exponential, :in}, progress) do
    :math.pow(2, 10 * progress - 10)
  end

  defp do_run({:exponential, :out}, progress) do
    1 - :math.pow(2, -10 * progress)
  end

  defp do_run({:exponential, :in_out}, progress) do
    cond do
      progress < 0.5 -> :math.pow(2, 20 * progress - 10) / 2
      true -> (2 - :math.pow(2, -20 * progress + 10)) / 2
    end
  end

  # Circular

  defp do_run({:circular, :in}, progress) do
    1 - :math.sqrt(1 - :math.pow(progress, 2))
  end

  defp do_run({:circular, :out}, progress) do
    :math.sqrt(1 - :math.pow(progress - 1, 2))
  end

  defp do_run({:circular, :in_out}, progress) do
    if progress < 0.5 do
      (1 - :math.sqrt(1 - :math.pow(2 * progress, 2))) / 2
    else
      (:math.sqrt(1 - :math.pow(-2 * progress + 2, 2)) + 1) / 2
    end
  end

  # Back

  defp do_run({:back, :in}, progress) do
    c1 = 1.70158
    c3 = c1 + 1

    c3 * :math.pow(progress, 3) - c1 * :math.pow(progress, 2)
  end

  defp do_run({:back, :out}, progress) do
    c1 = 1.70158
    c3 = c1 + 1

    1 + c3 * :math.pow(progress - 1, 3) + c1 * :math.pow(progress - 1, 2)
  end

  defp do_run({:back, :in_out}, progress) do
    c1 = 1.70158
    c2 = c1 * 1.525

    if progress < 0.5 do
      (:math.pow(2 * progress, 2) * ((c2 + 1) * 2 * progress - c2)) /2
    else
      (:math.pow(2 * progress - 2, 2) * ((c2 + 1) * (progress * 2 - 2) + c2) + 2) / 2
    end
  end

  # Elastic

  defp do_run({:elastic, :in}, progress) do
    c4 = (2 * :math.pi()) / 3

    -1 * :math.pow(2, 10 * progress - 10) * :math.sin((progress * 10 - 10.75) * c4)
  end

  defp do_run({:elastic, :out}, progress) do
    c4 = (2 * :math.pi()) / 3;

    :math.pow(2, -10 * progress) * :math.sin((progress * 10 - 0.75) * c4) + 1
  end

  defp do_run({:elastic, :in_out}, progress) do
    c5 = (2 * :math.pi()) / 4.5

    cond do
      progress < 0.5 -> -1 * (:math.pow(2, 20 * progress - 10) * :math.sin((20 * progress - 11.125) * c5)) / 2
      true -> (:math.pow(2, -20 * progress + 10) * :math.sin((20 * progress - 11.125) * c5)) / 2 + 1
    end
  end

  # Bounce

  defp do_run({:bounce, :in}, progress) do
    1.0 - run({:bounce, :out}, 1.0 - progress)
  end

  defp do_run({:bounce, :out}, progress) do
    n1 = 7.5625
    d1 = 2.75

    cond do
      progress < 1 / d1 -> n1 * :math.pow(progress, 2)
      progress < 2 / d1 ->
        p1 = progress - 1.5 / d1
        n1 * :math.pow(p1, 2) + 0.75
      progress < 2.5 / d1 ->
        p2 = progress - 2.25 / d1
        n1 * :math.pow(p2, 2) + 0.9375
      true ->
        p3 = progress - 2.625 / d1
        n1 * :math.pow(p3, 2) + 0.984375
    end
  end

  defp do_run({:bounce, :in_out}, progress) do
    if progress < 0.5 do
      (1 - run({:bounce, :out}, 1 - 2 * progress)) / 2
    else
      (1 + run({:bounce, :out}, 2 * progress - 1)) / 2
    end
  end
end