@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,297 +0,0 @@
1
- import { GameObject, Sphere3D } from "../../../src/index.js";
2
- import { polarToCartesian } from "../../../src/math/gr.js";
3
- import { CONFIG } from "./config.js";
4
-
5
- // Performance tuning: reduce update frequency for expensive operations
6
- const PERF_CONFIG = {
7
- geometryUpdateThreshold: 0.02, // Only regenerate geometry if radius changes by 2%
8
- uniformUpdateInterval: 2, // Update shader uniforms every N frames
9
- breathingEnabled: true, // Toggle breathing effect
10
- stressColorEnabled: true, // Toggle dynamic color shifts
11
- };
12
-
13
- export class Star extends GameObject {
14
- constructor(game, options = {}) {
15
- super(game, options);
16
- this.mass = options.initialMass ?? CONFIG.star.initialMass;
17
- this.initialMass = this.mass; // Store for mass ratio calculations
18
- this.phi = 0;
19
- // Initialize with reasonable defaults, will be updated by onResize
20
- this.baseRadius = game.baseScale ? game.baseScale * CONFIG.starRadiusRatio : 20;
21
- this.currentRadius = this.baseRadius;
22
- this.orbitalRadius = game.baseScale ? game.baseScale * CONFIG.star.initialOrbitRadius : 200;
23
- this.initialOrbitalRadius = this.orbitalRadius; // Store initial for decay calculations
24
-
25
- // Velocity tracking for particle emission
26
- this.velocityX = 0;
27
- this.velocityY = 0;
28
- this.velocityZ = 0;
29
- this._prevX = 0;
30
- this._prevY = 0;
31
- this._prevZ = 0;
32
-
33
- // Use WebGL shaders for star rendering
34
- this.useShader = options.useShader ?? true;
35
-
36
- // Cumulative rotation for angular emission detail
37
- this.rotation = 0;
38
- // Angular velocity (rad/s) - accumulates smoothly instead of discrete recalc
39
- this.angularVelocity = CONFIG.star.rotationSpeed ?? 0.5;
40
-
41
- // Tidal disruption state
42
- this.tidalStretch = 0; // 0 = spherical, 1 = max elongation
43
- this.pulsationPhase = 0; // Oscillation phase
44
- this.stressLevel = 0; // Surface chaos level
45
- this.tidalProgress = 0; // External tidal progress from FSM (0-1)
46
- this.tidalFlare = 0; // 0-1, sudden brightness burst at disruption start
47
- this.tidalWobble = 0; // 0-1, violent geometry wobble during trauma
48
-
49
- // Performance optimization state
50
- this._frameCount = 0;
51
- this._lastGeometryRadius = 0;
52
- this._cachedUniforms = null;
53
- }
54
-
55
- init() {
56
- // Initialize position on the orbit
57
- const pos = polarToCartesian(this.orbitalRadius, this.phi);
58
- this.x = pos.x;
59
- this.z = pos.z;
60
-
61
- // Initialize prev position to avoid velocity spike on first frame
62
- this._prevX = this.x;
63
- this._prevY = this.y || 0;
64
- this._prevZ = this.z;
65
- this.velocityX = 0;
66
- this.velocityY = 0;
67
- this.velocityZ = 0;
68
-
69
- // Reset tidal state
70
- this.tidalStretch = 0;
71
- this.pulsationPhase = 0;
72
- this.stressLevel = 0;
73
- this.tidalProgress = 0;
74
- this.tidalFlare = 0;
75
- this.tidalWobble = 0;
76
- this.angularVelocity = CONFIG.star.rotationSpeed ?? 0.5;
77
- this.rotation = 0;
78
-
79
- this.updateVisual();
80
- }
81
-
82
- /**
83
- * Reset velocity tracking (call after position changes like restart)
84
- */
85
- resetVelocity() {
86
- this._prevX = this.x;
87
- this._prevY = this.y || 0;
88
- this._prevZ = this.z;
89
- this.velocityX = 0;
90
- this.velocityY = 0;
91
- this.velocityZ = 0;
92
- }
93
-
94
- updateVisual() {
95
- const massRatio = this.mass / this.initialMass;
96
-
97
- // === NON-LINEAR SIZE COLLAPSE ===
98
- // Use sqrt for resistance curve (star resists early, then collapses)
99
- const collapseProgress = 1 - massRatio;
100
- const effectiveMassRatio = 1 - Math.sqrt(collapseProgress);
101
-
102
- // Base radius with non-linear collapse
103
- this.currentRadius = this.baseRadius * Math.max(0.05, effectiveMassRatio);
104
-
105
- // Don't update if star is consumed
106
- if (this.currentRadius <= 0 || this.mass <= 0) {
107
- return;
108
- }
109
-
110
- // === TIDAL STRETCH (Simplified) ===
111
- const zVal = this.z || 0;
112
- const distSq = this.x * this.x + zVal * zVal;
113
- const dist = Math.sqrt(distSq) || 1;
114
- const invDist = 1 / dist;
115
-
116
- // Direction toward black hole (unit vector)
117
- const dirX = -this.x * invDist;
118
- const dirZ = -zVal * invDist;
119
-
120
- // Proximity factor: closer to BH = more stretch
121
- const proximityFactor = Math.max(0, 1 - dist / this.initialOrbitalRadius);
122
-
123
- // Simplified stretch calculation
124
- if (collapseProgress > 0.8) {
125
- this.tidalStretch = (1 - collapseProgress) * 2;
126
- } else {
127
- this.tidalStretch = Math.min(1.8, this.tidalProgress * 1.2 + proximityFactor * 0.5);
128
- }
129
-
130
- // === BREATHING (Optional, can be disabled for performance) ===
131
- if (PERF_CONFIG.breathingEnabled) {
132
- const breathingAmp = 0.03 * (1 - collapseProgress * 0.5);
133
- this.currentRadius *= (1 + Math.sin(this.pulsationPhase) * breathingAmp);
134
- }
135
-
136
- // === STRESS LEVEL (Simplified power curve) ===
137
- const rawStress = proximityFactor * 0.4 + collapseProgress * 0.6;
138
- this.stressLevel = Math.min(1, rawStress * rawStress * rawStress); // Cubic approximation
139
-
140
- // === ACTIVITY & ROTATION ===
141
- const activityLevel = 0.3 + this.stressLevel * 0.7;
142
- const baseRotationSpeed = CONFIG.star.rotationSpeed ?? 0.5;
143
- const rotationSpeed = Math.min(10, baseRotationSpeed / Math.max(0.2, effectiveMassRatio));
144
-
145
- // === COLOR SHIFT (Simplified linear interpolation) ===
146
- let r = 1.0, g, b;
147
- const stress = this.stressLevel;
148
-
149
- if (PERF_CONFIG.stressColorEnabled) {
150
- // Simplified color: lerp from red-orange to white based on stress
151
- g = 0.35 + stress * 0.6; // 0.35 → 0.95
152
- b = 0.15 + stress * 0.7; // 0.15 → 0.85
153
- } else {
154
- g = 0.5;
155
- b = 0.2;
156
- }
157
-
158
- const stressColor = [r, g, b];
159
- this.currentColor = stressColor;
160
-
161
- // Temperature calculation
162
- const temperature = (CONFIG.star.temperature ?? 3800) + stress * stress * 2500;
163
-
164
- // === VISUAL UPDATE ===
165
- if (!this.visual) {
166
- this.visual = new Sphere3D(this.currentRadius, {
167
- color: CONFIG.star.color,
168
- camera: this.game.camera,
169
- useShader: this.useShader,
170
- shaderType: "star",
171
- shaderUniforms: {
172
- uStarColor: stressColor,
173
- uTemperature: temperature,
174
- uActivityLevel: activityLevel,
175
- uRotationSpeed: rotationSpeed,
176
- uTidalStretch: this.tidalStretch,
177
- uStretchDirX: dirX,
178
- uStretchDirZ: dirZ,
179
- uStressLevel: this.stressLevel,
180
- uTidalFlare: this.tidalFlare,
181
- uTidalWobble: this.tidalWobble,
182
- },
183
- });
184
- this._lastGeometryRadius = this.currentRadius;
185
- } else {
186
- this.visual.radius = this.currentRadius;
187
-
188
- // Only update shader uniforms every N frames
189
- this._frameCount++;
190
- if (this._frameCount >= PERF_CONFIG.uniformUpdateInterval) {
191
- this._frameCount = 0;
192
-
193
- if (this.visual.useShader) {
194
- this.visual.setShaderUniforms({
195
- uStarColor: stressColor,
196
- uTemperature: temperature,
197
- uActivityLevel: activityLevel,
198
- uRotationSpeed: rotationSpeed,
199
- uTidalStretch: this.tidalStretch,
200
- uStretchDirX: dirX,
201
- uStretchDirZ: dirZ,
202
- uStressLevel: this.stressLevel,
203
- uTidalFlare: this.tidalFlare,
204
- uTidalWobble: this.tidalWobble,
205
- });
206
- }
207
- }
208
-
209
- // Only regenerate geometry if radius changed significantly
210
- const radiusChange = Math.abs(this.currentRadius - this._lastGeometryRadius) / this._lastGeometryRadius;
211
- if (radiusChange > PERF_CONFIG.geometryUpdateThreshold) {
212
- this.visual._generateGeometry();
213
- this._lastGeometryRadius = this.currentRadius;
214
- }
215
- }
216
- }
217
-
218
- onResize(baseRadius, orbitalRadius) {
219
- this.baseRadius = baseRadius;
220
- this.orbitalRadius = orbitalRadius;
221
- this.initialOrbitalRadius = orbitalRadius;
222
-
223
- // Update position to match new orbital radius
224
- const pos = polarToCartesian(this.orbitalRadius, this.phi);
225
- this.x = pos.x;
226
- this.z = pos.z;
227
-
228
- this.updateVisual();
229
- }
230
-
231
- update(dt) {
232
- super.update(dt);
233
-
234
- // Calculate velocity from position change
235
- const currentY = this.y || 0;
236
- if (dt > 0) {
237
- this.velocityX = (this.x - this._prevX) / dt;
238
- this.velocityY = (currentY - this._prevY) / dt;
239
- this.velocityZ = (this.z - this._prevZ) / dt;
240
- }
241
-
242
- // Store current position for next frame
243
- this._prevX = this.x;
244
- this._prevY = currentY;
245
- this._prevZ = this.z;
246
-
247
- // Update self-rotation with smooth angular momentum conservation
248
- // As star shrinks, angular velocity increases (I*ω = constant)
249
- // But cap it when star is tiny (< 10% radius) - no point wasting frames
250
- const radiusRatio = this.currentRadius / this.baseRadius;
251
-
252
- if (radiusRatio > 0.1) {
253
- // Base rotation speed from config
254
- const baseSpeed = CONFIG.star.rotationSpeed ?? 0.5;
255
-
256
- // Spin-up factor based on tidal progress (FSM-driven, smooth)
257
- // Only significant spin-up during actual disruption (mass loss)
258
- const massRatio = (this.mass || 1) / (this.initialMass || 1);
259
- const massLoss = 1 - massRatio; // 0 = no loss, 1 = fully consumed
260
-
261
- // Gentle spin-up from tidal stress, moderate spin-up from mass loss
262
- // tidalProgress: 0-1 during stretch, 1 during disrupt
263
- // massLoss: 0 during stretch, 0-1 during disrupt
264
- const tidalSpinUp = 1 + this.tidalProgress * 0.3; // Up to 1.3x from tidal
265
- const collapseSpinUp = 1 + massLoss * 1.5; // Up to 2.5x from collapse
266
-
267
- const targetVelocity = baseSpeed * tidalSpinUp * collapseSpinUp;
268
-
269
- // Very slow approach to target - no sudden jumps
270
- const accelRate = 0.001;
271
- this.angularVelocity += (targetVelocity - this.angularVelocity) * accelRate * dt;
272
-
273
- // Hard cap on max spin (2.5 rad/s - calm, cosmic feel)
274
- this.angularVelocity = Math.min(2.5, this.angularVelocity);
275
- }
276
- // else: keep current velocity, don't accelerate tiny remnant
277
-
278
- this.rotation += this.angularVelocity * dt;
279
-
280
- // Update breathing phase - slow, cosmic rhythm (0.3-0.5 Hz)
281
- const breathingFreq = 0.3 + this.stressLevel * 0.2;
282
- this.pulsationPhase += breathingFreq * dt * Math.PI * 2;
283
-
284
- this.updateVisual();
285
- }
286
-
287
- render() {
288
- super.render();
289
- if (this.mass > 0 && this.visual) {
290
- // Sync visual position with star position
291
- this.visual.x = this.x;
292
- this.visual.y = this.y || 0;
293
- this.visual.z = this.z;
294
- this.visual.render();
295
- }
296
- }
297
- }
@@ -1,372 +0,0 @@
1
- import { GameObject, Painter } from "../../../src/index.js";
2
- import { applyGravitationalLensing } from "../../../src/math/gr.js";
3
-
4
- /**
5
- * TidalStream - Simple particle stream from star to black hole
6
- *
7
- * Physics:
8
- * - Particles emitted from star inherit star's velocity
9
- * - Gravity attracts particles toward black hole (0,0,0)
10
- * - Gravitational lensing bends particle paths near the BH
11
- */
12
-
13
- // Stream-specific config
14
- const STREAM_CONFIG = {
15
- gravity: 120000, // Strong gravity (linear falloff G/r)
16
- maxParticles: 10000,
17
- particleLifetime: 20, // Seconds - long lifetime so particles can orbit the BH
18
-
19
- // Velocity inheritance - how much of star's velocity particles get
20
- // Lower = particles emit more "from" the star, not ahead of it
21
- velocityInheritance: 0.3,
22
-
23
- // Inward velocity - particles should FALL toward BH, not orbit
24
- // This is the key to making particles flow INTO the black hole
25
- inwardVelocity: 8, // Base inward velocity toward BH
26
- inwardSpread: 15, // Random spread on inward velocity
27
-
28
- // Tangent spread for S-shape - higher = more spread along orbit direction
29
- tangentSpread: Math.PI * 150, // Spread for visible S-shape
30
-
31
- // Emission offset: 1.0 = star's BH-facing edge (L1 Lagrange point)
32
- // Positive = toward BH, negative = away from BH
33
- emissionOffset: -1 * Math.PI, // Larger numbers create bigger S-Shape. Negative PI works very well here for some reason makes the animation very cool.
34
-
35
- // Drag factor - removes angular momentum so orbits decay
36
- // 1.0 = no drag, 0.99 = slight drag, 0.95 = strong drag
37
- drag: 0.994,
38
-
39
- // Colors: match star shader at emission, cool as they approach BH
40
- colorHot: { r: 255, g: 95, b: 45 }, // Deep red-orange (matches star shader initial)
41
- colorCool: { r: 180, g: 40, b: 15 }, // Darker red near BH
42
-
43
- // Particle size
44
- sizeMin: 1,
45
- sizeMax: 1.2,
46
-
47
- // Gravitational lensing (visual effect)
48
- // These are multipliers relative to the BH's current radius
49
- lensing: {
50
- enabled: true,
51
- effectRadiusMult: 6.0, // Effect extends to 6x BH radius
52
- strengthMult: 2.5, // Strength scales with BH radius
53
- falloff: 0.008, // Exponential falloff (higher = tighter effect)
54
- minDistanceMult: 0.2, // Min distance as fraction of BH radius
55
- },
56
- };
57
-
58
- export class TidalStream extends GameObject {
59
- constructor(game, options = {}) {
60
- super(game, options);
61
-
62
- this.camera = options.camera;
63
- this.scene = options.scene; // Scene reference for screen center
64
- this.bhRadius = options.bhRadius ?? 50;
65
-
66
- // Callbacks for particle lifecycle
67
- this.onParticleConsumed = options.onParticleConsumed ?? null;
68
- this.onParticleCaptured = options.onParticleCaptured ?? null;
69
-
70
- // Particle array - simple flat structure
71
- this.particles = [];
72
- }
73
-
74
- init() {
75
- this.particles = [];
76
- }
77
-
78
- /**
79
- * Emit a particle from the star
80
- *
81
- * For S-shape formation, particles need TANGENTIAL velocity spread:
82
- * - Faster particles (more angular momentum) spiral outward
83
- * - Slower particles (less angular momentum) spiral inward
84
- * - This creates two opposing tails = S-shape
85
- *
86
- * @param {number} x - Star x position
87
- * @param {number} y - Star y position
88
- * @param {number} z - Star z position
89
- * @param {number} vx - Star velocity x
90
- * @param {number} vy - Star velocity y
91
- * @param {number} vz - Star velocity z
92
- * @param {number} starRadius - Current star radius (for position spread)
93
- * @param {number} starRotation - Current star rotation (for angular offset)
94
- * @param {Array<number>} starColor - Current star color as [r, g, b] normalized (0-1)
95
- */
96
- emit(x, y, z, vx, vy, vz, starRadius, starRotation = 0, starColor = null) {
97
- if (this.particles.length >= STREAM_CONFIG.maxParticles) return;
98
-
99
- const dist = Math.sqrt(x * x + z * z) || 1;
100
-
101
- // Direction toward BH in x-z plane (unit vector)
102
- const radialX = -x / dist;
103
- const radialZ = -z / dist;
104
-
105
- // Emit from star center with spread for visible "bleeding" effect
106
- // Larger spread = bigger emission hole on the star
107
- const emitX = x + (Math.random() - 0.5) * starRadius * 0.8;
108
- const emitY = y + (Math.random() - 0.5) * starRadius * 0.8;
109
- const emitZ = z + (Math.random() - 0.5) * starRadius * 0.8;
110
-
111
- // Tangent is perpendicular to radial - gives the orbital direction
112
- const tangentX = -radialZ;
113
- const tangentZ = radialX;
114
-
115
- // Reduce inherited velocity so gravity can dominate
116
- const inheritedVx = vx * STREAM_CONFIG.velocityInheritance;
117
- const inheritedVz = vz * STREAM_CONFIG.velocityInheritance;
118
-
119
- // INWARD velocity - particles flow TOWARD the black hole
120
- // radialX, radialZ point toward BH (origin)
121
- const inward = STREAM_CONFIG.inwardVelocity + (Math.random() - 0.5) * STREAM_CONFIG.inwardSpread;
122
-
123
- // Small tangential spread for the S-shape variation
124
- const tangent = (Math.random() - 0.5) * STREAM_CONFIG.tangentSpread;
125
-
126
- // Store star color at emission time (convert from normalized 0-1 to 0-255)
127
- const emitColor = starColor
128
- ? { r: starColor[0] * 255, g: starColor[1] * 255, b: starColor[2] * 255 }
129
- : STREAM_CONFIG.colorHot;
130
-
131
- this.particles.push({
132
- x: emitX,
133
- y: emitY,
134
- z: emitZ,
135
-
136
- // Velocity = inherited + INWARD toward BH + small tangent spread
137
- vx: inheritedVx + radialX * inward + tangentX * tangent,
138
- vy: vy,
139
- vz: inheritedVz + radialZ * inward + tangentZ * tangent,
140
-
141
- age: 0,
142
- size: STREAM_CONFIG.sizeMin + Math.random() * (STREAM_CONFIG.sizeMax - STREAM_CONFIG.sizeMin),
143
-
144
- // Track initial distance for color gradient
145
- initialDist: dist,
146
-
147
- // Store the star's color at emission time
148
- emitColor,
149
- });
150
- }
151
-
152
- updateDiskBounds(innerRadius, outerRadius) {
153
- // Don't override bhRadius here - it's set by updateBHRadius
154
- // We only care about disk bounds for potential capture detection
155
- this.diskInnerRadius = innerRadius;
156
- this.diskOuterRadius = outerRadius;
157
- }
158
-
159
- /**
160
- * Update all particles - just gravity
161
- */
162
- update(dt) {
163
- super.update(dt);
164
-
165
- // Consume particles at the BH's visual edge (not inside it)
166
- // Use 1.0x so particles disappear right at the event horizon
167
- const accretionRadius = this.bhRadius * 1.1;
168
-
169
- for (let i = this.particles.length - 1; i >= 0; i--) {
170
- const p = this.particles[i];
171
-
172
- p.age += dt;
173
-
174
- // Remove old or accreted particles
175
- if (p.age > STREAM_CONFIG.particleLifetime) {
176
- this.particles.splice(i, 1);
177
- continue;
178
- }
179
-
180
- // Skip physics on first frame - let particle appear at spawn point first
181
- // This prevents the "jump" where particles move before being rendered
182
- if (p.age < dt * 1.5) {
183
- continue;
184
- }
185
-
186
- // Distance to BH (at origin)
187
- const dist = Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z);
188
-
189
- // Accreted by black hole?
190
- if (dist < accretionRadius) {
191
- this.particles.splice(i, 1);
192
- // Trigger callback - feeds the black hole's glow!
193
- if (this.onParticleConsumed) {
194
- this.onParticleConsumed();
195
- }
196
- continue;
197
- }
198
-
199
- // Gravity: F = G/r (linear falloff for better visuals)
200
- // Linear falloff keeps gravity significant at larger distances
201
- const gravity = STREAM_CONFIG.gravity / dist;
202
- const dirX = -p.x * 2 / dist;
203
- const dirY = -p.y * 2 / dist;
204
- const dirZ = -p.z * 2 / dist;
205
-
206
- // Apply gravity acceleration
207
- p.vx += dirX * gravity * dt;
208
- p.vy += dirY * gravity * dt;
209
- p.vz += dirZ * gravity * dt;
210
-
211
- // Apply drag - removes angular momentum so particles spiral inward
212
- // Without drag, particles would orbit forever
213
- p.vx *= STREAM_CONFIG.drag;
214
- p.vy *= STREAM_CONFIG.drag;
215
- p.vz *= STREAM_CONFIG.drag;
216
-
217
- // Move particle
218
- p.x += p.vx * dt;
219
- p.y += p.vy * dt;
220
- p.z += p.vz * dt;
221
- }
222
- }
223
-
224
- /**
225
- * Clear all particles
226
- */
227
- clear() {
228
- this.particles = [];
229
- }
230
-
231
- /**
232
- * Update BH radius (for accretion check)
233
- */
234
- updateBHRadius(radius) {
235
- this.bhRadius = radius;
236
- }
237
-
238
- /**
239
- * Render particles
240
- * We reset the canvas transform to identity since Scene3D applies its own
241
- * transforms, and we need absolute screen coordinates for particle rendering.
242
- */
243
- render() {
244
- super.render();
245
-
246
- if (!this.camera || this.particles.length === 0) return;
247
-
248
- // Get the actual canvas transform that Scene3D has set up
249
- // This is the same approach Sphere3D uses to get screen position
250
- const ctx = Painter.ctx;
251
- const transform = ctx.getTransform();
252
-
253
- // TidalStream is at world (0,0,0), so Scene3D translated to:
254
- // scene.x + project(0,0,0).x which is approximately scene.x
255
- // We need to use this as our center, then add particle projections
256
- const cx = transform.e;
257
- const cy = transform.f;
258
-
259
- // Build render list with projection
260
- const renderList = [];
261
-
262
- // Project black hole position once for all particles
263
- // This is crucial when camera has moved (e.g., following the star)
264
- const bhProjected = this.camera.project(0, 0, 0);
265
- const bhScreenX = bhProjected.x;
266
- const bhScreenY = bhProjected.y;
267
-
268
- // Young particles stay invisible (appear to emerge from star)
269
- const fadeInTime = 0.05; // seconds before particles become visible
270
- const fadeInDuration = 0.1; // seconds to fade from invisible to full opacity
271
-
272
- for (const p of this.particles) {
273
- // Skip very young particles - they're "inside" the star
274
- if (p.age < fadeInTime) continue;
275
-
276
- const projected = this.camera.project(p.x, p.y, p.z);
277
-
278
- // Skip if behind camera
279
- if (projected.scale <= 0) continue;
280
-
281
- // Fade in young particles (after fadeInTime threshold)
282
- const fadeInProgress = Math.min(1, (p.age - fadeInTime) / fadeInDuration);
283
-
284
- // Distance from BH for color
285
- const dist = Math.sqrt(p.x * p.x + p.z * p.z);
286
- const colorT = Math.min(1, dist / (p.initialDist || 1));
287
-
288
- // Use particle's emitted color (star color at emission time)
289
- const hotColor = p.emitColor || STREAM_CONFIG.colorHot;
290
-
291
- // Lerp color: cool near BH, hot (star color) near initial position
292
- const color = {
293
- r: STREAM_CONFIG.colorCool.r + (hotColor.r - STREAM_CONFIG.colorCool.r) * colorT,
294
- g: STREAM_CONFIG.colorCool.g + (hotColor.g - STREAM_CONFIG.colorCool.g) * colorT,
295
- b: STREAM_CONFIG.colorCool.b + (hotColor.b - STREAM_CONFIG.colorCool.b) * colorT,
296
- };
297
-
298
- // Fade with age (fade out at end of life) and fade in at birth
299
- const fadeOutAlpha = Math.max(0, 1 - p.age / STREAM_CONFIG.particleLifetime);
300
- const alpha = fadeOutAlpha * fadeInProgress; // Combine fade-in and fade-out
301
-
302
- // Apply gravitational lensing to screen coordinates
303
- // Scale lensing with BH's current (pulsing) radius
304
- let screenX = projected.x;
305
- let screenY = projected.y;
306
-
307
- if (STREAM_CONFIG.lensing.enabled && this.bhRadius > 0) {
308
- // Calculate particle position RELATIVE to black hole's screen position
309
- const relX = screenX - bhScreenX;
310
- const relY = screenY - bhScreenY;
311
-
312
- const effectRadius = this.bhRadius * STREAM_CONFIG.lensing.effectRadiusMult;
313
- const strength = this.bhRadius * STREAM_CONFIG.lensing.strengthMult;
314
- const minDist = this.bhRadius * STREAM_CONFIG.lensing.minDistanceMult;
315
-
316
- // Apply lensing in BH-relative space (lensing curves toward origin)
317
- const lensed = applyGravitationalLensing(
318
- relX, relY,
319
- effectRadius,
320
- strength,
321
- STREAM_CONFIG.lensing.falloff,
322
- minDist
323
- );
324
-
325
- // Transform back to screen space
326
- screenX = lensed.x + bhScreenX;
327
- screenY = lensed.y + bhScreenY;
328
- }
329
-
330
- // Check if particle is visually inside the black hole
331
- // Use pre-calculated BH screen position
332
- const dxBH = screenX - bhScreenX;
333
- const dyBH = screenY - bhScreenY;
334
- const screenDistFromBH = Math.sqrt(dxBH * dxBH + dyBH * dyBH);
335
- const insideBH = screenDistFromBH < this.bhRadius;
336
-
337
- // Screen position = center + lensed offset
338
- renderList.push({
339
- x: cx + screenX,
340
- y: cy + screenY,
341
- z: projected.z,
342
- size: p.size * projected.scale,
343
- color: insideBH ? { r: 0, g: 0, b: 0 } : color,
344
- alpha,
345
- });
346
- }
347
-
348
- // Sort back to front
349
- renderList.sort((a, b) => b.z - a.z);
350
-
351
- // Draw particles with reset transform (absolute screen coords)
352
- Painter.useCtx((ctx) => {
353
- // Reset to identity matrix - Scene3D has applied transforms we need to bypass
354
- ctx.setTransform(1, 0, 0, 1, 0, 0);
355
-
356
- ctx.globalCompositeOperation = "lighter";
357
-
358
- for (const item of renderList) {
359
- const r = Math.round(item.color.r);
360
- const g = Math.round(item.color.g);
361
- const b = Math.round(item.color.b);
362
-
363
- ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${item.alpha})`;
364
- ctx.beginPath();
365
- ctx.arc(item.x, item.y, item.size, 0, Math.PI * 2);
366
- ctx.fill();
367
- }
368
-
369
- ctx.globalCompositeOperation = "source-over";
370
- });
371
- }
372
- }