@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,812 @@
1
+ /**
2
+ * Star - Deformable stellar object for TDE demo
3
+ *
4
+ * A star on an elliptical orbit that gets tidally disrupted.
5
+ * Uses proper orbital mechanics with cyclone-like particle ejection.
6
+ */
7
+ import {
8
+ GameObject,
9
+ ParticleSystem,
10
+ Easing,
11
+ Painter,
12
+ } from "../../../src/index.js";
13
+ import { polarToCartesian } from "../../../src/math/gr.js";
14
+
15
+ const CONFIG = {
16
+ // Star colors (yellow-white to orange core)
17
+ colorOuter: { r: 255, g: 245, b: 220 },
18
+ colorCore: { r: 255, g: 200, b: 100 },
19
+
20
+ // Particle sizes - visible but not too big
21
+ sizeMin: 1,
22
+ sizeMax: 3,
23
+ coronaExtent: 1.5, // Particles extend to 1.5x the star radius (corona)
24
+
25
+ // Orbital physics - star starts FAR and falls IN
26
+ initialOrbitAngle: Math.PI * 0.8, // Start position
27
+ orbitSpeed: 0.4, // Slower orbit for more drama
28
+ periapsisRatio: 0.08, // Close approach (grazes BH)
29
+ apoapsisRatio: 0.6, // Start FARTHER out for more dramatic approach
30
+
31
+ // Tidal effects - DRAMATIC spaghettification like the reference image
32
+ stretchMaxFactor: 8.0, // More extreme stretching
33
+ tidalRadiusRatio: 0.12, // Where tidal forces rip the star
34
+ tearDropFactor: 2.5, // How much particles toward BH stretch more
35
+
36
+ // Star body glow (smaller than corona)
37
+ glowRadius: 1.4,
38
+ bodyRadius: 0.6, // Core body is smaller than particle cloud
39
+
40
+ // Depth perception - star SHRINKS as it falls toward BH
41
+ perspectiveMultiplier: 1.0, // Base scale (no extra multiplier)
42
+ // DRAMATIC size change as star approaches
43
+ distanceScaleMax: 2.5, // BIG when far from BH (at start)
44
+ distanceScaleMin: 0.3, // SMALL when close to BH (falling in)
45
+
46
+ // Particle streaming toward BH
47
+ streamSpeed: 80, // How fast particles stream toward BH
48
+ streamSpread: 0.3, // Angular spread when streaming
49
+
50
+ // STAR PARTICLE DRIFT - particles drift toward BH while still attached
51
+ // MUCH STRONGER for visible streaming effect from the start
52
+ driftStartProgress: 0.0, // Drift starts immediately
53
+ driftStrength: 300, // Stronger drift for visible streaming
54
+ driftAcceleration: 3.0, // Faster acceleration toward BH
55
+ };
56
+
57
+ /**
58
+ * Simple particle data class for star particles.
59
+ */
60
+ class StarParticle {
61
+ constructor(options = {}) {
62
+ // Offset from star center (local space)
63
+ this.offsetX = options.offsetX ?? 0;
64
+ this.offsetY = options.offsetY ?? 0;
65
+ this.offsetZ = options.offsetZ ?? 0;
66
+
67
+ // Original offset (for deformation)
68
+ this.baseOffsetX = this.offsetX;
69
+ this.baseOffsetY = this.offsetY;
70
+ this.baseOffsetZ = this.offsetZ;
71
+
72
+ // Distance from center (for release ordering)
73
+ this.baseDist = Math.sqrt(
74
+ this.baseOffsetX ** 2 + this.baseOffsetY ** 2 + this.baseOffsetZ ** 2,
75
+ );
76
+
77
+ // Appearance
78
+ this.size = options.size ?? 3;
79
+ this.color = options.color ?? { r: 255, g: 240, b: 200, a: 1 };
80
+
81
+ // State
82
+ this.released = false;
83
+ this.releasedAt = 0;
84
+
85
+ // Spiral parameters (set on release)
86
+ this.spiralPhase = Math.random() * Math.PI * 2;
87
+ this.spiralSpeed = 0.5 + Math.random() * 1.5;
88
+
89
+ // DRIFT toward black hole (while still attached to star)
90
+ this.driftX = 0;
91
+ this.driftY = 0;
92
+ this.driftZ = 0;
93
+ this.driftVelX = 0;
94
+ this.driftVelY = 0;
95
+ this.driftVelZ = 0;
96
+ }
97
+ }
98
+
99
+ export class Star extends GameObject {
100
+ /**
101
+ * @param {Game} game - Game instance
102
+ * @param {Object} options
103
+ * @param {Camera3D} options.camera - Camera for projection
104
+ * @param {number} options.radius - Star radius
105
+ * @param {number} options.particleCount - Number of particles
106
+ * @param {number} options.baseScale - Base scale for sizing
107
+ * @param {number} options.startDistance - Initial distance from black hole
108
+ */
109
+ constructor(game, options = {}) {
110
+ super(game, options);
111
+
112
+ this.camera = options.camera;
113
+ this.radius = options.radius ?? 50;
114
+ this.particleCount = options.particleCount ?? 600;
115
+ this.baseScale = options.baseScale ?? 500;
116
+ this.startDistance = options.startDistance ?? 200;
117
+
118
+ // Orbital state
119
+ this.orbitAngle = CONFIG.initialOrbitAngle;
120
+ this.orbitRadius = this.startDistance;
121
+ this.periapsis = this.baseScale * CONFIG.periapsisRatio;
122
+ this.apoapsis = this.baseScale * CONFIG.apoapsisRatio;
123
+
124
+ // Star position (world space)
125
+ this.centerX = 0;
126
+ this.centerY = 0;
127
+ this.centerZ = 0;
128
+
129
+ // Star state
130
+ this.stretchFactor = 1.0;
131
+ this.intactRatio = 1.0;
132
+ this.phase = "approach";
133
+
134
+ // Cached render state (computed in update, used in draw)
135
+ this.screenX = 0;
136
+ this.screenY = 0;
137
+ this.effectiveRadius = 0;
138
+ this.cameraZ = 0;
139
+ this.perspectiveScale = 1.0; // Camera perspective scale
140
+ this.distanceScale = CONFIG.distanceScaleMax; // Start at max (far from BH)
141
+
142
+ // Particles
143
+ this.particles = [];
144
+
145
+ // Particle system for rendering
146
+ this.particleSystem = null;
147
+ }
148
+
149
+ /**
150
+ * Initialize the star.
151
+ */
152
+ init() {
153
+ this.createParticleSystem();
154
+ this.initParticles();
155
+ this.resetPosition();
156
+ }
157
+
158
+ /**
159
+ * Create the particle system for rendering.
160
+ */
161
+ createParticleSystem() {
162
+ this.particleSystem = new ParticleSystem(this.game, {
163
+ maxParticles: this.particleCount,
164
+ camera: this.camera,
165
+ depthSort: true,
166
+ blendMode: "lighter",
167
+ updaters: [],
168
+ });
169
+ }
170
+
171
+ /**
172
+ * Create spherical distribution of particles.
173
+ * Particles extend beyond the star body to form a corona.
174
+ */
175
+ initParticles() {
176
+ this.particles = [];
177
+
178
+ // Corona radius - particles extend beyond the solid body
179
+ const coronaRadius = this.radius * CONFIG.coronaExtent;
180
+
181
+ for (let i = 0; i < this.particleCount; i++) {
182
+ // Spherical distribution
183
+ const u = Math.random();
184
+ const v = Math.random();
185
+ const theta = 2 * Math.PI * u;
186
+ const phi = Math.acos(2 * v - 1);
187
+
188
+ // Distribution biased toward outer edge for corona effect
189
+ // More particles in the corona, fewer in the core
190
+ const r = coronaRadius * Math.pow(Math.random(), 0.8);
191
+
192
+ const offsetX = r * Math.sin(phi) * Math.cos(theta);
193
+ const offsetY = r * Math.sin(phi) * Math.sin(theta);
194
+ const offsetZ = r * Math.cos(phi);
195
+
196
+ // Color based on distance from center (hotter core)
197
+ const distRatio = r / coronaRadius;
198
+ const color = this.lerpColor(
199
+ CONFIG.colorCore,
200
+ CONFIG.colorOuter,
201
+ distRatio,
202
+ );
203
+
204
+ // Size - larger particles in the corona, smaller in core
205
+ // This makes the outer glow more prominent
206
+ const size =
207
+ CONFIG.sizeMin + (CONFIG.sizeMax - CONFIG.sizeMin) * distRatio;
208
+
209
+ // Alpha - brighter core, fainter corona
210
+ const alpha = 1.0 - distRatio * 0.5;
211
+
212
+ this.particles.push(
213
+ new StarParticle({
214
+ offsetX,
215
+ offsetY,
216
+ offsetZ,
217
+ size,
218
+ color: { ...color, a: alpha },
219
+ }),
220
+ );
221
+ }
222
+ }
223
+
224
+ lerpColor(c1, c2, t) {
225
+ return {
226
+ r: c1.r + (c2.r - c1.r) * t,
227
+ g: c1.g + (c2.g - c1.g) * t,
228
+ b: c1.b + (c2.b - c1.b) * t,
229
+ };
230
+ }
231
+
232
+ resetPosition() {
233
+ this.orbitAngle = CONFIG.initialOrbitAngle;
234
+ this.orbitRadius = this.startDistance;
235
+ const pos = polarToCartesian(this.orbitRadius, this.orbitAngle);
236
+ this.centerX = pos.x;
237
+ this.centerY = 0;
238
+ this.centerZ = pos.z;
239
+ }
240
+
241
+ reset() {
242
+ this.stretchFactor = 1.0;
243
+ this.intactRatio = 1.0;
244
+ this.orbitAngle = CONFIG.initialOrbitAngle;
245
+ this.initParticles();
246
+ this.resetPosition();
247
+ this.visible = true;
248
+ // Reset all particle drift
249
+ for (const p of this.particles) {
250
+ p.driftX = 0;
251
+ p.driftY = 0;
252
+ p.driftZ = 0;
253
+ p.driftVelX = 0;
254
+ p.driftVelY = 0;
255
+ p.driftVelZ = 0;
256
+ }
257
+ }
258
+
259
+ updateSizing(radius, baseScale) {
260
+ this.radius = radius;
261
+ this.baseScale = baseScale;
262
+ this.startDistance = baseScale * CONFIG.apoapsisRatio;
263
+ this.periapsis = baseScale * CONFIG.periapsisRatio;
264
+ this.apoapsis = baseScale * CONFIG.apoapsisRatio;
265
+ }
266
+
267
+ startApproach() {
268
+ this.phase = "approach";
269
+ this.resetPosition();
270
+ }
271
+
272
+ startStretch() {
273
+ this.phase = "stretch";
274
+ }
275
+
276
+ startDisrupt() {
277
+ this.phase = "disrupt";
278
+ }
279
+
280
+ /**
281
+ * Apply tidal stretching during approach phase.
282
+ * This creates the initial teardrop deformation before full stretch phase.
283
+ */
284
+ applyTidalStretch(intensity) {
285
+ if (intensity <= 0) return;
286
+
287
+ // Direction toward black hole
288
+ const dist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
289
+ const dirX = -this.centerX / dist;
290
+ const dirZ = -this.centerZ / dist;
291
+
292
+ // Mild stretching during approach
293
+ const stretchAmount = 1 + intensity * (CONFIG.stretchMaxFactor - 1) * 0.3;
294
+
295
+ for (const p of this.particles) {
296
+ if (p.released) continue;
297
+
298
+ const dot = p.baseOffsetX * dirX + p.baseOffsetZ * dirZ;
299
+ const normalizedDot = dot / (this.radius * CONFIG.coronaExtent);
300
+
301
+ // Teardrop effect - particles toward BH stretch more
302
+ const tearDropMult =
303
+ 1 + Math.max(0, normalizedDot) * CONFIG.tearDropFactor * intensity;
304
+ const actualStretch = stretchAmount * tearDropMult;
305
+ const compressAmount = 1 / Math.sqrt(actualStretch);
306
+
307
+ p.offsetX =
308
+ p.baseOffsetX * compressAmount + dirX * dot * (actualStretch - 1);
309
+ p.offsetZ =
310
+ p.baseOffsetZ * compressAmount + dirZ * dot * (actualStretch - 1);
311
+ p.offsetY = p.baseOffsetY * compressAmount;
312
+ }
313
+
314
+ this.stretchFactor = stretchAmount;
315
+ }
316
+
317
+ /**
318
+ * Apply particle drift toward black hole.
319
+ * Particles on the BH-facing side start drifting toward the BH
320
+ * while still attached to the star - creates streaming effect.
321
+ */
322
+ applyParticleDrift(dt, intensity) {
323
+ if (intensity <= 0) return;
324
+
325
+ // Direction toward black hole (world space, from star center)
326
+ const starDist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
327
+ const toBHx = -this.centerX / starDist;
328
+ const toBHz = -this.centerZ / starDist;
329
+
330
+ for (const p of this.particles) {
331
+ if (p.released) continue;
332
+
333
+ // How much this particle faces the BH (positive = toward BH)
334
+ const offsetDist = Math.sqrt(p.offsetX ** 2 + p.offsetZ ** 2) || 1;
335
+ const facingBH = (p.offsetX * toBHx + p.offsetZ * toBHz) / offsetDist;
336
+
337
+ // Only particles facing the BH drift toward it
338
+ if (facingBH > 0) {
339
+ // Drift strength increases for particles more toward BH
340
+ const driftMult = facingBH * CONFIG.driftAcceleration * intensity;
341
+
342
+ // Accelerate toward BH
343
+ p.driftVelX += toBHx * CONFIG.driftStrength * driftMult * dt;
344
+ p.driftVelZ += toBHz * CONFIG.driftStrength * driftMult * dt;
345
+ p.driftVelY *= 0.95; // Dampen vertical drift
346
+
347
+ // Apply velocity to drift position
348
+ p.driftX += p.driftVelX * dt;
349
+ p.driftY += p.driftVelY * dt;
350
+ p.driftZ += p.driftVelZ * dt;
351
+
352
+ // Damping to prevent runaway
353
+ p.driftVelX *= 0.98;
354
+ p.driftVelZ *= 0.98;
355
+ }
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Compute orbital radius for elliptical orbit.
361
+ * r = a(1-e²) / (1 + e*cos(θ))
362
+ */
363
+ getOrbitalRadius(angle, progress) {
364
+ const a = (this.periapsis + this.apoapsis) / 2;
365
+ const e =
366
+ (this.apoapsis - this.periapsis) / (this.apoapsis + this.periapsis);
367
+ return (a * (1 - e * e)) / (1 + e * Math.cos(angle));
368
+ }
369
+
370
+ /**
371
+ * Update during approach phase - star spirals in on elliptical orbit.
372
+ */
373
+ updateApproach(dt, progress) {
374
+ // Save previous position to calculate velocity
375
+ const prevX = this.centerX;
376
+ const prevZ = this.centerZ;
377
+
378
+ // Advance orbital angle (faster as it approaches periapsis - Kepler's 2nd law)
379
+ const speedFactor = 1 + progress * 2;
380
+ this.orbitAngle += dt * CONFIG.orbitSpeed * speedFactor;
381
+
382
+ // Interpolate toward periapsis
383
+ const t = Easing.easeInQuad(progress);
384
+ this.orbitRadius = Easing.lerp(this.startDistance, this.periapsis * 1.5, t);
385
+
386
+ // Update position
387
+ const pos = polarToCartesian(this.orbitRadius, this.orbitAngle);
388
+ this.centerX = pos.x;
389
+ this.centerZ = pos.z;
390
+
391
+ // Slight vertical oscillation
392
+ this.centerY = Math.sin(this.orbitAngle * 2) * this.baseScale * 0.02;
393
+
394
+ // Calculate velocity (units per second)
395
+ if (dt > 0) {
396
+ this.velocityX = (this.centerX - prevX) / dt;
397
+ this.velocityZ = (this.centerZ - prevZ) / dt;
398
+ this.velocityY = (this.centerY - 0) / dt; // approx
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Update during stretch phase - orbiting at periapsis, tidal stretching.
404
+ * Creates TEARDROP spaghettification like the reference image.
405
+ */
406
+ updateStretch(dt, progress, bhPosition) {
407
+ const prevX = this.centerX;
408
+ const prevZ = this.centerZ;
409
+
410
+ // Continue orbiting faster at periapsis
411
+ const speedFactor = 2 + progress;
412
+ this.orbitAngle += dt * CONFIG.orbitSpeed * speedFactor;
413
+
414
+ // Stay near periapsis
415
+ const t = Easing.easeInOutQuad(progress);
416
+ this.orbitRadius = Easing.lerp(this.periapsis * 1.5, this.periapsis, t);
417
+
418
+ const pos = polarToCartesian(this.orbitRadius, this.orbitAngle);
419
+ this.centerX = pos.x;
420
+ this.centerZ = pos.z;
421
+ this.centerY = Math.sin(this.orbitAngle * 2) * this.baseScale * 0.015;
422
+
423
+ // Calculate velocity
424
+ if (dt > 0) {
425
+ this.velocityX = (this.centerX - prevX) / dt;
426
+ this.velocityZ = (this.centerZ - prevZ) / dt;
427
+ this.velocityY = (this.centerY - 0) / dt;
428
+ }
429
+
430
+ // Apply tidal stretching - increases dramatically with progress
431
+ this.stretchFactor =
432
+ 1 + (CONFIG.stretchMaxFactor - 1) * Easing.easeInQuad(progress);
433
+
434
+ // Direction toward black hole
435
+ const dist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
436
+ const dirX = -this.centerX / dist;
437
+ const dirZ = -this.centerZ / dist;
438
+
439
+ // TEARDROP deformation - particles facing BH stretch MORE
440
+ for (const p of this.particles) {
441
+ if (p.released) continue;
442
+
443
+ // How much this particle faces the BH (-1 to 1)
444
+ const dot = p.baseOffsetX * dirX + p.baseOffsetZ * dirZ;
445
+ const normalizedDot = dot / (this.radius * CONFIG.coronaExtent);
446
+
447
+ // Particles toward BH (dot > 0) stretch MORE than those away
448
+ // This creates the teardrop/spaghetti shape
449
+ const tearDropMult =
450
+ 1 + Math.max(0, normalizedDot) * CONFIG.tearDropFactor * progress;
451
+ const stretchAmount = this.stretchFactor * tearDropMult;
452
+ const compressAmount = 1 / Math.sqrt(stretchAmount);
453
+
454
+ // Apply asymmetric stretching
455
+ p.offsetX =
456
+ p.baseOffsetX * compressAmount + dirX * dot * (stretchAmount - 1);
457
+ p.offsetZ =
458
+ p.baseOffsetZ * compressAmount + dirZ * dot * (stretchAmount - 1);
459
+ p.offsetY = p.baseOffsetY * compressAmount;
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Update during disrupt phase - extreme stretching, particles spiral off.
465
+ * Creates extreme spaghettification as star is torn apart.
466
+ */
467
+ updateDisrupt(dt, progress) {
468
+ const prevX = this.centerX;
469
+ const prevZ = this.centerZ;
470
+
471
+ // Continue orbiting past periapsis
472
+ const speedFactor = 3 - progress * 1.5;
473
+ this.orbitAngle += dt * CONFIG.orbitSpeed * speedFactor;
474
+
475
+ // Move outward slightly as star is disrupted
476
+ const t = Easing.easeOutQuad(progress);
477
+ this.orbitRadius = Easing.lerp(this.periapsis, this.periapsis * 1.5, t);
478
+
479
+ const pos = polarToCartesian(this.orbitRadius, this.orbitAngle);
480
+ this.centerX = pos.x;
481
+ this.centerZ = pos.z;
482
+
483
+ // Calculate velocity
484
+ if (dt > 0) {
485
+ this.velocityX = (this.centerX - prevX) / dt;
486
+ this.velocityZ = (this.centerZ - prevZ) / dt;
487
+ this.velocityY = (this.centerY - 0) / dt;
488
+ }
489
+
490
+ // EXTREME stretching during disruption
491
+ this.stretchFactor = CONFIG.stretchMaxFactor * (1 + progress * 3);
492
+
493
+ const dist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
494
+ const dirX = -this.centerX / dist;
495
+ const dirZ = -this.centerZ / dist;
496
+
497
+ // TEARDROP deformation continues with even more extreme stretching
498
+ for (const p of this.particles) {
499
+ if (p.released) continue;
500
+
501
+ const dot = p.baseOffsetX * dirX + p.baseOffsetZ * dirZ;
502
+ const normalizedDot = dot / (this.radius * CONFIG.coronaExtent);
503
+
504
+ // Even more extreme teardrop during disruption
505
+ const tearDropMult =
506
+ 1 + Math.max(0, normalizedDot) * CONFIG.tearDropFactor * 2;
507
+ const stretchAmount = this.stretchFactor * tearDropMult;
508
+ const compressAmount = 1 / Math.sqrt(stretchAmount);
509
+
510
+ p.offsetX =
511
+ p.baseOffsetX * compressAmount + dirX * dot * (stretchAmount - 1);
512
+ p.offsetZ =
513
+ p.baseOffsetZ * compressAmount + dirZ * dot * (stretchAmount - 1);
514
+ p.offsetY = p.baseOffsetY * compressAmount;
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Release particles that stream toward the black hole.
520
+ * ONLY particles on BH-facing side release, and they ALWAYS flow toward BH.
521
+ */
522
+ releaseParticles(progress) {
523
+ const released = [];
524
+
525
+ // Direction toward black hole from star center
526
+ const starDist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
527
+ const toBHx = -this.centerX / starDist;
528
+ const toBHz = -this.centerZ / starDist;
529
+
530
+ // Release rate increases with progress
531
+ const releaseRate = 0.003 + progress * progress * 0.02;
532
+ // Allow up to 60% to be released organically
533
+ const maxReleased = 0.6;
534
+ if (this.intactRatio < 1 - maxReleased) return released;
535
+
536
+ for (const p of this.particles) {
537
+ if (p.released) continue;
538
+
539
+ // How much this particle faces the BH (1 = directly toward BH, -1 = away)
540
+ const offsetDist = Math.sqrt(p.offsetX ** 2 + p.offsetZ ** 2) || 1;
541
+ const facingBH = (p.offsetX * toBHx + p.offsetZ * toBHz) / offsetDist;
542
+
543
+ // ONLY release particles on BH-facing side (facingBH > 0)
544
+ // Particles on the far side of star should NOT release
545
+ if (facingBH < 0.1) continue;
546
+
547
+ // Outer corona releases easier
548
+ const stretchedDist = Math.sqrt(
549
+ p.offsetX ** 2 + p.offsetY ** 2 + p.offsetZ ** 2,
550
+ );
551
+ const releaseScore = facingBH * 0.6 + (stretchedDist / this.radius) * 0.4;
552
+
553
+ // Threshold decreases with progress
554
+ const threshold = 0.6 - progress * 0.5;
555
+
556
+ if (releaseScore > threshold && Math.random() < releaseRate) {
557
+ p.released = true;
558
+ p.releasedAt = progress;
559
+
560
+ // World position of released particle
561
+ const worldX = this.centerX + p.offsetX + p.driftX;
562
+ const worldY = this.centerY + p.offsetY + p.driftY;
563
+ const worldZ = this.centerZ + p.offsetZ + p.driftZ;
564
+
565
+ // Inherit Star Velocity + Ejection Kick
566
+ // This is key for the "S" shape - conservation of momentum
567
+ const ejectionSpeed = 10 + Math.random() * 20;
568
+
569
+ released.push({
570
+ x: worldX,
571
+ y: worldY,
572
+ z: worldZ,
573
+ // Velocity = Star Velocity + Ejection (toward BH)
574
+ vx: (this.velocityX || 0) + toBHx * ejectionSpeed,
575
+ vy: (this.velocityY || 0) + (Math.random() - 0.5) * 5,
576
+ vz: (this.velocityZ || 0) + toBHz * ejectionSpeed,
577
+ size: p.size,
578
+ color: { ...p.color },
579
+ });
580
+ }
581
+ }
582
+
583
+ this.intactRatio =
584
+ this.particles.filter((p) => !p.released).length / this.particles.length;
585
+ return released;
586
+ }
587
+
588
+ /**
589
+ * Release all remaining particles streaming toward BH.
590
+ */
591
+ releaseAllParticles() {
592
+ const released = [];
593
+
594
+ const starDist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
595
+ const tangentX = this.centerZ / starDist;
596
+ const tangentZ = -this.centerX / starDist;
597
+
598
+ for (const p of this.particles) {
599
+ if (p.released) continue;
600
+
601
+ p.released = true;
602
+
603
+ const worldX = this.centerX + p.offsetX;
604
+ const worldY = this.centerY + p.offsetY;
605
+ const worldZ = this.centerZ + p.offsetZ;
606
+
607
+ // Stream toward BH center
608
+ const toCenter = Math.sqrt(worldX ** 2 + worldZ ** 2) || 1;
609
+ const toCenterX = -worldX / toCenter;
610
+ const toCenterZ = -worldZ / toCenter;
611
+
612
+ const speed = CONFIG.streamSpeed * (0.4 + Math.random() * 0.6);
613
+ const spread = (Math.random() - 0.5) * CONFIG.streamSpread * 2;
614
+
615
+ released.push({
616
+ x: worldX,
617
+ y: worldY,
618
+ z: worldZ,
619
+ vx:
620
+ toCenterX * speed * 0.6 +
621
+ tangentX * speed * 0.4 +
622
+ spread * tangentX * speed,
623
+ vy: -Math.abs(p.offsetY) * 0.3,
624
+ vz:
625
+ toCenterZ * speed * 0.6 +
626
+ tangentZ * speed * 0.4 +
627
+ spread * tangentZ * speed,
628
+ size: p.size,
629
+ color: { ...p.color },
630
+ });
631
+ }
632
+
633
+ this.intactRatio = 0;
634
+ this.visible = false;
635
+ return released;
636
+ }
637
+
638
+ /**
639
+ * Update - compute render state for use in draw().
640
+ */
641
+ update(dt) {
642
+ super.update(dt);
643
+
644
+ // Compute screen position and effective radius for rendering
645
+ if (this.camera && this.visible && this.intactRatio > 0.01) {
646
+ const projected = this.camera.project(
647
+ this.centerX,
648
+ this.centerY,
649
+ this.centerZ,
650
+ );
651
+ this.screenX = this.game.width / 2 + projected.x;
652
+ this.screenY = this.game.height / 2 + projected.y;
653
+
654
+ // Calculate camera-space z for z-ordering
655
+ this.cameraZ = this.getCameraZ();
656
+
657
+ // DISTANCE-BASED SCALING: Star SHRINKS as it approaches black hole
658
+ // This creates the effect of falling "into" the scene toward the BH
659
+ const distToBH = Math.sqrt(
660
+ this.centerX ** 2 + this.centerY ** 2 + this.centerZ ** 2,
661
+ );
662
+
663
+ // Normalize: 0 at periapsis (close), 1 at apoapsis (far)
664
+ const normalizedDist = Math.max(
665
+ 0,
666
+ Math.min(
667
+ 1,
668
+ (distToBH - this.periapsis) / (this.apoapsis - this.periapsis),
669
+ ),
670
+ );
671
+
672
+ // Scale: big when far (1.8x), small when close (0.4x)
673
+ this.distanceScale =
674
+ CONFIG.distanceScaleMin +
675
+ (CONFIG.distanceScaleMax - CONFIG.distanceScaleMin) * normalizedDist;
676
+
677
+ // Combined scale: distance + camera perspective + consumption
678
+ this.perspectiveScale = projected.scale;
679
+ const shrinkFactor = Math.pow(this.intactRatio, 0.7);
680
+
681
+ this.effectiveRadius =
682
+ this.radius *
683
+ shrinkFactor *
684
+ this.distanceScale *
685
+ CONFIG.perspectiveMultiplier;
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Get the star's z-position in camera space.
691
+ * Used for z-ordering relative to black hole.
692
+ */
693
+ getCameraZ() {
694
+ if (!this.camera) return 0;
695
+
696
+ // Transform star center to camera space
697
+ const cosY = Math.cos(this.camera.rotationY);
698
+ const sinY = Math.sin(this.camera.rotationY);
699
+ let zCam = this.centerX * sinY + this.centerZ * cosY;
700
+
701
+ const cosX = Math.cos(this.camera.rotationX);
702
+ const sinX = Math.sin(this.camera.rotationX);
703
+ zCam = this.centerY * sinX + zCam * cosX;
704
+
705
+ return zCam;
706
+ }
707
+
708
+ /**
709
+ * Draw the star body and particle texture.
710
+ * Uses CAMERA PERSPECTIVE for correct size scaling.
711
+ * ALPHA IS CONSTANT - no transparency during approach/disruption.
712
+ */
713
+ render() {
714
+ super.render();
715
+ if (!this.visible) return;
716
+ if (this.intactRatio <= 0.01) return;
717
+
718
+ // SIZE shrinks when disrupted, but ALPHA stays constant (no transparency)
719
+ const sizeFactor =
720
+ this.intactRatio > 0.6 ? 1.0 : Math.pow(this.intactRatio / 0.6, 0.5);
721
+
722
+ const cx = this.game.width / 2;
723
+ const cy = this.game.height / 2;
724
+
725
+ // Use DISTANCE-BASED scaling (computed in update)
726
+ // Star shrinks as it approaches BH
727
+ const distScale = this.distanceScale || 1.0;
728
+
729
+ // Base size scaled by DISTANCE TO BLACK HOLE
730
+ // Far = big (1.8x), Close = small (0.4x)
731
+ const baseVisualSize =
732
+ this.radius * distScale * CONFIG.perspectiveMultiplier;
733
+
734
+ Painter.useCtx((ctx) => {
735
+ // Draw each star particle with distance-based scaling
736
+ for (const p of this.particles) {
737
+ if (p.released) continue;
738
+
739
+ // World position INCLUDING DRIFT toward black hole
740
+ const wx = this.centerX + p.offsetX + p.driftX;
741
+ const wy = this.centerY + p.offsetY + p.driftY;
742
+ const wz = this.centerZ + p.offsetZ + p.driftZ;
743
+
744
+ // Project through camera
745
+ const projected = this.camera.project(wx, wy, wz);
746
+ const screenX = cx + projected.x;
747
+ const screenY = cy + projected.y;
748
+
749
+ // Skip if behind camera
750
+ if (projected.scale <= 0) continue;
751
+
752
+ // Size uses DISTANCE SCALE - shrinks as star approaches BH
753
+ const size =
754
+ p.size * distScale * sizeFactor * CONFIG.perspectiveMultiplier;
755
+
756
+ // Draw particle - ALPHA IS CONSTANT (no transparency!)
757
+ const alpha = p.color.a;
758
+ ctx.fillStyle = `rgba(${Math.round(p.color.r)}, ${Math.round(p.color.g)}, ${Math.round(p.color.b)}, ${alpha})`;
759
+ ctx.beginPath();
760
+ ctx.arc(screenX, screenY, size / 2, 0, Math.PI * 2);
761
+ ctx.fill();
762
+ }
763
+
764
+ // Draw star core with distance scale - ALPHA IS CONSTANT
765
+ const coreRadius = baseVisualSize * CONFIG.bodyRadius * sizeFactor;
766
+ const glowRadius = baseVisualSize * CONFIG.glowRadius * sizeFactor;
767
+
768
+ if (coreRadius > 1) {
769
+ // Outer glow - FULL OPACITY, scales with distance
770
+ const outerGradient = ctx.createRadialGradient(
771
+ this.screenX,
772
+ this.screenY,
773
+ coreRadius * 0.5,
774
+ this.screenX,
775
+ this.screenY,
776
+ glowRadius,
777
+ );
778
+ outerGradient.addColorStop(0, "rgba(255, 250, 220, 0.8)");
779
+ outerGradient.addColorStop(0.3, "rgba(255, 230, 180, 0.5)");
780
+ outerGradient.addColorStop(0.6, "rgba(255, 180, 100, 0.2)");
781
+ outerGradient.addColorStop(1, "rgba(255, 150, 50, 0)");
782
+
783
+ ctx.fillStyle = outerGradient;
784
+ ctx.shadowColor = "rgba(255, 220, 150, 0.6)";
785
+ ctx.shadowBlur = 20 * distScale; // Glow scales with distance
786
+ ctx.beginPath();
787
+ ctx.arc(this.screenX, this.screenY, glowRadius, 0, Math.PI * 2);
788
+ ctx.fill();
789
+
790
+ // Star body core - FULL OPACITY
791
+ ctx.shadowBlur = 0;
792
+ const bodyGradient = ctx.createRadialGradient(
793
+ this.screenX,
794
+ this.screenY,
795
+ 0,
796
+ this.screenX,
797
+ this.screenY,
798
+ coreRadius,
799
+ );
800
+ bodyGradient.addColorStop(0, "rgba(255, 255, 255, 1)");
801
+ bodyGradient.addColorStop(0.3, "rgba(255, 255, 240, 1)");
802
+ bodyGradient.addColorStop(0.6, "rgba(255, 240, 200, 0.95)");
803
+ bodyGradient.addColorStop(1, "rgba(255, 220, 150, 0.9)");
804
+
805
+ ctx.fillStyle = bodyGradient;
806
+ ctx.beginPath();
807
+ ctx.arc(this.screenX, this.screenY, coreRadius, 0, Math.PI * 2);
808
+ ctx.fill();
809
+ }
810
+ });
811
+ }
812
+ }