@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,559 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spacetime Curvature - Math & Physics Demo
|
|
3
|
+
*
|
|
4
|
+
* Visualization of how mass curves spacetime, based on Einstein's
|
|
5
|
+
* general relativity. The "rubber sheet" analogy where massive objects
|
|
6
|
+
* create wells/depressions in the fabric of spacetime.
|
|
7
|
+
*
|
|
8
|
+
* Uses gravitational potential: Φ(r) = -GM/r
|
|
9
|
+
* Grid deformation: y = Σ(-M_i / |r - r_i|)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Game,
|
|
14
|
+
Painter,
|
|
15
|
+
Camera3D,
|
|
16
|
+
Text,
|
|
17
|
+
applyAnchor,
|
|
18
|
+
Position,
|
|
19
|
+
Scene,
|
|
20
|
+
verticalLayout,
|
|
21
|
+
applyLayout,
|
|
22
|
+
} from "../../src/index.js";
|
|
23
|
+
|
|
24
|
+
// Configuration
|
|
25
|
+
const CONFIG = {
|
|
26
|
+
// Grid parameters
|
|
27
|
+
gridSize: 20, // Grid extends from -gridSize to +gridSize
|
|
28
|
+
gridResolution: 40, // Number of grid lines
|
|
29
|
+
gridScale: 15, // Scale factor for grid spacing
|
|
30
|
+
|
|
31
|
+
// Physics - Gaussian well profile for smooth falloff to flat edges
|
|
32
|
+
wellDepth: 75, // Base depth (scaled by sqrt of mass)
|
|
33
|
+
wellWidth: 4.0, // Base width (scaled by mass)
|
|
34
|
+
|
|
35
|
+
// 3D view
|
|
36
|
+
rotationX: 0.7, // Initial tilt (looking down at grid)
|
|
37
|
+
rotationY: 0.3, // Initial rotation
|
|
38
|
+
perspective: 1000, // Perspective depth
|
|
39
|
+
|
|
40
|
+
// Stellar body
|
|
41
|
+
initialBody: { x: 0, z: 0, mass: 3.0, type: "blackhole" },
|
|
42
|
+
|
|
43
|
+
// Body properties by type
|
|
44
|
+
bodyTypes: {
|
|
45
|
+
blackhole: {
|
|
46
|
+
color: "#111",
|
|
47
|
+
glowColor: "rgba(100, 50, 150, 0.8)",
|
|
48
|
+
minMass: 2.0,
|
|
49
|
+
maxMass: 5.0,
|
|
50
|
+
},
|
|
51
|
+
star: {
|
|
52
|
+
color: "#ff8800",
|
|
53
|
+
glowColor: "rgba(255, 200, 50, 0.6)",
|
|
54
|
+
minMass: 0.5,
|
|
55
|
+
maxMass: 2.0,
|
|
56
|
+
},
|
|
57
|
+
neutron: {
|
|
58
|
+
color: "#88ccff",
|
|
59
|
+
glowColor: "rgba(150, 200, 255, 0.7)",
|
|
60
|
+
minMass: 1.5,
|
|
61
|
+
maxMass: 3.0,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Visual
|
|
66
|
+
gridColor: "rgba(0, 180, 255, 0.4)",
|
|
67
|
+
gridHighlight: "rgba(100, 220, 255, 0.6)",
|
|
68
|
+
wellGradientStart: "rgba(80, 0, 120, 0.3)",
|
|
69
|
+
wellGradientEnd: "rgba(0, 100, 200, 0.1)",
|
|
70
|
+
|
|
71
|
+
// Animation
|
|
72
|
+
autoRotateSpeed: 0.15, // Auto-rotate speed (radians per second)
|
|
73
|
+
pulseSpeed: 2.0, // Glow pulse speed
|
|
74
|
+
wellPulseSpeed: 1.5, // Well breathing speed
|
|
75
|
+
wellPulseAmount: 0.08, // How much the well pulses (0-1)
|
|
76
|
+
|
|
77
|
+
// Orbiting body
|
|
78
|
+
orbitRadiusMultiplier: 2.0, // Orbit at this multiple of well width (sigma)
|
|
79
|
+
orbitSpeed: 0.8, // Base orbit speed (faster for heavier central mass)
|
|
80
|
+
orbiterSize: 4, // Size of orbiting body
|
|
81
|
+
orbiterColor: "#4af", // Color of orbiter
|
|
82
|
+
orbiterGlow: "rgba(100, 180, 255, 0.6)",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
class SpacetimeDemo extends Game {
|
|
86
|
+
constructor(canvas) {
|
|
87
|
+
super(canvas);
|
|
88
|
+
this.backgroundColor = "#000008";
|
|
89
|
+
this.enableFluidSize();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
init() {
|
|
93
|
+
super.init();
|
|
94
|
+
this.time = 0;
|
|
95
|
+
|
|
96
|
+
// Create 3D camera with mouse controls and auto-rotate
|
|
97
|
+
this.camera = new Camera3D({
|
|
98
|
+
rotationX: CONFIG.rotationX,
|
|
99
|
+
rotationY: CONFIG.rotationY,
|
|
100
|
+
perspective: CONFIG.perspective,
|
|
101
|
+
minRotationX: 0.2, // Don't allow looking from below
|
|
102
|
+
maxRotationX: 1.3, // Don't flip over
|
|
103
|
+
autoRotate: true,
|
|
104
|
+
autoRotateSpeed: CONFIG.autoRotateSpeed,
|
|
105
|
+
autoRotateAxis: "y",
|
|
106
|
+
});
|
|
107
|
+
this.camera.enableMouseControl(this.canvas);
|
|
108
|
+
|
|
109
|
+
// Initialize single stellar body
|
|
110
|
+
this.body = {
|
|
111
|
+
...CONFIG.initialBody,
|
|
112
|
+
orbitPhase: Math.random() * Math.PI * 2,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Initialize orbiting test particle
|
|
116
|
+
this.orbiterAngle = 0;
|
|
117
|
+
|
|
118
|
+
// Precompute grid vertex positions (flat)
|
|
119
|
+
this.initGrid();
|
|
120
|
+
|
|
121
|
+
// Click to add bodies
|
|
122
|
+
this.canvas.addEventListener("click", (e) => this.handleClick(e));
|
|
123
|
+
|
|
124
|
+
// Setup info panel
|
|
125
|
+
this.setupInfoPanel();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
setupInfoPanel() {
|
|
129
|
+
this.infoPanel = new Scene(this, { x: 0, y: 0 });
|
|
130
|
+
applyAnchor(this.infoPanel, {
|
|
131
|
+
anchor: Position.TOP_CENTER,
|
|
132
|
+
anchorOffsetY: 150,
|
|
133
|
+
});
|
|
134
|
+
this.pipeline.add(this.infoPanel);
|
|
135
|
+
|
|
136
|
+
this.titleText = new Text(this, "Spacetime Curvature", {
|
|
137
|
+
font: "bold 16px monospace",
|
|
138
|
+
color: "#7af",
|
|
139
|
+
align: "center",
|
|
140
|
+
baseline: "middle",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.equationText = new Text(
|
|
144
|
+
this,
|
|
145
|
+
"g\u03BC\u03BD = \u03B7\u03BC\u03BD + h\u03BC\u03BD | R\u03BC\u03BD - \u00BDRg\u03BC\u03BD = 8\u03C0GT\u03BC\u03BD",
|
|
146
|
+
{
|
|
147
|
+
font: "12px monospace",
|
|
148
|
+
color: "#888",
|
|
149
|
+
align: "center",
|
|
150
|
+
baseline: "middle",
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
this.statsText = new Text(this, "Blackhole | Mass: 3.0 M\u2609", {
|
|
155
|
+
font: "12px monospace",
|
|
156
|
+
color: "#6d8",
|
|
157
|
+
align: "center",
|
|
158
|
+
baseline: "middle",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const textItems = [this.titleText, this.equationText, this.statsText];
|
|
162
|
+
const layout = verticalLayout(textItems, { spacing: 18, align: "center" });
|
|
163
|
+
applyLayout(textItems, layout.positions);
|
|
164
|
+
textItems.forEach((item) => this.infoPanel.add(item));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
initGrid() {
|
|
168
|
+
const { gridSize, gridResolution } = CONFIG;
|
|
169
|
+
this.gridVertices = [];
|
|
170
|
+
|
|
171
|
+
// Create grid of vertices
|
|
172
|
+
for (let i = 0; i <= gridResolution; i++) {
|
|
173
|
+
const row = [];
|
|
174
|
+
for (let j = 0; j <= gridResolution; j++) {
|
|
175
|
+
const x = (i / gridResolution - 0.5) * 2 * gridSize;
|
|
176
|
+
const z = (j / gridResolution - 0.5) * 2 * gridSize;
|
|
177
|
+
row.push({ x, y: 0, z });
|
|
178
|
+
}
|
|
179
|
+
this.gridVertices.push(row);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
handleClick(e) {
|
|
184
|
+
if (this.camera.isDragging()) return;
|
|
185
|
+
this.shuffleBody();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
shuffleBody() {
|
|
189
|
+
// Randomly choose body type (weighted toward black holes for dramatic effect)
|
|
190
|
+
const rand = Math.random();
|
|
191
|
+
let type = "blackhole";
|
|
192
|
+
if (rand > 0.5) type = "star";
|
|
193
|
+
else if (rand > 0.3) type = "neutron";
|
|
194
|
+
|
|
195
|
+
const typeConfig = CONFIG.bodyTypes[type];
|
|
196
|
+
const mass =
|
|
197
|
+
typeConfig.minMass +
|
|
198
|
+
Math.random() * (typeConfig.maxMass - typeConfig.minMass);
|
|
199
|
+
|
|
200
|
+
// Always centered
|
|
201
|
+
this.body = {
|
|
202
|
+
x: 0,
|
|
203
|
+
z: 0,
|
|
204
|
+
mass,
|
|
205
|
+
type,
|
|
206
|
+
orbitPhase: Math.random() * Math.PI * 2,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Calculate well depth at a point using Gaussian profile
|
|
212
|
+
* Returns positive value (depth of well)
|
|
213
|
+
* Gaussian naturally falls to 0 at edges - no clamping needed
|
|
214
|
+
* Includes subtle pulsing animation
|
|
215
|
+
*/
|
|
216
|
+
calculateWellDepth(x, z) {
|
|
217
|
+
const dx = x - this.body.x;
|
|
218
|
+
const dz = z - this.body.z;
|
|
219
|
+
const rSquared = dx * dx + dz * dz;
|
|
220
|
+
|
|
221
|
+
// Gaussian well: depth = A * exp(-r²/2σ²)
|
|
222
|
+
// More mass = wider crater (sigma increases)
|
|
223
|
+
// More mass = deeper but with diminishing returns (sqrt)
|
|
224
|
+
const sigma = CONFIG.wellWidth * Math.sqrt(this.body.mass);
|
|
225
|
+
const baseAmplitude = CONFIG.wellDepth * Math.sqrt(this.body.mass);
|
|
226
|
+
|
|
227
|
+
// Pulsing animation - well "breathes"
|
|
228
|
+
const pulse =
|
|
229
|
+
1 + CONFIG.wellPulseAmount * Math.sin(this.time * CONFIG.wellPulseSpeed);
|
|
230
|
+
const amplitude = baseAmplitude * pulse;
|
|
231
|
+
|
|
232
|
+
return amplitude * Math.exp(-rSquared / (2 * sigma * sigma));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
update(dt) {
|
|
236
|
+
super.update(dt);
|
|
237
|
+
this.time += dt;
|
|
238
|
+
|
|
239
|
+
// Update camera (handles auto-rotate when not dragging)
|
|
240
|
+
this.camera.update(dt);
|
|
241
|
+
|
|
242
|
+
// Update grid vertices based on gravitational wells
|
|
243
|
+
// Positive Y = wells curving DOWN (with current camera angle)
|
|
244
|
+
const { gridResolution } = CONFIG;
|
|
245
|
+
for (let i = 0; i <= gridResolution; i++) {
|
|
246
|
+
for (let j = 0; j <= gridResolution; j++) {
|
|
247
|
+
const vertex = this.gridVertices[i][j];
|
|
248
|
+
vertex.y = this.calculateWellDepth(vertex.x, vertex.z);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Update orbiter - faster orbit for heavier central mass (Kepler's law)
|
|
253
|
+
const orbitSpeed = CONFIG.orbitSpeed * Math.sqrt(this.body.mass);
|
|
254
|
+
this.orbiterAngle += orbitSpeed * dt;
|
|
255
|
+
|
|
256
|
+
// Update stats text
|
|
257
|
+
if (this.statsText) {
|
|
258
|
+
const typeName =
|
|
259
|
+
this.body.type.charAt(0).toUpperCase() + this.body.type.slice(1);
|
|
260
|
+
this.statsText.text = `${typeName} | Mass: ${this.body.mass.toFixed(1)} M\u2609`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
render() {
|
|
265
|
+
const w = this.width;
|
|
266
|
+
const h = this.height;
|
|
267
|
+
const cx = w / 2;
|
|
268
|
+
const cy = h / 2 + 50;
|
|
269
|
+
|
|
270
|
+
super.render();
|
|
271
|
+
|
|
272
|
+
// Draw grid lines
|
|
273
|
+
this.drawGrid(cx, cy);
|
|
274
|
+
|
|
275
|
+
// Draw orbiting body (behind or in front based on position)
|
|
276
|
+
this.drawOrbiter(cx, cy);
|
|
277
|
+
|
|
278
|
+
// Draw stellar body
|
|
279
|
+
this.drawBody(cx, cy);
|
|
280
|
+
|
|
281
|
+
// Draw controls hint
|
|
282
|
+
this.drawControls(w, h);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
drawGrid(cx, cy) {
|
|
286
|
+
const { gridResolution, gridScale, gridColor, gridHighlight } = CONFIG;
|
|
287
|
+
|
|
288
|
+
// Project all vertices
|
|
289
|
+
const projected = this.gridVertices.map((row) =>
|
|
290
|
+
row.map((v) => {
|
|
291
|
+
const p = this.camera.project(v.x * gridScale, v.y, v.z * gridScale);
|
|
292
|
+
return {
|
|
293
|
+
x: cx + p.x,
|
|
294
|
+
y: cy + p.y,
|
|
295
|
+
z: p.z,
|
|
296
|
+
depth: v.y,
|
|
297
|
+
};
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Draw grid lines along X direction
|
|
302
|
+
for (let i = 0; i <= gridResolution; i++) {
|
|
303
|
+
const isMainLine = i % 5 === 0;
|
|
304
|
+
Painter.useCtx((ctx) => {
|
|
305
|
+
ctx.strokeStyle = isMainLine ? gridHighlight : gridColor;
|
|
306
|
+
ctx.lineWidth = isMainLine ? 1.5 : 0.8;
|
|
307
|
+
ctx.beginPath();
|
|
308
|
+
|
|
309
|
+
for (let j = 0; j <= gridResolution; j++) {
|
|
310
|
+
const p = projected[i][j];
|
|
311
|
+
if (j === 0) {
|
|
312
|
+
ctx.moveTo(p.x, p.y);
|
|
313
|
+
} else {
|
|
314
|
+
ctx.lineTo(p.x, p.y);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
ctx.stroke();
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Draw grid lines along Z direction
|
|
322
|
+
for (let j = 0; j <= gridResolution; j++) {
|
|
323
|
+
const isMainLine = j % 5 === 0;
|
|
324
|
+
Painter.useCtx((ctx) => {
|
|
325
|
+
ctx.strokeStyle = isMainLine ? gridHighlight : gridColor;
|
|
326
|
+
ctx.lineWidth = isMainLine ? 1.5 : 0.8;
|
|
327
|
+
ctx.beginPath();
|
|
328
|
+
|
|
329
|
+
for (let i = 0; i <= gridResolution; i++) {
|
|
330
|
+
const p = projected[i][j];
|
|
331
|
+
if (i === 0) {
|
|
332
|
+
ctx.moveTo(p.x, p.y);
|
|
333
|
+
} else {
|
|
334
|
+
ctx.lineTo(p.x, p.y);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
ctx.stroke();
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
drawOrbiter(cx, cy) {
|
|
343
|
+
// Orbit radius scales with well width (sigma)
|
|
344
|
+
const sigma = CONFIG.wellWidth * Math.sqrt(this.body.mass);
|
|
345
|
+
const orbitRadius = sigma * CONFIG.orbitRadiusMultiplier;
|
|
346
|
+
|
|
347
|
+
// Calculate orbiter position on the grid
|
|
348
|
+
const orbiterX = this.body.x + Math.cos(this.orbiterAngle) * orbitRadius;
|
|
349
|
+
const orbiterZ = this.body.z + Math.sin(this.orbiterAngle) * orbitRadius;
|
|
350
|
+
|
|
351
|
+
// Get well depth at orbiter position (follows the curvature)
|
|
352
|
+
const wellDepth = this.calculateWellDepth(orbiterX, orbiterZ);
|
|
353
|
+
|
|
354
|
+
// Project to screen
|
|
355
|
+
const p = this.camera.project(
|
|
356
|
+
orbiterX * CONFIG.gridScale,
|
|
357
|
+
wellDepth,
|
|
358
|
+
orbiterZ * CONFIG.gridScale,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const screenX = cx + p.x;
|
|
362
|
+
const screenY = cy + p.y;
|
|
363
|
+
const size = CONFIG.orbiterSize * p.scale;
|
|
364
|
+
|
|
365
|
+
// Draw glow
|
|
366
|
+
Painter.useCtx((ctx) => {
|
|
367
|
+
const gradient = ctx.createRadialGradient(
|
|
368
|
+
screenX,
|
|
369
|
+
screenY,
|
|
370
|
+
0,
|
|
371
|
+
screenX,
|
|
372
|
+
screenY,
|
|
373
|
+
size * 3,
|
|
374
|
+
);
|
|
375
|
+
gradient.addColorStop(0, CONFIG.orbiterGlow);
|
|
376
|
+
gradient.addColorStop(1, "transparent");
|
|
377
|
+
|
|
378
|
+
ctx.fillStyle = gradient;
|
|
379
|
+
ctx.beginPath();
|
|
380
|
+
ctx.arc(screenX, screenY, size * 3, 0, Math.PI * 2);
|
|
381
|
+
ctx.fill();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Draw orbiter body
|
|
385
|
+
Painter.useCtx((ctx) => {
|
|
386
|
+
const gradient = ctx.createRadialGradient(
|
|
387
|
+
screenX - size * 0.3,
|
|
388
|
+
screenY - size * 0.3,
|
|
389
|
+
0,
|
|
390
|
+
screenX,
|
|
391
|
+
screenY,
|
|
392
|
+
size,
|
|
393
|
+
);
|
|
394
|
+
gradient.addColorStop(0, "#fff");
|
|
395
|
+
gradient.addColorStop(0.5, CONFIG.orbiterColor);
|
|
396
|
+
gradient.addColorStop(1, CONFIG.orbiterGlow);
|
|
397
|
+
|
|
398
|
+
ctx.fillStyle = gradient;
|
|
399
|
+
ctx.beginPath();
|
|
400
|
+
ctx.arc(screenX, screenY, size, 0, Math.PI * 2);
|
|
401
|
+
ctx.fill();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// Draw trail (fading arc showing recent path)
|
|
405
|
+
this.drawOrbiterTrail(cx, cy);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
drawOrbiterTrail(cx, cy) {
|
|
409
|
+
// Orbit radius scales with well width (sigma)
|
|
410
|
+
const sigma = CONFIG.wellWidth * Math.sqrt(this.body.mass);
|
|
411
|
+
const orbitRadius = sigma * CONFIG.orbitRadiusMultiplier;
|
|
412
|
+
|
|
413
|
+
const trailLength = 40; // Number of trail segments
|
|
414
|
+
const trailArc = Math.PI * 0.8; // How much of the orbit to show as trail
|
|
415
|
+
|
|
416
|
+
Painter.useCtx((ctx) => {
|
|
417
|
+
ctx.lineCap = "round";
|
|
418
|
+
|
|
419
|
+
for (let i = 0; i < trailLength; i++) {
|
|
420
|
+
const t = i / trailLength;
|
|
421
|
+
const angle = this.orbiterAngle - t * trailArc;
|
|
422
|
+
|
|
423
|
+
const trailX = this.body.x + Math.cos(angle) * orbitRadius;
|
|
424
|
+
const trailZ = this.body.z + Math.sin(angle) * orbitRadius;
|
|
425
|
+
const wellDepth = this.calculateWellDepth(trailX, trailZ);
|
|
426
|
+
|
|
427
|
+
const p = this.camera.project(
|
|
428
|
+
trailX * CONFIG.gridScale,
|
|
429
|
+
wellDepth,
|
|
430
|
+
trailZ * CONFIG.gridScale,
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
if (i === 0) continue;
|
|
434
|
+
|
|
435
|
+
const prevAngle =
|
|
436
|
+
this.orbiterAngle - ((i - 1) / trailLength) * trailArc;
|
|
437
|
+
const prevX = this.body.x + Math.cos(prevAngle) * orbitRadius;
|
|
438
|
+
const prevZ = this.body.z + Math.sin(prevAngle) * orbitRadius;
|
|
439
|
+
const prevDepth = this.calculateWellDepth(prevX, prevZ);
|
|
440
|
+
const prevP = this.camera.project(
|
|
441
|
+
prevX * CONFIG.gridScale,
|
|
442
|
+
prevDepth,
|
|
443
|
+
prevZ * CONFIG.gridScale,
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const alpha = (1 - t) * 0.5;
|
|
447
|
+
ctx.strokeStyle = `rgba(100, 180, 255, ${alpha})`;
|
|
448
|
+
ctx.lineWidth = (1 - t) * 2 * p.scale;
|
|
449
|
+
ctx.beginPath();
|
|
450
|
+
ctx.moveTo(cx + prevP.x, cy + prevP.y);
|
|
451
|
+
ctx.lineTo(cx + p.x, cy + p.y);
|
|
452
|
+
ctx.stroke();
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
drawBody(cx, cy) {
|
|
458
|
+
const body = this.body;
|
|
459
|
+
const typeConfig = CONFIG.bodyTypes[body.type];
|
|
460
|
+
|
|
461
|
+
const wellDepth = this.calculateWellDepth(body.x, body.z);
|
|
462
|
+
const p = this.camera.project(
|
|
463
|
+
body.x * CONFIG.gridScale,
|
|
464
|
+
wellDepth * 0.7, // Place body in the well
|
|
465
|
+
body.z * CONFIG.gridScale,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
const screenX = cx + p.x;
|
|
469
|
+
const screenY = cy + p.y;
|
|
470
|
+
|
|
471
|
+
// Size based on mass and perspective
|
|
472
|
+
const baseSize = 8 + body.mass * 6;
|
|
473
|
+
const size = baseSize * p.scale;
|
|
474
|
+
|
|
475
|
+
// Pulsing glow effect
|
|
476
|
+
const pulse =
|
|
477
|
+
0.8 + 0.2 * Math.sin(this.time * CONFIG.pulseSpeed + body.orbitPhase);
|
|
478
|
+
|
|
479
|
+
// Draw glow
|
|
480
|
+
Painter.useCtx((ctx) => {
|
|
481
|
+
const gradient = ctx.createRadialGradient(
|
|
482
|
+
screenX,
|
|
483
|
+
screenY,
|
|
484
|
+
0,
|
|
485
|
+
screenX,
|
|
486
|
+
screenY,
|
|
487
|
+
size * 3 * pulse,
|
|
488
|
+
);
|
|
489
|
+
gradient.addColorStop(0, typeConfig.glowColor);
|
|
490
|
+
gradient.addColorStop(1, "transparent");
|
|
491
|
+
|
|
492
|
+
ctx.fillStyle = gradient;
|
|
493
|
+
ctx.beginPath();
|
|
494
|
+
ctx.arc(screenX, screenY, size * 3 * pulse, 0, Math.PI * 2);
|
|
495
|
+
ctx.fill();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Draw body
|
|
499
|
+
Painter.useCtx((ctx) => {
|
|
500
|
+
if (body.type === "blackhole") {
|
|
501
|
+
// Black hole: dark center with bright accretion disk effect
|
|
502
|
+
ctx.fillStyle = "#000";
|
|
503
|
+
ctx.beginPath();
|
|
504
|
+
ctx.arc(screenX, screenY, size, 0, Math.PI * 2);
|
|
505
|
+
ctx.fill();
|
|
506
|
+
|
|
507
|
+
// Event horizon ring
|
|
508
|
+
ctx.strokeStyle = "rgba(150, 100, 200, 0.8)";
|
|
509
|
+
ctx.lineWidth = 2;
|
|
510
|
+
ctx.beginPath();
|
|
511
|
+
ctx.arc(screenX, screenY, size * 1.3, 0, Math.PI * 2);
|
|
512
|
+
ctx.stroke();
|
|
513
|
+
} else {
|
|
514
|
+
// Stars and neutron stars: bright center
|
|
515
|
+
const gradient = ctx.createRadialGradient(
|
|
516
|
+
screenX - size * 0.3,
|
|
517
|
+
screenY - size * 0.3,
|
|
518
|
+
0,
|
|
519
|
+
screenX,
|
|
520
|
+
screenY,
|
|
521
|
+
size,
|
|
522
|
+
);
|
|
523
|
+
gradient.addColorStop(0, "#fff");
|
|
524
|
+
gradient.addColorStop(0.3, typeConfig.color);
|
|
525
|
+
gradient.addColorStop(1, typeConfig.glowColor);
|
|
526
|
+
|
|
527
|
+
ctx.fillStyle = gradient;
|
|
528
|
+
ctx.beginPath();
|
|
529
|
+
ctx.arc(screenX, screenY, size, 0, Math.PI * 2);
|
|
530
|
+
ctx.fill();
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
drawControls(w, h) {
|
|
536
|
+
Painter.useCtx((ctx) => {
|
|
537
|
+
ctx.fillStyle = "#445";
|
|
538
|
+
ctx.font = "10px monospace";
|
|
539
|
+
ctx.textAlign = "right";
|
|
540
|
+
ctx.fillText(
|
|
541
|
+
"click to shuffle | drag to rotate | double-click to reset",
|
|
542
|
+
w - 15,
|
|
543
|
+
h - 30,
|
|
544
|
+
);
|
|
545
|
+
ctx.fillText(
|
|
546
|
+
"Mass curves spacetime | Objects follow geodesics",
|
|
547
|
+
w - 15,
|
|
548
|
+
h - 15,
|
|
549
|
+
);
|
|
550
|
+
ctx.textAlign = "left";
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
window.addEventListener("load", () => {
|
|
556
|
+
const canvas = document.getElementById("game");
|
|
557
|
+
const demo = new SpacetimeDemo(canvas);
|
|
558
|
+
demo.start();
|
|
559
|
+
});
|