@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,318 @@
1
+ /**
2
+ * BlackHole - Event horizon, photon ring, and Hawking radiation
3
+ *
4
+ * Renders the black hole core with gravitational lensing effects,
5
+ * photon ring glow, and Hawking radiation particles.
6
+ */
7
+ import { GameObject, Easing } from "/gcanvas.es.min.js";
8
+
9
+ // Hawking radiation configuration
10
+ const HAWKING_SPAWN_RATE = 0.15;
11
+ const HAWKING_SPEED = 0.25;
12
+ const HAWKING_LIFETIME = 3.5;
13
+
14
+ export class BlackHole extends GameObject {
15
+ /**
16
+ * @param {Game} game - Game instance
17
+ * @param {Object} options
18
+ * @param {Camera3D} options.camera - Camera for projection
19
+ * @param {StateMachine} options.formationFSM - Formation state machine
20
+ * @param {number} options.baseScale - Base scale for sizing
21
+ * @param {number} options.bhRadius - Black hole radius
22
+ * @param {number} options.diskOuter - Outer disk radius (for Hawking bounds)
23
+ */
24
+ constructor(game, options = {}) {
25
+ super(game, options);
26
+
27
+ this.camera = options.camera;
28
+ this.formationFSM = options.formationFSM;
29
+
30
+ // Sizing
31
+ this.baseScale = options.baseScale ?? 500;
32
+ this.bhRadius = options.bhRadius ?? 40;
33
+ this.diskOuter = options.diskOuter ?? 175;
34
+
35
+ // Hawking radiation
36
+ this.hawkingParticles = [];
37
+ this.hawkingSpawnTimer = 0;
38
+ }
39
+
40
+ /**
41
+ * Update sizing when window resizes.
42
+ */
43
+ updateSizing(baseScale, bhRadius, diskOuter) {
44
+ this.baseScale = baseScale;
45
+ this.bhRadius = bhRadius;
46
+ this.diskOuter = diskOuter;
47
+ }
48
+
49
+ /**
50
+ * Reset Hawking radiation state.
51
+ */
52
+ reset() {
53
+ this.hawkingParticles = [];
54
+ this.hawkingSpawnTimer = 0;
55
+ }
56
+
57
+ /**
58
+ * Calculate formation progress (0-1) based on consumed particles.
59
+ */
60
+ getFormationLambda(particlesConsumed, totalParticleMass) {
61
+ const state = this.formationFSM.state;
62
+ const progress = this.formationFSM.progress;
63
+
64
+ if (state === "infall") {
65
+ if (!particlesConsumed || particlesConsumed === 0) return 0;
66
+ const maxConsumed = totalParticleMass * 0.4;
67
+ const consumedRatio = Math.min(1, particlesConsumed / maxConsumed);
68
+ return consumedRatio * 0.5;
69
+ } else if (state === "collapse") {
70
+ const delayedProgress = Math.max(0, (progress - 0.2) / 0.8);
71
+ return 0.5 + Easing.easeOutQuad(delayedProgress) * 0.3;
72
+ } else if (state === "circularize") {
73
+ return 0.8 + progress * 0.2;
74
+ }
75
+ return 1; // stable
76
+ }
77
+
78
+ /**
79
+ * Spawn a new Hawking radiation particle.
80
+ */
81
+ spawnHawkingParticle() {
82
+ const angle = Math.random() * Math.PI * 2;
83
+ const startRadius = this.bhRadius * 1.05;
84
+
85
+ // ~10% chance to be an "escaper"
86
+ const isEscaper = Math.random() < 0.1;
87
+
88
+ this.hawkingParticles.push({
89
+ angle,
90
+ radius: startRadius,
91
+ speed: isEscaper
92
+ ? HAWKING_SPEED * (1.5 + Math.random() * 1.0)
93
+ : HAWKING_SPEED * (0.6 + Math.random() * 0.8),
94
+ size: isEscaper ? 3 + Math.random() * 2 : 2 + Math.random() * 2,
95
+ brightness: isEscaper ? 1.0 : 0.8 + Math.random() * 0.2,
96
+ age: 0,
97
+ wobblePhase: Math.random() * Math.PI * 2,
98
+ wobbleSpeed: 2 + Math.random() * 3,
99
+ isEscaper,
100
+ maxRadius: isEscaper ? this.baseScale * 0.8 : this.diskOuter,
101
+ });
102
+ }
103
+
104
+ update(dt) {
105
+ super.update(dt);
106
+ this.updateHawkingRadiation(dt);
107
+ }
108
+
109
+ /**
110
+ * Update Hawking radiation particles.
111
+ */
112
+ updateHawkingRadiation(dt) {
113
+ // Only spawn when stable
114
+ if (this.formationFSM.is("stable")) {
115
+ this.hawkingSpawnTimer += dt;
116
+ if (this.hawkingSpawnTimer > 1 / HAWKING_SPAWN_RATE) {
117
+ this.spawnHawkingParticle();
118
+ this.hawkingSpawnTimer = 0;
119
+ }
120
+ }
121
+
122
+ // Update existing particles
123
+ for (let i = this.hawkingParticles.length - 1; i >= 0; i--) {
124
+ const p = this.hawkingParticles[i];
125
+ p.age += dt;
126
+ p.radius += p.speed * this.bhRadius * dt;
127
+ const wobble = Math.sin(p.age * p.wobbleSpeed + p.wobblePhase) * 0.02;
128
+ p.angle += (0.05 + wobble) * dt;
129
+
130
+ const maxLife = p.isEscaper ? HAWKING_LIFETIME * 2.5 : HAWKING_LIFETIME;
131
+ if (p.age > maxLife || p.radius > p.maxRadius) {
132
+ this.hawkingParticles.splice(i, 1);
133
+ }
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Build render list with black hole and Hawking particles.
139
+ */
140
+ buildRenderList(lambda) {
141
+ const renderList = [];
142
+
143
+ // Black hole (only render after formation starts)
144
+ if (lambda > 0.05) {
145
+ const holeProj = this.camera.project(0, 0, 0);
146
+ renderList.push({
147
+ type: "hole",
148
+ z: holeProj.z,
149
+ x: holeProj.x,
150
+ y: holeProj.y,
151
+ scale: holeProj.scale,
152
+ lambda: lambda,
153
+ });
154
+ }
155
+
156
+ // Hawking particles
157
+ for (const p of this.hawkingParticles) {
158
+ const x = Math.cos(p.angle) * p.radius;
159
+ const z = Math.sin(p.angle) * p.radius;
160
+ const y = 0;
161
+
162
+ const cosY = Math.cos(this.camera.rotationY);
163
+ const sinY = Math.sin(this.camera.rotationY);
164
+ let xCam = x * cosY - z * sinY;
165
+ let zCam = x * sinY + z * cosY;
166
+
167
+ const cosX = Math.cos(this.camera.rotationX);
168
+ const sinX = Math.sin(this.camera.rotationX);
169
+ let yCam = y * cosX - zCam * sinX;
170
+ zCam = y * sinX + zCam * cosX;
171
+
172
+ const perspectiveScale =
173
+ this.camera.perspective / (this.camera.perspective + zCam);
174
+ const screenX = xCam * perspectiveScale;
175
+ const screenY = yCam * perspectiveScale;
176
+
177
+ if (zCam < -this.camera.perspective + 10) continue;
178
+
179
+ const maxLife = p.isEscaper ? HAWKING_LIFETIME * 2.5 : HAWKING_LIFETIME;
180
+ const ageRatio = p.age / maxLife;
181
+ const fadeIn = Math.min(1, p.age * 4);
182
+ const fadeOut = 1 - Math.pow(ageRatio, 2);
183
+ const brightness = p.brightness * fadeIn * fadeOut;
184
+
185
+ renderList.push({
186
+ type: "hawking",
187
+ z: zCam,
188
+ x: screenX,
189
+ y: screenY,
190
+ scale: perspectiveScale,
191
+ size: p.size,
192
+ brightness: brightness,
193
+ age: p.age,
194
+ });
195
+ }
196
+
197
+ return renderList;
198
+ }
199
+
200
+ /**
201
+ * Draw the event horizon and photon ring.
202
+ * Static method for use in main render loop.
203
+ */
204
+ static drawHole(ctx, item, bhRadius) {
205
+ const r = bhRadius * item.scale * item.lambda;
206
+
207
+ // Photon ring glow (additive)
208
+ ctx.globalCompositeOperation = "screen";
209
+ const glowIntensity = item.lambda;
210
+ const gradient = ctx.createRadialGradient(
211
+ item.x,
212
+ item.y,
213
+ r * 0.8,
214
+ item.x,
215
+ item.y,
216
+ r * 1.5,
217
+ );
218
+ gradient.addColorStop(0, `rgba(255, 200, 100, ${glowIntensity})`);
219
+ gradient.addColorStop(0.2, `rgba(255, 150, 50, ${0.6 * glowIntensity})`);
220
+ gradient.addColorStop(1, "rgba(255, 50, 0, 0)");
221
+
222
+ ctx.fillStyle = gradient;
223
+ ctx.beginPath();
224
+ ctx.arc(item.x, item.y, r * 1.5, 0, Math.PI * 2);
225
+ ctx.fill();
226
+ ctx.globalCompositeOperation = "source-over";
227
+
228
+ // Event horizon (void)
229
+ ctx.beginPath();
230
+ ctx.arc(item.x, item.y, r, 0, Math.PI * 2);
231
+ ctx.fillStyle = "#000";
232
+ ctx.fill();
233
+ }
234
+
235
+ /**
236
+ * Draw a Hawking radiation particle.
237
+ * Static method for use in main render loop.
238
+ */
239
+ static drawHawkingParticle(ctx, item, baseScale) {
240
+ const size = item.size * item.scale * (baseScale * 0.001);
241
+ if (size < 0.1) return;
242
+
243
+ const pulseIntensity = 0.8 + 0.2 * Math.sin(item.age * 8);
244
+
245
+ // Outer glow
246
+ ctx.globalCompositeOperation = "screen";
247
+ ctx.shadowColor = "rgba(0, 255, 200, 0.9)";
248
+ ctx.shadowBlur = 25 * item.brightness;
249
+
250
+ // Core - cyan-green
251
+ ctx.fillStyle = `rgba(100, 255, 220, ${item.brightness * 0.9 * pulseIntensity})`;
252
+ ctx.beginPath();
253
+ ctx.arc(item.x, item.y, size, 0, Math.PI * 2);
254
+ ctx.fill();
255
+
256
+ // Inner bright core
257
+ ctx.fillStyle = `rgba(200, 255, 250, ${item.brightness * 0.7})`;
258
+ ctx.beginPath();
259
+ ctx.arc(item.x, item.y, size * 0.4, 0, Math.PI * 2);
260
+ ctx.fill();
261
+
262
+ ctx.shadowBlur = 0;
263
+ ctx.globalCompositeOperation = "source-over";
264
+ }
265
+
266
+ /**
267
+ * Draw the formation collapse flash.
268
+ */
269
+ drawFormationFlash(ctx, cx, cy, lambda, time) {
270
+ const intensity = 1 - lambda / 0.3;
271
+
272
+ // Collapse flash - bright white center
273
+ const size = this.bhRadius * (1 - lambda) * 2;
274
+ const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, size);
275
+ gradient.addColorStop(0, `rgba(255, 255, 255, ${0.9 * intensity})`);
276
+ gradient.addColorStop(0.3, `rgba(255, 220, 180, ${0.6 * intensity})`);
277
+ gradient.addColorStop(0.6, `rgba(255, 150, 100, ${0.3 * intensity})`);
278
+ gradient.addColorStop(1, "transparent");
279
+
280
+ ctx.fillStyle = gradient;
281
+ ctx.beginPath();
282
+ ctx.arc(cx, cy, size, 0, Math.PI * 2);
283
+ ctx.fill();
284
+
285
+ // Infalling streaks
286
+ if (lambda > 0.1) {
287
+ const streakAlpha = intensity * 0.6;
288
+ for (let i = 0; i < 8; i++) {
289
+ const angle = (i / 8) * Math.PI * 2 + time * 0.5;
290
+ const startR = this.bhRadius * 2;
291
+ const endR = this.bhRadius * lambda * 0.5;
292
+
293
+ const streakGradient = ctx.createLinearGradient(
294
+ cx + Math.cos(angle) * startR,
295
+ cy + Math.sin(angle) * startR,
296
+ cx + Math.cos(angle) * endR,
297
+ cy + Math.sin(angle) * endR,
298
+ );
299
+ streakGradient.addColorStop(0, "transparent");
300
+ streakGradient.addColorStop(
301
+ 0.5,
302
+ `rgba(255, 200, 150, ${streakAlpha * 0.5})`,
303
+ );
304
+ streakGradient.addColorStop(1, `rgba(255, 255, 200, ${streakAlpha})`);
305
+
306
+ ctx.strokeStyle = streakGradient;
307
+ ctx.lineWidth = 2;
308
+ ctx.beginPath();
309
+ ctx.moveTo(
310
+ cx + Math.cos(angle) * startR,
311
+ cy + Math.sin(angle) * startR,
312
+ );
313
+ ctx.lineTo(cx + Math.cos(angle) * endR, cy + Math.sin(angle) * endR);
314
+ ctx.stroke();
315
+ }
316
+ }
317
+ }
318
+ }