@guinetik/gcanvas 1.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.
- package/.github/workflows/release.yaml +70 -0
- package/.jshintrc +4 -0
- package/.vscode/settings.json +22 -0
- package/CLAUDE.md +310 -0
- package/blackhole.jpg +0 -0
- package/demo.png +0 -0
- package/demos/CNAME +1 -0
- package/demos/animations.html +31 -0
- package/demos/basic.html +38 -0
- package/demos/baskara.html +31 -0
- package/demos/bezier.html +35 -0
- package/demos/beziersignature.html +29 -0
- package/demos/blackhole.html +28 -0
- package/demos/blob.html +35 -0
- package/demos/demos.css +289 -0
- package/demos/easing.html +28 -0
- package/demos/events.html +195 -0
- package/demos/fluent.html +647 -0
- package/demos/fractals.html +36 -0
- package/demos/genart.html +26 -0
- package/demos/gendream.html +26 -0
- package/demos/group.html +36 -0
- package/demos/home.html +587 -0
- package/demos/index.html +364 -0
- package/demos/isometric.html +34 -0
- package/demos/js/animations.js +452 -0
- package/demos/js/basic.js +204 -0
- package/demos/js/baskara.js +751 -0
- package/demos/js/bezier.js +692 -0
- package/demos/js/beziersignature.js +241 -0
- package/demos/js/blackhole/accretiondisk.obj.js +379 -0
- package/demos/js/blackhole/blackhole.obj.js +318 -0
- package/demos/js/blackhole/index.js +409 -0
- package/demos/js/blackhole/particle.js +56 -0
- package/demos/js/blackhole/starfield.obj.js +218 -0
- package/demos/js/blob.js +2263 -0
- package/demos/js/easing.js +477 -0
- package/demos/js/fluent.js +183 -0
- package/demos/js/fractals.js +931 -0
- package/demos/js/fractalworker.js +93 -0
- package/demos/js/genart.js +268 -0
- package/demos/js/gendream.js +209 -0
- package/demos/js/group.js +140 -0
- package/demos/js/info-toggle.js +25 -0
- package/demos/js/isometric.js +863 -0
- package/demos/js/kerr.js +1556 -0
- package/demos/js/lavalamp.js +590 -0
- package/demos/js/layout.js +354 -0
- package/demos/js/mondrian.js +285 -0
- package/demos/js/opacity.js +275 -0
- package/demos/js/painter.js +484 -0
- package/demos/js/particles-showcase.js +514 -0
- package/demos/js/particles.js +299 -0
- package/demos/js/patterns.js +397 -0
- package/demos/js/penrose/artifact.js +69 -0
- package/demos/js/penrose/blackhole.js +121 -0
- package/demos/js/penrose/constants.js +73 -0
- package/demos/js/penrose/game.js +943 -0
- package/demos/js/penrose/lore.js +278 -0
- package/demos/js/penrose/penrosescene.js +892 -0
- package/demos/js/penrose/ship.js +216 -0
- package/demos/js/penrose/sounds.js +211 -0
- package/demos/js/penrose/voidparticle.js +55 -0
- package/demos/js/penrose/voidscene.js +258 -0
- package/demos/js/penrose/voidship.js +144 -0
- package/demos/js/penrose/wormhole.js +46 -0
- package/demos/js/pipeline.js +555 -0
- package/demos/js/scene.js +304 -0
- package/demos/js/scenes.js +320 -0
- package/demos/js/schrodinger.js +410 -0
- package/demos/js/schwarzschild.js +1023 -0
- package/demos/js/shapes.js +628 -0
- package/demos/js/space/alien.js +171 -0
- package/demos/js/space/boom.js +98 -0
- package/demos/js/space/boss.js +353 -0
- package/demos/js/space/buff.js +73 -0
- package/demos/js/space/bullet.js +102 -0
- package/demos/js/space/constants.js +85 -0
- package/demos/js/space/game.js +1884 -0
- package/demos/js/space/hud.js +112 -0
- package/demos/js/space/laserbeam.js +179 -0
- package/demos/js/space/lightning.js +277 -0
- package/demos/js/space/minion.js +192 -0
- package/demos/js/space/missile.js +212 -0
- package/demos/js/space/player.js +430 -0
- package/demos/js/space/powerup.js +90 -0
- package/demos/js/space/starfield.js +58 -0
- package/demos/js/space/starpower.js +90 -0
- package/demos/js/spacetime.js +559 -0
- package/demos/js/svgtween.js +204 -0
- package/demos/js/tde/accretiondisk.js +418 -0
- package/demos/js/tde/blackhole.js +219 -0
- package/demos/js/tde/blackholescene.js +209 -0
- package/demos/js/tde/config.js +59 -0
- package/demos/js/tde/index.js +695 -0
- package/demos/js/tde/jets.js +290 -0
- package/demos/js/tde/lensedstarfield.js +147 -0
- package/demos/js/tde/tdestar.js +317 -0
- package/demos/js/tde/tidalstream.js +356 -0
- package/demos/js/tde_old/blackhole.obj.js +354 -0
- package/demos/js/tde_old/debris.obj.js +791 -0
- package/demos/js/tde_old/flare.obj.js +239 -0
- package/demos/js/tde_old/index.js +448 -0
- package/demos/js/tde_old/star.obj.js +812 -0
- package/demos/js/tiles.js +312 -0
- package/demos/js/tweendemo.js +79 -0
- package/demos/js/visibility.js +102 -0
- package/demos/kerr.html +28 -0
- package/demos/lavalamp.html +27 -0
- package/demos/layouts.html +37 -0
- package/demos/logo.svg +4 -0
- package/demos/loop.html +84 -0
- package/demos/mondrian.html +32 -0
- package/demos/og_image.png +0 -0
- package/demos/opacity.html +36 -0
- package/demos/painter.html +39 -0
- package/demos/particles-showcase.html +28 -0
- package/demos/particles.html +24 -0
- package/demos/patterns.html +33 -0
- package/demos/penrose-game.html +31 -0
- package/demos/pipeline.html +737 -0
- package/demos/scene.html +33 -0
- package/demos/scenes.html +96 -0
- package/demos/schrodinger.html +27 -0
- package/demos/schwarzschild.html +27 -0
- package/demos/shapes.html +16 -0
- package/demos/space.html +85 -0
- package/demos/spacetime.html +27 -0
- package/demos/svgtween.html +29 -0
- package/demos/tde.html +28 -0
- package/demos/tiles.html +28 -0
- package/demos/transforms.html +400 -0
- package/demos/tween.html +45 -0
- package/demos/visibility.html +33 -0
- package/disk_example.png +0 -0
- package/docs/README.md +222 -0
- package/docs/concepts/architecture-overview.md +204 -0
- package/docs/concepts/lifecycle.md +255 -0
- package/docs/concepts/rendering-pipeline.md +279 -0
- package/docs/concepts/tde-zorder.md +106 -0
- package/docs/concepts/two-layer-architecture.md +229 -0
- package/docs/getting-started/first-game.md +354 -0
- package/docs/getting-started/hello-world.md +269 -0
- package/docs/getting-started/installation.md +157 -0
- package/docs/modules/collision/README.md +453 -0
- package/docs/modules/fluent/README.md +1075 -0
- package/docs/modules/game/README.md +303 -0
- package/docs/modules/isometric-camera.md +210 -0
- package/docs/modules/isometric.md +275 -0
- package/docs/modules/painter/README.md +328 -0
- package/docs/modules/particle/README.md +559 -0
- package/docs/modules/shapes/README.md +221 -0
- package/docs/modules/shapes/base/euclidian.md +123 -0
- package/docs/modules/shapes/base/geometry2d.md +204 -0
- package/docs/modules/shapes/base/renderable.md +215 -0
- package/docs/modules/shapes/base/shape.md +262 -0
- package/docs/modules/shapes/base/transformable.md +243 -0
- package/docs/modules/shapes/hierarchy.md +218 -0
- package/docs/modules/state/README.md +577 -0
- package/docs/modules/util/README.md +99 -0
- package/docs/modules/util/camera3d.md +412 -0
- package/docs/modules/util/scene3d.md +395 -0
- package/index.html +17 -0
- package/jsdoc.json +50 -0
- package/package.json +55 -0
- package/readme.md +599 -0
- package/scripts/build-demo.js +69 -0
- package/scripts/bundle4llm.js +276 -0
- package/scripts/clearconsole.js +48 -0
- package/src/collision/collision-system.js +332 -0
- package/src/collision/collision.js +303 -0
- package/src/collision/index.js +10 -0
- package/src/fluent/fluent-game.js +430 -0
- package/src/fluent/fluent-go.js +1060 -0
- package/src/fluent/fluent-layer.js +152 -0
- package/src/fluent/fluent-scene.js +291 -0
- package/src/fluent/index.js +98 -0
- package/src/fluent/sketch.js +380 -0
- package/src/game/game.js +467 -0
- package/src/game/index.js +49 -0
- package/src/game/objects/go.js +220 -0
- package/src/game/objects/imagego.js +30 -0
- package/src/game/objects/index.js +54 -0
- package/src/game/objects/isometric-scene.js +260 -0
- package/src/game/objects/layoutscene.js +549 -0
- package/src/game/objects/scene.js +175 -0
- package/src/game/objects/scene3d.js +118 -0
- package/src/game/objects/text.js +221 -0
- package/src/game/objects/wrapper.js +232 -0
- package/src/game/pipeline.js +243 -0
- package/src/game/ui/button.js +396 -0
- package/src/game/ui/cursor.js +93 -0
- package/src/game/ui/fps.js +91 -0
- package/src/game/ui/index.js +5 -0
- package/src/game/ui/togglebutton.js +93 -0
- package/src/game/ui/tooltip.js +249 -0
- package/src/index.js +25 -0
- package/src/io/events.js +20 -0
- package/src/io/index.js +86 -0
- package/src/io/input.js +70 -0
- package/src/io/keys.js +152 -0
- package/src/io/mouse.js +61 -0
- package/src/io/touch.js +39 -0
- package/src/logger/debugtab.js +138 -0
- package/src/logger/index.js +3 -0
- package/src/logger/loggable.js +47 -0
- package/src/logger/logger.js +113 -0
- package/src/math/complex.js +37 -0
- package/src/math/constants.js +1 -0
- package/src/math/fractal.js +1271 -0
- package/src/math/gr.js +201 -0
- package/src/math/heat.js +202 -0
- package/src/math/index.js +12 -0
- package/src/math/noise.js +433 -0
- package/src/math/orbital.js +191 -0
- package/src/math/patterns.js +1339 -0
- package/src/math/penrose.js +259 -0
- package/src/math/quantum.js +115 -0
- package/src/math/random.js +195 -0
- package/src/math/tensor.js +1009 -0
- package/src/mixins/anchor.js +131 -0
- package/src/mixins/draggable.js +72 -0
- package/src/mixins/index.js +2 -0
- package/src/motion/bezier.js +132 -0
- package/src/motion/bounce.js +58 -0
- package/src/motion/easing.js +349 -0
- package/src/motion/float.js +130 -0
- package/src/motion/follow.js +125 -0
- package/src/motion/hop.js +52 -0
- package/src/motion/index.js +82 -0
- package/src/motion/motion.js +1124 -0
- package/src/motion/orbit.js +49 -0
- package/src/motion/oscillate.js +39 -0
- package/src/motion/parabolic.js +141 -0
- package/src/motion/patrol.js +147 -0
- package/src/motion/pendulum.js +48 -0
- package/src/motion/pulse.js +88 -0
- package/src/motion/shake.js +83 -0
- package/src/motion/spiral.js +144 -0
- package/src/motion/spring.js +150 -0
- package/src/motion/swing.js +47 -0
- package/src/motion/tween.js +92 -0
- package/src/motion/tweenetik.js +139 -0
- package/src/motion/waypoint.js +210 -0
- package/src/painter/index.js +8 -0
- package/src/painter/painter.colors.js +331 -0
- package/src/painter/painter.effects.js +230 -0
- package/src/painter/painter.img.js +229 -0
- package/src/painter/painter.js +295 -0
- package/src/painter/painter.lines.js +189 -0
- package/src/painter/painter.opacity.js +41 -0
- package/src/painter/painter.shapes.js +277 -0
- package/src/painter/painter.text.js +273 -0
- package/src/particle/emitter.js +124 -0
- package/src/particle/index.js +11 -0
- package/src/particle/particle-system.js +322 -0
- package/src/particle/particle.js +71 -0
- package/src/particle/updaters.js +170 -0
- package/src/shapes/arc.js +43 -0
- package/src/shapes/arrow.js +33 -0
- package/src/shapes/bezier.js +42 -0
- package/src/shapes/circle.js +62 -0
- package/src/shapes/clouds.js +56 -0
- package/src/shapes/cone.js +219 -0
- package/src/shapes/cross.js +70 -0
- package/src/shapes/cube.js +244 -0
- package/src/shapes/cylinder.js +254 -0
- package/src/shapes/diamond.js +48 -0
- package/src/shapes/euclidian.js +111 -0
- package/src/shapes/figure.js +115 -0
- package/src/shapes/geometry.js +220 -0
- package/src/shapes/group.js +375 -0
- package/src/shapes/heart.js +42 -0
- package/src/shapes/hexagon.js +26 -0
- package/src/shapes/image.js +192 -0
- package/src/shapes/index.js +111 -0
- package/src/shapes/line.js +29 -0
- package/src/shapes/pattern.js +90 -0
- package/src/shapes/pin.js +44 -0
- package/src/shapes/poly.js +31 -0
- package/src/shapes/prism.js +226 -0
- package/src/shapes/rect.js +35 -0
- package/src/shapes/renderable.js +333 -0
- package/src/shapes/ring.js +26 -0
- package/src/shapes/roundrect.js +95 -0
- package/src/shapes/shape.js +117 -0
- package/src/shapes/slice.js +26 -0
- package/src/shapes/sphere.js +314 -0
- package/src/shapes/sphere3d.js +537 -0
- package/src/shapes/square.js +15 -0
- package/src/shapes/star.js +99 -0
- package/src/shapes/svg.js +408 -0
- package/src/shapes/text.js +553 -0
- package/src/shapes/traceable.js +83 -0
- package/src/shapes/transform.js +357 -0
- package/src/shapes/transformable.js +172 -0
- package/src/shapes/triangle.js +26 -0
- package/src/sound/index.js +17 -0
- package/src/sound/sound.js +473 -0
- package/src/sound/synth.analyzer.js +149 -0
- package/src/sound/synth.effects.js +207 -0
- package/src/sound/synth.envelope.js +59 -0
- package/src/sound/synth.js +229 -0
- package/src/sound/synth.musical.js +160 -0
- package/src/sound/synth.noise.js +85 -0
- package/src/sound/synth.oscillators.js +293 -0
- package/src/state/index.js +10 -0
- package/src/state/state-machine.js +371 -0
- package/src/util/camera3d.js +438 -0
- package/src/util/index.js +6 -0
- package/src/util/isometric-camera.js +235 -0
- package/src/util/layout.js +317 -0
- package/src/util/position.js +147 -0
- package/src/util/tasks.js +47 -0
- package/src/util/zindex.js +287 -0
- package/src/webgl/index.js +9 -0
- package/src/webgl/shaders/sphere-shaders.js +994 -0
- package/src/webgl/webgl-renderer.js +388 -0
- package/tde.png +0 -0
- package/test/math/orbital.test.js +61 -0
- package/test/math/tensor.test.js +114 -0
- package/test/particle/emitter.test.js +204 -0
- package/test/particle/particle-system.test.js +310 -0
- package/test/particle/particle.test.js +116 -0
- package/test/particle/updaters.test.js +386 -0
- package/test/setup.js +120 -0
- package/test/shapes/euclidian.test.js +44 -0
- package/test/shapes/geometry.test.js +86 -0
- package/test/shapes/group.test.js +86 -0
- package/test/shapes/rectangle.test.js +64 -0
- package/test/shapes/transform.test.js +379 -0
- package/test/util/camera3d.test.js +428 -0
- package/test/util/scene3d.test.js +352 -0
- package/types/collision.d.ts +249 -0
- package/types/common.d.ts +155 -0
- package/types/game.d.ts +497 -0
- package/types/index.d.ts +309 -0
- package/types/io.d.ts +188 -0
- package/types/logger.d.ts +127 -0
- package/types/math.d.ts +268 -0
- package/types/mixins.d.ts +92 -0
- package/types/motion.d.ts +678 -0
- package/types/painter.d.ts +378 -0
- package/types/shapes.d.ts +864 -0
- package/types/sound.d.ts +672 -0
- package/types/state.d.ts +251 -0
- package/types/util.d.ts +253 -0
- package/vite.config.js +50 -0
- package/vitest.config.js +13 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DebrisManager - Manages debris streams for TDE demo
|
|
3
|
+
*
|
|
4
|
+
* Uses ParticleSystem with gravitational attraction toward the black hole.
|
|
5
|
+
* Handles tidal shear, disk formation with lensing, and accretion tracking.
|
|
6
|
+
*/
|
|
7
|
+
import { GameObject, Easing, Painter } from "../../../src/index.js";
|
|
8
|
+
|
|
9
|
+
const CONFIG = {
|
|
10
|
+
// Physics - VISIBLE gravity for dramatic streaming
|
|
11
|
+
gravityStrength: 1000, // Reduced so particles are visible longer
|
|
12
|
+
damping: 0.992,
|
|
13
|
+
tidalShearStrength: 0.6,
|
|
14
|
+
|
|
15
|
+
// Disk formation - WIDER disk to match blackhole demo
|
|
16
|
+
diskInnerRatio: 1.6, // Inner disk radius as multiple of bhRadius
|
|
17
|
+
diskOuterRatio: 6.0, // Outer disk radius - MUCH WIDER for dense disk
|
|
18
|
+
circularizationRate: 0.084,
|
|
19
|
+
diskFlattenRate: 0.063,
|
|
20
|
+
|
|
21
|
+
// Accretion - particles must reach center to be consumed
|
|
22
|
+
accretionRadius: 0.24453, // Smaller so particles spiral longer
|
|
23
|
+
fallInRate: 0.22,
|
|
24
|
+
|
|
25
|
+
// Particle lifetime - LONG for stable disk
|
|
26
|
+
maxLifetime: 300, // 5 minutes - disk particles should persist
|
|
27
|
+
|
|
28
|
+
// Lensing
|
|
29
|
+
lensingStrength: 1.4,
|
|
30
|
+
|
|
31
|
+
// CYCLONE SPIRAL
|
|
32
|
+
spiralTurnsMin: 1.0,
|
|
33
|
+
spiralTurnsMax: 8.0,
|
|
34
|
+
spiralSpeedBase: 400,
|
|
35
|
+
|
|
36
|
+
// Falling particle speed
|
|
37
|
+
fallingSpiralRate: 0.08,
|
|
38
|
+
diskSpiralRate: 0.015,
|
|
39
|
+
|
|
40
|
+
// Colors - temperature gradient
|
|
41
|
+
colors: {
|
|
42
|
+
inner: [255, 240, 200], // Brighter white-yellow core
|
|
43
|
+
mid: [255, 140, 50], // Vibrant orange
|
|
44
|
+
outer: [200, 40, 10], // Deep red/crimson
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Max debris particles
|
|
48
|
+
maxDebris: 15000,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export class DebrisManager extends GameObject {
|
|
52
|
+
/**
|
|
53
|
+
* @param {Game} game - Game instance
|
|
54
|
+
* @param {Object} options
|
|
55
|
+
* @param {Camera3D} options.camera - Camera for projection
|
|
56
|
+
* @param {number} options.bhRadius - Black hole radius
|
|
57
|
+
* @param {number} options.baseScale - Base scale
|
|
58
|
+
*/
|
|
59
|
+
constructor(game, options = {}) {
|
|
60
|
+
super(game, options);
|
|
61
|
+
|
|
62
|
+
this.camera = options.camera;
|
|
63
|
+
this.bhRadius = options.bhRadius ?? 50;
|
|
64
|
+
this.baseScale = options.baseScale ?? 500;
|
|
65
|
+
|
|
66
|
+
// Black hole position (attraction target)
|
|
67
|
+
this.bhPosition = { x: 0, y: 0, z: 0 };
|
|
68
|
+
|
|
69
|
+
// Disk sizing
|
|
70
|
+
this.diskInner = this.bhRadius * CONFIG.diskInnerRatio;
|
|
71
|
+
this.diskOuter = this.bhRadius * CONFIG.diskOuterRatio;
|
|
72
|
+
|
|
73
|
+
// Particle system
|
|
74
|
+
this.particleSystem = null;
|
|
75
|
+
|
|
76
|
+
// Debris particles (manual tracking for lensing)
|
|
77
|
+
this.debris = [];
|
|
78
|
+
|
|
79
|
+
// Accretion tracking
|
|
80
|
+
this.particlesAccreted = 0;
|
|
81
|
+
this.accretionRate = 0;
|
|
82
|
+
this.lastAccretionCount = 0;
|
|
83
|
+
|
|
84
|
+
// Lensing strength (increases as disk forms)
|
|
85
|
+
this.lensingAmount = 0;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Initialize the debris manager.
|
|
90
|
+
*/
|
|
91
|
+
init() {
|
|
92
|
+
// We'll manually manage debris instead of using ParticleSystem
|
|
93
|
+
// This gives us control over lensing projection
|
|
94
|
+
this.debris = [];
|
|
95
|
+
this.diskParticles = []; // Permanent accretion disk (like blackhole demo)
|
|
96
|
+
this.diskFormed = false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Create a permanent accretion disk (like the blackhole demo).
|
|
101
|
+
* Called early to ensure there's always a visible disk.
|
|
102
|
+
*/
|
|
103
|
+
createAccretionDisk(particleCount = 2000) {
|
|
104
|
+
if (this.diskFormed) return;
|
|
105
|
+
this.diskFormed = true;
|
|
106
|
+
this.addToDisk(particleCount);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Add more particles to the accretion disk.
|
|
111
|
+
*/
|
|
112
|
+
addToDisk(particleCount) {
|
|
113
|
+
for (let i = 0; i < particleCount; i++) {
|
|
114
|
+
const angle = Math.random() * Math.PI * 2;
|
|
115
|
+
const t = Math.random();
|
|
116
|
+
// Bias toward inner (hotter) region like blackhole demo
|
|
117
|
+
const r = this.diskInner + t * t * (this.diskOuter - this.diskInner);
|
|
118
|
+
|
|
119
|
+
// Keplerian orbital speed
|
|
120
|
+
const speed = (1 / Math.sqrt(r / this.bhRadius)) * 2.0;
|
|
121
|
+
// FLAT disk - very small y offset
|
|
122
|
+
const yOffset = (Math.random() - 0.5) * this.bhRadius * 0.05;
|
|
123
|
+
|
|
124
|
+
this.diskParticles.push({
|
|
125
|
+
angle: angle,
|
|
126
|
+
distance: r,
|
|
127
|
+
yOffset: yOffset,
|
|
128
|
+
speed: speed,
|
|
129
|
+
baseColor: this.getHeatColor(r),
|
|
130
|
+
size: 1.5 + Math.random() * 2.5,
|
|
131
|
+
isFalling: false,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get temperature-based color for distance from black hole.
|
|
138
|
+
* Outer particles FADE TO TRANSPARENT - no hard edge.
|
|
139
|
+
*/
|
|
140
|
+
getHeatColor(dist) {
|
|
141
|
+
const t = (dist - this.diskInner) / (this.diskOuter - this.diskInner);
|
|
142
|
+
const clampedT = Math.max(0, Math.min(1, t));
|
|
143
|
+
|
|
144
|
+
let c1, c2, mix;
|
|
145
|
+
if (clampedT < 0.3) {
|
|
146
|
+
c1 = CONFIG.colors.inner;
|
|
147
|
+
c2 = CONFIG.colors.mid;
|
|
148
|
+
mix = clampedT / 0.3;
|
|
149
|
+
} else {
|
|
150
|
+
c1 = CONFIG.colors.mid;
|
|
151
|
+
c2 = CONFIG.colors.outer;
|
|
152
|
+
mix = (clampedT - 0.3) / 0.7;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Alpha FADES at outer edge - but stays visible longer
|
|
156
|
+
// Inner: fully opaque, Outer: still visible (0.2 minimum)
|
|
157
|
+
const alpha = 0.2 + Math.pow(1 - clampedT, 1.2) * 0.7;
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
r: c1[0] + (c2[0] - c1[0]) * mix,
|
|
161
|
+
g: c1[1] + (c2[1] - c1[1]) * mix,
|
|
162
|
+
b: c1[2] + (c2[2] - c1[2]) * mix,
|
|
163
|
+
a: alpha,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Update sizing.
|
|
169
|
+
*/
|
|
170
|
+
updateSizing(bhRadius, baseScale) {
|
|
171
|
+
this.bhRadius = bhRadius;
|
|
172
|
+
this.baseScale = baseScale;
|
|
173
|
+
this.diskInner = bhRadius * CONFIG.diskInnerRatio;
|
|
174
|
+
this.diskOuter = bhRadius * CONFIG.diskOuterRatio;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Add debris particles from the disrupted star.
|
|
179
|
+
* Particles flow FROM star TOWARD BH center - either into disk or consumed.
|
|
180
|
+
*/
|
|
181
|
+
addDebris(debrisData) {
|
|
182
|
+
for (const d of debrisData) {
|
|
183
|
+
// Limit total debris
|
|
184
|
+
if (this.debris.length >= CONFIG.maxDebris) break;
|
|
185
|
+
|
|
186
|
+
// Distance and angle from BH center (origin)
|
|
187
|
+
const dist = Math.sqrt(d.x * d.x + d.z * d.z);
|
|
188
|
+
const angle = Math.atan2(d.z, d.x);
|
|
189
|
+
|
|
190
|
+
// Determine fate: accretion disk or fall into BH
|
|
191
|
+
// MOST particles should form the disk - only small fraction falls in
|
|
192
|
+
const normalizedDist = Math.min(1, dist / (this.bhRadius * 8));
|
|
193
|
+
// 15% of close particles fall in, 5% of far particles - keeps disk DENSE
|
|
194
|
+
const fallInChance = 0.15 - normalizedDist * 0.1;
|
|
195
|
+
const willFallIn = Math.random() < fallInChance;
|
|
196
|
+
|
|
197
|
+
// Target orbit radius - biased toward inner disk
|
|
198
|
+
const randFactor = Math.pow(Math.random(), 1.5); // Bias toward inner
|
|
199
|
+
const targetDist = willFallIn
|
|
200
|
+
? 0
|
|
201
|
+
: this.diskInner + randFactor * (this.diskOuter - this.diskInner) * 0.6;
|
|
202
|
+
|
|
203
|
+
// Spiral toward BH - all particles spiral inward
|
|
204
|
+
const spiralTurns =
|
|
205
|
+
CONFIG.spiralTurnsMin +
|
|
206
|
+
Math.random() * (CONFIG.spiralTurnsMax - CONFIG.spiralTurnsMin);
|
|
207
|
+
|
|
208
|
+
// Target angle - continue in spiral direction toward BH
|
|
209
|
+
const targetAngle = angle + spiralTurns * Math.PI * 2;
|
|
210
|
+
|
|
211
|
+
// Initial velocities from star stream
|
|
212
|
+
const vx = d.vx || 0;
|
|
213
|
+
const vy = d.vy || 0;
|
|
214
|
+
const vz = d.vz || 0;
|
|
215
|
+
|
|
216
|
+
// FLAT DISK like the original blackhole demo
|
|
217
|
+
const yVariation = this.baseScale * 0.006;
|
|
218
|
+
|
|
219
|
+
this.debris.push({
|
|
220
|
+
// Current state
|
|
221
|
+
x: d.x,
|
|
222
|
+
y: d.y,
|
|
223
|
+
z: d.z,
|
|
224
|
+
vx: vx,
|
|
225
|
+
vy: vy,
|
|
226
|
+
vz: vz,
|
|
227
|
+
|
|
228
|
+
// SPIRAL trajectory state (for disk formation logic)
|
|
229
|
+
startAngle: angle,
|
|
230
|
+
startDistance: dist,
|
|
231
|
+
startYOffset: d.y,
|
|
232
|
+
|
|
233
|
+
// Current polar state (animated via physics)
|
|
234
|
+
angle: angle,
|
|
235
|
+
distance: dist,
|
|
236
|
+
yOffset: d.y,
|
|
237
|
+
|
|
238
|
+
// Target state - THICK disk with vertical variation
|
|
239
|
+
targetAngle: targetAngle,
|
|
240
|
+
targetDistance: targetDist,
|
|
241
|
+
targetYOffset: (Math.random() - 0.5) * yVariation,
|
|
242
|
+
|
|
243
|
+
// SPIRAL PARAMS
|
|
244
|
+
spiralTurns: spiralTurns,
|
|
245
|
+
spiralProgress: 0, // Start at zero - no jump
|
|
246
|
+
|
|
247
|
+
// Behavior flags
|
|
248
|
+
willFallIn: willFallIn,
|
|
249
|
+
// Treat ALL incoming debris as dynamic "falling" physics initially
|
|
250
|
+
// They will transition to disk orbit later if not consumed
|
|
251
|
+
isFalling: true,
|
|
252
|
+
|
|
253
|
+
// Appearance - slight size variation
|
|
254
|
+
size: d.size * (0.6 + Math.random() * 0.6),
|
|
255
|
+
baseColor: this.getHeatColor(dist),
|
|
256
|
+
|
|
257
|
+
// State
|
|
258
|
+
age: 0,
|
|
259
|
+
consumed: false,
|
|
260
|
+
circularized: false,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Clear all debris (but keep permanent disk if formed).
|
|
267
|
+
*/
|
|
268
|
+
clear() {
|
|
269
|
+
this.debris = [];
|
|
270
|
+
this.particlesAccreted = 0;
|
|
271
|
+
this.accretionRate = 0;
|
|
272
|
+
this.lastAccretionCount = 0;
|
|
273
|
+
this.lensingAmount = 0;
|
|
274
|
+
// Reset disk for new simulation
|
|
275
|
+
this.diskParticles = [];
|
|
276
|
+
this.diskFormed = false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get current accretion rate.
|
|
281
|
+
*/
|
|
282
|
+
getAccretionRate() {
|
|
283
|
+
return this.accretionRate;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Set lensing amount (0-1).
|
|
288
|
+
*/
|
|
289
|
+
setLensingAmount(amount) {
|
|
290
|
+
this.lensingAmount = Math.max(0, Math.min(1, amount));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
update(dt) {
|
|
294
|
+
super.update(dt);
|
|
295
|
+
|
|
296
|
+
const accretionDist = this.bhRadius * CONFIG.accretionRadius;
|
|
297
|
+
let newAccretions = 0;
|
|
298
|
+
|
|
299
|
+
// Update each debris particle
|
|
300
|
+
for (let i = this.debris.length - 1; i >= 0; i--) {
|
|
301
|
+
const p = this.debris[i];
|
|
302
|
+
if (p.consumed) continue;
|
|
303
|
+
|
|
304
|
+
p.age += dt;
|
|
305
|
+
|
|
306
|
+
if (p.isFalling) {
|
|
307
|
+
// --- NEW PHYSICS: ORBITAL MECHANICS ---
|
|
308
|
+
|
|
309
|
+
// 1. Gravity (Newtonian approx)
|
|
310
|
+
const distSq = p.x * p.x + p.y * p.y + p.z * p.z;
|
|
311
|
+
const dist = Math.sqrt(distSq);
|
|
312
|
+
|
|
313
|
+
// Gravity force = G * M / r^2
|
|
314
|
+
// We can tune strength to match visual scale
|
|
315
|
+
const gravityAccel = CONFIG.gravityStrength / Math.max(distSq, 100);
|
|
316
|
+
|
|
317
|
+
// Direction to center
|
|
318
|
+
const dirX = -p.x / dist;
|
|
319
|
+
const dirY = -p.y / dist;
|
|
320
|
+
const dirZ = -p.z / dist;
|
|
321
|
+
|
|
322
|
+
// Apply Gravity
|
|
323
|
+
p.vx = (p.vx || 0) + dirX * gravityAccel * dt;
|
|
324
|
+
p.vy = (p.vy || 0) + dirY * gravityAccel * dt;
|
|
325
|
+
p.vz = (p.vz || 0) + dirZ * gravityAccel * dt;
|
|
326
|
+
|
|
327
|
+
// 2. Drag / Circularization / Accretion Force
|
|
328
|
+
// Gently nudge velocity towards a circular orbit to form disk
|
|
329
|
+
// Target velocity for circular orbit: sqrt(GM/r)
|
|
330
|
+
// Tangent direction: cross product of vertical axis (0,1,0) and radius
|
|
331
|
+
|
|
332
|
+
// But for "S" shape, we primarily just want gravity to do the work first.
|
|
333
|
+
// We add a small "drag" to prevent them from flying off forever if they are too fast.
|
|
334
|
+
|
|
335
|
+
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy + p.vz * p.vz);
|
|
336
|
+
if (speed > 200) {
|
|
337
|
+
// Drag if moving too fast
|
|
338
|
+
p.vx *= 0.99;
|
|
339
|
+
p.vy *= 0.99;
|
|
340
|
+
p.vz *= 0.99;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 3. Move
|
|
344
|
+
p.x += p.vx * dt;
|
|
345
|
+
p.y += p.vy * dt;
|
|
346
|
+
p.z += p.vz * dt;
|
|
347
|
+
|
|
348
|
+
// Update derived polar coords for rendering logic
|
|
349
|
+
p.distance = Math.sqrt(p.x * p.x + p.z * p.z);
|
|
350
|
+
p.angle = Math.atan2(p.z, p.x);
|
|
351
|
+
|
|
352
|
+
// Check accretion
|
|
353
|
+
if (dist < accretionDist) {
|
|
354
|
+
p.consumed = true;
|
|
355
|
+
newAccretions++;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 4. Force Capture (Prevent flying off to infinity)
|
|
359
|
+
// If particle gets too far, nudge it back strongly
|
|
360
|
+
if (dist > this.bhRadius * 15) {
|
|
361
|
+
const pullBack = 2.0;
|
|
362
|
+
p.vx += dirX * pullBack * dt;
|
|
363
|
+
p.vy += dirY * pullBack * dt;
|
|
364
|
+
p.vz += dirZ * pullBack * dt;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
} else {
|
|
368
|
+
// Disk-forming particles - ALWAYS move inward first, then settle
|
|
369
|
+
p.spiralProgress = Math.min(1, p.spiralProgress + 0.05);
|
|
370
|
+
|
|
371
|
+
// ALWAYS apply inward pull - particles MUST move toward BH
|
|
372
|
+
p.distance -= p.inwardVel * dt * 0.4;
|
|
373
|
+
|
|
374
|
+
// Gravity pull toward center
|
|
375
|
+
const gravityAccel =
|
|
376
|
+
(CONFIG.gravityStrength * dt * 0.2) / Math.max(p.distance, 50);
|
|
377
|
+
p.inwardVel = Math.min(100, p.inwardVel + gravityAccel);
|
|
378
|
+
|
|
379
|
+
// Once close to target orbit, start settling into circular orbit
|
|
380
|
+
if (p.distance <= p.targetDistance * 1.3) {
|
|
381
|
+
const settleRate = 0.05;
|
|
382
|
+
p.distance = Easing.lerp(p.distance, p.targetDistance, settleRate);
|
|
383
|
+
p.circularized = p.distance < p.targetDistance * 1.1;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Clamp minimum distance - don't go inside inner disk
|
|
387
|
+
p.distance = Math.max(this.diskInner * 0.5, p.distance);
|
|
388
|
+
|
|
389
|
+
// Angular motion - rotate around BH
|
|
390
|
+
const angularSpeed =
|
|
391
|
+
1.8 / Math.sqrt(Math.max(1, p.distance / this.bhRadius));
|
|
392
|
+
p.angle += angularSpeed * dt;
|
|
393
|
+
|
|
394
|
+
// Flatten to disk plane
|
|
395
|
+
const flattenT = Math.min(1, p.age * 0.5);
|
|
396
|
+
p.yOffset = Easing.lerp(p.startYOffset, p.targetYOffset, flattenT);
|
|
397
|
+
|
|
398
|
+
// Once circularized, maintain stable Keplerian orbit
|
|
399
|
+
if (p.circularized) {
|
|
400
|
+
// Stable orbital motion - particles stay in orbit
|
|
401
|
+
const orbitalSpeed =
|
|
402
|
+
(1 / Math.sqrt(Math.max(1, p.distance / this.bhRadius))) * 2.0;
|
|
403
|
+
p.angle += orbitalSpeed * dt;
|
|
404
|
+
|
|
405
|
+
// Small wobble to keep disk flat but not perfectly static
|
|
406
|
+
const wobble =
|
|
407
|
+
Math.sin(p.angle * 3 + p.startAngle) * this.baseScale * 0.003;
|
|
408
|
+
p.yOffset = p.targetYOffset + wobble;
|
|
409
|
+
|
|
410
|
+
// VERY rarely, inner particles fall in (keeps disk dynamic but dense)
|
|
411
|
+
if (p.distance < this.diskInner * 1.1 && Math.random() < 0.00001) {
|
|
412
|
+
p.isFalling = true;
|
|
413
|
+
// Initialize velocity for falling physics
|
|
414
|
+
// Tangent velocity + slight inward kick
|
|
415
|
+
const tangentX = -Math.sin(p.angle);
|
|
416
|
+
const tangentZ = Math.cos(p.angle);
|
|
417
|
+
const orbSpeed = orbitalSpeed * p.distance; // rad/s * dist = units/s
|
|
418
|
+
|
|
419
|
+
p.vx = tangentX * orbSpeed * 0.9; // 0.9 to start spiral
|
|
420
|
+
p.vz = tangentZ * orbSpeed * 0.9;
|
|
421
|
+
p.vy = 0;
|
|
422
|
+
|
|
423
|
+
// Explicitly set positions from polar
|
|
424
|
+
p.x = Math.cos(p.angle) * p.distance;
|
|
425
|
+
p.z = Math.sin(p.angle) * p.distance;
|
|
426
|
+
p.y = p.yOffset;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Convert stray particles that haven't settled
|
|
431
|
+
if (p.age > 5 && p.distance > this.diskOuter) {
|
|
432
|
+
p.isFalling = true;
|
|
433
|
+
p.vx = (Math.random()-0.5) * 10;
|
|
434
|
+
p.vz = (Math.random()-0.5) * 10;
|
|
435
|
+
p.vy = 0;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Update Cartesian coordinates from polar (ONLY FOR DISK PARTICLES)
|
|
440
|
+
// Falling particles update Cartesian directly in physics block above
|
|
441
|
+
if (!p.isFalling) {
|
|
442
|
+
p.x = Math.cos(p.angle) * p.distance;
|
|
443
|
+
p.z = Math.sin(p.angle) * p.distance;
|
|
444
|
+
p.y = p.yOffset;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Update color based on current distance
|
|
448
|
+
// For falling particles, use full distance (3D)
|
|
449
|
+
const colorDist = p.isFalling ? Math.sqrt(p.x*p.x + p.z*p.z) : p.distance;
|
|
450
|
+
p.baseColor = this.getHeatColor(colorDist);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Remove consumed particles and expired particles
|
|
454
|
+
this.debris = this.debris.filter(
|
|
455
|
+
(p) => !p.consumed && p.age < CONFIG.maxLifetime,
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
this.particlesAccreted += newAccretions;
|
|
459
|
+
this.accretionRate = newAccretions / (dt || 1);
|
|
460
|
+
|
|
461
|
+
// Update permanent accretion disk particles (stable Keplerian orbits)
|
|
462
|
+
for (const p of this.diskParticles) {
|
|
463
|
+
if (p.isFalling) {
|
|
464
|
+
// Falling particles spiral in
|
|
465
|
+
p.distance *= 0.99;
|
|
466
|
+
p.angle += p.speed * dt * 1.5;
|
|
467
|
+
p.yOffset *= 0.95;
|
|
468
|
+
|
|
469
|
+
if (p.distance < accretionDist) {
|
|
470
|
+
p.consumed = true;
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
// Stable Keplerian orbit (like blackhole demo)
|
|
474
|
+
p.angle += p.speed * dt;
|
|
475
|
+
|
|
476
|
+
// Very rarely, inner particles fall in
|
|
477
|
+
if (p.distance < this.diskInner * 1.3 && Math.random() < 0.0001) {
|
|
478
|
+
p.isFalling = true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Remove consumed disk particles
|
|
484
|
+
this.diskParticles = this.diskParticles.filter((p) => !p.consumed);
|
|
485
|
+
|
|
486
|
+
// Increase lensing as disk forms
|
|
487
|
+
const circularizedCount = this.debris.filter((p) => p.circularized).length;
|
|
488
|
+
const targetLensing =
|
|
489
|
+
this.debris.length > 0
|
|
490
|
+
? Math.min(1, circularizedCount / (this.debris.length * 0.5))
|
|
491
|
+
: 0;
|
|
492
|
+
this.lensingAmount = Easing.lerp(this.lensingAmount, targetLensing, 0.02);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Build render list with lensing applied.
|
|
497
|
+
* Uses same simple lensing as the blackhole demo - just warps particles outward.
|
|
498
|
+
*/
|
|
499
|
+
buildRenderList() {
|
|
500
|
+
const renderList = [];
|
|
501
|
+
// Use full lensing once disk has some particles
|
|
502
|
+
const lensingStrength =
|
|
503
|
+
this.debris.length > 50 ? CONFIG.lensingStrength : 0;
|
|
504
|
+
|
|
505
|
+
for (const p of this.debris) {
|
|
506
|
+
if (p.consumed) continue;
|
|
507
|
+
|
|
508
|
+
// Transform to camera space
|
|
509
|
+
const cosY = Math.cos(this.camera.rotationY);
|
|
510
|
+
const sinY = Math.sin(this.camera.rotationY);
|
|
511
|
+
let xCam = p.x * cosY - p.z * sinY;
|
|
512
|
+
let zCam = p.x * sinY + p.z * cosY;
|
|
513
|
+
|
|
514
|
+
const cosX = Math.cos(this.camera.rotationX);
|
|
515
|
+
const sinX = Math.sin(this.camera.rotationX);
|
|
516
|
+
let yCam = p.y * cosX - zCam * sinX;
|
|
517
|
+
zCam = p.y * sinX + zCam * cosX;
|
|
518
|
+
|
|
519
|
+
// Apply gravitational lensing
|
|
520
|
+
if (lensingStrength > 0) {
|
|
521
|
+
let currentR = Math.sqrt(xCam * xCam + yCam * yCam);
|
|
522
|
+
const ringRadius = this.bhRadius * 1.5; // Einstein ring approx
|
|
523
|
+
|
|
524
|
+
if (zCam > 0) {
|
|
525
|
+
// Behind BH - The "Interstellar" Halo Effect
|
|
526
|
+
// Light from behind is bent around the BH.
|
|
527
|
+
// We see the light that traveled "up" and bent down to us.
|
|
528
|
+
// So we shift the image UP (negative Y) towards the Einstein ring.
|
|
529
|
+
|
|
530
|
+
// 1. Radial expansion (standard lensing)
|
|
531
|
+
const lensFactor = Math.exp(-currentR / (this.bhRadius * 2.0));
|
|
532
|
+
const warp = lensFactor * 2.0 * lensingStrength;
|
|
533
|
+
|
|
534
|
+
if (currentR > 0) {
|
|
535
|
+
const ratio = (currentR + ringRadius * warp) / currentR;
|
|
536
|
+
xCam *= ratio;
|
|
537
|
+
yCam *= ratio;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// 2. Vertical Arching (Crucial for edge-on view)
|
|
541
|
+
// If the particle is behind the BH, we pull it towards the ring radius vertically
|
|
542
|
+
// This creates the "hump" or halo over the shadow
|
|
543
|
+
const archStrength =
|
|
544
|
+
Math.exp(-(xCam * xCam) / (ringRadius * ringRadius * 4)) *
|
|
545
|
+
lensingStrength;
|
|
546
|
+
|
|
547
|
+
// Shift Y upwards (negative) to form the upper arc
|
|
548
|
+
// We blend the current Y with the ring height
|
|
549
|
+
// stronger shift when x is small (directly behind)
|
|
550
|
+
const targetY = -ringRadius * 0.9;
|
|
551
|
+
yCam = yCam + (targetY - yCam) * archStrength * 0.8;
|
|
552
|
+
|
|
553
|
+
} else if (currentR > 0 && currentR < this.bhRadius * 3) {
|
|
554
|
+
// In front of BH - bend around the black hole edge
|
|
555
|
+
// Particles near BH edge curve around it
|
|
556
|
+
const edgeProximity = currentR / this.bhRadius;
|
|
557
|
+
|
|
558
|
+
if (edgeProximity < 2.5) {
|
|
559
|
+
// Strong bending near the edge - pushes particles outward and around
|
|
560
|
+
const bendStrength =
|
|
561
|
+
Math.exp(-edgeProximity * 0.8) * lensingStrength;
|
|
562
|
+
const pushOut = 1 + bendStrength * 0.4;
|
|
563
|
+
|
|
564
|
+
// Also curve around - displace perpendicular to radius
|
|
565
|
+
const angle = Math.atan2(yCam, xCam);
|
|
566
|
+
const curvature = bendStrength * 0.3 * Math.sign(yCam || 1);
|
|
567
|
+
|
|
568
|
+
xCam =
|
|
569
|
+
xCam * pushOut +
|
|
570
|
+
Math.cos(angle + Math.PI / 2) * curvature * this.bhRadius;
|
|
571
|
+
yCam =
|
|
572
|
+
yCam * pushOut +
|
|
573
|
+
Math.sin(angle + Math.PI / 2) * curvature * this.bhRadius;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// OCCLUSION: Cull particles behind BH that project inside the shadow
|
|
579
|
+
const finalDist = Math.sqrt(xCam * xCam + yCam * yCam);
|
|
580
|
+
if (zCam > 0 && finalDist < this.bhRadius * 0.95) continue;
|
|
581
|
+
|
|
582
|
+
// Perspective projection
|
|
583
|
+
const perspectiveScale =
|
|
584
|
+
this.camera.perspective / (this.camera.perspective + zCam);
|
|
585
|
+
const screenX = xCam * perspectiveScale;
|
|
586
|
+
const screenY = yCam * perspectiveScale;
|
|
587
|
+
|
|
588
|
+
// Cull particles behind camera
|
|
589
|
+
if (zCam < -this.camera.perspective + 10) continue;
|
|
590
|
+
|
|
591
|
+
// Doppler effect
|
|
592
|
+
const velocityDir = Math.cos(p.angle + this.camera.rotationY);
|
|
593
|
+
const doppler = 1 + velocityDir * 0.4;
|
|
594
|
+
|
|
595
|
+
renderList.push({
|
|
596
|
+
z: zCam,
|
|
597
|
+
x: screenX,
|
|
598
|
+
y: screenY,
|
|
599
|
+
scale: perspectiveScale,
|
|
600
|
+
color: p.baseColor,
|
|
601
|
+
doppler: doppler,
|
|
602
|
+
size: p.size,
|
|
603
|
+
isFalling: p.willFallIn,
|
|
604
|
+
horizonProximity: p.distance / this.bhRadius,
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Add permanent accretion disk particles (if any exist)
|
|
609
|
+
for (const p of this.diskParticles) {
|
|
610
|
+
if (p.consumed) continue;
|
|
611
|
+
|
|
612
|
+
// Convert polar to Cartesian
|
|
613
|
+
const px = Math.cos(p.angle) * p.distance;
|
|
614
|
+
const pz = Math.sin(p.angle) * p.distance;
|
|
615
|
+
const py = p.yOffset;
|
|
616
|
+
|
|
617
|
+
// Transform to camera space
|
|
618
|
+
const cosY = Math.cos(this.camera.rotationY);
|
|
619
|
+
const sinY = Math.sin(this.camera.rotationY);
|
|
620
|
+
let xCam = px * cosY - pz * sinY;
|
|
621
|
+
let zCam = px * sinY + pz * cosY;
|
|
622
|
+
|
|
623
|
+
const cosX = Math.cos(this.camera.rotationX);
|
|
624
|
+
const sinX = Math.sin(this.camera.rotationX);
|
|
625
|
+
let yCam = py * cosX - zCam * sinX;
|
|
626
|
+
zCam = py * sinX + zCam * cosX;
|
|
627
|
+
|
|
628
|
+
// Apply gravitational lensing
|
|
629
|
+
if (lensingStrength > 0) {
|
|
630
|
+
const rSq = xCam * xCam + yCam * yCam;
|
|
631
|
+
const currentR = Math.sqrt(rSq);
|
|
632
|
+
const ringRadius = this.bhRadius * 1.5; // Approx Einstein ring radius
|
|
633
|
+
|
|
634
|
+
if (zCam > 0) {
|
|
635
|
+
// BEHIND THE BLACK HOLE (The "Halo")
|
|
636
|
+
// Light from the back of the disk is bent around the BH.
|
|
637
|
+
// We see it as a halo/ring surrounding the shadow.
|
|
638
|
+
|
|
639
|
+
// Warp factor: increases as we get closer to the center axis
|
|
640
|
+
// This pushes the image OUTWARD towards the Einstein ring radius
|
|
641
|
+
const distToRing = Math.abs(currentR - ringRadius);
|
|
642
|
+
|
|
643
|
+
// Simple geometric warp for the "Interstellar" look:
|
|
644
|
+
// If we are behind, we map the coordinates to the ring radius vertically.
|
|
645
|
+
|
|
646
|
+
// Strength of the warp depends on how close we are to the BH shadow
|
|
647
|
+
// Particles directly behind (small currentR) get pushed to the ring.
|
|
648
|
+
if (currentR < ringRadius * 2) {
|
|
649
|
+
const t = Math.max(0, 1 - currentR / (ringRadius * 2.5));
|
|
650
|
+
|
|
651
|
+
// Push Y towards the ring edge, preserving sign
|
|
652
|
+
// This creates the "arch" over and under the shadow
|
|
653
|
+
const targetY = (yCam > 0 ? 1 : -1) * Math.sqrt(Math.max(0, ringRadius*ringRadius - xCam*xCam * 0.5));
|
|
654
|
+
|
|
655
|
+
// Blend between original position and warped position
|
|
656
|
+
// Stronger blend near the center (high t)
|
|
657
|
+
const blend = t * t * lensingStrength * 0.9;
|
|
658
|
+
yCam = yCam * (1 - blend) + targetY * blend;
|
|
659
|
+
|
|
660
|
+
// Also slight radial expansion
|
|
661
|
+
const radialPush = 1 + t * 0.5 * lensingStrength;
|
|
662
|
+
xCam *= radialPush;
|
|
663
|
+
yCam *= radialPush;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
} else if (currentR > 0 && currentR < this.bhRadius * 3) {
|
|
667
|
+
// FRONT OF BLACK HOLE (Accretion Disk proper)
|
|
668
|
+
// Light is still bent, but less dramatically.
|
|
669
|
+
// Main effect is slight apparent magnification and distortion near the shadow.
|
|
670
|
+
|
|
671
|
+
const edgeProximity = currentR / this.bhRadius;
|
|
672
|
+
if (edgeProximity < 3.0) {
|
|
673
|
+
// Warp space slightly near the event horizon
|
|
674
|
+
const warp = 1.0 + Math.exp(-edgeProximity * 2.0) * 0.2 * lensingStrength;
|
|
675
|
+
xCam *= warp;
|
|
676
|
+
yCam *= warp;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// OCCLUSION: Cull particles that end up inside the Event Horizon shadow
|
|
682
|
+
// The shadow is slightly larger than the BH radius due to light capture
|
|
683
|
+
const finalRSq = xCam * xCam + yCam * yCam;
|
|
684
|
+
const shadowRadiusSq = (this.bhRadius * 0.9) ** 2; // 0.9 to allow slight overlap/glow
|
|
685
|
+
|
|
686
|
+
// If behind (z>0) AND projected inside shadow, it's occluded
|
|
687
|
+
if (zCam > 0 && finalRSq < shadowRadiusSq) continue;
|
|
688
|
+
|
|
689
|
+
// If in front (z<0) AND inside, it might be blocking the view (but we usually draw on top)
|
|
690
|
+
// Actually, particles *in front* should be visible even if projected on the black circle,
|
|
691
|
+
// because they are between us and the BH.
|
|
692
|
+
// BUT if they physically fell in (r < radius), they are gone.
|
|
693
|
+
// Our physics handles physical consumption. This check is purely for visual occlusion of background.
|
|
694
|
+
|
|
695
|
+
// Perspective projection
|
|
696
|
+
const perspectiveScale =
|
|
697
|
+
this.camera.perspective / (this.camera.perspective + zCam);
|
|
698
|
+
const screenX = xCam * perspectiveScale;
|
|
699
|
+
const screenY = yCam * perspectiveScale;
|
|
700
|
+
|
|
701
|
+
if (zCam < -this.camera.perspective + 10) continue;
|
|
702
|
+
|
|
703
|
+
// Doppler effect
|
|
704
|
+
const velocityDir = Math.cos(p.angle + this.camera.rotationY);
|
|
705
|
+
const doppler = 1 + velocityDir * 0.4;
|
|
706
|
+
|
|
707
|
+
renderList.push({
|
|
708
|
+
z: zCam,
|
|
709
|
+
x: screenX,
|
|
710
|
+
y: screenY,
|
|
711
|
+
scale: perspectiveScale,
|
|
712
|
+
color: p.baseColor,
|
|
713
|
+
doppler: doppler,
|
|
714
|
+
size: p.size,
|
|
715
|
+
isFalling: p.isFalling,
|
|
716
|
+
horizonProximity: p.distance / this.bhRadius,
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
return renderList;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Draw debris particles with lensing.
|
|
725
|
+
* Uses Painter.useCtx() for direct canvas drawing.
|
|
726
|
+
*/
|
|
727
|
+
draw() {
|
|
728
|
+
const renderList = this.buildRenderList();
|
|
729
|
+
if (renderList.length === 0) return;
|
|
730
|
+
|
|
731
|
+
// Sort by z for proper depth
|
|
732
|
+
renderList.sort((a, b) => b.z - a.z);
|
|
733
|
+
|
|
734
|
+
const cx = this.game.width / 2;
|
|
735
|
+
const cy = this.game.height / 2;
|
|
736
|
+
|
|
737
|
+
Painter.useCtx((ctx) => {
|
|
738
|
+
// Use additive blending for glowing plasma effect
|
|
739
|
+
ctx.globalCompositeOperation = "screen";
|
|
740
|
+
|
|
741
|
+
// Render each particle
|
|
742
|
+
for (const item of renderList) {
|
|
743
|
+
const screenX = cx + item.x;
|
|
744
|
+
const screenY = cy + item.y;
|
|
745
|
+
|
|
746
|
+
// Particle size based on type
|
|
747
|
+
// Larger base size for better blending
|
|
748
|
+
const baseSize = Math.max(1.5, item.size * item.scale * 1.5);
|
|
749
|
+
const size = item.isFalling ? baseSize * 0.4 : baseSize * 0.5;
|
|
750
|
+
|
|
751
|
+
// Apply doppler effect to color
|
|
752
|
+
const color = item.color;
|
|
753
|
+
const dopplerBoost = item.doppler; // 0.6 to 1.4
|
|
754
|
+
|
|
755
|
+
// Enhance doppler contrast - brighter approaching, dimmer receding
|
|
756
|
+
const boost = Math.pow(dopplerBoost, 1.5);
|
|
757
|
+
|
|
758
|
+
const r = Math.min(255, Math.round(color.r * boost));
|
|
759
|
+
const g = Math.min(255, Math.round(color.g * boost));
|
|
760
|
+
const b = Math.min(255, Math.round(color.b * boost));
|
|
761
|
+
|
|
762
|
+
// Falling particles are brighter
|
|
763
|
+
const alpha = item.isFalling
|
|
764
|
+
? Math.min(1, color.a * 1.5)
|
|
765
|
+
: color.a * 0.8;
|
|
766
|
+
|
|
767
|
+
// Brighter glow for particles near the horizon or falling
|
|
768
|
+
const horizonGlow =
|
|
769
|
+
item.horizonProximity < 2 ? (2 - item.horizonProximity) * 0.5 : 0;
|
|
770
|
+
|
|
771
|
+
// Draw particle
|
|
772
|
+
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
773
|
+
|
|
774
|
+
// Only use shadowBlur for very bright particles to save performance
|
|
775
|
+
// and create "hot spots"
|
|
776
|
+
if (item.isFalling || horizonGlow > 0.3) {
|
|
777
|
+
ctx.shadowColor = `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
778
|
+
ctx.shadowBlur = size * 2;
|
|
779
|
+
} else {
|
|
780
|
+
ctx.shadowBlur = 0;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
ctx.beginPath();
|
|
784
|
+
ctx.arc(screenX, screenY, size, 0, Math.PI * 2);
|
|
785
|
+
ctx.fill();
|
|
786
|
+
}
|
|
787
|
+
// Reset composite operation
|
|
788
|
+
ctx.globalCompositeOperation = "source-over";
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
}
|