@guinetik/gcanvas 1.0.2 → 1.0.4

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 (217) hide show
  1. package/dist/gcanvas.es.js +25656 -0
  2. package/dist/gcanvas.es.min.js +1 -0
  3. package/dist/gcanvas.umd.js +1 -0
  4. package/dist/gcanvas.umd.min.js +1 -0
  5. package/package.json +23 -6
  6. package/src/game/objects/index.js +1 -0
  7. package/src/game/objects/spritesheet.js +260 -0
  8. package/src/game/ui/theme.js +6 -0
  9. package/src/io/keys.js +9 -1
  10. package/src/math/boolean.js +481 -0
  11. package/src/math/index.js +1 -0
  12. package/.github/workflows/release.yaml +0 -70
  13. package/.jshintrc +0 -4
  14. package/.vscode/settings.json +0 -22
  15. package/CLAUDE.md +0 -310
  16. package/blackhole.jpg +0 -0
  17. package/demo.png +0 -0
  18. package/demos/CNAME +0 -1
  19. package/demos/animations.html +0 -31
  20. package/demos/basic.html +0 -38
  21. package/demos/baskara.html +0 -31
  22. package/demos/bezier.html +0 -35
  23. package/demos/beziersignature.html +0 -29
  24. package/demos/blackhole.html +0 -28
  25. package/demos/blob.html +0 -35
  26. package/demos/coordinates.html +0 -698
  27. package/demos/cube3d.html +0 -23
  28. package/demos/demos.css +0 -303
  29. package/demos/dino.html +0 -42
  30. package/demos/easing.html +0 -28
  31. package/demos/events.html +0 -195
  32. package/demos/fluent.html +0 -647
  33. package/demos/fluid-simple.html +0 -22
  34. package/demos/fluid.html +0 -37
  35. package/demos/fractals.html +0 -36
  36. package/demos/gameobjects.html +0 -626
  37. package/demos/genart.html +0 -26
  38. package/demos/gendream.html +0 -26
  39. package/demos/group.html +0 -36
  40. package/demos/home.html +0 -587
  41. package/demos/index.html +0 -376
  42. package/demos/isometric.html +0 -34
  43. package/demos/js/animations.js +0 -452
  44. package/demos/js/basic.js +0 -204
  45. package/demos/js/baskara.js +0 -751
  46. package/demos/js/bezier.js +0 -692
  47. package/demos/js/beziersignature.js +0 -241
  48. package/demos/js/blackhole/accretiondisk.obj.js +0 -379
  49. package/demos/js/blackhole/blackhole.obj.js +0 -318
  50. package/demos/js/blackhole/index.js +0 -409
  51. package/demos/js/blackhole/particle.js +0 -56
  52. package/demos/js/blackhole/starfield.obj.js +0 -218
  53. package/demos/js/blob.js +0 -2276
  54. package/demos/js/coordinates.js +0 -840
  55. package/demos/js/cube3d.js +0 -789
  56. package/demos/js/dino.js +0 -1420
  57. package/demos/js/easing.js +0 -477
  58. package/demos/js/fluent.js +0 -183
  59. package/demos/js/fluid-simple.js +0 -253
  60. package/demos/js/fluid.js +0 -527
  61. package/demos/js/fractals.js +0 -931
  62. package/demos/js/fractalworker.js +0 -93
  63. package/demos/js/gameobjects.js +0 -176
  64. package/demos/js/genart.js +0 -268
  65. package/demos/js/gendream.js +0 -209
  66. package/demos/js/group.js +0 -140
  67. package/demos/js/info-toggle.js +0 -25
  68. package/demos/js/isometric.js +0 -863
  69. package/demos/js/kerr.js +0 -1556
  70. package/demos/js/lavalamp.js +0 -590
  71. package/demos/js/layout.js +0 -354
  72. package/demos/js/mondrian.js +0 -285
  73. package/demos/js/opacity.js +0 -275
  74. package/demos/js/painter.js +0 -484
  75. package/demos/js/particles-showcase.js +0 -514
  76. package/demos/js/particles.js +0 -299
  77. package/demos/js/patterns.js +0 -397
  78. package/demos/js/penrose/artifact.js +0 -69
  79. package/demos/js/penrose/blackhole.js +0 -121
  80. package/demos/js/penrose/constants.js +0 -73
  81. package/demos/js/penrose/game.js +0 -943
  82. package/demos/js/penrose/lore.js +0 -278
  83. package/demos/js/penrose/penrosescene.js +0 -892
  84. package/demos/js/penrose/ship.js +0 -216
  85. package/demos/js/penrose/sounds.js +0 -211
  86. package/demos/js/penrose/voidparticle.js +0 -55
  87. package/demos/js/penrose/voidscene.js +0 -258
  88. package/demos/js/penrose/voidship.js +0 -144
  89. package/demos/js/penrose/wormhole.js +0 -46
  90. package/demos/js/pipeline.js +0 -555
  91. package/demos/js/plane3d.js +0 -256
  92. package/demos/js/platformer.js +0 -1579
  93. package/demos/js/scene.js +0 -304
  94. package/demos/js/scenes.js +0 -320
  95. package/demos/js/schrodinger.js +0 -410
  96. package/demos/js/schwarzschild.js +0 -1023
  97. package/demos/js/shapes.js +0 -628
  98. package/demos/js/space/alien.js +0 -171
  99. package/demos/js/space/boom.js +0 -98
  100. package/demos/js/space/boss.js +0 -353
  101. package/demos/js/space/buff.js +0 -73
  102. package/demos/js/space/bullet.js +0 -102
  103. package/demos/js/space/constants.js +0 -85
  104. package/demos/js/space/game.js +0 -1884
  105. package/demos/js/space/hud.js +0 -112
  106. package/demos/js/space/laserbeam.js +0 -179
  107. package/demos/js/space/lightning.js +0 -277
  108. package/demos/js/space/minion.js +0 -192
  109. package/demos/js/space/missile.js +0 -212
  110. package/demos/js/space/player.js +0 -430
  111. package/demos/js/space/powerup.js +0 -90
  112. package/demos/js/space/starfield.js +0 -58
  113. package/demos/js/space/starpower.js +0 -90
  114. package/demos/js/spacetime.js +0 -559
  115. package/demos/js/sphere3d.js +0 -229
  116. package/demos/js/sprite.js +0 -473
  117. package/demos/js/svgtween.js +0 -204
  118. package/demos/js/tde/accretiondisk.js +0 -471
  119. package/demos/js/tde/blackhole.js +0 -219
  120. package/demos/js/tde/blackholescene.js +0 -209
  121. package/demos/js/tde/config.js +0 -59
  122. package/demos/js/tde/index.js +0 -820
  123. package/demos/js/tde/jets.js +0 -290
  124. package/demos/js/tde/lensedstarfield.js +0 -154
  125. package/demos/js/tde/tdestar.js +0 -297
  126. package/demos/js/tde/tidalstream.js +0 -372
  127. package/demos/js/tde_old/blackhole.obj.js +0 -354
  128. package/demos/js/tde_old/debris.obj.js +0 -791
  129. package/demos/js/tde_old/flare.obj.js +0 -239
  130. package/demos/js/tde_old/index.js +0 -448
  131. package/demos/js/tde_old/star.obj.js +0 -812
  132. package/demos/js/tiles.js +0 -312
  133. package/demos/js/tweendemo.js +0 -79
  134. package/demos/js/visibility.js +0 -102
  135. package/demos/kerr.html +0 -28
  136. package/demos/lavalamp.html +0 -27
  137. package/demos/layouts.html +0 -37
  138. package/demos/logo.svg +0 -4
  139. package/demos/loop.html +0 -84
  140. package/demos/mondrian.html +0 -32
  141. package/demos/og_image.png +0 -0
  142. package/demos/opacity.html +0 -36
  143. package/demos/painter.html +0 -39
  144. package/demos/particles-showcase.html +0 -28
  145. package/demos/particles.html +0 -24
  146. package/demos/patterns.html +0 -33
  147. package/demos/penrose-game.html +0 -31
  148. package/demos/pipeline.html +0 -737
  149. package/demos/plane3d.html +0 -24
  150. package/demos/platformer.html +0 -43
  151. package/demos/scene.html +0 -33
  152. package/demos/scenes.html +0 -96
  153. package/demos/schrodinger.html +0 -27
  154. package/demos/schwarzschild.html +0 -27
  155. package/demos/shapes.html +0 -16
  156. package/demos/space.html +0 -85
  157. package/demos/spacetime.html +0 -27
  158. package/demos/sphere3d.html +0 -24
  159. package/demos/sprite.html +0 -18
  160. package/demos/svgtween.html +0 -29
  161. package/demos/tde.html +0 -28
  162. package/demos/tiles.html +0 -28
  163. package/demos/transforms.html +0 -400
  164. package/demos/tween.html +0 -45
  165. package/demos/visibility.html +0 -33
  166. package/docs/README.md +0 -230
  167. package/docs/api/FluidSystem.md +0 -173
  168. package/docs/concepts/architecture-overview.md +0 -204
  169. package/docs/concepts/coordinate-system.md +0 -384
  170. package/docs/concepts/lifecycle.md +0 -255
  171. package/docs/concepts/rendering-pipeline.md +0 -279
  172. package/docs/concepts/shapes-vs-gameobjects.md +0 -187
  173. package/docs/concepts/tde-zorder.md +0 -106
  174. package/docs/concepts/two-layer-architecture.md +0 -229
  175. package/docs/fluid-dynamics.md +0 -99
  176. package/docs/getting-started/first-game.md +0 -354
  177. package/docs/getting-started/hello-world.md +0 -269
  178. package/docs/getting-started/installation.md +0 -175
  179. package/docs/modules/collision/README.md +0 -453
  180. package/docs/modules/fluent/README.md +0 -1075
  181. package/docs/modules/game/README.md +0 -303
  182. package/docs/modules/isometric-camera.md +0 -210
  183. package/docs/modules/isometric.md +0 -275
  184. package/docs/modules/painter/README.md +0 -328
  185. package/docs/modules/particle/README.md +0 -559
  186. package/docs/modules/shapes/README.md +0 -221
  187. package/docs/modules/shapes/base/euclidian.md +0 -123
  188. package/docs/modules/shapes/base/geometry2d.md +0 -204
  189. package/docs/modules/shapes/base/renderable.md +0 -215
  190. package/docs/modules/shapes/base/shape.md +0 -262
  191. package/docs/modules/shapes/base/transformable.md +0 -243
  192. package/docs/modules/shapes/hierarchy.md +0 -218
  193. package/docs/modules/state/README.md +0 -577
  194. package/docs/modules/util/README.md +0 -99
  195. package/docs/modules/util/camera3d.md +0 -412
  196. package/docs/modules/util/scene3d.md +0 -395
  197. package/index.html +0 -17
  198. package/jsdoc.json +0 -50
  199. package/scripts/build-demo.js +0 -69
  200. package/scripts/bundle4llm.js +0 -276
  201. package/scripts/clearconsole.js +0 -48
  202. package/test/math/orbital.test.js +0 -61
  203. package/test/math/tensor.test.js +0 -114
  204. package/test/particle/emitter.test.js +0 -204
  205. package/test/particle/particle-system.test.js +0 -310
  206. package/test/particle/particle.test.js +0 -116
  207. package/test/particle/updaters.test.js +0 -386
  208. package/test/setup.js +0 -120
  209. package/test/shapes/euclidian.test.js +0 -44
  210. package/test/shapes/geometry.test.js +0 -86
  211. package/test/shapes/group.test.js +0 -86
  212. package/test/shapes/rectangle.test.js +0 -64
  213. package/test/shapes/transform.test.js +0 -379
  214. package/test/util/camera3d.test.js +0 -428
  215. package/test/util/scene3d.test.js +0 -352
  216. package/vite.config.js +0 -50
  217. package/vitest.config.js +0 -13
@@ -1,239 +0,0 @@
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
- }
@@ -1,448 +0,0 @@
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
- });