@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,219 @@
|
|
|
1
|
+
import { GameObject, Sphere3D, Painter, Easing } from "../../../src/index.js";
|
|
2
|
+
import { CONFIG } from "./config.js";
|
|
3
|
+
|
|
4
|
+
export class BlackHole extends GameObject {
|
|
5
|
+
constructor(game, options = {}) {
|
|
6
|
+
super(game, options);
|
|
7
|
+
this.mass = options.initialMass ?? CONFIG.blackHole.initialMass;
|
|
8
|
+
this.baseRadius = game.baseScale ? game.baseScale * CONFIG.bhRadiusRatio : 50;
|
|
9
|
+
this.currentRadius = this.baseRadius;
|
|
10
|
+
|
|
11
|
+
// Awakening state - BH starts dormant, wakes up as it feeds
|
|
12
|
+
this.awakeningLevel = 0; // 0 = dormant (pure black), 1 = fully awake
|
|
13
|
+
this.feedingPulse = 0; // Temporary glow boost when consuming
|
|
14
|
+
this.totalConsumed = 0; // Track total mass consumed
|
|
15
|
+
|
|
16
|
+
// Dynamic growth animation
|
|
17
|
+
this.growthSpurt = 0; // Overshoot when consuming (decays to 0)
|
|
18
|
+
this.breathPhase = 0; // Oscillation phase for breathing effect
|
|
19
|
+
this.targetRadius = this.baseRadius; // Smooth radius target
|
|
20
|
+
|
|
21
|
+
// Rotation - black holes spin!
|
|
22
|
+
this.rotation = 0;
|
|
23
|
+
this.rotationSpeed = options.rotationSpeed ?? 2.9; // Slow, ominous spin
|
|
24
|
+
|
|
25
|
+
// Use WebGL shaders for rendering
|
|
26
|
+
this.useShader = options.useShader ?? true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
init() {
|
|
30
|
+
this.updateVisual();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Add mass from consumed particles - triggers awakening and pulse
|
|
35
|
+
*
|
|
36
|
+
* @param {number} amount - Amount of mass to add
|
|
37
|
+
*
|
|
38
|
+
* Note: Feeding pulse is only triggered before the stable phase.
|
|
39
|
+
* Once stabilizing, particles can still be consumed but won't
|
|
40
|
+
* cause the visual pulse effect.
|
|
41
|
+
*/
|
|
42
|
+
addConsumedMass(amount) {
|
|
43
|
+
this.totalConsumed += amount;
|
|
44
|
+
|
|
45
|
+
// Skip pulse effects if we're in the stable phase
|
|
46
|
+
if (this.isStabilizing) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Awakening increases as BH feeds (slow ramp up)
|
|
51
|
+
const awakeningProgress = Math.min(1, this.totalConsumed * 0.1);
|
|
52
|
+
this.awakeningLevel = Math.max(this.awakeningLevel, awakeningProgress);
|
|
53
|
+
|
|
54
|
+
// Feeding pulse - temporary glow boost
|
|
55
|
+
this.feedingPulse = Math.min(1, this.feedingPulse + amount * 0.2);
|
|
56
|
+
|
|
57
|
+
// Growth spurt - overshoot effect when consuming
|
|
58
|
+
// More dramatic spurts as awakening increases
|
|
59
|
+
const spurtIntensity = 0.03 + this.awakeningLevel * 0.05;
|
|
60
|
+
this.growthSpurt = Math.min(0.15, this.growthSpurt + amount * spurtIntensity);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reset to dormant state
|
|
65
|
+
*/
|
|
66
|
+
resetAwakening() {
|
|
67
|
+
this.awakeningLevel = 0;
|
|
68
|
+
this.feedingPulse = 0;
|
|
69
|
+
this.totalConsumed = 0;
|
|
70
|
+
this.rotation = 0;
|
|
71
|
+
this.growthSpurt = 0;
|
|
72
|
+
this.breathPhase = 0;
|
|
73
|
+
this.isStabilizing = false; // Reset stabilization state
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
updateVisual() {
|
|
77
|
+
// Calculate how much mass has been absorbed (0 = none, 1 = full star)
|
|
78
|
+
const massAbsorbed = Math.max(0, this.mass - CONFIG.blackHole.initialMass);
|
|
79
|
+
const absorptionProgress = massAbsorbed / CONFIG.star.initialMass;
|
|
80
|
+
|
|
81
|
+
// Apply easing to make growth feel more organic
|
|
82
|
+
// easeOutCubic: rapid initial growth that slows as it fills
|
|
83
|
+
const easedProgress = Easing.easeOutCubic(absorptionProgress);
|
|
84
|
+
|
|
85
|
+
// Base radius from absorption with easing
|
|
86
|
+
const baseScale = this.baseRadius / CONFIG.bhRadiusRatio;
|
|
87
|
+
const radiusFraction = CONFIG.bhRadiusRatio +
|
|
88
|
+
easedProgress * (CONFIG.bhFinalRadiusRatio - CONFIG.bhRadiusRatio);
|
|
89
|
+
this.targetRadius = baseScale * radiusFraction;
|
|
90
|
+
|
|
91
|
+
// Breathing oscillation - subtle when dormant, stronger when awake
|
|
92
|
+
const breathAmplitude = 0.01 + this.awakeningLevel * 0.02 + this.feedingPulse * 0.03;
|
|
93
|
+
const breathSpeed = 1.5 + this.awakeningLevel * 0.5; // Faster when active
|
|
94
|
+
const breathOffset = Math.sin(this.breathPhase * breathSpeed) * breathAmplitude;
|
|
95
|
+
|
|
96
|
+
// Growth spurt overshoot effect (elastic rebound)
|
|
97
|
+
const spurtOffset = this.growthSpurt * (1 + Math.sin(this.breathPhase * 8) * 0.3);
|
|
98
|
+
|
|
99
|
+
// Combine all effects
|
|
100
|
+
this.currentRadius = this.targetRadius * (1 + breathOffset + spurtOffset);
|
|
101
|
+
|
|
102
|
+
if (this.currentRadius <= 0) {
|
|
103
|
+
this.currentRadius = this.baseRadius;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Edge brightness increases with awakening
|
|
107
|
+
const awakeFactor = this.awakeningLevel;
|
|
108
|
+
const pulseFactor = this.feedingPulse;
|
|
109
|
+
|
|
110
|
+
// For Canvas 2D fallback - gradient rendering
|
|
111
|
+
// Dormant: pure black edges (#101010)
|
|
112
|
+
// Awake: warmer edges with hint of orange/red glow
|
|
113
|
+
const edgeBase = 16 + Math.round(awakeFactor * 24 + pulseFactor * 16); // 16-56
|
|
114
|
+
const edgeR = Math.min(255, edgeBase + Math.round(awakeFactor * 40 + pulseFactor * 60));
|
|
115
|
+
const edgeG = Math.min(255, edgeBase + Math.round(awakeFactor * 20 + pulseFactor * 30));
|
|
116
|
+
const edgeB = edgeBase;
|
|
117
|
+
|
|
118
|
+
const midBase = 8 + Math.round(awakeFactor * 12 + pulseFactor * 8);
|
|
119
|
+
const midR = Math.min(255, midBase + Math.round(awakeFactor * 20 + pulseFactor * 30));
|
|
120
|
+
const midG = Math.min(255, midBase + Math.round(awakeFactor * 10 + pulseFactor * 15));
|
|
121
|
+
const midB = midBase;
|
|
122
|
+
|
|
123
|
+
const gradient = Painter.colors.radialGradient(
|
|
124
|
+
0, 0, 0.01 * this.currentRadius,
|
|
125
|
+
0, 0, this.currentRadius,
|
|
126
|
+
[
|
|
127
|
+
{ offset: 0, color: "#000" },
|
|
128
|
+
{ offset: 0.5, color: "#000" },
|
|
129
|
+
{ offset: 0.85, color: `rgb(${midR}, ${midG}, ${midB})` },
|
|
130
|
+
{ offset: 1, color: `rgb(${edgeR}, ${edgeG}, ${edgeB})` },
|
|
131
|
+
]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!this.core) {
|
|
135
|
+
this.core = new Sphere3D(this.currentRadius, {
|
|
136
|
+
color: gradient,
|
|
137
|
+
camera: this.game.camera,
|
|
138
|
+
stroke: null, // No wireframe
|
|
139
|
+
debug: false,
|
|
140
|
+
segments: 32, // Smoother sphere
|
|
141
|
+
// WebGL shader options
|
|
142
|
+
useShader: this.useShader,
|
|
143
|
+
shaderType: "blackHole",
|
|
144
|
+
shaderUniforms: {
|
|
145
|
+
uAwakeningLevel: awakeFactor,
|
|
146
|
+
uFeedingPulse: pulseFactor,
|
|
147
|
+
uRotation: this.rotation,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
this.core.radius = this.currentRadius;
|
|
152
|
+
this.core.color = gradient; // Keep gradient for Canvas 2D fallback
|
|
153
|
+
// Update shader uniforms
|
|
154
|
+
if (this.core.useShader) {
|
|
155
|
+
this.core.setShaderUniforms({
|
|
156
|
+
uAwakeningLevel: awakeFactor,
|
|
157
|
+
uFeedingPulse: pulseFactor,
|
|
158
|
+
uRotation: this.rotation,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
this.core._generateGeometry();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
update(dt) {
|
|
166
|
+
super.update(dt);
|
|
167
|
+
|
|
168
|
+
// Animate breathing phase
|
|
169
|
+
this.breathPhase += dt * Math.PI * 2; // Full cycle per second
|
|
170
|
+
|
|
171
|
+
// Spin the black hole - rotation speeds up when feeding
|
|
172
|
+
const spinMultiplier = 1 + this.feedingPulse * 2 + this.awakeningLevel * 0.5;
|
|
173
|
+
this.rotation += this.rotationSpeed * spinMultiplier * dt;
|
|
174
|
+
|
|
175
|
+
// Decay feeding pulse over time
|
|
176
|
+
if (this.feedingPulse > 0) {
|
|
177
|
+
this.feedingPulse = Math.max(0, this.feedingPulse - dt * 1.5);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Decay growth spurt with elastic damping
|
|
181
|
+
if (this.growthSpurt > 0) {
|
|
182
|
+
// Fast initial decay, slows down (feels like elastic settling)
|
|
183
|
+
const decayRate = 3 + this.growthSpurt * 5; // Faster when larger
|
|
184
|
+
this.growthSpurt = Math.max(0, this.growthSpurt - dt * decayRate);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Decay awakening level when stabilizing (slow cool-down)
|
|
188
|
+
// Minimum level is 0.3 - never goes fully dormant after feeding
|
|
189
|
+
if (this.isStabilizing && this.awakeningLevel > 0.3) {
|
|
190
|
+
this.awakeningLevel = Math.max(0.3, this.awakeningLevel - dt * 0.15);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.updateVisual();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Start the stabilization phase - black hole calms down
|
|
198
|
+
*/
|
|
199
|
+
startStabilizing() {
|
|
200
|
+
this.isStabilizing = true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reset stabilization state
|
|
205
|
+
*/
|
|
206
|
+
resetStabilizing() {
|
|
207
|
+
this.isStabilizing = false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
onResize(baseRadius) {
|
|
211
|
+
this.baseRadius = baseRadius;
|
|
212
|
+
this.updateVisual();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
render() {
|
|
216
|
+
super.render();
|
|
217
|
+
this.core.render();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Scene3D } from "../../../src/index.js";
|
|
2
|
+
import { BlackHole } from "./blackhole.js";
|
|
3
|
+
import { Star } from "./tdestar.js";
|
|
4
|
+
import { TidalStream } from "./tidalstream.js";
|
|
5
|
+
import { AccretionDisk } from "./accretiondisk.js";
|
|
6
|
+
import { RelativisticJets } from "./jets.js";
|
|
7
|
+
import { CONFIG } from "./config.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* BlackHoleScene - Main 3D scene containing the TDE visualization
|
|
11
|
+
*
|
|
12
|
+
* Z-Order Rendering Rules:
|
|
13
|
+
* 1. BlackHole renders at the back (dark shadow)
|
|
14
|
+
* 2. AccretionDisk renders over the black hole (particles curve around)
|
|
15
|
+
* 3. TidalStream always on top of star and black hole
|
|
16
|
+
* 4. Star position relative to BlackHole changes based on camera depth
|
|
17
|
+
*
|
|
18
|
+
* Z-Index Buckets (lower = renders first = behind):
|
|
19
|
+
* - starBack: 10 (star when behind BH)
|
|
20
|
+
* - blackHole: 15 (dark shadow at back)
|
|
21
|
+
* - disk: 20 (accretion disk over BH)
|
|
22
|
+
* - starFront: 25 (star when in front of BH)
|
|
23
|
+
* - stream: 30 (particles - always on top)
|
|
24
|
+
* - jets: 40 (always on top)
|
|
25
|
+
*
|
|
26
|
+
* @extends Scene3D
|
|
27
|
+
*/
|
|
28
|
+
export class BlackHoleScene extends Scene3D {
|
|
29
|
+
constructor(game, options = {}) {
|
|
30
|
+
super(game, options);
|
|
31
|
+
// Camera is passed via options and handled by Scene3D
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Z-index buckets for render ordering
|
|
35
|
+
* Scene3D sorts by zIndex first (lower renders first/behind),
|
|
36
|
+
* then uses camera depth as tie-breaker
|
|
37
|
+
*
|
|
38
|
+
* Layering (simple):
|
|
39
|
+
* - BlackHole at back (dark shadow)
|
|
40
|
+
* - Disk renders over BH (particles curve around it)
|
|
41
|
+
* - Stream always on top of star and BH
|
|
42
|
+
* - Star moves in front of/behind BH based on camera view
|
|
43
|
+
*/
|
|
44
|
+
this.Z = {
|
|
45
|
+
starBack: 10, // Star when behind BH
|
|
46
|
+
blackHole: 15, // BlackHole: dark shadow at back
|
|
47
|
+
disk: 20, // AccretionDisk: over the black hole
|
|
48
|
+
starFront: 25, // Star when in front of BH
|
|
49
|
+
stream: 30, // TidalStream: always on top of star and BH
|
|
50
|
+
jets: 40, // Jets: always on top
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
init() {
|
|
55
|
+
// Z-order buckets: lower draws first, equal uses depth
|
|
56
|
+
// IMPORTANT: Set zIndex AFTER add() because ZOrderedCollection.add()
|
|
57
|
+
// overwrites zIndex with array index
|
|
58
|
+
|
|
59
|
+
// Add BlackHole at (0,0,0)
|
|
60
|
+
this.bh = new BlackHole(this.game);
|
|
61
|
+
this.add(this.bh);
|
|
62
|
+
this.bh.zIndex = this.Z.blackHole; // Set AFTER add()
|
|
63
|
+
|
|
64
|
+
// Add a Star orbiting the black hole
|
|
65
|
+
this.star = new Star(this.game);
|
|
66
|
+
this.add(this.star);
|
|
67
|
+
this.star.zIndex = this.Z.starFront; // Set AFTER add(), will be updated per-frame
|
|
68
|
+
|
|
69
|
+
// Add accretion disk (starts inactive)
|
|
70
|
+
this.disk = new AccretionDisk(this.game, {
|
|
71
|
+
camera: this.game.camera,
|
|
72
|
+
bhRadius: this.game.baseScale * CONFIG.bhRadiusRatio,
|
|
73
|
+
bhMass: CONFIG.blackHole.initialMass,
|
|
74
|
+
onParticleConsumed: () => {
|
|
75
|
+
this.bh.addConsumedMass(0.1); // Disk particles worth more
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
this.add(this.disk);
|
|
79
|
+
this.disk.zIndex = this.Z.disk; // Set AFTER add()
|
|
80
|
+
|
|
81
|
+
// Add tidal stream for particles
|
|
82
|
+
// When particles are consumed, feed the black hole
|
|
83
|
+
// When particles circularize, transfer to accretion disk
|
|
84
|
+
this.stream = new TidalStream(this.game, {
|
|
85
|
+
camera: this.game.camera,
|
|
86
|
+
scene: this, // Pass scene reference for screen center
|
|
87
|
+
bhRadius: this.game.baseScale * CONFIG.bhRadiusRatio,
|
|
88
|
+
diskInnerRadius: this.disk.innerRadius,
|
|
89
|
+
diskOuterRadius: this.disk.outerRadius,
|
|
90
|
+
onParticleConsumed: () => {
|
|
91
|
+
this.bh.addConsumedMass(0.05); // Each particle adds small amount
|
|
92
|
+
},
|
|
93
|
+
onParticleCaptured: (p) => {
|
|
94
|
+
this.disk.captureParticle(p);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
this.add(this.stream);
|
|
98
|
+
this.stream.zIndex = this.Z.stream; // Set AFTER add()
|
|
99
|
+
|
|
100
|
+
// Add relativistic jets (activated during flare phase)
|
|
101
|
+
this.jets = new RelativisticJets(this.game, {
|
|
102
|
+
camera: this.game.camera,
|
|
103
|
+
bhRadius: this.game.baseScale * CONFIG.bhRadiusRatio,
|
|
104
|
+
});
|
|
105
|
+
this.add(this.jets);
|
|
106
|
+
this.jets.zIndex = this.Z.jets; // Set AFTER add()
|
|
107
|
+
|
|
108
|
+
// Mark z-order as dirty to ensure initial sort
|
|
109
|
+
if (this._collection) {
|
|
110
|
+
this._collection._zOrderDirty = true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
update(dt) {
|
|
115
|
+
super.update(dt);
|
|
116
|
+
|
|
117
|
+
// Sync all particle systems with current BH radius (grows as it feeds)
|
|
118
|
+
if (this.bh) {
|
|
119
|
+
if (this.stream) {
|
|
120
|
+
this.stream.updateBHRadius(this.bh.currentRadius);
|
|
121
|
+
}
|
|
122
|
+
if (this.disk) {
|
|
123
|
+
this.disk.updateBHRadius(this.bh.currentRadius);
|
|
124
|
+
// Also sync stream's disk bounds for capture detection
|
|
125
|
+
if (this.stream) {
|
|
126
|
+
this.stream.updateDiskBounds(this.disk.innerRadius, this.disk.outerRadius);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (this.jets) {
|
|
130
|
+
this.jets.updateBHRadius(this.bh.currentRadius);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Note: Z-order update is called from TDEDemo.update() AFTER star position
|
|
135
|
+
// is updated, to ensure we use the current frame's position, not the previous frame's.
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Update star's z-index based on its depth relative to the black hole
|
|
140
|
+
*
|
|
141
|
+
* Dynamic z-ordering:
|
|
142
|
+
* - When star in front of BH: star renders on top of BH
|
|
143
|
+
* - When star behind BH: BH renders on top of star
|
|
144
|
+
* - Stream (particles) always stays on top of both
|
|
145
|
+
*
|
|
146
|
+
* Camera3D.project() returns z as depth after rotation:
|
|
147
|
+
* - Lower z = closer to camera
|
|
148
|
+
* - Higher z = farther from camera
|
|
149
|
+
*
|
|
150
|
+
* IMPORTANT: Must be called AFTER star position is updated each frame.
|
|
151
|
+
*/
|
|
152
|
+
updateStarZOrder() {
|
|
153
|
+
if (!this.star || !this.bh || !this.game?.camera) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const camera = this.game.camera;
|
|
158
|
+
|
|
159
|
+
// Project star position to get its depth after camera rotation
|
|
160
|
+
const projStar = camera.project(
|
|
161
|
+
this.star.x || 0,
|
|
162
|
+
this.star.y || 0,
|
|
163
|
+
this.star.z || 0
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Black hole is always at origin (0, 0, 0)
|
|
167
|
+
const projBH = camera.project(0, 0, 0);
|
|
168
|
+
|
|
169
|
+
// Star is "in front" if its depth is LESS than BH's depth
|
|
170
|
+
const starInFront = projStar.z < projBH.z;
|
|
171
|
+
|
|
172
|
+
// Update star's z-index
|
|
173
|
+
const newZIndex = starInFront ? this.Z.starFront : this.Z.starBack;
|
|
174
|
+
|
|
175
|
+
if (this.star.zIndex !== newZIndex) {
|
|
176
|
+
this.star.zIndex = newZIndex;
|
|
177
|
+
// Mark ordering dirty so Scene3D resort happens this frame
|
|
178
|
+
if (this._collection) {
|
|
179
|
+
this._collection._zOrderDirty = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
onResize() {
|
|
185
|
+
const game = this.game;
|
|
186
|
+
if (this.bh) {
|
|
187
|
+
this.bh.onResize(game.baseScale * CONFIG.bhRadiusRatio);
|
|
188
|
+
}
|
|
189
|
+
if (this.star) {
|
|
190
|
+
this.star.onResize(
|
|
191
|
+
game.baseScale * CONFIG.starRadiusRatio,
|
|
192
|
+
game.baseScale * CONFIG.star.initialOrbitRadius
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (this.disk && this.bh) {
|
|
196
|
+
this.disk.updateBHRadius(this.bh.currentRadius);
|
|
197
|
+
}
|
|
198
|
+
if (this.stream && this.bh) {
|
|
199
|
+
this.stream.updateBHRadius(this.bh.currentRadius);
|
|
200
|
+
// Update disk bounds for capture detection
|
|
201
|
+
if (this.disk) {
|
|
202
|
+
this.stream.updateDiskBounds(this.disk.innerRadius, this.disk.outerRadius);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (this.jets && this.bh) {
|
|
206
|
+
this.jets.updateBHRadius(this.bh.currentRadius);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const CONFIG = {
|
|
2
|
+
// Sizing (as fraction of screen baseScale)
|
|
3
|
+
bhRadiusRatio: 0.01, // Initial dormant black hole size (larger for visible lensing)
|
|
4
|
+
bhFinalRadiusRatio: 0.12, // Final size after consuming star
|
|
5
|
+
starRadiusRatio: 0.08,
|
|
6
|
+
|
|
7
|
+
// Phase durations (seconds)
|
|
8
|
+
durations: {
|
|
9
|
+
approach: 10.0, // Stable wide orbit
|
|
10
|
+
stretch: 10.0, // Orbit begins to decay
|
|
11
|
+
disrupt: 20.0, // Mass transfer (event-based exit)
|
|
12
|
+
accrete: 1.0, // Debris accretion
|
|
13
|
+
flare: 5.0, // Jets firing - spectacular cosmic event!
|
|
14
|
+
stable: Infinity, // Final stable state
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
// Flash effect (now handled by Tweenetik in FSM)
|
|
18
|
+
|
|
19
|
+
// Physics params
|
|
20
|
+
blackHole: {
|
|
21
|
+
initialMass: 2,
|
|
22
|
+
color: "#000",
|
|
23
|
+
},
|
|
24
|
+
star: {
|
|
25
|
+
initialMass: 25,
|
|
26
|
+
color: "#FF6030", // Deep red-orange (cooler K/M type star)
|
|
27
|
+
// Orbit sizing (fraction of half the smaller screen dimension)
|
|
28
|
+
// apoapsis = initialOrbitRadius * (1 + eccentricity)
|
|
29
|
+
// periapsis = initialOrbitRadius * (1 - eccentricity)
|
|
30
|
+
initialOrbitRadius: 1.2, // Semi-major axis
|
|
31
|
+
eccentricity: 0.25, // Lower = more circular, periapsis closer to edge
|
|
32
|
+
// With these values:
|
|
33
|
+
// periapsis (right) = 1.2 * 0.75 = 0.9 (90% to edge - visible on RIGHT side!)
|
|
34
|
+
// apoapsis (left) = 1.2 * 1.25 = 1.5 (goes off screen left)
|
|
35
|
+
orbitSpeed: 0.4,
|
|
36
|
+
decayRate: 0.4,
|
|
37
|
+
massTransferStart: 0.1,
|
|
38
|
+
rotationSpeed: 0.71,
|
|
39
|
+
temperature: 3800,
|
|
40
|
+
orbitCenterX: 0,
|
|
41
|
+
orbitCenterY: 0,
|
|
42
|
+
bypassConstraints: true,
|
|
43
|
+
// Start angle: star should be VISIBLE at start, then swing through orbit
|
|
44
|
+
// 0 = right (periapsis), π/2 = top, π = left (apoapsis), 3π/2 = bottom
|
|
45
|
+
startAngle: Math.PI * 1.85, // Start lower-right, comes FROM right, swings up and around
|
|
46
|
+
},
|
|
47
|
+
sceneOptions: {
|
|
48
|
+
starCount: 3000,
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Accretion disk settings
|
|
52
|
+
disk: {
|
|
53
|
+
innerRadiusRatio: 0.03, // ISCO (innermost stable orbit)
|
|
54
|
+
outerRadiusRatio: 0.15, // Outer halo edge
|
|
55
|
+
maxParticles: 2000,
|
|
56
|
+
orbitalSpeed: 0.8,
|
|
57
|
+
activationProgress: 0.8, // Start at 80% of disrupt phase
|
|
58
|
+
},
|
|
59
|
+
};
|