@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,239 @@
1
+ /**
2
+ * Flare - Luminous burst effect for TDE demo
3
+ *
4
+ * Renders a bright flare using direct canvas drawing.
5
+ * State computed in update(), rendered in draw() via Painter.useCtx().
6
+ */
7
+ import { GameObject, Painter } from "../../../src/index.js";
8
+
9
+ const CONFIG = {
10
+ // Colors - BRIGHTER
11
+ colorPeak: { r: 255, g: 255, b: 255 },
12
+ colorMid: { r: 255, g: 220, b: 150 },
13
+ colorFade: { r: 255, g: 150, b: 80 },
14
+
15
+ // Animation
16
+ pulseSpeed: 8,
17
+ pulseAmount: 0.2,
18
+
19
+ // Shadow/glow - MORE INTENSE
20
+ glowBlurBase: 100,
21
+
22
+ // Flare size multipliers - LARGER AND DENSER
23
+ outerMultiplier: 2.0,
24
+ innerMultiplier: 0.8,
25
+ coreMultiplier: 0.35,
26
+ };
27
+
28
+ export class Flare extends GameObject {
29
+ /**
30
+ * @param {Game} game - Game instance
31
+ * @param {Object} options
32
+ * @param {number} options.radius - Flare radius
33
+ * @param {Camera3D} options.camera - Camera for projection
34
+ */
35
+ constructor(game, options = {}) {
36
+ super(game, options);
37
+
38
+ this.radius = options.radius ?? 100;
39
+ this.intensity = 0;
40
+ this.targetIntensity = 0;
41
+ this.pulsePhase = 0;
42
+ this.camera = options.camera;
43
+
44
+ // Screen position (computed in update)
45
+ this.screenX = game.width / 2;
46
+ this.screenY = game.height / 2;
47
+ this.screenScale = 1;
48
+ }
49
+
50
+ /**
51
+ * Initialize flare.
52
+ */
53
+ init() {
54
+ // Nothing to pre-create
55
+ }
56
+
57
+ /**
58
+ * Get current color based on intensity.
59
+ */
60
+ getCurrentColor() {
61
+ if (this.intensity > 0.7) {
62
+ // Peak - white
63
+ const t = (this.intensity - 0.7) / 0.3;
64
+ return this.lerpColor(CONFIG.colorMid, CONFIG.colorPeak, t);
65
+ } else if (this.intensity > 0.3) {
66
+ // Mid - orange
67
+ const t = (this.intensity - 0.3) / 0.4;
68
+ return this.lerpColor(CONFIG.colorFade, CONFIG.colorMid, t);
69
+ } else {
70
+ // Fade - red
71
+ return CONFIG.colorFade;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Linearly interpolate between two colors.
77
+ */
78
+ lerpColor(c1, c2, t) {
79
+ return {
80
+ r: Math.round(c1.r + (c2.r - c1.r) * t),
81
+ g: Math.round(c1.g + (c2.g - c1.g) * t),
82
+ b: Math.round(c1.b + (c2.b - c1.b) * t),
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Get effective intensity with pulse.
88
+ */
89
+ getEffectiveIntensity() {
90
+ const pulse = Math.sin(this.pulsePhase) * CONFIG.pulseAmount;
91
+ return Math.max(0, Math.min(1, this.intensity + pulse * this.intensity));
92
+ }
93
+
94
+ /**
95
+ * Update radius.
96
+ */
97
+ updateRadius(radius) {
98
+ this.radius = radius;
99
+ }
100
+
101
+ /**
102
+ * Trigger the flare.
103
+ */
104
+ trigger() {
105
+ this.intensity = 1;
106
+ this.targetIntensity = 1;
107
+ }
108
+
109
+ /**
110
+ * Set intensity directly.
111
+ */
112
+ setIntensity(intensity) {
113
+ this.intensity = Math.max(0, Math.min(1, intensity));
114
+ this.targetIntensity = this.intensity;
115
+ }
116
+
117
+ /**
118
+ * Start fading out.
119
+ */
120
+ fadeOut() {
121
+ this.targetIntensity = 0.3;
122
+ }
123
+
124
+ /**
125
+ * Reset flare.
126
+ */
127
+ reset() {
128
+ this.intensity = 0;
129
+ this.targetIntensity = 0;
130
+ this.pulsePhase = 0;
131
+ }
132
+
133
+ /**
134
+ * Update - compute render state.
135
+ */
136
+ update(dt) {
137
+ super.update(dt);
138
+
139
+ // Update pulse
140
+ this.pulsePhase += dt * CONFIG.pulseSpeed;
141
+
142
+ // Smooth intensity transition
143
+ const diff = this.targetIntensity - this.intensity;
144
+ if (Math.abs(diff) > 0.001) {
145
+ this.intensity += diff * dt * 2;
146
+ }
147
+
148
+ // Project (0,0,0) through camera to get screen position
149
+ if (this.camera) {
150
+ const projected = this.camera.project(0, 0, 0);
151
+ this.screenX = this.game.width / 2 + projected.x;
152
+ this.screenY = this.game.height / 2 + projected.y;
153
+ this.screenScale = projected.scale;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Draw the flare.
159
+ * Uses Painter.useCtx() for direct canvas drawing.
160
+ */
161
+ draw() {
162
+ const effectiveIntensity = this.getEffectiveIntensity();
163
+ if (effectiveIntensity < 0.01) return;
164
+
165
+ const scaledRadius = this.radius * this.screenScale;
166
+ const color = this.getCurrentColor();
167
+
168
+ Painter.useCtx((ctx) => {
169
+ // Outer glow (largest, most transparent)
170
+ const outerRadius =
171
+ scaledRadius * CONFIG.outerMultiplier * effectiveIntensity;
172
+ const outerGradient = ctx.createRadialGradient(
173
+ this.screenX,
174
+ this.screenY,
175
+ 0,
176
+ this.screenX,
177
+ this.screenY,
178
+ outerRadius,
179
+ );
180
+ outerGradient.addColorStop(
181
+ 0,
182
+ `rgba(${color.r}, ${color.g}, ${color.b}, ${effectiveIntensity * 0.6})`,
183
+ );
184
+ outerGradient.addColorStop(
185
+ 0.3,
186
+ `rgba(${color.r}, ${Math.round(color.g * 0.7)}, ${Math.round(color.b * 0.5)}, ${effectiveIntensity * 0.4})`,
187
+ );
188
+ outerGradient.addColorStop(
189
+ 0.6,
190
+ `rgba(${color.r}, ${Math.round(color.g * 0.5)}, ${Math.round(color.b * 0.3)}, ${effectiveIntensity * 0.2})`,
191
+ );
192
+ outerGradient.addColorStop(1, "rgba(255, 50, 0, 0)");
193
+
194
+ ctx.fillStyle = outerGradient;
195
+ ctx.shadowColor = `rgba(${color.r}, ${color.g}, ${color.b}, ${effectiveIntensity * 0.9})`;
196
+ ctx.shadowBlur = CONFIG.glowBlurBase * effectiveIntensity;
197
+ ctx.beginPath();
198
+ ctx.arc(this.screenX, this.screenY, outerRadius, 0, Math.PI * 2);
199
+ ctx.fill();
200
+
201
+ // Inner glow (smaller, brighter)
202
+ const innerRadius =
203
+ scaledRadius * CONFIG.innerMultiplier * effectiveIntensity;
204
+ const innerGradient = ctx.createRadialGradient(
205
+ this.screenX,
206
+ this.screenY,
207
+ 0,
208
+ this.screenX,
209
+ this.screenY,
210
+ innerRadius,
211
+ );
212
+ innerGradient.addColorStop(
213
+ 0,
214
+ `rgba(${color.r}, ${color.g}, ${color.b}, ${effectiveIntensity})`,
215
+ );
216
+ innerGradient.addColorStop(
217
+ 0.5,
218
+ `rgba(${color.r}, ${color.g}, ${color.b}, ${effectiveIntensity * 0.6})`,
219
+ );
220
+ innerGradient.addColorStop(1, "rgba(255, 150, 50, 0)");
221
+
222
+ ctx.shadowBlur = CONFIG.glowBlurBase * 0.6 * effectiveIntensity;
223
+ ctx.fillStyle = innerGradient;
224
+ ctx.beginPath();
225
+ ctx.arc(this.screenX, this.screenY, innerRadius, 0, Math.PI * 2);
226
+ ctx.fill();
227
+
228
+ // Core (brightest center)
229
+ const coreRadius =
230
+ scaledRadius * CONFIG.coreMultiplier * effectiveIntensity;
231
+ ctx.shadowColor = "rgba(255, 255, 255, 1)";
232
+ ctx.shadowBlur = 25 * effectiveIntensity;
233
+ ctx.fillStyle = `rgba(255, 255, 255, ${effectiveIntensity})`;
234
+ ctx.beginPath();
235
+ ctx.arc(this.screenX, this.screenY, coreRadius, 0, Math.PI * 2);
236
+ ctx.fill();
237
+ });
238
+ }
239
+ }
@@ -0,0 +1,448 @@
1
+ /**
2
+ * Tidal Disruption Event - Cinematic Visualization
3
+ *
4
+ * A star torn apart by a supermassive black hole's tidal forces.
5
+ * Uses ParticleSystem for debris, Shapes for black hole/flare,
6
+ * and StateMachine for phase management.
7
+ */
8
+ import {
9
+ Game,
10
+ Camera3D,
11
+ StateMachine,
12
+ Painter,
13
+ Text,
14
+ Scene,
15
+ } from "../../../src/index.js";
16
+
17
+ import { BlackHole } from "./blackhole.obj.js";
18
+ import { Star } from "./star.obj.js";
19
+ import { DebrisManager } from "./debris.obj.js";
20
+ import { Flare } from "./flare.obj.js";
21
+ import { StarField } from "../blackhole/starfield.obj.js";
22
+
23
+ // Configuration
24
+ const CONFIG = {
25
+ // Sizing (as fraction of screen)
26
+ bhRadiusRatio: 0.08,
27
+ starRadiusRatio: 0.05, // Smaller base size - distance scaling makes it look bigger when far
28
+ tidalRadiusRatio: 0.35,
29
+
30
+ // Star - starts FAR, falls IN
31
+ starParticleCount: 8000, // Much denser for fluid look
32
+ starStartDistance: 0.6, // Matches apoapsisRatio - start at farthest point
33
+
34
+ // Camera
35
+ cameraDistance: 0.7,
36
+ autoRotateSpeed: 0.05, // Slower rotation to admire the view
37
+
38
+ // Phase durations (seconds) - FASTER, MORE DRAMATIC
39
+ phases: {
40
+ approach: 10.0, // Longer approach to build tension
41
+ stretch: 3.0, // Orbital stretching
42
+ disrupt: 5.0, // Extended disruption - particles spiral off
43
+ accrete: 6.0, // Longer accretion for more drama
44
+ flare: 2.0, // Bright flare
45
+ },
46
+
47
+ // Visual
48
+ backgroundColor: "#020206",
49
+ };
50
+
51
+ class TDEDemo extends Game {
52
+ constructor(canvas) {
53
+ super(canvas);
54
+ this.backgroundColor = CONFIG.backgroundColor;
55
+ this.enableFluidSize();
56
+ }
57
+
58
+ init() {
59
+ super.init();
60
+ this.time = 0;
61
+
62
+ // Calculate scaled sizes
63
+ this.updateScaledSizes();
64
+
65
+ // Setup Camera3D - lower perspective = stronger depth effect
66
+ this.camera = new Camera3D({
67
+ rotationX: 1.2, // ~70 degrees - lower angle for Interstellar look
68
+ rotationY: 0,
69
+ perspective: this.baseScale * CONFIG.cameraDistance,
70
+ autoRotate: true,
71
+ autoRotateSpeed: CONFIG.autoRotateSpeed,
72
+ });
73
+ this.camera.enableMouseControl(this.canvas);
74
+
75
+ // Create Starfield (background) - LOTS OF STARS
76
+ this.starfield = new StarField(this, {
77
+ camera: this.camera,
78
+ starCount: 5000,
79
+ });
80
+ this.starfield.init();
81
+ this.starfield.zIndex = 0;
82
+ this.pipeline.add(this.starfield);
83
+
84
+ // Create Flare (renders as background glow)
85
+ this.flare = new Flare(this, {
86
+ radius: this.bhRadius * 4,
87
+ camera: this.camera,
88
+ });
89
+ this.flare.init();
90
+ this.flare.zIndex = 10;
91
+ this.pipeline.add(this.flare);
92
+
93
+ // Create a Scene to properly manage z-ordering of star and black hole
94
+ this.mainScene = new Scene(this, { sortByZIndex: true });
95
+ this.mainScene.zIndex = 30;
96
+ this.pipeline.add(this.mainScene);
97
+
98
+ // Create Black Hole (event horizon at center)
99
+ this.blackHole = new BlackHole(this, {
100
+ radius: this.bhRadius,
101
+ glowIntensity: 0,
102
+ camera: this.camera,
103
+ });
104
+ this.blackHole.init();
105
+ this.blackHole.zIndex = 50; // Middle layer
106
+ this.mainScene.add(this.blackHole);
107
+
108
+ // Create Star (approaching from outside)
109
+ // zIndex updated dynamically: behind BH when z > 0, in front when z < 0
110
+ this.star = new Star(this, {
111
+ camera: this.camera,
112
+ radius: this.starRadius,
113
+ particleCount: CONFIG.starParticleCount,
114
+ baseScale: this.baseScale,
115
+ startDistance: this.baseScale * CONFIG.starStartDistance,
116
+ });
117
+ this.star.init();
118
+ this.star.zIndex = 25; // Start behind black hole
119
+ this.mainScene.add(this.star);
120
+
121
+ // Create Debris Manager (accretion disk particles with lensing)
122
+ this.debrisManager = new DebrisManager(this, {
123
+ camera: this.camera,
124
+ bhRadius: this.bhRadius,
125
+ baseScale: this.baseScale,
126
+ });
127
+ this.debrisManager.init();
128
+ this.debrisManager.zIndex = 75; // On top of black hole
129
+ this.mainScene.add(this.debrisManager);
130
+
131
+ // Initialize state machine
132
+ this.initStateMachine();
133
+
134
+ // Click to restart
135
+ this.canvas.addEventListener("click", () => {
136
+ if (this.fsm.is("stable")) {
137
+ this.restart();
138
+ }
139
+ });
140
+
141
+ // Info label (always on top)
142
+ this.infoLabel = new Text(this, "", {
143
+ x: 15,
144
+ y: this.height - 30,
145
+ color: "#888",
146
+ font: "11px monospace",
147
+ });
148
+ this.infoLabel.zIndex = 100;
149
+ this.pipeline.add(this.infoLabel);
150
+
151
+ // Flash overlay for dramatic collapse moment
152
+ this.flashIntensity = 0;
153
+ this.flashDecay = 3.0; // How fast flash fades (higher = faster)
154
+ }
155
+
156
+ initStateMachine() {
157
+ this.fsm = StateMachine.fromSequence(
158
+ [
159
+ {
160
+ name: "approach",
161
+ duration: CONFIG.phases.approach,
162
+ enter: () => this.onApproachEnter(),
163
+ },
164
+ {
165
+ name: "stretch",
166
+ duration: CONFIG.phases.stretch,
167
+ enter: () => this.onStretchEnter(),
168
+ },
169
+ {
170
+ name: "disrupt",
171
+ duration: CONFIG.phases.disrupt,
172
+ enter: () => this.onDisruptEnter(),
173
+ },
174
+ {
175
+ name: "accrete",
176
+ duration: CONFIG.phases.accrete,
177
+ enter: () => this.onAccreteEnter(),
178
+ },
179
+ {
180
+ name: "flare",
181
+ duration: CONFIG.phases.flare,
182
+ enter: () => this.onFlareEnter(),
183
+ },
184
+ {
185
+ name: "stable",
186
+ duration: Infinity,
187
+ enter: () => this.onStableEnter(),
188
+ },
189
+ ],
190
+ { context: this },
191
+ );
192
+ }
193
+
194
+ // Phase callbacks
195
+ onApproachEnter() {
196
+ this.star.startApproach();
197
+ // No pre-made disk - it forms organically from star particles
198
+ }
199
+
200
+ onStretchEnter() {
201
+ this.star.startStretch();
202
+ // Glow controlled by awakening - just set target intensity
203
+ this.blackHole.setGlowIntensity(0.5);
204
+ }
205
+
206
+ onDisruptEnter() {
207
+ this.star.startDisrupt();
208
+ // Glow controlled by awakening - just set target intensity
209
+ this.blackHole.setGlowIntensity(0.8);
210
+ }
211
+
212
+ onAccreteEnter() {
213
+ // Flash white when star collapses!
214
+ this.flashIntensity = 1.0;
215
+
216
+ // Transfer remaining star particles to debris - these form the final disk
217
+ const debris = this.star.releaseAllParticles();
218
+ this.debrisManager.addDebris(debris);
219
+
220
+ // FLARE TRIGGERS IMMEDIATELY ON COLLISION - not waiting for flare phase
221
+ this.flare.trigger();
222
+ this.blackHole.setGlowIntensity(1.0);
223
+
224
+ // Activate relativistic jets during accretion (only if BH is awakened enough)
225
+ this.blackHole.setJetsActive(true);
226
+ }
227
+
228
+ onFlareEnter() {
229
+ // Flare already triggered in accrete - this phase is just for decay
230
+ // Keep intensity high
231
+ this.flare.setIntensity(1.0);
232
+ }
233
+
234
+ onStableEnter() {
235
+ this.blackHole.setGlowIntensity(1.0); // Keep full glow after collision
236
+ this.blackHole.setJetsActive(false); // Turn off jets when stable
237
+ this.flare.fadeOut();
238
+ }
239
+
240
+ restart() {
241
+ this.star.reset();
242
+ this.debrisManager.clear();
243
+ this.flare.reset();
244
+ this.flashIntensity = 0;
245
+ this.blackHole.setGlowIntensity(0);
246
+ this.blackHole.resetMass();
247
+ this.fsm.setState("approach");
248
+ }
249
+
250
+ updateScaledSizes() {
251
+ this.baseScale = Math.min(this.width, this.height);
252
+ this.bhRadius = this.baseScale * CONFIG.bhRadiusRatio;
253
+ this.starRadius = this.baseScale * CONFIG.starRadiusRatio;
254
+ this.tidalRadius = this.baseScale * CONFIG.tidalRadiusRatio;
255
+ }
256
+
257
+ onResize() {
258
+ this.updateScaledSizes();
259
+
260
+ if (this.camera) {
261
+ this.camera.perspective = this.baseScale * CONFIG.cameraDistance;
262
+ }
263
+
264
+ if (this.blackHole) {
265
+ this.blackHole.updateRadius(this.bhRadius);
266
+ }
267
+
268
+ if (this.star) {
269
+ this.star.updateSizing(this.starRadius, this.baseScale);
270
+ }
271
+
272
+ if (this.debrisManager) {
273
+ this.debrisManager.updateSizing(this.bhRadius, this.baseScale);
274
+ }
275
+
276
+ if (this.flare) {
277
+ this.flare.updateRadius(this.bhRadius * 4);
278
+ }
279
+
280
+ if (this.infoLabel) {
281
+ this.infoLabel.y = this.height - 30;
282
+ }
283
+ }
284
+
285
+ update(dt) {
286
+ super.update(dt);
287
+ this.time += dt;
288
+
289
+ // Update camera
290
+ this.camera.update(dt);
291
+
292
+ // Update state machine
293
+ this.fsm.update(dt);
294
+
295
+ // Update components based on current phase
296
+ const state = this.fsm.state;
297
+ const progress = this.fsm.progress;
298
+
299
+ // Black hole feeds on debris during ALL phases (awakens progressively)
300
+ const accretionRate = this.debrisManager.getAccretionRate();
301
+ if (accretionRate > 0) {
302
+ this.blackHole.addMass(accretionRate * dt * 0.01);
303
+ }
304
+
305
+ if (state === "approach") {
306
+ this.star.updateApproach(dt, progress);
307
+ // ORGANIC STREAMING FROM THE VERY START
308
+ // As star approaches, tidal forces gradually pull matter off
309
+ // Drift starts immediately but very weak, builds up over time
310
+ const driftStrength = progress * progress; // Quadratic buildup
311
+ this.star.applyParticleDrift(dt, driftStrength);
312
+
313
+ // Start releasing particles from 10% onward - very slow at first
314
+ if (progress > 0.1) {
315
+ const releaseProgress = (progress - 0.1) / 0.9;
316
+ // Mild tidal stretch that builds up
317
+ this.star.applyTidalStretch(releaseProgress * 0.2);
318
+ // Release particles - slow at first, faster as star approaches
319
+ const released = this.star.releaseParticles(releaseProgress * 0.25);
320
+ if (released.length > 0) {
321
+ this.debrisManager.addDebris(released);
322
+ // BH GROWS as star releases particles
323
+ this.blackHole.addMass(released.length * 0.002);
324
+ }
325
+ }
326
+ } else if (state === "stretch") {
327
+ this.star.updateStretch(dt, progress, { x: 0, y: 0, z: 0 });
328
+ // DRIFT particles toward BH - stronger during stretch
329
+ this.star.applyParticleDrift(dt, 1.0 + progress);
330
+ // Continue streaming during stretch - more aggressively
331
+ const released = this.star.releaseParticles(0.3 + progress * 0.4);
332
+ if (released.length > 0) {
333
+ this.debrisManager.addDebris(released);
334
+ // BH GROWS as star releases particles
335
+ this.blackHole.addMass(released.length * 0.003);
336
+ }
337
+ } else if (state === "disrupt") {
338
+ this.star.updateDisrupt(dt, progress);
339
+ // DRIFT particles toward BH - maximum during disruption
340
+ this.star.applyParticleDrift(dt, 2.0 + progress);
341
+ // Continue releasing particles during disruption
342
+ const released = this.star.releaseParticles(0.7 + progress * 0.3);
343
+ if (released.length > 0) {
344
+ this.debrisManager.addDebris(released);
345
+ // BH GROWS as star releases particles
346
+ this.blackHole.addMass(released.length * 0.004);
347
+ }
348
+
349
+ // COLLISION CHECK - trigger accrete when star is close to BH
350
+ const starDist = Math.sqrt(
351
+ this.star.centerX ** 2 +
352
+ this.star.centerY ** 2 +
353
+ this.star.centerZ ** 2,
354
+ );
355
+ if (starDist < this.bhRadius * 2) {
356
+ // Star has reached the black hole - trigger accrete immediately
357
+ this.fsm.setState("accrete");
358
+ }
359
+ } else if (state === "flare") {
360
+ this.flare.setIntensity(1 - progress * 0.5);
361
+ }
362
+
363
+ // Update star z-ordering based on camera-space z position
364
+ // Star behind black hole (z > 0) = lower zIndex, in front (z < 0) = higher zIndex
365
+ // Use hysteresis to prevent jittering when star is near the z=0 plane
366
+ if (this.star && this.star.visible) {
367
+ const starCameraZ = this.star.getCameraZ();
368
+ const hysteresis = this.bhRadius * 0.5; // Threshold to prevent jittering
369
+
370
+ let newZIndex = this.star.zIndex;
371
+ if (starCameraZ > hysteresis) {
372
+ // Clearly behind BH
373
+ newZIndex = 25;
374
+ } else if (starCameraZ < -hysteresis) {
375
+ // Clearly in front of BH
376
+ newZIndex = 75;
377
+ }
378
+ // If within hysteresis range, keep current zIndex to avoid flickering
379
+
380
+ if (this.star.zIndex !== newZIndex) {
381
+ this.star.zIndex = newZIndex;
382
+ // Mark z-order as dirty for re-sorting
383
+ this.mainScene._collection._zOrderDirty = true;
384
+ }
385
+ }
386
+
387
+ // Decay flash intensity
388
+ if (this.flashIntensity > 0) {
389
+ this.flashIntensity = Math.max(
390
+ 0,
391
+ this.flashIntensity - dt * this.flashDecay,
392
+ );
393
+ }
394
+
395
+ // Update info label
396
+ this.updateInfoLabel();
397
+ }
398
+
399
+ updateInfoLabel() {
400
+ const state = this.fsm.state;
401
+ const progress = this.fsm.progress;
402
+
403
+ const stateLabels = {
404
+ approach: "Star Approaching",
405
+ stretch: "Tidal Stretching",
406
+ disrupt: "Stellar Disruption",
407
+ accrete: "Debris Accretion",
408
+ flare: "Luminous Flare",
409
+ stable: "Stable Disk",
410
+ };
411
+
412
+ const stateColors = {
413
+ approach: "#88f",
414
+ stretch: "#fa8",
415
+ disrupt: "#f88",
416
+ accrete: "#ff8",
417
+ flare: "#fff",
418
+ stable: "#8a8",
419
+ };
420
+
421
+ this.infoLabel.color = stateColors[state] || "#888";
422
+
423
+ if (state === "stable") {
424
+ this.infoLabel.text = `${stateLabels[state]} — click to restart`;
425
+ } else {
426
+ this.infoLabel.text = `${stateLabels[state]}: ${Math.round(progress * 100)}%`;
427
+ }
428
+ }
429
+
430
+ render() {
431
+ // Pipeline already sorts by zIndex via ZOrderedCollection
432
+ super.render();
433
+
434
+ // Draw flash overlay on top of everything using direct canvas
435
+ if (this.flashIntensity > 0) {
436
+ Painter.useCtx((ctx) => {
437
+ ctx.fillStyle = `rgba(255, 255, 255, ${this.flashIntensity})`;
438
+ ctx.fillRect(0, 0, this.width, this.height);
439
+ });
440
+ }
441
+ }
442
+ }
443
+
444
+ window.addEventListener("load", () => {
445
+ const canvas = document.getElementById("game");
446
+ const demo = new TDEDemo(canvas);
447
+ demo.start();
448
+ });