@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,290 @@
1
+ import { GameObject, Painter, Tweenetik, Easing } from "../../../src/index.js";
2
+
3
+ /**
4
+ * RelativisticJets - Bipolar jets shooting from black hole poles
5
+ *
6
+ * Physics:
7
+ * - Particles ejected along ±Y axis (perpendicular to disk)
8
+ * - Conical spread gives characteristic jet shape
9
+ * - Velocity decreases with distance (deceleration)
10
+ * - Bright blue-white core fading to orange at edges
11
+ */
12
+
13
+ const JET_CONFIG = {
14
+ // Particle properties
15
+ maxParticles: 6000,
16
+ particleLifetime: 8.0, // Shorter lifetime - continuous turnover
17
+ spawnRatePerSecond: 800, // Particles per SECOND (not per frame)
18
+
19
+ // Jet geometry
20
+ coneAngle: 0.06, // Tight cone for focused beams
21
+ initialSpeed: 1200, // Fast ejection - relativistic!
22
+ speedVariation: 300, // Random variation in speed
23
+
24
+ // Jet length - shoots off screen
25
+ maxLength: 50000, // Way off screen
26
+
27
+ // Visual - bright particles
28
+ colorCore: { r: 220, g: 240, b: 255 }, // Blue-white core
29
+ colorEdge: { r: 255, g: 160, b: 80 }, // Orange edge
30
+ sizeMin: 0.8,
31
+ sizeMax: 1.0,
32
+
33
+ // Animation
34
+ activationDuration: 0.3, // Quick ignition
35
+ deactivationDuration: 5.0, // Graceful fade
36
+ };
37
+
38
+ export class RelativisticJets extends GameObject {
39
+ constructor(game, options = {}) {
40
+ super(game, options);
41
+
42
+ this.camera = options.camera;
43
+ this.bhRadius = options.bhRadius ?? 50;
44
+
45
+ // State
46
+ this.active = false;
47
+ this.intensity = 0; // 0-1, controls spawn rate and brightness
48
+ this.isDeactivating = false; // Prevent multiple deactivate() calls
49
+
50
+ // Particle arrays - one for each jet (up and down)
51
+ this.particles = [];
52
+ }
53
+
54
+ init() {
55
+ this.particles = [];
56
+ }
57
+
58
+ /**
59
+ * Activate jets with intensity ramp-up
60
+ * Uses easeOutExpo for explosive ignition feel
61
+ */
62
+ activate() {
63
+ if (this.active) return;
64
+ this.active = true;
65
+ this.intensity = 0;
66
+ // Explosive start, then sustains - like jets igniting
67
+ Tweenetik.to(this, { intensity: 1 }, JET_CONFIG.activationDuration, Easing.easeOutExpo);
68
+ }
69
+
70
+ /**
71
+ * Deactivate jets with fade-out
72
+ */
73
+ deactivate() {
74
+ if (!this.active || this.isDeactivating) return;
75
+ this.isDeactivating = true;
76
+ // Slow graceful fade
77
+ Tweenetik.to(this, { intensity: 0 }, JET_CONFIG.deactivationDuration, Easing.easeInQuad, {
78
+ onComplete: () => {
79
+ this.active = false;
80
+ this.isDeactivating = false;
81
+ }
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Pulse the jets - boost intensity for sustained firing
87
+ */
88
+ pulse() {
89
+ if (!this.active) return;
90
+ this.intensity = 1;
91
+ Tweenetik.to(this, { intensity: 0.6 }, 2.0, Easing.easeOutQuad);
92
+ }
93
+
94
+ /**
95
+ * Spawn jet particles from both poles
96
+ * @param {number} dt - Delta time for frame-rate independent spawning
97
+ */
98
+ spawnParticles(dt) {
99
+ if (this.particles.length >= JET_CONFIG.maxParticles) return;
100
+
101
+ // Frame-rate independent spawning
102
+ const spawnCount = Math.floor(JET_CONFIG.spawnRatePerSecond * dt * this.intensity);
103
+
104
+ for (let i = 0; i < spawnCount; i++) {
105
+ // Spawn from both poles (up and down)
106
+ const direction = Math.random() < 0.5 ? 1 : -1;
107
+
108
+ // Conical spread - random angle within cone
109
+ const spreadAngle = Math.random() * JET_CONFIG.coneAngle;
110
+ const azimuth = Math.random() * Math.PI * 2;
111
+
112
+ // Convert to velocity components
113
+ const speed = JET_CONFIG.initialSpeed +
114
+ (Math.random() - 0.5) * JET_CONFIG.speedVariation;
115
+
116
+ // Y is the main jet direction, x/z give the spread
117
+ const vy = direction * speed * Math.cos(spreadAngle);
118
+ const spreadMag = speed * Math.sin(spreadAngle);
119
+ const vx = spreadMag * Math.cos(azimuth);
120
+ const vz = spreadMag * Math.sin(azimuth);
121
+
122
+ // Start position - slightly offset from BH center along jet axis
123
+ const startOffset = this.bhRadius * 1.034;
124
+
125
+ this.particles.push({
126
+ x: vx * 0.01, // Tiny initial spread
127
+ y: direction * startOffset,
128
+ z: vz * 0.01,
129
+ vx,
130
+ vy,
131
+ vz,
132
+ age: 0,
133
+ direction, // Track which jet (for color)
134
+ size: JET_CONFIG.sizeMin + Math.random() * (JET_CONFIG.sizeMax - JET_CONFIG.sizeMin),
135
+ // Core particles (small spread) are brighter
136
+ isCore: spreadAngle < JET_CONFIG.coneAngle * 0.3,
137
+ });
138
+ }
139
+ }
140
+
141
+ update(dt) {
142
+ super.update(dt);
143
+
144
+ if (!this.active) return;
145
+
146
+ // Spawn new particles (dt-based for consistent rate)
147
+ this.spawnParticles(dt);
148
+
149
+ // Update existing particles
150
+ const maxDist = this.bhRadius * JET_CONFIG.maxLength;
151
+
152
+ for (let i = this.particles.length - 1; i >= 0; i--) {
153
+ const p = this.particles[i];
154
+ p.age += dt;
155
+
156
+ // Remove old particles
157
+ if (p.age > JET_CONFIG.particleLifetime) {
158
+ this.particles.splice(i, 1);
159
+ continue;
160
+ }
161
+
162
+ // Remove particles that traveled too far
163
+ const dist = Math.abs(p.y);
164
+ if (dist > maxDist) {
165
+ this.particles.splice(i, 1);
166
+ continue;
167
+ }
168
+
169
+ // No deceleration - relativistic jets maintain speed
170
+ // Particles stream continuously at near-constant velocity
171
+
172
+ // Move particle
173
+ p.x += p.vx * dt;
174
+ p.y += p.vy * dt;
175
+ p.z += p.vz * dt;
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Build render list with camera projection
181
+ */
182
+ buildRenderList() {
183
+ const renderList = [];
184
+ if (!this.camera || this.particles.length === 0) return renderList;
185
+
186
+ for (const p of this.particles) {
187
+ // Transform to camera space
188
+ const cosY = Math.cos(this.camera.rotationY);
189
+ const sinY = Math.sin(this.camera.rotationY);
190
+ let xCam = p.x * cosY - p.z * sinY;
191
+ let zCam = p.x * sinY + p.z * cosY;
192
+
193
+ const cosX = Math.cos(this.camera.rotationX);
194
+ const sinX = Math.sin(this.camera.rotationX);
195
+ let yCam = p.y * cosX - zCam * sinX;
196
+ zCam = p.y * sinX + zCam * cosX;
197
+
198
+ // Perspective projection
199
+ const perspectiveScale = this.camera.perspective / (this.camera.perspective + zCam);
200
+ const screenX = xCam * perspectiveScale;
201
+ const screenY = yCam * perspectiveScale;
202
+
203
+ // Skip particles behind camera
204
+ if (zCam < -this.camera.perspective + 10) continue;
205
+
206
+ // Color: core is blue-white, edge is orange
207
+ // Also fade with distance from BH
208
+ const distFactor = Math.min(1, Math.abs(p.y) / (this.bhRadius * JET_CONFIG.maxLength * 0.5));
209
+ const ageFactor = 1 - (p.age / JET_CONFIG.particleLifetime);
210
+
211
+ let color;
212
+ if (p.isCore) {
213
+ // Core: bright blue-white
214
+ color = {
215
+ r: JET_CONFIG.colorCore.r,
216
+ g: JET_CONFIG.colorCore.g,
217
+ b: JET_CONFIG.colorCore.b,
218
+ };
219
+ } else {
220
+ // Edge: lerp toward orange with distance
221
+ color = {
222
+ r: JET_CONFIG.colorCore.r + (JET_CONFIG.colorEdge.r - JET_CONFIG.colorCore.r) * distFactor,
223
+ g: JET_CONFIG.colorCore.g + (JET_CONFIG.colorEdge.g - JET_CONFIG.colorCore.g) * distFactor,
224
+ b: JET_CONFIG.colorCore.b + (JET_CONFIG.colorEdge.b - JET_CONFIG.colorCore.b) * distFactor,
225
+ };
226
+ }
227
+
228
+ // Alpha based on age, intensity, and distance
229
+ const alpha = ageFactor * this.intensity * (1 - distFactor * 0.5);
230
+
231
+ renderList.push({
232
+ x: screenX,
233
+ y: screenY,
234
+ z: zCam,
235
+ scale: perspectiveScale,
236
+ color,
237
+ alpha,
238
+ size: p.size * (p.isCore ? 1.5 : 1),
239
+ });
240
+ }
241
+
242
+ // Sort back to front
243
+ renderList.sort((a, b) => b.z - a.z);
244
+ return renderList;
245
+ }
246
+
247
+ /**
248
+ * Clear all particles
249
+ */
250
+ clear() {
251
+ this.particles = [];
252
+ this.active = false;
253
+ this.intensity = 0;
254
+ this.isDeactivating = false;
255
+ }
256
+
257
+ /**
258
+ * Update BH radius
259
+ */
260
+ updateBHRadius(radius) {
261
+ this.bhRadius = radius;
262
+ }
263
+
264
+ render() {
265
+ super.render();
266
+
267
+ if (!this.active || !this.camera || this.particles.length === 0) return;
268
+
269
+ const cx = this.game.width / 2;
270
+ const cy = this.game.height / 2;
271
+ const renderList = this.buildRenderList();
272
+
273
+ Painter.useCtx((ctx) => {
274
+ // Reset transform
275
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
276
+ ctx.globalCompositeOperation = "lighter";
277
+
278
+ for (const item of renderList) {
279
+ const { r, g, b } = item.color;
280
+
281
+ ctx.fillStyle = `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${item.alpha})`;
282
+ ctx.beginPath();
283
+ ctx.arc(cx + item.x, cy + item.y, item.size * item.scale, 0, Math.PI * 2);
284
+ ctx.fill();
285
+ }
286
+
287
+ ctx.globalCompositeOperation = "source-over";
288
+ });
289
+ }
290
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * LensedStarfield - Starfield with gravitational lensing around black hole
3
+ *
4
+ * Extends the base StarField to add camera-space lensing effects:
5
+ * - Stars behind the black hole are displaced radially outward
6
+ * - Creates the "Einstein ring" effect where light bends around massive objects
7
+ * - Lensing strength can be animated for dramatic effect
8
+ */
9
+ import { Painter } from "../../../src/index.js";
10
+ import { applyGravitationalLensing } from "../../../src/math/gr.js";
11
+ import { StarField } from "../blackhole/starfield.obj.js";
12
+
13
+ const LENSING_CONFIG = {
14
+ // How far the lensing effect reaches in screen pixels
15
+ effectRadiusPixels: 600,
16
+
17
+ // Base strength (multiplied by lensingStrength property)
18
+ baseStrength: 200,
19
+
20
+ // Falloff exponent - higher = tighter effect around BH
21
+ falloff: 0.008,
22
+
23
+ // Minimum screen distance to apply lensing (avoid division issues)
24
+ minDistance: 5,
25
+
26
+ // Occlusion radius multiplier (stars within BH radius * this are hidden)
27
+ occlusionMultiplier: 1.15,
28
+ };
29
+
30
+ export class LensedStarfield extends StarField {
31
+ /**
32
+ * @param {Game} game
33
+ * @param {Object} options
34
+ * @param {Camera3D} options.camera
35
+ * @param {BlackHole} options.blackHole - Reference to black hole for radius
36
+ * @param {number} options.starCount
37
+ */
38
+ constructor(game, options = {}) {
39
+ super(game, options);
40
+
41
+ // Reference to black hole (for radius and position)
42
+ this.blackHole = options.blackHole ?? null;
43
+
44
+ // Animated lensing strength (0 = off, 1 = full)
45
+ // Allows ramping up during disruption phases
46
+ this.lensingStrength = options.lensingStrength ?? 1.0;
47
+ }
48
+
49
+ /**
50
+ * Update black hole reference (if set after construction)
51
+ */
52
+ setBlackHole(bh) {
53
+ this.blackHole = bh;
54
+ }
55
+
56
+ /**
57
+ * Override render to apply gravitational lensing
58
+ */
59
+ render() {
60
+ // Skip parent render - we do our own with lensing
61
+ if (!this.camera) return;
62
+
63
+ const cx = this.game.width / 2;
64
+ const cy = this.game.height / 2;
65
+ const time = performance.now() / 1000;
66
+
67
+ // Lensing parameters (strength scales with lensingStrength property)
68
+ const lensingPower = LENSING_CONFIG.baseStrength * this.lensingStrength;
69
+ const effectRadius = LENSING_CONFIG.effectRadiusPixels;
70
+
71
+ Painter.useCtx((ctx) => {
72
+ ctx.globalCompositeOperation = "source-over";
73
+
74
+ for (const star of this.stars) {
75
+ // === CAMERA SPACE TRANSFORMATION ===
76
+ const cosY = Math.cos(this.camera.rotationY);
77
+ const sinY = Math.sin(this.camera.rotationY);
78
+ let xCam = star.x * cosY - star.z * sinY;
79
+ let zCam = star.x * sinY + star.z * cosY;
80
+
81
+ const cosX = Math.cos(this.camera.rotationX);
82
+ const sinX = Math.sin(this.camera.rotationX);
83
+ let yCam = star.y * cosX - zCam * sinX;
84
+ zCam = star.y * sinX + zCam * cosX;
85
+
86
+ // Skip stars behind camera
87
+ if (zCam < -this.camera.perspective + 50) continue;
88
+
89
+ // === PERSPECTIVE PROJECTION ===
90
+ const perspectiveScale = this.camera.perspective / (this.camera.perspective + zCam);
91
+ let screenX = xCam * perspectiveScale;
92
+ let screenY = yCam * perspectiveScale;
93
+
94
+ // === GRAVITATIONAL LENSING (screen space) ===
95
+ // Only apply to stars "behind" the black hole (zCam > 0)
96
+ if (lensingPower > 0 && zCam > 0) {
97
+ const lensed = applyGravitationalLensing(
98
+ screenX, screenY,
99
+ effectRadius,
100
+ lensingPower,
101
+ LENSING_CONFIG.falloff,
102
+ LENSING_CONFIG.minDistance
103
+ );
104
+ screenX = lensed.x;
105
+ screenY = lensed.y;
106
+ }
107
+
108
+ // === OCCLUSION CHECK ===
109
+ // Hide stars that fall within the black hole's occlusion radius
110
+ if (this.blackHole) {
111
+ const bhScreenRadius = this.blackHole.currentRadius * LENSING_CONFIG.occlusionMultiplier;
112
+ const distFromCenter = Math.sqrt(screenX * screenX + screenY * screenY);
113
+ if (distFromCenter < bhScreenRadius) continue;
114
+ }
115
+
116
+ // Final screen position
117
+ const finalX = cx + screenX;
118
+ const finalY = cy + screenY;
119
+
120
+ // Viewport cull
121
+ if (finalX < -30 || finalX > this.game.width + 30 ||
122
+ finalY < -30 || finalY > this.game.height + 30) continue;
123
+
124
+ // === TWINKLE ===
125
+ const val = Math.sin(time * star.twinkleSpeed + star.twinklePhase);
126
+ const alpha = 0.6 + 0.4 * val;
127
+ if (alpha < 0.1) continue;
128
+
129
+ // === DRAW ===
130
+ const finalSize = star.type.baseSize * star.baseScale * perspectiveScale;
131
+ const sprite = this.sprites.get(star.type.type);
132
+ if (!sprite) continue;
133
+
134
+ ctx.globalAlpha = alpha;
135
+ ctx.drawImage(
136
+ sprite,
137
+ finalX - finalSize * 2,
138
+ finalY - finalSize * 2,
139
+ finalSize * 4,
140
+ finalSize * 4
141
+ );
142
+ }
143
+
144
+ ctx.globalAlpha = 1.0;
145
+ });
146
+ }
147
+ }