lib/scrollable/utility/acceleration.ex

defmodule FloUI.Scrollable.Acceleration do
  @moduledoc """
  Module for calculating the scroll speed for `Scenic.Scrollable` components.
  """

  alias Scenic.Math.Vector2

  @typedoc """
  Shorthand for `t:Scenic.Math.vector_2/0`.
  Consists of a tuple containing the x and y numeric values.
  """
  @type v2 :: Scenic.Math.vector_2()

  @typedoc """
  Data structure containing settings that define the behaviour of the `Scenic.Scrollable` components scroll speed and acceleration. Note that the `Scenic.Scrollable` content may not be able to move when the acceleration is set too low, or the mass and counter_pressure are set too high.

  Default settings:
  - acceleration: 20
  - mass: 1
  - counter_pressure: 0.1
  """
  @type settings :: %{
          optional(:acceleration) => number,
          optional(:mass) => number,
          optional(:counter_pressure) => number
        }

  @typedoc """
  Data structure with the necessary values to calculate the current scroll speed.
  """
  @type t :: %{
          acceleration: number,
          mass: number,
          counter_pressure: number,
          force: v2,
          speed: v2
        }

  defstruct acceleration: 20,
            mass: 1,
            counter_pressure: 0.1,
            force: {0, 0},
            speed: {0, 0}

  # Value with which to multiply a speed value, to convert it to the distance it would travel during one frame.
  @speed_to_distance_factor 0.1

  @doc """
  Initializes a `t:Scenic.Scrollable.Acceleration.t` state object based on the passed `t:Scenic.Scrollable.Acceleration.settings/0`.
  When nil is passed, the default settings will be used.
  """
  @spec init(settings) :: t
  def init(nil), do: %__MODULE__{}

  def init(settings) do
    Enum.reduce(settings, %__MODULE__{}, fn {key, value}, state ->
      Map.put(state, key, value)
    end)
  end

  @doc """
  Find out if the `Scenic.Scrollable` component is currently stationary.
  """
  @spec is_stationary?(t) :: boolean
  def is_stationary?(%{speed: {0, 0}}), do: true

  def is_stationary?(_), do: false

  @doc """
  Apply force in the specified direction to make the `Scenic.Scrollable` component move.
  """
  @spec apply_force(t, v2) :: t
  def apply_force(state, force) do
    Map.update(state, :speed, {0, 0}, fn speed ->
      Vector2.mul(force, state.acceleration)
      |> Vector2.div(state.mass)
      |> Vector2.add(speed)
    end)
  end

  @doc """
  Directly update the speed of the `Scenic.Scrollable` components scroll movement, to make it move at a certain velocity in the given direction.
  """
  @spec set_speed(t, v2) :: t
  def set_speed(state, speed) do
    %{state | speed: speed}
  end

  @doc """
  Apply counter pressure to the current `Scenic.Scrollable` comonents movement.
  The counter pressures strength is calculated based on the `Scenic.Scrollable` components current speed, the components mass set during initialization, and the counter pressure value set during initialization.
  """
  @spec apply_counter_pressure(t) :: t
  def apply_counter_pressure(state) do
    Map.update(state, :speed, {0, 0}, fn speed ->
      Vector2.invert(speed)
      |> Vector2.mul(state.counter_pressure)
      |> Vector2.mul(state.mass)
      |> Vector2.add(speed)
      |> Vector2.trunc()
    end)
  end

  @doc """
  Calculate the translation of a point based on the current speed.
  """
  @spec translate(t, v2) :: v2
  def translate(%{speed: speed}, position) do
    Vector2.mul(speed, @speed_to_distance_factor)
    |> Vector2.add(position)
  end
end