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

eskil / scurry / 7180ab87890bb64e762715c125d912788cf1ffae

31 Mar 2025 02:28PM UTC coverage: 96.93% (+0.08%) from 96.847%
7180ab87890bb64e762715c125d912788cf1ffae

push

github

web-flow
Merge pull request #1 from eskil/docs-cleanup

V3 branch

77 of 80 new or added lines in 5 files covered. (96.25%)

1 existing line in 1 file now uncovered.

221 of 228 relevant lines covered (96.93%)

245.82 hits per line

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

96.3
/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, _y} = _v) do
305
    :math.pi() / 2
4✔
306
  end
307

308
  def angle({x, y} = _v) do
309
    # East-Counterclockwise Convention
310
    :math.atan(y / x)
12✔
311
  end
312

313
  @doc """
314
  Calls round on a vector to make a vector with `t:integer/0` instead of `t:float/0`.
315

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

320
  The name ends in `_pos` to avoid any confusion/collision with
321
  `Kernel.trunc/1` and to indicate the use in drawing "positions" on the
322
  screen.
323

324
  ## Params
325

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

328
  ## Returns
329

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

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

341
  @doc """
342
  Calls round on a vector to make a vector with `t:integer/0` instead of `t:float/0`.
343

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

348
  The name ends in `_pos` to avoid any confusion/collision with
349
  `Kernel.round/1` and to indicate the use in drawing "positions" on the
350
  screen.
351

352
  ## Params
353

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

356
  ## Returns
357

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

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

369
  @doc """
370
  This is a graph oriented degree.
371

372
  Graph degrees are oriented as for a graph layout.
373

374
  * Right (along the x-axis) is 0°
375
  * Up (along y-axis) is 90°,
376
  * 45° is "north-east / up and to the left".
377

378
  ## Params
379

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

382
  ## Returns
383

384
  The vector in degrees relative to the x-axis.
385

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

409
  @doc """
410
  This is a screen oriented degree.
411

412
  Screen degrees are in
413
  * North=up (0)
414
  * South=down (180) degrees.
415

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

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

422
  ## Params
423

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

426
  ## Returns
427

428
  The vector in degrees relative to the y-axis.
429

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

456
    if x < 0 do
457
      360 - d
3✔
458
    else
459
      d
5✔
460
    end
461
    |> Kernel.round
8✔
462
  end
463
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