@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,241 +0,0 @@
1
- import {
2
- BezierShape,
3
- Circle,
4
- FPSCounter,
5
- Game,
6
- GameObject,
7
- Tween,
8
- Painter,
9
- Scene,
10
- Easing,
11
- } from "../../src/index";
12
-
13
- class MyGame extends Game {
14
- constructor(canvas) {
15
- super(canvas);
16
- this.enableFluidSize();
17
- this.backgroundColor = "black";
18
- }
19
-
20
- init() {
21
- super.init();
22
- // Set up scenes
23
- this.scene = new Scene(this);
24
- this.ui = new Scene(this);
25
- this.pipeline.add(this.scene); // game layer
26
- this.pipeline.add(this.ui); // UI layer
27
-
28
- // Add signature animation
29
- this.signature = new SignatureAnimation(this, {
30
- debug: true,
31
- anchor: "center",
32
- });
33
- this.signature.width = 400;
34
- this.signature.height = 150;
35
- this.scene.add(this.signature);
36
-
37
- // Add FPS counter in the UI scene
38
- this.ui.add(new FPSCounter(this, { anchor: "bottom-right" }));
39
- }
40
- }
41
-
42
- // Signature Animation - A progressive bezier curve animation
43
- class SignatureAnimation extends GameObject {
44
- constructor(game, options = {}) {
45
- super(game, options);
46
-
47
- // The signature path - represents a cursive signature
48
- this.signaturePath = [
49
- // First part - the cursive letter (stylized name)
50
- ["M", -200, 20],
51
- ["C", -180, -40, -160, 40, -140, 10],
52
- ["C", -120, -30, -100, 40, -80, 0],
53
- ["C", -60, -40, -40, 40, -20, 10],
54
- ["C", 0, -20, 20, -30, 40, 10],
55
- ["C", 60, 40, 80, 40, 100, 20],
56
- ["C", 120, 0, 140, -10, 160, 10],
57
- ["C", 180, 30, 200, 30, 220, 10],
58
-
59
- // The underline swoop
60
- ["M", -180, 40],
61
- ["C", -100, 60, 100, 80, 250, 30],
62
- ];
63
-
64
- // Initialize state
65
- this.progress = 0;
66
- this.speed = 0.3; // Speed of animation
67
- this.complete = false;
68
-
69
- // Create visible bezier shape for the signature
70
- this.signature = new BezierShape(
71
- [], // Start with empty path
72
- {
73
- stroke: "#fff",
74
- lineWidth: 3,
75
- color: null,
76
- debug: true,
77
- }
78
- );
79
-
80
- // Create a circle to represent the pen tip
81
- this.penTip = new Circle(8, {
82
- x: game.width / 2,
83
- y: game.height / 2,
84
- color: "#4f8",
85
- shadowColor: "rgba(64, 255, 128, 0.8)",
86
- shadowBlur: 15,
87
- });
88
-
89
- // Canvas click handler to restart animation
90
- game.canvas.addEventListener("click", () => this.restart());
91
- }
92
-
93
- // Restart the animation
94
- restart() {
95
- this.progress = 0;
96
- this.complete = false;
97
- }
98
-
99
- // Calculate the current point along a particular bezier curve segment
100
- getBezierPoint(segment, t) {
101
- if (segment[0] === "M") {
102
- // For move commands, just return the point
103
- return { x: segment[1], y: segment[2] };
104
- } else if (segment[0] === "C") {
105
- // For Cubic Bezier curves, calculate the point at t
106
- const startX = this.prevX || 0;
107
- const startY = this.prevY || 0;
108
- const cp1x = segment[1];
109
- const cp1y = segment[2];
110
- const cp2x = segment[3];
111
- const cp2y = segment[4];
112
- const endX = segment[5];
113
- const endY = segment[6];
114
-
115
- // Cubic Bezier formula
116
- const x =
117
- Math.pow(1 - t, 3) * startX +
118
- 3 * Math.pow(1 - t, 2) * t * cp1x +
119
- 3 * (1 - t) * Math.pow(t, 2) * cp2x +
120
- Math.pow(t, 3) * endX;
121
-
122
- const y =
123
- Math.pow(1 - t, 3) * startY +
124
- 3 * Math.pow(1 - t, 2) * t * cp1y +
125
- 3 * (1 - t) * Math.pow(t, 2) * cp2y +
126
- Math.pow(t, 3) * endY;
127
-
128
- return { x, y };
129
- }
130
-
131
- return { x: 0, y: 0 };
132
- }
133
-
134
- // Get a subset of the path up to the current progress
135
- getPartialPath() {
136
- const result = [];
137
- let totalSegments = this.signaturePath.length;
138
- let segmentIndex = Math.floor(this.progress * totalSegments);
139
- let segmentProgress = (this.progress * totalSegments) % 1;
140
-
141
- // Add all completed segments
142
- for (let i = 0; i < segmentIndex; i++) {
143
- result.push([...this.signaturePath[i]]);
144
-
145
- // Keep track of the last point for calculating bezier curves
146
- if (this.signaturePath[i][0] === "M") {
147
- this.prevX = this.signaturePath[i][1];
148
- this.prevY = this.signaturePath[i][2];
149
- } else if (this.signaturePath[i][0] === "C") {
150
- this.prevX = this.signaturePath[i][5];
151
- this.prevY = this.signaturePath[i][6];
152
- }
153
- }
154
-
155
- // Add the current segment with partial progress
156
- if (segmentIndex < totalSegments) {
157
- const currentSegment = this.signaturePath[segmentIndex];
158
-
159
- if (currentSegment[0] === "M") {
160
- // For move commands, add the full command
161
- result.push([...currentSegment]);
162
- this.prevX = currentSegment[1];
163
- this.prevY = currentSegment[2];
164
-
165
- // Position pen tip at the move point
166
- this.penTipPos = {
167
- x: currentSegment[1],
168
- y: currentSegment[2],
169
- };
170
- } else if (currentSegment[0] === "C") {
171
- // For bezier curves, calculate the partial command
172
- const point = this.getBezierPoint(currentSegment, segmentProgress);
173
-
174
- // Add a partial curve to the result
175
- result.push([
176
- "C",
177
- currentSegment[1],
178
- currentSegment[2],
179
- currentSegment[3],
180
- currentSegment[4],
181
- point.x,
182
- point.y,
183
- ]);
184
-
185
- // Position pen tip at the end of the partial curve
186
- this.penTipPos = point;
187
- }
188
- }
189
-
190
- return result;
191
- }
192
-
193
- update(dt) {
194
- // Update progress if animation not complete
195
- if (!this.complete) {
196
- this.progress += dt * this.speed;
197
-
198
- if (this.progress >= 1) {
199
- this.progress = 1;
200
- this.complete = true;
201
- }
202
- // Calculate partial path based on current progress
203
- this.currentPath = this.getPartialPath();
204
- // Update signature path
205
- this.signature.path = this.currentPath;
206
- }
207
-
208
- // Add gentle bouncing motion when complete
209
- if (this.complete) {
210
- const time = performance.now() / 1000;
211
- this.signature.y = Math.sin(time * 2) * 5;
212
- }
213
-
214
- // Update pen tip position
215
- if (this.penTipPos) {
216
- this.penTip.x = this.penTipPos.x;
217
- this.penTip.y = this.penTipPos.y;
218
- }
219
- super.update(dt);
220
- }
221
-
222
- draw() {
223
- super.draw();
224
- // Draw signature
225
- this.signature.render();
226
- // Draw pen tip if animation is not complete
227
- if (!this.complete) {
228
- this.penTip.render();
229
- }
230
- }
231
-
232
- render() {
233
- super.render();
234
- Painter.text.setFont("18px monospace");
235
- Painter.text.setTextAlign("center");
236
- Painter.text.setTextBaseline("bottom");
237
- Painter.text.fillText("Click anywhere to restart the signature animation", this.game.width / 2, this.game.height - 40, "#4f8");
238
- }
239
- }
240
-
241
- export { MyGame };
@@ -1,379 +0,0 @@
1
- /**
2
- * AccretionDisk - Manages disk particles and formation animation
3
- *
4
- * Handles particle creation, formation state transitions,
5
- * camera-space projection with gravitational lensing, and Doppler effects.
6
- */
7
- import { GameObject, Easing } from "../../../src/index.js";
8
- import { Particle } from "./particle.js";
9
-
10
- // Formation source configuration
11
- const INFALL_SOURCE_ANGLE = Math.PI * 1.25; // Top-right corner
12
- const INFALL_STREAM_WIDTH = 0.12;
13
- const INFALL_SPIRAL_TURNS = 1.5;
14
-
15
- export class AccretionDisk extends GameObject {
16
- /**
17
- * @param {Game} game - Game instance
18
- * @param {Object} options
19
- * @param {Camera3D} options.camera - Camera for projection
20
- * @param {StateMachine} options.formationFSM - Formation state machine
21
- * @param {number} options.baseScale - Base scale for sizing
22
- * @param {number} options.bhRadius - Black hole radius
23
- * @param {number} options.diskInner - Inner disk radius
24
- * @param {number} options.diskOuter - Outer disk radius
25
- * @param {number} [options.particleCount=2500] - Number of particles
26
- * @param {number} [options.diskTilt=0] - Disk tilt in radians
27
- * @param {Object} [options.colors] - Color configuration
28
- */
29
- constructor(game, options = {}) {
30
- super(game, options);
31
-
32
- this.camera = options.camera;
33
- this.formationFSM = options.formationFSM;
34
-
35
- // Sizing (updated from main demo on resize)
36
- this.baseScale = options.baseScale ?? 500;
37
- this.bhRadius = options.bhRadius ?? 40;
38
- this.diskInner = options.diskInner ?? 60;
39
- this.diskOuter = options.diskOuter ?? 175;
40
- this.diskTilt = options.diskTilt ?? 0;
41
-
42
- // Particle configuration
43
- this.particleCount = options.particleCount ?? 2500;
44
- this.particles = [];
45
-
46
- // Colors for temperature gradient
47
- this.colors = options.colors ?? {
48
- inner: [255, 250, 220], // White-hot
49
- mid: [255, 160, 50], // Orange
50
- outer: [180, 40, 40], // Deep red
51
- };
52
-
53
- // Consumption tracking
54
- this.particlesConsumed = 0;
55
- this.totalParticleMass = this.particleCount;
56
- }
57
-
58
- /**
59
- * Update sizing when window resizes.
60
- */
61
- updateSizing(baseScale, bhRadius, diskInner, diskOuter) {
62
- this.baseScale = baseScale;
63
- this.bhRadius = bhRadius;
64
- this.diskInner = diskInner;
65
- this.diskOuter = diskOuter;
66
- }
67
-
68
- /**
69
- * Get temperature-based color for a particle at radius r.
70
- */
71
- getHeatColor(r) {
72
- const t = (r - this.diskInner) / (this.diskOuter - this.diskInner);
73
- const c1 = t < 0.3 ? this.colors.inner : this.colors.mid;
74
- const c2 = t < 0.3 ? this.colors.mid : this.colors.outer;
75
- const mix = t < 0.3 ? t / 0.3 : (t - 0.3) / 0.7;
76
-
77
- return {
78
- r: c1[0] + (c2[0] - c1[0]) * mix,
79
- g: c1[1] + (c2[1] - c1[1]) * mix,
80
- b: c1[2] + (c2[2] - c1[2]) * mix,
81
- a: 1 - t,
82
- };
83
- }
84
-
85
- /**
86
- * Initialize particles at their final disk positions.
87
- * Used for initial load and resize.
88
- */
89
- initParticles() {
90
- this.particles = [];
91
- for (let i = 0; i < this.particleCount; i++) {
92
- const angle = Math.random() * Math.PI * 2;
93
- const t = Math.random();
94
- // Bias toward inner (hotter) region
95
- const r = this.diskInner + t * t * (this.diskOuter - this.diskInner);
96
-
97
- const speed = (1 / Math.sqrt(r)) * 600; // Keplerian
98
- const yOffset = (Math.random() - 0.5) * this.baseScale * 0.006;
99
- const baseColor = this.getHeatColor(r);
100
-
101
- this.particles.push(
102
- Particle.createForDisk(angle, r, yOffset, speed, baseColor),
103
- );
104
- }
105
- }
106
-
107
- /**
108
- * Reset particles for infall animation.
109
- * Sets up continuous stream from top-right corner.
110
- */
111
- initParticlesForInfall() {
112
- this.particlesConsumed = 0;
113
- this.totalParticleMass = this.particles.length;
114
-
115
- for (let i = 0; i < this.particles.length; i++) {
116
- const p = this.particles[i];
117
-
118
- // Stream offset - like beads on a string
119
- p.streamOffset = i / this.particles.length;
120
-
121
- // Start position: far off-screen
122
- const angleOffset = (Math.random() - 0.5) * INFALL_STREAM_WIDTH;
123
- p.startAngle = INFALL_SOURCE_ANGLE + angleOffset;
124
- p.startDistance =
125
- this.baseScale * 0.9 + Math.random() * this.baseScale * 0.3;
126
- p.startYOffset = (Math.random() - 0.5) * this.baseScale * 0.08;
127
-
128
- // ~40% fall into black hole, ~60% form the disk
129
- p.willFallIn = Math.random() < 0.4;
130
- p.consumed = false;
131
-
132
- if (p.willFallIn) {
133
- p.targetDistance = 0;
134
- p.spiralTurns = INFALL_SPIRAL_TURNS * (1.5 + Math.random() * 0.5);
135
- } else {
136
- p.targetAngle = Math.random() * Math.PI * 2;
137
- const t = Math.random();
138
- p.targetDistance =
139
- this.diskInner + t * t * (this.diskOuter - this.diskInner);
140
- p.spiralTurns =
141
- INFALL_SPIRAL_TURNS +
142
- (p.targetAngle - INFALL_SOURCE_ANGLE) / (Math.PI * 2);
143
- }
144
-
145
- p.targetYOffset = (Math.random() - 0.5) * this.baseScale * 0.006;
146
-
147
- // Initialize to start position
148
- p.angle = p.startAngle;
149
- p.distance = p.startDistance;
150
- p.yOffset = p.startYOffset;
151
-
152
- // Color based on final position
153
- p.baseColor = p.willFallIn
154
- ? { r: 255, g: 200, b: 150, a: 1 }
155
- : this.getHeatColor(p.targetDistance);
156
- }
157
- }
158
-
159
- /**
160
- * Normalize angle to [-PI, PI] range.
161
- */
162
- normalizeAngle(angle) {
163
- while (angle > Math.PI) angle -= Math.PI * 2;
164
- while (angle < -Math.PI) angle += Math.PI * 2;
165
- return angle;
166
- }
167
-
168
- update(dt) {
169
- super.update(dt);
170
- this.updateParticleFormation(dt);
171
- }
172
-
173
- /**
174
- * Update particle positions based on formation state.
175
- */
176
- updateParticleFormation(dt) {
177
- const state = this.formationFSM.state;
178
- const progress = this.formationFSM.progress;
179
-
180
- for (const p of this.particles) {
181
- if (p.consumed) continue;
182
-
183
- if (state === "infall") {
184
- this.updateInfall(p, progress);
185
- } else if (state === "collapse") {
186
- this.updateCollapse(p, progress, dt);
187
- } else if (state === "circularize") {
188
- this.updateCircularize(p, progress, dt);
189
- } else if (state === "stable") {
190
- this.updateStable(p, dt);
191
- }
192
- }
193
- }
194
-
195
- updateInfall(p, progress) {
196
- const particleProgress = progress + p.streamOffset * 0.5;
197
- const t = Math.min(1, Easing.easeInQuad(Math.max(0, particleProgress)));
198
-
199
- // Spiral inward
200
- const spiralAngle = p.startAngle + p.spiralTurns * Math.PI * 2 * t;
201
- p.angle = spiralAngle;
202
-
203
- const targetDist = p.willFallIn ? 0 : this.bhRadius * 2;
204
- p.distance = Easing.lerp(p.startDistance, targetDist, t);
205
- p.yOffset = Easing.lerp(p.startYOffset, 0, t);
206
-
207
- // Check if consumed
208
- if (p.willFallIn && p.distance < this.bhRadius * 0.5) {
209
- p.consumed = true;
210
- this.particlesConsumed++;
211
- }
212
- }
213
-
214
- updateCollapse(p, progress, dt) {
215
- // Delay particle spreading so black hole grows first
216
- // Particles wait until 20% into collapse before spreading
217
- const delayedProgress = Math.max(0, (progress - 0.2) / 0.8);
218
- const t = Easing.easeOutQuad(delayedProgress);
219
-
220
- if (p.willFallIn) {
221
- // Falling particles spiral in immediately
222
- const fallT = Easing.easeInQuad(progress);
223
- p.distance = Easing.lerp(p.distance, 0, fallT * 0.7);
224
- p.angle += p.spiralTurns * 0.1 * (1 - progress);
225
-
226
- if (p.distance < this.bhRadius * 0.5) {
227
- p.consumed = true;
228
- this.particlesConsumed++;
229
- }
230
- } else if (delayedProgress > 0) {
231
- // Disk particles spread slowly - reduced factors for gradual expansion
232
- p.distance = Easing.lerp(p.distance, p.targetDistance, t * 0.3);
233
- p.angle = Easing.lerp(p.angle, p.targetAngle, t * 0.2);
234
- p.yOffset = Easing.lerp(p.yOffset, p.targetYOffset, t * 0.2);
235
- }
236
- }
237
-
238
- updateCircularize(p, progress, dt) {
239
- const t = Easing.easeOutCubic(progress) * p.circularizeSpeed;
240
- const clampedT = Math.min(1, t);
241
-
242
- p.distance = Easing.lerp(p.distance, p.targetDistance, clampedT * 0.15);
243
- p.yOffset = Easing.lerp(p.yOffset, p.targetYOffset, clampedT * 0.2);
244
-
245
- // Start orbital motion
246
- const orbitSpeed = p.speed * dt * 0.01 * clampedT;
247
- p.angle += orbitSpeed;
248
-
249
- // Drift toward target angle
250
- const angleDiff = this.normalizeAngle(p.targetAngle - p.angle);
251
- p.angle += angleDiff * 0.03 * clampedT;
252
- }
253
-
254
- updateStable(p, dt) {
255
- // Occasionally a particle loses angular momentum and falls in
256
- if (
257
- !p.isFalling &&
258
- p.distance < this.diskInner * 1.5 &&
259
- Math.random() < 0.0001
260
- ) {
261
- p.isFalling = true;
262
- }
263
-
264
- if (p.isFalling) {
265
- p.distance *= 0.985;
266
- p.angle += p.speed * dt * 0.03;
267
- p.yOffset *= 0.95;
268
-
269
- if (p.distance < this.bhRadius * 0.5) {
270
- p.consumed = true;
271
- this.particlesConsumed++;
272
- }
273
- } else {
274
- // Normal Keplerian orbits
275
- p.angle += p.speed * dt * 0.01;
276
- p.distance = Easing.lerp(p.distance, p.targetDistance, 0.02);
277
- p.yOffset = Easing.lerp(p.yOffset, p.targetYOffset, 0.02);
278
- }
279
- }
280
-
281
- /**
282
- * Build render list with projected particles.
283
- * Returns array of render items for depth sorting.
284
- */
285
- buildRenderList() {
286
- const state = this.formationFSM.state;
287
- const renderList = [];
288
-
289
- const diskAlpha =
290
- state === "infall" || state === "collapse"
291
- ? 0.9
292
- : state === "stable"
293
- ? 1
294
- : Math.max(0.5, this.formationFSM.progress);
295
-
296
- // Calculate lensing strength based on formation state
297
- let lensingStrength = 0;
298
- if (state === "stable") {
299
- lensingStrength = 1;
300
- } else if (state === "circularize") {
301
- lensingStrength =
302
- 0.4 + Easing.easeOutCubic(this.formationFSM.progress) * 0.6;
303
- } else if (state === "collapse") {
304
- lensingStrength = Easing.easeInQuad(this.formationFSM.progress) * 0.4;
305
- }
306
-
307
- const cosTilt = Math.cos(this.diskTilt);
308
- const sinTilt = Math.sin(this.diskTilt);
309
-
310
- for (const p of this.particles) {
311
- if (p.consumed) continue;
312
-
313
- // World coordinates (flat disk)
314
- let x = Math.cos(p.angle) * p.distance;
315
- let z = Math.sin(p.angle) * p.distance;
316
- let y = p.yOffset;
317
-
318
- // Apply disk tilt
319
- const yTilted = y * cosTilt - z * sinTilt;
320
- const zTilted = y * sinTilt + z * cosTilt;
321
- y = yTilted;
322
- z = zTilted;
323
-
324
- // Transform to camera space
325
- const cosY = Math.cos(this.camera.rotationY);
326
- const sinY = Math.sin(this.camera.rotationY);
327
- let xCam = x * cosY - z * sinY;
328
- let zCam = x * sinY + z * cosY;
329
-
330
- const cosX = Math.cos(this.camera.rotationX);
331
- const sinX = Math.sin(this.camera.rotationX);
332
- let yCam = y * cosX - zCam * sinX;
333
- zCam = y * sinX + zCam * cosX;
334
-
335
- // Apply gravitational lensing
336
- if (lensingStrength > 0 && zCam > 0) {
337
- const currentR = Math.sqrt(xCam * xCam + yCam * yCam);
338
- const ringRadius = this.bhRadius * 1.3;
339
- const lensFactor = Math.exp(-currentR / (this.bhRadius * 1.5));
340
- const warp = lensFactor * 1.2 * lensingStrength;
341
-
342
- if (currentR > 0) {
343
- const ratio = (currentR + ringRadius * warp) / currentR;
344
- xCam *= ratio;
345
- yCam *= ratio;
346
- } else {
347
- yCam = ringRadius * lensingStrength;
348
- }
349
- }
350
-
351
- // Perspective projection
352
- const perspectiveScale =
353
- this.camera.perspective / (this.camera.perspective + zCam);
354
- const screenX = xCam * perspectiveScale;
355
- const screenY = yCam * perspectiveScale;
356
-
357
- if (zCam < -this.camera.perspective + 10) continue;
358
-
359
- // Doppler effect
360
- const velocityDir = Math.cos(p.angle + this.camera.rotationY);
361
- const doppler = 1 + velocityDir * 0.4;
362
-
363
- renderList.push({
364
- type: "particle",
365
- z: zCam,
366
- x: screenX,
367
- y: screenY,
368
- scale: perspectiveScale,
369
- color: p.baseColor,
370
- doppler: doppler,
371
- diskAlpha: diskAlpha,
372
- isFalling: p.isFalling || p.willFallIn,
373
- horizonProximity: p.distance / this.bhRadius,
374
- });
375
- }
376
-
377
- return renderList;
378
- }
379
- }