@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
package/demos/js/kerr.js
ADDED
|
@@ -0,0 +1,1556 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kerr Metric - Rotating Black Hole Demo
|
|
3
|
+
*
|
|
4
|
+
* Visualization of the Kerr solution to Einstein's field equations.
|
|
5
|
+
* Shows frame dragging, ergosphere, and the non-diagonal metric tensor.
|
|
6
|
+
*
|
|
7
|
+
* Key difference from Schwarzschild: g_tφ ≠ 0 (frame dragging term)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Game, Painter, Camera3D } from "../../src/index.js";
|
|
11
|
+
import { GameObject } from "../../src/game/objects/go.js";
|
|
12
|
+
import { Rectangle } from "../../src/shapes/rect.js";
|
|
13
|
+
import { TextShape } from "../../src/shapes/text.js";
|
|
14
|
+
import { Position } from "../../src/util/position.js";
|
|
15
|
+
import { Tensor } from "../../src/math/tensor.js";
|
|
16
|
+
import { flammEmbeddingHeight } from "../../src/math/gr.js";
|
|
17
|
+
import {
|
|
18
|
+
keplerianOmega,
|
|
19
|
+
kerrPrecessionRate,
|
|
20
|
+
orbitalRadiusSimple,
|
|
21
|
+
updateTrail,
|
|
22
|
+
} from "../../src/math/orbital.js";
|
|
23
|
+
import { verticalLayout, applyLayout } from "../../src/util/layout.js";
|
|
24
|
+
import { Tooltip } from "../../src/game/ui/tooltip.js";
|
|
25
|
+
import { Button } from "../../src/game/ui/button.js";
|
|
26
|
+
|
|
27
|
+
// Configuration
|
|
28
|
+
const CONFIG = {
|
|
29
|
+
// Grid parameters - FULLSCREEN
|
|
30
|
+
gridSize: 20,
|
|
31
|
+
gridResolution: 100, // Dense grid for full coverage
|
|
32
|
+
baseGridScale: 12,
|
|
33
|
+
|
|
34
|
+
// Mobile breakpoint
|
|
35
|
+
mobileWidth: 600,
|
|
36
|
+
|
|
37
|
+
// Physics (geometrized units: G = c = 1)
|
|
38
|
+
defaultMass: 1.0,
|
|
39
|
+
defaultSpin: 0.7, // 70% of extremal
|
|
40
|
+
massRange: [1.0, 3.0],
|
|
41
|
+
spinRange: [0.1, 0.95], // As fraction of M
|
|
42
|
+
|
|
43
|
+
// Embedding diagram - visible funnel depth (matches Schwarzschild)
|
|
44
|
+
embeddingScale: 180, // Consistent with Schwarzschild
|
|
45
|
+
|
|
46
|
+
// 3D view - tilted to see frame dragging twist
|
|
47
|
+
rotationX: 0.5, // Slightly less tilt to see more of the surface
|
|
48
|
+
rotationY: 0.4,
|
|
49
|
+
perspective: 900, // Bit more perspective for drama
|
|
50
|
+
|
|
51
|
+
// Orbit parameters
|
|
52
|
+
orbitSemiMajor: 10,
|
|
53
|
+
orbitEccentricity: 0.15,
|
|
54
|
+
angularMomentum: 4.0,
|
|
55
|
+
|
|
56
|
+
// Animation
|
|
57
|
+
autoRotateSpeed: 0.1,
|
|
58
|
+
orbitSpeed: 0.5,
|
|
59
|
+
precessionFactor: 0.15,
|
|
60
|
+
frameDraggingAmplification: 3.0, // Visual enhancement
|
|
61
|
+
|
|
62
|
+
// Formation animation (λ: 0→1 interpolation from flat to Kerr)
|
|
63
|
+
// Slow enough for users to notice the transformation
|
|
64
|
+
formationDuration: 6.0, // Seconds to form the black hole
|
|
65
|
+
formationEasing: 0.3, // Easing factor for smooth transition
|
|
66
|
+
|
|
67
|
+
// Visual exaggeration for user understanding (rubber sheet analogy)
|
|
68
|
+
// These values are NOT physically accurate - intentionally amplified
|
|
69
|
+
frameDraggingReach: 3.0, // How far frame dragging visually extends (multiplier)
|
|
70
|
+
frameDraggingStrength: 40, // INCREASED from 25 for stronger twist
|
|
71
|
+
blackHoleSizeBase: 12, // Base visual size of black hole
|
|
72
|
+
blackHoleSizeMassScale: 10, // How much mass affects visual size (more dramatic)
|
|
73
|
+
|
|
74
|
+
// Colors
|
|
75
|
+
gridColor: "rgba(0, 180, 255, 0.3)",
|
|
76
|
+
gridHighlight: "rgba(100, 220, 255, 0.5)",
|
|
77
|
+
outerHorizonColor: "rgba(255, 50, 50, 0.8)",
|
|
78
|
+
innerHorizonColor: "rgba(200, 50, 100, 0.6)",
|
|
79
|
+
ergosphereColor: "rgba(255, 150, 0, 0.7)",
|
|
80
|
+
progradeISCOColor: "rgba(50, 255, 150, 0.6)",
|
|
81
|
+
retrogradeISCOColor: "rgba(100, 150, 255, 0.6)",
|
|
82
|
+
frameDragColor: "rgba(255, 200, 100, 0.5)",
|
|
83
|
+
orbiterColor: "#4af",
|
|
84
|
+
orbiterGlow: "rgba(100, 180, 255, 0.6)",
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* KerrMetricPanelGO - Displays the Kerr metric tensor components
|
|
89
|
+
* Highlights the off-diagonal g_tφ frame dragging term
|
|
90
|
+
* Responsive for mobile screens
|
|
91
|
+
*/
|
|
92
|
+
class KerrMetricPanelGO extends GameObject {
|
|
93
|
+
constructor(game, options = {}) {
|
|
94
|
+
// Responsive sizing
|
|
95
|
+
const isMobile = game.width < CONFIG.mobileWidth;
|
|
96
|
+
const panelWidth = isMobile ? 260 : 260;
|
|
97
|
+
const panelHeight = isMobile ? 300 : 280;
|
|
98
|
+
const lineHeight = isMobile ? 12 : 14;
|
|
99
|
+
const valueOffset = isMobile ? 140 : 180;
|
|
100
|
+
|
|
101
|
+
super(game, {
|
|
102
|
+
...options,
|
|
103
|
+
width: panelWidth,
|
|
104
|
+
height: panelHeight,
|
|
105
|
+
anchor: Position.BOTTOM_LEFT,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
this.bgRect = new Rectangle({
|
|
109
|
+
width: panelWidth,
|
|
110
|
+
height: panelHeight,
|
|
111
|
+
color: "rgba(0, 0, 0, 0.7)",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Define features with descriptions for tooltips
|
|
115
|
+
this.features = {
|
|
116
|
+
title: {
|
|
117
|
+
text: "Kerr Metric (Rotating Black Hole)",
|
|
118
|
+
font: "bold 12px monospace",
|
|
119
|
+
color: "#f7a",
|
|
120
|
+
height: lineHeight + 4,
|
|
121
|
+
desc: "The Kerr metric describes spacetime around a rotating black hole.\n\nKerr is STATIONARY - it doesn't evolve over time. This animation shows geometric interpolation from flat to Kerr.\n\nNOTE: Visual effects are EXAGGERATED (like rubber sheet analogy) to make curvature and frame dragging easier to see.",
|
|
122
|
+
},
|
|
123
|
+
equation: {
|
|
124
|
+
text: "ds² = gμν dxμ dxν (Boyer-Lindquist)",
|
|
125
|
+
font: "12px monospace",
|
|
126
|
+
color: "#888",
|
|
127
|
+
height: lineHeight,
|
|
128
|
+
desc: "Boyer-Lindquist coordinates (t, r, θ, φ) generalize Schwarzschild coordinates for rotating spacetime.",
|
|
129
|
+
},
|
|
130
|
+
mass: {
|
|
131
|
+
text: "M = 1.00",
|
|
132
|
+
font: "11px monospace",
|
|
133
|
+
color: "#888",
|
|
134
|
+
height: lineHeight,
|
|
135
|
+
desc: "Mass of the black hole in geometrized units (G = c = 1).",
|
|
136
|
+
},
|
|
137
|
+
spin: {
|
|
138
|
+
text: "a = 0.70M (70%)",
|
|
139
|
+
font: "bold 11px monospace",
|
|
140
|
+
color: "#fa8",
|
|
141
|
+
height: lineHeight + 4,
|
|
142
|
+
desc: "Spin parameter a = J/Mc (angular momentum per unit mass).\n\n0 = Schwarzschild (no rotation)\nM = Extremal Kerr (maximum spin)\n\nClick to randomize!",
|
|
143
|
+
},
|
|
144
|
+
gtt: {
|
|
145
|
+
text: "g_tt = -(1 - 2Mr/Σ)",
|
|
146
|
+
font: "10px monospace",
|
|
147
|
+
color: "#f88",
|
|
148
|
+
height: lineHeight,
|
|
149
|
+
value: "= -0.800",
|
|
150
|
+
desc: "Time-time component. Modified by Σ = r² + a²cos²θ.\nDepends on BOTH r and θ (not spherically symmetric!).",
|
|
151
|
+
},
|
|
152
|
+
grr: {
|
|
153
|
+
text: "g_rr = Σ/Δ",
|
|
154
|
+
font: "10px monospace",
|
|
155
|
+
color: "#8f8",
|
|
156
|
+
height: lineHeight,
|
|
157
|
+
value: "= 1.250",
|
|
158
|
+
desc: "Radial component. Δ = r² - 2Mr + a².\nDiverges at horizons where Δ = 0.",
|
|
159
|
+
},
|
|
160
|
+
gthth: {
|
|
161
|
+
text: "g_θθ = Σ",
|
|
162
|
+
font: "10px monospace",
|
|
163
|
+
color: "#88f",
|
|
164
|
+
height: lineHeight,
|
|
165
|
+
value: "= 100.00",
|
|
166
|
+
desc: "Theta component. Σ = r² + a²cos²θ.\nNot just r² - rotation breaks spherical symmetry.",
|
|
167
|
+
},
|
|
168
|
+
gphph: {
|
|
169
|
+
text: "g_φφ = (r²+a²+...)sin²θ",
|
|
170
|
+
font: "10px monospace",
|
|
171
|
+
color: "#f8f",
|
|
172
|
+
height: lineHeight,
|
|
173
|
+
value: "= 100.00",
|
|
174
|
+
desc: "Phi component. More complex than Schwarzschild.\nIncludes 2Ma²r sin²θ/Σ rotation term.",
|
|
175
|
+
},
|
|
176
|
+
gtph: {
|
|
177
|
+
text: "g_tφ = -2Mar sin²θ/Σ",
|
|
178
|
+
font: "bold 11px monospace",
|
|
179
|
+
color: "#ff0",
|
|
180
|
+
height: lineHeight + 6,
|
|
181
|
+
value: "= -0.180",
|
|
182
|
+
desc: "FRAME DRAGGING TERM\n\nThis off-diagonal component is THE key difference!\n\nIt couples time and rotation: even light must rotate with the black hole.\n\nInside the ergosphere, NOTHING can stay still.",
|
|
183
|
+
},
|
|
184
|
+
rplus: {
|
|
185
|
+
text: "r+ = 1.44",
|
|
186
|
+
font: "10px monospace",
|
|
187
|
+
color: "#f55",
|
|
188
|
+
height: lineHeight - 2,
|
|
189
|
+
desc: "Outer Event Horizon: r+ = M + √(M² - a²)\nSmaller than Schwarzschild 2M when spinning.\nApproaches M as a → M (extremal).",
|
|
190
|
+
},
|
|
191
|
+
rminus: {
|
|
192
|
+
text: "r- = 0.56",
|
|
193
|
+
font: "10px monospace",
|
|
194
|
+
color: "#a55",
|
|
195
|
+
height: lineHeight - 2,
|
|
196
|
+
desc: "Inner (Cauchy) Horizon: r- = M - √(M² - a²)\nUnique to rotating black holes.\nHides a ring singularity, not a point.",
|
|
197
|
+
},
|
|
198
|
+
rergo: {
|
|
199
|
+
text: "r_ergo = 2.00",
|
|
200
|
+
font: "10px monospace",
|
|
201
|
+
color: "#f80",
|
|
202
|
+
height: lineHeight - 2,
|
|
203
|
+
desc: "Ergosphere boundary (at equator)\nBetween r+ and r_ergo: the ergosphere.\nObjects can escape, but CANNOT stay stationary!",
|
|
204
|
+
},
|
|
205
|
+
riscoP: {
|
|
206
|
+
text: "r_ISCO(pro) = 2.32",
|
|
207
|
+
font: "10px monospace",
|
|
208
|
+
color: "#5f8",
|
|
209
|
+
height: lineHeight - 2,
|
|
210
|
+
desc: "ISCO for prograde (co-rotating) orbits.\nCloser than Schwarzschild ISCO!\nFrame dragging helps co-rotating orbits.",
|
|
211
|
+
},
|
|
212
|
+
riscoR: {
|
|
213
|
+
text: "r_ISCO(retro) = 8.71",
|
|
214
|
+
font: "10px monospace",
|
|
215
|
+
color: "#58f",
|
|
216
|
+
height: lineHeight - 2,
|
|
217
|
+
desc: "ISCO for retrograde (counter-rotating) orbits.\nFarther than Schwarzschild ISCO!\nFrame dragging opposes counter-rotation.",
|
|
218
|
+
},
|
|
219
|
+
pos: {
|
|
220
|
+
text: "Orbiter: r=10, Ω_drag=0.02",
|
|
221
|
+
font: "10px monospace",
|
|
222
|
+
color: "#aaa",
|
|
223
|
+
height: lineHeight,
|
|
224
|
+
desc: "Orbiter position and local frame-dragging rate.\nΩ_drag shows how fast spacetime rotates here.",
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
this.panelWidth = panelWidth;
|
|
229
|
+
this.panelHeight = panelHeight;
|
|
230
|
+
|
|
231
|
+
// Create TextShapes
|
|
232
|
+
const rowItems = [];
|
|
233
|
+
for (const [key, config] of Object.entries(this.features)) {
|
|
234
|
+
config.shape = new TextShape(config.text, {
|
|
235
|
+
font: config.font,
|
|
236
|
+
color: config.color,
|
|
237
|
+
align: "left",
|
|
238
|
+
baseline: "top",
|
|
239
|
+
height: config.height,
|
|
240
|
+
});
|
|
241
|
+
rowItems.push(config.shape);
|
|
242
|
+
|
|
243
|
+
if (config.value) {
|
|
244
|
+
config.valueShape = new TextShape(config.value, {
|
|
245
|
+
font: config.font,
|
|
246
|
+
color: "#fff",
|
|
247
|
+
align: "left",
|
|
248
|
+
baseline: "top",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Apply vertical layout
|
|
254
|
+
const layout = verticalLayout(rowItems, {
|
|
255
|
+
spacing: 10,
|
|
256
|
+
padding: 0,
|
|
257
|
+
align: "start",
|
|
258
|
+
centerItems: false,
|
|
259
|
+
});
|
|
260
|
+
applyLayout(rowItems, layout.positions, {
|
|
261
|
+
offsetX: -panelWidth / 2,
|
|
262
|
+
offsetY: -panelHeight / 2,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Position value shapes
|
|
266
|
+
for (const config of Object.values(this.features)) {
|
|
267
|
+
if (config.valueShape) {
|
|
268
|
+
config.valueShape.x = config.shape.x + valueOffset;
|
|
269
|
+
config.valueShape.y = config.shape.y;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
setMetricValues(r, theta, M, a) {
|
|
275
|
+
const metric = Tensor.kerr(r, theta, M, a);
|
|
276
|
+
const f = this.features;
|
|
277
|
+
|
|
278
|
+
// Diagonal components
|
|
279
|
+
f.gtt.valueShape.text = `= ${metric.get(0, 0).toFixed(4)}`;
|
|
280
|
+
f.grr.valueShape.text = `= ${metric.get(1, 1).toFixed(4)}`;
|
|
281
|
+
f.gthth.valueShape.text = `= ${metric.get(2, 2).toFixed(2)}`;
|
|
282
|
+
f.gphph.valueShape.text = `= ${metric.get(3, 3).toFixed(2)}`;
|
|
283
|
+
|
|
284
|
+
// OFF-DIAGONAL (the key term!)
|
|
285
|
+
f.gtph.valueShape.text = `= ${metric.get(0, 3).toFixed(4)}`;
|
|
286
|
+
|
|
287
|
+
// Parameters
|
|
288
|
+
const spinPercent = ((a / M) * 100).toFixed(0);
|
|
289
|
+
f.spin.shape.text = `a = ${a.toFixed(2)}M (${spinPercent}%)`;
|
|
290
|
+
f.mass.shape.text = `M = ${M.toFixed(2)}`;
|
|
291
|
+
|
|
292
|
+
// Key radii
|
|
293
|
+
const rPlus = Tensor.kerrHorizonRadius(M, a, false);
|
|
294
|
+
const rMinus = Tensor.kerrHorizonRadius(M, a, true);
|
|
295
|
+
const rErgo = Tensor.kerrErgosphereRadius(M, a, Math.PI / 2);
|
|
296
|
+
const iscoP = Tensor.kerrISCO(M, a, true);
|
|
297
|
+
const iscoR = Tensor.kerrISCO(M, a, false);
|
|
298
|
+
|
|
299
|
+
f.rplus.shape.text = `r+ = ${rPlus.toFixed(2)}`;
|
|
300
|
+
f.rminus.shape.text = `r- = ${rMinus.toFixed(2)}`;
|
|
301
|
+
f.rergo.shape.text = `r_ergo = ${rErgo.toFixed(2)}`;
|
|
302
|
+
f.riscoP.shape.text = `r_ISCO(pro) = ${iscoP.toFixed(2)}`;
|
|
303
|
+
f.riscoR.shape.text = `r_ISCO(retro) = ${iscoR.toFixed(2)}`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
setOrbiterPosition(r, phi, M, a) {
|
|
307
|
+
const omega = Tensor.kerrFrameDraggingOmega(r, Math.PI / 2, M, a);
|
|
308
|
+
this.features.pos.shape.text = `Orbiter: r=${r.toFixed(2)}, Ω_drag=${omega.toFixed(4)}`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
getFeatureAt(screenX, screenY) {
|
|
312
|
+
const localX = screenX - this.x;
|
|
313
|
+
const localY = screenY - this.y;
|
|
314
|
+
|
|
315
|
+
if (
|
|
316
|
+
localX < -this.panelWidth / 2 ||
|
|
317
|
+
localX > this.panelWidth / 2 ||
|
|
318
|
+
localY < -this.panelHeight / 2 ||
|
|
319
|
+
localY > this.panelHeight / 2
|
|
320
|
+
) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const config of Object.values(this.features)) {
|
|
325
|
+
const shape = config.shape;
|
|
326
|
+
const rowTop = shape.y;
|
|
327
|
+
const rowBottom = shape.y + (config.height || 14);
|
|
328
|
+
|
|
329
|
+
if (localY >= rowTop && localY <= rowBottom) {
|
|
330
|
+
return config;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
draw() {
|
|
338
|
+
super.draw();
|
|
339
|
+
this.bgRect.render();
|
|
340
|
+
|
|
341
|
+
for (const config of Object.values(this.features)) {
|
|
342
|
+
config.shape.render();
|
|
343
|
+
if (config.valueShape) config.valueShape.render();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
class KerrDemo extends Game {
|
|
349
|
+
constructor(canvas) {
|
|
350
|
+
super(canvas);
|
|
351
|
+
// Black background - it's space!
|
|
352
|
+
this.backgroundColor = "#000";
|
|
353
|
+
this.enableFluidSize();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
init() {
|
|
357
|
+
super.init();
|
|
358
|
+
this.time = 0;
|
|
359
|
+
|
|
360
|
+
// Mass and spin
|
|
361
|
+
this.mass = CONFIG.defaultMass;
|
|
362
|
+
this.spin = CONFIG.defaultSpin * this.mass;
|
|
363
|
+
|
|
364
|
+
// Grid scale
|
|
365
|
+
this.gridScale = CONFIG.baseGridScale;
|
|
366
|
+
|
|
367
|
+
// Camera with inertia for smooth drag
|
|
368
|
+
this.camera = new Camera3D({
|
|
369
|
+
rotationX: CONFIG.rotationX,
|
|
370
|
+
rotationY: CONFIG.rotationY,
|
|
371
|
+
perspective: CONFIG.perspective,
|
|
372
|
+
minRotationX: -0.5,
|
|
373
|
+
maxRotationX: 1.5,
|
|
374
|
+
autoRotate: true,
|
|
375
|
+
autoRotateSpeed: CONFIG.autoRotateSpeed,
|
|
376
|
+
autoRotateAxis: "y",
|
|
377
|
+
inertia: true,
|
|
378
|
+
friction: 0.95,
|
|
379
|
+
velocityScale: 2.0,
|
|
380
|
+
});
|
|
381
|
+
this.camera.enableMouseControl(this.canvas);
|
|
382
|
+
|
|
383
|
+
// Orbital state
|
|
384
|
+
this.orbitR = CONFIG.orbitSemiMajor;
|
|
385
|
+
this.orbitPhi = 0;
|
|
386
|
+
this.orbitVr = 0;
|
|
387
|
+
this.orbitL = CONFIG.angularMomentum;
|
|
388
|
+
this.precessionAngle = 0;
|
|
389
|
+
this.orbitTrail = [];
|
|
390
|
+
|
|
391
|
+
// Formation parameter λ: interpolates from flat (0) to Kerr (1)
|
|
392
|
+
// This is NOT physical time - it's a geometric interpolation parameter
|
|
393
|
+
// representing the cumulative effects during black hole formation
|
|
394
|
+
this.formationProgress = 0; // λ ∈ [0, 1]
|
|
395
|
+
|
|
396
|
+
// Initialize grid
|
|
397
|
+
this.initGrid();
|
|
398
|
+
this.updateGridScale();
|
|
399
|
+
|
|
400
|
+
// Create metric panel
|
|
401
|
+
this.metricPanel = new KerrMetricPanelGO(this, { name: "metricPanel" });
|
|
402
|
+
this.pipeline.add(this.metricPanel);
|
|
403
|
+
|
|
404
|
+
// Tooltip (responsive)
|
|
405
|
+
const isMobileTooltip = this.width < CONFIG.mobileWidth;
|
|
406
|
+
this.tooltip = new Tooltip(this, {
|
|
407
|
+
maxWidth: isMobileTooltip ? 200 : 300,
|
|
408
|
+
font: `${isMobileTooltip ? 9 : 11}px monospace`,
|
|
409
|
+
padding: isMobileTooltip ? 6 : 10,
|
|
410
|
+
bgColor: "rgba(20, 20, 30, 0.95)",
|
|
411
|
+
});
|
|
412
|
+
this.pipeline.add(this.tooltip);
|
|
413
|
+
|
|
414
|
+
this.hoveredFeature = null;
|
|
415
|
+
|
|
416
|
+
// Event listeners
|
|
417
|
+
this.canvas.addEventListener("mousemove", (e) => this.handleMouseMove(e));
|
|
418
|
+
this.canvas.addEventListener("mouseleave", () => this.tooltip.hide());
|
|
419
|
+
this.initControls();
|
|
420
|
+
|
|
421
|
+
// Button to form new black hole (positioned below the chart, same width)
|
|
422
|
+
const isMobile = this.width < CONFIG.mobileWidth;
|
|
423
|
+
const graphW = isMobile ? 120 : 160;
|
|
424
|
+
const graphH = isMobile ? 70 : 100;
|
|
425
|
+
const graphX = this.width - graphW - (isMobile ? 15 : 20);
|
|
426
|
+
const graphY = isMobile ? 80 : 220; // Desktop moved down to avoid info div
|
|
427
|
+
|
|
428
|
+
this.newBlackHoleBtn = new Button(this, {
|
|
429
|
+
anchor: Position.TOP_LEFT,
|
|
430
|
+
anchorRelative: this.metricPanel,
|
|
431
|
+
anchorOffsetX: -10,
|
|
432
|
+
anchorOffsetY: -60,
|
|
433
|
+
width: graphW,
|
|
434
|
+
height: isMobile ? 30 : 36,
|
|
435
|
+
text: "New Black Hole",
|
|
436
|
+
font: `${isMobile ? 10 : 12}px monospace`,
|
|
437
|
+
colorDefaultBg: "rgba(20, 20, 40, 0.8)",
|
|
438
|
+
colorDefaultStroke: "#f80",
|
|
439
|
+
colorDefaultText: "#fa8",
|
|
440
|
+
colorHoverBg: "rgba(40, 30, 60, 0.9)",
|
|
441
|
+
colorHoverStroke: "#ff0",
|
|
442
|
+
colorHoverText: "#ff0",
|
|
443
|
+
colorPressedBg: "rgba(60, 40, 80, 1)",
|
|
444
|
+
colorPressedStroke: "#fff",
|
|
445
|
+
colorPressedText: "#fff",
|
|
446
|
+
onClick: () => this.shuffleParameters(),
|
|
447
|
+
});
|
|
448
|
+
this.pipeline.add(this.newBlackHoleBtn);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
initControls() {
|
|
452
|
+
// Instructions (drag to rotate)
|
|
453
|
+
this.controlsText = new TextShape(
|
|
454
|
+
"drag to rotate",
|
|
455
|
+
{
|
|
456
|
+
font: "10px monospace",
|
|
457
|
+
color: "#aaa",
|
|
458
|
+
align: "right",
|
|
459
|
+
baseline: "bottom",
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// Explanatory text lines
|
|
464
|
+
const explanationLines = [
|
|
465
|
+
"Geometric Demonstration: Flat Spacetime \u2192 Kerr Metric", // Top line
|
|
466
|
+
"Visualizes the structural contrast, not physical time evolution.",
|
|
467
|
+
"Effects exaggerated for visibility.",
|
|
468
|
+
];
|
|
469
|
+
|
|
470
|
+
this.explanationShapes = explanationLines.map((line) => {
|
|
471
|
+
return new TextShape(line, {
|
|
472
|
+
font: "10px monospace",
|
|
473
|
+
color: "#aaa",
|
|
474
|
+
align: "right",
|
|
475
|
+
baseline: "bottom",
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
handleMouseMove(e) {
|
|
481
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
482
|
+
const mouseX = e.clientX - rect.left;
|
|
483
|
+
const mouseY = e.clientY - rect.top;
|
|
484
|
+
|
|
485
|
+
// Check metric panel
|
|
486
|
+
const feature = this.metricPanel.getFeatureAt(mouseX, mouseY);
|
|
487
|
+
if (feature && feature.desc) {
|
|
488
|
+
if (this.hoveredFeature !== feature) {
|
|
489
|
+
this.hoveredFeature = feature;
|
|
490
|
+
this.tooltip.show(feature.desc, mouseX, mouseY);
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Check effective potential graph (responsive)
|
|
496
|
+
const isMobile = this.width < CONFIG.mobileWidth;
|
|
497
|
+
const graphW = isMobile ? 120 : 160;
|
|
498
|
+
const graphH = isMobile ? 70 : 100;
|
|
499
|
+
const graphX = this.width - graphW - (isMobile ? 15 : 20);
|
|
500
|
+
const graphY = isMobile ? 80 : 220; // Desktop moved down to avoid info div
|
|
501
|
+
|
|
502
|
+
if (
|
|
503
|
+
mouseX >= graphX - 10 &&
|
|
504
|
+
mouseX <= graphX + graphW + 10 &&
|
|
505
|
+
mouseY >= graphY - 10 &&
|
|
506
|
+
mouseY <= graphY + graphH + 30
|
|
507
|
+
) {
|
|
508
|
+
if (this.hoveredFeature !== "graph") {
|
|
509
|
+
this.hoveredFeature = "graph";
|
|
510
|
+
this.tooltip.show(
|
|
511
|
+
"Kerr Effective Potential\n\nShows gravitational + centrifugal potential for the current spin.\n\nGreen = prograde ISCO (closer!)\nBlue = retrograde ISCO (farther!)\n\nFrame dragging makes co-rotating orbits more stable.",
|
|
512
|
+
mouseX,
|
|
513
|
+
mouseY,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (this.hoveredFeature) {
|
|
520
|
+
this.hoveredFeature = null;
|
|
521
|
+
this.tooltip.hide();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
initGrid() {
|
|
526
|
+
const { gridSize, gridResolution } = CONFIG;
|
|
527
|
+
this.gridVertices = [];
|
|
528
|
+
|
|
529
|
+
for (let i = 0; i <= gridResolution; i++) {
|
|
530
|
+
const row = [];
|
|
531
|
+
for (let j = 0; j <= gridResolution; j++) {
|
|
532
|
+
const x = (i / gridResolution - 0.5) * 2 * gridSize;
|
|
533
|
+
const z = (j / gridResolution - 0.5) * 2 * gridSize;
|
|
534
|
+
row.push({ x, y: 0, z, baseX: x, baseZ: z }); // Store original positions
|
|
535
|
+
}
|
|
536
|
+
this.gridVertices.push(row);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Initialize dragged particles in ergosphere
|
|
540
|
+
this.draggedParticles = [];
|
|
541
|
+
for (let i = 0; i < 20; i++) {
|
|
542
|
+
const angle = Math.random() * Math.PI * 2;
|
|
543
|
+
const r = 2 + Math.random() * 3; // Between horizon and ergosphere
|
|
544
|
+
this.draggedParticles.push({
|
|
545
|
+
angle,
|
|
546
|
+
r,
|
|
547
|
+
baseR: r,
|
|
548
|
+
phase: Math.random() * Math.PI * 2,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
updateGridScale() {
|
|
554
|
+
// Scale grid to show edges - user can see the fabric boundaries for frame dragging effect
|
|
555
|
+
const minDim = Math.min(this.width, this.height);
|
|
556
|
+
this.gridScale = (minDim / (CONFIG.gridSize * 2)) * 1.5;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
shuffleParameters() {
|
|
560
|
+
// Randomize mass
|
|
561
|
+
this.mass =
|
|
562
|
+
CONFIG.massRange[0] +
|
|
563
|
+
Math.random() * (CONFIG.massRange[1] - CONFIG.massRange[0]);
|
|
564
|
+
|
|
565
|
+
// Randomize spin (as fraction of M)
|
|
566
|
+
const spinFraction =
|
|
567
|
+
CONFIG.spinRange[0] +
|
|
568
|
+
Math.random() * (CONFIG.spinRange[1] - CONFIG.spinRange[0]);
|
|
569
|
+
this.spin = spinFraction * this.mass;
|
|
570
|
+
|
|
571
|
+
// Reset orbit outside prograde ISCO
|
|
572
|
+
const iscoP = Tensor.kerrISCO(this.mass, this.spin, true);
|
|
573
|
+
this.orbitR = iscoP + 2 + Math.random() * 8;
|
|
574
|
+
this.orbitPhi = Math.random() * Math.PI * 2;
|
|
575
|
+
this.orbitL = 3.5 + Math.random() * 2;
|
|
576
|
+
this.precessionAngle = 0;
|
|
577
|
+
this.orbitTrail = [];
|
|
578
|
+
|
|
579
|
+
// Reset formation - grid goes back to flat, then forms into new Kerr
|
|
580
|
+
this.formationProgress = 0;
|
|
581
|
+
this.formationCompleteTime = null; // Reset for new orbiter fade-in
|
|
582
|
+
|
|
583
|
+
// Reset grid to original positions
|
|
584
|
+
const { gridResolution } = CONFIG;
|
|
585
|
+
for (let i = 0; i <= gridResolution; i++) {
|
|
586
|
+
for (let j = 0; j <= gridResolution; j++) {
|
|
587
|
+
const vertex = this.gridVertices[i][j];
|
|
588
|
+
vertex.x = vertex.baseX;
|
|
589
|
+
vertex.z = vertex.baseZ;
|
|
590
|
+
vertex.y = 0;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Reset dragged particles
|
|
595
|
+
if (this.draggedParticles) {
|
|
596
|
+
for (const p of this.draggedParticles) {
|
|
597
|
+
p.angle = Math.random() * Math.PI * 2;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Embedding height for Kerr using shared gr.js module.
|
|
604
|
+
* Uses r+ (outer horizon) instead of 2M for the Kerr case.
|
|
605
|
+
*/
|
|
606
|
+
getEmbeddingHeight(r) {
|
|
607
|
+
const rPlus = Tensor.kerrHorizonRadius(this.mass, this.spin, false);
|
|
608
|
+
return flammEmbeddingHeight(
|
|
609
|
+
r,
|
|
610
|
+
rPlus,
|
|
611
|
+
this.mass,
|
|
612
|
+
CONFIG.gridSize,
|
|
613
|
+
CONFIG.embeddingScale,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Update geodesic motion with frame dragging using orbital.js utilities.
|
|
619
|
+
*/
|
|
620
|
+
updateGeodesic(dt) {
|
|
621
|
+
const M = this.mass;
|
|
622
|
+
const a = this.spin;
|
|
623
|
+
const r = this.orbitR;
|
|
624
|
+
|
|
625
|
+
// Base Keplerian angular velocity
|
|
626
|
+
const baseOmega = keplerianOmega(r, M, CONFIG.orbitSpeed);
|
|
627
|
+
|
|
628
|
+
// Frame dragging contribution (Kerr-specific, stays in Tensor)
|
|
629
|
+
const omegaDrag = Tensor.kerrFrameDraggingOmega(r, Math.PI / 2, M, a);
|
|
630
|
+
|
|
631
|
+
// Total angular velocity (frame dragging adds to prograde motion)
|
|
632
|
+
const totalOmega =
|
|
633
|
+
baseOmega + omegaDrag * CONFIG.frameDraggingAmplification;
|
|
634
|
+
|
|
635
|
+
// Update orbital angle
|
|
636
|
+
this.orbitPhi += totalOmega * dt;
|
|
637
|
+
|
|
638
|
+
// Radial oscillation for eccentricity
|
|
639
|
+
this.orbitR = orbitalRadiusSimple(
|
|
640
|
+
CONFIG.orbitSemiMajor,
|
|
641
|
+
CONFIG.orbitEccentricity,
|
|
642
|
+
this.orbitPhi,
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
// Keep orbit outside prograde ISCO
|
|
646
|
+
const minR = Tensor.kerrISCO(M, a, true) + 1;
|
|
647
|
+
if (this.orbitR < minR) this.orbitR = minR;
|
|
648
|
+
|
|
649
|
+
// GR precession (enhanced by frame dragging)
|
|
650
|
+
const precessionRate = kerrPrecessionRate(r, M, a, CONFIG.precessionFactor);
|
|
651
|
+
this.precessionAngle += precessionRate * dt;
|
|
652
|
+
|
|
653
|
+
// Store in trail
|
|
654
|
+
const totalAngle = this.orbitPhi + this.precessionAngle;
|
|
655
|
+
updateTrail(
|
|
656
|
+
this.orbitTrail,
|
|
657
|
+
{
|
|
658
|
+
x: Math.cos(totalAngle) * this.orbitR,
|
|
659
|
+
z: Math.sin(totalAngle) * this.orbitR,
|
|
660
|
+
r: this.orbitR,
|
|
661
|
+
omega: omegaDrag,
|
|
662
|
+
},
|
|
663
|
+
80,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
update(dt) {
|
|
668
|
+
super.update(dt);
|
|
669
|
+
this.time += dt;
|
|
670
|
+
|
|
671
|
+
// Formation progress: λ goes from 0 to 1 over formationDuration
|
|
672
|
+
// Once at 1, stays there (Kerr is stationary - the final state)
|
|
673
|
+
const wasForming = this.formationProgress < 1;
|
|
674
|
+
if (this.formationProgress < 1) {
|
|
675
|
+
this.formationProgress += dt / CONFIG.formationDuration;
|
|
676
|
+
if (this.formationProgress >= 1) {
|
|
677
|
+
this.formationProgress = 1;
|
|
678
|
+
// Record when formation completed (for orbiter fade-in)
|
|
679
|
+
this.formationCompleteTime = this.time;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Smooth easing for formation (ease-out cubic)
|
|
684
|
+
const lambda = 1 - Math.pow(1 - this.formationProgress, 3);
|
|
685
|
+
|
|
686
|
+
this.camera.update(dt);
|
|
687
|
+
|
|
688
|
+
// Only update geodesic motion after black hole has formed
|
|
689
|
+
// The orbiter appears after formation completes
|
|
690
|
+
if (this.formationProgress >= 1) {
|
|
691
|
+
this.updateGeodesic(dt);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
this.updateGridScale();
|
|
695
|
+
|
|
696
|
+
// Update grid with Kerr geometry
|
|
697
|
+
// The twist is proportional to λ (formation progress), NOT accumulating over time
|
|
698
|
+
// This shows the FINAL Kerr geometry, not "evolving" spacetime
|
|
699
|
+
const { gridResolution } = CONFIG;
|
|
700
|
+
const M = this.mass;
|
|
701
|
+
const a = this.spin;
|
|
702
|
+
const rPlus = Tensor.kerrHorizonRadius(M, a, false);
|
|
703
|
+
const rErgo = Tensor.kerrErgosphereRadius(M, a, Math.PI / 2);
|
|
704
|
+
|
|
705
|
+
// Extended reach for frame dragging visualization (rubber sheet analogy)
|
|
706
|
+
// Real effect falls off as ~1/r³, but we extend it for visual clarity
|
|
707
|
+
const visualReach = rErgo * CONFIG.frameDraggingReach;
|
|
708
|
+
|
|
709
|
+
for (let i = 0; i <= gridResolution; i++) {
|
|
710
|
+
for (let j = 0; j <= gridResolution; j++) {
|
|
711
|
+
const vertex = this.gridVertices[i][j];
|
|
712
|
+
const baseR = Math.sqrt(
|
|
713
|
+
vertex.baseX * vertex.baseX + vertex.baseZ * vertex.baseZ,
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
// Frame dragging twist proportional to formation progress (λ)
|
|
717
|
+
// At λ=0: flat spacetime, no twist
|
|
718
|
+
// At λ=1: full Kerr geometry with frame dragging
|
|
719
|
+
// INTENTIONALLY EXAGGERATED: extends beyond physical ergosphere for visibility
|
|
720
|
+
if (baseR > rPlus * 0.5 && baseR < visualReach) {
|
|
721
|
+
const omega = Tensor.kerrFrameDraggingOmega(
|
|
722
|
+
Math.max(baseR, rPlus + 0.1),
|
|
723
|
+
Math.PI / 2,
|
|
724
|
+
M,
|
|
725
|
+
a,
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
// Visual falloff: smooth transition from max twist near horizon to zero at visualReach
|
|
729
|
+
// Uses quadratic falloff for smoother visual effect
|
|
730
|
+
const proximityFactor =
|
|
731
|
+
1 - Math.pow((baseR - rPlus) / (visualReach - rPlus), 2);
|
|
732
|
+
const clampedProximity = Math.max(0, proximityFactor);
|
|
733
|
+
|
|
734
|
+
// Static twist angle - EXAGGERATED for visualization
|
|
735
|
+
const maxTwist = Math.PI / 4; // ~45 degrees max for dramatic effect
|
|
736
|
+
const twistAngle =
|
|
737
|
+
omega * CONFIG.frameDraggingStrength * lambda * clampedProximity;
|
|
738
|
+
const cappedTwist = Math.min(twistAngle, maxTwist);
|
|
739
|
+
|
|
740
|
+
// Apply rotation to grid point
|
|
741
|
+
const cosT = Math.cos(cappedTwist);
|
|
742
|
+
const sinT = Math.sin(cappedTwist);
|
|
743
|
+
vertex.x = vertex.baseX * cosT - vertex.baseZ * sinT;
|
|
744
|
+
vertex.z = vertex.baseX * sinT + vertex.baseZ * cosT;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Embedding depth also scales with λ (flat → curved)
|
|
748
|
+
// Function already clamps at horizon, no need for extra clamp here
|
|
749
|
+
const r = Math.sqrt(vertex.x * vertex.x + vertex.z * vertex.z);
|
|
750
|
+
vertex.y = this.getEmbeddingHeight(r) * lambda;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Update dragged particles in ergosphere
|
|
755
|
+
if (this.draggedParticles) {
|
|
756
|
+
for (const p of this.draggedParticles) {
|
|
757
|
+
// Particles get dragged by frame dragging
|
|
758
|
+
const omega = Tensor.kerrFrameDraggingOmega(p.baseR, Math.PI / 2, M, a);
|
|
759
|
+
p.angle += omega * dt * 50; // Strong visual drag
|
|
760
|
+
// Slight radial oscillation
|
|
761
|
+
p.r = p.baseR + Math.sin(this.time * 2 + p.phase) * 0.3;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Update metric panel
|
|
766
|
+
if (this.metricPanel) {
|
|
767
|
+
// Use EFFECTIVE mass and spin based on formation progress (lambda)
|
|
768
|
+
// This allows the numbers to evolve from Flat Spacetime values to final Kerr values
|
|
769
|
+
// Note: We clamp at a small epsilon for M to avoid division by zero if lambda is 0
|
|
770
|
+
const effectiveM = Math.max(0.001, this.mass * lambda);
|
|
771
|
+
const effectiveA = this.spin * lambda;
|
|
772
|
+
|
|
773
|
+
this.metricPanel.setMetricValues(
|
|
774
|
+
this.orbitR,
|
|
775
|
+
Math.PI / 2,
|
|
776
|
+
effectiveM,
|
|
777
|
+
effectiveA,
|
|
778
|
+
);
|
|
779
|
+
this.metricPanel.setOrbiterPosition(
|
|
780
|
+
this.orbitR,
|
|
781
|
+
this.orbitPhi,
|
|
782
|
+
effectiveM,
|
|
783
|
+
effectiveA,
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
render() {
|
|
789
|
+
const w = this.width;
|
|
790
|
+
const h = this.height;
|
|
791
|
+
const cx = w / 2;
|
|
792
|
+
const cy = h / 2; // Centered to see fabric edges from outside
|
|
793
|
+
|
|
794
|
+
super.render();
|
|
795
|
+
|
|
796
|
+
// Draw key radii (ergosphere, horizons, ISCOs)
|
|
797
|
+
this.drawKeyRadii(cx, cy);
|
|
798
|
+
|
|
799
|
+
// Draw ergosphere fill with dragged particles
|
|
800
|
+
this.drawErgosphere(cx, cy);
|
|
801
|
+
|
|
802
|
+
// Draw grid (now with frame dragging twist!)
|
|
803
|
+
this.drawGrid(cx, cy);
|
|
804
|
+
|
|
805
|
+
// Draw rotating black hole with accretion disk
|
|
806
|
+
this.drawHorizon(cx, cy);
|
|
807
|
+
|
|
808
|
+
// Draw orbiter
|
|
809
|
+
this.drawOrbiter(cx, cy);
|
|
810
|
+
|
|
811
|
+
// Draw effective potential graph
|
|
812
|
+
this.drawEffectivePotential();
|
|
813
|
+
|
|
814
|
+
// Draw formation progress indicator
|
|
815
|
+
this.drawFormationProgress(w, h);
|
|
816
|
+
|
|
817
|
+
// Draw controls
|
|
818
|
+
this.renderControls();
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
renderControls() {
|
|
822
|
+
const w = this.width;
|
|
823
|
+
const h = this.height;
|
|
824
|
+
const isMobile = w < CONFIG.mobileWidth;
|
|
825
|
+
const margin = isMobile ? 10 : 15;
|
|
826
|
+
const lineSpacing = isMobile ? 12 : 15;
|
|
827
|
+
|
|
828
|
+
// On mobile, use shorter text
|
|
829
|
+
if (isMobile) {
|
|
830
|
+
this.controlsText.text = "tap to form | drag to rotate";
|
|
831
|
+
this.controlsText.font = "8px monospace";
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Position and render main controls text
|
|
835
|
+
this.controlsText.x = w - margin;
|
|
836
|
+
this.controlsText.y = h - 25 - (isMobile ? 1 : this.explanationShapes.length) * lineSpacing;
|
|
837
|
+
this.controlsText.render();
|
|
838
|
+
|
|
839
|
+
// Position and render explanation lines (hide most on mobile)
|
|
840
|
+
this.explanationShapes.forEach((shape, i) => {
|
|
841
|
+
if (isMobile && i < this.explanationShapes.length - 1) return; // Only show last line on mobile
|
|
842
|
+
|
|
843
|
+
shape.font = isMobile ? "8px monospace" : "10px monospace";
|
|
844
|
+
const lineIndexFromBottom = this.explanationShapes.length - 1 - i;
|
|
845
|
+
shape.x = w - margin;
|
|
846
|
+
shape.y = h - 10 - (lineIndexFromBottom * lineSpacing);
|
|
847
|
+
shape.render();
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
drawFormationProgress(w, h) {
|
|
852
|
+
const progress = this.formationProgress;
|
|
853
|
+
const lambda = 1 - Math.pow(1 - progress, 3); // Eased progress
|
|
854
|
+
|
|
855
|
+
// Position above the chart (same x alignment as chart)
|
|
856
|
+
const isMobile = w < CONFIG.mobileWidth;
|
|
857
|
+
const graphW = isMobile ? 120 : 160;
|
|
858
|
+
const graphX = w - graphW - (isMobile ? 15 : 20);
|
|
859
|
+
const graphY = isMobile ? 80 : 220; // Desktop moved down to avoid info div
|
|
860
|
+
|
|
861
|
+
const barWidth = graphW; // Same width as chart
|
|
862
|
+
const barHeight = 6;
|
|
863
|
+
const barX = graphX;
|
|
864
|
+
const barY = graphY - 35; // Above the chart
|
|
865
|
+
|
|
866
|
+
Painter.useCtx((ctx) => {
|
|
867
|
+
// Phase-aware label
|
|
868
|
+
ctx.font = "10px monospace";
|
|
869
|
+
ctx.textAlign = "left";
|
|
870
|
+
let label, color;
|
|
871
|
+
|
|
872
|
+
if (progress >= 1) {
|
|
873
|
+
label = "Kerr (stationary)";
|
|
874
|
+
color = "#8f8";
|
|
875
|
+
} else if (lambda < 0.2) {
|
|
876
|
+
label = "Collapse...";
|
|
877
|
+
color = "#fff";
|
|
878
|
+
} else if (lambda < 0.5) {
|
|
879
|
+
label = "Horizon forming...";
|
|
880
|
+
color = "#f88";
|
|
881
|
+
} else if (lambda < 0.8) {
|
|
882
|
+
label = "Ergosphere emerging...";
|
|
883
|
+
color = "#fa8";
|
|
884
|
+
} else {
|
|
885
|
+
label = "Frame dragging stabilizing...";
|
|
886
|
+
color = "#ff0";
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
ctx.fillStyle = color;
|
|
890
|
+
ctx.fillText(label, barX, barY - 8);
|
|
891
|
+
|
|
892
|
+
// Background bar
|
|
893
|
+
ctx.fillStyle = "rgba(255, 255, 255, 0.1)";
|
|
894
|
+
ctx.fillRect(barX, barY, barWidth, barHeight);
|
|
895
|
+
|
|
896
|
+
// Progress bar
|
|
897
|
+
const gradient = ctx.createLinearGradient(barX, 0, barX + barWidth, 0);
|
|
898
|
+
gradient.addColorStop(0, "rgba(100, 100, 255, 0.8)");
|
|
899
|
+
gradient.addColorStop(1, "rgba(255, 100, 100, 0.8)");
|
|
900
|
+
ctx.fillStyle = gradient;
|
|
901
|
+
ctx.fillRect(barX, barY, barWidth * progress, barHeight);
|
|
902
|
+
|
|
903
|
+
// λ indicator
|
|
904
|
+
ctx.fillStyle = "#888";
|
|
905
|
+
ctx.font = "9px monospace";
|
|
906
|
+
ctx.fillText(`λ = ${progress.toFixed(2)}`, barX, barY + 16);
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
drawKeyRadii(cx, cy) {
|
|
911
|
+
const M = this.mass;
|
|
912
|
+
const a = this.spin;
|
|
913
|
+
|
|
914
|
+
const radii = [
|
|
915
|
+
{
|
|
916
|
+
r: Tensor.kerrHorizonRadius(M, a, false),
|
|
917
|
+
color: CONFIG.outerHorizonColor,
|
|
918
|
+
label: "r+",
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
r: Tensor.kerrErgosphereRadius(M, a, Math.PI / 2),
|
|
922
|
+
color: CONFIG.ergosphereColor,
|
|
923
|
+
label: "ergo",
|
|
924
|
+
dashed: true,
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
r: Tensor.kerrISCO(M, a, true),
|
|
928
|
+
color: CONFIG.progradeISCOColor,
|
|
929
|
+
label: "ISCO_pro",
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
r: Tensor.kerrISCO(M, a, false),
|
|
933
|
+
color: CONFIG.retrogradeISCOColor,
|
|
934
|
+
label: "ISCO_retro",
|
|
935
|
+
},
|
|
936
|
+
];
|
|
937
|
+
|
|
938
|
+
for (const { r, color, dashed } of radii) {
|
|
939
|
+
if (isNaN(r)) continue;
|
|
940
|
+
const segments = 48;
|
|
941
|
+
|
|
942
|
+
Painter.useCtx((ctx) => {
|
|
943
|
+
ctx.strokeStyle = color;
|
|
944
|
+
ctx.lineWidth = 1.5;
|
|
945
|
+
if (dashed) ctx.setLineDash([5, 5]);
|
|
946
|
+
ctx.beginPath();
|
|
947
|
+
|
|
948
|
+
for (let i = 0; i <= segments; i++) {
|
|
949
|
+
const angle = (i / segments) * Math.PI * 2;
|
|
950
|
+
const x = Math.cos(angle) * r;
|
|
951
|
+
const z = Math.sin(angle) * r;
|
|
952
|
+
const y = this.getEmbeddingHeight(r);
|
|
953
|
+
|
|
954
|
+
const p = this.camera.project(
|
|
955
|
+
x * this.gridScale,
|
|
956
|
+
y,
|
|
957
|
+
z * this.gridScale,
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
if (i === 0) ctx.moveTo(cx + p.x, cy + p.y);
|
|
961
|
+
else ctx.lineTo(cx + p.x, cy + p.y);
|
|
962
|
+
}
|
|
963
|
+
ctx.stroke();
|
|
964
|
+
ctx.setLineDash([]);
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
drawErgosphere(cx, cy) {
|
|
970
|
+
const M = this.mass;
|
|
971
|
+
const a = this.spin;
|
|
972
|
+
const rErgo = Tensor.kerrErgosphereRadius(M, a, Math.PI / 2);
|
|
973
|
+
const rPlus = Tensor.kerrHorizonRadius(M, a, false);
|
|
974
|
+
|
|
975
|
+
if (isNaN(rErgo) || isNaN(rPlus) || rErgo <= rPlus) return;
|
|
976
|
+
|
|
977
|
+
// Ergosphere only visible after significant formation (λ > 0.4)
|
|
978
|
+
// This is a property of the final Kerr geometry
|
|
979
|
+
const lambda = 1 - Math.pow(1 - this.formationProgress, 3);
|
|
980
|
+
const ergoVisibility = Math.max(0, (lambda - 0.4) / 0.6); // 0 at λ=0.4, 1 at λ=1
|
|
981
|
+
if (ergoVisibility <= 0) return;
|
|
982
|
+
|
|
983
|
+
const segments = 64;
|
|
984
|
+
|
|
985
|
+
Painter.useCtx((ctx) => {
|
|
986
|
+
// Semi-transparent orange fill for ergosphere - fades in with formation
|
|
987
|
+
ctx.fillStyle = `rgba(255, 100, 0, ${0.15 * ergoVisibility})`;
|
|
988
|
+
ctx.beginPath();
|
|
989
|
+
|
|
990
|
+
// Outer boundary (ergosphere)
|
|
991
|
+
for (let i = 0; i <= segments; i++) {
|
|
992
|
+
const angle = (i / segments) * Math.PI * 2;
|
|
993
|
+
const x = Math.cos(angle) * rErgo;
|
|
994
|
+
const z = Math.sin(angle) * rErgo;
|
|
995
|
+
const y = this.getEmbeddingHeight(rErgo);
|
|
996
|
+
|
|
997
|
+
const p = this.camera.project(
|
|
998
|
+
x * this.gridScale,
|
|
999
|
+
y,
|
|
1000
|
+
z * this.gridScale,
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
if (i === 0) ctx.moveTo(cx + p.x, cy + p.y);
|
|
1004
|
+
else ctx.lineTo(cx + p.x, cy + p.y);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Inner boundary (horizon) - reverse to create ring
|
|
1008
|
+
for (let i = segments; i >= 0; i--) {
|
|
1009
|
+
const angle = (i / segments) * Math.PI * 2;
|
|
1010
|
+
const x = Math.cos(angle) * rPlus;
|
|
1011
|
+
const z = Math.sin(angle) * rPlus;
|
|
1012
|
+
const y = this.getEmbeddingHeight(rPlus + 0.1);
|
|
1013
|
+
|
|
1014
|
+
const p = this.camera.project(
|
|
1015
|
+
x * this.gridScale,
|
|
1016
|
+
y,
|
|
1017
|
+
z * this.gridScale,
|
|
1018
|
+
);
|
|
1019
|
+
ctx.lineTo(cx + p.x, cy + p.y);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
ctx.closePath();
|
|
1023
|
+
ctx.fill();
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
// Draw dragged particles in ergosphere - shows frame dragging!
|
|
1027
|
+
// Particles also fade in with ergosphere visibility
|
|
1028
|
+
if (this.draggedParticles && ergoVisibility > 0) {
|
|
1029
|
+
Painter.useCtx((ctx) => {
|
|
1030
|
+
for (const p of this.draggedParticles) {
|
|
1031
|
+
// Only draw if within ergosphere
|
|
1032
|
+
if (p.r < rErgo && p.r > rPlus) {
|
|
1033
|
+
const x = Math.cos(p.angle) * p.r;
|
|
1034
|
+
const z = Math.sin(p.angle) * p.r;
|
|
1035
|
+
const y = this.getEmbeddingHeight(p.r);
|
|
1036
|
+
|
|
1037
|
+
const proj = this.camera.project(
|
|
1038
|
+
x * this.gridScale,
|
|
1039
|
+
y,
|
|
1040
|
+
z * this.gridScale,
|
|
1041
|
+
);
|
|
1042
|
+
|
|
1043
|
+
// Particles glow orange - they're being dragged!
|
|
1044
|
+
const size = 3 * proj.scale;
|
|
1045
|
+
ctx.fillStyle = `rgba(255, 180, 50, ${0.9 * ergoVisibility})`;
|
|
1046
|
+
ctx.beginPath();
|
|
1047
|
+
ctx.arc(cx + proj.x, cy + proj.y, size, 0, Math.PI * 2);
|
|
1048
|
+
ctx.fill();
|
|
1049
|
+
|
|
1050
|
+
// Trail showing direction of drag
|
|
1051
|
+
const trailAngle = p.angle - 0.3;
|
|
1052
|
+
const trailX = Math.cos(trailAngle) * p.r;
|
|
1053
|
+
const trailZ = Math.sin(trailAngle) * p.r;
|
|
1054
|
+
const trailProj = this.camera.project(
|
|
1055
|
+
trailX * this.gridScale,
|
|
1056
|
+
y,
|
|
1057
|
+
trailZ * this.gridScale,
|
|
1058
|
+
);
|
|
1059
|
+
|
|
1060
|
+
ctx.strokeStyle = `rgba(255, 150, 50, ${0.4 * ergoVisibility})`;
|
|
1061
|
+
ctx.lineWidth = 2 * proj.scale;
|
|
1062
|
+
ctx.beginPath();
|
|
1063
|
+
ctx.moveTo(cx + trailProj.x, cy + trailProj.y);
|
|
1064
|
+
ctx.lineTo(cx + proj.x, cy + proj.y);
|
|
1065
|
+
ctx.stroke();
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
drawGrid(cx, cy) {
|
|
1073
|
+
const { gridResolution, gridColor, gridHighlight } = CONFIG;
|
|
1074
|
+
const gridScale = this.gridScale;
|
|
1075
|
+
|
|
1076
|
+
const projected = this.gridVertices.map((row) =>
|
|
1077
|
+
row.map((v) => {
|
|
1078
|
+
const p = this.camera.project(v.x * gridScale, v.y, v.z * gridScale);
|
|
1079
|
+
return { x: cx + p.x, y: cy + p.y, z: p.z };
|
|
1080
|
+
}),
|
|
1081
|
+
);
|
|
1082
|
+
|
|
1083
|
+
for (let i = 0; i <= gridResolution; i++) {
|
|
1084
|
+
const isMain = i % 5 === 0;
|
|
1085
|
+
Painter.useCtx((ctx) => {
|
|
1086
|
+
ctx.strokeStyle = isMain ? gridHighlight : gridColor;
|
|
1087
|
+
ctx.lineWidth = isMain ? 1.2 : 0.6;
|
|
1088
|
+
ctx.beginPath();
|
|
1089
|
+
for (let j = 0; j <= gridResolution; j++) {
|
|
1090
|
+
const p = projected[i][j];
|
|
1091
|
+
if (j === 0) ctx.moveTo(p.x, p.y);
|
|
1092
|
+
else ctx.lineTo(p.x, p.y);
|
|
1093
|
+
}
|
|
1094
|
+
ctx.stroke();
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
for (let j = 0; j <= gridResolution; j++) {
|
|
1099
|
+
const isMain = j % 5 === 0;
|
|
1100
|
+
Painter.useCtx((ctx) => {
|
|
1101
|
+
ctx.strokeStyle = isMain ? gridHighlight : gridColor;
|
|
1102
|
+
ctx.lineWidth = isMain ? 1.2 : 0.6;
|
|
1103
|
+
ctx.beginPath();
|
|
1104
|
+
for (let i = 0; i <= gridResolution; i++) {
|
|
1105
|
+
const p = projected[i][j];
|
|
1106
|
+
if (i === 0) ctx.moveTo(p.x, p.y);
|
|
1107
|
+
else ctx.lineTo(p.x, p.y);
|
|
1108
|
+
}
|
|
1109
|
+
ctx.stroke();
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
drawHorizon(cx, cy) {
|
|
1115
|
+
const rPlus = Tensor.kerrHorizonRadius(this.mass, this.spin, false);
|
|
1116
|
+
|
|
1117
|
+
// Formation progress affects size, intensity, AND vertical position
|
|
1118
|
+
// Smooth easing for formation (ease-out cubic)
|
|
1119
|
+
const lambda = 1 - Math.pow(1 - this.formationProgress, 3);
|
|
1120
|
+
|
|
1121
|
+
// Black hole sinks down as space curves around it
|
|
1122
|
+
// At λ=0: sits at flat space level (y=0)
|
|
1123
|
+
// At λ=1: sits at bottom of the well
|
|
1124
|
+
const finalY = this.getEmbeddingHeight(rPlus + 0.1);
|
|
1125
|
+
const y = finalY * lambda; // Interpolate from 0 to final depth
|
|
1126
|
+
|
|
1127
|
+
const centerP = this.camera.project(0, y + 10, 0);
|
|
1128
|
+
const centerX = cx + centerP.x;
|
|
1129
|
+
const centerY = cy + centerP.y;
|
|
1130
|
+
|
|
1131
|
+
// Black hole size scales with mass AND formation progress
|
|
1132
|
+
// Starts as tiny seed (3px), grows to full size
|
|
1133
|
+
const fullSize =
|
|
1134
|
+
CONFIG.blackHoleSizeBase + this.mass * CONFIG.blackHoleSizeMassScale;
|
|
1135
|
+
const seedSize = 3; // Initial collapse seed
|
|
1136
|
+
const size = (seedSize + (fullSize - seedSize) * lambda) * centerP.scale;
|
|
1137
|
+
|
|
1138
|
+
// Spin direction indicator (which way the hole rotates)
|
|
1139
|
+
const spinDirection = this.spin > 0 ? 1 : -1;
|
|
1140
|
+
// Rotation speed increases as formation progresses
|
|
1141
|
+
const rotationAngle = this.time * 2 * spinDirection * lambda;
|
|
1142
|
+
|
|
1143
|
+
// During early formation, show bright collapse point
|
|
1144
|
+
if (lambda < 0.3) {
|
|
1145
|
+
const collapseIntensity = 1 - lambda / 0.3; // Fades out as formation progresses
|
|
1146
|
+
Painter.useCtx((ctx) => {
|
|
1147
|
+
// Bright white-blue collapse flash
|
|
1148
|
+
const flashSize = (10 + (1 - lambda) * 30) * centerP.scale;
|
|
1149
|
+
const gradient = ctx.createRadialGradient(
|
|
1150
|
+
centerX,
|
|
1151
|
+
centerY,
|
|
1152
|
+
0,
|
|
1153
|
+
centerX,
|
|
1154
|
+
centerY,
|
|
1155
|
+
flashSize,
|
|
1156
|
+
);
|
|
1157
|
+
gradient.addColorStop(
|
|
1158
|
+
0,
|
|
1159
|
+
`rgba(255, 255, 255, ${0.9 * collapseIntensity})`,
|
|
1160
|
+
);
|
|
1161
|
+
gradient.addColorStop(
|
|
1162
|
+
0.3,
|
|
1163
|
+
`rgba(150, 200, 255, ${0.6 * collapseIntensity})`,
|
|
1164
|
+
);
|
|
1165
|
+
gradient.addColorStop(1, "transparent");
|
|
1166
|
+
ctx.fillStyle = gradient;
|
|
1167
|
+
ctx.beginPath();
|
|
1168
|
+
ctx.arc(centerX, centerY, flashSize, 0, Math.PI * 2);
|
|
1169
|
+
ctx.fill();
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// Outer glow - intensity grows with formation
|
|
1174
|
+
const glowIntensity = 0.2 + lambda * 0.8; // 20% → 100%
|
|
1175
|
+
Painter.useCtx((ctx) => {
|
|
1176
|
+
const gradient = ctx.createRadialGradient(
|
|
1177
|
+
centerX,
|
|
1178
|
+
centerY,
|
|
1179
|
+
size,
|
|
1180
|
+
centerX,
|
|
1181
|
+
centerY,
|
|
1182
|
+
size * 4,
|
|
1183
|
+
);
|
|
1184
|
+
gradient.addColorStop(0, `rgba(100, 50, 150, ${0.5 * glowIntensity})`);
|
|
1185
|
+
gradient.addColorStop(0.5, `rgba(255, 100, 50, ${0.2 * glowIntensity})`);
|
|
1186
|
+
gradient.addColorStop(1, "transparent");
|
|
1187
|
+
ctx.fillStyle = gradient;
|
|
1188
|
+
ctx.beginPath();
|
|
1189
|
+
ctx.arc(centerX, centerY, size * 4, 0, Math.PI * 2);
|
|
1190
|
+
ctx.fill();
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
// ROTATING ACCRETION DISK - fades in as black hole forms
|
|
1194
|
+
// Only visible after initial collapse phase (λ > 0.2)
|
|
1195
|
+
const diskVisibility = Math.max(0, (lambda - 0.2) / 0.8); // 0 at λ=0.2, 1 at λ=1
|
|
1196
|
+
if (diskVisibility > 0) {
|
|
1197
|
+
Painter.useCtx((ctx) => {
|
|
1198
|
+
ctx.save();
|
|
1199
|
+
ctx.translate(centerX, centerY);
|
|
1200
|
+
|
|
1201
|
+
// Draw spinning spiral arms
|
|
1202
|
+
const numArms = 3;
|
|
1203
|
+
for (let arm = 0; arm < numArms; arm++) {
|
|
1204
|
+
const armAngle = (arm / numArms) * Math.PI * 2 + rotationAngle;
|
|
1205
|
+
|
|
1206
|
+
// Spiral gradient for each arm
|
|
1207
|
+
ctx.beginPath();
|
|
1208
|
+
ctx.moveTo(0, 0);
|
|
1209
|
+
|
|
1210
|
+
// Draw spiral
|
|
1211
|
+
for (let t = 0; t <= 1; t += 0.02) {
|
|
1212
|
+
const r = size * 1.2 + t * size * 2.5;
|
|
1213
|
+
const angle = armAngle + t * Math.PI * 1.5 * spinDirection;
|
|
1214
|
+
const x = Math.cos(angle) * r;
|
|
1215
|
+
const y = Math.sin(angle) * r * 0.4; // Flatten for disk perspective
|
|
1216
|
+
ctx.lineTo(x, y);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const baseAlpha = 0.6 - arm * 0.15;
|
|
1220
|
+
const alpha = baseAlpha * diskVisibility;
|
|
1221
|
+
ctx.strokeStyle = `rgba(255, ${150 + arm * 30}, ${50 + arm * 20}, ${alpha})`;
|
|
1222
|
+
ctx.lineWidth = 3 - arm * 0.5;
|
|
1223
|
+
ctx.stroke();
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Inner bright ring (hot gas closest to horizon)
|
|
1227
|
+
ctx.strokeStyle = `rgba(255, 200, 100, ${0.8 * diskVisibility})`;
|
|
1228
|
+
ctx.lineWidth = 2;
|
|
1229
|
+
ctx.beginPath();
|
|
1230
|
+
ctx.ellipse(0, 0, size * 1.5, size * 0.6, 0, 0, Math.PI * 2);
|
|
1231
|
+
ctx.stroke();
|
|
1232
|
+
|
|
1233
|
+
// Spinning particles in the disk
|
|
1234
|
+
const numParticles = 12;
|
|
1235
|
+
for (let i = 0; i < numParticles; i++) {
|
|
1236
|
+
const baseAngle = (i / numParticles) * Math.PI * 2;
|
|
1237
|
+
const particleR = size * 1.8 + Math.sin(i * 2.7) * size * 0.5;
|
|
1238
|
+
const particleAngle =
|
|
1239
|
+
baseAngle + rotationAngle * (2 - particleR / (size * 3));
|
|
1240
|
+
const px = Math.cos(particleAngle) * particleR;
|
|
1241
|
+
const py = Math.sin(particleAngle) * particleR * 0.4;
|
|
1242
|
+
|
|
1243
|
+
const brightness = 150 + Math.sin(this.time * 3 + i) * 50;
|
|
1244
|
+
ctx.fillStyle = `rgba(255, ${brightness}, 50, ${0.8 * diskVisibility})`;
|
|
1245
|
+
ctx.beginPath();
|
|
1246
|
+
ctx.arc(px, py, 2, 0, Math.PI * 2);
|
|
1247
|
+
ctx.fill();
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
ctx.restore();
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// Black hole body (actual black center)
|
|
1255
|
+
Painter.useCtx((ctx) => {
|
|
1256
|
+
ctx.fillStyle = "#000";
|
|
1257
|
+
ctx.beginPath();
|
|
1258
|
+
ctx.arc(centerX, centerY, size, 0, Math.PI * 2);
|
|
1259
|
+
ctx.fill();
|
|
1260
|
+
|
|
1261
|
+
// Inner edge glow
|
|
1262
|
+
const innerGrad = ctx.createRadialGradient(
|
|
1263
|
+
centerX,
|
|
1264
|
+
centerY,
|
|
1265
|
+
size * 0.7,
|
|
1266
|
+
centerX,
|
|
1267
|
+
centerY,
|
|
1268
|
+
size,
|
|
1269
|
+
);
|
|
1270
|
+
innerGrad.addColorStop(0, "transparent");
|
|
1271
|
+
innerGrad.addColorStop(1, "rgba(255, 100, 0, 0.5)");
|
|
1272
|
+
ctx.fillStyle = innerGrad;
|
|
1273
|
+
ctx.beginPath();
|
|
1274
|
+
ctx.arc(centerX, centerY, size, 0, Math.PI * 2);
|
|
1275
|
+
ctx.fill();
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// Event horizon circle on grid
|
|
1279
|
+
const segments = 32;
|
|
1280
|
+
Painter.useCtx((ctx) => {
|
|
1281
|
+
ctx.strokeStyle = CONFIG.outerHorizonColor;
|
|
1282
|
+
ctx.lineWidth = 2;
|
|
1283
|
+
ctx.beginPath();
|
|
1284
|
+
|
|
1285
|
+
for (let i = 0; i <= segments; i++) {
|
|
1286
|
+
const angle = (i / segments) * Math.PI * 2;
|
|
1287
|
+
const x = Math.cos(angle) * rPlus;
|
|
1288
|
+
const z = Math.sin(angle) * rPlus;
|
|
1289
|
+
|
|
1290
|
+
const p = this.camera.project(
|
|
1291
|
+
x * this.gridScale,
|
|
1292
|
+
y,
|
|
1293
|
+
z * this.gridScale,
|
|
1294
|
+
);
|
|
1295
|
+
|
|
1296
|
+
if (i === 0) ctx.moveTo(cx + p.x, cy + p.y);
|
|
1297
|
+
else ctx.lineTo(cx + p.x, cy + p.y);
|
|
1298
|
+
}
|
|
1299
|
+
ctx.closePath();
|
|
1300
|
+
ctx.stroke();
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
drawOrbiter(cx, cy) {
|
|
1305
|
+
// Only show orbiter after black hole has fully formed
|
|
1306
|
+
// Geodesic motion is a property of the final Kerr spacetime
|
|
1307
|
+
if (this.formationProgress < 1) return;
|
|
1308
|
+
|
|
1309
|
+
// Fade in the orbiter over 0.5 seconds after formation completes
|
|
1310
|
+
const timeSinceFormation = this.formationProgress >= 1
|
|
1311
|
+
? (this.time - this.formationCompleteTime || 0)
|
|
1312
|
+
: 0;
|
|
1313
|
+
const orbiterAlpha = Math.min(1, timeSinceFormation * 2); // 0.5s fade-in
|
|
1314
|
+
|
|
1315
|
+
const totalAngle = this.orbitPhi + this.precessionAngle;
|
|
1316
|
+
const orbiterX = Math.cos(totalAngle) * this.orbitR;
|
|
1317
|
+
const orbiterZ = Math.sin(totalAngle) * this.orbitR;
|
|
1318
|
+
const orbiterY = this.getEmbeddingHeight(this.orbitR);
|
|
1319
|
+
|
|
1320
|
+
const p = this.camera.project(
|
|
1321
|
+
orbiterX * this.gridScale,
|
|
1322
|
+
orbiterY,
|
|
1323
|
+
orbiterZ * this.gridScale,
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
const screenX = cx + p.x;
|
|
1327
|
+
const screenY = cy + p.y;
|
|
1328
|
+
const size = 5 * p.scale;
|
|
1329
|
+
|
|
1330
|
+
// Glow
|
|
1331
|
+
Painter.useCtx((ctx) => {
|
|
1332
|
+
ctx.globalAlpha = orbiterAlpha;
|
|
1333
|
+
const gradient = ctx.createRadialGradient(
|
|
1334
|
+
screenX,
|
|
1335
|
+
screenY,
|
|
1336
|
+
0,
|
|
1337
|
+
screenX,
|
|
1338
|
+
screenY,
|
|
1339
|
+
size * 4,
|
|
1340
|
+
);
|
|
1341
|
+
gradient.addColorStop(0, CONFIG.orbiterGlow);
|
|
1342
|
+
gradient.addColorStop(1, "transparent");
|
|
1343
|
+
ctx.fillStyle = gradient;
|
|
1344
|
+
ctx.beginPath();
|
|
1345
|
+
ctx.arc(screenX, screenY, size * 4, 0, Math.PI * 2);
|
|
1346
|
+
ctx.fill();
|
|
1347
|
+
ctx.globalAlpha = 1;
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
// Body
|
|
1351
|
+
Painter.useCtx((ctx) => {
|
|
1352
|
+
ctx.globalAlpha = orbiterAlpha;
|
|
1353
|
+
const gradient = ctx.createRadialGradient(
|
|
1354
|
+
screenX - size * 0.3,
|
|
1355
|
+
screenY - size * 0.3,
|
|
1356
|
+
0,
|
|
1357
|
+
screenX,
|
|
1358
|
+
screenY,
|
|
1359
|
+
size,
|
|
1360
|
+
);
|
|
1361
|
+
gradient.addColorStop(0, "#fff");
|
|
1362
|
+
gradient.addColorStop(0.5, CONFIG.orbiterColor);
|
|
1363
|
+
gradient.addColorStop(1, CONFIG.orbiterGlow);
|
|
1364
|
+
ctx.fillStyle = gradient;
|
|
1365
|
+
ctx.beginPath();
|
|
1366
|
+
ctx.arc(screenX, screenY, size, 0, Math.PI * 2);
|
|
1367
|
+
ctx.fill();
|
|
1368
|
+
ctx.globalAlpha = 1;
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
this.drawOrbitPath(cx, cy, orbiterAlpha);
|
|
1372
|
+
this.drawOrbitalTrail(cx, cy, orbiterAlpha);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
drawOrbitPath(cx, cy, alpha = 1) {
|
|
1376
|
+
const segments = 64;
|
|
1377
|
+
const eccentricity = CONFIG.orbitEccentricity;
|
|
1378
|
+
|
|
1379
|
+
Painter.useCtx((ctx) => {
|
|
1380
|
+
ctx.globalAlpha = alpha;
|
|
1381
|
+
ctx.strokeStyle = "rgba(100, 180, 255, 0.25)";
|
|
1382
|
+
ctx.lineWidth = 1.5;
|
|
1383
|
+
ctx.beginPath();
|
|
1384
|
+
|
|
1385
|
+
for (let i = 0; i <= segments; i++) {
|
|
1386
|
+
const angle = (i / segments) * Math.PI * 2 + this.precessionAngle;
|
|
1387
|
+
const phi = (i / segments) * Math.PI * 2;
|
|
1388
|
+
const radialOscillation = eccentricity * Math.sin(phi * 2);
|
|
1389
|
+
const r = CONFIG.orbitSemiMajor + radialOscillation * 2;
|
|
1390
|
+
|
|
1391
|
+
const x = Math.cos(angle) * r;
|
|
1392
|
+
const z = Math.sin(angle) * r;
|
|
1393
|
+
const y = this.getEmbeddingHeight(r);
|
|
1394
|
+
|
|
1395
|
+
const p = this.camera.project(
|
|
1396
|
+
x * this.gridScale,
|
|
1397
|
+
y,
|
|
1398
|
+
z * this.gridScale,
|
|
1399
|
+
);
|
|
1400
|
+
|
|
1401
|
+
if (i === 0) ctx.moveTo(cx + p.x, cy + p.y);
|
|
1402
|
+
else ctx.lineTo(cx + p.x, cy + p.y);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
ctx.closePath();
|
|
1406
|
+
ctx.stroke();
|
|
1407
|
+
ctx.globalAlpha = 1;
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
drawOrbitalTrail(cx, cy, fadeAlpha = 1) {
|
|
1412
|
+
if (this.orbitTrail.length < 2) return;
|
|
1413
|
+
|
|
1414
|
+
Painter.useCtx((ctx) => {
|
|
1415
|
+
ctx.lineCap = "round";
|
|
1416
|
+
|
|
1417
|
+
for (let i = 1; i < this.orbitTrail.length; i++) {
|
|
1418
|
+
const t = i / this.orbitTrail.length;
|
|
1419
|
+
const point = this.orbitTrail[i];
|
|
1420
|
+
const prevPoint = this.orbitTrail[i - 1];
|
|
1421
|
+
|
|
1422
|
+
const trailY = this.getEmbeddingHeight(point.r);
|
|
1423
|
+
const prevY = this.getEmbeddingHeight(prevPoint.r);
|
|
1424
|
+
|
|
1425
|
+
const p = this.camera.project(
|
|
1426
|
+
point.x * this.gridScale,
|
|
1427
|
+
trailY,
|
|
1428
|
+
point.z * this.gridScale,
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
const prevP = this.camera.project(
|
|
1432
|
+
prevPoint.x * this.gridScale,
|
|
1433
|
+
prevY,
|
|
1434
|
+
prevPoint.z * this.gridScale,
|
|
1435
|
+
);
|
|
1436
|
+
|
|
1437
|
+
const alpha = (1 - t) * 0.5 * fadeAlpha;
|
|
1438
|
+
ctx.strokeStyle = `rgba(100, 180, 255, ${alpha})`;
|
|
1439
|
+
ctx.lineWidth = (1 - t) * 2.5 * p.scale;
|
|
1440
|
+
ctx.beginPath();
|
|
1441
|
+
ctx.moveTo(cx + prevP.x, cy + prevP.y);
|
|
1442
|
+
ctx.lineTo(cx + p.x, cy + p.y);
|
|
1443
|
+
ctx.stroke();
|
|
1444
|
+
}
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
drawEffectivePotential() {
|
|
1449
|
+
// Responsive graph sizing
|
|
1450
|
+
const isMobile = this.width < CONFIG.mobileWidth;
|
|
1451
|
+
const graphW = isMobile ? 120 : 160;
|
|
1452
|
+
const graphH = isMobile ? 70 : 100;
|
|
1453
|
+
const graphX = this.width - graphW - (isMobile ? 15 : 20);
|
|
1454
|
+
const graphY = isMobile ? 80 : 220; // Desktop moved down to avoid info div
|
|
1455
|
+
const M = this.mass;
|
|
1456
|
+
const a = this.spin;
|
|
1457
|
+
|
|
1458
|
+
Painter.useCtx((ctx) => {
|
|
1459
|
+
// Background
|
|
1460
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
|
|
1461
|
+
ctx.fillRect(graphX - 10, graphY - 10, graphW + 20, graphH + 40);
|
|
1462
|
+
|
|
1463
|
+
// Title
|
|
1464
|
+
ctx.fillStyle = "#ccc"; // Brightened from #888
|
|
1465
|
+
ctx.font = "10px monospace";
|
|
1466
|
+
ctx.textAlign = "center";
|
|
1467
|
+
ctx.fillText("Kerr Effective Potential", graphX + graphW / 2, graphY);
|
|
1468
|
+
|
|
1469
|
+
// Axes
|
|
1470
|
+
ctx.strokeStyle = "#444";
|
|
1471
|
+
ctx.lineWidth = 1;
|
|
1472
|
+
ctx.beginPath();
|
|
1473
|
+
ctx.moveTo(graphX, graphY + graphH);
|
|
1474
|
+
ctx.lineTo(graphX + graphW, graphY + graphH);
|
|
1475
|
+
ctx.moveTo(graphX, graphY + 10);
|
|
1476
|
+
ctx.lineTo(graphX, graphY + graphH);
|
|
1477
|
+
ctx.stroke();
|
|
1478
|
+
|
|
1479
|
+
// Labels
|
|
1480
|
+
ctx.fillStyle = "#aaa"; // Brightened from #666
|
|
1481
|
+
ctx.font = "10px monospace";
|
|
1482
|
+
ctx.textAlign = "left";
|
|
1483
|
+
ctx.fillText("r", graphX + graphW - 10, graphY + graphH + 12);
|
|
1484
|
+
|
|
1485
|
+
// Plot V_eff (using Schwarzschild approximation for display)
|
|
1486
|
+
const rPlus = Tensor.kerrHorizonRadius(M, a, false);
|
|
1487
|
+
const rMin = rPlus * 1.2;
|
|
1488
|
+
const rMax = 20;
|
|
1489
|
+
|
|
1490
|
+
ctx.strokeStyle = "#8f8";
|
|
1491
|
+
ctx.lineWidth = 1.5;
|
|
1492
|
+
ctx.beginPath();
|
|
1493
|
+
|
|
1494
|
+
let firstPoint = true;
|
|
1495
|
+
for (let i = 0; i <= 100; i++) {
|
|
1496
|
+
const r = rMin + (i / 100) * (rMax - rMin);
|
|
1497
|
+
const V = Tensor.effectivePotential(M, this.orbitL, r);
|
|
1498
|
+
|
|
1499
|
+
const px = graphX + ((r - rMin) / (rMax - rMin)) * graphW;
|
|
1500
|
+
const py = graphY + graphH - 20 - (V + 0.1) * 300;
|
|
1501
|
+
|
|
1502
|
+
if (py > graphY + 10 && py < graphY + graphH) {
|
|
1503
|
+
if (firstPoint) {
|
|
1504
|
+
ctx.moveTo(px, py);
|
|
1505
|
+
firstPoint = false;
|
|
1506
|
+
} else {
|
|
1507
|
+
ctx.lineTo(px, py);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
ctx.stroke();
|
|
1512
|
+
|
|
1513
|
+
// Current position
|
|
1514
|
+
const currentPx =
|
|
1515
|
+
graphX + ((this.orbitR - rMin) / (rMax - rMin)) * graphW;
|
|
1516
|
+
const currentV = Tensor.effectivePotential(M, this.orbitL, this.orbitR);
|
|
1517
|
+
const currentPy = graphY + graphH - 20 - (currentV + 0.1) * 300;
|
|
1518
|
+
|
|
1519
|
+
if (currentPy > graphY + 10 && currentPy < graphY + graphH) {
|
|
1520
|
+
ctx.fillStyle = CONFIG.orbiterColor;
|
|
1521
|
+
ctx.beginPath();
|
|
1522
|
+
ctx.arc(currentPx, currentPy, 4, 0, Math.PI * 2);
|
|
1523
|
+
ctx.fill();
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Mark prograde ISCO
|
|
1527
|
+
const iscoP = Tensor.kerrISCO(M, a, true);
|
|
1528
|
+
const iscoPx = graphX + ((iscoP - rMin) / (rMax - rMin)) * graphW;
|
|
1529
|
+
ctx.strokeStyle = CONFIG.progradeISCOColor;
|
|
1530
|
+
ctx.setLineDash([2, 2]);
|
|
1531
|
+
ctx.beginPath();
|
|
1532
|
+
ctx.moveTo(iscoPx, graphY + 10);
|
|
1533
|
+
ctx.lineTo(iscoPx, graphY + graphH);
|
|
1534
|
+
ctx.stroke();
|
|
1535
|
+
|
|
1536
|
+
// Mark retrograde ISCO
|
|
1537
|
+
const iscoR = Tensor.kerrISCO(M, a, false);
|
|
1538
|
+
const iscoRx = graphX + ((iscoR - rMin) / (rMax - rMin)) * graphW;
|
|
1539
|
+
if (iscoRx < graphX + graphW) {
|
|
1540
|
+
ctx.strokeStyle = CONFIG.retrogradeISCOColor;
|
|
1541
|
+
ctx.beginPath();
|
|
1542
|
+
ctx.moveTo(iscoRx, graphY + 10);
|
|
1543
|
+
ctx.lineTo(iscoRx, graphY + graphH);
|
|
1544
|
+
ctx.stroke();
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
ctx.setLineDash([]);
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
window.addEventListener("load", () => {
|
|
1553
|
+
const canvas = document.getElementById("game");
|
|
1554
|
+
const demo = new KerrDemo(canvas);
|
|
1555
|
+
demo.start();
|
|
1556
|
+
});
|