@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,317 @@
1
+ import { GameObject, Sphere3D } from "../../../src/index.js";
2
+ import { polarToCartesian } from "../../../src/math/gr.js";
3
+ import { CONFIG } from "./config.js";
4
+
5
+ // Star shader configuration
6
+ const STAR_SHADER_CONFIG = {
7
+ useShader: true,
8
+ shaderType: "star",
9
+ shaderUniforms: {
10
+ uStarColor: [1.0, 0.85, 0.3], // Golden yellow
11
+ uTemperature: 5500, // K (slightly cooler than Sun)
12
+ uActivityLevel: 0.75, // Moderate surface activity
13
+ },
14
+ };
15
+
16
+ export class Star extends GameObject {
17
+ constructor(game, options = {}) {
18
+ super(game, options);
19
+ this.mass = options.initialMass ?? CONFIG.star.initialMass;
20
+ this.initialMass = this.mass; // Store for mass ratio calculations
21
+ this.phi = 0;
22
+ // Initialize with reasonable defaults, will be updated by onResize
23
+ this.baseRadius = game.baseScale ? game.baseScale * CONFIG.starRadiusRatio : 20;
24
+ this.currentRadius = this.baseRadius;
25
+ this.orbitalRadius = game.baseScale ? game.baseScale * CONFIG.star.initialOrbitRadius : 200;
26
+ this.initialOrbitalRadius = this.orbitalRadius; // Store initial for decay calculations
27
+
28
+ // Velocity tracking for particle emission
29
+ this.velocityX = 0;
30
+ this.velocityY = 0;
31
+ this.velocityZ = 0;
32
+ this._prevX = 0;
33
+ this._prevY = 0;
34
+ this._prevZ = 0;
35
+
36
+ // Use WebGL shaders for star rendering
37
+ this.useShader = options.useShader ?? true;
38
+
39
+ // Cumulative rotation for angular emission detail
40
+ this.rotation = 0;
41
+ // Angular velocity (rad/s) - accumulates smoothly instead of discrete recalc
42
+ this.angularVelocity = CONFIG.star.rotationSpeed ?? 0.5;
43
+
44
+ // Tidal disruption state
45
+ this.tidalStretch = 0; // 0 = spherical, 1 = max elongation
46
+ this.pulsationPhase = 0; // Oscillation phase
47
+ this.stressLevel = 0; // Surface chaos level
48
+ this.tidalProgress = 0; // External tidal progress from FSM (0-1)
49
+ this.tidalFlare = 0; // 0-1, sudden brightness burst at disruption start
50
+ this.tidalWobble = 0; // 0-1, violent geometry wobble during trauma
51
+ }
52
+
53
+ init() {
54
+ // Initialize position on the orbit
55
+ const pos = polarToCartesian(this.orbitalRadius, this.phi);
56
+ this.x = pos.x;
57
+ this.z = pos.z;
58
+
59
+ // Initialize prev position to avoid velocity spike on first frame
60
+ this._prevX = this.x;
61
+ this._prevY = this.y || 0;
62
+ this._prevZ = this.z;
63
+ this.velocityX = 0;
64
+ this.velocityY = 0;
65
+ this.velocityZ = 0;
66
+
67
+ // Reset tidal state
68
+ this.tidalStretch = 0;
69
+ this.pulsationPhase = 0;
70
+ this.stressLevel = 0;
71
+ this.tidalProgress = 0;
72
+ this.tidalFlare = 0;
73
+ this.tidalWobble = 0;
74
+ this.angularVelocity = CONFIG.star.rotationSpeed ?? 0.5;
75
+ this.rotation = 0;
76
+
77
+ this.updateVisual();
78
+ }
79
+
80
+ /**
81
+ * Reset velocity tracking (call after position changes like restart)
82
+ */
83
+ resetVelocity() {
84
+ this._prevX = this.x;
85
+ this._prevY = this.y || 0;
86
+ this._prevZ = this.z;
87
+ this.velocityX = 0;
88
+ this.velocityY = 0;
89
+ this.velocityZ = 0;
90
+ }
91
+
92
+ updateVisual() {
93
+ const massRatio = this.mass / this.initialMass;
94
+
95
+ // === NON-LINEAR SIZE COLLAPSE ===
96
+ // Star resists at first (internal pressure), then collapses rapidly
97
+ // Use a power curve: slow start, rapid end
98
+ const collapseProgress = 1 - massRatio; // 0 = full, 1 = gone
99
+ const resistanceCurve = Math.pow(collapseProgress, 0.5); // sqrt = resists early
100
+ const effectiveMassRatio = 1 - resistanceCurve;
101
+
102
+ // Base radius with non-linear collapse
103
+ this.currentRadius = this.baseRadius * Math.max(0.05, effectiveMassRatio);
104
+
105
+ // Don't update geometry if star is consumed
106
+ if (this.currentRadius <= 0 || this.mass <= 0) {
107
+ return;
108
+ }
109
+
110
+ // === TIDAL STRETCH (Spaghettification) ===
111
+ // Create comet/teardrop shape pointed toward black hole
112
+ const dist = Math.sqrt(this.x * this.x + (this.z || 0) * (this.z || 0)) || 1;
113
+
114
+ // Direction toward black hole (unit vector)
115
+ let dirX = -this.x / dist;
116
+ let dirZ = -(this.z || 0) / dist;
117
+
118
+ // Proximity factor: closer to BH = more stretch
119
+ let proximityFactor = Math.max(0, 1 - dist / this.initialOrbitalRadius);
120
+
121
+ // Calculate stretch amount based on phase and proximity
122
+ if (collapseProgress > 0.8) {
123
+ // Very late stage - reduce stretch as star becomes tiny
124
+ this.tidalStretch = (1 - collapseProgress) * 2;
125
+ } else {
126
+ // Main deformation: builds with tidalProgress and proximity
127
+ // tidalProgress is driven by FSM state (0 in approach, ramps in stretch/disrupt)
128
+ const baseStretch = this.tidalProgress * 1.2; // Up to 1.2 stretch
129
+ const proximityBoost = proximityFactor * 0.5; // Extra stretch when close
130
+
131
+ this.tidalStretch = baseStretch + proximityBoost;
132
+ this.tidalStretch = Math.min(1.8, this.tidalStretch); // Cap at 1.8
133
+ }
134
+
135
+ // === BREATHING (Slow, ominous expansion/contraction) ===
136
+ // Very slow rhythm - like a dying star's final gasps
137
+ // No rapid bouncing - this should feel cosmic, not cartoonish
138
+ const breathingAmp = 0.03 * (1 - collapseProgress * 0.5); // Subtle, weakens as disrupted
139
+ const breathing = Math.sin(this.pulsationPhase) * breathingAmp;
140
+
141
+ // Apply breathing to radius (very subtle)
142
+ this.currentRadius *= (1 + breathing);
143
+
144
+ // === STRESS LEVEL ===
145
+ // Combines proximity and mass loss - drives surface chaos
146
+ // Use power curve so stress stays LOW for most of disruption, then ramps up sharply
147
+ // This gives more time to see the red-orange star with surface chaos
148
+ const rawStress = proximityFactor * 0.4 + collapseProgress * 0.6; // Reduced weights
149
+ // Power of 3 = stays low longer, ramps up sharply at the end
150
+ this.stressLevel = Math.min(1, Math.pow(rawStress, 2.5));
151
+
152
+ // === ACTIVITY & ROTATION ===
153
+ const activityLevel = 0.3 + this.stressLevel * 0.7; // 0.3 -> 1.0
154
+
155
+ // Angular momentum conservation: shrinking = faster spin
156
+ const baseRotationSpeed = CONFIG.star.rotationSpeed ?? 0.5;
157
+ const spinUpFactor = 1 / Math.max(0.2, effectiveMassRatio); // Inverse of size
158
+ const rotationSpeed = Math.min(10, baseRotationSpeed * spinUpFactor);
159
+
160
+ // === COLOR SHIFT ===
161
+ // Start deep red-orange, transition through orange → yellow → white
162
+ // This lets us see the tidal chaos on a colorful surface before brightening
163
+ //
164
+ // Phase 1 (stress 0-0.5): Deep red-orange, surface chaos building
165
+ // Phase 2 (stress 0.5-0.8): Shift to orange-yellow, intense activity
166
+ // Phase 3 (stress 0.8-1.0): Rapid shift to white-hot, death throes
167
+
168
+ // Temperature increases with stress (tidal heating is real physics!)
169
+ const tempShift = this.stressLevel * this.stressLevel * 2500; // Up to +2500K at max stress
170
+ const temperature = (CONFIG.star.temperature ?? 3800) + tempShift;
171
+
172
+ // Color transition: red-orange → orange → yellow → white
173
+ // R stays high, G increases with stress, B only increases late
174
+ let r = 1.0;
175
+ let g, b;
176
+
177
+ if (this.stressLevel < 0.5) {
178
+ // Phase 1: Deep red-orange → orange (stress 0-0.5)
179
+ const t = this.stressLevel * 2; // 0 to 1 over this phase
180
+ g = 0.35 + t * 0.25; // 0.35 → 0.6
181
+ b = 0.15 + t * 0.1; // 0.15 → 0.25
182
+ } else if (this.stressLevel < 0.8) {
183
+ // Phase 2: Orange → yellow-orange (stress 0.5-0.8)
184
+ const t = (this.stressLevel - 0.5) / 0.3; // 0 to 1
185
+ g = 0.6 + t * 0.2; // 0.6 → 0.8
186
+ b = 0.25 + t * 0.1; // 0.25 → 0.35
187
+ } else {
188
+ // Phase 3: Yellow-orange → white-hot (stress 0.8-1.0)
189
+ const t = (this.stressLevel - 0.8) / 0.2; // 0 to 1
190
+ g = 0.8 + t * 0.15; // 0.8 → 0.95
191
+ b = 0.35 + t * 0.5; // 0.35 → 0.85 (rapid blue increase = white)
192
+ }
193
+
194
+ const stressColor = [r, g, b];
195
+
196
+ // Expose current color for particle emission
197
+ this.currentColor = stressColor;
198
+
199
+ if (!this.visual) {
200
+ this.visual = new Sphere3D(this.currentRadius, {
201
+ color: CONFIG.star.color,
202
+ camera: this.game.camera,
203
+ useShader: this.useShader,
204
+ shaderType: "star",
205
+ shaderUniforms: {
206
+ uStarColor: stressColor,
207
+ uTemperature: temperature,
208
+ uActivityLevel: activityLevel,
209
+ uRotationSpeed: rotationSpeed,
210
+ uTidalStretch: this.tidalStretch,
211
+ uStretchDirX: dirX,
212
+ uStretchDirZ: dirZ,
213
+ uStressLevel: this.stressLevel,
214
+ uTidalFlare: this.tidalFlare,
215
+ uTidalWobble: this.tidalWobble,
216
+ },
217
+ });
218
+ } else {
219
+ this.visual.radius = this.currentRadius;
220
+ if (this.visual.useShader) {
221
+ this.visual.setShaderUniforms({
222
+ uStarColor: stressColor,
223
+ uTemperature: temperature,
224
+ uActivityLevel: activityLevel,
225
+ uRotationSpeed: rotationSpeed,
226
+ uTidalStretch: this.tidalStretch,
227
+ uStretchDirX: dirX,
228
+ uStretchDirZ: dirZ,
229
+ uStressLevel: this.stressLevel,
230
+ uTidalFlare: this.tidalFlare,
231
+ uTidalWobble: this.tidalWobble,
232
+ });
233
+ }
234
+ this.visual._generateGeometry();
235
+ }
236
+ }
237
+
238
+ onResize(baseRadius, orbitalRadius) {
239
+ this.baseRadius = baseRadius;
240
+ this.orbitalRadius = orbitalRadius;
241
+ this.initialOrbitalRadius = orbitalRadius;
242
+
243
+ // Update position to match new orbital radius
244
+ const pos = polarToCartesian(this.orbitalRadius, this.phi);
245
+ this.x = pos.x;
246
+ this.z = pos.z;
247
+
248
+ this.updateVisual();
249
+ }
250
+
251
+ update(dt) {
252
+ super.update(dt);
253
+
254
+ // Calculate velocity from position change
255
+ const currentY = this.y || 0;
256
+ if (dt > 0) {
257
+ this.velocityX = (this.x - this._prevX) / dt;
258
+ this.velocityY = (currentY - this._prevY) / dt;
259
+ this.velocityZ = (this.z - this._prevZ) / dt;
260
+ }
261
+
262
+ // Store current position for next frame
263
+ this._prevX = this.x;
264
+ this._prevY = currentY;
265
+ this._prevZ = this.z;
266
+
267
+ // Update self-rotation with smooth angular momentum conservation
268
+ // As star shrinks, angular velocity increases (I*ω = constant)
269
+ // But cap it when star is tiny (< 10% radius) - no point wasting frames
270
+ const radiusRatio = this.currentRadius / this.baseRadius;
271
+
272
+ if (radiusRatio > 0.1) {
273
+ // Base rotation speed from config
274
+ const baseSpeed = CONFIG.star.rotationSpeed ?? 0.5;
275
+
276
+ // Spin-up factor based on tidal progress (FSM-driven, smooth)
277
+ // Only significant spin-up during actual disruption (mass loss)
278
+ const massRatio = (this.mass || 1) / (this.initialMass || 1);
279
+ const massLoss = 1 - massRatio; // 0 = no loss, 1 = fully consumed
280
+
281
+ // Gentle spin-up from tidal stress, moderate spin-up from mass loss
282
+ // tidalProgress: 0-1 during stretch, 1 during disrupt
283
+ // massLoss: 0 during stretch, 0-1 during disrupt
284
+ const tidalSpinUp = 1 + this.tidalProgress * 0.3; // Up to 1.3x from tidal
285
+ const collapseSpinUp = 1 + massLoss * 1.5; // Up to 2.5x from collapse
286
+
287
+ const targetVelocity = baseSpeed * tidalSpinUp * collapseSpinUp;
288
+
289
+ // Very slow approach to target - no sudden jumps
290
+ const accelRate = 0.001;
291
+ this.angularVelocity += (targetVelocity - this.angularVelocity) * accelRate * dt;
292
+
293
+ // Hard cap on max spin (2.5 rad/s - calm, cosmic feel)
294
+ this.angularVelocity = Math.min(2.5, this.angularVelocity);
295
+ }
296
+ // else: keep current velocity, don't accelerate tiny remnant
297
+
298
+ this.rotation += this.angularVelocity * dt;
299
+
300
+ // Update breathing phase - slow, cosmic rhythm (0.3-0.5 Hz)
301
+ const breathingFreq = 0.3 + this.stressLevel * 0.2;
302
+ this.pulsationPhase += breathingFreq * dt * Math.PI * 2;
303
+
304
+ this.updateVisual();
305
+ }
306
+
307
+ render() {
308
+ super.render();
309
+ if (this.mass > 0 && this.visual) {
310
+ // Sync visual position with star position
311
+ this.visual.x = this.x;
312
+ this.visual.y = this.y || 0;
313
+ this.visual.z = this.z;
314
+ this.visual.render();
315
+ }
316
+ }
317
+ }
@@ -0,0 +1,356 @@
1
+ import { GameObject, Painter } from "../../../src/index.js";
2
+ import { applyGravitationalLensing } from "../../../src/math/gr.js";
3
+
4
+ /**
5
+ * TidalStream - Simple particle stream from star to black hole
6
+ *
7
+ * Physics:
8
+ * - Particles emitted from star inherit star's velocity
9
+ * - Gravity attracts particles toward black hole (0,0,0)
10
+ * - Gravitational lensing bends particle paths near the BH
11
+ */
12
+
13
+ // Stream-specific config
14
+ const STREAM_CONFIG = {
15
+ gravity: 120000, // Strong gravity (linear falloff G/r)
16
+ maxParticles: 5000,
17
+ particleLifetime: 12, // Seconds - long lifetime so particles can orbit the BH
18
+
19
+ // Velocity inheritance - how much of star's velocity particles get
20
+ // Lower = particles emit more "from" the star, not ahead of it
21
+ velocityInheritance: 0.3,
22
+
23
+ // Inward velocity - particles should FALL toward BH, not orbit
24
+ // This is the key to making particles flow INTO the black hole
25
+ inwardVelocity: 8, // Base inward velocity toward BH
26
+ inwardSpread: 15, // Random spread on inward velocity
27
+
28
+ // Tangent spread for S-shape - higher = more spread along orbit direction
29
+ tangentSpread: Math.PI * 150, // Spread for visible S-shape
30
+
31
+ // Emission offset: 1.0 = star's BH-facing edge (L1 Lagrange point)
32
+ // Positive = toward BH, negative = away from BH
33
+ emissionOffset: -1 * Math.PI, // Larger numbers create bigger S-Shape. Negative PI works very well here for some reason makes the animation very cool.
34
+
35
+ // Drag factor - removes angular momentum so orbits decay
36
+ // 1.0 = no drag, 0.99 = slight drag, 0.95 = strong drag
37
+ drag: 0.994,
38
+
39
+ // Colors: match star shader at emission, cool as they approach BH
40
+ colorHot: { r: 255, g: 95, b: 45 }, // Deep red-orange (matches star shader initial)
41
+ colorCool: { r: 180, g: 40, b: 15 }, // Darker red near BH
42
+
43
+ // Particle size
44
+ sizeMin: 1,
45
+ sizeMax: 3,
46
+
47
+ // Gravitational lensing (visual effect)
48
+ // These are multipliers relative to the BH's current radius
49
+ lensing: {
50
+ enabled: true,
51
+ effectRadiusMult: 6.0, // Effect extends to 6x BH radius
52
+ strengthMult: 2.5, // Strength scales with BH radius
53
+ falloff: 0.008, // Exponential falloff (higher = tighter effect)
54
+ minDistanceMult: 0.2, // Min distance as fraction of BH radius
55
+ },
56
+ };
57
+
58
+ export class TidalStream extends GameObject {
59
+ constructor(game, options = {}) {
60
+ super(game, options);
61
+
62
+ this.camera = options.camera;
63
+ this.scene = options.scene; // Scene reference for screen center
64
+ this.bhRadius = options.bhRadius ?? 50;
65
+
66
+ // Callbacks for particle lifecycle
67
+ this.onParticleConsumed = options.onParticleConsumed ?? null;
68
+ this.onParticleCaptured = options.onParticleCaptured ?? null;
69
+
70
+ // Particle array - simple flat structure
71
+ this.particles = [];
72
+ }
73
+
74
+ init() {
75
+ this.particles = [];
76
+ }
77
+
78
+ /**
79
+ * Emit a particle from the star
80
+ *
81
+ * For S-shape formation, particles need TANGENTIAL velocity spread:
82
+ * - Faster particles (more angular momentum) spiral outward
83
+ * - Slower particles (less angular momentum) spiral inward
84
+ * - This creates two opposing tails = S-shape
85
+ *
86
+ * @param {number} x - Star x position
87
+ * @param {number} y - Star y position
88
+ * @param {number} z - Star z position
89
+ * @param {number} vx - Star velocity x
90
+ * @param {number} vy - Star velocity y
91
+ * @param {number} vz - Star velocity z
92
+ * @param {number} starRadius - Current star radius (for position spread)
93
+ * @param {number} starRotation - Current star rotation (for angular offset)
94
+ * @param {Array<number>} starColor - Current star color as [r, g, b] normalized (0-1)
95
+ */
96
+ emit(x, y, z, vx, vy, vz, starRadius, starRotation = 0, starColor = null) {
97
+ if (this.particles.length >= STREAM_CONFIG.maxParticles) return;
98
+
99
+ const dist = Math.sqrt(x * x + z * z) || 1;
100
+
101
+ // Direction toward BH in x-z plane (unit vector)
102
+ const radialX = -x / dist;
103
+ const radialZ = -z / dist;
104
+
105
+ // Emit from star center with spread for visible "bleeding" effect
106
+ // Larger spread = bigger emission hole on the star
107
+ const emitX = x + (Math.random() - 0.5) * starRadius * 0.8;
108
+ const emitY = y + (Math.random() - 0.5) * starRadius * 0.8;
109
+ const emitZ = z + (Math.random() - 0.5) * starRadius * 0.8;
110
+
111
+ // Tangent is perpendicular to radial - gives the orbital direction
112
+ const tangentX = -radialZ;
113
+ const tangentZ = radialX;
114
+
115
+ // Reduce inherited velocity so gravity can dominate
116
+ const inheritedVx = vx * STREAM_CONFIG.velocityInheritance;
117
+ const inheritedVz = vz * STREAM_CONFIG.velocityInheritance;
118
+
119
+ // INWARD velocity - particles flow TOWARD the black hole
120
+ // radialX, radialZ point toward BH (origin)
121
+ const inward = STREAM_CONFIG.inwardVelocity + (Math.random() - 0.5) * STREAM_CONFIG.inwardSpread;
122
+
123
+ // Small tangential spread for the S-shape variation
124
+ const tangent = (Math.random() - 0.5) * STREAM_CONFIG.tangentSpread;
125
+
126
+ // Store star color at emission time (convert from normalized 0-1 to 0-255)
127
+ const emitColor = starColor
128
+ ? { r: starColor[0] * 255, g: starColor[1] * 255, b: starColor[2] * 255 }
129
+ : STREAM_CONFIG.colorHot;
130
+
131
+ this.particles.push({
132
+ x: emitX,
133
+ y: emitY,
134
+ z: emitZ,
135
+
136
+ // Velocity = inherited + INWARD toward BH + small tangent spread
137
+ vx: inheritedVx + radialX * inward + tangentX * tangent,
138
+ vy: vy,
139
+ vz: inheritedVz + radialZ * inward + tangentZ * tangent,
140
+
141
+ age: 0,
142
+ size: STREAM_CONFIG.sizeMin + Math.random() * (STREAM_CONFIG.sizeMax - STREAM_CONFIG.sizeMin),
143
+
144
+ // Track initial distance for color gradient
145
+ initialDist: dist,
146
+
147
+ // Store the star's color at emission time
148
+ emitColor,
149
+ });
150
+ }
151
+
152
+ updateDiskBounds(innerRadius, outerRadius) {
153
+ // Don't override bhRadius here - it's set by updateBHRadius
154
+ // We only care about disk bounds for potential capture detection
155
+ this.diskInnerRadius = innerRadius;
156
+ this.diskOuterRadius = outerRadius;
157
+ }
158
+
159
+ /**
160
+ * Update all particles - just gravity
161
+ */
162
+ update(dt) {
163
+ super.update(dt);
164
+
165
+ // Consume particles at the BH's visual edge (not inside it)
166
+ // Use 1.0x so particles disappear right at the event horizon
167
+ const accretionRadius = this.bhRadius * 1.1;
168
+
169
+ for (let i = this.particles.length - 1; i >= 0; i--) {
170
+ const p = this.particles[i];
171
+
172
+ p.age += dt;
173
+
174
+ // Remove old or accreted particles
175
+ if (p.age > STREAM_CONFIG.particleLifetime) {
176
+ this.particles.splice(i, 1);
177
+ continue;
178
+ }
179
+
180
+ // Skip physics on first frame - let particle appear at spawn point first
181
+ // This prevents the "jump" where particles move before being rendered
182
+ if (p.age < dt * 1.5) {
183
+ continue;
184
+ }
185
+
186
+ // Distance to BH (at origin)
187
+ const dist = Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z);
188
+
189
+ // Accreted by black hole?
190
+ if (dist < accretionRadius) {
191
+ this.particles.splice(i, 1);
192
+ // Trigger callback - feeds the black hole's glow!
193
+ if (this.onParticleConsumed) {
194
+ this.onParticleConsumed();
195
+ }
196
+ continue;
197
+ }
198
+
199
+ // Gravity: F = G/r (linear falloff for better visuals)
200
+ // Linear falloff keeps gravity significant at larger distances
201
+ const gravity = STREAM_CONFIG.gravity / dist;
202
+ const dirX = -p.x * 2 / dist;
203
+ const dirY = -p.y * 2 / dist;
204
+ const dirZ = -p.z * 2 / dist;
205
+
206
+ // Apply gravity acceleration
207
+ p.vx += dirX * gravity * dt;
208
+ p.vy += dirY * gravity * dt;
209
+ p.vz += dirZ * gravity * dt;
210
+
211
+ // Apply drag - removes angular momentum so particles spiral inward
212
+ // Without drag, particles would orbit forever
213
+ p.vx *= STREAM_CONFIG.drag;
214
+ p.vy *= STREAM_CONFIG.drag;
215
+ p.vz *= STREAM_CONFIG.drag;
216
+
217
+ // Move particle
218
+ p.x += p.vx * dt;
219
+ p.y += p.vy * dt;
220
+ p.z += p.vz * dt;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Clear all particles
226
+ */
227
+ clear() {
228
+ this.particles = [];
229
+ }
230
+
231
+ /**
232
+ * Update BH radius (for accretion check)
233
+ */
234
+ updateBHRadius(radius) {
235
+ this.bhRadius = radius;
236
+ }
237
+
238
+ /**
239
+ * Render particles
240
+ * We reset the canvas transform to identity since Scene3D applies its own
241
+ * transforms, and we need absolute screen coordinates for particle rendering.
242
+ */
243
+ render() {
244
+ super.render();
245
+
246
+ if (!this.camera || this.particles.length === 0) return;
247
+
248
+ // Get the actual canvas transform that Scene3D has set up
249
+ // This is the same approach Sphere3D uses to get screen position
250
+ const ctx = Painter.ctx;
251
+ const transform = ctx.getTransform();
252
+
253
+ // TidalStream is at world (0,0,0), so Scene3D translated to:
254
+ // scene.x + project(0,0,0).x which is approximately scene.x
255
+ // We need to use this as our center, then add particle projections
256
+ const cx = transform.e;
257
+ const cy = transform.f;
258
+
259
+ // Build render list with projection
260
+ const renderList = [];
261
+
262
+ // Young particles stay invisible (appear to emerge from star)
263
+ const fadeInTime = 0.05; // seconds before particles become visible
264
+ const fadeInDuration = 0.1; // seconds to fade from invisible to full opacity
265
+
266
+ for (const p of this.particles) {
267
+ // Skip very young particles - they're "inside" the star
268
+ if (p.age < fadeInTime) continue;
269
+
270
+ const projected = this.camera.project(p.x, p.y, p.z);
271
+
272
+ // Skip if behind camera
273
+ if (projected.scale <= 0) continue;
274
+
275
+ // Fade in young particles (after fadeInTime threshold)
276
+ const fadeInProgress = Math.min(1, (p.age - fadeInTime) / fadeInDuration);
277
+
278
+ // Distance from BH for color
279
+ const dist = Math.sqrt(p.x * p.x + p.z * p.z);
280
+ const colorT = Math.min(1, dist / (p.initialDist || 1));
281
+
282
+ // Use particle's emitted color (star color at emission time)
283
+ const hotColor = p.emitColor || STREAM_CONFIG.colorHot;
284
+
285
+ // Lerp color: cool near BH, hot (star color) near initial position
286
+ const color = {
287
+ r: STREAM_CONFIG.colorCool.r + (hotColor.r - STREAM_CONFIG.colorCool.r) * colorT,
288
+ g: STREAM_CONFIG.colorCool.g + (hotColor.g - STREAM_CONFIG.colorCool.g) * colorT,
289
+ b: STREAM_CONFIG.colorCool.b + (hotColor.b - STREAM_CONFIG.colorCool.b) * colorT,
290
+ };
291
+
292
+ // Fade with age (fade out at end of life) and fade in at birth
293
+ const fadeOutAlpha = Math.max(0, 1 - p.age / STREAM_CONFIG.particleLifetime);
294
+ const alpha = fadeOutAlpha * fadeInProgress; // Combine fade-in and fade-out
295
+
296
+ // Apply gravitational lensing to screen coordinates
297
+ // Scale lensing with BH's current (pulsing) radius
298
+ let screenX = projected.x;
299
+ let screenY = projected.y;
300
+
301
+ if (STREAM_CONFIG.lensing.enabled && this.bhRadius > 0) {
302
+ const effectRadius = this.bhRadius * STREAM_CONFIG.lensing.effectRadiusMult;
303
+ const strength = this.bhRadius * STREAM_CONFIG.lensing.strengthMult;
304
+ const minDist = this.bhRadius * STREAM_CONFIG.lensing.minDistanceMult;
305
+
306
+ const lensed = applyGravitationalLensing(
307
+ screenX, screenY,
308
+ effectRadius,
309
+ strength,
310
+ STREAM_CONFIG.lensing.falloff,
311
+ minDist
312
+ );
313
+ screenX = lensed.x;
314
+ screenY = lensed.y;
315
+ }
316
+
317
+ // Check if particle is visually inside the black hole
318
+ const screenDist = Math.sqrt(screenX * screenX + screenY * screenY);
319
+ const insideBH = screenDist < this.bhRadius;
320
+
321
+ // Screen position = center + lensed offset
322
+ renderList.push({
323
+ x: cx + screenX,
324
+ y: cy + screenY,
325
+ z: projected.z,
326
+ size: p.size * projected.scale,
327
+ color: insideBH ? { r: 0, g: 0, b: 0 } : color,
328
+ alpha,
329
+ });
330
+ }
331
+
332
+ // Sort back to front
333
+ renderList.sort((a, b) => b.z - a.z);
334
+
335
+ // Draw particles with reset transform (absolute screen coords)
336
+ Painter.useCtx((ctx) => {
337
+ // Reset to identity matrix - Scene3D has applied transforms we need to bypass
338
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
339
+
340
+ ctx.globalCompositeOperation = "lighter";
341
+
342
+ for (const item of renderList) {
343
+ const r = Math.round(item.color.r);
344
+ const g = Math.round(item.color.g);
345
+ const b = Math.round(item.color.b);
346
+
347
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${item.alpha})`;
348
+ ctx.beginPath();
349
+ ctx.arc(item.x, item.y, item.size, 0, Math.PI * 2);
350
+ ctx.fill();
351
+ }
352
+
353
+ ctx.globalCompositeOperation = "source-over";
354
+ });
355
+ }
356
+ }