@guinetik/gcanvas 1.0.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/dist/CNAME +1 -0
  2. package/dist/aizawa.html +27 -0
  3. package/dist/animations.html +31 -0
  4. package/dist/basic.html +38 -0
  5. package/dist/baskara.html +31 -0
  6. package/dist/bezier.html +35 -0
  7. package/dist/beziersignature.html +29 -0
  8. package/dist/blackhole.html +28 -0
  9. package/dist/blob.html +35 -0
  10. package/dist/clifford.html +25 -0
  11. package/dist/cmb.html +24 -0
  12. package/dist/coordinates.html +698 -0
  13. package/dist/cube3d.html +23 -0
  14. package/dist/dadras.html +26 -0
  15. package/dist/dejong.html +25 -0
  16. package/dist/demos.css +303 -0
  17. package/dist/dino.html +42 -0
  18. package/dist/easing.html +28 -0
  19. package/dist/events.html +195 -0
  20. package/dist/fluent.html +647 -0
  21. package/dist/fluid-simple.html +22 -0
  22. package/dist/fluid.html +37 -0
  23. package/dist/fractals.html +36 -0
  24. package/dist/gameobjects.html +626 -0
  25. package/dist/gcanvas.es.js +14368 -9093
  26. package/dist/gcanvas.es.min.js +1 -1
  27. package/dist/gcanvas.umd.js +1 -1
  28. package/dist/gcanvas.umd.min.js +1 -1
  29. package/dist/genart.html +26 -0
  30. package/dist/gendream.html +26 -0
  31. package/dist/group.html +36 -0
  32. package/dist/halvorsen.html +27 -0
  33. package/dist/home.html +587 -0
  34. package/dist/hyperbolic001.html +23 -0
  35. package/dist/hyperbolic002.html +23 -0
  36. package/dist/hyperbolic003.html +23 -0
  37. package/dist/hyperbolic004.html +23 -0
  38. package/dist/hyperbolic005.html +22 -0
  39. package/dist/index.html +446 -0
  40. package/dist/isometric.html +34 -0
  41. package/dist/js/aizawa.js +425 -0
  42. package/dist/js/animations.js +452 -0
  43. package/dist/js/basic.js +204 -0
  44. package/dist/js/baskara.js +751 -0
  45. package/dist/js/bezier.js +692 -0
  46. package/dist/js/beziersignature.js +241 -0
  47. package/dist/js/blackhole/accretiondisk.obj.js +379 -0
  48. package/dist/js/blackhole/blackhole.obj.js +318 -0
  49. package/dist/js/blackhole/index.js +409 -0
  50. package/dist/js/blackhole/particle.js +56 -0
  51. package/dist/js/blackhole/starfield.obj.js +218 -0
  52. package/dist/js/blob.js +2276 -0
  53. package/dist/js/clifford.js +236 -0
  54. package/dist/js/cmb.js +594 -0
  55. package/dist/js/coordinates.js +840 -0
  56. package/dist/js/cube3d.js +789 -0
  57. package/dist/js/dadras.js +405 -0
  58. package/dist/js/dejong.js +257 -0
  59. package/dist/js/dino.js +1420 -0
  60. package/dist/js/easing.js +477 -0
  61. package/dist/js/fluent.js +183 -0
  62. package/dist/js/fluid-simple.js +253 -0
  63. package/dist/js/fluid.js +527 -0
  64. package/dist/js/fractals.js +932 -0
  65. package/dist/js/fractalworker.js +93 -0
  66. package/dist/js/gameobjects.js +176 -0
  67. package/dist/js/genart.js +268 -0
  68. package/dist/js/gendream.js +209 -0
  69. package/dist/js/group.js +140 -0
  70. package/dist/js/halvorsen.js +405 -0
  71. package/dist/js/hyperbolic001.js +310 -0
  72. package/dist/js/hyperbolic002.js +388 -0
  73. package/dist/js/hyperbolic003.js +319 -0
  74. package/dist/js/hyperbolic004.js +345 -0
  75. package/dist/js/hyperbolic005.js +340 -0
  76. package/dist/js/info-toggle.js +25 -0
  77. package/dist/js/isometric.js +851 -0
  78. package/dist/js/kerr.js +1547 -0
  79. package/dist/js/lavalamp.js +590 -0
  80. package/dist/js/layout.js +354 -0
  81. package/dist/js/lorenz.js +425 -0
  82. package/dist/js/mondrian.js +285 -0
  83. package/dist/js/opacity.js +275 -0
  84. package/dist/js/painter.js +484 -0
  85. package/dist/js/particles-showcase.js +514 -0
  86. package/dist/js/particles.js +299 -0
  87. package/dist/js/patterns.js +397 -0
  88. package/dist/js/penrose/artifact.js +69 -0
  89. package/dist/js/penrose/blackhole.js +121 -0
  90. package/dist/js/penrose/constants.js +73 -0
  91. package/dist/js/penrose/game.js +943 -0
  92. package/dist/js/penrose/lore.js +278 -0
  93. package/dist/js/penrose/penrosescene.js +892 -0
  94. package/dist/js/penrose/ship.js +216 -0
  95. package/dist/js/penrose/sounds.js +211 -0
  96. package/dist/js/penrose/voidparticle.js +55 -0
  97. package/dist/js/penrose/voidscene.js +258 -0
  98. package/dist/js/penrose/voidship.js +144 -0
  99. package/dist/js/penrose/wormhole.js +46 -0
  100. package/dist/js/pipeline.js +555 -0
  101. package/dist/js/plane3d.js +256 -0
  102. package/dist/js/platformer.js +1579 -0
  103. package/dist/js/rossler.js +480 -0
  104. package/dist/js/scene.js +304 -0
  105. package/dist/js/scenes.js +320 -0
  106. package/dist/js/schrodinger.js +706 -0
  107. package/dist/js/schwarzschild.js +1015 -0
  108. package/dist/js/shapes.js +628 -0
  109. package/dist/js/space/alien.js +171 -0
  110. package/dist/js/space/boom.js +98 -0
  111. package/dist/js/space/boss.js +353 -0
  112. package/dist/js/space/buff.js +73 -0
  113. package/dist/js/space/bullet.js +102 -0
  114. package/dist/js/space/constants.js +85 -0
  115. package/dist/js/space/game.js +1884 -0
  116. package/dist/js/space/hud.js +112 -0
  117. package/dist/js/space/laserbeam.js +179 -0
  118. package/dist/js/space/lightning.js +277 -0
  119. package/dist/js/space/minion.js +192 -0
  120. package/dist/js/space/missile.js +212 -0
  121. package/dist/js/space/player.js +430 -0
  122. package/dist/js/space/powerup.js +90 -0
  123. package/dist/js/space/starfield.js +58 -0
  124. package/dist/js/space/starpower.js +90 -0
  125. package/dist/js/spacetime.js +559 -0
  126. package/dist/js/sphere3d.js +229 -0
  127. package/dist/js/sprite.js +473 -0
  128. package/dist/js/starfaux/config.js +118 -0
  129. package/dist/js/starfaux/enemy.js +353 -0
  130. package/dist/js/starfaux/hud.js +78 -0
  131. package/dist/js/starfaux/index.js +482 -0
  132. package/dist/js/starfaux/laser.js +182 -0
  133. package/dist/js/starfaux/player.js +468 -0
  134. package/dist/js/starfaux/terrain.js +560 -0
  135. package/dist/js/study001.js +275 -0
  136. package/dist/js/study002.js +366 -0
  137. package/dist/js/study003.js +331 -0
  138. package/dist/js/study004.js +389 -0
  139. package/dist/js/study005.js +209 -0
  140. package/dist/js/study006.js +194 -0
  141. package/dist/js/study007.js +192 -0
  142. package/dist/js/study008.js +413 -0
  143. package/dist/js/svgtween.js +204 -0
  144. package/dist/js/tde/accretiondisk.js +471 -0
  145. package/dist/js/tde/blackhole.js +219 -0
  146. package/dist/js/tde/blackholescene.js +209 -0
  147. package/dist/js/tde/config.js +59 -0
  148. package/dist/js/tde/index.js +820 -0
  149. package/dist/js/tde/jets.js +290 -0
  150. package/dist/js/tde/lensedstarfield.js +154 -0
  151. package/dist/js/tde/tdestar.js +297 -0
  152. package/dist/js/tde/tidalstream.js +372 -0
  153. package/dist/js/tde_old/blackhole.obj.js +354 -0
  154. package/dist/js/tde_old/debris.obj.js +791 -0
  155. package/dist/js/tde_old/flare.obj.js +239 -0
  156. package/dist/js/tde_old/index.js +448 -0
  157. package/dist/js/tde_old/star.obj.js +812 -0
  158. package/dist/js/tetris/config.js +157 -0
  159. package/dist/js/tetris/grid.js +286 -0
  160. package/dist/js/tetris/index.js +1195 -0
  161. package/dist/js/tetris/renderer.js +634 -0
  162. package/dist/js/tetris/tetrominos.js +280 -0
  163. package/dist/js/thomas.js +394 -0
  164. package/dist/js/tiles.js +312 -0
  165. package/dist/js/tweendemo.js +79 -0
  166. package/dist/js/visibility.js +102 -0
  167. package/dist/kerr.html +28 -0
  168. package/dist/lavalamp.html +27 -0
  169. package/dist/layouts.html +37 -0
  170. package/dist/logo.svg +4 -0
  171. package/dist/loop.html +84 -0
  172. package/dist/lorenz.html +27 -0
  173. package/dist/mondrian.html +32 -0
  174. package/dist/og_image.png +0 -0
  175. package/dist/opacity.html +36 -0
  176. package/dist/painter.html +39 -0
  177. package/dist/particles-showcase.html +28 -0
  178. package/dist/particles.html +24 -0
  179. package/dist/patterns.html +33 -0
  180. package/dist/penrose-game.html +31 -0
  181. package/dist/pipeline.html +737 -0
  182. package/dist/plane3d.html +24 -0
  183. package/dist/platformer.html +43 -0
  184. package/dist/rossler.html +27 -0
  185. package/dist/scene-interactivity-test.html +220 -0
  186. package/dist/scene.html +33 -0
  187. package/dist/scenes.html +96 -0
  188. package/dist/schrodinger.html +27 -0
  189. package/dist/schwarzschild.html +27 -0
  190. package/dist/shapes.html +16 -0
  191. package/dist/space.html +85 -0
  192. package/dist/spacetime.html +27 -0
  193. package/dist/sphere3d.html +24 -0
  194. package/dist/sprite.html +18 -0
  195. package/dist/starfaux.html +22 -0
  196. package/dist/study001.html +23 -0
  197. package/dist/study002.html +23 -0
  198. package/dist/study003.html +23 -0
  199. package/dist/study004.html +23 -0
  200. package/dist/study005.html +22 -0
  201. package/dist/study006.html +24 -0
  202. package/dist/study007.html +24 -0
  203. package/dist/study008.html +22 -0
  204. package/dist/svgtween.html +29 -0
  205. package/dist/tde.html +28 -0
  206. package/dist/tetris3d.html +25 -0
  207. package/dist/thomas.html +27 -0
  208. package/dist/tiles.html +28 -0
  209. package/dist/transforms.html +400 -0
  210. package/dist/tween.html +45 -0
  211. package/dist/visibility.html +33 -0
  212. package/package.json +1 -1
  213. package/readme.md +30 -22
  214. package/src/game/objects/go.js +7 -0
  215. package/src/game/objects/index.js +2 -0
  216. package/src/game/objects/isometric-scene.js +53 -3
  217. package/src/game/objects/layoutscene.js +57 -0
  218. package/src/game/objects/mask.js +241 -0
  219. package/src/game/objects/scene.js +19 -0
  220. package/src/game/objects/wrapper.js +14 -2
  221. package/src/game/pipeline.js +17 -0
  222. package/src/game/ui/button.js +101 -16
  223. package/src/game/ui/theme.js +0 -6
  224. package/src/game/ui/togglebutton.js +25 -14
  225. package/src/game/ui/tooltip.js +12 -4
  226. package/src/index.js +3 -0
  227. package/src/io/gesture.js +409 -0
  228. package/src/io/index.js +4 -1
  229. package/src/io/keys.js +9 -1
  230. package/src/io/screen.js +476 -0
  231. package/src/math/attractors.js +664 -0
  232. package/src/math/heat.js +106 -0
  233. package/src/math/index.js +1 -0
  234. package/src/mixins/draggable.js +15 -19
  235. package/src/painter/painter.shapes.js +11 -5
  236. package/src/particle/particle-system.js +165 -1
  237. package/src/physics/index.js +26 -0
  238. package/src/physics/physics-updaters.js +333 -0
  239. package/src/physics/physics.js +375 -0
  240. package/src/shapes/image.js +5 -5
  241. package/src/shapes/index.js +2 -0
  242. package/src/shapes/parallelogram.js +147 -0
  243. package/src/shapes/righttriangle.js +115 -0
  244. package/src/shapes/svg.js +281 -100
  245. package/src/shapes/text.js +22 -6
  246. package/src/shapes/transformable.js +5 -0
  247. package/src/sound/effects.js +807 -0
  248. package/src/sound/index.js +13 -0
  249. package/src/webgl/index.js +7 -0
  250. package/src/webgl/shaders/clifford-point-shaders.js +131 -0
  251. package/src/webgl/shaders/dejong-point-shaders.js +131 -0
  252. package/src/webgl/shaders/point-sprite-shaders.js +152 -0
  253. package/src/webgl/webgl-clifford-renderer.js +477 -0
  254. package/src/webgl/webgl-dejong-renderer.js +472 -0
  255. package/src/webgl/webgl-line-renderer.js +391 -0
  256. package/src/webgl/webgl-particle-renderer.js +410 -0
  257. package/types/index.d.ts +30 -2
  258. package/types/io.d.ts +217 -0
  259. package/types/physics.d.ts +299 -0
  260. package/types/shapes.d.ts +8 -0
  261. package/types/webgl.d.ts +188 -109
@@ -0,0 +1,297 @@
1
+ import { GameObject, Sphere3D } from "/gcanvas.es.min.js";
2
+ import { polarToCartesian } from "/gcanvas.es.min.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
+ }
@@ -0,0 +1,372 @@
1
+ import { GameObject, Painter } from "/gcanvas.es.min.js";
2
+ import { applyGravitationalLensing } from "/gcanvas.es.min.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
+ }