• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eskil / scurry / b113af7a7d241a78fc435c4db8d6bb9a264e7979

01 Apr 2025 02:46AM UTC coverage: 96.507% (-0.4%) from 96.93%
b113af7a7d241a78fc435c4db8d6bb9a264e7979

push

github

web-flow
Merge pull request #3 from eskil/otp27

Fix OTP 27 warning

0 of 1 new or added line in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

221 of 229 relevant lines covered (96.51%)

244.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.86
/lib/vector.ex
1
defmodule Scurry.Vector do
2
  @moduledoc """
3
  Functions to work on 2D vectors.
4

5
  Vectors are represented as tuples of x and y components, `{x :: number, y :: number}`. This
6
  module provides basic trigonometry functions.
7

8
  See [Euclidian Vector](https://en.wikipedia.org/wiki/Euclidean_vector) on
9
  Wikipedia for further descriptions of the maths and use cases.
10
  """
11

12
  use Scurry.Types
13

14
  @doc """
15
  Get the length of a vector, aka magnitude.
16

17
  ## Params
18

19
  * `v` (`t:vector/0`) the vector to find the length for.
20

21
  ## Returns
22

23
  The length of the vector `v`.
24

25
  ## Examples
26
      iex> Vector.len({1, 1})
27
      1.4142135623730951
28
      iex> Vector.len({3, 4})
29
      5.0
30
      iex> Vector.len({12, 5})
31
      13.0
32
  """
33
  @spec len(v :: vector()) :: float()
34
  def len({x, y} = _v) do
35
    :math.sqrt(x * x + y * y)
25✔
36
  end
37

38
  @doc """
39
  Add two vectors together.
40

41
  ## Params
42

43
  * `v1` (`t:vector/0`) the first vector.
44
  * `v2` (`t:vector/0`) the second vector to add to `v1`.
45

46
  ## Returns
47

48
  The resulting vector of adding `v1` and `v2` together.
49

50
  ## Examples
51
      iex> Vector.add({1, 2}, {3, 4})
52
      {4, 6}
53
  """
54
  @spec add(v1 :: vector(), v2 :: vector()) :: vector()
55
  def add({ax, ay} = _v1, {bx, by} = _v2) do
99✔
56
    {ax + bx, ay + by}
57
  end
58

59
  @doc """
60
  Subtract a vector from another.
61

62
  ## Params
63

64
  * `v1` (`t:vector/0`) the first vector.
65
  * `v2` (`t:vector/0`) the second vector to subtract from `v1`.
66

67
  ## Returns
68

69
  The resulting vector of subtracting of `v2` from `v1`.
70

71
  ## Examples
72
      iex> Vector.sub({5, 7}, {1, 2})
73
      {4, 5}
74
  """
75
  @spec sub(v1 :: vector(), v2 :: vector()) :: vector()
76
  def sub({ax, ay} = _v1, {bx, by} = _v2) do
191✔
77
    {ax - bx, ay - by}
78
  end
79

80
  @doc """
81
  Divide a vector by a constant.
82

83
  Dividing/multiplying a vector essentially shortens/lengthens it.
84

85
  ## Params
86

87
  * `v` (`t:vector/0`) the vector to divide.
88
  * `c` (`t:number/0`) the constant to divide `v` by.
89

90
  ## Returns
91

92
  The result of dividing `v` by `c`.
93

94
  ## Examples
95
      iex> Vector.div({10, 14}, 2)
96
      {5.0, 7.0}
97
  """
98
  @spec div(v :: vector(), c :: number()) :: vector()
99
  def div({x, y} = _v, c) do
99✔
100
    {x / c, y / c}
101
  end
102

103
  @doc """
104
  Multiply a vector by a constant.
105

106
  ## Params
107

108
  * `v` (`t:vector/0`) describing the vector.
109
  * `c` (`t:number/0` the constant to multiply `v` by.
110

111
  Returns the result of multiplying `v` by `c`.
112

113
  ## Examples
114
      iex> Vector.mul({5, 7}, 2)
115
      {10, 14}
116
  """
117
  @spec mul(v :: vector(), c :: number()) :: vector()
118
  def mul({x, y} = _v, c) do
1✔
119
    {x * c, y * c}
120
  end
121

122
  @doc """
123
  Get the distance of two vectors.
124

125
  The distance is the length of the vector between the ends (second tuple) of the vectors.
126

127
  This is equivalent to the square root of the `distance_squared/2`.
128

129
  ## Params
130

131
  * `v1` (`t:vector/0`) describing the first vector.
132
  * `v2` (`t:vector/0`) describing the second vector to get the distance from `v1`.
133

134
  ## Returns
135

136
  The distance between the ends of `v1` and `v2`.
137

138
  ## Examples
139
      iex> Vector.distance({0, 0}, {1, 1})
140
      1.4142135623730951
141
      iex> Vector.distance({5, 7}, {2, 3})
142
      5.0
143
  """
144
  @spec distance(v1 :: vector(), v2 :: vector()) :: float()
145
  def distance({_ax, _ay} = v1, {_bx, _by} = v2) do
146
    :math.sqrt(distance_squared(v1, v2))
185✔
147
  end
148

149
  @doc """
150
  Get the distance squared of two vectors.
151

152
  This is equivalent to the square of the `distance/2`.
153

154
  ## Params
155

156
  * `v1` (`t:vector/0`) describing the first vector.
157
  * `v2` (`t:vector/0`) describing the second vector to get the distance squared from `v1`.
158

159
  ## Returns
160

161
  The squared distance between the ends of `v1` and `v2`.
162

163
  ## Examples
164
      iex> Vector.distance_squared({0, 0}, {1, 1})
165
      2.0
166
      iex> Vector.distance_squared({5, 7}, {2, 3})
167
      25.0
168
  """
169
  @spec distance_squared(v1 :: vector(), v2 :: vector()) :: float()
170
  def distance_squared({ax, ay} = _v1, {bx, by} = _v2) do
171
    :math.pow(ax - bx, 2) + :math.pow(ay - by, 2)
5,814✔
172
  end
173

174
  @doc """
175
  Normalise a vector to length 1.
176

177
  This shortens the vector by it's length, so the resulting vector `w` has
178
  `len(w) == 1`, and same x/y ratio.
179

180
  ## Params
181

182
  * `v` (`t:vector/0`) describing the vector to normalise.
183

184
  ## Returns
185

186
  A `t:vector/0` with length 1, and same x/y ratio.
187

188
  ## Examples
189
      iex> Vector.normalise({0, 1})
190
      {0.0, 1.0}
191
      iex> Vector.normalise({10, 0})
192
      {1.0, 0.0}
193
      iex> Vector.normalise({10, 10})
194
      {0.7071067811865475, 0.7071067811865475}
195
  """
196
  @spec normalise(v :: vector()) :: vector()
197
  def normalise({x, y} = _v) do
198
    l = len({x, y})
3✔
199
    {x / l, y / l}
200
  end
201

202
  @doc """
203
  Get the dot product of two vectors.
204

205
  ## Params
206

207
  * `v1` (`t:vector/0`) describing the first vector.
208
  * `v2` (`t:vector/0`) describing the second vector to 'dot' against `v1`.
209

210
  ## Returns
211

212
  The dot product of `v1` and `v2`, `v1` · `v2`.
213

214
  ## Examples
215
      iex> Vector.dot({1, 2}, {3, 4})
216
      11
217
  """
218
  @spec dot(v1 :: vector(), v2 :: vector()) :: float()
219
  def dot({ax, ay} = _v1, {bx, by} = _v2) do
220
    ax * bx + ay * by
9✔
221
  end
222

223
  @doc """
224
  Get the cross product of two vectors.
225

226
  ## Params
227

228
  * `v1` (`t:vector/0`) describing the first vector.
229
  * `v2` (`t:vector/0`) describing the second vector to 'cross' against `v2`.
230

231
  Returns the cross product of `v1` and `v2`, `v1` × `v2`.
232

233
  ## Examples
234
      iex> Vector.cross({1, 2}, {3, 4})
235
      -2
236
  """
237
  @spec cross(v1 :: vector(), v2 :: vector()) :: float()
238
  def cross({ax, ay} = _v1, {bx, by} = _v2) do
239
    ax * by - ay * bx
96✔
240
  end
241

242
  @doc """
243
  Get the magnitude of a vector, aka len.
244

245
  This is an alias for `len/2`.
246

247
  ## Examples
248
      iex> Vector.mag({1, 1})
249
      1.4142135623730951
250
      iex> Vector.mag({3, 4})
251
      5.0
252
      iex> Vector.mag({12, 5})
253
      13.0
254
  """
255
  @spec mag(v :: vector()) :: float()
256
  def mag(v), do: len(v)
3✔
257

258
  @doc """
259
  Get the angle of a vector in radians.
260

261
  ## Params
262

263
  * `v` (`t:vector/0`) describing the vector to obtain the angle for.
264

265
  ## Returns
266

267
  The angle in radians in relationship to the x-axis.
268

269
  ## Examples
270
      iex> Vector.angle({1, 1})
271
      0.7853981633974483
272
      iex> Vector.angle({0, 1})
273
      1.5707963267948966
274
      iex> Vector.angle({1, 0})
275
      0.0
276
      iex> Vector.angle({-1, 1})
277
      2.356194490192345
278
      iex> Vector.angle({-1, 0})
279
      3.141592653589793
280
      iex> Vector.angle({-1, -1})
281
      3.9269908169872414
282
      iex> Vector.angle({0, -1})
283
      4.71238898038469
284
      iex> Vector.angle({1, -1})
285
      5.497787143782138
286
  """
287
  @spec angle(v :: vector()) :: float()
288
  def angle({x, y} = _v) when x < 0 and y < 0 do
289
    :math.pi() + angle({-x, -y})
2✔
290
  end
291

292
  def angle({x, y} = _v) when x < 0 do
293
    :math.pi() - angle({-x, y})
4✔
294
  end
295

296
  def angle({x, y} = _v) when y < 0 do
297
    2 * :math.pi() - angle({x, -y})
4✔
298
  end
299

300
  def angle({+0.0, _y} = _v) do
NEW
301
    :math.pi() / 2
×
302
  end
303

304
  def angle({-0.0, _y} = _v) do
UNCOV
305
    :math.pi() / 2
×
306
  end
307

308
  def angle({0, _y} = _v) do
309
    :math.pi() / 2
4✔
310
  end
311

312
  def angle({x, y} = _v) do
313
    # East-Counterclockwise Convention
314
    :math.atan(y / x)
12✔
315
  end
316

317
  @doc """
318
  Calls round on a vector to make a vector with `t:integer/0` instead of `t:float/0`.
319

320
  This is provided for interoperability with other libraries where coordinates
321
  must be expressed in integers, for example
322
  [`:wx`](https://www.erlang.org/doc/man/wx.html) operations for drawing.
323

324
  The name ends in `_pos` to avoid any confusion/collision with
325
  `Kernel.trunc/1` and to indicate the use in drawing "positions" on the
326
  screen.
327

328
  ## Params
329

330
  * `v` (`t:vector/0`) describing the vector to round to integers.
331

332
  ## Returns
333

334
  The vector with it's components converted to integers using `Kernel.trunc/1`.
335

336
  ## Examples
337
      iex> Vector.trunc_pos({10.1, 10.9})
338
      {10, 10}
339
  """
340
  @spec trunc_pos(v :: vector()) :: { integer(), integer() }
341
  def trunc_pos({x, y} = _v) do
1✔
342
    {Kernel.trunc(x), Kernel.trunc(y)}
343
  end
344

345
  @doc """
346
  Calls round on a vector to make a vector with `t:integer/0` instead of `t:float/0`.
347

348
  This is for interoperability with other libraries where coordinates must be
349
  expressed in integers, for example
350
  [`:wx`](https://www.erlang.org/doc/man/wx.html) operations for drawing.
351

352
  The name ends in `_pos` to avoid any confusion/collision with
353
  `Kernel.round/1` and to indicate the use in drawing "positions" on the
354
  screen.
355

356
  ## Params
357

358
  * `v` (`t:vector/0`) describing the vector to round to integers.
359

360
  ## Returns
361

362
  Avector with it's components converted to integers using `Kernel.round/1`.
363

364
  ## Examples
365
      iex> Vector.round_pos({10.1, 10.9})
366
      {10, 11}
367
  """
368
  @spec round_pos(v :: vector()) :: { integer(), integer() }
369
  def round_pos({x, y} = _v) do
1✔
370
    {Kernel.round(x), Kernel.round(y)}
371
  end
372

373
  @doc """
374
  This is a graph oriented degree.
375

376
  Graph degrees are oriented as for a graph layout.
377

378
  * Right (along the x-axis) is 0°
379
  * Up (along y-axis) is 90°,
380
  * 45° is "north-east / up and to the left".
381

382
  ## Params
383

384
  * `v` (`t:vector/0`) describing the vector to obtain the angle for.
385

386
  ## Returns
387

388
  The vector in degrees relative to the x-axis.
389

390
  ## Examples
391
      iex> Vector.degrees_graph({1, 1})
392
      45.0
393
      iex> Vector.degrees_graph({0, 1})
394
      90.0
395
      iex> Vector.degrees_graph({1, 0})
396
      0.0
397
      iex> Vector.degrees_graph({-1, 1})
398
      135.0
399
      iex> Vector.degrees_graph({-1, 0})
400
      180.0
401
      iex> Vector.degrees_graph({-1, -1})
402
      225.0
403
      iex> Vector.degrees_graph({0, -1})
404
      270.0
405
      iex> Vector.degrees_graph({1, -1})
406
      315.0
407
  """
408
  @spec degrees_graph(v :: vector()) :: float()
409
  def degrees_graph({_x, _y} = v) do
410
    angle(v) * (180 / :math.pi())
8✔
411
  end
412

413
  @doc """
414
  This is a screen oriented degree.
415

416
  Screen degrees are in
417
  * North=up (0)
418
  * South=down (180) degrees.
419

420
  Vectors are in `(x,y)` screen coordinate, so `(1,1)` = 135 degrees (down to the
421
  right/ south-east) from `0,0`, the top left corner is `0,0`.
422

423
  This layout maps how eg. [`:wx`](https://www.erlang.org/doc/man/wx.html)
424
  defines the coordinates.
425

426
  ## Params
427

428
  * `v` (`t:vector/0`) of coordinates describing the vector to obtain the angle for.
429

430
  ## Returns
431

432
  The vector in degrees relative to the y-axis.
433

434
  ## Examples
435
      iex> Vector.degrees({0, -1})
436
      0
437
      iex> Vector.degrees({1, -1})
438
      45
439
      iex> Vector.degrees({1, 0})
440
      90
441
      iex> Vector.degrees({1, 1})
442
      135
443
      iex> Vector.degrees({0, 1})
444
      180
445
      iex> Vector.degrees({-1, 1})
446
      225
447
      iex> Vector.degrees({-1, 0})
448
      270
449
      iex> Vector.degrees({-1, -1})
450
      315
451
  """
452
  @spec degrees(v :: vector()) :: integer()
453
  def degrees({x, _y} = v) do
454
    # z is our "north" and it's pointing "right" to rotate.
455
    z = {0, -1}
8✔
456
    d = dot(v, z)
8✔
457
    cos_a = d / (len(v) * len(z))
8✔
458
    d = :math.acos(cos_a) * (180 / :math.pi())
8✔
459

460
    if x < 0 do
461
      360 - d
3✔
462
    else
463
      d
5✔
464
    end
465
    |> Kernel.round
8✔
466
  end
467
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc