@flighthq/math 0.1.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 (80) hide show
  1. package/dist/angle.d.ts +13 -0
  2. package/dist/angle.d.ts.map +1 -0
  3. package/dist/angle.js +24 -0
  4. package/dist/angle.js.map +1 -0
  5. package/dist/clamp.d.ts +15 -0
  6. package/dist/clamp.d.ts.map +1 -0
  7. package/dist/clamp.js +23 -0
  8. package/dist/clamp.js.map +1 -0
  9. package/dist/comparison.d.ts +17 -0
  10. package/dist/comparison.d.ts.map +1 -0
  11. package/dist/comparison.js +26 -0
  12. package/dist/comparison.js.map +1 -0
  13. package/dist/constants.d.ts +11 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +11 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/hash.d.ts +35 -0
  18. package/dist/hash.d.ts.map +1 -0
  19. package/dist/hash.js +51 -0
  20. package/dist/hash.js.map +1 -0
  21. package/dist/index.d.ts +16 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +16 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/interpolation.d.ts +27 -0
  26. package/dist/interpolation.d.ts.map +1 -0
  27. package/dist/interpolation.js +43 -0
  28. package/dist/interpolation.js.map +1 -0
  29. package/dist/interpolationAdvanced.d.ts +42 -0
  30. package/dist/interpolationAdvanced.d.ts.map +1 -0
  31. package/dist/interpolationAdvanced.js +71 -0
  32. package/dist/interpolationAdvanced.js.map +1 -0
  33. package/dist/nextPowerOfTwo.d.ts +22 -0
  34. package/dist/nextPowerOfTwo.d.ts.map +1 -0
  35. package/dist/nextPowerOfTwo.js +52 -0
  36. package/dist/nextPowerOfTwo.js.map +1 -0
  37. package/dist/numberTheory.d.ts +33 -0
  38. package/dist/numberTheory.d.ts.map +1 -0
  39. package/dist/numberTheory.js +62 -0
  40. package/dist/numberTheory.js.map +1 -0
  41. package/dist/random.d.ts +20 -0
  42. package/dist/random.d.ts.map +1 -0
  43. package/dist/random.js +27 -0
  44. package/dist/random.js.map +1 -0
  45. package/dist/randomDistributions.d.ts +105 -0
  46. package/dist/randomDistributions.d.ts.map +1 -0
  47. package/dist/randomDistributions.js +209 -0
  48. package/dist/randomDistributions.js.map +1 -0
  49. package/dist/randomRange.d.ts +18 -0
  50. package/dist/randomRange.d.ts.map +1 -0
  51. package/dist/randomRange.js +29 -0
  52. package/dist/randomRange.js.map +1 -0
  53. package/dist/rounding.d.ts +35 -0
  54. package/dist/rounding.d.ts.map +1 -0
  55. package/dist/rounding.js +53 -0
  56. package/dist/rounding.js.map +1 -0
  57. package/dist/scalar.d.ts +30 -0
  58. package/dist/scalar.d.ts.map +1 -0
  59. package/dist/scalar.js +42 -0
  60. package/dist/scalar.js.map +1 -0
  61. package/dist/statistics.d.ts +29 -0
  62. package/dist/statistics.d.ts.map +1 -0
  63. package/dist/statistics.js +67 -0
  64. package/dist/statistics.js.map +1 -0
  65. package/package.json +37 -0
  66. package/src/angle.test.ts +83 -0
  67. package/src/clamp.test.ts +61 -0
  68. package/src/comparison.test.ts +54 -0
  69. package/src/constants.test.ts +38 -0
  70. package/src/hash.test.ts +84 -0
  71. package/src/interpolation.test.ts +93 -0
  72. package/src/interpolationAdvanced.test.ts +122 -0
  73. package/src/nextPowerOfTwo.test.ts +84 -0
  74. package/src/numberTheory.test.ts +95 -0
  75. package/src/random.test.ts +32 -0
  76. package/src/randomDistributions.test.ts +348 -0
  77. package/src/randomRange.test.ts +99 -0
  78. package/src/rounding.test.ts +81 -0
  79. package/src/scalar.test.ts +58 -0
  80. package/src/statistics.test.ts +74 -0
@@ -0,0 +1,30 @@
1
+ /** Round `n` up to the next power of two.
2
+ *
3
+ * Alias for `nextPowerOfTwo` — provided for clarity at texture-sizing call
4
+ * sites where the direction is important.
5
+ */
6
+ export declare function ceilPowerOfTwo(n: number): number;
7
+ /** Round `n` down to the previous power of two.
8
+ *
9
+ * Alias for `previousPowerOfTwo` — provided for clarity at texture-sizing
10
+ * call sites where the direction is important.
11
+ */
12
+ export declare function floorPowerOfTwo(n: number): number;
13
+ /** Quantize `value` into `steps` equal buckets over `[min, max]`.
14
+ *
15
+ * Returns a value snapped to one of `steps + 1` evenly-spaced values between
16
+ * `min` and `max`. When `steps <= 0` the result is `min`. When `min === max`
17
+ * the result is `min`.
18
+ *
19
+ * ```ts
20
+ * quantize(0.7, 4, 0, 1) // 0.75 (nearest of 0, 0.25, 0.5, 0.75, 1.0)
21
+ * ```
22
+ */
23
+ export declare function quantize(value: number, steps: number, min: number, max: number): number;
24
+ /** Zero-aware sign: returns `1` for positive, `-1` for negative, `0` for zero.
25
+ *
26
+ * Unlike `Math.sign`, this is explicit about the zero-aware contract, and the
27
+ * name makes intent clear at call sites in animation and physics code.
28
+ */
29
+ export declare function sign(value: number): number;
30
+ //# sourceMappingURL=scalar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scalar.d.ts","sourceRoot":"","sources":["../src/scalar.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;;;;GASG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAIvF;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE1C"}
package/dist/scalar.js ADDED
@@ -0,0 +1,42 @@
1
+ import { nextPowerOfTwo, previousPowerOfTwo } from './nextPowerOfTwo';
2
+ /** Round `n` up to the next power of two.
3
+ *
4
+ * Alias for `nextPowerOfTwo` — provided for clarity at texture-sizing call
5
+ * sites where the direction is important.
6
+ */
7
+ export function ceilPowerOfTwo(n) {
8
+ return nextPowerOfTwo(n);
9
+ }
10
+ /** Round `n` down to the previous power of two.
11
+ *
12
+ * Alias for `previousPowerOfTwo` — provided for clarity at texture-sizing
13
+ * call sites where the direction is important.
14
+ */
15
+ export function floorPowerOfTwo(n) {
16
+ return previousPowerOfTwo(n);
17
+ }
18
+ /** Quantize `value` into `steps` equal buckets over `[min, max]`.
19
+ *
20
+ * Returns a value snapped to one of `steps + 1` evenly-spaced values between
21
+ * `min` and `max`. When `steps <= 0` the result is `min`. When `min === max`
22
+ * the result is `min`.
23
+ *
24
+ * ```ts
25
+ * quantize(0.7, 4, 0, 1) // 0.75 (nearest of 0, 0.25, 0.5, 0.75, 1.0)
26
+ * ```
27
+ */
28
+ export function quantize(value, steps, min, max) {
29
+ if (steps <= 0 || min === max)
30
+ return min;
31
+ const t = (value - min) / (max - min);
32
+ return min + (Math.round(Math.max(0, Math.min(1, t)) * steps) / steps) * (max - min);
33
+ }
34
+ /** Zero-aware sign: returns `1` for positive, `-1` for negative, `0` for zero.
35
+ *
36
+ * Unlike `Math.sign`, this is explicit about the zero-aware contract, and the
37
+ * name makes intent clear at call sites in animation and physics code.
38
+ */
39
+ export function sign(value) {
40
+ return value > 0 ? 1 : value < 0 ? -1 : 0;
41
+ }
42
+ //# sourceMappingURL=scalar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scalar.js","sourceRoot":"","sources":["../src/scalar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,CAAS;IACtC,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC;AAC3B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,CAAS;IACvC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,QAAQ,CAAC,KAAa,EAAE,KAAa,EAAE,GAAW,EAAE,GAAW;IAC7E,IAAI,KAAK,IAAI,CAAC,IAAI,GAAG,KAAK,GAAG;QAAE,OAAO,GAAG,CAAC;IAC1C,MAAM,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC;IACtC,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC;AACvF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,IAAI,CAAC,KAAa;IAChC,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,29 @@
1
+ /** Return the arithmetic mean of `values`.
2
+ *
3
+ * Returns `NaN` for an empty array. No allocation beyond the accumulator.
4
+ */
5
+ export declare function mean(values: Readonly<number[]>): number;
6
+ /** Return the median of `values`.
7
+ *
8
+ * Allocates a sorted copy — does not mutate the input. Returns `NaN` for an
9
+ * empty array.
10
+ */
11
+ export declare function median(values: Readonly<number[]>): number;
12
+ /** Return the population standard deviation of `values`.
13
+ *
14
+ * Returns `NaN` for an empty array.
15
+ */
16
+ export declare function standardDeviation(values: Readonly<number[]>): number;
17
+ /** Return the population variance of `values`.
18
+ *
19
+ * Returns `NaN` for an empty array, `0` for a single element.
20
+ */
21
+ export declare function variance(values: Readonly<number[]>): number;
22
+ /** Return the weighted average of `values` using `weights`.
23
+ *
24
+ * Each `values[i]` is weighted by `weights[i]`. The arrays must have the same
25
+ * length. Returns `NaN` for an empty array or if total weight is `0`. Throws
26
+ * if `values.length !== weights.length`.
27
+ */
28
+ export declare function weightedAverage(values: Readonly<number[]>, weights: Readonly<number[]>): number;
29
+ //# sourceMappingURL=statistics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statistics.d.ts","sourceRoot":"","sources":["../src/statistics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,CAKvD;AAED;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,CAKzD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,CAEpE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,CAS3D;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,MAAM,CAY/F"}
@@ -0,0 +1,67 @@
1
+ /** Return the arithmetic mean of `values`.
2
+ *
3
+ * Returns `NaN` for an empty array. No allocation beyond the accumulator.
4
+ */
5
+ export function mean(values) {
6
+ if (values.length === 0)
7
+ return NaN;
8
+ let sum = 0;
9
+ for (let i = 0; i < values.length; i++)
10
+ sum += values[i];
11
+ return sum / values.length;
12
+ }
13
+ /** Return the median of `values`.
14
+ *
15
+ * Allocates a sorted copy — does not mutate the input. Returns `NaN` for an
16
+ * empty array.
17
+ */
18
+ export function median(values) {
19
+ if (values.length === 0)
20
+ return NaN;
21
+ const sorted = values.slice().sort((a, b) => a - b);
22
+ const mid = Math.floor(sorted.length / 2);
23
+ return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
24
+ }
25
+ /** Return the population standard deviation of `values`.
26
+ *
27
+ * Returns `NaN` for an empty array.
28
+ */
29
+ export function standardDeviation(values) {
30
+ return Math.sqrt(variance(values));
31
+ }
32
+ /** Return the population variance of `values`.
33
+ *
34
+ * Returns `NaN` for an empty array, `0` for a single element.
35
+ */
36
+ export function variance(values) {
37
+ if (values.length === 0)
38
+ return NaN;
39
+ const m = mean(values);
40
+ let sum = 0;
41
+ for (let i = 0; i < values.length; i++) {
42
+ const d = values[i] - m;
43
+ sum += d * d;
44
+ }
45
+ return sum / values.length;
46
+ }
47
+ /** Return the weighted average of `values` using `weights`.
48
+ *
49
+ * Each `values[i]` is weighted by `weights[i]`. The arrays must have the same
50
+ * length. Returns `NaN` for an empty array or if total weight is `0`. Throws
51
+ * if `values.length !== weights.length`.
52
+ */
53
+ export function weightedAverage(values, weights) {
54
+ if (values.length !== weights.length) {
55
+ throw new RangeError('weightedAverage: values and weights must have the same length');
56
+ }
57
+ if (values.length === 0)
58
+ return NaN;
59
+ let sumWeights = 0;
60
+ let sumProduct = 0;
61
+ for (let i = 0; i < values.length; i++) {
62
+ sumWeights += weights[i];
63
+ sumProduct += values[i] * weights[i];
64
+ }
65
+ return sumWeights === 0 ? NaN : sumProduct / sumWeights;
66
+ }
67
+ //# sourceMappingURL=statistics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statistics.js","sourceRoot":"","sources":["../src/statistics.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,IAAI,CAAC,MAA0B;IAC7C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACpC,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,GAAG,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;IACzD,OAAO,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC;AAC7B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,MAAM,CAAC,MAA0B;IAC/C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACpC,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC1C,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AACrF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAA0B;IAC1D,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,MAA0B;IACjD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACpC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACxB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;IACD,OAAO,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAA0B,EAAE,OAA2B;IACrF,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC;QACrC,MAAM,IAAI,UAAU,CAAC,+DAA+D,CAAC,CAAC;IACxF,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACpC,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,UAAU,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;QACzB,UAAU,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,GAAG,UAAU,CAAC;AAC1D,CAAC"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@flighthq/math",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/types": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.3.0"
34
+ },
35
+ "description": "Scalar numeric utilities — interpolation, clamping, angles, rounding, randomness, hashing, statistics, and number theory",
36
+ "sideEffects": false
37
+ }
@@ -0,0 +1,83 @@
1
+ import { degToRad, deltaAngle, normalizeAngle, radToDeg } from './angle';
2
+
3
+ describe('degToRad', () => {
4
+ it('converts 0 degrees to 0 radians', () => {
5
+ expect(degToRad(0)).toBe(0);
6
+ });
7
+ it('converts 180 degrees to π', () => {
8
+ expect(degToRad(180)).toBeCloseTo(Math.PI, 10);
9
+ });
10
+ it('converts 360 degrees to 2π', () => {
11
+ expect(degToRad(360)).toBeCloseTo(Math.PI * 2, 10);
12
+ });
13
+ it('converts 90 degrees to π/2', () => {
14
+ expect(degToRad(90)).toBeCloseTo(Math.PI / 2, 10);
15
+ });
16
+ it('converts negative degrees', () => {
17
+ expect(degToRad(-90)).toBeCloseTo(-Math.PI / 2, 10);
18
+ });
19
+ });
20
+
21
+ describe('deltaAngle', () => {
22
+ it('returns 0 for identical angles', () => {
23
+ expect(deltaAngle(1, 1)).toBeCloseTo(0, 10);
24
+ });
25
+ it('returns the shortest arc when crossing 0', () => {
26
+ // from = π - 0.1, to = -(π - 0.1). The short arc going forward is 0.2 rad.
27
+ expect(deltaAngle(Math.PI - 0.1, -(Math.PI - 0.1))).toBeCloseTo(0.2, 5);
28
+ });
29
+ it('returns a positive delta when the target is ahead', () => {
30
+ expect(deltaAngle(0, Math.PI / 2)).toBeCloseTo(Math.PI / 2, 10);
31
+ });
32
+ it('returns a negative delta when the target is behind', () => {
33
+ expect(deltaAngle(Math.PI / 2, 0)).toBeCloseTo(-Math.PI / 2, 10);
34
+ });
35
+ it('result is always within (-π, π]', () => {
36
+ for (let i = -10; i <= 10; i++) {
37
+ const d = deltaAngle(i, i + 3);
38
+ expect(d).toBeGreaterThan(-Math.PI);
39
+ expect(d).toBeLessThanOrEqual(Math.PI);
40
+ }
41
+ });
42
+ });
43
+
44
+ describe('normalizeAngle', () => {
45
+ it('normalizes 0 to 0', () => {
46
+ expect(normalizeAngle(0)).toBeCloseTo(0, 10);
47
+ });
48
+ it('normalizes π to -π (range is [-π, π))', () => {
49
+ // The range is [-π, π): π is outside the range, maps to -π.
50
+ expect(normalizeAngle(Math.PI)).toBeCloseTo(-Math.PI, 5);
51
+ });
52
+ it('wraps 2π to 0', () => {
53
+ expect(normalizeAngle(Math.PI * 2)).toBeCloseTo(0, 10);
54
+ });
55
+ it('wraps values above π to the negative side', () => {
56
+ expect(normalizeAngle(Math.PI + 1)).toBeCloseTo(-(Math.PI - 1), 10);
57
+ });
58
+ it('wraps negative values into [-π, π)', () => {
59
+ expect(normalizeAngle(-Math.PI * 3)).toBeCloseTo(-Math.PI, 5);
60
+ });
61
+ it('result is always in [-π, π)', () => {
62
+ for (let i = -20; i <= 20; i++) {
63
+ const a = normalizeAngle(i);
64
+ expect(a).toBeGreaterThanOrEqual(-Math.PI);
65
+ expect(a).toBeLessThan(Math.PI);
66
+ }
67
+ });
68
+ });
69
+
70
+ describe('radToDeg', () => {
71
+ it('converts 0 radians to 0 degrees', () => {
72
+ expect(radToDeg(0)).toBe(0);
73
+ });
74
+ it('converts π to 180 degrees', () => {
75
+ expect(radToDeg(Math.PI)).toBeCloseTo(180, 10);
76
+ });
77
+ it('converts 2π to 360 degrees', () => {
78
+ expect(radToDeg(Math.PI * 2)).toBeCloseTo(360, 10);
79
+ });
80
+ it('is the inverse of degToRad', () => {
81
+ expect(radToDeg(degToRad(45))).toBeCloseTo(45, 10);
82
+ });
83
+ });
@@ -0,0 +1,61 @@
1
+ import { clamp, inRange, saturate } from './clamp';
2
+
3
+ describe('clamp', () => {
4
+ it('clamps below minimum', () => {
5
+ expect(clamp(-5, 0, 10)).toBe(0);
6
+ });
7
+ it('clamps above maximum', () => {
8
+ expect(clamp(15, 0, 10)).toBe(10);
9
+ });
10
+ it('leaves value within range unchanged', () => {
11
+ expect(clamp(5, 0, 10)).toBe(5);
12
+ });
13
+ it('leaves value equal to min unchanged', () => {
14
+ expect(clamp(0, 0, 10)).toBe(0);
15
+ });
16
+ it('leaves value equal to max unchanged', () => {
17
+ expect(clamp(10, 0, 10)).toBe(10);
18
+ });
19
+ it('propagates NaN', () => {
20
+ expect(clamp(NaN, 0, 10)).toBeNaN();
21
+ });
22
+ });
23
+
24
+ describe('inRange', () => {
25
+ it('returns true for a value within range', () => {
26
+ expect(inRange(5, 0, 10)).toBe(true);
27
+ });
28
+ it('returns true at the minimum boundary', () => {
29
+ expect(inRange(0, 0, 10)).toBe(true);
30
+ });
31
+ it('returns true at the maximum boundary', () => {
32
+ expect(inRange(10, 0, 10)).toBe(true);
33
+ });
34
+ it('returns false below minimum', () => {
35
+ expect(inRange(-1, 0, 10)).toBe(false);
36
+ });
37
+ it('returns false above maximum', () => {
38
+ expect(inRange(11, 0, 10)).toBe(false);
39
+ });
40
+ });
41
+
42
+ describe('saturate', () => {
43
+ it('clamps below 0 to 0', () => {
44
+ expect(saturate(-1)).toBe(0);
45
+ });
46
+ it('clamps above 1 to 1', () => {
47
+ expect(saturate(2)).toBe(1);
48
+ });
49
+ it('leaves value within [0, 1] unchanged', () => {
50
+ expect(saturate(0.5)).toBe(0.5);
51
+ });
52
+ it('leaves 0 unchanged', () => {
53
+ expect(saturate(0)).toBe(0);
54
+ });
55
+ it('leaves 1 unchanged', () => {
56
+ expect(saturate(1)).toBe(1);
57
+ });
58
+ it('returns 0 for NaN (GPU semantics)', () => {
59
+ expect(saturate(NaN)).toBe(0);
60
+ });
61
+ });
@@ -0,0 +1,54 @@
1
+ import { approxEqual, approxEqualRelative, approxZero } from './comparison';
2
+
3
+ describe('approxEqual', () => {
4
+ it('returns true for identical values', () => {
5
+ expect(approxEqual(1, 1)).toBe(true);
6
+ });
7
+ it('returns true when values are within the default epsilon', () => {
8
+ expect(approxEqual(1, 1 + 1e-7)).toBe(true);
9
+ });
10
+ it('returns false when values differ by more than the default epsilon', () => {
11
+ expect(approxEqual(1, 1 + 1e-5)).toBe(false);
12
+ });
13
+ it('accepts a custom epsilon', () => {
14
+ expect(approxEqual(1, 1.1, 0.2)).toBe(true);
15
+ expect(approxEqual(1, 1.3, 0.2)).toBe(false);
16
+ });
17
+ it('returns true for zero and near-zero', () => {
18
+ expect(approxEqual(0, 1e-7)).toBe(true);
19
+ });
20
+ it('works with negative values', () => {
21
+ expect(approxEqual(-1, -1 - 1e-7)).toBe(true);
22
+ });
23
+ });
24
+
25
+ describe('approxEqualRelative', () => {
26
+ it('returns true for identical values', () => {
27
+ expect(approxEqualRelative(1000, 1000)).toBe(true);
28
+ });
29
+ it('returns true for large values within relative epsilon', () => {
30
+ expect(approxEqualRelative(1e8, 1e8 + 1)).toBe(true);
31
+ });
32
+ it('returns false when large values differ beyond relative epsilon', () => {
33
+ expect(approxEqualRelative(1e8, 1e8 * 2)).toBe(false);
34
+ });
35
+ it('returns true for near-zero values within absolute epsilon', () => {
36
+ expect(approxEqualRelative(0, 1e-7)).toBe(true);
37
+ });
38
+ });
39
+
40
+ describe('approxZero', () => {
41
+ it('returns true for exact zero', () => {
42
+ expect(approxZero(0)).toBe(true);
43
+ });
44
+ it('returns true for values within epsilon', () => {
45
+ expect(approxZero(1e-7)).toBe(true);
46
+ });
47
+ it('returns false for values outside epsilon', () => {
48
+ expect(approxZero(1e-4)).toBe(false);
49
+ });
50
+ it('accepts a custom epsilon', () => {
51
+ expect(approxZero(0.01, 0.1)).toBe(true);
52
+ expect(approxZero(0.2, 0.1)).toBe(false);
53
+ });
54
+ });
@@ -0,0 +1,38 @@
1
+ import { DEG_TO_RAD, EPSILON, HALF_PI, RAD_TO_DEG, TAU } from './constants';
2
+
3
+ describe('constants', () => {
4
+ describe('DEG_TO_RAD', () => {
5
+ it('converts 180 degrees to π', () => {
6
+ expect(180 * DEG_TO_RAD).toBeCloseTo(Math.PI, 10);
7
+ });
8
+ it('converts 360 degrees to 2π', () => {
9
+ expect(360 * DEG_TO_RAD).toBeCloseTo(Math.PI * 2, 10);
10
+ });
11
+ });
12
+ describe('EPSILON', () => {
13
+ it('is a positive number', () => {
14
+ expect(EPSILON).toBeGreaterThan(0);
15
+ });
16
+ it('is smaller than 1e-5', () => {
17
+ expect(EPSILON).toBeLessThan(1e-5);
18
+ });
19
+ });
20
+ describe('HALF_PI', () => {
21
+ it('equals π / 2', () => {
22
+ expect(HALF_PI).toBeCloseTo(Math.PI / 2, 10);
23
+ });
24
+ });
25
+ describe('RAD_TO_DEG', () => {
26
+ it('converts π radians to 180 degrees', () => {
27
+ expect(Math.PI * RAD_TO_DEG).toBeCloseTo(180, 10);
28
+ });
29
+ it('is the reciprocal of DEG_TO_RAD', () => {
30
+ expect(RAD_TO_DEG * DEG_TO_RAD).toBeCloseTo(1, 10);
31
+ });
32
+ });
33
+ describe('TAU', () => {
34
+ it('equals 2π', () => {
35
+ expect(TAU).toBeCloseTo(Math.PI * 2, 10);
36
+ });
37
+ });
38
+ });
@@ -0,0 +1,84 @@
1
+ import { createRandomSourceFromHash, hash2D, hash3D, hashCombine, hashUint32 } from './hash';
2
+
3
+ describe('createRandomSourceFromHash', () => {
4
+ it('returns a random source that produces values in [0, 1)', () => {
5
+ const rng = createRandomSourceFromHash(3, 7);
6
+ for (let i = 0; i < 20; i++) {
7
+ const v = rng();
8
+ expect(v).toBeGreaterThanOrEqual(0);
9
+ expect(v).toBeLessThan(1);
10
+ }
11
+ });
12
+ it('produces different sequences for different (x, y) positions', () => {
13
+ const a = createRandomSourceFromHash(0, 0);
14
+ const b = createRandomSourceFromHash(1, 0);
15
+ let differs = false;
16
+ for (let i = 0; i < 10; i++) if (a() !== b()) differs = true;
17
+ expect(differs).toBe(true);
18
+ });
19
+ it('produces the same sequence for the same (x, y)', () => {
20
+ const a = createRandomSourceFromHash(5, 9);
21
+ const b = createRandomSourceFromHash(5, 9);
22
+ for (let i = 0; i < 20; i++) expect(a()).toBe(b());
23
+ });
24
+ });
25
+
26
+ describe('hash2D', () => {
27
+ it('returns a non-negative integer', () => {
28
+ const h = hash2D(3, 7);
29
+ expect(h).toBeGreaterThanOrEqual(0);
30
+ expect(Number.isInteger(h)).toBe(true);
31
+ });
32
+ it('is deterministic', () => {
33
+ expect(hash2D(5, 10)).toBe(hash2D(5, 10));
34
+ });
35
+ it('differs for different inputs', () => {
36
+ expect(hash2D(0, 0)).not.toBe(hash2D(1, 0));
37
+ expect(hash2D(0, 0)).not.toBe(hash2D(0, 1));
38
+ });
39
+ });
40
+
41
+ describe('hash3D', () => {
42
+ it('returns a non-negative integer', () => {
43
+ const h = hash3D(1, 2, 3);
44
+ expect(h).toBeGreaterThanOrEqual(0);
45
+ expect(Number.isInteger(h)).toBe(true);
46
+ });
47
+ it('is deterministic', () => {
48
+ expect(hash3D(1, 2, 3)).toBe(hash3D(1, 2, 3));
49
+ });
50
+ it('differs for different inputs', () => {
51
+ expect(hash3D(0, 0, 0)).not.toBe(hash3D(1, 0, 0));
52
+ expect(hash3D(0, 0, 0)).not.toBe(hash3D(0, 0, 1));
53
+ });
54
+ });
55
+
56
+ describe('hashCombine', () => {
57
+ it('returns a number', () => {
58
+ expect(typeof hashCombine(1, 2)).toBe('number');
59
+ });
60
+ it('is deterministic', () => {
61
+ expect(hashCombine(100, 200)).toBe(hashCombine(100, 200));
62
+ });
63
+ it('produces different results for different seeds', () => {
64
+ expect(hashCombine(1, 100)).not.toBe(hashCombine(2, 100));
65
+ });
66
+ });
67
+
68
+ describe('hashUint32', () => {
69
+ it('returns a non-negative integer', () => {
70
+ const h = hashUint32(42);
71
+ expect(h).toBeGreaterThanOrEqual(0);
72
+ expect(Number.isInteger(h)).toBe(true);
73
+ });
74
+ it('is deterministic', () => {
75
+ expect(hashUint32(12345)).toBe(hashUint32(12345));
76
+ });
77
+ it('produces different results for different inputs', () => {
78
+ expect(hashUint32(0)).not.toBe(hashUint32(1));
79
+ expect(hashUint32(100)).not.toBe(hashUint32(101));
80
+ });
81
+ it('handles 0', () => {
82
+ expect(hashUint32(0)).toBeGreaterThanOrEqual(0);
83
+ });
84
+ });
@@ -0,0 +1,93 @@
1
+ import { inverseLerp, lerp, remap, smoothStep, step } from './interpolation';
2
+
3
+ describe('inverseLerp', () => {
4
+ it('returns 0 when value equals a', () => {
5
+ expect(inverseLerp(0, 10, 0)).toBe(0);
6
+ });
7
+ it('returns 1 when value equals b', () => {
8
+ expect(inverseLerp(0, 10, 10)).toBe(1);
9
+ });
10
+ it('returns 0.5 for the midpoint', () => {
11
+ expect(inverseLerp(0, 10, 5)).toBe(0.5);
12
+ });
13
+ it('returns 0 when a equals b to avoid division by zero', () => {
14
+ expect(inverseLerp(5, 5, 5)).toBe(0);
15
+ });
16
+ it('works with negative ranges', () => {
17
+ expect(inverseLerp(-10, 10, 0)).toBe(0.5);
18
+ });
19
+ });
20
+
21
+ describe('lerp', () => {
22
+ it('returns a when t is 0', () => {
23
+ expect(lerp(0, 10, 0)).toBe(0);
24
+ });
25
+ it('returns b when t is 1', () => {
26
+ expect(lerp(0, 10, 1)).toBe(10);
27
+ });
28
+ it('returns the midpoint for t = 0.5', () => {
29
+ expect(lerp(0, 10, 0.5)).toBe(5);
30
+ });
31
+ it('extrapolates when t is outside [0, 1]', () => {
32
+ expect(lerp(0, 10, 2)).toBe(20);
33
+ expect(lerp(0, 10, -1)).toBe(-10);
34
+ });
35
+ it('works with negative values', () => {
36
+ expect(lerp(-10, 10, 0.5)).toBe(0);
37
+ });
38
+ });
39
+
40
+ describe('remap', () => {
41
+ it('maps a value from one range to another', () => {
42
+ expect(remap(5, 0, 10, 0, 100)).toBe(50);
43
+ });
44
+ it('maps the minimum input to the minimum output', () => {
45
+ expect(remap(0, 0, 10, 50, 150)).toBe(50);
46
+ });
47
+ it('maps the maximum input to the maximum output', () => {
48
+ expect(remap(10, 0, 10, 50, 150)).toBe(150);
49
+ });
50
+ it('returns outMin when inMin equals inMax', () => {
51
+ expect(remap(5, 5, 5, 0, 100)).toBe(0);
52
+ });
53
+ it('works with inverted output ranges', () => {
54
+ expect(remap(5, 0, 10, 100, 0)).toBe(50);
55
+ });
56
+ });
57
+
58
+ describe('smoothStep', () => {
59
+ it('returns 0 at the lower edge', () => {
60
+ expect(smoothStep(0, 1, 0)).toBe(0);
61
+ });
62
+ it('returns 1 at the upper edge', () => {
63
+ expect(smoothStep(0, 1, 1)).toBe(1);
64
+ });
65
+ it('returns 0.5 at the midpoint', () => {
66
+ expect(smoothStep(0, 1, 0.5)).toBe(0.5);
67
+ });
68
+ it('returns 0 for x below the lower edge', () => {
69
+ expect(smoothStep(0, 1, -1)).toBe(0);
70
+ });
71
+ it('returns 1 for x above the upper edge', () => {
72
+ expect(smoothStep(0, 1, 2)).toBe(1);
73
+ });
74
+ it('has zero derivative at the edges', () => {
75
+ const h = 0.001;
76
+ const dLow = (smoothStep(0, 1, h) - smoothStep(0, 1, 0)) / h;
77
+ const dHigh = (smoothStep(0, 1, 1) - smoothStep(0, 1, 1 - h)) / h;
78
+ expect(dLow).toBeCloseTo(0, 1);
79
+ expect(dHigh).toBeCloseTo(0, 1);
80
+ });
81
+ });
82
+
83
+ describe('step', () => {
84
+ it('returns 0 when x is less than edge', () => {
85
+ expect(step(0.5, 0.3)).toBe(0);
86
+ });
87
+ it('returns 1 when x equals edge', () => {
88
+ expect(step(0.5, 0.5)).toBe(1);
89
+ });
90
+ it('returns 1 when x is greater than edge', () => {
91
+ expect(step(0.5, 0.7)).toBe(1);
92
+ });
93
+ });