@arvarus/perlin-noise 0.1.1
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/LICENSE +18 -0
- package/README.md +132 -0
- package/dist/__tests__/grid.test.d.ts +1 -0
- package/dist/__tests__/grid.test.js +270 -0
- package/dist/__tests__/index.test.d.ts +1 -0
- package/dist/__tests__/index.test.js +240 -0
- package/dist/__tests__/interpolation.test.d.ts +1 -0
- package/dist/__tests__/interpolation.test.js +253 -0
- package/dist/__tests__/scalar.test.d.ts +1 -0
- package/dist/__tests__/scalar.test.js +210 -0
- package/dist/grid.d.ts +23 -0
- package/dist/grid.js +107 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +54 -0
- package/dist/interpolation.d.ts +57 -0
- package/dist/interpolation.js +117 -0
- package/dist/scalar.d.ts +14 -0
- package/dist/scalar.js +85 -0
- package/package.json +40 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const interpolation_1 = require("../interpolation");
|
|
4
|
+
describe('smoothstep', () => {
|
|
5
|
+
it('should return 0 for t = 0', () => {
|
|
6
|
+
expect((0, interpolation_1.smoothstep)(0)).toBe(0);
|
|
7
|
+
});
|
|
8
|
+
it('should return 1 for t = 1', () => {
|
|
9
|
+
expect((0, interpolation_1.smoothstep)(1)).toBe(1);
|
|
10
|
+
});
|
|
11
|
+
it('should return 0.5 for t = 0.5', () => {
|
|
12
|
+
expect((0, interpolation_1.smoothstep)(0.5)).toBe(0.5);
|
|
13
|
+
});
|
|
14
|
+
it('should be symmetric around 0.5', () => {
|
|
15
|
+
const t1 = 0.3;
|
|
16
|
+
const t2 = 0.7;
|
|
17
|
+
const s1 = (0, interpolation_1.smoothstep)(t1);
|
|
18
|
+
const s2 = (0, interpolation_1.smoothstep)(t2);
|
|
19
|
+
expect(s1 + s2).toBeCloseTo(1, 10);
|
|
20
|
+
});
|
|
21
|
+
it('should clamp values outside [0, 1]', () => {
|
|
22
|
+
expect((0, interpolation_1.smoothstep)(-1)).toBe(0);
|
|
23
|
+
expect((0, interpolation_1.smoothstep)(2)).toBe(1);
|
|
24
|
+
});
|
|
25
|
+
it('should have zero derivative at endpoints', () => {
|
|
26
|
+
// Numerical test of derivative near 0
|
|
27
|
+
const h = 0.0001;
|
|
28
|
+
const derivativeAt0 = ((0, interpolation_1.smoothstep)(h) - (0, interpolation_1.smoothstep)(0)) / h;
|
|
29
|
+
expect(derivativeAt0).toBeCloseTo(0, 2);
|
|
30
|
+
// Numerical test of derivative near 1
|
|
31
|
+
const derivativeAt1 = ((0, interpolation_1.smoothstep)(1) - (0, interpolation_1.smoothstep)(1 - h)) / h;
|
|
32
|
+
expect(derivativeAt1).toBeCloseTo(0, 2);
|
|
33
|
+
});
|
|
34
|
+
it('should be monotonically increasing', () => {
|
|
35
|
+
const values = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
|
|
36
|
+
for (let i = 1; i < values.length; i++) {
|
|
37
|
+
expect((0, interpolation_1.smoothstep)(values[i])).toBeGreaterThanOrEqual((0, interpolation_1.smoothstep)(values[i - 1]));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('calculateFractionalCoordinates', () => {
|
|
42
|
+
it('should calculate fractional coordinates for a 1D point', () => {
|
|
43
|
+
const point = [2.3];
|
|
44
|
+
const fractional = (0, interpolation_1.calculateFractionalCoordinates)(point);
|
|
45
|
+
expect(fractional).toHaveLength(1);
|
|
46
|
+
expect(fractional[0]).toBeCloseTo(0.3, 10);
|
|
47
|
+
});
|
|
48
|
+
it('should calculate fractional coordinates for a 2D point', () => {
|
|
49
|
+
const point = [1.7, 3.2];
|
|
50
|
+
const fractional = (0, interpolation_1.calculateFractionalCoordinates)(point);
|
|
51
|
+
expect(fractional).toHaveLength(2);
|
|
52
|
+
expect(fractional[0]).toBeCloseTo(0.7, 10);
|
|
53
|
+
expect(fractional[1]).toBeCloseTo(0.2, 10);
|
|
54
|
+
});
|
|
55
|
+
it('should return 0 for a point exactly on a grid node', () => {
|
|
56
|
+
const point = [2.0, 3.0];
|
|
57
|
+
const fractional = (0, interpolation_1.calculateFractionalCoordinates)(point);
|
|
58
|
+
expect(fractional[0]).toBe(0);
|
|
59
|
+
expect(fractional[1]).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
it('should handle negative coordinates', () => {
|
|
62
|
+
const point = [-1.3];
|
|
63
|
+
const fractional = (0, interpolation_1.calculateFractionalCoordinates)(point);
|
|
64
|
+
// -1.3 is in cell [-2], so fractional coordinate is -1.3 - (-2) = 0.7
|
|
65
|
+
expect(fractional[0]).toBeCloseTo(0.7, 10);
|
|
66
|
+
});
|
|
67
|
+
it('should handle fractional coordinates close to 1', () => {
|
|
68
|
+
const point = [2.99];
|
|
69
|
+
const fractional = (0, interpolation_1.calculateFractionalCoordinates)(point);
|
|
70
|
+
expect(fractional[0]).toBeCloseTo(0.99, 10);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe('interpolate1D', () => {
|
|
74
|
+
it('should return a0 for t = 0', () => {
|
|
75
|
+
expect((0, interpolation_1.interpolate1D)(5, 10, 0)).toBe(5);
|
|
76
|
+
});
|
|
77
|
+
it('should return a1 for t = 1', () => {
|
|
78
|
+
expect((0, interpolation_1.interpolate1D)(5, 10, 1)).toBe(10);
|
|
79
|
+
});
|
|
80
|
+
it('should return the average for t = 0.5', () => {
|
|
81
|
+
expect((0, interpolation_1.interpolate1D)(0, 10, 0.5)).toBeCloseTo(5, 10);
|
|
82
|
+
});
|
|
83
|
+
it('should interpolate correctly for different values', () => {
|
|
84
|
+
const result = (0, interpolation_1.interpolate1D)(0, 100, 0.3);
|
|
85
|
+
// For t=0.3, smoothstep(0.3) ≈ 0.216, so result ≈ 0 + 0.216 * 100 = 21.6
|
|
86
|
+
expect(result).toBeGreaterThan(0);
|
|
87
|
+
expect(result).toBeLessThan(100);
|
|
88
|
+
});
|
|
89
|
+
it('should handle negative values', () => {
|
|
90
|
+
expect((0, interpolation_1.interpolate1D)(-10, 10, 0.5)).toBeCloseTo(0, 10);
|
|
91
|
+
});
|
|
92
|
+
it('should be symmetric', () => {
|
|
93
|
+
const result1 = (0, interpolation_1.interpolate1D)(0, 10, 0.3);
|
|
94
|
+
const result2 = (0, interpolation_1.interpolate1D)(10, 0, 0.7);
|
|
95
|
+
expect(result1).toBeCloseTo(result2, 10);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('interpolateScalarValues', () => {
|
|
99
|
+
describe('1D interpolation', () => {
|
|
100
|
+
it('should interpolate between 2 scalar values for a 1D point', () => {
|
|
101
|
+
const scalarValues = [5, 10];
|
|
102
|
+
const point = [0.3]; // Fractional coordinate = 0.3
|
|
103
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
104
|
+
// Should be equivalent to interpolate1D(5, 10, 0.3)
|
|
105
|
+
const expected = (0, interpolation_1.interpolate1D)(5, 10, 0.3);
|
|
106
|
+
expect(result).toBeCloseTo(expected, 10);
|
|
107
|
+
});
|
|
108
|
+
it('should return the first value for a point at node 0', () => {
|
|
109
|
+
const scalarValues = [5, 10];
|
|
110
|
+
const point = [2.0]; // Exactly at node
|
|
111
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
112
|
+
expect(result).toBeCloseTo(5, 10);
|
|
113
|
+
});
|
|
114
|
+
it('should return the second value for a point at node 1', () => {
|
|
115
|
+
const scalarValues = [5, 10];
|
|
116
|
+
// Note: for point=[3.0], fractional coordinate is 0, so we're at node of cell [3]
|
|
117
|
+
// But scalar values are for cell [2], so we test with point=[2.999...]
|
|
118
|
+
const point2 = [2.999999];
|
|
119
|
+
const result2 = (0, interpolation_1.interpolateScalarValues)(scalarValues, point2);
|
|
120
|
+
expect(result2).toBeCloseTo(10, 2);
|
|
121
|
+
});
|
|
122
|
+
it('should throw error if number of values does not match', () => {
|
|
123
|
+
const scalarValues = [5, 10, 15]; // 3 values instead of 2
|
|
124
|
+
const point = [0.5];
|
|
125
|
+
expect(() => (0, interpolation_1.interpolateScalarValues)(scalarValues, point)).toThrow();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
describe('2D interpolation', () => {
|
|
129
|
+
it('should interpolate between 4 scalar values for a 2D point', () => {
|
|
130
|
+
const scalarValues = [0, 10, 20, 30];
|
|
131
|
+
const point = [0.5, 0.5]; // Cell center
|
|
132
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
133
|
+
// At center, we should get approximately the average
|
|
134
|
+
expect(result).toBeGreaterThan(0);
|
|
135
|
+
expect(result).toBeLessThan(30);
|
|
136
|
+
});
|
|
137
|
+
it('should return corner value for a point exactly on a corner', () => {
|
|
138
|
+
const scalarValues = [5, 10, 15, 20];
|
|
139
|
+
const point = [2.0, 3.0]; // Exactly at corner [2,3]
|
|
140
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
141
|
+
// Should return the first value (bottom-left corner)
|
|
142
|
+
expect(result).toBeCloseTo(5, 10);
|
|
143
|
+
});
|
|
144
|
+
it('should interpolate correctly for different positions', () => {
|
|
145
|
+
const scalarValues = [0, 1, 2, 3];
|
|
146
|
+
const point = [0.0, 0.0]; // Bottom-left corner
|
|
147
|
+
const result1 = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
148
|
+
expect(result1).toBeCloseTo(0, 10);
|
|
149
|
+
const point2 = [0.999, 0.999]; // Close to top-right corner
|
|
150
|
+
const result2 = (0, interpolation_1.interpolateScalarValues)(scalarValues, point2);
|
|
151
|
+
expect(result2).toBeCloseTo(3, 1);
|
|
152
|
+
});
|
|
153
|
+
it('should throw error if number of values does not match', () => {
|
|
154
|
+
const scalarValues = [0, 1, 2]; // 3 values instead of 4
|
|
155
|
+
const point = [0.5, 0.5];
|
|
156
|
+
expect(() => (0, interpolation_1.interpolateScalarValues)(scalarValues, point)).toThrow();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe('3D interpolation', () => {
|
|
160
|
+
it('should interpolate between 8 scalar values for a 3D point', () => {
|
|
161
|
+
const scalarValues = [0, 1, 2, 3, 4, 5, 6, 7];
|
|
162
|
+
const point = [0.5, 0.5, 0.5]; // Cell center
|
|
163
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
164
|
+
expect(result).toBeGreaterThanOrEqual(0);
|
|
165
|
+
expect(result).toBeLessThanOrEqual(7);
|
|
166
|
+
});
|
|
167
|
+
it('should return corner value for a point exactly on a corner', () => {
|
|
168
|
+
const scalarValues = [10, 20, 30, 40, 50, 60, 70, 80];
|
|
169
|
+
const point = [1.0, 2.0, 3.0]; // Exactly at corner
|
|
170
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
171
|
+
// Should return the first value
|
|
172
|
+
expect(result).toBeCloseTo(10, 10);
|
|
173
|
+
});
|
|
174
|
+
it('should throw error if number of values does not match', () => {
|
|
175
|
+
const scalarValues = [0, 1, 2, 3, 4, 5, 6]; // 7 values instead of 8
|
|
176
|
+
const point = [0.5, 0.5, 0.5];
|
|
177
|
+
expect(() => (0, interpolation_1.interpolateScalarValues)(scalarValues, point)).toThrow();
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe('higher dimensions', () => {
|
|
181
|
+
it('should interpolate between 16 scalar values for a 4D point', () => {
|
|
182
|
+
const scalarValues = Array.from({ length: 16 }, (_, i) => i);
|
|
183
|
+
const point = [0.5, 0.5, 0.5, 0.5];
|
|
184
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
185
|
+
expect(result).toBeGreaterThanOrEqual(0);
|
|
186
|
+
expect(result).toBeLessThanOrEqual(15);
|
|
187
|
+
});
|
|
188
|
+
it('should interpolate between 32 scalar values for a 5D point', () => {
|
|
189
|
+
const scalarValues = Array.from({ length: 32 }, (_, i) => i);
|
|
190
|
+
const point = [0.5, 0.5, 0.5, 0.5, 0.5];
|
|
191
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
192
|
+
expect(result).toBeGreaterThanOrEqual(0);
|
|
193
|
+
expect(result).toBeLessThanOrEqual(31);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('edge cases', () => {
|
|
197
|
+
it('should handle all identical scalar values', () => {
|
|
198
|
+
const scalarValues = [5, 5, 5, 5];
|
|
199
|
+
const point = [0.7, 0.3];
|
|
200
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
201
|
+
expect(result).toBe(5);
|
|
202
|
+
});
|
|
203
|
+
it('should handle negative scalar values', () => {
|
|
204
|
+
const scalarValues = [-10, -5, 5, 10];
|
|
205
|
+
const point = [0.5, 0.5];
|
|
206
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
207
|
+
expect(result).toBeGreaterThan(-10);
|
|
208
|
+
expect(result).toBeLessThan(10);
|
|
209
|
+
});
|
|
210
|
+
it('should handle very large scalar values', () => {
|
|
211
|
+
const scalarValues = [1000, 2000, 3000, 4000];
|
|
212
|
+
const point = [0.5, 0.5];
|
|
213
|
+
const result = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
214
|
+
expect(result).toBeGreaterThan(1000);
|
|
215
|
+
expect(result).toBeLessThan(4000);
|
|
216
|
+
});
|
|
217
|
+
it('should produce consistent results for the same point', () => {
|
|
218
|
+
const scalarValues = [0, 1, 2, 3];
|
|
219
|
+
const point = [0.7, 0.3];
|
|
220
|
+
const result1 = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
221
|
+
const result2 = (0, interpolation_1.interpolateScalarValues)(scalarValues, point);
|
|
222
|
+
expect(result1).toBeCloseTo(result2, 10);
|
|
223
|
+
});
|
|
224
|
+
it('should produce different results for different points', () => {
|
|
225
|
+
const scalarValues = [0, 1, 2, 3];
|
|
226
|
+
const point1 = [0.1, 0.1];
|
|
227
|
+
const point2 = [0.9, 0.9];
|
|
228
|
+
const result1 = (0, interpolation_1.interpolateScalarValues)(scalarValues, point1);
|
|
229
|
+
const result2 = (0, interpolation_1.interpolateScalarValues)(scalarValues, point2);
|
|
230
|
+
expect(result1).not.toBeCloseTo(result2, 10);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('scaleNoiseValue', () => {
|
|
235
|
+
it('should return unchanged value with scale factor of 1.0', () => {
|
|
236
|
+
expect((0, interpolation_1.scaleNoiseValue)(0.5)).toBe(0.5);
|
|
237
|
+
expect((0, interpolation_1.scaleNoiseValue)(-0.3)).toBe(-0.3);
|
|
238
|
+
expect((0, interpolation_1.scaleNoiseValue)(1.0)).toBe(1.0);
|
|
239
|
+
});
|
|
240
|
+
it('should scale correctly with custom scale factor', () => {
|
|
241
|
+
expect((0, interpolation_1.scaleNoiseValue)(0.5, 2.0)).toBe(1.0);
|
|
242
|
+
expect((0, interpolation_1.scaleNoiseValue)(0.5, 0.5)).toBe(0.25);
|
|
243
|
+
expect((0, interpolation_1.scaleNoiseValue)(-0.5, 2.0)).toBe(-1.0);
|
|
244
|
+
});
|
|
245
|
+
it('should handle scale factor of 0', () => {
|
|
246
|
+
expect((0, interpolation_1.scaleNoiseValue)(0.5, 0)).toBe(0);
|
|
247
|
+
expect((0, interpolation_1.scaleNoiseValue)(-0.3, 0)).toBeCloseTo(0, 10);
|
|
248
|
+
});
|
|
249
|
+
it('should handle negative values', () => {
|
|
250
|
+
expect((0, interpolation_1.scaleNoiseValue)(-0.5, 2.0)).toBe(-1.0);
|
|
251
|
+
expect((0, interpolation_1.scaleNoiseValue)(-1.0, 0.5)).toBe(-0.5);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const grid_1 = require("../grid");
|
|
4
|
+
const scalar_1 = require("../scalar");
|
|
5
|
+
describe('calculateScalarValues', () => {
|
|
6
|
+
describe('1D grid', () => {
|
|
7
|
+
it('should return 2 scalar values for a point in 1D (2 vertices)', () => {
|
|
8
|
+
const grid = (0, grid_1.generateGradientGrid)(1, [5], 123);
|
|
9
|
+
const point = [2.5]; // Point in cell [2]
|
|
10
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
11
|
+
expect(scalarValues).toHaveLength(2); // 2^1 = 2 vertices
|
|
12
|
+
});
|
|
13
|
+
it('should calculate correct dot products for 1D', () => {
|
|
14
|
+
const grid = (0, grid_1.generateGradientGrid)(1, [5], 123);
|
|
15
|
+
const point = [1.3]; // Point in cell [1]
|
|
16
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
17
|
+
// Cell [1] has vertices at [1] and [2]
|
|
18
|
+
const gradient1 = grid.get('1');
|
|
19
|
+
const gradient2 = grid.get('2');
|
|
20
|
+
// Distance vectors: point - vertex
|
|
21
|
+
const distance1 = point[0] - 1; // 1.3 - 1 = 0.3
|
|
22
|
+
const distance2 = point[0] - 2; // 1.3 - 2 = -0.7
|
|
23
|
+
// Dot products: gradient * distance
|
|
24
|
+
const expected1 = gradient1 * distance1;
|
|
25
|
+
const expected2 = gradient2 * distance2;
|
|
26
|
+
expect(scalarValues[0]).toBeCloseTo(expected1, 10);
|
|
27
|
+
expect(scalarValues[1]).toBeCloseTo(expected2, 10);
|
|
28
|
+
});
|
|
29
|
+
it('should handle point at cell boundary', () => {
|
|
30
|
+
const grid = (0, grid_1.generateGradientGrid)(1, [5], 123);
|
|
31
|
+
const point = [2.0]; // Exactly at cell boundary (cell [2])
|
|
32
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
33
|
+
expect(scalarValues).toHaveLength(2);
|
|
34
|
+
// Cell [2] has vertices at [2] (i=0) and [3] (i=1)
|
|
35
|
+
// Distance to vertex [2] should be 0
|
|
36
|
+
const gradient2 = grid.get('2');
|
|
37
|
+
expect(scalarValues[0]).toBeCloseTo(gradient2 * 0, 10); // First vertex is [2]
|
|
38
|
+
});
|
|
39
|
+
it('should handle negative coordinates', () => {
|
|
40
|
+
// Create a grid that covers negative coordinates by using a larger grid
|
|
41
|
+
// and testing a point that maps to a valid cell
|
|
42
|
+
const grid = (0, grid_1.generateGradientGrid)(1, [10], 123);
|
|
43
|
+
const point = [0.5]; // Cell [0], which is valid
|
|
44
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
45
|
+
expect(scalarValues).toHaveLength(2);
|
|
46
|
+
// Verify vertices are [0] and [1]
|
|
47
|
+
expect(grid.has('0')).toBe(true);
|
|
48
|
+
expect(grid.has('1')).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('should throw error when point is outside grid bounds', () => {
|
|
51
|
+
const grid = (0, grid_1.generateGradientGrid)(1, [5], 123);
|
|
52
|
+
const point = [10.0]; // Outside grid (max is 5, so vertices go up to 6)
|
|
53
|
+
expect(() => (0, scalar_1.calculateScalarValues)(grid, point)).toThrow();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('2D grid', () => {
|
|
57
|
+
it('should return 4 scalar values for a point in 2D (4 vertices)', () => {
|
|
58
|
+
const grid = (0, grid_1.generateGradientGrid)(2, [5, 5], 123);
|
|
59
|
+
const point = [2.5, 3.2]; // Point in cell [2, 3]
|
|
60
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
61
|
+
expect(scalarValues).toHaveLength(4); // 2^2 = 4 vertices
|
|
62
|
+
});
|
|
63
|
+
it('should calculate correct dot products for 2D', () => {
|
|
64
|
+
const grid = (0, grid_1.generateGradientGrid)(2, [5, 5], 123);
|
|
65
|
+
const point = [1.3, 2.7]; // Point in cell [1, 2]
|
|
66
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
67
|
+
// Cell [1, 2] has vertices in order: [1,2] (i=0), [2,2] (i=1), [1,3] (i=2), [2,3] (i=3)
|
|
68
|
+
const gradient12 = grid.get('1,2');
|
|
69
|
+
const gradient22 = grid.get('2,2');
|
|
70
|
+
const gradient13 = grid.get('1,3');
|
|
71
|
+
const gradient23 = grid.get('2,3');
|
|
72
|
+
// Distance vectors: point - vertex
|
|
73
|
+
const distance12 = [point[0] - 1, point[1] - 2]; // [0.3, 0.7]
|
|
74
|
+
const distance22 = [point[0] - 2, point[1] - 2]; // [-0.7, 0.7]
|
|
75
|
+
const distance13 = [point[0] - 1, point[1] - 3]; // [0.3, -0.3]
|
|
76
|
+
const distance23 = [point[0] - 2, point[1] - 3]; // [-0.7, -0.3]
|
|
77
|
+
// Dot products
|
|
78
|
+
const expected12 = gradient12[0] * distance12[0] + gradient12[1] * distance12[1];
|
|
79
|
+
const expected22 = gradient22[0] * distance22[0] + gradient22[1] * distance22[1];
|
|
80
|
+
const expected13 = gradient13[0] * distance13[0] + gradient13[1] * distance13[1];
|
|
81
|
+
const expected23 = gradient23[0] * distance23[0] + gradient23[1] * distance23[1];
|
|
82
|
+
expect(scalarValues[0]).toBeCloseTo(expected12, 10);
|
|
83
|
+
expect(scalarValues[1]).toBeCloseTo(expected22, 10);
|
|
84
|
+
expect(scalarValues[2]).toBeCloseTo(expected13, 10);
|
|
85
|
+
expect(scalarValues[3]).toBeCloseTo(expected23, 10);
|
|
86
|
+
});
|
|
87
|
+
it('should handle point at cell corner', () => {
|
|
88
|
+
const grid = (0, grid_1.generateGradientGrid)(2, [5, 5], 123);
|
|
89
|
+
const point = [2.0, 3.0]; // Exactly at vertex [2, 3] in cell [2, 3]
|
|
90
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
91
|
+
expect(scalarValues).toHaveLength(4);
|
|
92
|
+
// Cell [2, 3] has vertices in order: [2,3] (i=0), [3,3] (i=1), [2,4] (i=2), [3,4] (i=3)
|
|
93
|
+
// Distance to vertex [2, 3] should be [0, 0]
|
|
94
|
+
expect(scalarValues[0]).toBeCloseTo(0, 10); // dot product with [0, 0] is 0
|
|
95
|
+
});
|
|
96
|
+
it('should handle decimal coordinates correctly', () => {
|
|
97
|
+
const grid = (0, grid_1.generateGradientGrid)(2, [5, 5], 123);
|
|
98
|
+
const point = [0.1, 0.9]; // Should be in cell [0, 0]
|
|
99
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
100
|
+
expect(scalarValues).toHaveLength(4);
|
|
101
|
+
// All vertices should be in cell [0, 0]: [0,0], [0,1], [1,0], [1,1]
|
|
102
|
+
expect(grid.has('0,0')).toBe(true);
|
|
103
|
+
expect(grid.has('0,1')).toBe(true);
|
|
104
|
+
expect(grid.has('1,0')).toBe(true);
|
|
105
|
+
expect(grid.has('1,1')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it('should throw error when point is outside grid bounds', () => {
|
|
108
|
+
const grid = (0, grid_1.generateGradientGrid)(2, [3, 3], 123);
|
|
109
|
+
const point = [10.0, 10.0]; // Outside grid
|
|
110
|
+
expect(() => (0, scalar_1.calculateScalarValues)(grid, point)).toThrow();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('3D grid', () => {
|
|
114
|
+
it('should return 8 scalar values for a point in 3D (8 vertices)', () => {
|
|
115
|
+
const grid = (0, grid_1.generateGradientGrid)(3, [5, 5, 5], 123);
|
|
116
|
+
const point = [2.5, 3.2, 1.8]; // Point in cell [2, 3, 1]
|
|
117
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
118
|
+
expect(scalarValues).toHaveLength(8); // 2^3 = 8 vertices
|
|
119
|
+
});
|
|
120
|
+
it('should calculate correct dot products for 3D', () => {
|
|
121
|
+
const grid = (0, grid_1.generateGradientGrid)(3, [5, 5, 5], 123);
|
|
122
|
+
const point = [1.0, 1.0, 1.0]; // Point in cell [1, 1, 1]
|
|
123
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
124
|
+
// Verify we get 8 values
|
|
125
|
+
expect(scalarValues).toHaveLength(8);
|
|
126
|
+
// Verify all vertices exist in grid
|
|
127
|
+
const cell = [1, 1, 1];
|
|
128
|
+
for (let i = 0; i < 8; i++) {
|
|
129
|
+
const vertex = [
|
|
130
|
+
cell[0] + ((i >> 0) & 1),
|
|
131
|
+
cell[1] + ((i >> 1) & 1),
|
|
132
|
+
cell[2] + ((i >> 2) & 1),
|
|
133
|
+
];
|
|
134
|
+
const key = vertex.join(',');
|
|
135
|
+
expect(grid.has(key)).toBe(true);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
it('should handle point at cell center', () => {
|
|
139
|
+
const grid = (0, grid_1.generateGradientGrid)(3, [5, 5, 5], 123);
|
|
140
|
+
const point = [1.5, 1.5, 1.5]; // Center of cell [1, 1, 1]
|
|
141
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
142
|
+
expect(scalarValues).toHaveLength(8);
|
|
143
|
+
// All distance vectors should have components of ±0.5
|
|
144
|
+
expect(scalarValues.every(val => typeof val === 'number')).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('higher dimensions', () => {
|
|
148
|
+
it('should return 16 scalar values for a point in 4D (16 vertices)', () => {
|
|
149
|
+
const grid = (0, grid_1.generateGradientGrid)(4, [3, 3, 3, 3], 123);
|
|
150
|
+
const point = [1.0, 1.0, 1.0, 1.0]; // Point in cell [1, 1, 1, 1]
|
|
151
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
152
|
+
expect(scalarValues).toHaveLength(16); // 2^4 = 16 vertices
|
|
153
|
+
});
|
|
154
|
+
it('should return 32 scalar values for a point in 5D (32 vertices)', () => {
|
|
155
|
+
const grid = (0, grid_1.generateGradientGrid)(5, [2, 2, 2, 2, 2], 123);
|
|
156
|
+
const point = [1.0, 1.0, 1.0, 1.0, 1.0]; // Point in cell [1, 1, 1, 1, 1]
|
|
157
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
158
|
+
expect(scalarValues).toHaveLength(32); // 2^5 = 32 vertices
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
describe('edge cases', () => {
|
|
162
|
+
it('should handle point at origin', () => {
|
|
163
|
+
const grid = (0, grid_1.generateGradientGrid)(2, [5, 5], 123);
|
|
164
|
+
const point = [0.0, 0.0]; // Cell [0, 0]
|
|
165
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
166
|
+
expect(scalarValues).toHaveLength(4);
|
|
167
|
+
expect(grid.has('0,0')).toBe(true);
|
|
168
|
+
expect(grid.has('0,1')).toBe(true);
|
|
169
|
+
expect(grid.has('1,0')).toBe(true);
|
|
170
|
+
expect(grid.has('1,1')).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
it('should handle very small coordinates', () => {
|
|
173
|
+
const grid = (0, grid_1.generateGradientGrid)(2, [5, 5], 123);
|
|
174
|
+
const point = [0.0001, 0.0001]; // Cell [0, 0]
|
|
175
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
176
|
+
expect(scalarValues).toHaveLength(4);
|
|
177
|
+
});
|
|
178
|
+
it('should handle large coordinates within bounds', () => {
|
|
179
|
+
const grid = (0, grid_1.generateGradientGrid)(2, [10, 10], 123);
|
|
180
|
+
const point = [9.9, 9.9]; // Cell [9, 9], should have vertices at [9,9], [9,10], [10,9], [10,10]
|
|
181
|
+
const scalarValues = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
182
|
+
expect(scalarValues).toHaveLength(4);
|
|
183
|
+
// Grid size is 10, so vertices go up to 10
|
|
184
|
+
expect(grid.has('9,9')).toBe(true);
|
|
185
|
+
expect(grid.has('9,10')).toBe(true);
|
|
186
|
+
expect(grid.has('10,9')).toBe(true);
|
|
187
|
+
expect(grid.has('10,10')).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
it('should produce consistent results for same point', () => {
|
|
190
|
+
const grid = (0, grid_1.generateGradientGrid)(2, [5, 5], 123);
|
|
191
|
+
const point = [2.5, 3.2];
|
|
192
|
+
const scalarValues1 = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
193
|
+
const scalarValues2 = (0, scalar_1.calculateScalarValues)(grid, point);
|
|
194
|
+
expect(scalarValues1).toHaveLength(scalarValues2.length);
|
|
195
|
+
for (let i = 0; i < scalarValues1.length; i++) {
|
|
196
|
+
expect(scalarValues1[i]).toBeCloseTo(scalarValues2[i], 10);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
it('should produce different results for different points', () => {
|
|
200
|
+
const grid = (0, grid_1.generateGradientGrid)(2, [5, 5], 123);
|
|
201
|
+
const point1 = [1.0, 1.0];
|
|
202
|
+
const point2 = [2.0, 2.0];
|
|
203
|
+
const scalarValues1 = (0, scalar_1.calculateScalarValues)(grid, point1);
|
|
204
|
+
const scalarValues2 = (0, scalar_1.calculateScalarValues)(grid, point2);
|
|
205
|
+
// They might have some same values, but at least one should be different
|
|
206
|
+
expect(scalarValues1).toHaveLength(4);
|
|
207
|
+
expect(scalarValues2).toHaveLength(4);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|
package/dist/grid.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid generation for Perlin noise
|
|
3
|
+
* Generates a grid with random gradient vectors at each intersection
|
|
4
|
+
*/
|
|
5
|
+
export type GridDimension = number;
|
|
6
|
+
export type GradientVector = number | number[];
|
|
7
|
+
/**
|
|
8
|
+
* Generates a grid with gradient vectors at each intersection
|
|
9
|
+
*
|
|
10
|
+
* @param dimension - The dimension of the grid (any positive integer)
|
|
11
|
+
* @param size - Array of sizes for each dimension (number of cells per dimension)
|
|
12
|
+
* @param seed - Optional seed for random number generation
|
|
13
|
+
* @returns A Map with grid coordinates as keys and gradient vectors as values
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateGradientGrid(dimension: GridDimension, size: number[], seed?: number): Map<string, GradientVector>;
|
|
16
|
+
/**
|
|
17
|
+
* Gets the gradient vector at a specific grid intersection
|
|
18
|
+
*
|
|
19
|
+
* @param grid - The gradient grid
|
|
20
|
+
* @param coordinates - The grid coordinates as an array
|
|
21
|
+
* @returns The gradient vector at the specified coordinates, or undefined if not found
|
|
22
|
+
*/
|
|
23
|
+
export declare function getGradientAt(grid: Map<string, GradientVector>, coordinates: number[]): GradientVector | undefined;
|
package/dist/grid.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Grid generation for Perlin noise
|
|
4
|
+
* Generates a grid with random gradient vectors at each intersection
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.generateGradientGrid = generateGradientGrid;
|
|
8
|
+
exports.getGradientAt = getGradientAt;
|
|
9
|
+
/**
|
|
10
|
+
* Generates a random number between min and max (inclusive)
|
|
11
|
+
*/
|
|
12
|
+
function randomRange(min, max, rng) {
|
|
13
|
+
return min + rng() * (max - min);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generates a random number between 0 and 1 using a seeded random number generator
|
|
17
|
+
*/
|
|
18
|
+
function createSeededRNG(seed) {
|
|
19
|
+
let value = seed;
|
|
20
|
+
return () => {
|
|
21
|
+
value = (value * 9301 + 49297) % 233280;
|
|
22
|
+
return value / 233280;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes a vector to unit length
|
|
27
|
+
*/
|
|
28
|
+
function normalizeVector(vector) {
|
|
29
|
+
const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
|
|
30
|
+
if (magnitude === 0) {
|
|
31
|
+
return vector.map(() => 0);
|
|
32
|
+
}
|
|
33
|
+
return vector.map(val => val / magnitude);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Generates a random unit-length gradient vector for the given dimension
|
|
37
|
+
*/
|
|
38
|
+
function generateUnitGradient(dimension, rng) {
|
|
39
|
+
const vector = [];
|
|
40
|
+
for (let i = 0; i < dimension; i++) {
|
|
41
|
+
vector.push(randomRange(-1, 1, rng));
|
|
42
|
+
}
|
|
43
|
+
return normalizeVector(vector);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Recursively generates all coordinate combinations for a given dimension
|
|
47
|
+
*/
|
|
48
|
+
function generateCoordinates(dimension, gridPoints, currentCoords = []) {
|
|
49
|
+
if (currentCoords.length === dimension) {
|
|
50
|
+
return [currentCoords];
|
|
51
|
+
}
|
|
52
|
+
const coordinates = [];
|
|
53
|
+
const currentDim = currentCoords.length;
|
|
54
|
+
const pointsForThisDim = gridPoints[currentDim];
|
|
55
|
+
for (let i = 0; i < pointsForThisDim; i++) {
|
|
56
|
+
const newCoords = [...currentCoords, i];
|
|
57
|
+
coordinates.push(...generateCoordinates(dimension, gridPoints, newCoords));
|
|
58
|
+
}
|
|
59
|
+
return coordinates;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generates a grid with gradient vectors at each intersection
|
|
63
|
+
*
|
|
64
|
+
* @param dimension - The dimension of the grid (any positive integer)
|
|
65
|
+
* @param size - Array of sizes for each dimension (number of cells per dimension)
|
|
66
|
+
* @param seed - Optional seed for random number generation
|
|
67
|
+
* @returns A Map with grid coordinates as keys and gradient vectors as values
|
|
68
|
+
*/
|
|
69
|
+
function generateGradientGrid(dimension, size, seed = Math.floor(Math.random() * 1000000)) {
|
|
70
|
+
const grid = new Map();
|
|
71
|
+
const rng = createSeededRNG(seed);
|
|
72
|
+
if (!Number.isInteger(dimension) || dimension < 1) {
|
|
73
|
+
throw new Error(`Dimension must be a positive integer, got ${dimension}`);
|
|
74
|
+
}
|
|
75
|
+
if (size.length !== dimension) {
|
|
76
|
+
throw new Error(`Size array length (${size.length}) must match dimension (${dimension})`);
|
|
77
|
+
}
|
|
78
|
+
// Calculate the number of grid points for each dimension (size + 1 for intersections)
|
|
79
|
+
const gridPoints = size.map(s => s + 1);
|
|
80
|
+
const coordinates = generateCoordinates(dimension, gridPoints);
|
|
81
|
+
for (const coord of coordinates) {
|
|
82
|
+
const key = coord.join(',');
|
|
83
|
+
let gradient;
|
|
84
|
+
if (dimension === 1) {
|
|
85
|
+
// 1D case: random scalars between -1 and 1
|
|
86
|
+
gradient = randomRange(-1, 1, rng);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Multi-dimensional case: unit-length vectors
|
|
90
|
+
const vector = generateUnitGradient(dimension, rng);
|
|
91
|
+
gradient = vector;
|
|
92
|
+
}
|
|
93
|
+
grid.set(key, gradient);
|
|
94
|
+
}
|
|
95
|
+
return grid;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Gets the gradient vector at a specific grid intersection
|
|
99
|
+
*
|
|
100
|
+
* @param grid - The gradient grid
|
|
101
|
+
* @param coordinates - The grid coordinates as an array
|
|
102
|
+
* @returns The gradient vector at the specified coordinates, or undefined if not found
|
|
103
|
+
*/
|
|
104
|
+
function getGradientAt(grid, coordinates) {
|
|
105
|
+
const key = coordinates.join(',');
|
|
106
|
+
return grid.get(key);
|
|
107
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @arvarus/perlin-noise
|
|
3
|
+
* Perlin noise implementation in TypeScript
|
|
4
|
+
*/
|
|
5
|
+
export { generateGradientGrid, getGradientAt } from './grid';
|
|
6
|
+
export type { GridDimension, GradientVector } from './grid';
|
|
7
|
+
/**
|
|
8
|
+
* Configuration options for PerlinNoise
|
|
9
|
+
*/
|
|
10
|
+
export interface PerlinNoiseOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Seed for random number generation
|
|
13
|
+
*/
|
|
14
|
+
seed?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Grid size for each dimension (default: [64, 64, 64])
|
|
17
|
+
* The grid will wrap around, so values can be smaller for repeating patterns
|
|
18
|
+
* Supports 1 to 10 dimensions
|
|
19
|
+
*/
|
|
20
|
+
gridSize?: number[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* PerlinNoise class for generating Perlin noise values
|
|
24
|
+
*/
|
|
25
|
+
export declare class PerlinNoise {
|
|
26
|
+
private grid;
|
|
27
|
+
private seed;
|
|
28
|
+
private gridSize;
|
|
29
|
+
private dimension;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a new PerlinNoise instance
|
|
32
|
+
*
|
|
33
|
+
* @param options - Configuration options (seed and grid size)
|
|
34
|
+
*/
|
|
35
|
+
constructor(options?: PerlinNoiseOptions);
|
|
36
|
+
/**
|
|
37
|
+
* Generate noise value at given coordinates
|
|
38
|
+
* Supports noise generation for any dimension (1 to 10)
|
|
39
|
+
*
|
|
40
|
+
* @param coordinates - Array of coordinates, length must match grid dimension
|
|
41
|
+
* @returns Noise value in the range approximately [-1, 1]
|
|
42
|
+
*/
|
|
43
|
+
noise(coordinates: number[]): number;
|
|
44
|
+
}
|