@inglorious/utils 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/utils",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "A set of general-purpose utility functions designed with functional programming principles in mind.",
5
5
  "author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -1,3 +1,6 @@
1
+ import { v } from "../v.js"
2
+ import { isArray } from "./array.js"
3
+
1
4
  const INITIAL_LEVEL = 0
2
5
  const NEXT_LEVEL = 2
3
6
 
@@ -8,7 +11,46 @@ const NEXT_LEVEL = 2
8
11
  * @returns {Object} A deep clone of the input object.
9
12
  */
10
13
  export function clone(obj) {
11
- return JSON.parse(JSON.stringify(obj))
14
+ return deserialize(serialize(obj))
15
+ }
16
+
17
+ /**
18
+ * Deserializes an object, converting plain object representations back into
19
+ * their original types, such as vector-like objects. This is the inverse
20
+ * operation of `serialize`.
21
+ *
22
+ * - Recursively deserializes nested objects.
23
+ * - Converts objects with a `_type: "vector"` property and `coords` array
24
+ * back into a vector-like object using the `v` factory function.
25
+ * - Copies all other property values as-is.
26
+ *
27
+ * @param {string} str - The JSON string to deserialize.
28
+ * @returns {Object} The deserialized object.
29
+ */
30
+ export function deserialize(str) {
31
+ const data = JSON.parse(str)
32
+
33
+ return revive(data)
34
+
35
+ function revive(value) {
36
+ if (isArray(value)) {
37
+ return value.map(revive)
38
+ }
39
+ if (isObject(value)) {
40
+ if (value._type === "vector" && value.coords) {
41
+ return v(...value.coords)
42
+ }
43
+
44
+ const deserialized = {}
45
+ for (const key in value) {
46
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
47
+ deserialized[key] = revive(value[key])
48
+ }
49
+ }
50
+ return deserialized
51
+ }
52
+ return value
53
+ }
12
54
  }
13
55
 
14
56
  /**
@@ -73,7 +115,7 @@ export function map(obj, callback) {
73
115
  *
74
116
  * **Important:** Unlike Immer, which uses structural sharing via proxies for
75
117
  * high performance, this implementation performs a full deep clone of the base
76
- * state on every call using `JSON.parse(JSON.stringify())`. This can be
118
+ * state on every call using `deserialize(serialize())`. This can be
77
119
  * inefficient for large or complex states. It is intended for simple use cases
78
120
  * where the convenience of the API outweighs the performance cost.
79
121
  *
@@ -102,6 +144,47 @@ export function produce(baseState, recipe, ...args) {
102
144
  return result === undefined ? draft : result
103
145
  }
104
146
 
147
+ /**
148
+ * Serializes an object, converting special types like vectors into a plain
149
+ * object representation. This is useful for processes like saving state to a
150
+ * file or sending it over a network.
151
+ *
152
+ * - Recursively serializes nested objects.
153
+ * - Converts objects with a `__isVector__` property into a serializable
154
+ * format: `{ _type: "vector", coords: [...] }`.
155
+ * - Copies all other property values as-is.
156
+ *
157
+ * @param {Object} obj - The object to serialize.
158
+ * @returns {string} The serialized JSON string.
159
+ */
160
+ export function serialize(obj) {
161
+ function replacer(key, value) {
162
+ // Handle top-level vector
163
+ if (value?.__isVector__) {
164
+ return {
165
+ _type: "vector",
166
+ coords: Array.from(value),
167
+ }
168
+ }
169
+
170
+ if (isObject(value)) {
171
+ const serialized = {}
172
+
173
+ for (const k in value) {
174
+ if (Object.prototype.hasOwnProperty.call(value, k)) {
175
+ serialized[k] = replacer(undefined, value[k])
176
+ }
177
+ }
178
+
179
+ return serialized
180
+ }
181
+
182
+ return value
183
+ }
184
+
185
+ return JSON.stringify(obj, replacer)
186
+ }
187
+
105
188
  /**
106
189
  * Converts an object or array to a formatted string representation.
107
190
  *
@@ -1,12 +1,15 @@
1
1
  import { expect, test } from "vitest"
2
2
 
3
+ import { v } from "../v.js"
3
4
  import {
4
5
  clone,
6
+ deserialize,
5
7
  filter,
6
8
  find,
7
9
  isObject,
8
10
  map,
9
11
  produce,
12
+ serialize,
10
13
  toString,
11
14
  } from "./object.js"
12
15
 
@@ -112,7 +115,7 @@ test("it should produce a new state without mutating the original", () => {
112
115
  g: 6,
113
116
  }
114
117
 
115
- const originalBaseState = JSON.parse(JSON.stringify(baseState))
118
+ const originalBaseState = deserialize(serialize(baseState))
116
119
 
117
120
  const nextState = produce(baseState, recipe)
118
121
 
@@ -216,3 +219,117 @@ test("it should return a string representation of a nested object", () => {
216
219
  g: 6
217
220
  }`)
218
221
  })
222
+
223
+ test("it should serialize a basic object with primitive values", () => {
224
+ const obj = { a: 1, b: "hello", c: true }
225
+ const expected = `{"a":1,"b":"hello","c":true}`
226
+
227
+ expect(serialize(obj)).toBe(expected)
228
+ })
229
+
230
+ test("it should serialize an object with nested objects", () => {
231
+ const obj = { a: 1, b: { c: "nested", d: { e: false } } }
232
+ const expected = `{"a":1,"b":{"c":"nested","d":{"e":false}}}`
233
+
234
+ expect(serialize(obj)).toBe(expected)
235
+ })
236
+
237
+ test("it should serialize an object containing a vector-like object", () => {
238
+ const obj = { position: v(1, 2, 3) }
239
+ const expected = `{"position":{"_type":"vector","coords":[1,2,3]}}`
240
+
241
+ expect(serialize(obj)).toBe(expected)
242
+ })
243
+
244
+ test("it should serialize an object with nested vector-like objects", () => {
245
+ const obj = {
246
+ entity: {
247
+ id: "player",
248
+ pos: v(10, 20),
249
+ vel: v(1, 0),
250
+ stats: { health: 100 },
251
+ },
252
+ }
253
+ const expected = `{"entity":{"id":"player","pos":{"_type":"vector","coords":[10,20]},"vel":{"_type":"vector","coords":[1,0]},"stats":{"health":100}}}`
254
+
255
+ expect(serialize(obj)).toBe(expected)
256
+ })
257
+
258
+ test("it should serialize an empty object", () => {
259
+ const obj = {}
260
+ const expected = `{}`
261
+
262
+ expect(serialize(obj)).toBe(expected)
263
+ })
264
+
265
+ test("it should serialize an object with null and undefined values (undefined should be omitted)", () => {
266
+ const obj = { a: null, b: undefined, c: 1 }
267
+ const expected = `{"a":null,"c":1}` // JSON.stringify omits undefined properties
268
+
269
+ expect(serialize(obj)).toBe(expected)
270
+ })
271
+
272
+ test("it should deserialize a basic object with primitive values", () => {
273
+ const serialized = `{"a":1,"b":"hello","c":true}`
274
+ const expected = { a: 1, b: "hello", c: true }
275
+
276
+ expect(deserialize(serialized)).toStrictEqual(expected)
277
+ })
278
+
279
+ test("it should deserialize an object with nested objects", () => {
280
+ const serialized = `{"a":1,"b":{"c":"nested","d":{"e":false}}}`
281
+ const expected = { a: 1, b: { c: "nested", d: { e: false } } }
282
+
283
+ expect(deserialize(serialized)).toStrictEqual(expected)
284
+ })
285
+
286
+ test("it should deserialize an object containing a vector representation", () => {
287
+ const serialized = `{"position":{"_type":"vector","coords":[1,2,3]}}`
288
+ const expected = { position: v(1, 2, 3) }
289
+
290
+ const result = deserialize(serialized)
291
+
292
+ expect(result).toStrictEqual(expected)
293
+ expect(result.position.__isVector__).toBe(true)
294
+ })
295
+
296
+ test("it should deserialize an object with nested vector representations", () => {
297
+ const serialized = `{"entity":{"id":"player","pos":{"_type":"vector","coords":[10,20]},"vel":{"_type":"vector","coords":[1,0]},"stats":{"health":100}}}`
298
+ const expected = {
299
+ entity: {
300
+ id: "player",
301
+ pos: v(10, 20),
302
+ vel: v(1, 0),
303
+ stats: { health: 100 },
304
+ },
305
+ }
306
+ const result = deserialize(serialized)
307
+
308
+ expect(result).toStrictEqual(expected)
309
+ expect(result.entity.pos.__isVector__).toBe(true)
310
+ expect(result.entity.vel.__isVector__).toBe(true)
311
+ })
312
+
313
+ test("it should deserialize an empty object", () => {
314
+ const serialized = `{}`
315
+ const expected = {}
316
+
317
+ expect(deserialize(serialized)).toStrictEqual(expected)
318
+ })
319
+
320
+ test("it should correctly round-trip a complex object through serialize and deserialize", () => {
321
+ const original = {
322
+ id: "game1",
323
+ player: { name: "Alice", score: 100, position: v(10, 20) },
324
+ enemies: [{ type: "goblin", pos: v(5, 5) }],
325
+ settings: { volume: 0.8, mute: false },
326
+ }
327
+ const jsonString = serialize(original)
328
+ const deserialized = deserialize(jsonString)
329
+
330
+ expect(deserialized).toStrictEqual(original)
331
+ expect(deserialized.player.position.__isVector__).toBe(true)
332
+ expect(deserialized.enemies[0].pos.__isVector__).toBe(true)
333
+ // Ensure that non-vector objects are still distinct references if they were originally
334
+ expect(deserialized.player).not.toBe(original.player)
335
+ })
package/src/math/line.js CHANGED
@@ -9,7 +9,6 @@ import { hypothenuse } from "./triangle.js"
9
9
 
10
10
  /**
11
11
  * Calculates the shortest distance from a point to a line in 2D space.
12
- *
13
12
  * @param {Line} line - The line represented by the equation ax + bz + c = 0, where `line` is [a, b, c].
14
13
  * @param {Point} point - The point in 3D space represented as [x, y, z].
15
14
  * @returns {number} The shortest distance from the point to the line.
@@ -24,7 +23,6 @@ export function distanceFromPoint(line, point) {
24
23
  // @see https://math.stackexchange.com/questions/275529/check-if-line-intersects-with-circles-perimeter
25
24
  /**
26
25
  * Determines whether a line intersects with the perimeter of a circle.
27
- *
28
26
  * @param {Line} line - The line represented by the equation ax + bz + c = 0, where `line` is [a, b, c].
29
27
  * @param {Circle} circle - The circle defined by its position (center) and radius.
30
28
  * @returns {boolean} `true` if the line intersects the circle's perimeter, otherwise `false`.
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Performs linear interpolation between two numbers.
3
- *
4
3
  * @param {number} a - The start value.
5
4
  * @param {number} b - The end value.
6
5
  * @param {number} t - The interpolation factor, typically in the range [0, 1].
@@ -13,11 +13,10 @@ const Y_AXIS = v(0, 1, 0) // Default axis of rotation (Y-axis).
13
13
 
14
14
  /**
15
15
  * Computes a quaternion representing a rotation around a given axis.
16
- *
17
16
  * @param {number} [angle=0] - The angle of rotation in radians. Defaults to 0.
18
17
  * @param {Vector3} [axis=Y_AXIS] - The axis of rotation as a 3D vector Defaults to the Y-axis.
19
18
  * @returns {Quaternion} The quaternion as an array [w, x, y, z].
20
19
  */
21
20
  export function quaternion(angle = DEFAULT_ANGLE, axis = Y_AXIS) {
22
- return [cos(angle / HALF), ...axis.map((coord) => coord * sin(angle / HALF))]
21
+ return v(cos(angle / HALF), ...axis.map((coord) => coord * sin(angle / HALF)))
23
22
  }
@@ -2,19 +2,22 @@
2
2
  * @typedef {import('./types').Quaternion} Quaternion
3
3
  */
4
4
 
5
+ import { v } from "../v.js"
5
6
  import { times } from "./vector.js"
6
7
  import { cross, dot, sum } from "./vectors.js"
7
8
 
8
9
  /**
9
10
  * Combines two quaternions using the Hamilton product.
10
- *
11
11
  * @param {Quaternion} q1 - The first quaternion.
12
12
  * @param {Quaternion} q2 - The second quaternion.
13
13
  * @returns {Quaternion} - The resulting quaternion after combining q1 and q2.
14
14
  */
15
15
  export function combine(q1, q2) {
16
- const [s, ...v] = q1
17
- const [t, ...w] = q2
16
+ const [s1, ...v1] = q1
17
+ const [s2, ...v2] = q2
18
18
 
19
- return [s * t - dot(v, w), ...sum(times(w, s), times(v, t), cross(v, w))]
19
+ return v(
20
+ s1 * s2 - dot(v1, v2),
21
+ ...sum(times(v2, s1), times(v1, s2), cross(v1, v2)),
22
+ )
20
23
  }
@@ -21,7 +21,6 @@ export function intersectsPoint(rectangle, point) {
21
21
 
22
22
  /**
23
23
  * Checks if a rectangle intersects with a circle.
24
- *
25
24
  * @param {Rectangle} rectangle - The rectangle to check.
26
25
  * @param {Circle} circle - The circle to check.
27
26
  * @returns {boolean} True if the rectangle intersects the circle, false otherwise.
@@ -32,7 +31,6 @@ export function intersectsCircle(rectangle, circle) {
32
31
 
33
32
  /**
34
33
  * Determines whether a rectangle intersects another in 3D space.
35
- *
36
34
  * @param {Rectangle} rectangle1 - The first rectangle, defined by its position (x, y, z) and size (width, height, depth).
37
35
  * @param {Rectangle} rectangle2 - The second rectangle, defined by its position (x, y, z) and size (width, height, depth).
38
36
  * @returns {boolean} True if the two rectangles intersect, false otherwise.
package/src/math/rng.js CHANGED
@@ -5,7 +5,6 @@
5
5
  * - If no arguments are provided, returns a random float between 0 (inclusive) and 1 (exclusive).
6
6
  * - If one argument is provided, returns a random integer between 0 and the given number (inclusive).
7
7
  * - If two or more arguments are provided, returns a random integer within the specified range and step.
8
- *
9
8
  * @param {number} [to] - The upper bound (inclusive) if one argument is provided.
10
9
  * @param {number} [from] - The lower bound (inclusive) if two arguments are provided.
11
10
  * @param {number} [step=1] - The step size if two or more arguments are provided.
@@ -36,7 +35,6 @@ export function random(...args) {
36
35
  /**
37
36
  * Generates a random binomial value.
38
37
  * The result is the difference between two random numbers.
39
- *
40
38
  * @returns {number} A random binomial value.
41
39
  */
42
40
  export function randomBinomial() {
@@ -12,7 +12,6 @@ const BEFORE_SEGMENT = 0
12
12
  /**
13
13
  * Calculates the coefficients [a, b, c] of the line equation ax + bz + c = 0
14
14
  * for a given segment in 2D space.
15
- *
16
15
  * @param {Segment} segment - The segment defined by its start (`from`) and end (`to`) points.
17
16
  * @returns {[number, number, number]} An array [a, b, c] representing the line equation.
18
17
  */
@@ -24,7 +23,6 @@ export function coefficients(segment) {
24
23
 
25
24
  /**
26
25
  * Finds the closest point on a segment to a given point in 3D space.
27
- *
28
26
  * @param {Segment} segment - The segment defined by its start (`from`) and end (`to`) points.
29
27
  * @param {Point} point - The point in 3D space represented as [x, y, z].
30
28
  * @returns {Point} The closest point on the segment to the given point.
@@ -50,7 +48,6 @@ export function closestPoint(segment, point) {
50
48
 
51
49
  /**
52
50
  * Calculates the shortest distance from a point to a segment in 3D space.
53
- *
54
51
  * @param {Segment} segment - The segment defined by its start (`from`) and end (`to`) points.
55
52
  * @param {Point} point - The point in 3D space represented as [x, y, z].
56
53
  * @returns {number} The shortest distance from the point to the segment.
@@ -62,7 +59,6 @@ export function distanceFromPoint(segment, point) {
62
59
 
63
60
  /**
64
61
  * Determines whether a segment intersects with the perimeter of a circle.
65
- *
66
62
  * @param {Segment} segment - The segment defined by its start (`from`) and end (`to`) points.
67
63
  * @param {Circle} circle - The circle defined by its position (center) and radius.
68
64
  * @returns {boolean} `true` if the segment intersects the circle's perimeter, otherwise `false`.
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Calculates the length of the hypothenuse (or magnitude) for a given set of numbers
3
3
  * using the Pythagorean theorem.
4
- *
5
4
  * @param {number[]} nums - A list of numbers representing the sides of a right triangle.
6
5
  * @returns {number} The length of the hypothenuse.
7
6
  */
@@ -11,5 +10,6 @@ export function hypothenuse(...nums) {
11
10
 
12
11
  /**
13
12
  * Alias for the `hypothenuse` function.
13
+ * @type {typeof hypothenuse}
14
14
  */
15
15
  export const pythagoras = hypothenuse
@@ -31,17 +31,15 @@ const DEFAULT_DECIMALS = 0
31
31
 
32
32
  /**
33
33
  * Returns the absolute value of each component in the vector.
34
- *
35
34
  * @param {Vector} vector - The input vector.
36
35
  * @returns {Vector} The vector with absolute values.
37
36
  */
38
37
  export function abs(vector) {
39
- return vector.map(nAbs)
38
+ return v(...vector.map(nAbs))
40
39
  }
41
40
 
42
41
  /**
43
42
  * Calculates the angle of the vector in radians.
44
- *
45
43
  * @param {Vector} vector - The input vector.
46
44
  * @returns {number} The angle of the vector in the XZ plane (for 3D) or XY plane (for 2D), in radians.
47
45
  */
@@ -51,7 +49,6 @@ export function angle(vector) {
51
49
 
52
50
  /**
53
51
  * Clamps the magnitude of the vector between the given min and max values.
54
- *
55
52
  * @param {Vector} vector - The input vector.
56
53
  * @param {number|Vector} min - The minimum magnitude or a vector representing the lower bounds for each component.
57
54
  * @param {number|Vector} max - The maximum magnitude or a vector representing the upper bounds for each component.
@@ -69,8 +66,10 @@ export function clamp(vector, min, max) {
69
66
  }
70
67
 
71
68
  if (typeof min !== "number" && typeof max !== "number") {
72
- return vector.map((coordinate, index) =>
73
- nClamp(coordinate, min[index], max[index]),
69
+ return v(
70
+ ...vector.map((coordinate, index) =>
71
+ nClamp(coordinate, min[index], max[index]),
72
+ ),
74
73
  )
75
74
  }
76
75
 
@@ -79,17 +78,17 @@ export function clamp(vector, min, max) {
79
78
 
80
79
  /**
81
80
  * Returns the conjugate of the vector.
82
- *
83
81
  * @param {Vector} vector - The input vector.
84
82
  * @returns {Vector} The conjugated vector.
85
83
  */
86
84
  export function conjugate(vector) {
87
- return vector.map((coordinate, index) => (index ? -coordinate : coordinate))
85
+ return v(
86
+ ...vector.map((coordinate, index) => (index ? -coordinate : coordinate)),
87
+ )
88
88
  }
89
89
 
90
90
  /**
91
91
  * Creates a 3D vector with the given magnitude and angle.
92
- *
93
92
  * @param {number} magnitude - The magnitude of the vector.
94
93
  * @param {number} angle - The angle of the vector in radians.
95
94
  * @returns {Vector3} The created 3D vector.
@@ -100,24 +99,22 @@ export function createVector(magnitude, angle) {
100
99
 
101
100
  /**
102
101
  * Divides each component of the vector by the given scalar.
103
- *
104
102
  * @param {Vector} vector - The input vector.
105
103
  * @param {number} scalar - The scalar value.
106
104
  * @returns {Vector} The resulting vector.
107
105
  */
108
106
  export function divide(vector, scalar) {
109
- return vector.map((coordinate) => coordinate / scalar)
107
+ return v(...vector.map((coordinate) => coordinate / scalar))
110
108
  }
111
109
 
112
110
  /**
113
111
  * Converts a 2D vector [x, z] into a 3D vector [x, 0, z].
114
- *
115
112
  * @param {Vector2} vector - A 2D vector represented as [x, z].
116
113
  * @returns {Vector3} A 3D vector represented as [x, 0, z].
117
114
  */
118
115
  export function from2D(vector) {
119
116
  const [x, z] = vector
120
- return [x, NO_Y, z]
117
+ return v(x, NO_Y, z)
121
118
  }
122
119
 
123
120
  /**
@@ -133,7 +130,6 @@ export function fromAngle(angle) {
133
130
  * Checks if a value is a vector.
134
131
  * A vector is an array of numbers. For performance, vectors created with `v()`
135
132
  * are tagged and can be identified more quickly.
136
- *
137
133
  * @param {*} value - The value to check.
138
134
  * @returns {boolean} True if the value is a vector, false otherwise.
139
135
  */
@@ -147,8 +143,6 @@ export function isVector(value) {
147
143
 
148
144
  /**
149
145
  * Calculates the magnitude (length) of the vector.
150
- * Alias: `length`.
151
- *
152
146
  * @param {Vector} vector - The input vector.
153
147
  * @returns {number} The magnitude of the vector.
154
148
  */
@@ -160,49 +154,71 @@ export const length = magnitude
160
154
 
161
155
  /**
162
156
  * Calculates the modulus of each component in the vector with the given divisor.
163
- * Alias: `remainder`.
164
- *
165
157
  * @param {Vector} vector - The input vector.
166
158
  * @param {number} divisor - The divisor value.
167
159
  * @returns {Vector} The resulting vector.
168
160
  */
169
161
  export function mod(vector, divisor) {
170
- return vector.map((coordinate) => nMod(coordinate, divisor))
162
+ return v(...vector.map((coordinate) => nMod(coordinate, divisor)))
171
163
  }
172
164
 
173
165
  /**
174
166
  * Multiplies each component of the vector by the given scalar.
175
- * Aliases: `scale`, `times`.
176
- *
177
167
  * @param {Vector} vector - The input vector.
178
168
  * @param {number} scalar - The scalar value.
179
169
  * @returns {Vector} The resulting vector.
180
170
  */
181
171
  export function multiply(vector, scalar) {
182
- return vector.map((coordinate) => coordinate * scalar)
172
+ return v(...vector.map((coordinate) => coordinate * scalar))
173
+ }
174
+
175
+ /**
176
+ * Alias for the power function.
177
+ * @type {typeof power}
178
+ */
179
+ export const pow = power
180
+
181
+ /**
182
+ * Raises each component of the vector to the given exponent.
183
+ * @param {Vector} vector - The input vector.
184
+ * @param {number} exponent - The exponent value.
185
+ * @returns {Vector} The resulting vector.
186
+ */
187
+ export function power(vector, exponent) {
188
+ return v(...vector.map((coordinate) => coordinate ** exponent))
183
189
  }
184
190
 
191
+ /**
192
+ * Alias for the multiply function.
193
+ * @type {typeof multiply}
194
+ */
185
195
  export const scale = multiply
186
196
 
197
+ /**
198
+ * Alias for the multiply function.
199
+ * @type {typeof multiply}
200
+ */
187
201
  export const times = multiply
188
202
 
189
203
  /**
190
204
  * Normalizes the vector to have a magnitude of 1.
191
- *
192
205
  * @param {Vector} vector - The input vector.
193
206
  * @returns {Vector} The normalized vector.
194
207
  */
195
208
  export function normalize(vector) {
196
209
  const length = magnitude(vector)
197
- return vector.map((coordinate) => coordinate / length)
210
+ return v(...vector.map((coordinate) => coordinate / length))
198
211
  }
199
212
 
213
+ /**
214
+ * Alias for the mod function.
215
+ * @type {typeof mod}
216
+ */
200
217
  export const remainder = mod
201
218
 
202
219
  /**
203
220
  * Rotates the vector by the given angle. Handles 2D and 3D vectors.
204
221
  * For 3D vectors, rotation is around the Y-axis.
205
- *
206
222
  * @param {Vector} vector - The input vector (2D or 3D).
207
223
  * @param {number} angle - The angle in radians.
208
224
  * @returns {Vector} The rotated vector.
@@ -219,7 +235,6 @@ export function rotate(vector, angle) {
219
235
 
220
236
  /**
221
237
  * Sets the angle of the vector in the XZ plane while maintaining its magnitude.
222
- *
223
238
  * @param {Vector} vector - The input vector.
224
239
  * @param {number} angle - The new angle in radians.
225
240
  * @returns {Vector3} The vector with the updated angle.
@@ -227,19 +242,17 @@ export function rotate(vector, angle) {
227
242
  export function setAngle(vector, angle) {
228
243
  const length = magnitude(vector)
229
244
  const [x, z] = toCartesian([length, angle])
230
- return [x, NO_Y, z]
245
+ return v(x, NO_Y, z)
231
246
  }
232
247
 
233
248
  /**
234
- * Sets the length of the vector while maintaining its direction. Alias of `setMagnitude`.
235
- * Alias: `setLength`.
249
+ * Alias for the setMagnitude function.
250
+ * @type {typeof setMagnitude}
236
251
  */
237
252
  export const setLength = setMagnitude
238
253
 
239
254
  /**
240
255
  * Sets the magnitude of the vector while maintaining its direction.
241
- * Alias: `setLength`.
242
- *
243
256
  * @param {Vector} vector - The input vector.
244
257
  * @param {number} length - The new magnitude.
245
258
  * @returns {Vector} The vector with the updated magnitude.
@@ -251,72 +264,67 @@ export function setMagnitude(vector, length) {
251
264
 
252
265
  /**
253
266
  * Shifts the components of the vector by the given index.
254
- *
255
267
  * @param {Vector} vector - The input vector.
256
268
  * @param {number} index - The index to shift by.
257
269
  * @returns {Vector} The shifted vector.
258
270
  */
259
271
  export function shift(vector, index) {
260
- return [...vector.slice(index), ...vector.slice(X, index)]
272
+ return v(...vector.slice(index), ...vector.slice(X, index))
261
273
  }
262
274
 
263
275
  /**
264
276
  * Snaps each component of the vector to the nearest multiple of the given precision.
265
- *
266
277
  * @param {Vector} vector - The input vector.
267
278
  * @param {number} [precision=DEFAULT_PRECISION] - The precision value.
268
279
  * @returns {Vector} The snapped vector.
269
280
  */
270
281
  export function snap(vector, precision = DEFAULT_PRECISION) {
271
- return vector.map((coordinate) => nSnap(coordinate, -precision, precision))
282
+ return v(
283
+ ...vector.map((coordinate) => nSnap(coordinate, -precision, precision)),
284
+ )
272
285
  }
273
286
 
274
287
  /**
275
288
  * Converts a 3D vector [x, y, z] into a 2D vector [x, z].
276
- *
277
289
  * @param {Vector3} vector - A 3D vector represented as [x, y, z].
278
290
  * @returns {Vector2} A 2D vector represented as [x, z].
279
291
  */
280
292
  export function to2D(vector) {
281
293
  const [x, , z] = vector
282
- return [x, z]
294
+ return v(x, z)
283
295
  }
284
296
 
285
297
  /**
286
298
  * Converts 2D polar coordinates to 2D Cartesian coordinates.
287
- *
288
299
  * @param {Vector2} vector - The polar coordinates [magnitude, angle].
289
300
  * @returns {Vector2} The Cartesian coordinates [x, y].
290
301
  */
291
302
  export function toCartesian([magnitude, angle]) {
292
- return [magnitude * cos(angle), magnitude * sin(angle)]
303
+ return v(magnitude * cos(angle), magnitude * sin(angle))
293
304
  }
294
305
 
295
306
  /**
296
307
  * Converts a 3D cartesian vector to cylindrical coordinates.
297
- *
298
308
  * @param {Vector3} vector - The input vector [x, y, z].
299
309
  * @returns {Vector3} The cylindrical coordinates [radius, theta, z].
300
310
  */
301
311
  export function toCylindrical(vector) {
302
312
  const radius = magnitude(vector)
303
313
  const theta = angle(vector)
304
- return [radius * cos(theta), radius * sin(theta), vector[Z]]
314
+ return v(radius * cos(theta), radius * sin(theta), vector[Z])
305
315
  }
306
316
 
307
317
  /**
308
318
  * Converts a 2D cartesian vector to 2D polar coordinates.
309
- *
310
319
  * @param {Vector2} vector - The input vector [x, y].
311
320
  * @returns {Vector2} The polar coordinates [magnitude, angle].
312
321
  */
313
322
  export function toPolar(vector) {
314
- return [magnitude(vector), angle(vector)]
323
+ return v(magnitude(vector), angle(vector))
315
324
  }
316
325
 
317
326
  /**
318
327
  * Converts a vector to a string representation.
319
- *
320
328
  * @param {Vector} vector - The input vector.
321
329
  * @param {number} [decimals=DEFAULT_DECIMALS] - The number of decimal places.
322
330
  * @returns {string} The string representation of the vector.
@@ -327,14 +335,11 @@ export function toString(vector, decimals = DEFAULT_DECIMALS) {
327
335
  .join(", ")}]`
328
336
  }
329
337
 
330
- // TODO: add toSpherical(vector),
331
-
332
338
  /**
333
339
  * Converts a 3D cartesian vector to spherical coordinates [radius, inclination, azimuth].
334
340
  * - radius (r): distance from the origin.
335
341
  * - inclination (θ): angle from the Y-axis (0 to PI).
336
342
  * - azimuth (φ): angle from the X-axis in the XZ-plane (-PI to PI).
337
- *
338
343
  * @param {Vector3} vector The cartesian vector [x, y, z].
339
344
  * @returns {Vector3} The spherical coordinates [r, θ, φ].
340
345
  */
@@ -350,18 +355,17 @@ export function toSpherical(vector) {
350
355
  // Azimuth is the angle in the XZ plane, which `angle()` calculates.
351
356
  const azimuth = angle(vector)
352
357
 
353
- return [r, inclination, azimuth]
358
+ return v(r, inclination, azimuth)
354
359
  }
355
360
 
356
361
  /**
357
362
  * Creates a 3D unit vector in the XZ plane with the given angle.
358
- *
359
363
  * @param {number} [angle=0] - The angle in radians.
360
364
  * @returns {Vector3} The unit vector.
361
365
  */
362
366
  export function unit(angle) {
363
367
  if (!angle) {
364
- return [...UNIT_VECTOR]
368
+ return v(...UNIT_VECTOR)
365
369
  }
366
370
 
367
371
  return setAngle(UNIT_VECTOR, angle)
@@ -369,16 +373,14 @@ export function unit(angle) {
369
373
 
370
374
  /**
371
375
  * Creates a zero vector of 3 dimensions.
372
- *
373
376
  * @returns {Vector3} The zero vector.
374
377
  */
375
378
  export function zero() {
376
- return [...ZERO_VECTOR]
379
+ return v(...ZERO_VECTOR)
377
380
  }
378
381
 
379
382
  /**
380
383
  * Rotates a 3D vector around the Y-axis using a quaternion.
381
- *
382
384
  * @param {Vector3} vector - The input vector.
383
385
  * @param {number} angle - The angle in radians.
384
386
  * @returns {Vector3} The rotated vector.
@@ -17,6 +17,7 @@ import {
17
17
  mod,
18
18
  multiply,
19
19
  normalize,
20
+ power,
20
21
  rotate,
21
22
  setAngle,
22
23
  setMagnitude,
@@ -40,7 +41,7 @@ test("it should compute the absolute value of a vector's coordinates", () => {
40
41
  })
41
42
 
42
43
  test("it should compute the angle of a 2D vector", () => {
43
- const vector = [1, 1]
44
+ const vector = v(1, 1)
44
45
  const expectedResult = pi() / 4
45
46
 
46
47
  expect(angle(vector)).toBe(expectedResult)
@@ -54,28 +55,28 @@ test("it should compute the angle of a 3D vector", () => {
54
55
  })
55
56
 
56
57
  test("it should clamp the magnitude of a vector to a certain length if too long", () => {
57
- const vector = [6, 8]
58
+ const vector = v(6, 8)
58
59
  const min = 0
59
60
  const max = 5
60
- const expectedResult = [3, 4]
61
+ const expectedResult = v(3, 4)
61
62
 
62
63
  expect(clamp(vector, min, max)).toStrictEqual(expectedResult)
63
64
  })
64
65
 
65
66
  test("it should not clamp the magnitude of a vector to a certain length if not too long", () => {
66
- const vector = [3, 4]
67
+ const vector = v(3, 4)
67
68
  const min = 0
68
69
  const max = 6
69
- const expectedResult = [3, 4]
70
+ const expectedResult = v(3, 4)
70
71
 
71
72
  expect(clamp(vector, min, max)).toStrictEqual(expectedResult)
72
73
  })
73
74
 
74
75
  test("it should clamp the magnitude of a vector to the values of other vector bounds", () => {
75
- const vector = [6, 8]
76
+ const vector = v(6, 8)
76
77
  const lowerBound = [0, 0]
77
78
  const upperBound = [3, 4]
78
- const expectedResult = [3, 4]
79
+ const expectedResult = v(3, 4)
79
80
 
80
81
  expect(clamp(vector, lowerBound, upperBound)).toStrictEqual(expectedResult)
81
82
  })
@@ -104,7 +105,7 @@ test("it should divide a vector by a scalar", () => {
104
105
  })
105
106
 
106
107
  test("it should build a 3D vector from a 2D one", () => {
107
- const vector = [3, 4]
108
+ const vector = v(3, 4)
108
109
  const expectedResult = v(3, 0, 4)
109
110
 
110
111
  expect(from2D(vector)).toStrictEqual(expectedResult)
@@ -118,14 +119,14 @@ test("it should create a 3D unit vector from an angle", () => {
118
119
  })
119
120
 
120
121
  test("it should compute the magnitude of a vector (aka length)", () => {
121
- const vector = [3, 4]
122
+ const vector = v(3, 4)
122
123
  const expectedResult = 5
123
124
 
124
125
  expect(magnitude(vector)).toBe(expectedResult)
125
126
  })
126
127
 
127
128
  test("it should check if a value is a vector", () => {
128
- expect(isVector(v(1, 2, 3))).toBe(true)
129
+ expect(isVector([1, 2, 3])).toBe(true)
129
130
  expect(isVector([])).toBe(true)
130
131
  expect(isVector(v(1, 2, 3))).toBe(true)
131
132
  })
@@ -155,23 +156,31 @@ test("it should multiply a vector with a scalar (aka times)", () => {
155
156
  })
156
157
 
157
158
  test("it should normalize a vector so it has unit length", () => {
158
- const vector = [3, 4]
159
- const expectedResult = [0.6, 0.8]
159
+ const vector = v(3, 4)
160
+ const expectedResult = v(0.6, 0.8)
160
161
 
161
162
  expect(normalize(vector)).toStrictEqual(expectedResult)
162
163
  })
163
164
 
164
165
  test("it should normalize a negative vector", () => {
165
- const vector = [-3, -4]
166
- const expectedResult = [-0.6, -0.8]
166
+ const vector = v(-3, -4)
167
+ const expectedResult = v(-0.6, -0.8)
167
168
 
168
169
  expect(normalize(vector)).toStrictEqual(expectedResult)
169
170
  })
170
171
 
172
+ test("it should raise each component of a vector to the given exponent (aka pow)", () => {
173
+ const vector = v(1, 2, 3)
174
+ const exponent = 2
175
+ const expectedResult = v(1, 4, 9)
176
+
177
+ expect(power(vector, exponent)).toStrictEqual(expectedResult)
178
+ })
179
+
171
180
  test("it should rotate a 2D vector by a certain angle", () => {
172
- const vector = [1, 0]
181
+ const vector = v(1, 0)
173
182
  const angle = pi() / 4
174
- const expectedResult = [0.7071067811865475, 0.7071067811865476] // close to [cos(pi/4), sin(pi/4)]
183
+ const expectedResult = v(0.7071067811865475, 0.7071067811865476) // close to [cos(pi/4), sin(pi/4)]
175
184
 
176
185
  expect(rotate(vector, angle)).toStrictEqual(expectedResult)
177
186
  })
@@ -201,32 +210,32 @@ test("it should rotate a vector that faces left by a certain angle", () => {
201
210
  })
202
211
 
203
212
  test("it should change the angle of a vector", () => {
204
- const vector = [cos(pi() / 4), 0, sin(pi() / 4)]
205
- const expectedResult = [6.123233995736766e-17, 0, 1] // close to v(0, 0, 1)
213
+ const vector = v(cos(pi() / 4), 0, sin(pi() / 4))
214
+ const expectedResult = v(6.123233995736766e-17, 0, 1) // close to v(0, 0, 1)
206
215
 
207
216
  expect(setAngle(vector, 1.5707963267948966)).toStrictEqual(expectedResult)
208
217
  })
209
218
 
210
219
  test("it should change magnitude of a vector (aka setLength)", () => {
211
- const vector = [3, 4]
220
+ const vector = v(3, 4)
212
221
  const magnitude = 10
213
- const expectedResult = [6, 8]
222
+ const expectedResult = v(6, 8)
214
223
 
215
224
  expect(setMagnitude(vector, magnitude)).toStrictEqual(expectedResult)
216
225
  })
217
226
 
218
227
  test("it should change magnitude of a negative vector", () => {
219
- const vector = [-3, -4]
228
+ const vector = v(-3, -4)
220
229
  const magnitude = 10
221
- const expectedResult = [-6, -8]
230
+ const expectedResult = v(-6, -8)
222
231
 
223
232
  expect(setMagnitude(vector, magnitude)).toStrictEqual(expectedResult)
224
233
  })
225
234
 
226
235
  test("it should shift a vector at a certain index", () => {
227
- const vector = [1, 2, 3, 4, 5]
236
+ const vector = v(1, 2, 3, 4, 5)
228
237
  const index = 2
229
- const expectedResult = [3, 4, 5, 1, 2]
238
+ const expectedResult = v(3, 4, 5, 1, 2)
230
239
 
231
240
  expect(shift(vector, index)).toStrictEqual(expectedResult)
232
241
  })
@@ -241,14 +250,14 @@ test("it should snap a floating vector to a certain precision", () => {
241
250
 
242
251
  test("it should build a 2D vector from a 3D one", () => {
243
252
  const vector = v(3, 0, 4)
244
- const expectedResult = [3, 4]
253
+ const expectedResult = v(3, 4)
245
254
 
246
255
  expect(to2D(vector)).toStrictEqual(expectedResult)
247
256
  })
248
257
 
249
258
  test("it should convert a 2D polar vector to cartesian coordinates", () => {
250
- const vector = [sqrt(2), pi() / 4]
251
- const expectedResult = [1.0000000000000002, 1]
259
+ const vector = v(sqrt(2), pi() / 4)
260
+ const expectedResult = v(1.0000000000000002, 1)
252
261
 
253
262
  expect(toCartesian(vector)).toStrictEqual(expectedResult)
254
263
  })
@@ -261,28 +270,28 @@ test("it should convert a 3D cartesian vector to cylindrical coordinates", () =>
261
270
  })
262
271
 
263
272
  test("it should convert a 2D cartesian vector to polar coordinates", () => {
264
- const vector = [1, 1]
265
- const expectedResult = [sqrt(2), pi() / 4]
273
+ const vector = v(1, 1)
274
+ const expectedResult = v(sqrt(2), pi() / 4)
266
275
 
267
276
  expect(toPolar(vector)).toStrictEqual(expectedResult)
268
277
  })
269
278
 
270
279
  test("it should convert a 3D cartesian vector to spherical coordinates", () => {
271
280
  const vector = v(1, 1, 1)
272
- const expectedResult = [sqrt(3), Math.acos(1 / sqrt(3)), pi() / 4]
281
+ const expectedResult = v(sqrt(3), Math.acos(1 / sqrt(3)), pi() / 4)
273
282
 
274
283
  expect(toSpherical(vector)).toStrictEqual(expectedResult)
275
284
  })
276
285
 
277
286
  test("it should create a string representation of an integer vector", () => {
278
- const vector = [3, 4]
287
+ const vector = v(3, 4)
279
288
  const expectedResult = "[3, 4]"
280
289
 
281
290
  expect(toString(vector)).toBe(expectedResult)
282
291
  })
283
292
 
284
293
  test("it should create a string representation of a float vector", () => {
285
- const vector = [pi() / 4, pi() / 4]
294
+ const vector = v(pi() / 4, pi() / 4)
286
295
  const decimals = 2
287
296
  const expectedResult = "[0.79, 0.79]"
288
297
 
@@ -294,21 +303,15 @@ test("it should create a unit vector oriented on the X-axis", () => {
294
303
  })
295
304
 
296
305
  test("it should create a unit vector oriented on the Z-axis", () => {
297
- expect(unit(pi() / 2)).toStrictEqual([6.123233995736766e-17, 0, 1]) // close to v(0, 0, 1)
306
+ expect(unit(pi() / 2)).toStrictEqual(v(6.123233995736766e-17, 0, 1)) // close to v(0, 0, 1)
298
307
  })
299
308
 
300
309
  test("it should create a vector from a list of coordinates", () => {
301
- expect(v(1, 2, 3)).toStrictEqual(v(1, 2, 3))
310
+ expect(v(1, 2, 3)).toStrictEqual([1, 2, 3])
302
311
  expect(v()).toStrictEqual([])
303
312
  expect(v(0, -5.5)).toStrictEqual([0, -5.5])
304
313
  })
305
314
 
306
- test("it should create a vector with a __isVector__ property", () => {
307
- const vector = v(1, 2, 3)
308
- expect(vector).toStrictEqual(v(1, 2, 3))
309
- expect(vector.__isVector__).toBe(true)
310
- })
311
-
312
315
  test("it should create a 3D zero vector", () => {
313
316
  const expectedResult = v(0, 0, 0)
314
317
 
@@ -4,6 +4,7 @@
4
4
  * @typedef {import('./types').Vector7} Vector7
5
5
  */
6
6
 
7
+ import { v } from "../v.js"
7
8
  import { magnitude, shift } from "./vector.js"
8
9
 
9
10
  /**
@@ -18,7 +19,7 @@ export const add = sum
18
19
  * @returns {Vector3 | Vector7} The resulting vector after the cross product.
19
20
  */
20
21
  export function cross(...vectors) {
21
- return vectors.reduce(crossMultiplyCoordinates)
22
+ return v(...vectors.reduce(crossMultiplyCoordinates))
22
23
  }
23
24
 
24
25
  /**
@@ -53,7 +54,7 @@ export const scalarProduct = dot
53
54
  * @returns {Vector2 | Vector3} The resulting vector after subtraction.
54
55
  */
55
56
  export function subtract(...vectors) {
56
- return vectors.reduce(subtractCoordinates)
57
+ return v(...vectors.reduce(subtractCoordinates))
57
58
  }
58
59
 
59
60
  /**
@@ -62,7 +63,7 @@ export function subtract(...vectors) {
62
63
  * @returns {Vector2 | Vector3} The resulting vector after summation.
63
64
  */
64
65
  export function sum(...vectors) {
65
- return vectors.reduce(sumCoordinates)
66
+ return v(...vectors.reduce(sumCoordinates))
66
67
  }
67
68
 
68
69
  /**
@@ -78,7 +79,7 @@ export const vectorProduct = cross
78
79
  * @returns {Vector2 | Vector3} The resulting vector after addition.
79
80
  */
80
81
  function sumCoordinates(vector1, vector2) {
81
- return vector1.map((coordinate, index) => coordinate + vector2[index])
82
+ return v(...vector1.map((coordinate, index) => coordinate + vector2[index]))
82
83
  }
83
84
 
84
85
  /**
@@ -88,7 +89,7 @@ function sumCoordinates(vector1, vector2) {
88
89
  * @returns {Vector2 | Vector3} The resulting vector after subtraction.
89
90
  */
90
91
  function subtractCoordinates(vector1, vector2) {
91
- return vector1.map((coordinate, index) => coordinate - vector2[index])
92
+ return v(...vector1.map((coordinate, index) => coordinate - vector2[index]))
92
93
  }
93
94
 
94
95
  /**
@@ -102,13 +103,17 @@ function crossMultiplyCoordinates(vector1, vector2) {
102
103
  .fill(null)
103
104
  .map((_, index) => index)
104
105
 
105
- return indexes.map((_, index) => {
106
- const [index1, index2] = shift(
107
- indexes.filter((_, i) => i !== index),
108
- index,
109
- )
110
- return vector1[index1] * vector2[index2] - vector1[index2] * vector2[index1]
111
- })
106
+ return v(
107
+ ...indexes.map((_, index) => {
108
+ const [index1, index2] = shift(
109
+ indexes.filter((_, i) => i !== index),
110
+ index,
111
+ )
112
+ return (
113
+ vector1[index1] * vector2[index2] - vector1[index2] * vector2[index1]
114
+ )
115
+ }),
116
+ )
112
117
  }
113
118
 
114
119
  /**
@@ -118,5 +123,5 @@ function crossMultiplyCoordinates(vector1, vector2) {
118
123
  * @returns {Vector2 | Vector3} The resulting vector containing multiplied coordinates.
119
124
  */
120
125
  function dotMultiplyCoordinates(vector1, vector2) {
121
- return vector1.map((coordinate, index) => coordinate * vector2[index])
126
+ return v(...vector1.map((coordinate, index) => coordinate * vector2[index]))
122
127
  }
@@ -14,8 +14,8 @@ const HALF_ACCELERATION = 0.5 // Half of the acceleration factor used in positio
14
14
  * Applies acceleration to an object using Euler's integration method.
15
15
  *
16
16
  * Euler's Integration:
17
- * v += a * dt
18
- * p += v * dt + 1/2 * a * dt * dt
17
+ * 1. v += a * dt
18
+ * 2. p += v * dt + 1/2 * a * dt * dt
19
19
  *
20
20
  * @param {Object} params - The parameters for the function.
21
21
  * @param {number} params.maxAcceleration - The maximum allowed acceleration.
@@ -9,7 +9,6 @@ const NO_FRICTION = 0 // No friction constant.
9
9
 
10
10
  /**
11
11
  * Applies friction to a given velocity vector.
12
- *
13
12
  * @param {Object} params - The parameters for the function.
14
13
  * @param {Vector3} params.velocity - The velocity vector. Defaults to a zero vector.
15
14
  * @param {number} params.friction - The friction coefficient. Defaults to 0.
@@ -11,7 +11,6 @@ const DEFAULT_MAX_SPEED = 0 // Default maximum speed.
11
11
  /**
12
12
  * Calculates the vertical velocity (vy) of a jump
13
13
  * based on the provided parameters.
14
- *
15
14
  * @param {Object} params - Parameters for the jump calculation.
16
15
  * @param {number} params.maxJump - Maximum jump height. Defaults to 0.
17
16
  * @param {number} params.maxLeap - Maximum leap distance. Defaults to 0.
@@ -10,7 +10,6 @@ const DEFAULT_MAX_SPEED = 0 // Default maximum speed for velocity clamping.
10
10
 
11
11
  /**
12
12
  * Applies velocity to a position based on the given parameters and options.
13
- *
14
13
  * @param {Object} params - The parameters for velocity application.
15
14
  * @param {number} [params.maxSpeed] - The maximum speed for clamping the velocity. Defaults to 0.
16
15
  * @param {Vector3} [params.velocity] - The current velocity vector. Defaults to a zero vector.
package/src/v.js CHANGED
@@ -3,7 +3,6 @@
3
3
  * This is a shorthand for `[x, y, z, ...]`.
4
4
  * It also tags the array with a non-enumerable `__isVector__` property for
5
5
  * efficient type checking.
6
- *
7
6
  * @param {...number} coords - The coordinates of the vector.
8
7
  * @returns {Vector} The created vector.
9
8
  */
package/src/v.test.js ADDED
@@ -0,0 +1,13 @@
1
+ import { expect, test } from "vitest"
2
+
3
+ import { v } from "./v"
4
+
5
+ test("a vector created with the v() function is just a number array", () => {
6
+ const vector = v(1, 2, 3)
7
+ expect(vector).toStrictEqual([1, 2, 3])
8
+ })
9
+
10
+ test("a vector created with the v() function has a __isVector__ property", () => {
11
+ const vector = v(1, 2, 3)
12
+ expect(vector.__isVector__).toBe(true)
13
+ })