nova64 0.2.4 → 0.2.6

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 (140) hide show
  1. package/README.md +25 -8
  2. package/bin/nova64.js +165 -0
  3. package/dist/assets/console-CY_kygm3.js +14 -0
  4. package/dist/assets/console-CY_kygm3.js.map +1 -0
  5. package/dist/assets/main-l0sNRNKZ.js.map +1 -0
  6. package/dist/assets/sky/studio/nx.png +0 -0
  7. package/dist/assets/sky/studio/ny.png +0 -0
  8. package/dist/assets/sky/studio/nz.png +0 -0
  9. package/dist/assets/sky/studio/px.png +0 -0
  10. package/dist/assets/sky/studio/py.png +0 -0
  11. package/dist/assets/sky/studio/pz.png +0 -0
  12. package/dist/assets/vanilla-Dcuy32gi.js +2 -0
  13. package/dist/assets/vanilla-Dcuy32gi.js.map +1 -0
  14. package/dist/console.html +899 -0
  15. package/dist/docs/BENCHMARK.md +77 -0
  16. package/dist/docs/CHEATSHEET.md +255 -0
  17. package/dist/docs/EFFECTS_API_GUIDE.md +577 -0
  18. package/dist/docs/EFFECTS_QUICK_REFERENCE.md +331 -0
  19. package/dist/docs/FONT_CHARACTER_REFERENCE.md +219 -0
  20. package/dist/docs/FREE_GLB_ASSETS.md +330 -0
  21. package/dist/docs/FULLSCREEN_BUTTON_FEATURE.md +296 -0
  22. package/dist/docs/GAMEPAD_SUPPORT.md +348 -0
  23. package/dist/docs/GAME_IMPROVEMENTS.md +278 -0
  24. package/dist/docs/GAME_QUALITY_STATUS.md +300 -0
  25. package/dist/docs/MIGRATION_GUIDE.md +553 -0
  26. package/dist/docs/NOVA64_3D_API.md +356 -0
  27. package/dist/docs/NOVA64_API_REFERENCE.md +1406 -0
  28. package/dist/docs/NOVA64_UI_API.md +503 -0
  29. package/dist/docs/UI_SYSTEM_SUMMARY.md +445 -0
  30. package/dist/docs/VOXEL_ENGINE_GUIDE.md +662 -0
  31. package/dist/docs/VOXEL_QUICK_REFERENCE.md +386 -0
  32. package/dist/docs/api-3d.html +750 -0
  33. package/dist/docs/api-effects.html +385 -0
  34. package/dist/docs/api-improvements.md +121 -0
  35. package/dist/docs/api-skybox.html +407 -0
  36. package/dist/docs/api-sprites.html +321 -0
  37. package/dist/docs/api-voxel.html +337 -0
  38. package/dist/docs/api.html +543 -0
  39. package/dist/docs/assets.html +306 -0
  40. package/dist/docs/audio.html +340 -0
  41. package/dist/docs/blogs.html +286 -0
  42. package/dist/docs/collision.html +316 -0
  43. package/dist/docs/console.html +247 -0
  44. package/dist/docs/editor.html +297 -0
  45. package/dist/docs/font.html +247 -0
  46. package/dist/docs/framebuffer.html +247 -0
  47. package/dist/docs/fullscreen-button.html +297 -0
  48. package/dist/docs/gpu-systems.html +247 -0
  49. package/dist/docs/index.html +580 -0
  50. package/dist/docs/input.html +491 -0
  51. package/dist/docs/physics.html +311 -0
  52. package/dist/docs/screens.html +311 -0
  53. package/dist/docs/storage.html +311 -0
  54. package/dist/docs/textinput.html +332 -0
  55. package/dist/docs/ui.html +488 -0
  56. package/dist/examples/3d-advanced/code.js +695 -0
  57. package/dist/examples/adventure-comic-3d/code.js +342 -0
  58. package/dist/examples/audio-lab/code.js +150 -0
  59. package/dist/examples/boids-flocking/code.js +270 -0
  60. package/dist/examples/crystal-cathedral-3d/code.js +706 -0
  61. package/dist/examples/cyberpunk-city-3d/code.js +1383 -0
  62. package/dist/examples/demoscene/README.md +192 -0
  63. package/dist/examples/demoscene/code.js +1081 -0
  64. package/dist/examples/demoscene/meta.json +21 -0
  65. package/dist/examples/dungeon-crawler-3d/code.js +1117 -0
  66. package/dist/examples/f-zero-nova-3d/code.js +865 -0
  67. package/dist/examples/f-zero-nova-3d/code_old.js +1555 -0
  68. package/dist/examples/fps-demo-3d/code.js +744 -0
  69. package/dist/examples/game-of-life-3d/code.js +338 -0
  70. package/dist/examples/generative-art/code.js +632 -0
  71. package/dist/examples/hello-3d/code.js +325 -0
  72. package/dist/examples/hello-skybox/code.js +183 -0
  73. package/dist/examples/hello-world/code.js +19 -0
  74. package/dist/examples/input-showcase/code.js +109 -0
  75. package/dist/examples/instancing-demo/code.js +315 -0
  76. package/dist/examples/minecraft-demo/code.js +387 -0
  77. package/dist/examples/model-viewer-3d/code.js +114 -0
  78. package/dist/examples/mystical-realm-3d/code.js +1203 -0
  79. package/dist/examples/nature-explorer-3d/code.js +1318 -0
  80. package/dist/examples/particles-demo/code.js +522 -0
  81. package/dist/examples/pbr-showcase/code.js +140 -0
  82. package/dist/examples/physics-demo-3d/code.js +948 -0
  83. package/dist/examples/screen-demo/code.js +267 -0
  84. package/dist/examples/shooter-demo-3d/code.js +1286 -0
  85. package/dist/examples/space-combat-3d/IMPLEMENTATION_SUMMARY.md +109 -0
  86. package/dist/examples/space-combat-3d/README.md +135 -0
  87. package/dist/examples/space-combat-3d/code.js +1332 -0
  88. package/dist/examples/space-harrier-3d/code.js +923 -0
  89. package/dist/examples/star-fox-nova-3d/code.js +1116 -0
  90. package/dist/examples/star-fox-nova-3d/code_backup.js +410 -0
  91. package/dist/examples/star-fox-nova-3d/code_broken.js +1821 -0
  92. package/dist/examples/storage-quest/code.js +209 -0
  93. package/dist/examples/strider-demo-3d/IMPROVEMENT_OPTIONS.md +285 -0
  94. package/dist/examples/strider-demo-3d/cache-test.html +132 -0
  95. package/dist/examples/strider-demo-3d/code-fixed.js +582 -0
  96. package/dist/examples/strider-demo-3d/code-old.js +1537 -0
  97. package/dist/examples/strider-demo-3d/code.js +1462 -0
  98. package/dist/examples/strider-demo-3d/code.js.bak2 +1169 -0
  99. package/dist/examples/strider-demo-3d/fix-game.sh +53 -0
  100. package/dist/examples/super-plumber-64/README.md +128 -0
  101. package/dist/examples/super-plumber-64/code.js +1185 -0
  102. package/dist/examples/super-plumber-64/index.html +88 -0
  103. package/dist/examples/test-2d-overlay/code.js +32 -0
  104. package/dist/examples/test-font/code.js +51 -0
  105. package/dist/examples/test-minimal/code.js +21 -0
  106. package/dist/examples/ui-demo/code.js +306 -0
  107. package/dist/examples/wing-commander-space/README.md +180 -0
  108. package/dist/examples/wing-commander-space/code.js +1285 -0
  109. package/dist/examples/wizardry-3d/CHANGELOG.md +366 -0
  110. package/dist/examples/wizardry-3d/code.js +3928 -0
  111. package/dist/index.html +666 -0
  112. package/dist/os9-shell/assets/index-DIHfrTaW.css +1 -0
  113. package/dist/os9-shell/assets/index-KchE_ngx.js +483 -0
  114. package/dist/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  115. package/dist/os9-shell/index.html +23 -0
  116. package/dist/os9-shell/nova-icon.svg +12 -0
  117. package/index.html +6 -1
  118. package/package.json +37 -32
  119. package/public/assets/sky/studio/nx.png +0 -0
  120. package/public/assets/sky/studio/ny.png +0 -0
  121. package/public/assets/sky/studio/nz.png +0 -0
  122. package/public/assets/sky/studio/px.png +0 -0
  123. package/public/assets/sky/studio/py.png +0 -0
  124. package/public/assets/sky/studio/pz.png +0 -0
  125. package/public/os9-shell/assets/index-KchE_ngx.js +483 -0
  126. package/public/os9-shell/assets/index-KchE_ngx.js.map +1 -0
  127. package/public/os9-shell/index.html +10 -1
  128. package/runtime/api-2d.js +301 -21
  129. package/runtime/api-3d/pbr.js +45 -1
  130. package/runtime/api-3d.js +1 -0
  131. package/runtime/api-effects.js +90 -3
  132. package/runtime/api-gameutils.js +476 -0
  133. package/runtime/api-generative.js +610 -0
  134. package/runtime/api-skybox.js +54 -0
  135. package/runtime/api-voxel.js +139 -28
  136. package/runtime/gpu-threejs.js +13 -9
  137. package/runtime/ui.js +2 -2
  138. package/src/main.js +24 -1
  139. package/public/os9-shell/assets/index-B1Uvacma.js +0 -32825
  140. package/public/os9-shell/assets/index-B1Uvacma.js.map +0 -1
@@ -0,0 +1,610 @@
1
+ // runtime/api-generative.js
2
+ // Nova64 Generative Art API — Processing.js-style creative coding primitives
3
+ //
4
+ // Provides: Perlin noise, ellipse, arc, bezier curves, quadratic curves,
5
+ // matrix stack (push/pop/translate/rotate/scale), color modes (RGB/HSB),
6
+ // easing functions, flow fields, and particle emitters.
7
+ //
8
+ // Exposed globals: noise, noiseSeed, noiseDetail, ellipse, arc, bezier,
9
+ // quadCurve, pushMatrix, popMatrix, translate, rotate, scale2d,
10
+ // colorMode, hsb, lerpColor, noiseMap, flowField, TWO_PI, HALF_PI,
11
+ // QUARTER_PI, ease, smoothstep, frameCount
12
+
13
+ import { rgba8 } from './api.js';
14
+
15
+ export function generativeApi(gpu) {
16
+ const fb = gpu.getFramebuffer();
17
+
18
+ // ─── Constants ──────────────────────────────────────────────────────────────
19
+ const TWO_PI = Math.PI * 2;
20
+ const HALF_PI = Math.PI / 2;
21
+ const QUARTER_PI = Math.PI / 4;
22
+ let _frameCount = 0;
23
+
24
+ function _advanceFrame() {
25
+ _frameCount++;
26
+ }
27
+
28
+ // ─── Internal pixel writer (respects transform stack) ───────────────────────
29
+ function _pset(x, y, c) {
30
+ // Apply current transformation
31
+ const [tx, ty] = _applyTransform(x, y);
32
+ const ix = Math.round(tx);
33
+ const iy = Math.round(ty);
34
+ if (ix < 0 || iy < 0 || ix >= fb.width || iy >= fb.height) return;
35
+ const { r, g, b, a } = _unpackColor(c);
36
+ fb.pset(ix, iy, r, g, b, a);
37
+ }
38
+
39
+ function _unpackColor(c) {
40
+ if (typeof c === 'bigint') {
41
+ return {
42
+ r: Number((c >> 48n) & 0xffffn),
43
+ g: Number((c >> 32n) & 0xffffn),
44
+ b: Number((c >> 16n) & 0xffffn),
45
+ a: Number(c & 0xffffn),
46
+ };
47
+ }
48
+ const bc = BigInt(Math.floor(c));
49
+ return {
50
+ r: Number((bc >> 48n) & 0xffffn),
51
+ g: Number((bc >> 32n) & 0xffffn),
52
+ b: Number((bc >> 16n) & 0xffffn),
53
+ a: Number(bc & 0xffffn),
54
+ };
55
+ }
56
+
57
+ // ─── Perlin Noise (classic improved, 2D/3D) ────────────────────────────────
58
+
59
+ // Permutation table (seeded)
60
+ let _perm = new Uint8Array(512);
61
+ // eslint-disable-next-line no-unused-vars
62
+ let _noiseSeed = 0;
63
+ let _noiseOctaves = 4;
64
+ let _noiseFalloff = 0.5;
65
+
66
+ function _initPerm(seed) {
67
+ const p = new Uint8Array(256);
68
+ for (let i = 0; i < 256; i++) p[i] = i;
69
+ // Fisher-Yates shuffle with seed
70
+ let s = seed | 0;
71
+ for (let i = 255; i > 0; i--) {
72
+ s = (s * 1664525 + 1013904223) & 0x7fffffff;
73
+ const j = s % (i + 1);
74
+ [p[i], p[j]] = [p[j], p[i]];
75
+ }
76
+ for (let i = 0; i < 512; i++) _perm[i] = p[i & 255];
77
+ }
78
+
79
+ _initPerm(0);
80
+
81
+ const _grad3 = [
82
+ [1, 1, 0],
83
+ [-1, 1, 0],
84
+ [1, -1, 0],
85
+ [-1, -1, 0],
86
+ [1, 0, 1],
87
+ [-1, 0, 1],
88
+ [1, 0, -1],
89
+ [-1, 0, -1],
90
+ [0, 1, 1],
91
+ [0, -1, 1],
92
+ [0, 1, -1],
93
+ [0, -1, -1],
94
+ ];
95
+
96
+ function _fade(t) {
97
+ return t * t * t * (t * (t * 6 - 15) + 10);
98
+ }
99
+
100
+ function _perlin3(x, y, z) {
101
+ const X = Math.floor(x) & 255;
102
+ const Y = Math.floor(y) & 255;
103
+ const Z = Math.floor(z) & 255;
104
+ const xf = x - Math.floor(x);
105
+ const yf = y - Math.floor(y);
106
+ const zf = z - Math.floor(z);
107
+ const u = _fade(xf);
108
+ const v = _fade(yf);
109
+ const w = _fade(zf);
110
+
111
+ const A = _perm[X] + Y;
112
+ const AA = _perm[A] + Z;
113
+ const AB = _perm[A + 1] + Z;
114
+ const B = _perm[X + 1] + Y;
115
+ const BA = _perm[B] + Z;
116
+ const BB = _perm[B + 1] + Z;
117
+
118
+ const g = (hash, dx, dy, dz) => {
119
+ const gr = _grad3[hash % 12];
120
+ return gr[0] * dx + gr[1] * dy + gr[2] * dz;
121
+ };
122
+
123
+ return _lerp(
124
+ _lerp(
125
+ _lerp(g(_perm[AA], xf, yf, zf), g(_perm[BA], xf - 1, yf, zf), u),
126
+ _lerp(g(_perm[AB], xf, yf - 1, zf), g(_perm[BB], xf - 1, yf - 1, zf), u),
127
+ v
128
+ ),
129
+ _lerp(
130
+ _lerp(g(_perm[AA + 1], xf, yf, zf - 1), g(_perm[BA + 1], xf - 1, yf, zf - 1), u),
131
+ _lerp(g(_perm[AB + 1], xf, yf - 1, zf - 1), g(_perm[BB + 1], xf - 1, yf - 1, zf - 1), u),
132
+ v
133
+ ),
134
+ w
135
+ );
136
+ }
137
+
138
+ function _lerp(a, b, t) {
139
+ return a + (b - a) * t;
140
+ }
141
+
142
+ /**
143
+ * Perlin noise — returns value in [0, 1] range (centered ~0.5).
144
+ * noise(x) — 1D, noise(x, y) — 2D, noise(x, y, z) — 3D.
145
+ * Uses octaves and falloff set by noiseDetail().
146
+ */
147
+ function noise(x, y = 0, z = 0) {
148
+ let total = 0;
149
+ let amp = 1;
150
+ let freq = 1;
151
+ let maxAmp = 0;
152
+ for (let i = 0; i < _noiseOctaves; i++) {
153
+ total += _perlin3(x * freq, y * freq, z * freq) * amp;
154
+ maxAmp += amp;
155
+ amp *= _noiseFalloff;
156
+ freq *= 2;
157
+ }
158
+ // Normalize to 0..1
159
+ return (total / maxAmp + 1) * 0.5;
160
+ }
161
+
162
+ /** Set noise seed (integer). */
163
+ function noiseSeed(seed) {
164
+ _noiseSeed = seed;
165
+ _initPerm(seed);
166
+ }
167
+
168
+ /** Set noise octaves and falloff. */
169
+ function noiseDetail(octaves = 4, falloff = 0.5) {
170
+ _noiseOctaves = Math.max(1, Math.min(8, octaves));
171
+ _noiseFalloff = Math.max(0, Math.min(1, falloff));
172
+ }
173
+
174
+ /**
175
+ * Generate a 2D noise map as Float32Array.
176
+ * noiseMap(w, h, scale, offsetX, offsetY)
177
+ */
178
+ function noiseMap(w, h, scale = 0.02, offsetX = 0, offsetY = 0) {
179
+ const data = new Float32Array(w * h);
180
+ for (let y = 0; y < h; y++) {
181
+ for (let x = 0; x < w; x++) {
182
+ data[y * w + x] = noise((x + offsetX) * scale, (y + offsetY) * scale);
183
+ }
184
+ }
185
+ return data;
186
+ }
187
+
188
+ // ─── Transformation Matrix Stack ───────────────────────────────────────────
189
+
190
+ // 2D affine: [a, b, tx, c, d, ty] — column-major-ish
191
+ // [a c tx] [x] [ax + cy + tx]
192
+ // [b d ty] × [y] = [bx + dy + ty]
193
+ // [0 0 1] [1] [1 ]
194
+ let _matStack = [];
195
+ let _mat = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
196
+
197
+ function _applyTransform(x, y) {
198
+ return [_mat.a * x + _mat.c * y + _mat.tx, _mat.b * x + _mat.d * y + _mat.ty];
199
+ }
200
+
201
+ /** Save current transform. */
202
+ function pushMatrix() {
203
+ _matStack.push({ ..._mat });
204
+ }
205
+
206
+ /** Restore previous transform. */
207
+ function popMatrix() {
208
+ if (_matStack.length > 0) {
209
+ _mat = _matStack.pop();
210
+ }
211
+ }
212
+
213
+ /** Translate the coordinate origin. */
214
+ function translate(x, y) {
215
+ _mat.tx += _mat.a * x + _mat.c * y;
216
+ _mat.ty += _mat.b * x + _mat.d * y;
217
+ }
218
+
219
+ /** Rotate coordinates by angle (radians). */
220
+ function rotate(angle) {
221
+ const cos = Math.cos(angle);
222
+ const sin = Math.sin(angle);
223
+ const { a, b, c, d } = _mat;
224
+ _mat.a = a * cos + c * sin;
225
+ _mat.b = b * cos + d * sin;
226
+ _mat.c = a * -sin + c * cos;
227
+ _mat.d = b * -sin + d * cos;
228
+ }
229
+
230
+ /** Scale coordinates. scale2d(s) for uniform, scale2d(sx, sy) for non-uniform. */
231
+ function scale2d(sx, sy) {
232
+ if (sy === undefined) sy = sx;
233
+ _mat.a *= sx;
234
+ _mat.b *= sx;
235
+ _mat.c *= sy;
236
+ _mat.d *= sy;
237
+ }
238
+
239
+ /** Reset transform to identity. */
240
+ function resetMatrix() {
241
+ _mat = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 };
242
+ _matStack = [];
243
+ }
244
+
245
+ // ─── Shapes ────────────────────────────────────────────────────────────────
246
+
247
+ /**
248
+ * Draw an ellipse. ellipse(cx, cy, rx, ry, color, fill).
249
+ * If ry is omitted, draws a circle.
250
+ */
251
+ function ellipse(cx, cy, rx, ry, color, fill = true) {
252
+ if (ry === undefined) ry = rx;
253
+ if (fill) {
254
+ // Scanline fill
255
+ for (let dy = -ry; dy <= ry; dy++) {
256
+ const halfW = Math.sqrt(Math.max(0, 1 - (dy * dy) / (ry * ry))) * rx;
257
+ for (let dx = -Math.ceil(halfW); dx <= Math.ceil(halfW); dx++) {
258
+ _pset(cx + dx, cy + dy, color);
259
+ }
260
+ }
261
+ } else {
262
+ // Outline using parametric
263
+ const steps = Math.max(32, Math.ceil(Math.max(rx, ry) * 4));
264
+ for (let i = 0; i < steps; i++) {
265
+ const a = (i / steps) * TWO_PI;
266
+ _pset(Math.round(cx + Math.cos(a) * rx), Math.round(cy + Math.sin(a) * ry), color);
267
+ }
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Draw an arc. arc(cx, cy, rx, ry, startAngle, endAngle, color, fill).
273
+ * Angles in radians. Draws clockwise from start to end.
274
+ */
275
+ function arc(cx, cy, rx, ry, startAngle, endAngle, color, fill = false) {
276
+ const steps = Math.max(32, Math.ceil(Math.max(rx, ry) * 4));
277
+ const range = endAngle - startAngle;
278
+ if (fill) {
279
+ // Pie-slice fill: draw filled triangles from center
280
+ for (let i = 0; i < steps; i++) {
281
+ const a1 = startAngle + (i / steps) * range;
282
+ const a2 = startAngle + ((i + 1) / steps) * range;
283
+ const x1 = cx + Math.cos(a1) * rx;
284
+ const y1 = cy + Math.sin(a1) * ry;
285
+ const x2 = cx + Math.cos(a2) * rx;
286
+ const y2 = cy + Math.sin(a2) * ry;
287
+ // Fill triangle cx,cy → x1,y1 → x2,y2
288
+ _fillTriangle(cx, cy, x1, y1, x2, y2, color);
289
+ }
290
+ } else {
291
+ for (let i = 0; i <= steps; i++) {
292
+ const a = startAngle + (i / steps) * range;
293
+ _pset(Math.round(cx + Math.cos(a) * rx), Math.round(cy + Math.sin(a) * ry), color);
294
+ }
295
+ }
296
+ }
297
+
298
+ function _fillTriangle(x0, y0, x1, y1, x2, y2, color) {
299
+ // Simple scanline triangle fill
300
+ let pts = [
301
+ { x: x0, y: y0 },
302
+ { x: x1, y: y1 },
303
+ { x: x2, y: y2 },
304
+ ];
305
+ pts.sort((a, b) => a.y - b.y);
306
+
307
+ const [p0, p1, p2] = pts;
308
+ const totalHeight = p2.y - p0.y;
309
+ if (totalHeight < 1) return;
310
+
311
+ for (let y = Math.ceil(p0.y); y <= Math.floor(p2.y); y++) {
312
+ const secondHalf = y > p1.y || p1.y === p0.y;
313
+ const segH = secondHalf ? p2.y - p1.y : p1.y - p0.y;
314
+ if (segH < 0.5) continue;
315
+
316
+ const alpha = (y - p0.y) / totalHeight;
317
+ const beta = secondHalf ? (y - p1.y) / segH : (y - p0.y) / segH;
318
+
319
+ let xa = p0.x + (p2.x - p0.x) * alpha;
320
+ let xb = secondHalf ? p1.x + (p2.x - p1.x) * beta : p0.x + (p1.x - p0.x) * beta;
321
+ if (xa > xb) [xa, xb] = [xb, xa];
322
+
323
+ for (let x = Math.ceil(xa); x <= Math.floor(xb); x++) {
324
+ _pset(x, y, color);
325
+ }
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Draw a cubic bezier curve.
331
+ * bezier(x0, y0, cx0, cy0, cx1, cy1, x1, y1, color, detail)
332
+ */
333
+ function bezier(x0, y0, cx0, cy0, cx1, cy1, x1, y1, color, detail = 60) {
334
+ let prevX = x0,
335
+ prevY = y0;
336
+ for (let i = 1; i <= detail; i++) {
337
+ const t = i / detail;
338
+ const t2 = t * t;
339
+ const t3 = t2 * t;
340
+ const mt = 1 - t;
341
+ const mt2 = mt * mt;
342
+ const mt3 = mt2 * mt;
343
+ const x = mt3 * x0 + 3 * mt2 * t * cx0 + 3 * mt * t2 * cx1 + t3 * x1;
344
+ const y = mt3 * y0 + 3 * mt2 * t * cy0 + 3 * mt * t2 * cy1 + t3 * y1;
345
+ _drawLine(prevX, prevY, x, y, color);
346
+ prevX = x;
347
+ prevY = y;
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Draw a quadratic curve.
353
+ * quadCurve(x0, y0, cx, cy, x1, y1, color, detail)
354
+ */
355
+ function quadCurve(x0, y0, cx, cy, x1, y1, color, detail = 40) {
356
+ let prevX = x0,
357
+ prevY = y0;
358
+ for (let i = 1; i <= detail; i++) {
359
+ const t = i / detail;
360
+ const mt = 1 - t;
361
+ const x = mt * mt * x0 + 2 * mt * t * cx + t * t * x1;
362
+ const y = mt * mt * y0 + 2 * mt * t * cy + t * t * y1;
363
+ _drawLine(prevX, prevY, x, y, color);
364
+ prevX = x;
365
+ prevY = y;
366
+ }
367
+ }
368
+
369
+ /** Internal Bresenham line for curve segments */
370
+ function _drawLine(x0, y0, x1, y1, color) {
371
+ x0 = Math.round(x0);
372
+ y0 = Math.round(y0);
373
+ x1 = Math.round(x1);
374
+ y1 = Math.round(y1);
375
+ const dx = Math.abs(x1 - x0);
376
+ const dy = Math.abs(y1 - y0);
377
+ const sx = x0 < x1 ? 1 : -1;
378
+ const sy = y0 < y1 ? 1 : -1;
379
+ let err = dx - dy;
380
+ // eslint-disable-next-line no-constant-condition
381
+ while (true) {
382
+ _pset(x0, y0, color);
383
+ if (x0 === x1 && y0 === y1) break;
384
+ const e2 = 2 * err;
385
+ if (e2 > -dy) {
386
+ err -= dy;
387
+ x0 += sx;
388
+ }
389
+ if (e2 < dx) {
390
+ err += dx;
391
+ y0 += sy;
392
+ }
393
+ }
394
+ }
395
+
396
+ // ─── Color Modes ───────────────────────────────────────────────────────────
397
+
398
+ let _colorModeType = 'rgb'; // 'rgb' or 'hsb'
399
+ let _colorMax = [255, 255, 255, 255]; // max values for each channel
400
+
401
+ /**
402
+ * Set color mode. colorMode('rgb') or colorMode('hsb').
403
+ * Optional max values: colorMode('hsb', 360, 100, 100, 255)
404
+ */
405
+ function colorMode(mode, max1 = 255, max2, max3, maxA) {
406
+ _colorModeType = mode.toLowerCase();
407
+ if (max2 === undefined) {
408
+ _colorMax = [max1, max1, max1, max1];
409
+ } else {
410
+ _colorMax = [max1, max2 || max1, max3 || max1, maxA || 255];
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Create a color using current color mode.
416
+ * color(v1, v2, v3, alpha)
417
+ */
418
+ function color(v1, v2, v3, a) {
419
+ if (a === undefined) a = _colorMax[3];
420
+
421
+ // Normalize to 0-1
422
+ const n1 = v1 / _colorMax[0];
423
+ const n2 = v2 / _colorMax[1];
424
+ const n3 = v3 / _colorMax[2];
425
+ const na = a / _colorMax[3];
426
+
427
+ if (_colorModeType === 'hsb' || _colorModeType === 'hsv') {
428
+ // HSB to RGB
429
+ const h = (n1 * 360 + 360) % 360;
430
+ const s = Math.max(0, Math.min(1, n2));
431
+ const b = Math.max(0, Math.min(1, n3));
432
+ const c = b * s;
433
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
434
+ const m = b - c;
435
+ let r, g, bl;
436
+ if (h < 60) [r, g, bl] = [c, x, 0];
437
+ else if (h < 120) [r, g, bl] = [x, c, 0];
438
+ else if (h < 180) [r, g, bl] = [0, c, x];
439
+ else if (h < 240) [r, g, bl] = [0, x, c];
440
+ else if (h < 300) [r, g, bl] = [x, 0, c];
441
+ else [r, g, bl] = [c, 0, x];
442
+ return rgba8((r + m) * 255, (g + m) * 255, (bl + m) * 255, na * 255);
443
+ }
444
+
445
+ // RGB mode
446
+ return rgba8(n1 * 255, n2 * 255, n3 * 255, na * 255);
447
+ }
448
+
449
+ /**
450
+ * Create HSB color directly (convenience).
451
+ * hsb(hue, saturation, brightness, alpha)
452
+ * hue: 0-360, saturation: 0-1, brightness: 0-1, alpha: 0-255
453
+ */
454
+ function hsb(h, s = 1, b = 1, a = 255) {
455
+ h = ((h % 360) + 360) % 360;
456
+ const c = b * s;
457
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
458
+ const m = b - c;
459
+ let r, g, bl;
460
+ if (h < 60) [r, g, bl] = [c, x, 0];
461
+ else if (h < 120) [r, g, bl] = [x, c, 0];
462
+ else if (h < 180) [r, g, bl] = [0, c, x];
463
+ else if (h < 240) [r, g, bl] = [0, x, c];
464
+ else if (h < 300) [r, g, bl] = [x, 0, c];
465
+ else [r, g, bl] = [c, 0, x];
466
+ return rgba8((r + m) * 255, (g + m) * 255, (bl + m) * 255, a);
467
+ }
468
+
469
+ /**
470
+ * Interpolate between two colors. lerpColor(c1, c2, t).
471
+ * Works correctly regardless of color mode.
472
+ */
473
+ function lerpColor(c1, c2, t) {
474
+ const a = _unpackColor(c1);
475
+ const b = _unpackColor(c2);
476
+ return _packColor(
477
+ a.r + (b.r - a.r) * t,
478
+ a.g + (b.g - a.g) * t,
479
+ a.b + (b.b - a.b) * t,
480
+ a.a + (b.a - a.a) * t
481
+ );
482
+ }
483
+
484
+ function _packColor(r, g, b, a) {
485
+ return (
486
+ (BigInt(Math.round(r)) << 48n) |
487
+ (BigInt(Math.round(g)) << 32n) |
488
+ (BigInt(Math.round(b)) << 16n) |
489
+ BigInt(Math.round(a))
490
+ );
491
+ }
492
+
493
+ // ─── Easing Functions ──────────────────────────────────────────────────────
494
+
495
+ const _easings = {
496
+ linear: t => t,
497
+ easeInQuad: t => t * t,
498
+ easeOutQuad: t => t * (2 - t),
499
+ easeInOutQuad: t => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t),
500
+ easeInCubic: t => t * t * t,
501
+ easeOutCubic: t => --t * t * t + 1,
502
+ easeInOutCubic: t => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1),
503
+ easeInElastic: t =>
504
+ t === 0
505
+ ? 0
506
+ : t === 1
507
+ ? 1
508
+ : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * ((2 * Math.PI) / 3)),
509
+ easeOutElastic: t =>
510
+ t === 0
511
+ ? 0
512
+ : t === 1
513
+ ? 1
514
+ : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * ((2 * Math.PI) / 3)) + 1,
515
+ easeOutBounce: t => {
516
+ if (t < 1 / 2.75) return 7.5625 * t * t;
517
+ if (t < 2 / 2.75) return 7.5625 * (t -= 1.5 / 2.75) * t + 0.75;
518
+ if (t < 2.5 / 2.75) return 7.5625 * (t -= 2.25 / 2.75) * t + 0.9375;
519
+ return 7.5625 * (t -= 2.625 / 2.75) * t + 0.984375;
520
+ },
521
+ };
522
+
523
+ /**
524
+ * Apply an easing function. ease(t, type).
525
+ * Types: 'linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
526
+ * 'easeInCubic', 'easeOutCubic', 'easeInOutCubic',
527
+ * 'easeInElastic', 'easeOutElastic', 'easeOutBounce'
528
+ */
529
+ function ease(t, type = 'easeInOutQuad') {
530
+ const fn = _easings[type] || _easings.linear;
531
+ return fn(Math.max(0, Math.min(1, t)));
532
+ }
533
+
534
+ /** Hermite smoothstep interpolation. */
535
+ function smoothstep(edge0, edge1, x) {
536
+ const t = Math.max(0, Math.min(1, (x - edge0) / (edge1 - edge0)));
537
+ return t * t * (3 - 2 * t);
538
+ }
539
+
540
+ // ─── Flow Field ────────────────────────────────────────────────────────────
541
+
542
+ /**
543
+ * Generate a 2D flow field (array of angle values driven by noise).
544
+ * flowField(cols, rows, scale, time)
545
+ * Returns Float32Array of angles in radians.
546
+ */
547
+ function flowField(cols, rows, scale = 0.06, time = 0) {
548
+ const field = new Float32Array(cols * rows);
549
+ for (let y = 0; y < rows; y++) {
550
+ for (let x = 0; x < cols; x++) {
551
+ const n = noise(x * scale, y * scale, time);
552
+ field[y * cols + x] = n * TWO_PI * 2;
553
+ }
554
+ }
555
+ return field;
556
+ }
557
+
558
+ // ─── expose ────────────────────────────────────────────────────────────────
559
+
560
+ return {
561
+ _advanceFrame,
562
+ exposeTo(target) {
563
+ Object.assign(target, {
564
+ // Constants
565
+ TWO_PI,
566
+ HALF_PI,
567
+ QUARTER_PI,
568
+
569
+ // Noise
570
+ noise,
571
+ noiseSeed,
572
+ noiseDetail,
573
+ noiseMap,
574
+
575
+ // Shapes
576
+ ellipse,
577
+ arc,
578
+ bezier,
579
+ quadCurve,
580
+
581
+ // Transform stack
582
+ pushMatrix,
583
+ popMatrix,
584
+ translate,
585
+ rotate,
586
+ scale2d,
587
+ resetMatrix,
588
+
589
+ // Color
590
+ colorMode,
591
+ color,
592
+ hsb,
593
+ lerpColor,
594
+
595
+ // Math
596
+ ease,
597
+ smoothstep,
598
+
599
+ // Generators
600
+ flowField,
601
+ });
602
+
603
+ // frameCount as getter
604
+ Object.defineProperty(target, 'frameCount', {
605
+ get: () => _frameCount,
606
+ configurable: true,
607
+ });
608
+ },
609
+ };
610
+ }
@@ -83,6 +83,59 @@ export function skyboxApi(gpu) {
83
83
  gpu.scene.background = new THREE.Color(color);
84
84
  }
85
85
 
86
+ // ── Image-based (cube map) skybox ────────────────────────────────────────────
87
+ /**
88
+ * Load a cube-map skybox from six image URLs.
89
+ * The cube map is also wired up as the scene environment map so PBR metallic
90
+ * surfaces automatically reflect the sky.
91
+ *
92
+ * @param {string[]} urls - Six face URLs in order: [+X, -X, +Y, -Y, +Z, -Z]
93
+ * i.e. [right, left, top, bottom, front, back]
94
+ * @returns {Promise<THREE.CubeTexture>} Resolves when all six faces are loaded.
95
+ *
96
+ * @example
97
+ * await createImageSkybox([
98
+ * '/assets/sky/px.jpg', '/assets/sky/nx.jpg',
99
+ * '/assets/sky/py.jpg', '/assets/sky/ny.jpg',
100
+ * '/assets/sky/pz.jpg', '/assets/sky/nz.jpg',
101
+ * ]);
102
+ */
103
+ function createImageSkybox(urls) {
104
+ return new Promise((resolve, reject) => {
105
+ if (!Array.isArray(urls) || urls.length !== 6) {
106
+ reject(
107
+ new Error('createImageSkybox: requires exactly 6 face URLs [+X, -X, +Y, -Y, +Z, -Z]')
108
+ );
109
+ return;
110
+ }
111
+
112
+ _clearSky();
113
+
114
+ const loader = new THREE.CubeTextureLoader();
115
+ loader.load(
116
+ urls,
117
+ cubeTexture => {
118
+ cubeTexture.colorSpace = THREE.SRGBColorSpace;
119
+ gpu.scene.background = cubeTexture;
120
+
121
+ // Process through PMREMGenerator so metallic/PBR surfaces reflect the sky
122
+ try {
123
+ const pmrem = new THREE.PMREMGenerator(gpu.renderer);
124
+ pmrem.compileCubemapShader();
125
+ gpu.scene.environment = pmrem.fromCubemap(cubeTexture).texture;
126
+ pmrem.dispose();
127
+ } catch (_) {
128
+ // PMREM is optional; the skybox still renders without env reflections
129
+ }
130
+
131
+ resolve(cubeTexture);
132
+ },
133
+ undefined,
134
+ err => reject(err)
135
+ );
136
+ });
137
+ }
138
+
86
139
  // ── Shared gradient-sphere helper ────────────────────────────────────────────
87
140
  function _gradientSphere(topColor, bottomColor) {
88
141
  const geo = new THREE.SphereGeometry(800, 32, 32);
@@ -167,6 +220,7 @@ export function skyboxApi(gpu) {
167
220
  createSpaceSkybox,
168
221
  createGradientSkybox,
169
222
  createSolidSkybox,
223
+ createImageSkybox,
170
224
  animateSkybox,
171
225
  setSkyboxSpeed,
172
226
  enableSkyboxAutoAnimate,