@guinetik/gcanvas 1.0.2 → 1.0.4

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 (217) hide show
  1. package/dist/gcanvas.es.js +25656 -0
  2. package/dist/gcanvas.es.min.js +1 -0
  3. package/dist/gcanvas.umd.js +1 -0
  4. package/dist/gcanvas.umd.min.js +1 -0
  5. package/package.json +23 -6
  6. package/src/game/objects/index.js +1 -0
  7. package/src/game/objects/spritesheet.js +260 -0
  8. package/src/game/ui/theme.js +6 -0
  9. package/src/io/keys.js +9 -1
  10. package/src/math/boolean.js +481 -0
  11. package/src/math/index.js +1 -0
  12. package/.github/workflows/release.yaml +0 -70
  13. package/.jshintrc +0 -4
  14. package/.vscode/settings.json +0 -22
  15. package/CLAUDE.md +0 -310
  16. package/blackhole.jpg +0 -0
  17. package/demo.png +0 -0
  18. package/demos/CNAME +0 -1
  19. package/demos/animations.html +0 -31
  20. package/demos/basic.html +0 -38
  21. package/demos/baskara.html +0 -31
  22. package/demos/bezier.html +0 -35
  23. package/demos/beziersignature.html +0 -29
  24. package/demos/blackhole.html +0 -28
  25. package/demos/blob.html +0 -35
  26. package/demos/coordinates.html +0 -698
  27. package/demos/cube3d.html +0 -23
  28. package/demos/demos.css +0 -303
  29. package/demos/dino.html +0 -42
  30. package/demos/easing.html +0 -28
  31. package/demos/events.html +0 -195
  32. package/demos/fluent.html +0 -647
  33. package/demos/fluid-simple.html +0 -22
  34. package/demos/fluid.html +0 -37
  35. package/demos/fractals.html +0 -36
  36. package/demos/gameobjects.html +0 -626
  37. package/demos/genart.html +0 -26
  38. package/demos/gendream.html +0 -26
  39. package/demos/group.html +0 -36
  40. package/demos/home.html +0 -587
  41. package/demos/index.html +0 -376
  42. package/demos/isometric.html +0 -34
  43. package/demos/js/animations.js +0 -452
  44. package/demos/js/basic.js +0 -204
  45. package/demos/js/baskara.js +0 -751
  46. package/demos/js/bezier.js +0 -692
  47. package/demos/js/beziersignature.js +0 -241
  48. package/demos/js/blackhole/accretiondisk.obj.js +0 -379
  49. package/demos/js/blackhole/blackhole.obj.js +0 -318
  50. package/demos/js/blackhole/index.js +0 -409
  51. package/demos/js/blackhole/particle.js +0 -56
  52. package/demos/js/blackhole/starfield.obj.js +0 -218
  53. package/demos/js/blob.js +0 -2276
  54. package/demos/js/coordinates.js +0 -840
  55. package/demos/js/cube3d.js +0 -789
  56. package/demos/js/dino.js +0 -1420
  57. package/demos/js/easing.js +0 -477
  58. package/demos/js/fluent.js +0 -183
  59. package/demos/js/fluid-simple.js +0 -253
  60. package/demos/js/fluid.js +0 -527
  61. package/demos/js/fractals.js +0 -931
  62. package/demos/js/fractalworker.js +0 -93
  63. package/demos/js/gameobjects.js +0 -176
  64. package/demos/js/genart.js +0 -268
  65. package/demos/js/gendream.js +0 -209
  66. package/demos/js/group.js +0 -140
  67. package/demos/js/info-toggle.js +0 -25
  68. package/demos/js/isometric.js +0 -863
  69. package/demos/js/kerr.js +0 -1556
  70. package/demos/js/lavalamp.js +0 -590
  71. package/demos/js/layout.js +0 -354
  72. package/demos/js/mondrian.js +0 -285
  73. package/demos/js/opacity.js +0 -275
  74. package/demos/js/painter.js +0 -484
  75. package/demos/js/particles-showcase.js +0 -514
  76. package/demos/js/particles.js +0 -299
  77. package/demos/js/patterns.js +0 -397
  78. package/demos/js/penrose/artifact.js +0 -69
  79. package/demos/js/penrose/blackhole.js +0 -121
  80. package/demos/js/penrose/constants.js +0 -73
  81. package/demos/js/penrose/game.js +0 -943
  82. package/demos/js/penrose/lore.js +0 -278
  83. package/demos/js/penrose/penrosescene.js +0 -892
  84. package/demos/js/penrose/ship.js +0 -216
  85. package/demos/js/penrose/sounds.js +0 -211
  86. package/demos/js/penrose/voidparticle.js +0 -55
  87. package/demos/js/penrose/voidscene.js +0 -258
  88. package/demos/js/penrose/voidship.js +0 -144
  89. package/demos/js/penrose/wormhole.js +0 -46
  90. package/demos/js/pipeline.js +0 -555
  91. package/demos/js/plane3d.js +0 -256
  92. package/demos/js/platformer.js +0 -1579
  93. package/demos/js/scene.js +0 -304
  94. package/demos/js/scenes.js +0 -320
  95. package/demos/js/schrodinger.js +0 -410
  96. package/demos/js/schwarzschild.js +0 -1023
  97. package/demos/js/shapes.js +0 -628
  98. package/demos/js/space/alien.js +0 -171
  99. package/demos/js/space/boom.js +0 -98
  100. package/demos/js/space/boss.js +0 -353
  101. package/demos/js/space/buff.js +0 -73
  102. package/demos/js/space/bullet.js +0 -102
  103. package/demos/js/space/constants.js +0 -85
  104. package/demos/js/space/game.js +0 -1884
  105. package/demos/js/space/hud.js +0 -112
  106. package/demos/js/space/laserbeam.js +0 -179
  107. package/demos/js/space/lightning.js +0 -277
  108. package/demos/js/space/minion.js +0 -192
  109. package/demos/js/space/missile.js +0 -212
  110. package/demos/js/space/player.js +0 -430
  111. package/demos/js/space/powerup.js +0 -90
  112. package/demos/js/space/starfield.js +0 -58
  113. package/demos/js/space/starpower.js +0 -90
  114. package/demos/js/spacetime.js +0 -559
  115. package/demos/js/sphere3d.js +0 -229
  116. package/demos/js/sprite.js +0 -473
  117. package/demos/js/svgtween.js +0 -204
  118. package/demos/js/tde/accretiondisk.js +0 -471
  119. package/demos/js/tde/blackhole.js +0 -219
  120. package/demos/js/tde/blackholescene.js +0 -209
  121. package/demos/js/tde/config.js +0 -59
  122. package/demos/js/tde/index.js +0 -820
  123. package/demos/js/tde/jets.js +0 -290
  124. package/demos/js/tde/lensedstarfield.js +0 -154
  125. package/demos/js/tde/tdestar.js +0 -297
  126. package/demos/js/tde/tidalstream.js +0 -372
  127. package/demos/js/tde_old/blackhole.obj.js +0 -354
  128. package/demos/js/tde_old/debris.obj.js +0 -791
  129. package/demos/js/tde_old/flare.obj.js +0 -239
  130. package/demos/js/tde_old/index.js +0 -448
  131. package/demos/js/tde_old/star.obj.js +0 -812
  132. package/demos/js/tiles.js +0 -312
  133. package/demos/js/tweendemo.js +0 -79
  134. package/demos/js/visibility.js +0 -102
  135. package/demos/kerr.html +0 -28
  136. package/demos/lavalamp.html +0 -27
  137. package/demos/layouts.html +0 -37
  138. package/demos/logo.svg +0 -4
  139. package/demos/loop.html +0 -84
  140. package/demos/mondrian.html +0 -32
  141. package/demos/og_image.png +0 -0
  142. package/demos/opacity.html +0 -36
  143. package/demos/painter.html +0 -39
  144. package/demos/particles-showcase.html +0 -28
  145. package/demos/particles.html +0 -24
  146. package/demos/patterns.html +0 -33
  147. package/demos/penrose-game.html +0 -31
  148. package/demos/pipeline.html +0 -737
  149. package/demos/plane3d.html +0 -24
  150. package/demos/platformer.html +0 -43
  151. package/demos/scene.html +0 -33
  152. package/demos/scenes.html +0 -96
  153. package/demos/schrodinger.html +0 -27
  154. package/demos/schwarzschild.html +0 -27
  155. package/demos/shapes.html +0 -16
  156. package/demos/space.html +0 -85
  157. package/demos/spacetime.html +0 -27
  158. package/demos/sphere3d.html +0 -24
  159. package/demos/sprite.html +0 -18
  160. package/demos/svgtween.html +0 -29
  161. package/demos/tde.html +0 -28
  162. package/demos/tiles.html +0 -28
  163. package/demos/transforms.html +0 -400
  164. package/demos/tween.html +0 -45
  165. package/demos/visibility.html +0 -33
  166. package/docs/README.md +0 -230
  167. package/docs/api/FluidSystem.md +0 -173
  168. package/docs/concepts/architecture-overview.md +0 -204
  169. package/docs/concepts/coordinate-system.md +0 -384
  170. package/docs/concepts/lifecycle.md +0 -255
  171. package/docs/concepts/rendering-pipeline.md +0 -279
  172. package/docs/concepts/shapes-vs-gameobjects.md +0 -187
  173. package/docs/concepts/tde-zorder.md +0 -106
  174. package/docs/concepts/two-layer-architecture.md +0 -229
  175. package/docs/fluid-dynamics.md +0 -99
  176. package/docs/getting-started/first-game.md +0 -354
  177. package/docs/getting-started/hello-world.md +0 -269
  178. package/docs/getting-started/installation.md +0 -175
  179. package/docs/modules/collision/README.md +0 -453
  180. package/docs/modules/fluent/README.md +0 -1075
  181. package/docs/modules/game/README.md +0 -303
  182. package/docs/modules/isometric-camera.md +0 -210
  183. package/docs/modules/isometric.md +0 -275
  184. package/docs/modules/painter/README.md +0 -328
  185. package/docs/modules/particle/README.md +0 -559
  186. package/docs/modules/shapes/README.md +0 -221
  187. package/docs/modules/shapes/base/euclidian.md +0 -123
  188. package/docs/modules/shapes/base/geometry2d.md +0 -204
  189. package/docs/modules/shapes/base/renderable.md +0 -215
  190. package/docs/modules/shapes/base/shape.md +0 -262
  191. package/docs/modules/shapes/base/transformable.md +0 -243
  192. package/docs/modules/shapes/hierarchy.md +0 -218
  193. package/docs/modules/state/README.md +0 -577
  194. package/docs/modules/util/README.md +0 -99
  195. package/docs/modules/util/camera3d.md +0 -412
  196. package/docs/modules/util/scene3d.md +0 -395
  197. package/index.html +0 -17
  198. package/jsdoc.json +0 -50
  199. package/scripts/build-demo.js +0 -69
  200. package/scripts/bundle4llm.js +0 -276
  201. package/scripts/clearconsole.js +0 -48
  202. package/test/math/orbital.test.js +0 -61
  203. package/test/math/tensor.test.js +0 -114
  204. package/test/particle/emitter.test.js +0 -204
  205. package/test/particle/particle-system.test.js +0 -310
  206. package/test/particle/particle.test.js +0 -116
  207. package/test/particle/updaters.test.js +0 -386
  208. package/test/setup.js +0 -120
  209. package/test/shapes/euclidian.test.js +0 -44
  210. package/test/shapes/geometry.test.js +0 -86
  211. package/test/shapes/group.test.js +0 -86
  212. package/test/shapes/rectangle.test.js +0 -64
  213. package/test/shapes/transform.test.js +0 -379
  214. package/test/util/camera3d.test.js +0 -428
  215. package/test/util/scene3d.test.js +0 -352
  216. package/vite.config.js +0 -50
  217. package/vitest.config.js +0 -13
@@ -1,276 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * bundle4llm
5
- * -------------
6
- * Create a comprehensive, single-file, import/export-free JavaScript bundle
7
- * ideal for feeding into LLMs without worrying about dependencies.
8
- *
9
- * Features:
10
- * - Recursively resolves and includes all exported modules
11
- * - Flattens directory structure
12
- * - Orders files by export order in index files
13
- * - Sanitizes code based on selected preset (defaults to ES2020)
14
- * - Skips index.js files from output (used only for ordering)
15
- * - Ensures comprehensive module inclusion
16
- * - Supports verbose logging with --verbose or -v
17
- * - Optional comment stripping with --strip-comments
18
- * - Accurate token estimation for various LLM models
19
- *
20
- * Usage:
21
- * bundle4llm --src ./src --out ./dist --file bundle.js --preset es2020 --strip-comments --model claude -v
22
- */
23
-
24
- import fs from 'fs';
25
- import path from 'path';
26
- import { argv } from 'process';
27
-
28
- const args = Object.fromEntries(
29
- argv.slice(2).map(arg => {
30
- const [key, val] = arg.replace(/^--?/, '').split('=');
31
- return [key, val ?? true];
32
- })
33
- );
34
-
35
- const srcDir = path.resolve(args.src || './src');
36
- const outDir = path.resolve(args.out || './dist');
37
- const outFile = args.file || 'llm-bundle.js';
38
- const verbose = args.v || args.verbose;
39
- const stripComments = args['strip-comments'] || false;
40
- const preset = args.preset || 'es2020';
41
- const model = args.model || 'claude'; // Default to Claude model
42
-
43
- // Deep file collection with recursive export resolution
44
- function collectModules(indexPath, fileMap = {}, visited = new Set(), depth = 0) {
45
- // Prevent infinite recursion
46
- if (depth > 10) return fileMap;
47
-
48
- // Prevent revisiting the same index file
49
- if (visited.has(indexPath)) return fileMap;
50
- visited.add(indexPath);
51
-
52
- // Read index file contents
53
- if (!fs.existsSync(indexPath)) return fileMap;
54
- const content = fs.readFileSync(indexPath, 'utf-8');
55
-
56
- // Find all export declarations
57
- const exportMatches = [
58
- ...content.matchAll(/export\s+(?:\*|{[^}]*})\s*from\s+['"](.+)['"]/g),
59
- ...content.matchAll(/export\s+{[^}]*}\s*from\s+['"](.+)['"]/g)
60
- ];
61
-
62
- // Resolve and process each exported module
63
- for (const match of exportMatches) {
64
- const rawPath = match[1];
65
- const modulePath = rawPath.startsWith('.')
66
- ? path.resolve(path.dirname(indexPath), rawPath)
67
- : path.resolve(srcDir, rawPath);
68
-
69
- // Normalize path
70
- const normalizedPath = modulePath.replace(/\.js$/, '');
71
- const jsPath = normalizedPath + '.js';
72
-
73
- // If it's a directory, look for its index file
74
- const indexInDir = path.join(normalizedPath, 'index.js');
75
-
76
- if (fs.existsSync(jsPath) && !fileMap[jsPath]) {
77
- // Add the module file
78
- fileMap[jsPath] = jsPath;
79
-
80
- // If it's a directory with an index, recursively collect its exports
81
- if (fs.existsSync(indexInDir)) {
82
- collectModules(indexInDir, fileMap, visited, depth + 1);
83
- }
84
- } else if (fs.existsSync(indexInDir)) {
85
- // Recursively collect exports from subdirectory index
86
- collectModules(indexInDir, fileMap, visited, depth + 1);
87
- }
88
- }
89
-
90
- return fileMap;
91
- }
92
-
93
- // Order modules based on index file export order
94
- function orderModules(srcDir) {
95
- const indexPath = path.join(srcDir, 'index.js');
96
- const fileMap = collectModules(indexPath);
97
-
98
- // Convert to array and sort
99
- return Object.values(fileMap)
100
- .filter(filePath => !filePath.endsWith('index.js'));
101
- }
102
-
103
- // Sanitization presets
104
- const sanitizationPresets = {
105
- // ES2020 sanitization - strips imports and optionally comments
106
- es2020: (code, options = {}) => {
107
- let result = code;
108
-
109
- // Strip import statements
110
- result = result
111
- .split('\n')
112
- .filter(line => !/^\s*import\b/.test(line))
113
- .join('\n');
114
-
115
- // Optionally strip comments
116
- if (options.stripComments) {
117
- // Strip block comments
118
- result = result.replace(/\/\*[\s\S]*?\*\//g, '');
119
- // Strip line comments
120
- result = result.replace(/\/\/.*$/gm, '');
121
- }
122
-
123
- return result.trim();
124
- },
125
-
126
- // ES2015 sanitization - more conservative approach
127
- es2015: (code, options = {}) => {
128
- let result = code;
129
-
130
- // Strip import statements
131
- result = result
132
- .split('\n')
133
- .filter(line => !/^\s*import\b/.test(line))
134
- .join('\n');
135
-
136
- // Optionally strip comments
137
- if (options.stripComments) {
138
- // Strip block comments
139
- result = result.replace(/\/\*[\s\S]*?\*\//g, '');
140
- // Strip line comments
141
- result = result.replace(/\/\/.*$/gm, '');
142
- }
143
-
144
- return result.trim();
145
- },
146
-
147
- // Minimal sanitization - only strips imports
148
- minimal: (code, options = {}) => {
149
- let result = code
150
- .split('\n')
151
- .filter(line => !/^\s*import\b/.test(line))
152
- .join('\n');
153
-
154
- return result.trim();
155
- }
156
- };
157
-
158
- // Generic sanitize function that uses the selected preset
159
- function sanitizeCode(code, presetName, options = {}) {
160
- const sanitizer = sanitizationPresets[presetName] || sanitizationPresets.es2020;
161
- return sanitizer(code, options);
162
- }
163
-
164
- /**
165
- * Improved token estimation function supporting multiple LLM models
166
- *
167
- * @param {string} text - The text to estimate tokens for
168
- * @param {string} model - Model identifier ('claude', 'gpt3', 'gpt4', etc.)
169
- * @return {number} Estimated token count
170
- */
171
- function estimateTokens(text, model = 'claude') {
172
- // Basic character counting
173
- const charCount = text.length;
174
- const wordCount = text.split(/\s+/).filter(Boolean).length;
175
-
176
- // Model-specific token ratio estimations
177
- const modelRatios = {
178
- // Anthropic Claude models
179
- 'claude': {
180
- charRatio: 3.5, // ~3.5 chars per token for Claude
181
- wordRatio: 0.75, // ~0.75 words per token for Claude
182
- },
183
- // OpenAI models
184
- 'gpt3': {
185
- charRatio: 4.0, // ~4 chars per token for GPT-3
186
- wordRatio: 0.75, // ~0.75 words per token for GPT-3
187
- },
188
- 'gpt4': {
189
- charRatio: 3.8, // ~3.8 chars per token for GPT-4
190
- wordRatio: 0.7, // ~0.7 words per token for GPT-4
191
- },
192
- // Default fallback
193
- 'default': {
194
- charRatio: 4.0,
195
- wordRatio: 0.75,
196
- }
197
- };
198
-
199
- // Get the appropriate ratio for the model
200
- const ratio = modelRatios[model] || modelRatios.default;
201
-
202
- // Estimate based on character count and word count, using an average of both methods
203
- const charBasedEstimate = Math.ceil(charCount / ratio.charRatio);
204
- const wordBasedEstimate = Math.ceil(wordCount / ratio.wordRatio);
205
-
206
- // Language features that tend to increase token count
207
- const codeFeatures = {
208
- symbols: (text.match(/[{}[\]()<>:;,."'`~!@#$%^&*+=|\\/?-]/g) || []).length,
209
- indentation: (text.match(/^ +/gm) || []).length,
210
- camelCase: (text.match(/[a-z][A-Z]/g) || []).length,
211
- };
212
-
213
- // Adjustment for code-specific features (symbols, indentation, camelCase)
214
- const codeAdjustment = codeFeatures.symbols * 0.1 +
215
- codeFeatures.indentation * 0.05 +
216
- codeFeatures.camelCase * 0.2;
217
-
218
- // Calculate weighted average with more weight on char-based for code
219
- const weightedEstimate = (charBasedEstimate * 0.7) + (wordBasedEstimate * 0.3);
220
-
221
- // Add code-specific adjustment
222
- const finalEstimate = Math.ceil(weightedEstimate + codeAdjustment);
223
-
224
- return finalEstimate;
225
- }
226
-
227
- // Main bundling function
228
- function bundleLLMBuild(srcDir, outDir, outFile, options = {}) {
229
- const orderedFiles = orderModules(srcDir);
230
- const { verbose, stripComments, preset, model } = options;
231
-
232
- let output = '/**\n * bundle4llm Output\n';
233
- output += ` * Generated: ${new Date().toISOString()}\n`;
234
- output += ` * Source: ${srcDir}\n`;
235
- output += ` * Preset: ${preset}\n`;
236
- if (stripComments) output += ` * Comments: Stripped\n`;
237
- output += ` */\n\n`;
238
-
239
- let currentDir = '';
240
- for (const file of orderedFiles) {
241
- const fileDir = path.dirname(file).replace(srcDir, '');
242
- if (fileDir !== currentDir) {
243
- currentDir = fileDir;
244
- output += `\n// =========================================\n`;
245
- output += `// DIRECTORY: ${currentDir || '/'}\n`;
246
- output += `// =========================================\n\n`;
247
- }
248
-
249
- let contents = fs.readFileSync(file, 'utf-8');
250
- contents = sanitizeCode(contents, preset, { stripComments });
251
-
252
- if (verbose) console.log('📦 Including:', file);
253
- output += contents + '\n\n';
254
- }
255
-
256
- // Ensure output directory exists
257
- fs.mkdirSync(outDir, { recursive: true });
258
-
259
- // Write the bundled file
260
- const fullOutputPath = path.join(outDir, outFile);
261
- fs.writeFileSync(fullOutputPath, output);
262
-
263
- const totalTokens = estimateTokens(output, model).toLocaleString();
264
- console.log(`✅ Created LLM build → ${fullOutputPath}`);
265
- console.log(`📊 Total modules included: ${orderedFiles.length}`);
266
- console.log(`🔧 Preset: ${preset}${stripComments ? ', comments stripped' : ''}`);
267
- console.log(`🔍 Estimated tokens (${model}): ${totalTokens}`);
268
- }
269
-
270
- // Execute the bundle
271
- bundleLLMBuild(srcDir, outDir, outFile, {
272
- verbose,
273
- stripComments,
274
- preset,
275
- model
276
- });
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
-
6
- const targetDir = process.argv[2] || "./src"; // default to ./src
7
-
8
- function isLineCommented(line) {
9
- return /^\s*\/\/.*console\.log/.test(line);
10
- }
11
-
12
- function isInsideBlockComment(lines, index) {
13
- for (let i = index; i >= 0; i--) {
14
- if (lines[i].includes("*/")) break;
15
- if (lines[i].includes("/*")) return true;
16
- }
17
- return false;
18
- }
19
-
20
- function processFile(filePath) {
21
- const content = fs.readFileSync(filePath, "utf8");
22
- const lines = content.split("\n");
23
- const modified = lines.map((line, i) => {
24
- if (
25
- line.includes("console.log") &&
26
- !isLineCommented(line) &&
27
- !isInsideBlockComment(lines, i)
28
- ) {
29
- return line.replace(/(.*?)(console\.log.*)/, "$1// $2");
30
- }
31
- return line;
32
- });
33
- fs.writeFileSync(filePath, modified.join("\n"), "utf8");
34
- console.log(`✔ Commented console.log in: ${filePath}`);
35
- }
36
-
37
- function walkDir(dir) {
38
- fs.readdirSync(dir).forEach((file) => {
39
- const fullPath = path.join(dir, file);
40
- if (fs.statSync(fullPath).isDirectory()) {
41
- walkDir(fullPath);
42
- } else if (/\.(js|ts|jsx|tsx)$/.test(fullPath)) {
43
- processFile(fullPath);
44
- }
45
- });
46
- }
47
-
48
- walkDir(targetDir);
@@ -1,61 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { decayingOrbitalRadius, getTerminalTrajectory } from "../../src/math/orbital";
3
-
4
- describe("Orbital Math Utilities", () => {
5
- describe("decayingOrbitalRadius", () => {
6
- it("should return the initial radius when t=0", () => {
7
- const r0 = 100;
8
- const decay = 0.5;
9
- const r = decayingOrbitalRadius(r0, decay, 0);
10
- expect(r).toBe(r0);
11
- });
12
-
13
- it("should decay the radius over time", () => {
14
- const r0 = 100;
15
- const decay = 0.5;
16
- const r1 = decayingOrbitalRadius(r0, decay, 1);
17
- const r2 = decayingOrbitalRadius(r0, decay, 2);
18
-
19
- expect(r1).toBeLessThan(r0);
20
- expect(r2).toBeLessThan(r1);
21
- expect(r1).toBeCloseTo(100 * Math.exp(-0.5), 5);
22
- });
23
-
24
- it("should handle zero decay factor", () => {
25
- const r0 = 100;
26
- const r = decayingOrbitalRadius(r0, 0, 10);
27
- expect(r).toBe(r0);
28
- });
29
- });
30
-
31
- describe("getTerminalTrajectory", () => {
32
- it("should return start position at progress=0", () => {
33
- const start = { x: 100, y: 50, z: 25 };
34
- const pos = getTerminalTrajectory(start.x, start.y, start.z, 0);
35
- expect(pos).toEqual(start);
36
- });
37
-
38
- it("should return origin at progress=1", () => {
39
- const start = { x: 100, y: 50, z: 25 };
40
- const pos = getTerminalTrajectory(start.x, start.y, start.z, 1);
41
- expect(pos).toEqual({ x: 0, y: 0, z: 0 });
42
- });
43
-
44
- it("should interpolate linearly by default", () => {
45
- const start = { x: 100, y: 100, z: 100 };
46
- const pos = getTerminalTrajectory(start.x, start.y, start.z, 0.5);
47
- expect(pos).toEqual({ x: 50, y: 50, z: 50 });
48
- });
49
-
50
- it("should apply easing function if provided", () => {
51
- const start = { x: 100, y: 100, z: 100 };
52
- const easeInQuad = (t) => t * t;
53
- const pos = getTerminalTrajectory(start.x, start.y, start.z, 0.5, easeInQuad);
54
- // 0.5 * 0.5 = 0.25
55
- // 100 * (1 - 0.25) = 75
56
- expect(pos.x).toBe(75);
57
- expect(pos.y).toBe(75);
58
- expect(pos.z).toBe(75);
59
- });
60
- });
61
- });
@@ -1,114 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { Tensor } from "../../src/math/tensor";
3
-
4
- describe("Tensor class", () => {
5
- describe("Basic Operations", () => {
6
- it("should create a tensor from components", () => {
7
- const components = [
8
- [1, 2],
9
- [3, 4],
10
- ];
11
- const t = new Tensor(components);
12
- expect(t.get(0, 0)).toBe(1);
13
- expect(t.get(1, 1)).toBe(4);
14
- expect(t.dimension).toBe(2);
15
- });
16
-
17
- it("should be immutable", () => {
18
- const components = [[1, 2], [3, 4]];
19
- const t1 = new Tensor(components);
20
- const t2 = t1.set(0, 0, 9);
21
-
22
- expect(t1.get(0, 0)).toBe(1);
23
- expect(t2.get(0, 0)).toBe(9);
24
- expect(t1).not.toBe(t2);
25
- });
26
-
27
- it("should add two tensors", () => {
28
- const t1 = new Tensor([[1, 0], [0, 1]]);
29
- const t2 = new Tensor([[1, 2], [3, 4]]);
30
- const sum = t1.add(t2);
31
- expect(sum.get(0, 1)).toBe(2);
32
- expect(sum.get(1, 1)).toBe(5);
33
- });
34
-
35
- it("should scale a tensor", () => {
36
- const t = new Tensor([[1, 2], [3, 4]]);
37
- const scaled = t.scale(2);
38
- expect(scaled.get(0, 0)).toBe(2);
39
- expect(scaled.get(1, 1)).toBe(8);
40
- });
41
- });
42
-
43
- describe("Inversion & Determinant (Diagonal Optimizations)", () => {
44
- it("should compute inverse of a diagonal tensor (fast path)", () => {
45
- const t = Tensor.diagonal([-1, 0.5, 2, 4]);
46
- const inv = t.inverse();
47
- expect(inv.get(0, 0)).toBe(-1);
48
- expect(inv.get(1, 1)).toBe(2);
49
- expect(inv.get(2, 2)).toBe(0.5);
50
- expect(inv.get(3, 3)).toBe(0.25);
51
- });
52
-
53
- it("should compute determinant of a diagonal matrix (fast path)", () => {
54
- const t = Tensor.diagonal([-1, 1, 1, 1]);
55
- expect(t.determinant()).toBe(-1);
56
- });
57
-
58
- it("should compute inverse of a non-diagonal matrix (Gaussian elimination)", () => {
59
- const t = new Tensor([
60
- [1, 2],
61
- [3, 4]
62
- ]);
63
- const inv = t.inverse();
64
- // det = 1*4 - 2*3 = -2
65
- // inv = (-1/2) * [4, -2; -3, 1] = [-2, 1; 1.5, -0.5]
66
- expect(inv.get(0, 0)).toBeCloseTo(-2, 10);
67
- expect(inv.get(0, 1)).toBeCloseTo(1, 10);
68
- expect(inv.get(1, 0)).toBeCloseTo(1.5, 10);
69
- expect(inv.get(1, 1)).toBeCloseTo(-0.5, 10);
70
- });
71
- });
72
-
73
- describe("GR Metrics", () => {
74
- it("should create Schwarzschild metric", () => {
75
- const g = Tensor.schwarzschild(10, 2);
76
- expect(g.name).toBe("Schwarzschild");
77
- // factor = 1 - 2/10 = 0.8
78
- expect(g.get(0, 0)).toBe(-0.8);
79
- expect(g.get(1, 1)).toBeCloseTo(1 / 0.8, 5);
80
- });
81
-
82
- it("should create contravariant Schwarzschild metric directly", () => {
83
- const gInv = Tensor.schwarzschildContravariant(10, 2);
84
- expect(gInv.get(0, 0)).toBeCloseTo(-1 / 0.8, 5);
85
- expect(gInv.get(1, 1)).toBe(0.8);
86
- });
87
-
88
- it("should match numerical inverse for Kerr metric", () => {
89
- const r = 10, theta = Math.PI / 4, M = 1, a = 0.6;
90
- const g = Tensor.kerr(r, theta, M, a);
91
- const gInvNumerical = g.inverse();
92
- const gInvAnalytical = Tensor.kerrContravariant(r, theta, M, a);
93
-
94
- for (let i = 0; i < 4; i++) {
95
- for (let j = 0; j < 4; j++) {
96
- expect(gInvAnalytical.get(i, j)).toBeCloseTo(gInvNumerical.get(i, j), 8);
97
- }
98
- }
99
- });
100
-
101
- it("should compute analytical Christoffel symbols for Schwarzschild", () => {
102
- const r = 10, rs = 2, theta = Math.PI / 2;
103
- const pos = [0, r, theta, 0];
104
- pos._rs = rs;
105
-
106
- const gamma = Tensor.christoffel((p) => Tensor.schwarzschild(p[1], rs, p[2]), pos);
107
-
108
- // factor = 0.8
109
- // Gamma^t_tr = rs / (2r^2 * factor) = 2 / (200 * 0.8) = 2 / 160 = 0.0125
110
- expect(gamma[0][0][1]).toBeCloseTo(0.0125, 8);
111
- expect(gamma[0][1][0]).toBeCloseTo(0.0125, 8);
112
- });
113
- });
114
- });
@@ -1,204 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { ParticleEmitter } from "../../src/particle/emitter";
3
- import { Particle } from "../../src/particle/particle";
4
-
5
- describe("ParticleEmitter", () => {
6
- describe("constructor", () => {
7
- it("should initialize with default values", () => {
8
- const emitter = new ParticleEmitter();
9
-
10
- expect(emitter.rate).toBe(10);
11
- expect(emitter.position).toEqual({ x: 0, y: 0, z: 0 });
12
- expect(emitter.spread).toEqual({ x: 0, y: 0, z: 0 });
13
- expect(emitter.velocity).toEqual({ x: 0, y: 0, z: 0 });
14
- expect(emitter.velocitySpread).toEqual({ x: 0, y: 0, z: 0 });
15
- expect(emitter.lifetime).toEqual({ min: 1, max: 2 });
16
- expect(emitter.size).toEqual({ min: 1, max: 1 });
17
- expect(emitter.color).toEqual({ r: 255, g: 255, b: 255, a: 1 });
18
- expect(emitter.shape).toBe("circle");
19
- expect(emitter.active).toBe(true);
20
- });
21
-
22
- it("should accept custom options", () => {
23
- const emitter = new ParticleEmitter({
24
- rate: 50,
25
- position: { x: 100, y: 200 },
26
- velocity: { y: -100 },
27
- lifetime: { min: 0.5, max: 1.5 },
28
- size: { min: 2, max: 5 },
29
- color: { r: 255, g: 0, b: 0, a: 0.8 },
30
- shape: "square",
31
- active: false,
32
- });
33
-
34
- expect(emitter.rate).toBe(50);
35
- expect(emitter.position).toEqual({ x: 100, y: 200, z: 0 });
36
- expect(emitter.velocity).toEqual({ x: 0, y: -100, z: 0 });
37
- expect(emitter.lifetime).toEqual({ min: 0.5, max: 1.5 });
38
- expect(emitter.size).toEqual({ min: 2, max: 5 });
39
- expect(emitter.color).toEqual({ r: 255, g: 0, b: 0, a: 0.8 });
40
- expect(emitter.shape).toBe("square");
41
- expect(emitter.active).toBe(false);
42
- });
43
- });
44
-
45
- describe("emit", () => {
46
- it("should initialize particle with emitter settings", () => {
47
- const emitter = new ParticleEmitter({
48
- position: { x: 100, y: 200, z: 50 },
49
- velocity: { x: 10, y: -20, z: 5 },
50
- lifetime: { min: 2, max: 2 },
51
- size: { min: 3, max: 3 },
52
- color: { r: 128, g: 64, b: 32, a: 0.5 },
53
- shape: "triangle",
54
- });
55
-
56
- const p = new Particle();
57
- emitter.emit(p);
58
-
59
- expect(p.x).toBe(100);
60
- expect(p.y).toBe(200);
61
- expect(p.z).toBe(50);
62
- expect(p.vx).toBe(10);
63
- expect(p.vy).toBe(-20);
64
- expect(p.vz).toBe(5);
65
- expect(p.lifetime).toBe(2);
66
- expect(p.size).toBe(3);
67
- expect(p.color).toEqual({ r: 128, g: 64, b: 32, a: 0.5 });
68
- expect(p.shape).toBe("triangle");
69
- expect(p.age).toBe(0);
70
- expect(p.alive).toBe(true);
71
- });
72
-
73
- it("should apply position spread", () => {
74
- const emitter = new ParticleEmitter({
75
- position: { x: 0, y: 0, z: 0 },
76
- spread: { x: 100, y: 100, z: 100 },
77
- });
78
-
79
- // Mock Math.random to return predictable values
80
- const mockRandom = vi.spyOn(Math, "random");
81
- mockRandom.mockReturnValue(0.5); // Mid-point = 0 spread
82
-
83
- const p = new Particle();
84
- emitter.emit(p);
85
-
86
- // With Math.random() = 0.5, spread = (0.5 - 0.5) * 2 * spread = 0
87
- expect(p.x).toBe(0);
88
- expect(p.y).toBe(0);
89
- expect(p.z).toBe(0);
90
-
91
- mockRandom.mockRestore();
92
- });
93
-
94
- it("should apply velocity spread", () => {
95
- const emitter = new ParticleEmitter({
96
- velocity: { x: 100 },
97
- velocitySpread: { x: 50 },
98
- });
99
-
100
- const mockRandom = vi.spyOn(Math, "random");
101
- mockRandom.mockReturnValue(0); // (0 - 0.5) * 2 * 50 = -50
102
-
103
- const p = new Particle();
104
- emitter.emit(p);
105
-
106
- expect(p.vx).toBe(50); // 100 + (-50)
107
-
108
- mockRandom.mockRestore();
109
- });
110
-
111
- it("should randomize lifetime within range", () => {
112
- const emitter = new ParticleEmitter({
113
- lifetime: { min: 1, max: 3 },
114
- });
115
-
116
- const mockRandom = vi.spyOn(Math, "random");
117
- mockRandom.mockReturnValue(0.5);
118
-
119
- const p = new Particle();
120
- emitter.emit(p);
121
-
122
- expect(p.lifetime).toBe(2); // 1 + 0.5 * (3 - 1)
123
-
124
- mockRandom.mockRestore();
125
- });
126
-
127
- it("should randomize size within range", () => {
128
- const emitter = new ParticleEmitter({
129
- size: { min: 2, max: 10 },
130
- });
131
-
132
- const mockRandom = vi.spyOn(Math, "random");
133
- mockRandom.mockReturnValue(0.25);
134
-
135
- const p = new Particle();
136
- emitter.emit(p);
137
-
138
- expect(p.size).toBe(4); // 2 + 0.25 * (10 - 2)
139
-
140
- mockRandom.mockRestore();
141
- });
142
- });
143
-
144
- describe("update", () => {
145
- it("should return 0 when inactive", () => {
146
- const emitter = new ParticleEmitter({ rate: 100, active: false });
147
-
148
- expect(emitter.update(1)).toBe(0);
149
- });
150
-
151
- it("should return 0 when rate is 0", () => {
152
- const emitter = new ParticleEmitter({ rate: 0 });
153
-
154
- expect(emitter.update(1)).toBe(0);
155
- });
156
-
157
- it("should emit particles based on rate", () => {
158
- const emitter = new ParticleEmitter({ rate: 10 }); // 10 particles/second
159
-
160
- // 0.2 seconds should spawn 2 particles
161
- const count = emitter.update(0.2);
162
-
163
- expect(count).toBe(2);
164
- });
165
-
166
- it("should accumulate time between frames", () => {
167
- const emitter = new ParticleEmitter({ rate: 10 }); // interval = 0.1s
168
-
169
- // First frame: 0.05s (not enough for a particle)
170
- expect(emitter.update(0.05)).toBe(0);
171
-
172
- // Second frame: 0.05s more (total 0.1s = 1 particle)
173
- expect(emitter.update(0.05)).toBe(1);
174
- });
175
-
176
- it("should handle high frame rates correctly", () => {
177
- const emitter = new ParticleEmitter({ rate: 60 });
178
-
179
- // One frame at 60fps = ~0.0167s
180
- // At 60 particles/second, interval = 0.0167s, so 1 particle per frame
181
- let total = 0;
182
- for (let i = 0; i < 60; i++) {
183
- total += emitter.update(1 / 60);
184
- }
185
-
186
- expect(total).toBe(60);
187
- });
188
- });
189
-
190
- describe("reset", () => {
191
- it("should reset the emission timer", () => {
192
- const emitter = new ParticleEmitter({ rate: 10 });
193
-
194
- // Accumulate some time
195
- emitter.update(0.05);
196
- expect(emitter._timer).toBeGreaterThan(0);
197
-
198
- // Reset
199
- emitter.reset();
200
-
201
- expect(emitter._timer).toBe(0);
202
- });
203
- });
204
- });