@holoscript/std 2.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.
package/src/math.ts ADDED
@@ -0,0 +1,658 @@
1
+ /**
2
+ * Math utilities for HoloScript
3
+ *
4
+ * Vector math, quaternion operations, interpolation, and noise functions.
5
+ */
6
+
7
+ import type { Vec2, Vec3, Vec4, Quat, EulerAngles, AABB, Ray, RaycastHit } from './types.js';
8
+ import { vec3, quat } from './types.js';
9
+
10
+ // =============================================================================
11
+ // CONSTANTS
12
+ // =============================================================================
13
+
14
+ export const PI = Math.PI;
15
+ export const TAU = Math.PI * 2;
16
+ export const HALF_PI = Math.PI / 2;
17
+ export const DEG_TO_RAD = Math.PI / 180;
18
+ export const RAD_TO_DEG = 180 / Math.PI;
19
+ export const EPSILON = 1e-6;
20
+
21
+ // =============================================================================
22
+ // BASIC MATH
23
+ // =============================================================================
24
+
25
+ export function clamp(value: number, min: number, max: number): number {
26
+ return Math.max(min, Math.min(max, value));
27
+ }
28
+
29
+ export function lerp(a: number, b: number, t: number): number {
30
+ return a + (b - a) * t;
31
+ }
32
+
33
+ export function inverseLerp(a: number, b: number, value: number): number {
34
+ return (value - a) / (b - a);
35
+ }
36
+
37
+ export function remap(value: number, inMin: number, inMax: number, outMin: number, outMax: number): number {
38
+ return lerp(outMin, outMax, inverseLerp(inMin, inMax, value));
39
+ }
40
+
41
+ export function smoothstep(edge0: number, edge1: number, x: number): number {
42
+ const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
43
+ return t * t * (3 - 2 * t);
44
+ }
45
+
46
+ export function smootherstep(edge0: number, edge1: number, x: number): number {
47
+ const t = clamp((x - edge0) / (edge1 - edge0), 0, 1);
48
+ return t * t * t * (t * (t * 6 - 15) + 10);
49
+ }
50
+
51
+ export function degToRad(degrees: number): number {
52
+ return degrees * DEG_TO_RAD;
53
+ }
54
+
55
+ export function radToDeg(radians: number): number {
56
+ return radians * RAD_TO_DEG;
57
+ }
58
+
59
+ export function mod(n: number, m: number): number {
60
+ return ((n % m) + m) % m;
61
+ }
62
+
63
+ export function fract(x: number): number {
64
+ return x - Math.floor(x);
65
+ }
66
+
67
+ export function sign(x: number): number {
68
+ return x > 0 ? 1 : x < 0 ? -1 : 0;
69
+ }
70
+
71
+ export function step(edge: number, x: number): number {
72
+ return x < edge ? 0 : 1;
73
+ }
74
+
75
+ // =============================================================================
76
+ // VEC2 OPERATIONS
77
+ // =============================================================================
78
+
79
+ export const vec2Math = {
80
+ add: (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y }),
81
+ sub: (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y }),
82
+ mul: (a: Vec2, s: number): Vec2 => ({ x: a.x * s, y: a.y * s }),
83
+ div: (a: Vec2, s: number): Vec2 => ({ x: a.x / s, y: a.y / s }),
84
+ dot: (a: Vec2, b: Vec2): number => a.x * b.x + a.y * b.y,
85
+ length: (v: Vec2): number => Math.sqrt(v.x * v.x + v.y * v.y),
86
+ lengthSq: (v: Vec2): number => v.x * v.x + v.y * v.y,
87
+ normalize: (v: Vec2): Vec2 => {
88
+ const len = vec2Math.length(v);
89
+ return len > 0 ? vec2Math.div(v, len) : { x: 0, y: 0 };
90
+ },
91
+ distance: (a: Vec2, b: Vec2): number => vec2Math.length(vec2Math.sub(a, b)),
92
+ lerp: (a: Vec2, b: Vec2, t: number): Vec2 => ({
93
+ x: lerp(a.x, b.x, t),
94
+ y: lerp(a.y, b.y, t),
95
+ }),
96
+ rotate: (v: Vec2, angle: number): Vec2 => {
97
+ const c = Math.cos(angle);
98
+ const s = Math.sin(angle);
99
+ return { x: v.x * c - v.y * s, y: v.x * s + v.y * c };
100
+ },
101
+ angle: (v: Vec2): number => Math.atan2(v.y, v.x),
102
+ angleBetween: (a: Vec2, b: Vec2): number => {
103
+ return Math.acos(clamp(vec2Math.dot(vec2Math.normalize(a), vec2Math.normalize(b)), -1, 1));
104
+ },
105
+ };
106
+
107
+ // =============================================================================
108
+ // VEC3 OPERATIONS
109
+ // =============================================================================
110
+
111
+ export const vec3Math = {
112
+ zero: (): Vec3 => vec3(0, 0, 0),
113
+ one: (): Vec3 => vec3(1, 1, 1),
114
+ up: (): Vec3 => vec3(0, 1, 0),
115
+ down: (): Vec3 => vec3(0, -1, 0),
116
+ forward: (): Vec3 => vec3(0, 0, -1),
117
+ back: (): Vec3 => vec3(0, 0, 1),
118
+ left: (): Vec3 => vec3(-1, 0, 0),
119
+ right: (): Vec3 => vec3(1, 0, 0),
120
+
121
+ add: (a: Vec3, b: Vec3): Vec3 => ({ x: a.x + b.x, y: a.y + b.y, z: a.z + b.z }),
122
+ sub: (a: Vec3, b: Vec3): Vec3 => ({ x: a.x - b.x, y: a.y - b.y, z: a.z - b.z }),
123
+ mul: (a: Vec3, s: number): Vec3 => ({ x: a.x * s, y: a.y * s, z: a.z * s }),
124
+ div: (a: Vec3, s: number): Vec3 => ({ x: a.x / s, y: a.y / s, z: a.z / s }),
125
+ mulVec: (a: Vec3, b: Vec3): Vec3 => ({ x: a.x * b.x, y: a.y * b.y, z: a.z * b.z }),
126
+
127
+ dot: (a: Vec3, b: Vec3): number => a.x * b.x + a.y * b.y + a.z * b.z,
128
+
129
+ cross: (a: Vec3, b: Vec3): Vec3 => ({
130
+ x: a.y * b.z - a.z * b.y,
131
+ y: a.z * b.x - a.x * b.z,
132
+ z: a.x * b.y - a.y * b.x,
133
+ }),
134
+
135
+ length: (v: Vec3): number => Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z),
136
+ lengthSq: (v: Vec3): number => v.x * v.x + v.y * v.y + v.z * v.z,
137
+
138
+ normalize: (v: Vec3): Vec3 => {
139
+ const len = vec3Math.length(v);
140
+ return len > EPSILON ? vec3Math.div(v, len) : vec3(0, 0, 0);
141
+ },
142
+
143
+ distance: (a: Vec3, b: Vec3): number => vec3Math.length(vec3Math.sub(a, b)),
144
+ distanceSq: (a: Vec3, b: Vec3): number => vec3Math.lengthSq(vec3Math.sub(a, b)),
145
+
146
+ lerp: (a: Vec3, b: Vec3, t: number): Vec3 => ({
147
+ x: lerp(a.x, b.x, t),
148
+ y: lerp(a.y, b.y, t),
149
+ z: lerp(a.z, b.z, t),
150
+ }),
151
+
152
+ slerp: (a: Vec3, b: Vec3, t: number): Vec3 => {
153
+ const dot = clamp(vec3Math.dot(a, b), -1, 1);
154
+ const theta = Math.acos(dot) * t;
155
+ const relative = vec3Math.normalize(vec3Math.sub(b, vec3Math.mul(a, dot)));
156
+ return vec3Math.add(
157
+ vec3Math.mul(a, Math.cos(theta)),
158
+ vec3Math.mul(relative, Math.sin(theta))
159
+ );
160
+ },
161
+
162
+ negate: (v: Vec3): Vec3 => ({ x: -v.x, y: -v.y, z: -v.z }),
163
+
164
+ abs: (v: Vec3): Vec3 => ({ x: Math.abs(v.x), y: Math.abs(v.y), z: Math.abs(v.z) }),
165
+
166
+ min: (a: Vec3, b: Vec3): Vec3 => ({
167
+ x: Math.min(a.x, b.x),
168
+ y: Math.min(a.y, b.y),
169
+ z: Math.min(a.z, b.z),
170
+ }),
171
+
172
+ max: (a: Vec3, b: Vec3): Vec3 => ({
173
+ x: Math.max(a.x, b.x),
174
+ y: Math.max(a.y, b.y),
175
+ z: Math.max(a.z, b.z),
176
+ }),
177
+
178
+ clamp: (v: Vec3, min: Vec3, max: Vec3): Vec3 => ({
179
+ x: clamp(v.x, min.x, max.x),
180
+ y: clamp(v.y, min.y, max.y),
181
+ z: clamp(v.z, min.z, max.z),
182
+ }),
183
+
184
+ floor: (v: Vec3): Vec3 => ({
185
+ x: Math.floor(v.x),
186
+ y: Math.floor(v.y),
187
+ z: Math.floor(v.z),
188
+ }),
189
+
190
+ ceil: (v: Vec3): Vec3 => ({
191
+ x: Math.ceil(v.x),
192
+ y: Math.ceil(v.y),
193
+ z: Math.ceil(v.z),
194
+ }),
195
+
196
+ round: (v: Vec3): Vec3 => ({
197
+ x: Math.round(v.x),
198
+ y: Math.round(v.y),
199
+ z: Math.round(v.z),
200
+ }),
201
+
202
+ reflect: (v: Vec3, normal: Vec3): Vec3 => {
203
+ const dot = vec3Math.dot(v, normal);
204
+ return vec3Math.sub(v, vec3Math.mul(normal, 2 * dot));
205
+ },
206
+
207
+ project: (v: Vec3, onto: Vec3): Vec3 => {
208
+ const dot = vec3Math.dot(v, onto);
209
+ const lenSq = vec3Math.lengthSq(onto);
210
+ return vec3Math.mul(onto, dot / lenSq);
211
+ },
212
+
213
+ angle: (a: Vec3, b: Vec3): number => {
214
+ const dot = vec3Math.dot(vec3Math.normalize(a), vec3Math.normalize(b));
215
+ return Math.acos(clamp(dot, -1, 1));
216
+ },
217
+
218
+ equals: (a: Vec3, b: Vec3, epsilon: number = EPSILON): boolean => {
219
+ return (
220
+ Math.abs(a.x - b.x) < epsilon &&
221
+ Math.abs(a.y - b.y) < epsilon &&
222
+ Math.abs(a.z - b.z) < epsilon
223
+ );
224
+ },
225
+ };
226
+
227
+ // =============================================================================
228
+ // QUATERNION OPERATIONS
229
+ // =============================================================================
230
+
231
+ export const quatMath = {
232
+ identity: (): Quat => quat(0, 0, 0, 1),
233
+
234
+ fromAxisAngle: (axis: Vec3, angle: number): Quat => {
235
+ const halfAngle = angle / 2;
236
+ const s = Math.sin(halfAngle);
237
+ const normalized = vec3Math.normalize(axis);
238
+ return {
239
+ x: normalized.x * s,
240
+ y: normalized.y * s,
241
+ z: normalized.z * s,
242
+ w: Math.cos(halfAngle),
243
+ };
244
+ },
245
+
246
+ fromEuler: (euler: EulerAngles): Quat => {
247
+ const { x, y, z } = euler;
248
+ const c1 = Math.cos(degToRad(x) / 2);
249
+ const c2 = Math.cos(degToRad(y) / 2);
250
+ const c3 = Math.cos(degToRad(z) / 2);
251
+ const s1 = Math.sin(degToRad(x) / 2);
252
+ const s2 = Math.sin(degToRad(y) / 2);
253
+ const s3 = Math.sin(degToRad(z) / 2);
254
+
255
+ return {
256
+ x: s1 * c2 * c3 + c1 * s2 * s3,
257
+ y: c1 * s2 * c3 - s1 * c2 * s3,
258
+ z: c1 * c2 * s3 + s1 * s2 * c3,
259
+ w: c1 * c2 * c3 - s1 * s2 * s3,
260
+ };
261
+ },
262
+
263
+ toEuler: (q: Quat): EulerAngles => {
264
+ const sinr_cosp = 2 * (q.w * q.x + q.y * q.z);
265
+ const cosr_cosp = 1 - 2 * (q.x * q.x + q.y * q.y);
266
+ const x = Math.atan2(sinr_cosp, cosr_cosp);
267
+
268
+ const sinp = 2 * (q.w * q.y - q.z * q.x);
269
+ let y: number;
270
+ if (Math.abs(sinp) >= 1) {
271
+ y = (Math.PI / 2) * Math.sign(sinp);
272
+ } else {
273
+ y = Math.asin(sinp);
274
+ }
275
+
276
+ const siny_cosp = 2 * (q.w * q.z + q.x * q.y);
277
+ const cosy_cosp = 1 - 2 * (q.y * q.y + q.z * q.z);
278
+ const z = Math.atan2(siny_cosp, cosy_cosp);
279
+
280
+ return {
281
+ x: radToDeg(x),
282
+ y: radToDeg(y),
283
+ z: radToDeg(z),
284
+ };
285
+ },
286
+
287
+ multiply: (a: Quat, b: Quat): Quat => ({
288
+ x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
289
+ y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
290
+ z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
291
+ w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z,
292
+ }),
293
+
294
+ conjugate: (q: Quat): Quat => ({ x: -q.x, y: -q.y, z: -q.z, w: q.w }),
295
+
296
+ inverse: (q: Quat): Quat => {
297
+ const lenSq = q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w;
298
+ return {
299
+ x: -q.x / lenSq,
300
+ y: -q.y / lenSq,
301
+ z: -q.z / lenSq,
302
+ w: q.w / lenSq,
303
+ };
304
+ },
305
+
306
+ normalize: (q: Quat): Quat => {
307
+ const len = Math.sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
308
+ return { x: q.x / len, y: q.y / len, z: q.z / len, w: q.w / len };
309
+ },
310
+
311
+ dot: (a: Quat, b: Quat): number => a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w,
312
+
313
+ slerp: (a: Quat, b: Quat, t: number): Quat => {
314
+ let dot = quatMath.dot(a, b);
315
+
316
+ // If dot is negative, negate one quaternion to take shorter path
317
+ let bAdjusted = b;
318
+ if (dot < 0) {
319
+ bAdjusted = { x: -b.x, y: -b.y, z: -b.z, w: -b.w };
320
+ dot = -dot;
321
+ }
322
+
323
+ if (dot > 0.9995) {
324
+ // Linear interpolation for very close quaternions
325
+ return quatMath.normalize({
326
+ x: lerp(a.x, bAdjusted.x, t),
327
+ y: lerp(a.y, bAdjusted.y, t),
328
+ z: lerp(a.z, bAdjusted.z, t),
329
+ w: lerp(a.w, bAdjusted.w, t),
330
+ });
331
+ }
332
+
333
+ const theta0 = Math.acos(dot);
334
+ const theta = theta0 * t;
335
+ const sinTheta = Math.sin(theta);
336
+ const sinTheta0 = Math.sin(theta0);
337
+
338
+ const s0 = Math.cos(theta) - (dot * sinTheta) / sinTheta0;
339
+ const s1 = sinTheta / sinTheta0;
340
+
341
+ return {
342
+ x: a.x * s0 + bAdjusted.x * s1,
343
+ y: a.y * s0 + bAdjusted.y * s1,
344
+ z: a.z * s0 + bAdjusted.z * s1,
345
+ w: a.w * s0 + bAdjusted.w * s1,
346
+ };
347
+ },
348
+
349
+ rotateVec3: (q: Quat, v: Vec3): Vec3 => {
350
+ const qv = { x: q.x, y: q.y, z: q.z };
351
+ const uv = vec3Math.cross(qv, v);
352
+ const uuv = vec3Math.cross(qv, uv);
353
+ return vec3Math.add(v, vec3Math.add(vec3Math.mul(uv, 2 * q.w), vec3Math.mul(uuv, 2)));
354
+ },
355
+
356
+ lookAt: (forward: Vec3, up: Vec3 = vec3(0, 1, 0)): Quat => {
357
+ const f = vec3Math.normalize(forward);
358
+ const r = vec3Math.normalize(vec3Math.cross(up, f));
359
+ const u = vec3Math.cross(f, r);
360
+
361
+ const trace = r.x + u.y + f.z;
362
+ let q: Quat;
363
+
364
+ if (trace > 0) {
365
+ const s = 0.5 / Math.sqrt(trace + 1);
366
+ q = {
367
+ w: 0.25 / s,
368
+ x: (u.z - f.y) * s,
369
+ y: (f.x - r.z) * s,
370
+ z: (r.y - u.x) * s,
371
+ };
372
+ } else if (r.x > u.y && r.x > f.z) {
373
+ const s = 2 * Math.sqrt(1 + r.x - u.y - f.z);
374
+ q = {
375
+ w: (u.z - f.y) / s,
376
+ x: 0.25 * s,
377
+ y: (u.x + r.y) / s,
378
+ z: (f.x + r.z) / s,
379
+ };
380
+ } else if (u.y > f.z) {
381
+ const s = 2 * Math.sqrt(1 + u.y - r.x - f.z);
382
+ q = {
383
+ w: (f.x - r.z) / s,
384
+ x: (u.x + r.y) / s,
385
+ y: 0.25 * s,
386
+ z: (f.y + u.z) / s,
387
+ };
388
+ } else {
389
+ const s = 2 * Math.sqrt(1 + f.z - r.x - u.y);
390
+ q = {
391
+ w: (r.y - u.x) / s,
392
+ x: (f.x + r.z) / s,
393
+ y: (f.y + u.z) / s,
394
+ z: 0.25 * s,
395
+ };
396
+ }
397
+
398
+ return quatMath.normalize(q);
399
+ },
400
+ };
401
+
402
+ // =============================================================================
403
+ // AABB OPERATIONS
404
+ // =============================================================================
405
+
406
+ export const aabbMath = {
407
+ contains: (aabb: AABB, point: Vec3): boolean => {
408
+ return (
409
+ point.x >= aabb.min.x && point.x <= aabb.max.x &&
410
+ point.y >= aabb.min.y && point.y <= aabb.max.y &&
411
+ point.z >= aabb.min.z && point.z <= aabb.max.z
412
+ );
413
+ },
414
+
415
+ intersects: (a: AABB, b: AABB): boolean => {
416
+ return (
417
+ a.min.x <= b.max.x && a.max.x >= b.min.x &&
418
+ a.min.y <= b.max.y && a.max.y >= b.min.y &&
419
+ a.min.z <= b.max.z && a.max.z >= b.min.z
420
+ );
421
+ },
422
+
423
+ center: (aabb: AABB): Vec3 => vec3Math.mul(vec3Math.add(aabb.min, aabb.max), 0.5),
424
+
425
+ size: (aabb: AABB): Vec3 => vec3Math.sub(aabb.max, aabb.min),
426
+
427
+ expand: (aabb: AABB, point: Vec3): AABB => ({
428
+ min: vec3Math.min(aabb.min, point),
429
+ max: vec3Math.max(aabb.max, point),
430
+ }),
431
+
432
+ merge: (a: AABB, b: AABB): AABB => ({
433
+ min: vec3Math.min(a.min, b.min),
434
+ max: vec3Math.max(a.max, b.max),
435
+ }),
436
+ };
437
+
438
+ // =============================================================================
439
+ // NOISE FUNCTIONS
440
+ // =============================================================================
441
+
442
+ // Simple permutation table for noise
443
+ const perm = new Uint8Array(512);
444
+ for (let i = 0; i < 256; i++) perm[i] = perm[i + 256] = i;
445
+ for (let i = 255; i > 0; i--) {
446
+ const j = Math.floor(Math.random() * (i + 1));
447
+ [perm[i], perm[j]] = [perm[j], perm[i]];
448
+ perm[i + 256] = perm[i];
449
+ }
450
+
451
+ function fade(t: number): number {
452
+ return t * t * t * (t * (t * 6 - 15) + 10);
453
+ }
454
+
455
+ function grad(hash: number, x: number, y: number, z: number): number {
456
+ const h = hash & 15;
457
+ const u = h < 8 ? x : y;
458
+ const v = h < 4 ? y : h === 12 || h === 14 ? x : z;
459
+ return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v);
460
+ }
461
+
462
+ export const noise = {
463
+ /**
464
+ * 3D Perlin noise (-1 to 1)
465
+ */
466
+ perlin3d: (x: number, y: number, z: number): number => {
467
+ const X = Math.floor(x) & 255;
468
+ const Y = Math.floor(y) & 255;
469
+ const Z = Math.floor(z) & 255;
470
+
471
+ x -= Math.floor(x);
472
+ y -= Math.floor(y);
473
+ z -= Math.floor(z);
474
+
475
+ const u = fade(x);
476
+ const v = fade(y);
477
+ const w = fade(z);
478
+
479
+ const A = perm[X] + Y;
480
+ const AA = perm[A] + Z;
481
+ const AB = perm[A + 1] + Z;
482
+ const B = perm[X + 1] + Y;
483
+ const BA = perm[B] + Z;
484
+ const BB = perm[B + 1] + Z;
485
+
486
+ return lerp(
487
+ lerp(
488
+ lerp(grad(perm[AA], x, y, z), grad(perm[BA], x - 1, y, z), u),
489
+ lerp(grad(perm[AB], x, y - 1, z), grad(perm[BB], x - 1, y - 1, z), u),
490
+ v
491
+ ),
492
+ lerp(
493
+ lerp(grad(perm[AA + 1], x, y, z - 1), grad(perm[BA + 1], x - 1, y, z - 1), u),
494
+ lerp(grad(perm[AB + 1], x, y - 1, z - 1), grad(perm[BB + 1], x - 1, y - 1, z - 1), u),
495
+ v
496
+ ),
497
+ w
498
+ );
499
+ },
500
+
501
+ /**
502
+ * 2D Perlin noise (-1 to 1)
503
+ */
504
+ perlin2d: (x: number, y: number): number => {
505
+ return noise.perlin3d(x, y, 0);
506
+ },
507
+
508
+ /**
509
+ * Fractal Brownian Motion (fBm)
510
+ */
511
+ fbm: (x: number, y: number, z: number, octaves: number = 6, lacunarity: number = 2, gain: number = 0.5): number => {
512
+ let value = 0;
513
+ let amplitude = 1;
514
+ let frequency = 1;
515
+ let maxValue = 0;
516
+
517
+ for (let i = 0; i < octaves; i++) {
518
+ value += amplitude * noise.perlin3d(x * frequency, y * frequency, z * frequency);
519
+ maxValue += amplitude;
520
+ amplitude *= gain;
521
+ frequency *= lacunarity;
522
+ }
523
+
524
+ return value / maxValue;
525
+ },
526
+
527
+ /**
528
+ * Simplex-like noise (approximation using perlin)
529
+ */
530
+ simplex: (x: number, y: number): number => {
531
+ return noise.perlin2d(x * 1.2, y * 1.2);
532
+ },
533
+
534
+ /**
535
+ * Worley/Cellular noise
536
+ */
537
+ worley: (x: number, y: number, z: number): number => {
538
+ const xi = Math.floor(x);
539
+ const yi = Math.floor(y);
540
+ const zi = Math.floor(z);
541
+
542
+ let minDist = Infinity;
543
+
544
+ for (let i = -1; i <= 1; i++) {
545
+ for (let j = -1; j <= 1; j++) {
546
+ for (let k = -1; k <= 1; k++) {
547
+ const cellX = xi + i;
548
+ const cellY = yi + j;
549
+ const cellZ = zi + k;
550
+
551
+ // Pseudo-random point in cell
552
+ const hash = perm[(perm[(perm[cellX & 255] + cellY) & 255] + cellZ) & 255];
553
+ const px = cellX + (hash / 255);
554
+ const py = cellY + ((hash * 7) % 255) / 255;
555
+ const pz = cellZ + ((hash * 13) % 255) / 255;
556
+
557
+ const dist = (x - px) * (x - px) + (y - py) * (y - py) + (z - pz) * (z - pz);
558
+ minDist = Math.min(minDist, dist);
559
+ }
560
+ }
561
+ }
562
+
563
+ return Math.sqrt(minDist);
564
+ },
565
+ };
566
+
567
+ // =============================================================================
568
+ // RANDOM
569
+ // =============================================================================
570
+
571
+ export const random = {
572
+ /**
573
+ * Random float between 0 and 1
574
+ */
575
+ float: (): number => Math.random(),
576
+
577
+ /**
578
+ * Random float in range [min, max)
579
+ */
580
+ range: (min: number, max: number): number => min + Math.random() * (max - min),
581
+
582
+ /**
583
+ * Random integer in range [min, max]
584
+ */
585
+ int: (min: number, max: number): number => Math.floor(min + Math.random() * (max - min + 1)),
586
+
587
+ /**
588
+ * Random boolean with probability
589
+ */
590
+ bool: (probability: number = 0.5): boolean => Math.random() < probability,
591
+
592
+ /**
593
+ * Random element from array
594
+ */
595
+ pick: <T>(array: T[]): T => array[Math.floor(Math.random() * array.length)],
596
+
597
+ /**
598
+ * Shuffle array (Fisher-Yates)
599
+ */
600
+ shuffle: <T>(array: T[]): T[] => {
601
+ const result = [...array];
602
+ for (let i = result.length - 1; i > 0; i--) {
603
+ const j = Math.floor(Math.random() * (i + 1));
604
+ [result[i], result[j]] = [result[j], result[i]];
605
+ }
606
+ return result;
607
+ },
608
+
609
+ /**
610
+ * Random Vec3 in unit sphere
611
+ */
612
+ insideUnitSphere: (): Vec3 => {
613
+ const theta = Math.random() * TAU;
614
+ const phi = Math.acos(2 * Math.random() - 1);
615
+ const r = Math.cbrt(Math.random());
616
+ return {
617
+ x: r * Math.sin(phi) * Math.cos(theta),
618
+ y: r * Math.sin(phi) * Math.sin(theta),
619
+ z: r * Math.cos(phi),
620
+ };
621
+ },
622
+
623
+ /**
624
+ * Random Vec3 on unit sphere surface
625
+ */
626
+ onUnitSphere: (): Vec3 => {
627
+ const theta = Math.random() * TAU;
628
+ const phi = Math.acos(2 * Math.random() - 1);
629
+ return {
630
+ x: Math.sin(phi) * Math.cos(theta),
631
+ y: Math.sin(phi) * Math.sin(theta),
632
+ z: Math.cos(phi),
633
+ };
634
+ },
635
+
636
+ /**
637
+ * Random Vec2 in unit circle
638
+ */
639
+ insideUnitCircle: (): Vec2 => {
640
+ const theta = Math.random() * TAU;
641
+ const r = Math.sqrt(Math.random());
642
+ return {
643
+ x: r * Math.cos(theta),
644
+ y: r * Math.sin(theta),
645
+ };
646
+ },
647
+
648
+ /**
649
+ * Seeded random number generator
650
+ */
651
+ seeded: (seed: number) => {
652
+ let s = seed;
653
+ return () => {
654
+ s = (s * 1103515245 + 12345) & 0x7fffffff;
655
+ return s / 0x7fffffff;
656
+ };
657
+ },
658
+ };