Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/animation/physics/force_field.ex
defmodule Raxol.Animation.Physics.ForceField do
@moduledoc """
Force field implementation for physics simulations.
Force fields apply forces to physics objects within their influence.
Types of force fields include:
* Point (radial forces emanating from a point)
* Directional (constant force in a direction, like wind)
* Vortex (spinning forces)
* Noise (random forces based on position)
* Custom (user-defined force function)
"""
alias Raxol.Animation.Physics.Vector
@type field_type :: :point | :directional | :vortex | :noise | :custom
@type t :: %__MODULE__{
type: field_type(),
position: Vector.t(),
direction: Vector.t(),
strength: float(),
radius: float(),
falloff: :linear | :quadratic | :none,
function: (any(), any() -> Vector.t()) | nil,
properties: map()
}
defstruct type: :point,
position: %Vector{},
direction: %Vector{x: +0.0, y: 1, z: +0.0},
strength: 1.0,
radius: 10.0,
falloff: :quadratic,
function: nil,
properties: %{}
@doc """
Creates a new point force field.
A point force field applies forces radiating from or towards a point.
Positive strength = repulsive, Negative strength = attractive.
## Options
* `:position` - Position of the field (default: origin)
* `:strength` - Strength of the field (default: 1.0)
* `:radius` - Radius of influence (default: 10.0)
* `:falloff` - How force decreases with distance (:linear, :quadratic, :none) (default: :quadratic)
"""
def point_field(opts \\ []) do
%__MODULE__{
type: :point,
position: Keyword.get(opts, :position, %Vector{}),
strength: Keyword.get(opts, :strength, 1.0),
radius: Keyword.get(opts, :radius, 10.0),
falloff: Keyword.get(opts, :falloff, :quadratic)
}
end
@doc """
Creates a new directional force field.
A directional force field applies a constant force in a specific direction,
like wind or gravity.
## Options
* `:direction` - Direction of the force (default: up)
* `:strength` - Strength of the field (default: 1.0)
"""
def directional_field(opts \\ []) do
%__MODULE__{
type: :directional,
direction:
Keyword.get(opts, :direction, %Vector{x: +0.0, y: 1, z: +0.0})
|> Vector.normalize(),
strength: Keyword.get(opts, :strength, 1.0),
radius: :infinity
}
end
@doc """
Creates a new vortex force field.
A vortex force field applies spinning forces around an axis.
## Options
* `:position` - Center of the vortex (default: origin)
* `:direction` - Axis of rotation (default: up)
* `:strength` - Strength of the field (default: 1.0)
* `:radius` - Radius of influence (default: 10.0)
* `:falloff` - How force decreases with distance (:linear, :quadratic, :none) (default: :linear)
"""
def vortex_field(opts \\ []) do
%__MODULE__{
type: :vortex,
position: Keyword.get(opts, :position, %Vector{}),
direction:
Keyword.get(opts, :direction, %Vector{x: 0, y: 1, z: 0})
|> Vector.normalize(),
strength: Keyword.get(opts, :strength, 1.0),
radius: Keyword.get(opts, :radius, 10.0),
falloff: Keyword.get(opts, :falloff, :linear)
}
end
@doc """
Creates a new noise force field.
A noise force field applies pseudo-random forces based on position.
## Options
* `:strength` - Strength of the field (default: 1.0)
* `:scale` - Scale of the noise (default: 0.1)
* `:seed` - Random seed (default: random)
"""
def noise_field(opts \\ []) do
%__MODULE__{
type: :noise,
strength: Keyword.get(opts, :strength, 1.0),
radius: :infinity,
properties: %{
scale: Keyword.get(opts, :scale, 0.1),
seed: Keyword.get(opts, :seed, :rand.uniform(10_000))
}
}
end
@doc """
Creates a new custom force field.
A custom force field uses a user-provided function to calculate forces.
## Options
* `:function` - Function to calculate force (fn object, field -> force_vector end)
* `:properties` - Additional properties for the function (default: %{})
"""
def custom_field(function, opts \\ []) when is_function(function, 2) do
%__MODULE__{
type: :custom,
function: function,
properties: Keyword.get(opts, :properties, %{}),
radius: Keyword.get(opts, :radius, :infinity)
}
end
@doc """
Calculates the force applied by a field on an object.
"""
def calculate_force(%__MODULE__{} = field, object) do
case field.type do
:point -> calculate_point_force(field, object)
:directional -> calculate_directional_force(field, object)
:vortex -> calculate_vortex_force(field, object)
:noise -> calculate_noise_force(field, object)
:custom -> calculate_custom_force(field, object)
end
end
# Private functions
defp calculate_point_force(%__MODULE__{type: :point} = field, object) do
direction = Vector.subtract(object.position, field.position)
distance = Vector.magnitude(direction)
case check_distance_within_radius(field.radius, distance) do
:out_of_range ->
%Vector{}
:within_range ->
direction = normalize_direction(direction, distance)
force_magnitude = calculate_force_magnitude(field, distance)
Vector.scale(direction, force_magnitude)
end
end
defp check_distance_within_radius(:infinity, _distance), do: :within_range
defp check_distance_within_radius(radius, distance) when distance > radius,
do: :out_of_range
defp check_distance_within_radius(_radius, _distance), do: :within_range
defp normalize_direction(direction, distance) do
case distance > 0 do
true ->
Vector.scale(direction, 1 / distance)
false ->
theta = :rand.uniform() * 2 * :math.pi()
phi = :rand.uniform() * :math.pi()
Vector.from_spherical(1, theta, phi)
end
end
defp calculate_force_magnitude(field, distance) do
magnitude =
case field.falloff do
:none ->
field.strength
:linear ->
field.strength * (1 - distance / field.radius)
:quadratic ->
field.strength *
(1 - distance / field.radius * (distance / field.radius))
end
max(0, magnitude)
end
defp calculate_directional_force(
%__MODULE__{type: :directional} = field,
_object
) do
# Simply apply the force in the specified direction
Vector.scale(field.direction, field.strength)
end
defp calculate_vortex_force(%__MODULE__{type: :vortex} = field, object) do
to_object = Vector.subtract(object.position, field.position)
distance = Vector.magnitude(to_object)
case check_distance_within_radius(field.radius, distance) do
:out_of_range ->
%Vector{}
:within_range ->
calculate_vortex_force_at_point(field, to_object, distance)
end
end
defp calculate_vortex_force_at_point(field, to_object, distance) do
axis_projection =
Vector.scale(field.direction, Vector.dot(to_object, field.direction))
perpendicular = Vector.subtract(to_object, axis_projection)
perp_distance = Vector.magnitude(perpendicular)
case perp_distance > 0 do
true ->
perp_normalized = Vector.scale(perpendicular, 1 / perp_distance)
tangent = Vector.cross(field.direction, perp_normalized)
force_magnitude = calculate_force_magnitude(field, distance)
Vector.scale(tangent, force_magnitude)
false ->
%Vector{}
end
end
defp calculate_noise_force(%__MODULE__{type: :noise} = field, object) do
scale = field.properties.scale
seed = field.properties.seed
scaled_pos = scale_position(object.position, scale, seed)
noise_vector = calculate_noise_vector(scaled_pos)
Vector.scale(noise_vector, field.strength)
end
defp scale_position(position, scale, seed) do
%Vector{
x: position.x * scale + seed,
y: position.y * scale + seed * 2,
z: position.z * scale + seed * 3
}
end
defp calculate_noise_vector(%Vector{x: x, y: y, z: z}) do
offsets = [{0.2, 0.5}, {0.1, 0.3}, {0.3, 0.4}]
[x_val, y_val, z_val] =
Enum.map(offsets, fn {y_off, z_off} ->
calculate_noise_component(x, y, z, y_off, z_off)
end)
%Vector{x: x_val, y: y_val, z: z_val}
end
defp calculate_noise_component(x, y, z, y_offset, z_offset) do
:math.sin(x) * :math.cos(y + y_offset) * :math.sin(z + z_offset)
end
defp calculate_custom_force(%__MODULE__{type: :custom} = field, object) do
case field.function do
nil -> %Vector{}
fun -> fun.(object, field)
end
end
end