@guinetik/gcanvas 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/.github/workflows/release.yaml +70 -0
  2. package/.jshintrc +4 -0
  3. package/.vscode/settings.json +22 -0
  4. package/CLAUDE.md +310 -0
  5. package/blackhole.jpg +0 -0
  6. package/demo.png +0 -0
  7. package/demos/CNAME +1 -0
  8. package/demos/animations.html +31 -0
  9. package/demos/basic.html +38 -0
  10. package/demos/baskara.html +31 -0
  11. package/demos/bezier.html +35 -0
  12. package/demos/beziersignature.html +29 -0
  13. package/demos/blackhole.html +28 -0
  14. package/demos/blob.html +35 -0
  15. package/demos/demos.css +289 -0
  16. package/demos/easing.html +28 -0
  17. package/demos/events.html +195 -0
  18. package/demos/fluent.html +647 -0
  19. package/demos/fractals.html +36 -0
  20. package/demos/genart.html +26 -0
  21. package/demos/gendream.html +26 -0
  22. package/demos/group.html +36 -0
  23. package/demos/home.html +587 -0
  24. package/demos/index.html +364 -0
  25. package/demos/isometric.html +34 -0
  26. package/demos/js/animations.js +452 -0
  27. package/demos/js/basic.js +204 -0
  28. package/demos/js/baskara.js +751 -0
  29. package/demos/js/bezier.js +692 -0
  30. package/demos/js/beziersignature.js +241 -0
  31. package/demos/js/blackhole/accretiondisk.obj.js +379 -0
  32. package/demos/js/blackhole/blackhole.obj.js +318 -0
  33. package/demos/js/blackhole/index.js +409 -0
  34. package/demos/js/blackhole/particle.js +56 -0
  35. package/demos/js/blackhole/starfield.obj.js +218 -0
  36. package/demos/js/blob.js +2263 -0
  37. package/demos/js/easing.js +477 -0
  38. package/demos/js/fluent.js +183 -0
  39. package/demos/js/fractals.js +931 -0
  40. package/demos/js/fractalworker.js +93 -0
  41. package/demos/js/genart.js +268 -0
  42. package/demos/js/gendream.js +209 -0
  43. package/demos/js/group.js +140 -0
  44. package/demos/js/info-toggle.js +25 -0
  45. package/demos/js/isometric.js +863 -0
  46. package/demos/js/kerr.js +1556 -0
  47. package/demos/js/lavalamp.js +590 -0
  48. package/demos/js/layout.js +354 -0
  49. package/demos/js/mondrian.js +285 -0
  50. package/demos/js/opacity.js +275 -0
  51. package/demos/js/painter.js +484 -0
  52. package/demos/js/particles-showcase.js +514 -0
  53. package/demos/js/particles.js +299 -0
  54. package/demos/js/patterns.js +397 -0
  55. package/demos/js/penrose/artifact.js +69 -0
  56. package/demos/js/penrose/blackhole.js +121 -0
  57. package/demos/js/penrose/constants.js +73 -0
  58. package/demos/js/penrose/game.js +943 -0
  59. package/demos/js/penrose/lore.js +278 -0
  60. package/demos/js/penrose/penrosescene.js +892 -0
  61. package/demos/js/penrose/ship.js +216 -0
  62. package/demos/js/penrose/sounds.js +211 -0
  63. package/demos/js/penrose/voidparticle.js +55 -0
  64. package/demos/js/penrose/voidscene.js +258 -0
  65. package/demos/js/penrose/voidship.js +144 -0
  66. package/demos/js/penrose/wormhole.js +46 -0
  67. package/demos/js/pipeline.js +555 -0
  68. package/demos/js/scene.js +304 -0
  69. package/demos/js/scenes.js +320 -0
  70. package/demos/js/schrodinger.js +410 -0
  71. package/demos/js/schwarzschild.js +1023 -0
  72. package/demos/js/shapes.js +628 -0
  73. package/demos/js/space/alien.js +171 -0
  74. package/demos/js/space/boom.js +98 -0
  75. package/demos/js/space/boss.js +353 -0
  76. package/demos/js/space/buff.js +73 -0
  77. package/demos/js/space/bullet.js +102 -0
  78. package/demos/js/space/constants.js +85 -0
  79. package/demos/js/space/game.js +1884 -0
  80. package/demos/js/space/hud.js +112 -0
  81. package/demos/js/space/laserbeam.js +179 -0
  82. package/demos/js/space/lightning.js +277 -0
  83. package/demos/js/space/minion.js +192 -0
  84. package/demos/js/space/missile.js +212 -0
  85. package/demos/js/space/player.js +430 -0
  86. package/demos/js/space/powerup.js +90 -0
  87. package/demos/js/space/starfield.js +58 -0
  88. package/demos/js/space/starpower.js +90 -0
  89. package/demos/js/spacetime.js +559 -0
  90. package/demos/js/svgtween.js +204 -0
  91. package/demos/js/tde/accretiondisk.js +418 -0
  92. package/demos/js/tde/blackhole.js +219 -0
  93. package/demos/js/tde/blackholescene.js +209 -0
  94. package/demos/js/tde/config.js +59 -0
  95. package/demos/js/tde/index.js +695 -0
  96. package/demos/js/tde/jets.js +290 -0
  97. package/demos/js/tde/lensedstarfield.js +147 -0
  98. package/demos/js/tde/tdestar.js +317 -0
  99. package/demos/js/tde/tidalstream.js +356 -0
  100. package/demos/js/tde_old/blackhole.obj.js +354 -0
  101. package/demos/js/tde_old/debris.obj.js +791 -0
  102. package/demos/js/tde_old/flare.obj.js +239 -0
  103. package/demos/js/tde_old/index.js +448 -0
  104. package/demos/js/tde_old/star.obj.js +812 -0
  105. package/demos/js/tiles.js +312 -0
  106. package/demos/js/tweendemo.js +79 -0
  107. package/demos/js/visibility.js +102 -0
  108. package/demos/kerr.html +28 -0
  109. package/demos/lavalamp.html +27 -0
  110. package/demos/layouts.html +37 -0
  111. package/demos/logo.svg +4 -0
  112. package/demos/loop.html +84 -0
  113. package/demos/mondrian.html +32 -0
  114. package/demos/og_image.png +0 -0
  115. package/demos/opacity.html +36 -0
  116. package/demos/painter.html +39 -0
  117. package/demos/particles-showcase.html +28 -0
  118. package/demos/particles.html +24 -0
  119. package/demos/patterns.html +33 -0
  120. package/demos/penrose-game.html +31 -0
  121. package/demos/pipeline.html +737 -0
  122. package/demos/scene.html +33 -0
  123. package/demos/scenes.html +96 -0
  124. package/demos/schrodinger.html +27 -0
  125. package/demos/schwarzschild.html +27 -0
  126. package/demos/shapes.html +16 -0
  127. package/demos/space.html +85 -0
  128. package/demos/spacetime.html +27 -0
  129. package/demos/svgtween.html +29 -0
  130. package/demos/tde.html +28 -0
  131. package/demos/tiles.html +28 -0
  132. package/demos/transforms.html +400 -0
  133. package/demos/tween.html +45 -0
  134. package/demos/visibility.html +33 -0
  135. package/disk_example.png +0 -0
  136. package/docs/README.md +222 -0
  137. package/docs/concepts/architecture-overview.md +204 -0
  138. package/docs/concepts/lifecycle.md +255 -0
  139. package/docs/concepts/rendering-pipeline.md +279 -0
  140. package/docs/concepts/tde-zorder.md +106 -0
  141. package/docs/concepts/two-layer-architecture.md +229 -0
  142. package/docs/getting-started/first-game.md +354 -0
  143. package/docs/getting-started/hello-world.md +269 -0
  144. package/docs/getting-started/installation.md +157 -0
  145. package/docs/modules/collision/README.md +453 -0
  146. package/docs/modules/fluent/README.md +1075 -0
  147. package/docs/modules/game/README.md +303 -0
  148. package/docs/modules/isometric-camera.md +210 -0
  149. package/docs/modules/isometric.md +275 -0
  150. package/docs/modules/painter/README.md +328 -0
  151. package/docs/modules/particle/README.md +559 -0
  152. package/docs/modules/shapes/README.md +221 -0
  153. package/docs/modules/shapes/base/euclidian.md +123 -0
  154. package/docs/modules/shapes/base/geometry2d.md +204 -0
  155. package/docs/modules/shapes/base/renderable.md +215 -0
  156. package/docs/modules/shapes/base/shape.md +262 -0
  157. package/docs/modules/shapes/base/transformable.md +243 -0
  158. package/docs/modules/shapes/hierarchy.md +218 -0
  159. package/docs/modules/state/README.md +577 -0
  160. package/docs/modules/util/README.md +99 -0
  161. package/docs/modules/util/camera3d.md +412 -0
  162. package/docs/modules/util/scene3d.md +395 -0
  163. package/index.html +17 -0
  164. package/jsdoc.json +50 -0
  165. package/package.json +55 -0
  166. package/readme.md +599 -0
  167. package/scripts/build-demo.js +69 -0
  168. package/scripts/bundle4llm.js +276 -0
  169. package/scripts/clearconsole.js +48 -0
  170. package/src/collision/collision-system.js +332 -0
  171. package/src/collision/collision.js +303 -0
  172. package/src/collision/index.js +10 -0
  173. package/src/fluent/fluent-game.js +430 -0
  174. package/src/fluent/fluent-go.js +1060 -0
  175. package/src/fluent/fluent-layer.js +152 -0
  176. package/src/fluent/fluent-scene.js +291 -0
  177. package/src/fluent/index.js +98 -0
  178. package/src/fluent/sketch.js +380 -0
  179. package/src/game/game.js +467 -0
  180. package/src/game/index.js +49 -0
  181. package/src/game/objects/go.js +220 -0
  182. package/src/game/objects/imagego.js +30 -0
  183. package/src/game/objects/index.js +54 -0
  184. package/src/game/objects/isometric-scene.js +260 -0
  185. package/src/game/objects/layoutscene.js +549 -0
  186. package/src/game/objects/scene.js +175 -0
  187. package/src/game/objects/scene3d.js +118 -0
  188. package/src/game/objects/text.js +221 -0
  189. package/src/game/objects/wrapper.js +232 -0
  190. package/src/game/pipeline.js +243 -0
  191. package/src/game/ui/button.js +396 -0
  192. package/src/game/ui/cursor.js +93 -0
  193. package/src/game/ui/fps.js +91 -0
  194. package/src/game/ui/index.js +5 -0
  195. package/src/game/ui/togglebutton.js +93 -0
  196. package/src/game/ui/tooltip.js +249 -0
  197. package/src/index.js +25 -0
  198. package/src/io/events.js +20 -0
  199. package/src/io/index.js +86 -0
  200. package/src/io/input.js +70 -0
  201. package/src/io/keys.js +152 -0
  202. package/src/io/mouse.js +61 -0
  203. package/src/io/touch.js +39 -0
  204. package/src/logger/debugtab.js +138 -0
  205. package/src/logger/index.js +3 -0
  206. package/src/logger/loggable.js +47 -0
  207. package/src/logger/logger.js +113 -0
  208. package/src/math/complex.js +37 -0
  209. package/src/math/constants.js +1 -0
  210. package/src/math/fractal.js +1271 -0
  211. package/src/math/gr.js +201 -0
  212. package/src/math/heat.js +202 -0
  213. package/src/math/index.js +12 -0
  214. package/src/math/noise.js +433 -0
  215. package/src/math/orbital.js +191 -0
  216. package/src/math/patterns.js +1339 -0
  217. package/src/math/penrose.js +259 -0
  218. package/src/math/quantum.js +115 -0
  219. package/src/math/random.js +195 -0
  220. package/src/math/tensor.js +1009 -0
  221. package/src/mixins/anchor.js +131 -0
  222. package/src/mixins/draggable.js +72 -0
  223. package/src/mixins/index.js +2 -0
  224. package/src/motion/bezier.js +132 -0
  225. package/src/motion/bounce.js +58 -0
  226. package/src/motion/easing.js +349 -0
  227. package/src/motion/float.js +130 -0
  228. package/src/motion/follow.js +125 -0
  229. package/src/motion/hop.js +52 -0
  230. package/src/motion/index.js +82 -0
  231. package/src/motion/motion.js +1124 -0
  232. package/src/motion/orbit.js +49 -0
  233. package/src/motion/oscillate.js +39 -0
  234. package/src/motion/parabolic.js +141 -0
  235. package/src/motion/patrol.js +147 -0
  236. package/src/motion/pendulum.js +48 -0
  237. package/src/motion/pulse.js +88 -0
  238. package/src/motion/shake.js +83 -0
  239. package/src/motion/spiral.js +144 -0
  240. package/src/motion/spring.js +150 -0
  241. package/src/motion/swing.js +47 -0
  242. package/src/motion/tween.js +92 -0
  243. package/src/motion/tweenetik.js +139 -0
  244. package/src/motion/waypoint.js +210 -0
  245. package/src/painter/index.js +8 -0
  246. package/src/painter/painter.colors.js +331 -0
  247. package/src/painter/painter.effects.js +230 -0
  248. package/src/painter/painter.img.js +229 -0
  249. package/src/painter/painter.js +295 -0
  250. package/src/painter/painter.lines.js +189 -0
  251. package/src/painter/painter.opacity.js +41 -0
  252. package/src/painter/painter.shapes.js +277 -0
  253. package/src/painter/painter.text.js +273 -0
  254. package/src/particle/emitter.js +124 -0
  255. package/src/particle/index.js +11 -0
  256. package/src/particle/particle-system.js +322 -0
  257. package/src/particle/particle.js +71 -0
  258. package/src/particle/updaters.js +170 -0
  259. package/src/shapes/arc.js +43 -0
  260. package/src/shapes/arrow.js +33 -0
  261. package/src/shapes/bezier.js +42 -0
  262. package/src/shapes/circle.js +62 -0
  263. package/src/shapes/clouds.js +56 -0
  264. package/src/shapes/cone.js +219 -0
  265. package/src/shapes/cross.js +70 -0
  266. package/src/shapes/cube.js +244 -0
  267. package/src/shapes/cylinder.js +254 -0
  268. package/src/shapes/diamond.js +48 -0
  269. package/src/shapes/euclidian.js +111 -0
  270. package/src/shapes/figure.js +115 -0
  271. package/src/shapes/geometry.js +220 -0
  272. package/src/shapes/group.js +375 -0
  273. package/src/shapes/heart.js +42 -0
  274. package/src/shapes/hexagon.js +26 -0
  275. package/src/shapes/image.js +192 -0
  276. package/src/shapes/index.js +111 -0
  277. package/src/shapes/line.js +29 -0
  278. package/src/shapes/pattern.js +90 -0
  279. package/src/shapes/pin.js +44 -0
  280. package/src/shapes/poly.js +31 -0
  281. package/src/shapes/prism.js +226 -0
  282. package/src/shapes/rect.js +35 -0
  283. package/src/shapes/renderable.js +333 -0
  284. package/src/shapes/ring.js +26 -0
  285. package/src/shapes/roundrect.js +95 -0
  286. package/src/shapes/shape.js +117 -0
  287. package/src/shapes/slice.js +26 -0
  288. package/src/shapes/sphere.js +314 -0
  289. package/src/shapes/sphere3d.js +537 -0
  290. package/src/shapes/square.js +15 -0
  291. package/src/shapes/star.js +99 -0
  292. package/src/shapes/svg.js +408 -0
  293. package/src/shapes/text.js +553 -0
  294. package/src/shapes/traceable.js +83 -0
  295. package/src/shapes/transform.js +357 -0
  296. package/src/shapes/transformable.js +172 -0
  297. package/src/shapes/triangle.js +26 -0
  298. package/src/sound/index.js +17 -0
  299. package/src/sound/sound.js +473 -0
  300. package/src/sound/synth.analyzer.js +149 -0
  301. package/src/sound/synth.effects.js +207 -0
  302. package/src/sound/synth.envelope.js +59 -0
  303. package/src/sound/synth.js +229 -0
  304. package/src/sound/synth.musical.js +160 -0
  305. package/src/sound/synth.noise.js +85 -0
  306. package/src/sound/synth.oscillators.js +293 -0
  307. package/src/state/index.js +10 -0
  308. package/src/state/state-machine.js +371 -0
  309. package/src/util/camera3d.js +438 -0
  310. package/src/util/index.js +6 -0
  311. package/src/util/isometric-camera.js +235 -0
  312. package/src/util/layout.js +317 -0
  313. package/src/util/position.js +147 -0
  314. package/src/util/tasks.js +47 -0
  315. package/src/util/zindex.js +287 -0
  316. package/src/webgl/index.js +9 -0
  317. package/src/webgl/shaders/sphere-shaders.js +994 -0
  318. package/src/webgl/webgl-renderer.js +388 -0
  319. package/tde.png +0 -0
  320. package/test/math/orbital.test.js +61 -0
  321. package/test/math/tensor.test.js +114 -0
  322. package/test/particle/emitter.test.js +204 -0
  323. package/test/particle/particle-system.test.js +310 -0
  324. package/test/particle/particle.test.js +116 -0
  325. package/test/particle/updaters.test.js +386 -0
  326. package/test/setup.js +120 -0
  327. package/test/shapes/euclidian.test.js +44 -0
  328. package/test/shapes/geometry.test.js +86 -0
  329. package/test/shapes/group.test.js +86 -0
  330. package/test/shapes/rectangle.test.js +64 -0
  331. package/test/shapes/transform.test.js +379 -0
  332. package/test/util/camera3d.test.js +428 -0
  333. package/test/util/scene3d.test.js +352 -0
  334. package/types/collision.d.ts +249 -0
  335. package/types/common.d.ts +155 -0
  336. package/types/game.d.ts +497 -0
  337. package/types/index.d.ts +309 -0
  338. package/types/io.d.ts +188 -0
  339. package/types/logger.d.ts +127 -0
  340. package/types/math.d.ts +268 -0
  341. package/types/mixins.d.ts +92 -0
  342. package/types/motion.d.ts +678 -0
  343. package/types/painter.d.ts +378 -0
  344. package/types/shapes.d.ts +864 -0
  345. package/types/sound.d.ts +672 -0
  346. package/types/state.d.ts +251 -0
  347. package/types/util.d.ts +253 -0
  348. package/vite.config.js +50 -0
  349. package/vitest.config.js +13 -0
@@ -0,0 +1,559 @@
1
+ /**
2
+ * Spacetime Curvature - Math & Physics Demo
3
+ *
4
+ * Visualization of how mass curves spacetime, based on Einstein's
5
+ * general relativity. The "rubber sheet" analogy where massive objects
6
+ * create wells/depressions in the fabric of spacetime.
7
+ *
8
+ * Uses gravitational potential: Φ(r) = -GM/r
9
+ * Grid deformation: y = Σ(-M_i / |r - r_i|)
10
+ */
11
+
12
+ import {
13
+ Game,
14
+ Painter,
15
+ Camera3D,
16
+ Text,
17
+ applyAnchor,
18
+ Position,
19
+ Scene,
20
+ verticalLayout,
21
+ applyLayout,
22
+ } from "../../src/index.js";
23
+
24
+ // Configuration
25
+ const CONFIG = {
26
+ // Grid parameters
27
+ gridSize: 20, // Grid extends from -gridSize to +gridSize
28
+ gridResolution: 40, // Number of grid lines
29
+ gridScale: 15, // Scale factor for grid spacing
30
+
31
+ // Physics - Gaussian well profile for smooth falloff to flat edges
32
+ wellDepth: 75, // Base depth (scaled by sqrt of mass)
33
+ wellWidth: 4.0, // Base width (scaled by mass)
34
+
35
+ // 3D view
36
+ rotationX: 0.7, // Initial tilt (looking down at grid)
37
+ rotationY: 0.3, // Initial rotation
38
+ perspective: 1000, // Perspective depth
39
+
40
+ // Stellar body
41
+ initialBody: { x: 0, z: 0, mass: 3.0, type: "blackhole" },
42
+
43
+ // Body properties by type
44
+ bodyTypes: {
45
+ blackhole: {
46
+ color: "#111",
47
+ glowColor: "rgba(100, 50, 150, 0.8)",
48
+ minMass: 2.0,
49
+ maxMass: 5.0,
50
+ },
51
+ star: {
52
+ color: "#ff8800",
53
+ glowColor: "rgba(255, 200, 50, 0.6)",
54
+ minMass: 0.5,
55
+ maxMass: 2.0,
56
+ },
57
+ neutron: {
58
+ color: "#88ccff",
59
+ glowColor: "rgba(150, 200, 255, 0.7)",
60
+ minMass: 1.5,
61
+ maxMass: 3.0,
62
+ },
63
+ },
64
+
65
+ // Visual
66
+ gridColor: "rgba(0, 180, 255, 0.4)",
67
+ gridHighlight: "rgba(100, 220, 255, 0.6)",
68
+ wellGradientStart: "rgba(80, 0, 120, 0.3)",
69
+ wellGradientEnd: "rgba(0, 100, 200, 0.1)",
70
+
71
+ // Animation
72
+ autoRotateSpeed: 0.15, // Auto-rotate speed (radians per second)
73
+ pulseSpeed: 2.0, // Glow pulse speed
74
+ wellPulseSpeed: 1.5, // Well breathing speed
75
+ wellPulseAmount: 0.08, // How much the well pulses (0-1)
76
+
77
+ // Orbiting body
78
+ orbitRadiusMultiplier: 2.0, // Orbit at this multiple of well width (sigma)
79
+ orbitSpeed: 0.8, // Base orbit speed (faster for heavier central mass)
80
+ orbiterSize: 4, // Size of orbiting body
81
+ orbiterColor: "#4af", // Color of orbiter
82
+ orbiterGlow: "rgba(100, 180, 255, 0.6)",
83
+ };
84
+
85
+ class SpacetimeDemo extends Game {
86
+ constructor(canvas) {
87
+ super(canvas);
88
+ this.backgroundColor = "#000008";
89
+ this.enableFluidSize();
90
+ }
91
+
92
+ init() {
93
+ super.init();
94
+ this.time = 0;
95
+
96
+ // Create 3D camera with mouse controls and auto-rotate
97
+ this.camera = new Camera3D({
98
+ rotationX: CONFIG.rotationX,
99
+ rotationY: CONFIG.rotationY,
100
+ perspective: CONFIG.perspective,
101
+ minRotationX: 0.2, // Don't allow looking from below
102
+ maxRotationX: 1.3, // Don't flip over
103
+ autoRotate: true,
104
+ autoRotateSpeed: CONFIG.autoRotateSpeed,
105
+ autoRotateAxis: "y",
106
+ });
107
+ this.camera.enableMouseControl(this.canvas);
108
+
109
+ // Initialize single stellar body
110
+ this.body = {
111
+ ...CONFIG.initialBody,
112
+ orbitPhase: Math.random() * Math.PI * 2,
113
+ };
114
+
115
+ // Initialize orbiting test particle
116
+ this.orbiterAngle = 0;
117
+
118
+ // Precompute grid vertex positions (flat)
119
+ this.initGrid();
120
+
121
+ // Click to add bodies
122
+ this.canvas.addEventListener("click", (e) => this.handleClick(e));
123
+
124
+ // Setup info panel
125
+ this.setupInfoPanel();
126
+ }
127
+
128
+ setupInfoPanel() {
129
+ this.infoPanel = new Scene(this, { x: 0, y: 0 });
130
+ applyAnchor(this.infoPanel, {
131
+ anchor: Position.TOP_CENTER,
132
+ anchorOffsetY: 150,
133
+ });
134
+ this.pipeline.add(this.infoPanel);
135
+
136
+ this.titleText = new Text(this, "Spacetime Curvature", {
137
+ font: "bold 16px monospace",
138
+ color: "#7af",
139
+ align: "center",
140
+ baseline: "middle",
141
+ });
142
+
143
+ this.equationText = new Text(
144
+ this,
145
+ "g\u03BC\u03BD = \u03B7\u03BC\u03BD + h\u03BC\u03BD | R\u03BC\u03BD - \u00BDRg\u03BC\u03BD = 8\u03C0GT\u03BC\u03BD",
146
+ {
147
+ font: "12px monospace",
148
+ color: "#888",
149
+ align: "center",
150
+ baseline: "middle",
151
+ },
152
+ );
153
+
154
+ this.statsText = new Text(this, "Blackhole | Mass: 3.0 M\u2609", {
155
+ font: "12px monospace",
156
+ color: "#6d8",
157
+ align: "center",
158
+ baseline: "middle",
159
+ });
160
+
161
+ const textItems = [this.titleText, this.equationText, this.statsText];
162
+ const layout = verticalLayout(textItems, { spacing: 18, align: "center" });
163
+ applyLayout(textItems, layout.positions);
164
+ textItems.forEach((item) => this.infoPanel.add(item));
165
+ }
166
+
167
+ initGrid() {
168
+ const { gridSize, gridResolution } = CONFIG;
169
+ this.gridVertices = [];
170
+
171
+ // Create grid of vertices
172
+ for (let i = 0; i <= gridResolution; i++) {
173
+ const row = [];
174
+ for (let j = 0; j <= gridResolution; j++) {
175
+ const x = (i / gridResolution - 0.5) * 2 * gridSize;
176
+ const z = (j / gridResolution - 0.5) * 2 * gridSize;
177
+ row.push({ x, y: 0, z });
178
+ }
179
+ this.gridVertices.push(row);
180
+ }
181
+ }
182
+
183
+ handleClick(e) {
184
+ if (this.camera.isDragging()) return;
185
+ this.shuffleBody();
186
+ }
187
+
188
+ shuffleBody() {
189
+ // Randomly choose body type (weighted toward black holes for dramatic effect)
190
+ const rand = Math.random();
191
+ let type = "blackhole";
192
+ if (rand > 0.5) type = "star";
193
+ else if (rand > 0.3) type = "neutron";
194
+
195
+ const typeConfig = CONFIG.bodyTypes[type];
196
+ const mass =
197
+ typeConfig.minMass +
198
+ Math.random() * (typeConfig.maxMass - typeConfig.minMass);
199
+
200
+ // Always centered
201
+ this.body = {
202
+ x: 0,
203
+ z: 0,
204
+ mass,
205
+ type,
206
+ orbitPhase: Math.random() * Math.PI * 2,
207
+ };
208
+ }
209
+
210
+ /**
211
+ * Calculate well depth at a point using Gaussian profile
212
+ * Returns positive value (depth of well)
213
+ * Gaussian naturally falls to 0 at edges - no clamping needed
214
+ * Includes subtle pulsing animation
215
+ */
216
+ calculateWellDepth(x, z) {
217
+ const dx = x - this.body.x;
218
+ const dz = z - this.body.z;
219
+ const rSquared = dx * dx + dz * dz;
220
+
221
+ // Gaussian well: depth = A * exp(-r²/2σ²)
222
+ // More mass = wider crater (sigma increases)
223
+ // More mass = deeper but with diminishing returns (sqrt)
224
+ const sigma = CONFIG.wellWidth * Math.sqrt(this.body.mass);
225
+ const baseAmplitude = CONFIG.wellDepth * Math.sqrt(this.body.mass);
226
+
227
+ // Pulsing animation - well "breathes"
228
+ const pulse =
229
+ 1 + CONFIG.wellPulseAmount * Math.sin(this.time * CONFIG.wellPulseSpeed);
230
+ const amplitude = baseAmplitude * pulse;
231
+
232
+ return amplitude * Math.exp(-rSquared / (2 * sigma * sigma));
233
+ }
234
+
235
+ update(dt) {
236
+ super.update(dt);
237
+ this.time += dt;
238
+
239
+ // Update camera (handles auto-rotate when not dragging)
240
+ this.camera.update(dt);
241
+
242
+ // Update grid vertices based on gravitational wells
243
+ // Positive Y = wells curving DOWN (with current camera angle)
244
+ const { gridResolution } = CONFIG;
245
+ for (let i = 0; i <= gridResolution; i++) {
246
+ for (let j = 0; j <= gridResolution; j++) {
247
+ const vertex = this.gridVertices[i][j];
248
+ vertex.y = this.calculateWellDepth(vertex.x, vertex.z);
249
+ }
250
+ }
251
+
252
+ // Update orbiter - faster orbit for heavier central mass (Kepler's law)
253
+ const orbitSpeed = CONFIG.orbitSpeed * Math.sqrt(this.body.mass);
254
+ this.orbiterAngle += orbitSpeed * dt;
255
+
256
+ // Update stats text
257
+ if (this.statsText) {
258
+ const typeName =
259
+ this.body.type.charAt(0).toUpperCase() + this.body.type.slice(1);
260
+ this.statsText.text = `${typeName} | Mass: ${this.body.mass.toFixed(1)} M\u2609`;
261
+ }
262
+ }
263
+
264
+ render() {
265
+ const w = this.width;
266
+ const h = this.height;
267
+ const cx = w / 2;
268
+ const cy = h / 2 + 50;
269
+
270
+ super.render();
271
+
272
+ // Draw grid lines
273
+ this.drawGrid(cx, cy);
274
+
275
+ // Draw orbiting body (behind or in front based on position)
276
+ this.drawOrbiter(cx, cy);
277
+
278
+ // Draw stellar body
279
+ this.drawBody(cx, cy);
280
+
281
+ // Draw controls hint
282
+ this.drawControls(w, h);
283
+ }
284
+
285
+ drawGrid(cx, cy) {
286
+ const { gridResolution, gridScale, gridColor, gridHighlight } = CONFIG;
287
+
288
+ // Project all vertices
289
+ const projected = this.gridVertices.map((row) =>
290
+ row.map((v) => {
291
+ const p = this.camera.project(v.x * gridScale, v.y, v.z * gridScale);
292
+ return {
293
+ x: cx + p.x,
294
+ y: cy + p.y,
295
+ z: p.z,
296
+ depth: v.y,
297
+ };
298
+ }),
299
+ );
300
+
301
+ // Draw grid lines along X direction
302
+ for (let i = 0; i <= gridResolution; i++) {
303
+ const isMainLine = i % 5 === 0;
304
+ Painter.useCtx((ctx) => {
305
+ ctx.strokeStyle = isMainLine ? gridHighlight : gridColor;
306
+ ctx.lineWidth = isMainLine ? 1.5 : 0.8;
307
+ ctx.beginPath();
308
+
309
+ for (let j = 0; j <= gridResolution; j++) {
310
+ const p = projected[i][j];
311
+ if (j === 0) {
312
+ ctx.moveTo(p.x, p.y);
313
+ } else {
314
+ ctx.lineTo(p.x, p.y);
315
+ }
316
+ }
317
+ ctx.stroke();
318
+ });
319
+ }
320
+
321
+ // Draw grid lines along Z direction
322
+ for (let j = 0; j <= gridResolution; j++) {
323
+ const isMainLine = j % 5 === 0;
324
+ Painter.useCtx((ctx) => {
325
+ ctx.strokeStyle = isMainLine ? gridHighlight : gridColor;
326
+ ctx.lineWidth = isMainLine ? 1.5 : 0.8;
327
+ ctx.beginPath();
328
+
329
+ for (let i = 0; i <= gridResolution; i++) {
330
+ const p = projected[i][j];
331
+ if (i === 0) {
332
+ ctx.moveTo(p.x, p.y);
333
+ } else {
334
+ ctx.lineTo(p.x, p.y);
335
+ }
336
+ }
337
+ ctx.stroke();
338
+ });
339
+ }
340
+ }
341
+
342
+ drawOrbiter(cx, cy) {
343
+ // Orbit radius scales with well width (sigma)
344
+ const sigma = CONFIG.wellWidth * Math.sqrt(this.body.mass);
345
+ const orbitRadius = sigma * CONFIG.orbitRadiusMultiplier;
346
+
347
+ // Calculate orbiter position on the grid
348
+ const orbiterX = this.body.x + Math.cos(this.orbiterAngle) * orbitRadius;
349
+ const orbiterZ = this.body.z + Math.sin(this.orbiterAngle) * orbitRadius;
350
+
351
+ // Get well depth at orbiter position (follows the curvature)
352
+ const wellDepth = this.calculateWellDepth(orbiterX, orbiterZ);
353
+
354
+ // Project to screen
355
+ const p = this.camera.project(
356
+ orbiterX * CONFIG.gridScale,
357
+ wellDepth,
358
+ orbiterZ * CONFIG.gridScale,
359
+ );
360
+
361
+ const screenX = cx + p.x;
362
+ const screenY = cy + p.y;
363
+ const size = CONFIG.orbiterSize * p.scale;
364
+
365
+ // Draw glow
366
+ Painter.useCtx((ctx) => {
367
+ const gradient = ctx.createRadialGradient(
368
+ screenX,
369
+ screenY,
370
+ 0,
371
+ screenX,
372
+ screenY,
373
+ size * 3,
374
+ );
375
+ gradient.addColorStop(0, CONFIG.orbiterGlow);
376
+ gradient.addColorStop(1, "transparent");
377
+
378
+ ctx.fillStyle = gradient;
379
+ ctx.beginPath();
380
+ ctx.arc(screenX, screenY, size * 3, 0, Math.PI * 2);
381
+ ctx.fill();
382
+ });
383
+
384
+ // Draw orbiter body
385
+ Painter.useCtx((ctx) => {
386
+ const gradient = ctx.createRadialGradient(
387
+ screenX - size * 0.3,
388
+ screenY - size * 0.3,
389
+ 0,
390
+ screenX,
391
+ screenY,
392
+ size,
393
+ );
394
+ gradient.addColorStop(0, "#fff");
395
+ gradient.addColorStop(0.5, CONFIG.orbiterColor);
396
+ gradient.addColorStop(1, CONFIG.orbiterGlow);
397
+
398
+ ctx.fillStyle = gradient;
399
+ ctx.beginPath();
400
+ ctx.arc(screenX, screenY, size, 0, Math.PI * 2);
401
+ ctx.fill();
402
+ });
403
+
404
+ // Draw trail (fading arc showing recent path)
405
+ this.drawOrbiterTrail(cx, cy);
406
+ }
407
+
408
+ drawOrbiterTrail(cx, cy) {
409
+ // Orbit radius scales with well width (sigma)
410
+ const sigma = CONFIG.wellWidth * Math.sqrt(this.body.mass);
411
+ const orbitRadius = sigma * CONFIG.orbitRadiusMultiplier;
412
+
413
+ const trailLength = 40; // Number of trail segments
414
+ const trailArc = Math.PI * 0.8; // How much of the orbit to show as trail
415
+
416
+ Painter.useCtx((ctx) => {
417
+ ctx.lineCap = "round";
418
+
419
+ for (let i = 0; i < trailLength; i++) {
420
+ const t = i / trailLength;
421
+ const angle = this.orbiterAngle - t * trailArc;
422
+
423
+ const trailX = this.body.x + Math.cos(angle) * orbitRadius;
424
+ const trailZ = this.body.z + Math.sin(angle) * orbitRadius;
425
+ const wellDepth = this.calculateWellDepth(trailX, trailZ);
426
+
427
+ const p = this.camera.project(
428
+ trailX * CONFIG.gridScale,
429
+ wellDepth,
430
+ trailZ * CONFIG.gridScale,
431
+ );
432
+
433
+ if (i === 0) continue;
434
+
435
+ const prevAngle =
436
+ this.orbiterAngle - ((i - 1) / trailLength) * trailArc;
437
+ const prevX = this.body.x + Math.cos(prevAngle) * orbitRadius;
438
+ const prevZ = this.body.z + Math.sin(prevAngle) * orbitRadius;
439
+ const prevDepth = this.calculateWellDepth(prevX, prevZ);
440
+ const prevP = this.camera.project(
441
+ prevX * CONFIG.gridScale,
442
+ prevDepth,
443
+ prevZ * CONFIG.gridScale,
444
+ );
445
+
446
+ const alpha = (1 - t) * 0.5;
447
+ ctx.strokeStyle = `rgba(100, 180, 255, ${alpha})`;
448
+ ctx.lineWidth = (1 - t) * 2 * p.scale;
449
+ ctx.beginPath();
450
+ ctx.moveTo(cx + prevP.x, cy + prevP.y);
451
+ ctx.lineTo(cx + p.x, cy + p.y);
452
+ ctx.stroke();
453
+ }
454
+ });
455
+ }
456
+
457
+ drawBody(cx, cy) {
458
+ const body = this.body;
459
+ const typeConfig = CONFIG.bodyTypes[body.type];
460
+
461
+ const wellDepth = this.calculateWellDepth(body.x, body.z);
462
+ const p = this.camera.project(
463
+ body.x * CONFIG.gridScale,
464
+ wellDepth * 0.7, // Place body in the well
465
+ body.z * CONFIG.gridScale,
466
+ );
467
+
468
+ const screenX = cx + p.x;
469
+ const screenY = cy + p.y;
470
+
471
+ // Size based on mass and perspective
472
+ const baseSize = 8 + body.mass * 6;
473
+ const size = baseSize * p.scale;
474
+
475
+ // Pulsing glow effect
476
+ const pulse =
477
+ 0.8 + 0.2 * Math.sin(this.time * CONFIG.pulseSpeed + body.orbitPhase);
478
+
479
+ // Draw glow
480
+ Painter.useCtx((ctx) => {
481
+ const gradient = ctx.createRadialGradient(
482
+ screenX,
483
+ screenY,
484
+ 0,
485
+ screenX,
486
+ screenY,
487
+ size * 3 * pulse,
488
+ );
489
+ gradient.addColorStop(0, typeConfig.glowColor);
490
+ gradient.addColorStop(1, "transparent");
491
+
492
+ ctx.fillStyle = gradient;
493
+ ctx.beginPath();
494
+ ctx.arc(screenX, screenY, size * 3 * pulse, 0, Math.PI * 2);
495
+ ctx.fill();
496
+ });
497
+
498
+ // Draw body
499
+ Painter.useCtx((ctx) => {
500
+ if (body.type === "blackhole") {
501
+ // Black hole: dark center with bright accretion disk effect
502
+ ctx.fillStyle = "#000";
503
+ ctx.beginPath();
504
+ ctx.arc(screenX, screenY, size, 0, Math.PI * 2);
505
+ ctx.fill();
506
+
507
+ // Event horizon ring
508
+ ctx.strokeStyle = "rgba(150, 100, 200, 0.8)";
509
+ ctx.lineWidth = 2;
510
+ ctx.beginPath();
511
+ ctx.arc(screenX, screenY, size * 1.3, 0, Math.PI * 2);
512
+ ctx.stroke();
513
+ } else {
514
+ // Stars and neutron stars: bright center
515
+ const gradient = ctx.createRadialGradient(
516
+ screenX - size * 0.3,
517
+ screenY - size * 0.3,
518
+ 0,
519
+ screenX,
520
+ screenY,
521
+ size,
522
+ );
523
+ gradient.addColorStop(0, "#fff");
524
+ gradient.addColorStop(0.3, typeConfig.color);
525
+ gradient.addColorStop(1, typeConfig.glowColor);
526
+
527
+ ctx.fillStyle = gradient;
528
+ ctx.beginPath();
529
+ ctx.arc(screenX, screenY, size, 0, Math.PI * 2);
530
+ ctx.fill();
531
+ }
532
+ });
533
+ }
534
+
535
+ drawControls(w, h) {
536
+ Painter.useCtx((ctx) => {
537
+ ctx.fillStyle = "#445";
538
+ ctx.font = "10px monospace";
539
+ ctx.textAlign = "right";
540
+ ctx.fillText(
541
+ "click to shuffle | drag to rotate | double-click to reset",
542
+ w - 15,
543
+ h - 30,
544
+ );
545
+ ctx.fillText(
546
+ "Mass curves spacetime | Objects follow geodesics",
547
+ w - 15,
548
+ h - 15,
549
+ );
550
+ ctx.textAlign = "left";
551
+ });
552
+ }
553
+ }
554
+
555
+ window.addEventListener("load", () => {
556
+ const canvas = document.getElementById("game");
557
+ const demo = new SpacetimeDemo(canvas);
558
+ demo.start();
559
+ });