@inglorious/utils 4.0.0 → 5.0.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.
Files changed (42) hide show
  1. package/README.md +22 -6
  2. package/package.json +35 -19
  3. package/src/algorithms/path-finding.js +2 -2
  4. package/src/data-structures/array.js +12 -0
  5. package/src/data-structures/index.js +2 -2
  6. package/src/data-structures/{objects.js → object.js} +87 -36
  7. package/src/data-structures/string.js +77 -0
  8. package/src/functions/index.js +1 -1
  9. package/src/math/circle.js +1 -1
  10. package/src/math/hitmask.js +1 -1
  11. package/src/math/index.js +4 -4
  12. package/src/math/line.js +1 -1
  13. package/src/math/point.js +23 -2
  14. package/src/math/{quaternions.js → quaternion.js} +3 -3
  15. package/src/math/rectangle.js +12 -0
  16. package/src/math/rng.js +5 -2
  17. package/src/math/segment.js +1 -1
  18. package/src/math/trigonometry.js +1 -1
  19. package/src/math/{vectors.js → vector.js} +67 -8
  20. package/src/physics/acceleration.js +2 -2
  21. package/src/physics/friction.js +2 -2
  22. package/src/physics/gravity.js +2 -2
  23. package/src/physics/velocity.js +2 -2
  24. package/src/v.js +1 -1
  25. package/types/algorithms/path-finding.d.ts +1 -1
  26. package/types/data-structures/array.d.ts +5 -0
  27. package/types/data-structures/index.d.ts +1 -1
  28. package/types/data-structures/{objects.d.ts → object.d.ts} +1 -1
  29. package/types/math/index.d.ts +3 -3
  30. package/types/math/point.d.ts +26 -1
  31. package/types/math/{quaternions.d.ts → quaternion.d.ts} +1 -1
  32. package/types/math/rectangle.d.ts +23 -2
  33. package/types/math/{vectors.d.ts → vector.d.ts} +29 -0
  34. package/types/physics/acceleration.d.ts +1 -1
  35. package/types/physics/friction.d.ts +1 -1
  36. package/types/physics/gravity.d.ts +1 -1
  37. package/types/physics/velocity.d.ts +1 -1
  38. package/types/v.d.ts +1 -1
  39. /package/src/functions/{functions.js → function.js} +0 -0
  40. /package/src/math/{numbers.js → number.js} +0 -0
  41. /package/types/functions/{functions.d.ts → function.d.ts} +0 -0
  42. /package/types/math/{numbers.d.ts → number.d.ts} +0 -0
package/README.md CHANGED
@@ -92,7 +92,7 @@ const throttledSave = throttle(() => {
92
92
  Perform vector operations on plain objects.
93
93
 
94
94
  ```javascript
95
- import { add } from "@inglorious/utils/vectors"
95
+ import { add } from "@inglorious/utils/vector.js"
96
96
 
97
97
  const position = [10, 20]
98
98
  const velocity = [2, -1]
@@ -101,6 +101,23 @@ const newPosition = add(position, velocity)
101
101
  // => [12, 19]
102
102
  ```
103
103
 
104
+ Two vectors are worth knowing by name: `ZERO_VECTOR`, with every component at zero, and
105
+ `UNIT_VECTOR`, one unit long along the x axis. The zero one is handy for naming the components
106
+ you care about, or for building a vector up from scratch:
107
+
108
+ ```javascript
109
+ import { ZERO_VECTOR } from "@inglorious/utils/math/vector.js"
110
+ import { v } from "@inglorious/utils/v.js"
111
+
112
+ const [, NO_RISE, NO_DEPTH] = ZERO_VECTOR
113
+
114
+ const position = v(10, 20, NO_DEPTH)
115
+ ```
116
+
117
+ Vectors are mutable arrays, so copy a shared vector rather than storing it where something writes
118
+ to one, such as an entity position. Copy it with `v(...ZERO_VECTOR)`: a plain spread
119
+ (`[...ZERO_VECTOR]`) gives you a new array but drops the tag that `isVector` looks for.
120
+
104
121
  ---
105
122
 
106
123
  ## API
@@ -111,7 +128,6 @@ Most modules are grouped by category:
111
128
 
112
129
  - **`@inglorious/utils/algorithms`**: Utilities for AI algorithms, like decision trees and A\*.
113
130
  - **`@inglorious/utils/data-structures`**: Utilities for arrays, boards, heaps, objects, sets, strings, and trees.
114
- - **`@inglorious/utils/functions`**: Utilities for function composition and timing.
115
131
  - **`@inglorious/utils/math`**: Utilities for math operations.
116
132
  - **`@inglorious/utils/physics`**: Utilities for calculations on friction, acceleration, and gravity.
117
133
 
@@ -119,15 +135,15 @@ The four largest modules are also available as top-level subpaths, so you don't
119
135
  know whether a helper lives under `data-structures` or `math`:
120
136
 
121
137
  - **`@inglorious/utils/functions`**: `compose`, `debounce`, `isFunction`, `pipe`, `throttle`.
122
- - **`@inglorious/utils/objects`**: Single-object helpers (`clone`, `get`, `set`, `produce`, ...) alongside multi-object helpers (`merge`, `extend`, `defaults`, ...).
123
- - **`@inglorious/utils/quaternions`**: `quaternion` and `combine`.
124
- - **`@inglorious/utils/vectors`**: Single-vector helpers (`magnitude`, `normalize`, `rotate`, ...) alongside multi-vector helpers (`cross`, `dot`, `sum`, ...).
138
+ - **`@inglorious/utils/object`**: Single-object helpers (`clone`, `get`, `set`, `produce`, ...) alongside multi-object helpers (`merge`, `extend`, `defaults`, ...).
139
+ - **`@inglorious/utils/quaternion`**: `quaternion` and `combine`.
140
+ - **`@inglorious/utils/vector`**: Single-vector helpers (`magnitude`, `normalize`, `rotate`, ...) alongside multi-vector helpers (`cross`, `dot`, `sum`, ...).
125
141
 
126
142
  `divide`, `mod`, `multiply` and `power` accept either a scalar or one or more vectors:
127
143
  a scalar is applied to every component, while vectors are combined component-wise.
128
144
 
129
145
  ```javascript
130
- import { multiply } from "@inglorious/utils/vectors"
146
+ import { multiply } from "@inglorious/utils/vector.js"
131
147
 
132
148
  multiply([1, 2, 3], 2) // => [2, 4, 6]
133
149
  multiply([1, 2, 3], [4, 5, 6]) // => [4, 10, 18]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/utils",
3
- "version": "4.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "A set of general-purpose utility functions designed with functional programming principles in mind.",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -26,6 +26,14 @@
26
26
  "types": "./types/index.d.ts",
27
27
  "import": "./src/index.js"
28
28
  },
29
+ "./array.js": {
30
+ "types": "./types/data-structures/array.d.ts",
31
+ "import": "./src/data-structures/array.js"
32
+ },
33
+ "./array": {
34
+ "types": "./types/data-structures/array.d.ts",
35
+ "import": "./src/data-structures/array.js"
36
+ },
29
37
  "./algorithms": {
30
38
  "types": "./types/algorithms/index.d.ts",
31
39
  "import": "./src/algorithms/index.js"
@@ -50,6 +58,14 @@
50
58
  "types": "./types/data-structures/*.d.ts",
51
59
  "import": "./src/data-structures/*.js"
52
60
  },
61
+ "./function.js": {
62
+ "types": "./types/functions/function.d.ts",
63
+ "import": "./src/functions/function.js"
64
+ },
65
+ "./function": {
66
+ "types": "./types/functions/function.d.ts",
67
+ "import": "./src/functions/function.js"
68
+ },
53
69
  "./functions": {
54
70
  "types": "./types/functions/index.d.ts",
55
71
  "import": "./src/functions/index.js"
@@ -78,29 +94,29 @@
78
94
  "types": "./types/physics/*.d.ts",
79
95
  "import": "./src/physics/*.js"
80
96
  },
81
- "./objects.js": {
82
- "types": "./types/data-structures/objects.d.ts",
83
- "import": "./src/data-structures/objects.js"
97
+ "./object.js": {
98
+ "types": "./types/data-structures/object.d.ts",
99
+ "import": "./src/data-structures/object.js"
84
100
  },
85
- "./objects": {
86
- "types": "./types/data-structures/objects.d.ts",
87
- "import": "./src/data-structures/objects.js"
101
+ "./object": {
102
+ "types": "./types/data-structures/object.d.ts",
103
+ "import": "./src/data-structures/object.js"
88
104
  },
89
- "./quaternions.js": {
90
- "types": "./types/math/quaternions.d.ts",
91
- "import": "./src/math/quaternions.js"
105
+ "./quaternion.js": {
106
+ "types": "./types/math/quaternion.d.ts",
107
+ "import": "./src/math/quaternion.js"
92
108
  },
93
- "./quaternions": {
94
- "types": "./types/math/quaternions.d.ts",
95
- "import": "./src/math/quaternions.js"
109
+ "./quaternion": {
110
+ "types": "./types/math/quaternion.d.ts",
111
+ "import": "./src/math/quaternion.js"
96
112
  },
97
- "./vectors.js": {
98
- "types": "./types/math/vectors.d.ts",
99
- "import": "./src/math/vectors.js"
113
+ "./vector.js": {
114
+ "types": "./types/math/vector.d.ts",
115
+ "import": "./src/math/vector.js"
100
116
  },
101
- "./vectors": {
102
- "types": "./types/math/vectors.d.ts",
103
- "import": "./src/math/vectors.js"
117
+ "./vector": {
118
+ "types": "./types/math/vector.d.ts",
119
+ "import": "./src/math/vector.js"
104
120
  },
105
121
  "./v.js": {
106
122
  "types": "./types/v.d.ts",
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * @typedef {import("../../types/algorithms/path-finding").Node} Node
3
3
  * @typedef {import("../../types/algorithms/path-finding").Graph} Graph
4
- * @typedef {import("../../types/math/vectors").Vector2} Vector2
4
+ * @typedef {import("../../types/math/vector").Vector2} Vector2
5
5
  */
6
6
 
7
7
  import { contains, push, remove, root } from "../data-structures/heap.js"
8
- import { abs, magnitude, subtract } from "../math/vectors.js"
8
+ import { abs, magnitude, subtract } from "../math/vector.js"
9
9
 
10
10
  const NO_COST = 0
11
11
 
@@ -22,6 +22,18 @@ export function contains(arr, item) {
22
22
  return arr.includes(item)
23
23
  }
24
24
 
25
+ /**
26
+ * The index `step` places along a list of `length`, wrapping round at either end.
27
+ *
28
+ * @param {number} index - Where to start from.
29
+ * @param {number} length - How long the list is.
30
+ * @param {number} step - How far to move. Negative steps move backwards.
31
+ * @returns {number} - The index landed on, always within the list.
32
+ */
33
+ export function cycleIndex(index, length, step) {
34
+ return (((index + step) % length) + length) % length
35
+ }
36
+
25
37
  /**
26
38
  * Ensures that the given value is an array. If it's not an array, it wraps it in one.
27
39
  *
@@ -2,9 +2,9 @@ import * as array from "./array.js"
2
2
  import * as board from "./board.js"
3
3
  import * as boolean from "./boolean.js"
4
4
  import * as heap from "./heap.js"
5
- import * as objects from "./objects.js"
5
+ import * as object from "./object.js"
6
6
  import * as set from "./set.js"
7
7
  import * as string from "./string.js"
8
8
  import * as tree from "./tree.js"
9
9
 
10
- export { array, board, boolean, heap, objects, set, string, tree }
10
+ export { array, board, boolean, heap, object, set, string, tree }
@@ -1,4 +1,4 @@
1
- import { isFunction } from "../functions/functions.js"
1
+ import { isFunction } from "../functions/function.js"
2
2
  import { v } from "../v.js"
3
3
  import { isArray } from "./array.js"
4
4
 
@@ -28,6 +28,39 @@ export function clone(obj) {
28
28
  return deserialize(serialize(obj))
29
29
  }
30
30
 
31
+ /**
32
+ * Recursively merges properties from a source object into a target object.
33
+ * This is a helper function for `merge` and `extend`.
34
+ *
35
+ * @param {Object} target - The target object to merge into.
36
+ * @param {Object} source - The source object to merge from.
37
+ * @param {Function} [merger] - An optional function to customize merging behavior for specific keys.
38
+ * @returns {Object} - The modified target object.
39
+ */
40
+ function deepMerge(target, source, merger) {
41
+ for (const [key, value] of Object.entries(source)) {
42
+ if (isFunction(merger)) {
43
+ const mergedValue = merger(target[key], value)
44
+ if (mergedValue !== undefined) {
45
+ target[key] = mergedValue
46
+ continue
47
+ }
48
+ }
49
+
50
+ if (isArray(value)) {
51
+ target[key] = value
52
+ } else if (isObject(value)) {
53
+ if (!isObject(target[key])) {
54
+ target[key] = {}
55
+ }
56
+ target[key] = deepMerge(target[key], value, merger)
57
+ } else {
58
+ target[key] = value
59
+ }
60
+ }
61
+ return target
62
+ }
63
+
31
64
  /**
32
65
  * Assigns default properties to a target object from a source object.
33
66
  * For each key in `defaultProps`, if `target[key]` is `null` or `undefined`,
@@ -186,6 +219,57 @@ export function get(obj, path, defaultValue = undefined) {
186
219
  * @param {*} value - The value to check.
187
220
  * @returns {boolean} True if the value is a plain object, false otherwise.
188
221
  */
222
+ /**
223
+ * Whether two values are the same, all the way down.
224
+ *
225
+ * Written here rather than with `JSON.stringify` because stringify cannot tell a key
226
+ * that was left out from one that was set to nothing, and those are different: a key
227
+ * mapped to `undefined` is a key with nothing mapped to it, which is how a mapping is
228
+ * taken off.
229
+ *
230
+ * @param {*} left - The first value.
231
+ * @param {*} right - The second value.
232
+ * @param {WeakMap} [seen] - The pairs already compared, so a structure that contains
233
+ * itself is not walked for ever. Passed along rather than asked for.
234
+ *
235
+ * What it does not settle is whether two structures *share* the same parts. One value
236
+ * used twice against two equal ones written out twice are reported as different, because
237
+ * only the pairs already met are remembered. That errs towards calling things changed
238
+ * when they are not, which is the safe way round for the thing this is asked about.
239
+ *
240
+ * @returns {boolean} Whether the two are the same.
241
+ */
242
+ export function isDeepEqual(left, right, seen = new WeakMap()) {
243
+ if (Object.is(left, right)) return true
244
+
245
+ // `Object.is` has already said no to these two being the same object, and neither can be
246
+ // walked: a function is equal to itself and nothing else, and a key present in one and
247
+ // not the other is a difference even when both read as undefined.
248
+ if (typeof left !== "object" || typeof right !== "object") return false
249
+ if (left === null || right === null) return false
250
+
251
+ if (Array.isArray(left) !== Array.isArray(right)) return false
252
+
253
+ // A thing that contains itself would otherwise be walked for ever. Meeting the same
254
+ // thing twice means it has already been compared, and it was the same then or the walk
255
+ // would have stopped there -- so the only question left is whether it was matched to
256
+ // this one. A structure that loops back to itself differently is a difference, which is
257
+ // what stops this reporting two equal loops that are not.
258
+ if (seen.has(left)) return seen.get(left) === right
259
+
260
+ seen.set(left, right)
261
+
262
+ const leftKeys = Object.keys(left)
263
+ const rightKeys = Object.keys(right)
264
+
265
+ if (leftKeys.length !== rightKeys.length) return false
266
+
267
+ return leftKeys.every(
268
+ (key) =>
269
+ Object.hasOwn(right, key) && isDeepEqual(left[key], right[key], seen),
270
+ )
271
+ }
272
+
189
273
  export function isObject(value) {
190
274
  return value != null && value.constructor === Object
191
275
  }
@@ -286,7 +370,7 @@ export function produce(baseState, recipe, ...args) {
286
370
  */
287
371
  export function serialize(obj) {
288
372
  function replacer(key, value) {
289
- // Handle top-level vector
373
+ // Handle top-stage vector
290
374
  if (value?.__isVector__) {
291
375
  return {
292
376
  _type: "vector",
@@ -360,7 +444,7 @@ export function set(obj, path, value) {
360
444
  * Converts an object or array to a formatted string representation.
361
445
  *
362
446
  * @param {*} obj - The object or array to convert.
363
- * @param {number} [indentationLevel=INITIAL_LEVEL] - The current indentation level (used for nested structures).
447
+ * @param {number} [indentationLevel=INITIAL_LEVEL] - The current indentation stage (used for nested structures).
364
448
  * @returns {string} A string representation of the input object or array.
365
449
  */
366
450
  export function toString(obj, indentationLevel = INITIAL_LEVEL) {
@@ -396,36 +480,3 @@ ${" ".repeat(indentationLevel)}}`
396
480
 
397
481
  return obj
398
482
  }
399
-
400
- /**
401
- * Recursively merges properties from a source object into a target object.
402
- * This is a helper function for `merge` and `extend`.
403
- *
404
- * @param {Object} target - The target object to merge into.
405
- * @param {Object} source - The source object to merge from.
406
- * @param {Function} [merger] - An optional function to customize merging behavior for specific keys.
407
- * @returns {Object} - The modified target object.
408
- */
409
- function deepMerge(target, source, merger) {
410
- for (const [key, value] of Object.entries(source)) {
411
- if (isFunction(merger)) {
412
- const mergedValue = merger(target[key], value)
413
- if (mergedValue !== undefined) {
414
- target[key] = mergedValue
415
- continue
416
- }
417
- }
418
-
419
- if (isArray(value)) {
420
- target[key] = value
421
- } else if (isObject(value)) {
422
- if (!isObject(target[key])) {
423
- target[key] = {}
424
- }
425
- target[key] = deepMerge(target[key], value, merger)
426
- } else {
427
- target[key] = value
428
- }
429
- }
430
- return target
431
- }
@@ -11,3 +11,80 @@ export function toCamelCase(input) {
11
11
 
12
12
  return [firstChar.toLowerCase(), ...rest].join("")
13
13
  }
14
+
15
+ // How long a row or column is by one, which is the whole of what the offsets below mean.
16
+ // The grid's own edges are counted by the same one.
17
+ const ONE = 1
18
+ const NONE = 0
19
+ const FIRST = 0
20
+
21
+ /**
22
+ * How far one string is from another, in single-character edits.
23
+ *
24
+ * The grid is built in full rather than in the one row that would do, because this is
25
+ * short and the short version is where an off-by-one hides.
26
+ *
27
+ * @example
28
+ * ```js
29
+ * import { editDistance } from "@inglorious/utils/data-structures/string.js"
30
+ *
31
+ * editDistance("audio", "audi") // 1
32
+ * ```
33
+ *
34
+ * @param {string} a - The string that was asked about.
35
+ * @param {string} b - The string to compare it with.
36
+ * @returns {number} How many edits turn one into the other.
37
+ */
38
+ export function editDistance(a, b) {
39
+ const grid = Array.from({ length: a.length + ONE }, () =>
40
+ Array.from({ length: b.length + ONE }, () => NONE),
41
+ )
42
+
43
+ for (let i = FIRST; i <= a.length; i++) grid[i][FIRST] = i
44
+ for (let j = FIRST; j <= b.length; j++) grid[FIRST][j] = j
45
+
46
+ for (let i = ONE; i <= a.length; i++) {
47
+ for (let j = ONE; j <= b.length; j++) {
48
+ grid[i][j] = Math.min(
49
+ grid[i - ONE][j] + ONE,
50
+ grid[i][j - ONE] + ONE,
51
+ grid[i - ONE][j - ONE] + (a[i - ONE] === b[j - ONE] ? NONE : ONE),
52
+ )
53
+ }
54
+ }
55
+
56
+ return grid[a.length][b.length]
57
+ }
58
+
59
+ // How far a string may be from the one asked about and still be worth suggesting. A
60
+ // mistyped name is nearly always one or two edits away from the right one; anything
61
+ // further off is a different string rather than a wrong one.
62
+ const NEAR_ENOUGH = 3
63
+
64
+ /**
65
+ * The strings closest to one that was asked for, nearest first.
66
+ *
67
+ * Case is folded, because a name differing from a declared one only by a capital is the
68
+ * commonest kind of mistake and the one where saying nothing is least helpful.
69
+ *
70
+ * @example
71
+ * ```js
72
+ * import { namesNear } from "@inglorious/utils/data-structures/string.js"
73
+ *
74
+ * namesNear("audio", ["Game", "Audio"]) // ["Audio"]
75
+ * ```
76
+ *
77
+ * @param {string} wanted - The string that was asked for.
78
+ * @param {string[]} candidates - Every string that could have been meant.
79
+ * @returns {string[]} The ones worth suggesting, which may be none of them.
80
+ */
81
+ export function namesNear(wanted, candidates) {
82
+ const target = wanted.toLowerCase()
83
+ const allowed = Math.max(ONE, Math.floor(target.length / NEAR_ENOUGH))
84
+
85
+ return candidates
86
+ .map((name) => [name, editDistance(target, name.toLowerCase())])
87
+ .filter(([, d]) => d <= allowed)
88
+ .sort(([, a], [, b]) => a - b)
89
+ .map(([name]) => name)
90
+ }
@@ -1 +1 @@
1
- export * from "./functions.js"
1
+ export * from "./function.js"
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { intersectsCircle as pointIntersectsCircle } from "./point.js"
8
8
  import { hypothenuse } from "./triangle.js"
9
- import { clamp, subtract } from "./vectors.js"
9
+ import { clamp, subtract } from "./vector.js"
10
10
 
11
11
  const HALF = 2
12
12
  const SQUARED = 2
@@ -1,6 +1,6 @@
1
1
  // @see https://jonathanwhiting.com/tutorial/collision/
2
2
 
3
- import { isBetween } from "./numbers.js"
3
+ import { isBetween } from "./number.js"
4
4
  import { intersectsRectangle } from "./rectangle.js"
5
5
 
6
6
  const FIRST = 0
package/src/math/index.js CHANGED
@@ -2,16 +2,16 @@ import * as circle from "./circle.js"
2
2
  import * as hitmask from "./hitmask.js"
3
3
  import * as line from "./line.js"
4
4
  import * as linearInterpolation from "./linear-interpolation.js"
5
- import * as numbers from "./numbers.js"
5
+ import * as numbers from "./number.js"
6
6
  import * as point from "./point.js"
7
- import * as quaternions from "./quaternions.js"
7
+ import * as quaternions from "./quaternion.js"
8
8
  import * as rectangle from "./rectangle.js"
9
9
  import * as rng from "./rng.js"
10
10
  import * as segment from "./segment.js"
11
11
  import * as statistics from "./statistics.js"
12
12
  import * as triangle from "./triangle.js"
13
13
  import * as trigonometry from "./trigonometry.js"
14
- import * as vectors from "./vectors.js"
14
+ import * as vector from "./vector.js"
15
15
 
16
16
  export {
17
17
  circle,
@@ -27,5 +27,5 @@ export {
27
27
  statistics,
28
28
  triangle,
29
29
  trigonometry,
30
- vectors,
30
+ vector,
31
31
  }
package/src/math/line.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @typedef {import("../../types/math/circle").Circle} Circle
5
5
  */
6
6
 
7
- import { abs } from "./numbers.js"
7
+ import { abs } from "./number.js"
8
8
  import { hypothenuse } from "./triangle.js"
9
9
 
10
10
  /**
package/src/math/point.js CHANGED
@@ -1,4 +1,25 @@
1
1
  /**
2
+ * A point in the Inglorious coordinate system: `[x, y, z]`.
3
+ *
4
+ * The world is y-up: the origin sits on the floor, and a bigger `y` is always a
5
+ * higher place in the world. Gravity subtracts from it, a jump adds to it, and an
6
+ * object resting on the ground has `y = 0`.
7
+ *
8
+ * `z` is the depth axis, and it is y-up as well — it is not screen-down, so both
9
+ * vertical axes grow upwards. Renderers turn them into screen coordinates:
10
+ * `@inglorious/renderer-2d` projects a point to `canvasY = height - y - z`,
11
+ * which puts `(0, 0, 0)` at the bottom-left of the canvas and turns both axes
12
+ * upside down.
13
+ *
14
+ * @example Reading a position in a 2D game
15
+ * ```js
16
+ * v(0, 0, 0) // the floor
17
+ * v(0, 100, 0) // a hundred pixels up
18
+ *
19
+ * entity.velocity[1] -= GRAVITY * dt // falling loses height
20
+ * entity.position[1] = 0 // keeping a body on the ground
21
+ * ```
22
+ *
2
23
  * @typedef {import("../../types/math/point").Point} Point
3
24
  * @typedef {import("../../types/math/point").PointInput} PointInput
4
25
  * @typedef {import("../../types/math/line").Line} Line
@@ -10,8 +31,8 @@ const SQUARED = 2
10
31
  const HALF = 2
11
32
 
12
33
  import { distanceFromPoint } from "./line.js"
13
- import { isBetween } from "./numbers.js"
14
- import { isVector } from "./vectors.js"
34
+ import { isBetween } from "./number.js"
35
+ import { isVector } from "./vector.js"
15
36
 
16
37
  /**
17
38
  * Calculates the distance from a point to a line.
@@ -1,11 +1,11 @@
1
1
  /**
2
- * @typedef {import("../../types/math/vectors").Vector3} Vector3
3
- * @typedef {import("../../types/math/quaternions").Quaternion} Quaternion
2
+ * @typedef {import("../../types/math/vector").Vector3} Vector3
3
+ * @typedef {import("../../types/math/quaternion").Quaternion} Quaternion
4
4
  */
5
5
 
6
6
  import { v } from "../v.js"
7
7
  import { cos, sin } from "./trigonometry.js"
8
- import { cross, dot, multiply, sum } from "./vectors.js"
8
+ import { cross, dot, multiply, sum } from "./vector.js"
9
9
 
10
10
  const DEFAULT_ANGLE = 0
11
11
  const HALF = 2 // Constant representing the divisor for halving an angle.
@@ -1,4 +1,11 @@
1
1
  /**
2
+ * `height` and `depth` are independent axes, not halves of one screen
3
+ * dimension, so a body on the `xy` plane is sized `v(w, h, 0)` and one on the
4
+ * `xz` plane `v(w, 0, h)`. `@inglorious/renderer-2d` flattens both onto the
5
+ * screen and draws a rectangle `height + depth` tall, while
6
+ * {@link intersectsRectangle} tests the two axes separately.
7
+ *
8
+ * @typedef {import("../../types/math/rectangle").Size} Size
2
9
  * @typedef {import("../../types/math/circle").Circle} Circle
3
10
  * @typedef {import("../../types/math/point").PointInput} PointInput
4
11
  * @typedef {import("../../types/math/rectangle").Rectangle} Rectangle
@@ -11,6 +18,11 @@ const HALF = 2
11
18
 
12
19
  /**
13
20
  * Checks if a rectangle intersects with a point.
21
+ *
22
+ * The rectangle's `position` is its center, and `size` extends around it. An
23
+ * anchored shape is the exception: its `anchor` picks which point sits on the
24
+ * position, so the two only line up when the anchor matches, such as
25
+ * `[0.5, 0.5]` for a centred one.
14
26
  * @param {Rectangle} rectangle - The rectangle to check.
15
27
  * @param {PointInput} point - The point to check.
16
28
  * @returns {boolean} True if the point intersects the circle, false otherwise.
package/src/math/rng.js CHANGED
@@ -31,10 +31,13 @@ export function random(...args) {
31
31
  return Math.random()
32
32
  }
33
33
 
34
+ // A range with a fractional bound cannot be stepped through as integers, so it
35
+ // is read as a continuous range. Requiring both bounds to be floats used to
36
+ // send `random(0.5, 1)` down the integer path, where it could only ever answer
37
+ // 0 or 1.
34
38
  if (
35
39
  args.length > 1 &&
36
- !Number.isInteger(args[0]) &&
37
- !Number.isInteger(args[1])
40
+ (!Number.isInteger(args[0]) || !Number.isInteger(args[1]))
38
41
  ) {
39
42
  const [from, to] = args
40
43
  return Math.random() * (to - from) + from
@@ -11,7 +11,7 @@ import {
11
11
  setMagnitude,
12
12
  subtract,
13
13
  sum,
14
- } from "./vectors.js"
14
+ } from "./vector.js"
15
15
 
16
16
  const BEFORE_SEGMENT = 0
17
17
 
@@ -1,4 +1,4 @@
1
- import { mod } from "./numbers.js"
1
+ import { mod } from "./number.js"
2
2
 
3
3
  const HALF_CIRCLE_IN_DEGRESS = 180 // Half-circle in degrees (180°)
4
4
  const FULL_CIRCLE = 2 // Full circle multiplier for radians (2π)
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @typedef {import("../../types/math/vectors").Vector} Vector
3
- * @typedef {import("../../types/math/vectors").Vector2} Vector2
4
- * @typedef {import("../../types/math/vectors").Vector3} Vector3
5
- * @typedef {import("../../types/math/vectors").Vector7} Vector7
2
+ * @typedef {import("../../types/math/vector").Vector} Vector
3
+ * @typedef {import("../../types/math/vector").Vector2} Vector2
4
+ * @typedef {import("../../types/math/vector").Vector3} Vector3
5
+ * @typedef {import("../../types/math/vector").Vector7} Vector7
6
6
  */
7
7
 
8
8
  import { v } from "../v.js"
@@ -11,13 +11,32 @@ import {
11
11
  clamp as nClamp,
12
12
  mod as nMod,
13
13
  snap as nSnap,
14
- } from "./numbers.js"
15
- import { quaternion } from "./quaternions.js"
14
+ } from "./number.js"
15
+ import { quaternion } from "./quaternion.js"
16
+ import { random } from "./rng.js"
16
17
  import { hypothenuse } from "./triangle.js"
17
18
  import { atan2, cos, sin } from "./trigonometry.js"
18
19
 
19
- const ZERO_VECTOR = v(0, 0, 0) // eslint-disable-line no-magic-numbers
20
- const UNIT_VECTOR = v(1, 0, 0) // eslint-disable-line no-magic-numbers
20
+ /**
21
+ * The vector with every component at zero. Useful for naming the components you
22
+ * care about, and for building a vector up from scratch:
23
+ *
24
+ * const [, NO_RISE, NO_DEPTH] = ZERO_VECTOR
25
+ * const position = v(10, 20, NO_DEPTH)
26
+ *
27
+ * Vectors are mutable arrays, so copy it rather than storing it where something
28
+ * writes to one, such as an entity position. Copy it with `v(...ZERO_VECTOR)`,
29
+ * since a plain spread drops the tag `isVector` looks for.
30
+ */
31
+ export const ZERO_VECTOR = v(0, 0, 0) // eslint-disable-line no-magic-numbers
32
+
33
+ /**
34
+ * The vector one unit long along the x axis. Useful for facing a direction, as
35
+ * in `rotate(UNIT_VECTOR, angle)`.
36
+ *
37
+ * As with `ZERO_VECTOR`, copy it with `v(...UNIT_VECTOR)` before storing it.
38
+ */
39
+ export const UNIT_VECTOR = v(1, 0, 0) // eslint-disable-line no-magic-numbers
21
40
 
22
41
  const X = 0
23
42
  const Y = 1
@@ -25,6 +44,7 @@ const Z = 2
25
44
  const LAST_COORDINATE = 1
26
45
  const TWO_COORDINATES = 2
27
46
  const NO_Y = 0
47
+ const NO_COMPONENT = 0
28
48
  const DEFAULT_PRECISION = 1
29
49
  const DEFAULT_DECIMALS = 0
30
50
 
@@ -228,6 +248,16 @@ export function multiply(...operands) {
228
248
  return v(...operands.reduce(multiplyCoordinates))
229
249
  }
230
250
 
251
+ /**
252
+ * Flips the direction of every component, so that adding the result to a vector
253
+ * gives back the point mirrored through it.
254
+ * @param {Vector} vector - The input vector.
255
+ * @returns {Vector} The negated vector.
256
+ */
257
+ export function negate(vector) {
258
+ return v(...vector.map((coordinate) => -coordinate))
259
+ }
260
+
231
261
  /**
232
262
  * Normalizes the vector to have a magnitude of 1.
233
263
  * @param {Vector} vector - The input vector.
@@ -264,6 +294,35 @@ export function powerOf(scalar, vector) {
264
294
  return v(...vector.map((coordinate) => scalar ** coordinate))
265
295
  }
266
296
 
297
+ /**
298
+ * Draws a random vector, taking each component from its own range: the low end is
299
+ * where that axis starts, the high end where it can reach. This is `random`
300
+ * applied per axis, so it suits ranges that differ by axis, where a single pair of
301
+ * bounds would have to be square.
302
+ *
303
+ * A component whose bounds are the same always comes out equal to them, and either
304
+ * bound may be left out to fall back to zero on that axis.
305
+ *
306
+ * @param {Vector} [low] - Where each axis starts.
307
+ * @param {Vector} [high] - Where each axis can reach.
308
+ * @returns {Vector} The random vector.
309
+ * @example
310
+ * ```js
311
+ * // Debris thrown to the left or right, never up or down.
312
+ * randomVector(v(-15, 0, 0), v(15, 0, 0))
313
+ * ```
314
+ */
315
+ export function randomVector(low, high) {
316
+ const [lowX, lowY, lowZ] = low ?? []
317
+ const [highX, highY, highZ] = high ?? []
318
+
319
+ return v(
320
+ random(lowX ?? NO_COMPONENT, highX ?? NO_COMPONENT),
321
+ random(lowY ?? NO_COMPONENT, highY ?? NO_COMPONENT),
322
+ random(lowZ ?? NO_COMPONENT, highZ ?? NO_COMPONENT),
323
+ )
324
+ }
325
+
267
326
  /**
268
327
  * Alias for the mod function.
269
328
  * @type {typeof mod}
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @typedef {import("../../types/math/vectors").Vector3} Vector3
2
+ * @typedef {import("../../types/math/vector").Vector3} Vector3
3
3
  */
4
4
 
5
- import { clamp, multiply, sum, zero } from "../math/vectors.js"
5
+ import { clamp, multiply, sum, zero } from "../math/vector.js"
6
6
  import { applyFriction } from "./friction.js"
7
7
 
8
8
  const DEFAULT_DT = 1 // Default time delta for the applyAcceleration function.
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @typedef {import("../../types/math/vectors").Vector3} Vector3
2
+ * @typedef {import("../../types/math/vector").Vector3} Vector3
3
3
  */
4
4
 
5
- import { magnitude, setMagnitude, zero } from "../math/vectors.js"
5
+ import { magnitude, setMagnitude, zero } from "../math/vector.js"
6
6
 
7
7
  const DEFAULT_DT = 1 // Default time delta for the applyFriction function.
8
8
  const NO_FRICTION = 0 // No friction constant.
@@ -1,10 +1,10 @@
1
1
  /**
2
- * @typedef {import("../../types/math/vectors").Vector3} Vector3
2
+ * @typedef {import("../../types/math/vector").Vector3} Vector3
3
3
  */
4
4
 
5
5
  /* eslint-disable no-magic-numbers */
6
6
 
7
- import { zero } from "../math/vectors.js"
7
+ import { zero } from "../math/vector.js"
8
8
 
9
9
  const DEFAULT_DT = 1 // Default delta time for the applyGravity function.
10
10
  const DEFAULT_JUMP = 0 // Default maximum jump height.
@@ -1,8 +1,8 @@
1
1
  /**
2
- * @typedef {import("../../types/math/vectors").Vector3} Vector3
2
+ * @typedef {import("../../types/math/vector").Vector3} Vector3
3
3
  */
4
4
 
5
- import { clamp, multiply, sum, zero } from "../math/vectors.js"
5
+ import { clamp, multiply, sum, zero } from "../math/vector.js"
6
6
 
7
7
  const DEFAULT_DT = 1 // Default time delta for the applyVelocity function.
8
8
  const DEFAULT_MAX_SPEED = 0 // Default maximum speed for velocity clamping.
package/src/v.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @typedef {import("../types/math/vectors").Vector} Vector
2
+ * @typedef {import("../types/math/vector").Vector} Vector
3
3
  */
4
4
 
5
5
  import { isArray } from "./data-structures/array.js"
@@ -1,4 +1,4 @@
1
- import type { Vector2 } from "../math/vectors"
1
+ import type { Vector2 } from "../math/vector"
2
2
 
3
3
  /**
4
4
  * A map where keys are node identifiers and values are their [x, y] coordinates.
@@ -5,6 +5,11 @@ export type Comparator<T> = (left: T, right: T) => number
5
5
  */
6
6
  export function contains<T>(arr: readonly T[], item: T): boolean
7
7
 
8
+ /**
9
+ * The index `step` places along a list of `length`, wrapping round at either end.
10
+ */
11
+ export function cycleIndex(index: number, length: number, step: number): number
12
+
8
13
  /**
9
14
  * Ensures that a value is an array.
10
15
  */
@@ -2,7 +2,7 @@ export * as array from "./array"
2
2
  export * as board from "./board"
3
3
  export * as boolean from "./boolean"
4
4
  export * as heap from "./heap"
5
- export * as objects from "./objects"
5
+ export * as object from "./object"
6
6
  export * as set from "./set"
7
7
  export * as string from "./string"
8
8
  export * as tree from "./tree"
@@ -1,4 +1,4 @@
1
- import type { Vector } from "../math/vectors"
1
+ import type { Vector } from "../math/vector"
2
2
 
3
3
  export type ObjectValue<T> = T extends object ? T[keyof T] : never
4
4
 
@@ -2,13 +2,13 @@ export * as circle from "./circle"
2
2
  export * as hitmask from "./hitmask"
3
3
  export * as line from "./line"
4
4
  export * as linearInterpolation from "./linear-interpolation"
5
- export * as numbers from "./numbers"
5
+ export * as number from "./number"
6
6
  export * as point from "./point"
7
- export * as quaternions from "./quaternions"
7
+ export * as quaternion from "./quaternion"
8
8
  export * as rectangle from "./rectangle"
9
9
  export * as rng from "./rng"
10
10
  export * as segment from "./segment"
11
11
  export * as statistics from "./statistics"
12
12
  export * as triangle from "./triangle"
13
13
  export * as trigonometry from "./trigonometry"
14
- export * as vectors from "./vectors"
14
+ export * as vector from "./vector"
@@ -3,7 +3,32 @@ import type { Circle } from "./circle"
3
3
  import type { Line } from "./line"
4
4
  import type { Rectangle } from "./rectangle"
5
5
 
6
- /** Represents a point in 3D space. */
6
+ /**
7
+ * A point in the Inglorious coordinate system: `[x, y, z]`.
8
+ *
9
+ * The world is **y-up**: the origin sits on the floor, and a bigger `y` is
10
+ * always a higher place in the world. Gravity subtracts from it, a jump adds to
11
+ * it, and an object resting on the ground has `y = 0`.
12
+ *
13
+ * `z` is the depth axis, and it is **y-up as well** — it is not screen-down, so
14
+ * both vertical axes grow upwards. Renderers are what turn them into screen
15
+ * coordinates: `@inglorious/renderer-2d` projects a point to
16
+ * `canvasY = viewportHeight - y - z`, which puts `(0, 0, 0)` at the bottom-left
17
+ * corner of the canvas and turns both axes upside down.
18
+ *
19
+ * @example Reading a position in a 2D game
20
+ * ```js
21
+ * // Ground level, and a hundred pixels above it.
22
+ * v(0, 0, 0) // the floor
23
+ * v(0, 100, 0) // a hundred pixels up
24
+ *
25
+ * // Falling means losing height, in 2D as well as in 3D.
26
+ * entity.velocity[1] -= GRAVITY * dt
27
+ *
28
+ * // Keep a body on the ground.
29
+ * entity.position[1] = 0
30
+ * ```
31
+ */
7
32
  export type Point = readonly [x: number, y: number, z: number]
8
33
 
9
34
  /** Accepts a 3D point, branded vector, or object with a point position. */
@@ -1,4 +1,4 @@
1
- import type { Vector, Vector3 } from "./vectors"
1
+ import type { Vector, Vector3 } from "./vector"
2
2
 
3
3
  /** A quaternion represented as a four-dimensional vector. */
4
4
  export type Quaternion = Vector<readonly [number, number, number, number]>
@@ -1,10 +1,31 @@
1
1
  import type { Circle } from "./circle"
2
2
  import type { Point, PointInput } from "./point"
3
3
 
4
- /** Represents the dimensions of a 3D object. */
4
+ /**
5
+ * The dimensions of a 3D object, as `[width, height, depth]`.
6
+ *
7
+ * `width` is the extent along `x`, `height` the extent along `y` and `depth` the
8
+ * extent along `z`. `height` and `depth` are **independent axes**, not halves of
9
+ * one screen dimension, so in a 2D game that plays on the `xy` plane a size reads
10
+ * as `v(w, h, 0)` — and an entity whose body is placed on the `xz` plane instead
11
+ * reads as `v(w, 0, h)`.
12
+ *
13
+ * When a renderer has to flatten both vertical axes onto the screen, it adds
14
+ * them: `@inglorious/renderer-2d` draws a rectangle `height + depth` tall. A
15
+ * collision shape never does — `intersectsRectangle` tests the two axes
16
+ * separately, which is why a body cannot collide with something that is merely
17
+ * "near it" on the other axis.
18
+ */
5
19
  export type Size = readonly [width: number, height: number, depth: number]
6
20
 
7
- /** Represents an axis-aligned rectangle. */
21
+ /**
22
+ * Represents an axis-aligned rectangle.
23
+ *
24
+ * `position` is the point the box hangs off and `size` is its extent, so with
25
+ * the default centred anchor the position is the middle of the box. Set an
26
+ * `anchor` to describe the box by a different point, such as `[0, 0]` for its
27
+ * bottom-left corner, so that the box stands on its position.
28
+ */
8
29
  export interface Rectangle {
9
30
  /** The center of the rectangle. */
10
31
  position: Point
@@ -18,6 +18,22 @@ export type Vector7 = Vector<
18
18
  readonly [number, number, number, number, number, number, number]
19
19
  >
20
20
 
21
+ /**
22
+ * The vector with every component at zero.
23
+ *
24
+ * Vectors are mutable, so copy it with `v(...ZERO_VECTOR)` before storing it
25
+ * where something writes to one.
26
+ */
27
+ export const ZERO_VECTOR: Vector3
28
+
29
+ /**
30
+ * The vector one unit long along the x axis.
31
+ *
32
+ * Vectors are mutable, so copy it with `v(...UNIT_VECTOR)` before storing it
33
+ * where something writes to one.
34
+ */
35
+ export const UNIT_VECTOR: Vector3
36
+
21
37
  /** Returns the absolute value of each vector component. */
22
38
  export function abs<T extends Vector>(vector: T): T
23
39
 
@@ -107,6 +123,11 @@ export function multiply<T extends Vector2 | Vector3>(
107
123
  ): T
108
124
 
109
125
  /** Normalizes a vector to unit magnitude. */
126
+ /**
127
+ * Flips the direction of every component.
128
+ */
129
+ export function negate<T extends Vector>(vector: T): T
130
+
110
131
  export function normalize<T extends Vector>(vector: T): T
111
132
 
112
133
  /** Alias for `power`. */
@@ -124,6 +145,14 @@ export function power<T extends Vector2 | Vector3>(
124
145
  /** Raises a scalar to each vector component. */
125
146
  export function powerOf<T extends Vector>(scalar: number, vector: T): T
126
147
 
148
+ /**
149
+ * Draws a random vector, taking each component from its own range.
150
+ */
151
+ export function randomVector(
152
+ low?: readonly number[],
153
+ high?: readonly number[],
154
+ ): Vector3
155
+
127
156
  /** Alias for `mod`. */
128
157
  export const remainder: typeof mod
129
158
 
@@ -1,4 +1,4 @@
1
- import type { Vector3 } from "../math/vectors"
1
+ import type { Vector3 } from "../math/vector"
2
2
 
3
3
  export interface AccelerationParams {
4
4
  acceleration?: Vector3
@@ -1,4 +1,4 @@
1
- import type { Vector3 } from "../math/vectors"
1
+ import type { Vector3 } from "../math/vector"
2
2
 
3
3
  export interface FrictionParams {
4
4
  friction: number
@@ -1,4 +1,4 @@
1
- import type { Vector3 } from "../math/vectors"
1
+ import type { Vector3 } from "../math/vector"
2
2
 
3
3
  export interface GravityParams {
4
4
  maxJump?: number
@@ -1,4 +1,4 @@
1
- import type { Vector3 } from "../math/vectors"
1
+ import type { Vector3 } from "../math/vector"
2
2
 
3
3
  export interface VelocityParams {
4
4
  maxSpeed?: number
package/types/v.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Vector } from "./math/vectors"
1
+ import type { Vector } from "./math/vector"
2
2
 
3
3
  /**
4
4
  * Ensures that a given value is a vector.
File without changes
File without changes
File without changes