@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,290 +0,0 @@
1
- import { GameObject, Painter, Tweenetik, Easing } from "../../../src/index.js";
2
-
3
- /**
4
- * RelativisticJets - Bipolar jets shooting from black hole poles
5
- *
6
- * Physics:
7
- * - Particles ejected along ±Y axis (perpendicular to disk)
8
- * - Conical spread gives characteristic jet shape
9
- * - Velocity decreases with distance (deceleration)
10
- * - Bright blue-white core fading to orange at edges
11
- */
12
-
13
- const JET_CONFIG = {
14
- // Particle properties
15
- maxParticles: 6000,
16
- particleLifetime: 8.0, // Shorter lifetime - continuous turnover
17
- spawnRatePerSecond: 800, // Particles per SECOND (not per frame)
18
-
19
- // Jet geometry
20
- coneAngle: 0.06, // Tight cone for focused beams
21
- initialSpeed: 1200, // Fast ejection - relativistic!
22
- speedVariation: 300, // Random variation in speed
23
-
24
- // Jet length - shoots off screen
25
- maxLength: 50000, // Way off screen
26
-
27
- // Visual - bright particles
28
- colorCore: { r: 220, g: 240, b: 255 }, // Blue-white core
29
- colorEdge: { r: 255, g: 160, b: 80 }, // Orange edge
30
- sizeMin: 0.8,
31
- sizeMax: 1.0,
32
-
33
- // Animation
34
- activationDuration: 0.3, // Quick ignition
35
- deactivationDuration: 5.0, // Graceful fade
36
- };
37
-
38
- export class RelativisticJets extends GameObject {
39
- constructor(game, options = {}) {
40
- super(game, options);
41
-
42
- this.camera = options.camera;
43
- this.bhRadius = options.bhRadius ?? 50;
44
-
45
- // State
46
- this.active = false;
47
- this.intensity = 0; // 0-1, controls spawn rate and brightness
48
- this.isDeactivating = false; // Prevent multiple deactivate() calls
49
-
50
- // Particle arrays - one for each jet (up and down)
51
- this.particles = [];
52
- }
53
-
54
- init() {
55
- this.particles = [];
56
- }
57
-
58
- /**
59
- * Activate jets with intensity ramp-up
60
- * Uses easeOutExpo for explosive ignition feel
61
- */
62
- activate() {
63
- if (this.active) return;
64
- this.active = true;
65
- this.intensity = 0;
66
- // Explosive start, then sustains - like jets igniting
67
- Tweenetik.to(this, { intensity: 1 }, JET_CONFIG.activationDuration, Easing.easeOutExpo);
68
- }
69
-
70
- /**
71
- * Deactivate jets with fade-out
72
- */
73
- deactivate() {
74
- if (!this.active || this.isDeactivating) return;
75
- this.isDeactivating = true;
76
- // Slow graceful fade
77
- Tweenetik.to(this, { intensity: 0 }, JET_CONFIG.deactivationDuration, Easing.easeInQuad, {
78
- onComplete: () => {
79
- this.active = false;
80
- this.isDeactivating = false;
81
- }
82
- });
83
- }
84
-
85
- /**
86
- * Pulse the jets - boost intensity for sustained firing
87
- */
88
- pulse() {
89
- if (!this.active) return;
90
- this.intensity = 1;
91
- Tweenetik.to(this, { intensity: 0.6 }, 2.0, Easing.easeOutQuad);
92
- }
93
-
94
- /**
95
- * Spawn jet particles from both poles
96
- * @param {number} dt - Delta time for frame-rate independent spawning
97
- */
98
- spawnParticles(dt) {
99
- if (this.particles.length >= JET_CONFIG.maxParticles) return;
100
-
101
- // Frame-rate independent spawning
102
- const spawnCount = Math.floor(JET_CONFIG.spawnRatePerSecond * dt * this.intensity);
103
-
104
- for (let i = 0; i < spawnCount; i++) {
105
- // Spawn from both poles (up and down)
106
- const direction = Math.random() < 0.5 ? 1 : -1;
107
-
108
- // Conical spread - random angle within cone
109
- const spreadAngle = Math.random() * JET_CONFIG.coneAngle;
110
- const azimuth = Math.random() * Math.PI * 2;
111
-
112
- // Convert to velocity components
113
- const speed = JET_CONFIG.initialSpeed +
114
- (Math.random() - 0.5) * JET_CONFIG.speedVariation;
115
-
116
- // Y is the main jet direction, x/z give the spread
117
- const vy = direction * speed * Math.cos(spreadAngle);
118
- const spreadMag = speed * Math.sin(spreadAngle);
119
- const vx = spreadMag * Math.cos(azimuth);
120
- const vz = spreadMag * Math.sin(azimuth);
121
-
122
- // Start position - slightly offset from BH center along jet axis
123
- const startOffset = this.bhRadius * 1.034;
124
-
125
- this.particles.push({
126
- x: vx * 0.01, // Tiny initial spread
127
- y: direction * startOffset,
128
- z: vz * 0.01,
129
- vx,
130
- vy,
131
- vz,
132
- age: 0,
133
- direction, // Track which jet (for color)
134
- size: JET_CONFIG.sizeMin + Math.random() * (JET_CONFIG.sizeMax - JET_CONFIG.sizeMin),
135
- // Core particles (small spread) are brighter
136
- isCore: spreadAngle < JET_CONFIG.coneAngle * 0.3,
137
- });
138
- }
139
- }
140
-
141
- update(dt) {
142
- super.update(dt);
143
-
144
- if (!this.active) return;
145
-
146
- // Spawn new particles (dt-based for consistent rate)
147
- this.spawnParticles(dt);
148
-
149
- // Update existing particles
150
- const maxDist = this.bhRadius * JET_CONFIG.maxLength;
151
-
152
- for (let i = this.particles.length - 1; i >= 0; i--) {
153
- const p = this.particles[i];
154
- p.age += dt;
155
-
156
- // Remove old particles
157
- if (p.age > JET_CONFIG.particleLifetime) {
158
- this.particles.splice(i, 1);
159
- continue;
160
- }
161
-
162
- // Remove particles that traveled too far
163
- const dist = Math.abs(p.y);
164
- if (dist > maxDist) {
165
- this.particles.splice(i, 1);
166
- continue;
167
- }
168
-
169
- // No deceleration - relativistic jets maintain speed
170
- // Particles stream continuously at near-constant velocity
171
-
172
- // Move particle
173
- p.x += p.vx * dt;
174
- p.y += p.vy * dt;
175
- p.z += p.vz * dt;
176
- }
177
- }
178
-
179
- /**
180
- * Build render list with camera projection
181
- */
182
- buildRenderList() {
183
- const renderList = [];
184
- if (!this.camera || this.particles.length === 0) return renderList;
185
-
186
- for (const p of this.particles) {
187
- // Transform to camera space
188
- const cosY = Math.cos(this.camera.rotationY);
189
- const sinY = Math.sin(this.camera.rotationY);
190
- let xCam = p.x * cosY - p.z * sinY;
191
- let zCam = p.x * sinY + p.z * cosY;
192
-
193
- const cosX = Math.cos(this.camera.rotationX);
194
- const sinX = Math.sin(this.camera.rotationX);
195
- let yCam = p.y * cosX - zCam * sinX;
196
- zCam = p.y * sinX + zCam * cosX;
197
-
198
- // Perspective projection
199
- const perspectiveScale = this.camera.perspective / (this.camera.perspective + zCam);
200
- const screenX = xCam * perspectiveScale;
201
- const screenY = yCam * perspectiveScale;
202
-
203
- // Skip particles behind camera
204
- if (zCam < -this.camera.perspective + 10) continue;
205
-
206
- // Color: core is blue-white, edge is orange
207
- // Also fade with distance from BH
208
- const distFactor = Math.min(1, Math.abs(p.y) / (this.bhRadius * JET_CONFIG.maxLength * 0.5));
209
- const ageFactor = 1 - (p.age / JET_CONFIG.particleLifetime);
210
-
211
- let color;
212
- if (p.isCore) {
213
- // Core: bright blue-white
214
- color = {
215
- r: JET_CONFIG.colorCore.r,
216
- g: JET_CONFIG.colorCore.g,
217
- b: JET_CONFIG.colorCore.b,
218
- };
219
- } else {
220
- // Edge: lerp toward orange with distance
221
- color = {
222
- r: JET_CONFIG.colorCore.r + (JET_CONFIG.colorEdge.r - JET_CONFIG.colorCore.r) * distFactor,
223
- g: JET_CONFIG.colorCore.g + (JET_CONFIG.colorEdge.g - JET_CONFIG.colorCore.g) * distFactor,
224
- b: JET_CONFIG.colorCore.b + (JET_CONFIG.colorEdge.b - JET_CONFIG.colorCore.b) * distFactor,
225
- };
226
- }
227
-
228
- // Alpha based on age, intensity, and distance
229
- const alpha = ageFactor * this.intensity * (1 - distFactor * 0.5);
230
-
231
- renderList.push({
232
- x: screenX,
233
- y: screenY,
234
- z: zCam,
235
- scale: perspectiveScale,
236
- color,
237
- alpha,
238
- size: p.size * (p.isCore ? 1.5 : 1),
239
- });
240
- }
241
-
242
- // Sort back to front
243
- renderList.sort((a, b) => b.z - a.z);
244
- return renderList;
245
- }
246
-
247
- /**
248
- * Clear all particles
249
- */
250
- clear() {
251
- this.particles = [];
252
- this.active = false;
253
- this.intensity = 0;
254
- this.isDeactivating = false;
255
- }
256
-
257
- /**
258
- * Update BH radius
259
- */
260
- updateBHRadius(radius) {
261
- this.bhRadius = radius;
262
- }
263
-
264
- render() {
265
- super.render();
266
-
267
- if (!this.active || !this.camera || this.particles.length === 0) return;
268
-
269
- const cx = this.game.width / 2;
270
- const cy = this.game.height / 2;
271
- const renderList = this.buildRenderList();
272
-
273
- Painter.useCtx((ctx) => {
274
- // Reset transform
275
- ctx.setTransform(1, 0, 0, 1, 0, 0);
276
- ctx.globalCompositeOperation = "lighter";
277
-
278
- for (const item of renderList) {
279
- const { r, g, b } = item.color;
280
-
281
- ctx.fillStyle = `rgba(${Math.round(r)}, ${Math.round(g)}, ${Math.round(b)}, ${item.alpha})`;
282
- ctx.beginPath();
283
- ctx.arc(cx + item.x, cy + item.y, item.size * item.scale, 0, Math.PI * 2);
284
- ctx.fill();
285
- }
286
-
287
- ctx.globalCompositeOperation = "source-over";
288
- });
289
- }
290
- }
@@ -1,154 +0,0 @@
1
- /**
2
- * LensedStarfield - Starfield with gravitational lensing around black hole
3
- *
4
- * Extends the base StarField to add camera-space lensing effects:
5
- * - Stars behind the black hole are displaced radially outward
6
- * - Creates the "Einstein ring" effect where light bends around massive objects
7
- * - Lensing strength can be animated for dramatic effect
8
- */
9
- import { Painter } from "../../../src/index.js";
10
- import { applyGravitationalLensing } from "../../../src/math/gr.js";
11
- import { StarField } from "../blackhole/starfield.obj.js";
12
-
13
- const LENSING_CONFIG = {
14
- // How far the lensing effect reaches in screen pixels
15
- effectRadiusPixels: 600,
16
-
17
- // Base strength (multiplied by lensingStrength property)
18
- baseStrength: 200,
19
-
20
- // Falloff exponent - higher = tighter effect around BH
21
- falloff: 0.008,
22
-
23
- // Minimum screen distance to apply lensing (avoid division issues)
24
- minDistance: 5,
25
-
26
- // Occlusion radius multiplier (stars within BH radius * this are hidden)
27
- // The dark shadow region extends to ~2.6x the event horizon (photon sphere)
28
- occlusionMultiplier: 2.6,
29
- };
30
-
31
- export class LensedStarfield extends StarField {
32
- /**
33
- * @param {Game} game
34
- * @param {Object} options
35
- * @param {Camera3D} options.camera
36
- * @param {BlackHole} options.blackHole - Reference to black hole for radius
37
- * @param {number} options.starCount
38
- */
39
- constructor(game, options = {}) {
40
- super(game, options);
41
-
42
- // Reference to black hole (for radius and position)
43
- this.blackHole = options.blackHole ?? null;
44
-
45
- // Animated lensing strength (0 = off, 1 = full)
46
- // Allows ramping up during disruption phases
47
- this.lensingStrength = options.lensingStrength ?? 1.0;
48
- }
49
-
50
- /**
51
- * Update black hole reference (if set after construction)
52
- */
53
- setBlackHole(bh) {
54
- this.blackHole = bh;
55
- }
56
-
57
- /**
58
- * Override render to apply gravitational lensing
59
- */
60
- render() {
61
- // Skip parent render - we do our own with lensing
62
- if (!this.camera) return;
63
-
64
- const cx = this.game.width / 2;
65
- const cy = this.game.height / 2;
66
- const time = performance.now() / 1000;
67
-
68
- // Lensing parameters (strength scales with lensingStrength property)
69
- const lensingPower = LENSING_CONFIG.baseStrength * this.lensingStrength;
70
- const effectRadius = LENSING_CONFIG.effectRadiusPixels;
71
-
72
- // Project black hole position once for all stars
73
- // This is crucial when camera has moved (e.g., following the star)
74
- const bhProjected = this.camera.project(0, 0, 0);
75
- const bhScreenX = bhProjected.x;
76
- const bhScreenY = bhProjected.y;
77
-
78
- Painter.useCtx((ctx) => {
79
- ctx.globalCompositeOperation = "source-over";
80
-
81
- for (const star of this.stars) {
82
- // === USE CAMERA.PROJECT FOR PROPER POSITION HANDLING ===
83
- // This accounts for camera position (translation) not just rotation
84
- const projected = this.camera.project(star.x, star.y, star.z);
85
-
86
- // Skip stars behind camera
87
- if (projected.scale <= 0) continue;
88
-
89
- let screenX = projected.x;
90
- let screenY = projected.y;
91
- const perspectiveScale = projected.scale;
92
-
93
- // === GRAVITATIONAL LENSING (relative to BH screen position) ===
94
- // Only apply to stars "behind" the black hole (positive z after projection)
95
- if (lensingPower > 0 && projected.z > 0) {
96
- // Calculate position relative to BH's screen position
97
- const relX = screenX - bhScreenX;
98
- const relY = screenY - bhScreenY;
99
-
100
- const lensed = applyGravitationalLensing(
101
- relX, relY,
102
- effectRadius,
103
- lensingPower,
104
- LENSING_CONFIG.falloff,
105
- LENSING_CONFIG.minDistance
106
- );
107
-
108
- // Transform back to screen space
109
- screenX = lensed.x + bhScreenX;
110
- screenY = lensed.y + bhScreenY;
111
- }
112
-
113
- // === OCCLUSION CHECK (relative to BH screen position) ===
114
- // Hide stars that fall within the black hole's occlusion radius
115
- if (this.blackHole) {
116
- const bhScreenRadius = this.blackHole.currentRadius * LENSING_CONFIG.occlusionMultiplier;
117
- const dxBH = screenX - bhScreenX;
118
- const dyBH = screenY - bhScreenY;
119
- const distFromBH = Math.sqrt(dxBH * dxBH + dyBH * dyBH);
120
- if (distFromBH < bhScreenRadius) continue;
121
- }
122
-
123
- // Final screen position
124
- const finalX = cx + screenX;
125
- const finalY = cy + screenY;
126
-
127
- // Viewport cull
128
- if (finalX < -30 || finalX > this.game.width + 30 ||
129
- finalY < -30 || finalY > this.game.height + 30) continue;
130
-
131
- // === TWINKLE ===
132
- const val = Math.sin(time * star.twinkleSpeed + star.twinklePhase);
133
- const alpha = 0.6 + 0.4 * val;
134
- if (alpha < 0.1) continue;
135
-
136
- // === DRAW ===
137
- const finalSize = star.type.baseSize * star.baseScale * perspectiveScale;
138
- const sprite = this.sprites.get(star.type.type);
139
- if (!sprite) continue;
140
-
141
- ctx.globalAlpha = alpha;
142
- ctx.drawImage(
143
- sprite,
144
- finalX - finalSize * 2,
145
- finalY - finalSize * 2,
146
- finalSize * 4,
147
- finalSize * 4
148
- );
149
- }
150
-
151
- ctx.globalAlpha = 1.0;
152
- });
153
- }
154
- }