defmodule Scurry.Vector do
@moduledoc """
Functions to work on 2D vectors.
Vectors are represented as tuples of x and y components, `{x :: number, y :: number}`. This
module provides basic trigonometry functions.
See [Euclidian Vector](https://en.wikipedia.org/wiki/Euclidean_vector) on
Wikipedia for further descriptions of the maths and use cases.
"""
use Scurry.Types
@doc """
Get the length of a vector, aka magnitude.
## Params
* `v` (`t:vector/0`) the vector to find the length for.
## Returns
The length of the vector `v`.
## Examples
iex> Vector.len({1, 1})
1.4142135623730951
iex> Vector.len({3, 4})
5.0
iex> Vector.len({12, 5})
13.0
"""
@spec len(v :: vector()) :: float()
def len({x, y} = _v) do
:math.sqrt(x * x + y * y)
end
@doc """
Add two vectors together.
## Params
* `v1` (`t:vector/0`) the first vector.
* `v2` (`t:vector/0`) the second vector to add to `v1`.
## Returns
The resulting vector of adding `v1` and `v2` together.
## Examples
iex> Vector.add({1, 2}, {3, 4})
{4, 6}
"""
@spec add(v1 :: vector(), v2 :: vector()) :: vector()
def add({ax, ay} = _v1, {bx, by} = _v2) do
{ax + bx, ay + by}
end
@doc """
Subtract a vector from another.
## Params
* `v1` (`t:vector/0`) the first vector.
* `v2` (`t:vector/0`) the second vector to subtract from `v1`.
## Returns
The resulting vector of subtracting of `v2` from `v1`.
## Examples
iex> Vector.sub({5, 7}, {1, 2})
{4, 5}
"""
@spec sub(v1 :: vector(), v2 :: vector()) :: vector()
def sub({ax, ay} = _v1, {bx, by} = _v2) do
{ax - bx, ay - by}
end
@doc """
Divide a vector by a constant.
Dividing/multiplying a vector essentially shortens/lengthens it.
## Params
* `v` (`t:vector/0`) the vector to divide.
* `c` (`t:number/0`) the constant to divide `v` by.
## Returns
The result of dividing `v` by `c`.
## Examples
iex> Vector.div({10, 14}, 2)
{5.0, 7.0}
"""
@spec div(v :: vector(), c :: number()) :: vector()
def div({x, y} = _v, c) do
{x / c, y / c}
end
@doc """
Multiply a vector by a constant.
## Params
* `v` (`t:vector/0`) describing the vector.
* `c` (`t:number/0` the constant to multiply `v` by.
Returns the result of multiplying `v` by `c`.
## Examples
iex> Vector.mul({5, 7}, 2)
{10, 14}
"""
@spec mul(v :: vector(), c :: number()) :: vector()
def mul({x, y} = _v, c) do
{x * c, y * c}
end
@doc """
Get the distance of two vectors.
The distance is the length of the vector between the ends (second tuple) of the vectors.
This is equivalent to the square root of the `distance_squared/2`.
## Params
* `v1` (`t:vector/0`) describing the first vector.
* `v2` (`t:vector/0`) describing the second vector to get the distance from `v1`.
## Returns
The distance between the ends of `v1` and `v2`.
## Examples
iex> Vector.distance({0, 0}, {1, 1})
1.4142135623730951
iex> Vector.distance({5, 7}, {2, 3})
5.0
"""
@spec distance(v1 :: vector(), v2 :: vector()) :: float()
def distance({_ax, _ay} = v1, {_bx, _by} = v2) do
:math.sqrt(distance_squared(v1, v2))
end
@doc """
Get the distance squared of two vectors.
This is equivalent to the square of the `distance/2`.
## Params
* `v1` (`t:vector/0`) describing the first vector.
* `v2` (`t:vector/0`) describing the second vector to get the distance squared from `v1`.
## Returns
The squared distance between the ends of `v1` and `v2`.
## Examples
iex> Vector.distance_squared({0, 0}, {1, 1})
2.0
iex> Vector.distance_squared({5, 7}, {2, 3})
25.0
"""
@spec distance_squared(v1 :: vector(), v2 :: vector()) :: float()
def distance_squared({ax, ay} = _v1, {bx, by} = _v2) do
:math.pow(ax - bx, 2) + :math.pow(ay - by, 2)
end
@doc """
Normalise a vector to length 1.
This shortens the vector by it's length, so the resulting vector `w` has
`len(w) == 1`, and same x/y ratio.
## Params
* `v` (`t:vector/0`) describing the vector to normalise.
## Returns
A `t:vector/0` with length 1, and same x/y ratio.
## Examples
iex> Vector.normalise({0, 1})
{0.0, 1.0}
iex> Vector.normalise({10, 0})
{1.0, 0.0}
iex> Vector.normalise({10, 10})
{0.7071067811865475, 0.7071067811865475}
"""
@spec normalise(v :: vector()) :: vector()
def normalise({x, y} = _v) do
l = len({x, y})
{x / l, y / l}
end
@doc """
Get the dot product of two vectors.
## Params
* `v1` (`t:vector/0`) describing the first vector.
* `v2` (`t:vector/0`) describing the second vector to 'dot' against `v1`.
## Returns
The dot product of `v1` and `v2`, `v1` · `v2`.
## Examples
iex> Vector.dot({1, 2}, {3, 4})
11
"""
@spec dot(v1 :: vector(), v2 :: vector()) :: float()
def dot({ax, ay} = _v1, {bx, by} = _v2) do
ax * bx + ay * by
end
@doc """
Get the cross product of two vectors.
## Params
* `v1` (`t:vector/0`) describing the first vector.
* `v2` (`t:vector/0`) describing the second vector to 'cross' against `v2`.
Returns the cross product of `v1` and `v2`, `v1` × `v2`.
## Examples
iex> Vector.cross({1, 2}, {3, 4})
-2
"""
@spec cross(v1 :: vector(), v2 :: vector()) :: float()
def cross({ax, ay} = _v1, {bx, by} = _v2) do
ax * by - ay * bx
end
@doc """
Get the magnitude of a vector, aka len.
This is an alias for `len/2`.
## Examples
iex> Vector.mag({1, 1})
1.4142135623730951
iex> Vector.mag({3, 4})
5.0
iex> Vector.mag({12, 5})
13.0
"""
@spec mag(v :: vector()) :: float()
def mag(v), do: len(v)
@doc """
Get the angle of a vector in radians.
## Params
* `v` (`t:vector/0`) describing the vector to obtain the angle for.
## Returns
The angle in radians in relationship to the x-axis.
## Examples
iex> Vector.angle({1, 1})
0.7853981633974483
iex> Vector.angle({0, 1})
1.5707963267948966
iex> Vector.angle({1, 0})
0.0
iex> Vector.angle({-1, 1})
2.356194490192345
iex> Vector.angle({-1, 0})
3.141592653589793
iex> Vector.angle({-1, -1})
3.9269908169872414
iex> Vector.angle({0, -1})
4.71238898038469
iex> Vector.angle({1, -1})
5.497787143782138
"""
@spec angle(v :: vector()) :: float()
def angle({x, y} = _v) when x < 0 and y < 0 do
:math.pi() + angle({-x, -y})
end
def angle({x, y} = _v) when x < 0 do
:math.pi() - angle({-x, y})
end
def angle({x, y} = _v) when y < 0 do
2 * :math.pi() - angle({x, -y})
end
def angle({+0.0, _y} = _v) do
:math.pi() / 2
end
def angle({-0.0, _y} = _v) do
:math.pi() / 2
end
def angle({0, _y} = _v) do
:math.pi() / 2
end
def angle({x, y} = _v) do
# East-Counterclockwise Convention
:math.atan(y / x)
end
@doc """
Calls round on a vector to make a vector with `t:integer/0` instead of `t:float/0`.
This is provided for interoperability with other libraries where coordinates
must be expressed in integers, for example
[`:wx`](https://www.erlang.org/doc/man/wx.html) operations for drawing.
The name ends in `_pos` to avoid any confusion/collision with
`Kernel.trunc/1` and to indicate the use in drawing "positions" on the
screen.
## Params
* `v` (`t:vector/0`) describing the vector to round to integers.
## Returns
The vector with it's components converted to integers using `Kernel.trunc/1`.
## Examples
iex> Vector.trunc_pos({10.1, 10.9})
{10, 10}
"""
@spec trunc_pos(v :: vector()) :: { integer(), integer() }
def trunc_pos({x, y} = _v) do
{Kernel.trunc(x), Kernel.trunc(y)}
end
@doc """
Calls round on a vector to make a vector with `t:integer/0` instead of `t:float/0`.
This is for interoperability with other libraries where coordinates must be
expressed in integers, for example
[`:wx`](https://www.erlang.org/doc/man/wx.html) operations for drawing.
The name ends in `_pos` to avoid any confusion/collision with
`Kernel.round/1` and to indicate the use in drawing "positions" on the
screen.
## Params
* `v` (`t:vector/0`) describing the vector to round to integers.
## Returns
Avector with it's components converted to integers using `Kernel.round/1`.
## Examples
iex> Vector.round_pos({10.1, 10.9})
{10, 11}
"""
@spec round_pos(v :: vector()) :: { integer(), integer() }
def round_pos({x, y} = _v) do
{Kernel.round(x), Kernel.round(y)}
end
@doc """
This is a graph oriented degree.
Graph degrees are oriented as for a graph layout.
* Right (along the x-axis) is 0°
* Up (along y-axis) is 90°,
* 45° is "north-east / up and to the left".
## Params
* `v` (`t:vector/0`) describing the vector to obtain the angle for.
## Returns
The vector in degrees relative to the x-axis.
## Examples
iex> Vector.degrees_graph({1, 1})
45.0
iex> Vector.degrees_graph({0, 1})
90.0
iex> Vector.degrees_graph({1, 0})
0.0
iex> Vector.degrees_graph({-1, 1})
135.0
iex> Vector.degrees_graph({-1, 0})
180.0
iex> Vector.degrees_graph({-1, -1})
225.0
iex> Vector.degrees_graph({0, -1})
270.0
iex> Vector.degrees_graph({1, -1})
315.0
"""
@spec degrees_graph(v :: vector()) :: float()
def degrees_graph({_x, _y} = v) do
angle(v) * (180 / :math.pi())
end
@doc """
This is a screen oriented degree.
Screen degrees are in
* North=up (0)
* South=down (180) degrees.
Vectors are in `(x,y)` screen coordinate, so `(1,1)` = 135 degrees (down to the
right/ south-east) from `0,0`, the top left corner is `0,0`.
This layout maps how eg. [`:wx`](https://www.erlang.org/doc/man/wx.html)
defines the coordinates.
## Params
* `v` (`t:vector/0`) of coordinates describing the vector to obtain the angle for.
## Returns
The vector in degrees relative to the y-axis.
## Examples
iex> Vector.degrees({0, -1})
0
iex> Vector.degrees({1, -1})
45
iex> Vector.degrees({1, 0})
90
iex> Vector.degrees({1, 1})
135
iex> Vector.degrees({0, 1})
180
iex> Vector.degrees({-1, 1})
225
iex> Vector.degrees({-1, 0})
270
iex> Vector.degrees({-1, -1})
315
"""
@spec degrees(v :: vector()) :: integer()
def degrees({x, _y} = v) do
# z is our "north" and it's pointing "right" to rotate.
z = {0, -1}
d = dot(v, z)
cos_a = d / (len(v) * len(z))
d = :math.acos(cos_a) * (180 / :math.pi())
if x < 0 do
360 - d
else
d
end
|> Kernel.round
end
end