Current section

Files

Jump to
exray lib Exray Structs Vector2.ex
Raw

lib/Exray/Structs/Vector2.ex

defmodule Exray.Structs.Vector2 do
@moduledoc """
Vector2 TypedStruct. Defines functions for Vector2 from Raymath
"""
use TypedStruct
typedstruct enforce: true do
field :x, float, default: 0.0
field :y, float, default: 0.0
end
import Exray.Utils.Guards
alias Exray.Structs.Vector2
alias Exray.Utils.Math
def new(x, y) when is_number(x) and is_number(y), do: %__MODULE__{x: x / 1, y: y / 1}
def new({x, y}) when is_number(x) and is_number(y), do: new(x, y)
def new(x: x, y: y) when is_number(x) and is_number(y), do: new(x, y)
def new(%{x: x, y: y}) when is_number(x) and is_number(y), do: new(x, y)
@doc "Vector with components value 0.0f"
@spec zero() :: Vector2
def zero, do: new(x: 0.0, y: 0.0)
@doc "Vector with components value 1.0f"
@spec one() :: Vector2
def one, do: new(x: 1.0, y: 1.0)
# For some absolutely shit reason, I can't just write @doc here and have them pick it up,
# even with function headers. So I'm doing @doc on the _second_ one and will not elaborate.
@spec add(Vector2, Vector2) :: Vector2
def add(v1, v2) when is_vector2(v1) and is_vector2(v2),
do: new(x: v1.x + v2.x, y: v1.y + v2.y)
@doc """
Add vector and vector or float
"""
@spec add(Vector2, number) :: Vector2
def add(v, f) when is_vector2(v) and is_number(f),
do: new(x: v.x + f, y: v.y + f)
@spec subtract(Vector2, Vector2) :: Vector2
def subtract(v1, v2) when is_vector2(v1) and is_vector2(v2),
do: new(x: v1.x - v2.x, y: v1.y - v2.y)
@doc "Subtract vector by vector or float"
@spec subtract(Vector2, float) :: Vector2
def subtract(v, f) when is_vector2(v) and is_number(f), do: new(x: v.x - f, y: v.y - f)
@doc "Calculate vector length"
@spec length(Vector2) :: float
def length(v) when is_vector2(v), do: :math.sqrt(v.x * v.x + v.y * v.y)
@doc "Calculate vector square length"
@spec length_sqr(Vector2) :: float
def length_sqr(v) when is_vector2(v), do: v.x * v.x + v.y * v.y
@doc "Calculate two vectors dot product"
@spec dot_product(Vector2, Vector2) :: float
def dot_product(v1, v2) when is_vector2(v1) and is_vector2(v2), do: v1.x * v2.x + v1.y * v2.y
@doc "Calculate distance between two vectors"
@spec distance(Vector2, Vector2) :: float
def distance(v1, v2) when is_vector2(v1) and is_vector2(v2),
do: :math.sqrt((v1.x - v2.x) * (v1.x - v2.x) + (v1.y - v2.y) * (v1.y - v2.y))
@doc "Calculate square distance between two vectors"
@spec distance_sqr(Vector2, Vector2) :: float
def distance_sqr(v1, v2) when is_vector2(v1) and is_vector2(v2),
do: (v1.x - v2.x) * (v1.x - v2.x) + (v1.y - v2.y) * (v1.y - v2.y)
@doc """
Calculate angle between two vectors
NOTE: Angle is calculated from origin point (0, 0)
"""
@spec angle(Vector2, Vector2) :: float
def angle(v1, v2) when is_vector2(v1) and is_vector2(v2) do
dot = v1.x * v2.x + v1.y * v2.y
det = v1.x * v2.y - v1.y * v2.x
:math.atan2(det, dot)
end
@doc """
Calculate angle defined by a two vectors line
NOTE: Parameters need to be normalized
Current implementation should be aligned with glm::angle
"""
@spec line_angle(Vector2, Vector2) :: float
def line_angle(start, stop) when is_vector2(start) and is_vector2(stop) do
-:math.atan2(stop.y - start.y, stop.x - start.x)
end
@doc "Scale vector (multiply by value)"
@spec scale(Vector2, number) :: Vector2
def scale(v, scale) when is_vector2(v) and is_number(scale),
do: new(x: v.x * scale, y: v.y * scale)
@doc "Multiply vector by vector"
@spec multiply(Vector2, Vector2) :: Vector2
def multiply(v1, v2) when is_vector2(v1) and is_vector2(v2),
do: new(x: v1.x * v2.x, y: v1.y * v2.y)
@doc "Negate vector"
@spec negate(Vector2) :: Vector2
def negate(v) when is_vector2(v), do: new(x: -v.x, y: -v.y)
@doc "Divide vector by vector"
@spec divide(Vector2, Vector2) :: Vector2
def divide(v1, v2) when is_vector2(v1) and is_vector2(v2),
do: new(x: v1.x / v2.x, y: v1.y / v2.y)
@doc "Normalize provided vector"
@spec normalize(Vector2) :: Vector2
def normalize(v) when is_vector2(v) do
case :math.sqrt(v.x * v.x + v.y * v.y) do
length when length > 0 ->
ilength = 1.0 / length
new(x: v.x * ilength, y: v.y * ilength)
_ ->
zero()
end
end
@doc " Transforms a Vector2 by a given Matrix"
@spec transform(Vector2, Matrix) :: Vector2
def transform(v, mat) when is_vector2(v) and is_matrix(mat) do
new(
x: mat.m0 * v.x + mat.m4 * v.y + mat.m8 * 0.0 + mat.m12,
y: mat.m1 * v.x + mat.m5 * v.y + mat.m9 * 0.0 + mat.m13
)
end
@doc "Calculate linear interpolation between two vectors"
@spec lerp(Vector2, Vector2, number) :: Vector2
def lerp(v1, v2, amount) when is_vector2(v1) and is_vector2(v2) and is_number(amount) do
new(x: v1.x + amount * (v2.x - v1.x), y: v1.y + amount * (v2.y - v1.y))
end
@doc "Calculate reflected vector to normal"
@spec reflect(Vector2, Vector2) :: Vector2
def reflect(v, normal) when is_vector2(v) and is_vector2(normal) do
dot = dot_product(v, normal)
new(x: v.x - 2.0 * normal.x * dot, y: v.y - 2.0 * normal.y * dot)
end
@doc "Get min value for each pair of components"
@spec min(Vector2, Vector2) :: Vector2
def min(v1, v2) when is_vector2(v1) and is_vector2(v2),
do: new(x: Kernel.min(v1.x, v2.x), y: Kernel.min(v1.y, v2.y))
@doc "Get max value for each pair of components"
@spec max(Vector2, Vector2) :: Vector2
def max(v1, v2) when is_vector2(v1) and is_vector2(v2),
do: new(x: Kernel.max(v1.x, v2.x), y: Kernel.max(v1.y, v2.y))
@doc "Rotate vector by angle"
@spec rotate(Vector2, number) :: Vector2
def rotate(v, angle) when is_vector2(v) and is_number(angle) do
cos = :math.cos(angle)
sin = :math.sin(angle)
new(
x: v.x * cos - v.y * sin,
y: v.x * sin + v.y * cos
)
end
@doc "Move Vector towards target"
@spec move_towards(Vector2, Vector2, number) :: Vector2
def move_towards(v, target, max_distance)
when is_vector2(v) and is_vector2(target) and is_number(max_distance) do
dx = target.x - v.x
dy = target.y - v.y
value = dx * dx + dy * dy
if value == 0 or (max_distance >= 0 and value <= max_distance * max_distance) do
target
else
dist = :math.sqrt(value)
new(x: v.x + dx / dist * max_distance, y: v.y + dy / dist * max_distance)
end
end
@doc "Invert the given vector. Doesn't quite work as well as `Exray.Structs.Vector2.negate/1`, though."
@spec invert(Vector2) :: Vector2
def invert(v) when is_vector2(v), do: new(x: 1.0 / v.x, y: 1.0 / v.y)
@doc """
Clamp the components of the vector between min and max values specified by the given vectors
"""
@spec clamp(Vector, Vector, Vector) :: Vector
def clamp(v, min, max) when is_vector2(v) and is_vector2(min) and is_vector2(max),
do:
new(
x: Kernel.min(max.x, Kernel.max(min.x, v.x)),
y: Kernel.min(max.y, Kernel.max(min.y, v.y))
)
@doc "Clamp the magnitude of the vector between two min and max values"
@spec clamp_value(Vector2, number, number) :: Vector2
def clamp_value(v, min, max) when is_vector2(v) and is_number(min) and is_number(max) do
case v.x * v.x + v.y * v.y do
length when length > 0 ->
scale =
case :math.sqrt(length) do
length_squared when length_squared < min ->
min / length_squared
length_squared when length_squared > max ->
max / length_squared
_ ->
1
end
new(x: v.x * scale, y: v.y * scale)
_ ->
v
end
end
@doc "Check whether two given vectors are almost equal.
Very useful given `1.00000001 != 1.0000000003`"
@spec equals(Vector2, Vector2) :: boolean
def equals(p, q) when is_vector2(p) and is_vector2(q) do
Kernel.abs(p.x - q.x) <=
Math.epsilon() * Kernel.max(1.0, Kernel.max(Kernel.abs(p.x), Kernel.abs(q.x))) and
Kernel.abs(p.y - q.y) <=
Math.epsilon() * Kernel.max(1.0, Kernel.max(Kernel.abs(p.y), Kernel.abs(q.y)))
end
@doc """
Compute the direction of a refracted ray
v: normalized direction of the incoming ray
n: normalized normal vector of the interface of two optical media
r: ratio of the refractive index of the medium from where the ray comes
to the refractive index of the medium on the other side of the surface
"""
@spec refract(Vector2, Vector2, number) :: Vector2
def refract(v, n, r) when is_vector2(v) and is_vector2(n) and is_number(r) do
dot = v.x * n.x + v.y * n.y
d = 1.0 - r * r * (1.0 - dot * dot)
if d >= 0.0 do
d = :math.sqrt(d)
new(x: r * v.x - (r * dot + d) * n.x, y: r * v.y - (r * dot + d) * n.y)
else
zero()
end
end
end