@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,219 @@
1
+ import { GameObject, Sphere3D, Painter, Easing } from "../../../src/index.js";
2
+ import { CONFIG } from "./config.js";
3
+
4
+ export class BlackHole extends GameObject {
5
+ constructor(game, options = {}) {
6
+ super(game, options);
7
+ this.mass = options.initialMass ?? CONFIG.blackHole.initialMass;
8
+ this.baseRadius = game.baseScale ? game.baseScale * CONFIG.bhRadiusRatio : 50;
9
+ this.currentRadius = this.baseRadius;
10
+
11
+ // Awakening state - BH starts dormant, wakes up as it feeds
12
+ this.awakeningLevel = 0; // 0 = dormant (pure black), 1 = fully awake
13
+ this.feedingPulse = 0; // Temporary glow boost when consuming
14
+ this.totalConsumed = 0; // Track total mass consumed
15
+
16
+ // Dynamic growth animation
17
+ this.growthSpurt = 0; // Overshoot when consuming (decays to 0)
18
+ this.breathPhase = 0; // Oscillation phase for breathing effect
19
+ this.targetRadius = this.baseRadius; // Smooth radius target
20
+
21
+ // Rotation - black holes spin!
22
+ this.rotation = 0;
23
+ this.rotationSpeed = options.rotationSpeed ?? 2.9; // Slow, ominous spin
24
+
25
+ // Use WebGL shaders for rendering
26
+ this.useShader = options.useShader ?? true;
27
+ }
28
+
29
+ init() {
30
+ this.updateVisual();
31
+ }
32
+
33
+ /**
34
+ * Add mass from consumed particles - triggers awakening and pulse
35
+ *
36
+ * @param {number} amount - Amount of mass to add
37
+ *
38
+ * Note: Feeding pulse is only triggered before the stable phase.
39
+ * Once stabilizing, particles can still be consumed but won't
40
+ * cause the visual pulse effect.
41
+ */
42
+ addConsumedMass(amount) {
43
+ this.totalConsumed += amount;
44
+
45
+ // Skip pulse effects if we're in the stable phase
46
+ if (this.isStabilizing) {
47
+ return;
48
+ }
49
+
50
+ // Awakening increases as BH feeds (slow ramp up)
51
+ const awakeningProgress = Math.min(1, this.totalConsumed * 0.1);
52
+ this.awakeningLevel = Math.max(this.awakeningLevel, awakeningProgress);
53
+
54
+ // Feeding pulse - temporary glow boost
55
+ this.feedingPulse = Math.min(1, this.feedingPulse + amount * 0.2);
56
+
57
+ // Growth spurt - overshoot effect when consuming
58
+ // More dramatic spurts as awakening increases
59
+ const spurtIntensity = 0.03 + this.awakeningLevel * 0.05;
60
+ this.growthSpurt = Math.min(0.15, this.growthSpurt + amount * spurtIntensity);
61
+ }
62
+
63
+ /**
64
+ * Reset to dormant state
65
+ */
66
+ resetAwakening() {
67
+ this.awakeningLevel = 0;
68
+ this.feedingPulse = 0;
69
+ this.totalConsumed = 0;
70
+ this.rotation = 0;
71
+ this.growthSpurt = 0;
72
+ this.breathPhase = 0;
73
+ this.isStabilizing = false; // Reset stabilization state
74
+ }
75
+
76
+ updateVisual() {
77
+ // Calculate how much mass has been absorbed (0 = none, 1 = full star)
78
+ const massAbsorbed = Math.max(0, this.mass - CONFIG.blackHole.initialMass);
79
+ const absorptionProgress = massAbsorbed / CONFIG.star.initialMass;
80
+
81
+ // Apply easing to make growth feel more organic
82
+ // easeOutCubic: rapid initial growth that slows as it fills
83
+ const easedProgress = Easing.easeOutCubic(absorptionProgress);
84
+
85
+ // Base radius from absorption with easing
86
+ const baseScale = this.baseRadius / CONFIG.bhRadiusRatio;
87
+ const radiusFraction = CONFIG.bhRadiusRatio +
88
+ easedProgress * (CONFIG.bhFinalRadiusRatio - CONFIG.bhRadiusRatio);
89
+ this.targetRadius = baseScale * radiusFraction;
90
+
91
+ // Breathing oscillation - subtle when dormant, stronger when awake
92
+ const breathAmplitude = 0.01 + this.awakeningLevel * 0.02 + this.feedingPulse * 0.03;
93
+ const breathSpeed = 1.5 + this.awakeningLevel * 0.5; // Faster when active
94
+ const breathOffset = Math.sin(this.breathPhase * breathSpeed) * breathAmplitude;
95
+
96
+ // Growth spurt overshoot effect (elastic rebound)
97
+ const spurtOffset = this.growthSpurt * (1 + Math.sin(this.breathPhase * 8) * 0.3);
98
+
99
+ // Combine all effects
100
+ this.currentRadius = this.targetRadius * (1 + breathOffset + spurtOffset);
101
+
102
+ if (this.currentRadius <= 0) {
103
+ this.currentRadius = this.baseRadius;
104
+ }
105
+
106
+ // Edge brightness increases with awakening
107
+ const awakeFactor = this.awakeningLevel;
108
+ const pulseFactor = this.feedingPulse;
109
+
110
+ // For Canvas 2D fallback - gradient rendering
111
+ // Dormant: pure black edges (#101010)
112
+ // Awake: warmer edges with hint of orange/red glow
113
+ const edgeBase = 16 + Math.round(awakeFactor * 24 + pulseFactor * 16); // 16-56
114
+ const edgeR = Math.min(255, edgeBase + Math.round(awakeFactor * 40 + pulseFactor * 60));
115
+ const edgeG = Math.min(255, edgeBase + Math.round(awakeFactor * 20 + pulseFactor * 30));
116
+ const edgeB = edgeBase;
117
+
118
+ const midBase = 8 + Math.round(awakeFactor * 12 + pulseFactor * 8);
119
+ const midR = Math.min(255, midBase + Math.round(awakeFactor * 20 + pulseFactor * 30));
120
+ const midG = Math.min(255, midBase + Math.round(awakeFactor * 10 + pulseFactor * 15));
121
+ const midB = midBase;
122
+
123
+ const gradient = Painter.colors.radialGradient(
124
+ 0, 0, 0.01 * this.currentRadius,
125
+ 0, 0, this.currentRadius,
126
+ [
127
+ { offset: 0, color: "#000" },
128
+ { offset: 0.5, color: "#000" },
129
+ { offset: 0.85, color: `rgb(${midR}, ${midG}, ${midB})` },
130
+ { offset: 1, color: `rgb(${edgeR}, ${edgeG}, ${edgeB})` },
131
+ ]
132
+ );
133
+
134
+ if (!this.core) {
135
+ this.core = new Sphere3D(this.currentRadius, {
136
+ color: gradient,
137
+ camera: this.game.camera,
138
+ stroke: null, // No wireframe
139
+ debug: false,
140
+ segments: 32, // Smoother sphere
141
+ // WebGL shader options
142
+ useShader: this.useShader,
143
+ shaderType: "blackHole",
144
+ shaderUniforms: {
145
+ uAwakeningLevel: awakeFactor,
146
+ uFeedingPulse: pulseFactor,
147
+ uRotation: this.rotation,
148
+ },
149
+ });
150
+ } else {
151
+ this.core.radius = this.currentRadius;
152
+ this.core.color = gradient; // Keep gradient for Canvas 2D fallback
153
+ // Update shader uniforms
154
+ if (this.core.useShader) {
155
+ this.core.setShaderUniforms({
156
+ uAwakeningLevel: awakeFactor,
157
+ uFeedingPulse: pulseFactor,
158
+ uRotation: this.rotation,
159
+ });
160
+ }
161
+ this.core._generateGeometry();
162
+ }
163
+ }
164
+
165
+ update(dt) {
166
+ super.update(dt);
167
+
168
+ // Animate breathing phase
169
+ this.breathPhase += dt * Math.PI * 2; // Full cycle per second
170
+
171
+ // Spin the black hole - rotation speeds up when feeding
172
+ const spinMultiplier = 1 + this.feedingPulse * 2 + this.awakeningLevel * 0.5;
173
+ this.rotation += this.rotationSpeed * spinMultiplier * dt;
174
+
175
+ // Decay feeding pulse over time
176
+ if (this.feedingPulse > 0) {
177
+ this.feedingPulse = Math.max(0, this.feedingPulse - dt * 1.5);
178
+ }
179
+
180
+ // Decay growth spurt with elastic damping
181
+ if (this.growthSpurt > 0) {
182
+ // Fast initial decay, slows down (feels like elastic settling)
183
+ const decayRate = 3 + this.growthSpurt * 5; // Faster when larger
184
+ this.growthSpurt = Math.max(0, this.growthSpurt - dt * decayRate);
185
+ }
186
+
187
+ // Decay awakening level when stabilizing (slow cool-down)
188
+ // Minimum level is 0.3 - never goes fully dormant after feeding
189
+ if (this.isStabilizing && this.awakeningLevel > 0.3) {
190
+ this.awakeningLevel = Math.max(0.3, this.awakeningLevel - dt * 0.15);
191
+ }
192
+
193
+ this.updateVisual();
194
+ }
195
+
196
+ /**
197
+ * Start the stabilization phase - black hole calms down
198
+ */
199
+ startStabilizing() {
200
+ this.isStabilizing = true;
201
+ }
202
+
203
+ /**
204
+ * Reset stabilization state
205
+ */
206
+ resetStabilizing() {
207
+ this.isStabilizing = false;
208
+ }
209
+
210
+ onResize(baseRadius) {
211
+ this.baseRadius = baseRadius;
212
+ this.updateVisual();
213
+ }
214
+
215
+ render() {
216
+ super.render();
217
+ this.core.render();
218
+ }
219
+ }
@@ -0,0 +1,209 @@
1
+ import { Scene3D } from "../../../src/index.js";
2
+ import { BlackHole } from "./blackhole.js";
3
+ import { Star } from "./tdestar.js";
4
+ import { TidalStream } from "./tidalstream.js";
5
+ import { AccretionDisk } from "./accretiondisk.js";
6
+ import { RelativisticJets } from "./jets.js";
7
+ import { CONFIG } from "./config.js";
8
+
9
+ /**
10
+ * BlackHoleScene - Main 3D scene containing the TDE visualization
11
+ *
12
+ * Z-Order Rendering Rules:
13
+ * 1. BlackHole renders at the back (dark shadow)
14
+ * 2. AccretionDisk renders over the black hole (particles curve around)
15
+ * 3. TidalStream always on top of star and black hole
16
+ * 4. Star position relative to BlackHole changes based on camera depth
17
+ *
18
+ * Z-Index Buckets (lower = renders first = behind):
19
+ * - starBack: 10 (star when behind BH)
20
+ * - blackHole: 15 (dark shadow at back)
21
+ * - disk: 20 (accretion disk over BH)
22
+ * - starFront: 25 (star when in front of BH)
23
+ * - stream: 30 (particles - always on top)
24
+ * - jets: 40 (always on top)
25
+ *
26
+ * @extends Scene3D
27
+ */
28
+ export class BlackHoleScene extends Scene3D {
29
+ constructor(game, options = {}) {
30
+ super(game, options);
31
+ // Camera is passed via options and handled by Scene3D
32
+
33
+ /**
34
+ * Z-index buckets for render ordering
35
+ * Scene3D sorts by zIndex first (lower renders first/behind),
36
+ * then uses camera depth as tie-breaker
37
+ *
38
+ * Layering (simple):
39
+ * - BlackHole at back (dark shadow)
40
+ * - Disk renders over BH (particles curve around it)
41
+ * - Stream always on top of star and BH
42
+ * - Star moves in front of/behind BH based on camera view
43
+ */
44
+ this.Z = {
45
+ starBack: 10, // Star when behind BH
46
+ blackHole: 15, // BlackHole: dark shadow at back
47
+ disk: 20, // AccretionDisk: over the black hole
48
+ starFront: 25, // Star when in front of BH
49
+ stream: 30, // TidalStream: always on top of star and BH
50
+ jets: 40, // Jets: always on top
51
+ };
52
+ }
53
+
54
+ init() {
55
+ // Z-order buckets: lower draws first, equal uses depth
56
+ // IMPORTANT: Set zIndex AFTER add() because ZOrderedCollection.add()
57
+ // overwrites zIndex with array index
58
+
59
+ // Add BlackHole at (0,0,0)
60
+ this.bh = new BlackHole(this.game);
61
+ this.add(this.bh);
62
+ this.bh.zIndex = this.Z.blackHole; // Set AFTER add()
63
+
64
+ // Add a Star orbiting the black hole
65
+ this.star = new Star(this.game);
66
+ this.add(this.star);
67
+ this.star.zIndex = this.Z.starFront; // Set AFTER add(), will be updated per-frame
68
+
69
+ // Add accretion disk (starts inactive)
70
+ this.disk = new AccretionDisk(this.game, {
71
+ camera: this.game.camera,
72
+ bhRadius: this.game.baseScale * CONFIG.bhRadiusRatio,
73
+ bhMass: CONFIG.blackHole.initialMass,
74
+ onParticleConsumed: () => {
75
+ this.bh.addConsumedMass(0.1); // Disk particles worth more
76
+ },
77
+ });
78
+ this.add(this.disk);
79
+ this.disk.zIndex = this.Z.disk; // Set AFTER add()
80
+
81
+ // Add tidal stream for particles
82
+ // When particles are consumed, feed the black hole
83
+ // When particles circularize, transfer to accretion disk
84
+ this.stream = new TidalStream(this.game, {
85
+ camera: this.game.camera,
86
+ scene: this, // Pass scene reference for screen center
87
+ bhRadius: this.game.baseScale * CONFIG.bhRadiusRatio,
88
+ diskInnerRadius: this.disk.innerRadius,
89
+ diskOuterRadius: this.disk.outerRadius,
90
+ onParticleConsumed: () => {
91
+ this.bh.addConsumedMass(0.05); // Each particle adds small amount
92
+ },
93
+ onParticleCaptured: (p) => {
94
+ this.disk.captureParticle(p);
95
+ },
96
+ });
97
+ this.add(this.stream);
98
+ this.stream.zIndex = this.Z.stream; // Set AFTER add()
99
+
100
+ // Add relativistic jets (activated during flare phase)
101
+ this.jets = new RelativisticJets(this.game, {
102
+ camera: this.game.camera,
103
+ bhRadius: this.game.baseScale * CONFIG.bhRadiusRatio,
104
+ });
105
+ this.add(this.jets);
106
+ this.jets.zIndex = this.Z.jets; // Set AFTER add()
107
+
108
+ // Mark z-order as dirty to ensure initial sort
109
+ if (this._collection) {
110
+ this._collection._zOrderDirty = true;
111
+ }
112
+ }
113
+
114
+ update(dt) {
115
+ super.update(dt);
116
+
117
+ // Sync all particle systems with current BH radius (grows as it feeds)
118
+ if (this.bh) {
119
+ if (this.stream) {
120
+ this.stream.updateBHRadius(this.bh.currentRadius);
121
+ }
122
+ if (this.disk) {
123
+ this.disk.updateBHRadius(this.bh.currentRadius);
124
+ // Also sync stream's disk bounds for capture detection
125
+ if (this.stream) {
126
+ this.stream.updateDiskBounds(this.disk.innerRadius, this.disk.outerRadius);
127
+ }
128
+ }
129
+ if (this.jets) {
130
+ this.jets.updateBHRadius(this.bh.currentRadius);
131
+ }
132
+ }
133
+
134
+ // Note: Z-order update is called from TDEDemo.update() AFTER star position
135
+ // is updated, to ensure we use the current frame's position, not the previous frame's.
136
+ }
137
+
138
+ /**
139
+ * Update star's z-index based on its depth relative to the black hole
140
+ *
141
+ * Dynamic z-ordering:
142
+ * - When star in front of BH: star renders on top of BH
143
+ * - When star behind BH: BH renders on top of star
144
+ * - Stream (particles) always stays on top of both
145
+ *
146
+ * Camera3D.project() returns z as depth after rotation:
147
+ * - Lower z = closer to camera
148
+ * - Higher z = farther from camera
149
+ *
150
+ * IMPORTANT: Must be called AFTER star position is updated each frame.
151
+ */
152
+ updateStarZOrder() {
153
+ if (!this.star || !this.bh || !this.game?.camera) {
154
+ return;
155
+ }
156
+
157
+ const camera = this.game.camera;
158
+
159
+ // Project star position to get its depth after camera rotation
160
+ const projStar = camera.project(
161
+ this.star.x || 0,
162
+ this.star.y || 0,
163
+ this.star.z || 0
164
+ );
165
+
166
+ // Black hole is always at origin (0, 0, 0)
167
+ const projBH = camera.project(0, 0, 0);
168
+
169
+ // Star is "in front" if its depth is LESS than BH's depth
170
+ const starInFront = projStar.z < projBH.z;
171
+
172
+ // Update star's z-index
173
+ const newZIndex = starInFront ? this.Z.starFront : this.Z.starBack;
174
+
175
+ if (this.star.zIndex !== newZIndex) {
176
+ this.star.zIndex = newZIndex;
177
+ // Mark ordering dirty so Scene3D resort happens this frame
178
+ if (this._collection) {
179
+ this._collection._zOrderDirty = true;
180
+ }
181
+ }
182
+ }
183
+
184
+ onResize() {
185
+ const game = this.game;
186
+ if (this.bh) {
187
+ this.bh.onResize(game.baseScale * CONFIG.bhRadiusRatio);
188
+ }
189
+ if (this.star) {
190
+ this.star.onResize(
191
+ game.baseScale * CONFIG.starRadiusRatio,
192
+ game.baseScale * CONFIG.star.initialOrbitRadius
193
+ );
194
+ }
195
+ if (this.disk && this.bh) {
196
+ this.disk.updateBHRadius(this.bh.currentRadius);
197
+ }
198
+ if (this.stream && this.bh) {
199
+ this.stream.updateBHRadius(this.bh.currentRadius);
200
+ // Update disk bounds for capture detection
201
+ if (this.disk) {
202
+ this.stream.updateDiskBounds(this.disk.innerRadius, this.disk.outerRadius);
203
+ }
204
+ }
205
+ if (this.jets && this.bh) {
206
+ this.jets.updateBHRadius(this.bh.currentRadius);
207
+ }
208
+ }
209
+ }
@@ -0,0 +1,59 @@
1
+ export const CONFIG = {
2
+ // Sizing (as fraction of screen baseScale)
3
+ bhRadiusRatio: 0.01, // Initial dormant black hole size (larger for visible lensing)
4
+ bhFinalRadiusRatio: 0.12, // Final size after consuming star
5
+ starRadiusRatio: 0.08,
6
+
7
+ // Phase durations (seconds)
8
+ durations: {
9
+ approach: 10.0, // Stable wide orbit
10
+ stretch: 10.0, // Orbit begins to decay
11
+ disrupt: 20.0, // Mass transfer (event-based exit)
12
+ accrete: 1.0, // Debris accretion
13
+ flare: 5.0, // Jets firing - spectacular cosmic event!
14
+ stable: Infinity, // Final stable state
15
+ },
16
+
17
+ // Flash effect (now handled by Tweenetik in FSM)
18
+
19
+ // Physics params
20
+ blackHole: {
21
+ initialMass: 2,
22
+ color: "#000",
23
+ },
24
+ star: {
25
+ initialMass: 25,
26
+ color: "#FF6030", // Deep red-orange (cooler K/M type star)
27
+ // Orbit sizing (fraction of half the smaller screen dimension)
28
+ // apoapsis = initialOrbitRadius * (1 + eccentricity)
29
+ // periapsis = initialOrbitRadius * (1 - eccentricity)
30
+ initialOrbitRadius: 1.2, // Semi-major axis
31
+ eccentricity: 0.25, // Lower = more circular, periapsis closer to edge
32
+ // With these values:
33
+ // periapsis (right) = 1.2 * 0.75 = 0.9 (90% to edge - visible on RIGHT side!)
34
+ // apoapsis (left) = 1.2 * 1.25 = 1.5 (goes off screen left)
35
+ orbitSpeed: 0.4,
36
+ decayRate: 0.4,
37
+ massTransferStart: 0.1,
38
+ rotationSpeed: 0.71,
39
+ temperature: 3800,
40
+ orbitCenterX: 0,
41
+ orbitCenterY: 0,
42
+ bypassConstraints: true,
43
+ // Start angle: star should be VISIBLE at start, then swing through orbit
44
+ // 0 = right (periapsis), π/2 = top, π = left (apoapsis), 3π/2 = bottom
45
+ startAngle: Math.PI * 1.85, // Start lower-right, comes FROM right, swings up and around
46
+ },
47
+ sceneOptions: {
48
+ starCount: 3000,
49
+ },
50
+
51
+ // Accretion disk settings
52
+ disk: {
53
+ innerRadiusRatio: 0.03, // ISCO (innermost stable orbit)
54
+ outerRadiusRatio: 0.15, // Outer halo edge
55
+ maxParticles: 2000,
56
+ orbitalSpeed: 0.8,
57
+ activationProgress: 0.8, // Start at 80% of disrupt phase
58
+ },
59
+ };