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