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

eskil / scurry / be54ba3aace9ecff1535e1d40e083e9a228d1b7d

28 Mar 2025 04:37AM UTC coverage: 96.93% (+0.08%) from 96.847%
be54ba3aace9ecff1535e1d40e083e9a228d1b7d

Pull #1

github

eskil
test more elixit otp combos
Pull Request #1: V3 branch

66 of 67 new or added lines in 5 files covered. (98.51%)

7 existing lines in 3 files now uncovered.

221 of 228 relevant lines covered (96.93%)

245.79 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`, a `t:vector/0` describing the vector.
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` a `t:vector/0` describing the first vector.
44
  * `v2` a `t:vector/0` describing the second vector.
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` a `t:vector/0` describing the first vector.
65
  * `v2` a `t:vector/0` describing the second vector.
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
  ## Params
84

85
  * `v` a `t:vector/0` describing the vector.
86
  * `c` the constant to divide by.
87

88
  ## Returns
89

90
  The result of dividing `v` by `c`.
91

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

101
  @doc """
102
  Multiply a vector by a constant.
103

104
  ## Params
105

106
  * `v` a `t:vector/0` describing the vector.
107
  * `c` the constant to multiple by.
108

109
  Returns the result of multiplying `v` by `c`.
110

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

120
  @doc """
121
  Get the distance of two vectors.
122

123
  The distance is the length of the vector between the ends (second tuple) of the vectors.
124

125
  This is equivalent to the square root of the `distance_squared/2`.
126

127
  ## Params
128

129
  * `v1` a `t:vector/0` describing the first vector.
130
  * `v2` a `t:vector/0` describing the second vector.
131

132
  ## Returns
133

134
  The distance between the ends of `v1` and `v2`.
135

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

147
  @doc """
148
  Get the distance squared of two vectors.
149

150
  This is equivalent to the square of the `distance/2`.
151

152
  ## Params
153

154
  * `v1` a `t:vector/0` describing the first vector.
155
  * `v2` a `t:vector/0` describing the second vector.
156

157
  ## Returns
158

159
  The squared distance between the ends of `v1` and `v2`.
160

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

172
  @doc """
173
  Normalise a vector to length 1.
174

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

178
  ## Params
179

180
  * `v` a `t:vector/0` describing the vector to normalise.
181

182
  ## Returns
183

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

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

200
  @doc """
201
  Get the dot product of two vectors.
202

203
  ## Params
204

205
  * `v1` a `t:vector/0` describing the first vector.
206
  * `v2` a `t:vector/0` describing the second vector.
207

208
  ## Returns
209

210
  The dot product of `v1` and `v2`.
211

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

221
  @doc """
222
  Get the cross product of two vectors.
223

224
  ## Params
225

226
  * `v1` a `t:vector/0` describing the first vector.
227
  * `v2` a `t:vector/0` describing the second vector.
228

229
  Returns the cross product of `v1` and `v2`.
230

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

240
  @doc """
241
  Get the magnitude of a vector, aka len.
242

243
  This is an alias for `len/2`.
244

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

256
  @doc """
257
  Get the angle of a vector in radians.
258

259
  ## Params
260

261
  * `v` a `t:vector/0` describing the vector to normalise.
262

263
  ## Returns
264

265
  The angle in radians in relationship to the x-axis.
266

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

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

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

298
  def angle({0.0, _y} = _v) do
NEW
UNCOV
299
    :math.pi() / 2
×
300
  end
301

302
  def angle({0, _y} = _v) do
303
    :math.pi() / 2
4✔
304
  end
305

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

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

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

318
  The name ends in `_pos` to avoid any confusion/collision with `Kernel.trunc/1`.
319

320
  ## Params
321

322
  * `v` a `t:vector/0` describing the vector.
323

324
  ## Returns
325

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

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

337
  @doc """
338
  Calls round on a vector to make a vector with `t:integer/0` instead of `t:float/0`.
339

340
  This is for interoperability with other libraries where coordinates must be
341
  expressed in integers, for example
342
  [`:wx`](https://www.erlang.org/doc/man/wx.html).
343

344
  The name ends in `_pos` to avoid any confusion/collision with `Kernel.round/1`.
345

346
  ## Params
347

348
  * `v` a `t:vector/0` describing the vector.
349

350
  ## Returns
351

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

354
  ## Examples
355
      iex> Vector.round_pos({10.1, 10.9})
356
      {10, 11}
357
  """
358
  @spec round_pos(v :: vector()) :: { integer(), integer() }
359
  def round_pos({x, y} = _v) do
1✔
360
    {Kernel.round(x), Kernel.round(y)}
361
  end
362

363
  @doc """
364
  This is a graph oriented degree.
365

366
  Graph degrees are oriented as for a graph layout.
367

368
  * Right (along the x-axis) is 0°
369
  * Up (along y-axis) is 90°,
370
  * 45° is "north-east / up and to the left".
371

372
  ## Params
373

374
  * `v` a `t:vector/0` describing the vector.
375

376
  ## Returns
377

378
  The vector in degrees relative to the x-axis.
379

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

403
  @doc """
404
  This is a screen oriented degree.
405

406
  Screen degrees are in
407
  * North=up (0)
408
  * South=down (180) degrees.
409

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

413
  ## Params
414

415
  * `v` a `t:vector/0` of coordinates describing the vector.
416

417
  ## Returns
418

419
  The vector in degrees relative to the y-axis.
420

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

447
    if x < 0 do
448
      360 - d
3✔
449
    else
450
      d
5✔
451
    end
452
    |> Kernel.round
8✔
453
  end
454
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