@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,695 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Game,
|
|
3
|
+
Camera3D,
|
|
4
|
+
StateMachine,
|
|
5
|
+
Painter,
|
|
6
|
+
Text,
|
|
7
|
+
Button,
|
|
8
|
+
Tweenetik,
|
|
9
|
+
Easing,
|
|
10
|
+
} from "../../../src/index.js";
|
|
11
|
+
import { FPSCounter } from "../../../src/game/ui/fps.js";
|
|
12
|
+
import { LensedStarfield } from "./lensedstarfield.js";
|
|
13
|
+
import { polarToCartesian } from "../../../src/math/gr.js";
|
|
14
|
+
import { keplerianOmega, decayingOrbitalRadius, orbitalRadius } from "../../../src/math/orbital.js";
|
|
15
|
+
import { applyAnchor } from "../../../src/mixins/anchor.js";
|
|
16
|
+
import { Position } from "../../../src/util/position.js";
|
|
17
|
+
|
|
18
|
+
import { CONFIG } from "./config.js";
|
|
19
|
+
import { BlackHoleScene } from "./blackholescene.js";
|
|
20
|
+
|
|
21
|
+
export class TDEDemo extends Game {
|
|
22
|
+
constructor(canvas) {
|
|
23
|
+
super(canvas);
|
|
24
|
+
this.enableFluidSize();
|
|
25
|
+
this.backgroundColor = "#020202";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
updateScaledSizes() {
|
|
29
|
+
this.baseScale = Math.min(this.width, this.height);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
init() {
|
|
33
|
+
super.init();
|
|
34
|
+
this.updateScaledSizes();
|
|
35
|
+
|
|
36
|
+
this.camera = new Camera3D({
|
|
37
|
+
rotationX: 0.3,
|
|
38
|
+
rotationY: 0,
|
|
39
|
+
rotationZ: 0,
|
|
40
|
+
perspective: this.baseScale * 1.8, // Zoomed out for wider view
|
|
41
|
+
autoRotate: true,
|
|
42
|
+
autoRotateSpeed: 0.08,
|
|
43
|
+
// Inertia for smooth camera drag
|
|
44
|
+
inertia: true,
|
|
45
|
+
friction: 0.94, // 0.9 = fast stop, 0.98 = slow drift
|
|
46
|
+
velocityScale: 1.2, // Throw strength multiplier
|
|
47
|
+
});
|
|
48
|
+
this.camera.enableMouseControl(this.canvas);
|
|
49
|
+
|
|
50
|
+
// Apply orbit center offset to shift the scene (and thus the orbit) on screen
|
|
51
|
+
const orbitOffsetX = (CONFIG.star.orbitCenterX || 0) * this.width;
|
|
52
|
+
const orbitOffsetY = (CONFIG.star.orbitCenterY || 0) * this.height;
|
|
53
|
+
|
|
54
|
+
this.scene = new BlackHoleScene(this, {
|
|
55
|
+
camera: this.camera,
|
|
56
|
+
x: this.width / 2 + orbitOffsetX,
|
|
57
|
+
y: this.height / 2 + orbitOffsetY,
|
|
58
|
+
});
|
|
59
|
+
this.pipeline.add(this.scene);
|
|
60
|
+
|
|
61
|
+
// Initialize scene sizes and positions before first frame
|
|
62
|
+
this.scene.onResize();
|
|
63
|
+
|
|
64
|
+
// Create lensed starfield AFTER scene so we can pass the black hole reference
|
|
65
|
+
this.starField = new LensedStarfield(this, {
|
|
66
|
+
camera: this.camera,
|
|
67
|
+
starCount: CONFIG.sceneOptions.starCount,
|
|
68
|
+
blackHole: this.scene.bh,
|
|
69
|
+
lensingStrength: 0, // Starts at 0, ramps up during disruption
|
|
70
|
+
});
|
|
71
|
+
this.pipeline.add(this.starField);
|
|
72
|
+
this.pipeline.sendToBack(this.starField);
|
|
73
|
+
|
|
74
|
+
// Flash overlay for dramatic collision moment
|
|
75
|
+
this.flashIntensity = 0;
|
|
76
|
+
|
|
77
|
+
// Phase Info Label (bottom left)
|
|
78
|
+
this.infoLabel = new Text(this, "", {
|
|
79
|
+
color: "#888",
|
|
80
|
+
font: "14px monospace",
|
|
81
|
+
});
|
|
82
|
+
applyAnchor(this.infoLabel, {
|
|
83
|
+
anchor: Position.BOTTOM_LEFT,
|
|
84
|
+
anchorOffsetX: -60,
|
|
85
|
+
anchorMargin: 20,
|
|
86
|
+
});
|
|
87
|
+
this.infoLabel.zIndex = 100;
|
|
88
|
+
this.pipeline.add(this.infoLabel);
|
|
89
|
+
|
|
90
|
+
// Replay button (above phase text)
|
|
91
|
+
this.replayButton = new Button(this, {
|
|
92
|
+
width: 120,
|
|
93
|
+
height: 32,
|
|
94
|
+
text: "▶ Replay",
|
|
95
|
+
font: "14px monospace",
|
|
96
|
+
colorDefaultBg: "rgba(0, 0, 0, 0.6)",
|
|
97
|
+
colorDefaultStroke: "#666",
|
|
98
|
+
colorDefaultText: "#aaa",
|
|
99
|
+
colorHoverBg: "rgba(30, 30, 30, 0.8)",
|
|
100
|
+
colorHoverStroke: "#ff8844",
|
|
101
|
+
colorHoverText: "#ff8844",
|
|
102
|
+
colorPressedBg: "rgba(50, 50, 50, 0.9)",
|
|
103
|
+
colorPressedStroke: "#ffaa66",
|
|
104
|
+
colorPressedText: "#ffaa66",
|
|
105
|
+
onClick: () => this.restart(),
|
|
106
|
+
});
|
|
107
|
+
applyAnchor(this.replayButton, {
|
|
108
|
+
anchor: Position.BOTTOM_LEFT,
|
|
109
|
+
anchorMargin: 20,
|
|
110
|
+
anchorOffsetY: -30, // Above the phase text
|
|
111
|
+
});
|
|
112
|
+
this.replayButton.zIndex = 100;
|
|
113
|
+
this.pipeline.add(this.replayButton);
|
|
114
|
+
|
|
115
|
+
// FPS Counter (bottom right)
|
|
116
|
+
this.fpsCounter = new FPSCounter(this, {
|
|
117
|
+
font: "12px monospace",
|
|
118
|
+
color: "#666",
|
|
119
|
+
});
|
|
120
|
+
applyAnchor(this.fpsCounter, {
|
|
121
|
+
anchor: Position.BOTTOM_RIGHT,
|
|
122
|
+
anchorMargin: 20,
|
|
123
|
+
});
|
|
124
|
+
this.fpsCounter.zIndex = 100;
|
|
125
|
+
this.pipeline.add(this.fpsCounter);
|
|
126
|
+
|
|
127
|
+
this.initStateMachine();
|
|
128
|
+
|
|
129
|
+
// Initialize state properly (same as restart)
|
|
130
|
+
this.restart();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
initStateMachine() {
|
|
134
|
+
this.fsm = new StateMachine({
|
|
135
|
+
initial: "approach",
|
|
136
|
+
context: this,
|
|
137
|
+
states: {
|
|
138
|
+
approach: {
|
|
139
|
+
// Star in stable wide orbit
|
|
140
|
+
duration: CONFIG.durations.approach,
|
|
141
|
+
next: "stretch",
|
|
142
|
+
},
|
|
143
|
+
stretch: {
|
|
144
|
+
// Orbit begins to decay, tidal forces start
|
|
145
|
+
duration: CONFIG.durations.stretch,
|
|
146
|
+
next: "disrupt",
|
|
147
|
+
enter: () => {
|
|
148
|
+
// TIDAL TRAUMA - dramatic visual feedback when disruption begins
|
|
149
|
+
const star = this.scene?.star;
|
|
150
|
+
if (star) {
|
|
151
|
+
// === VIOLENT BRIGHTNESS FLARE ===
|
|
152
|
+
star.tidalFlare = 2.0;
|
|
153
|
+
// Fade over 5 seconds
|
|
154
|
+
Tweenetik.to(star, { tidalFlare: 0 }, 5.0, Easing.easeOutQuad);
|
|
155
|
+
|
|
156
|
+
// === GEOMETRY WOBBLE - comet-like trauma ===
|
|
157
|
+
// Spike the wobble high - violent shaking
|
|
158
|
+
star.tidalWobble = 1.2;
|
|
159
|
+
// Fade back to stable over 6 seconds (star tries to recover)
|
|
160
|
+
Tweenetik.to(star, { tidalWobble: 0.1 }, 6.0, Easing.easeOutElastic);
|
|
161
|
+
|
|
162
|
+
// === SUDDEN STRETCH SPIKE - comet shape ===
|
|
163
|
+
// Force immediate elongation like panels 2-3 in reference
|
|
164
|
+
star.tidalStretch = 0.8;
|
|
165
|
+
// Ease back toward spherical (but not fully - it can't recover)
|
|
166
|
+
Tweenetik.to(star, { tidalStretch: 0.2 }, 4.0, Easing.easeOutQuad);
|
|
167
|
+
|
|
168
|
+
// === STRESS SPIKE ===
|
|
169
|
+
star.stressLevel = 0.6;
|
|
170
|
+
// Slowly calm down but not fully
|
|
171
|
+
Tweenetik.to(star, { stressLevel: 0.25 }, 5.0, Easing.easeOutQuad);
|
|
172
|
+
|
|
173
|
+
// === PARTICLE BURST ===
|
|
174
|
+
if (this.scene.stream) {
|
|
175
|
+
for (let i = 0; i < 80; i++) {
|
|
176
|
+
this.emitStreamParticles(0.016, 1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
disrupt: {
|
|
183
|
+
// No duration - transitions via trigger when star mass depletes
|
|
184
|
+
on: {
|
|
185
|
+
starConsumed: "accrete",
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
accrete: {
|
|
189
|
+
// Debris accretion phase - triggers flash
|
|
190
|
+
duration: CONFIG.durations.accrete,
|
|
191
|
+
next: "flare",
|
|
192
|
+
enter: () => {
|
|
193
|
+
// White flash with dramatic falloff
|
|
194
|
+
this.flashIntensity = 1.0;
|
|
195
|
+
Tweenetik.to(this, { flashIntensity: 0 }, 5.0, Easing.easeOutExpo);
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
flare: {
|
|
199
|
+
// Luminous flare after consumption - JETS FIRE!
|
|
200
|
+
duration: CONFIG.durations.flare,
|
|
201
|
+
next: "stable",
|
|
202
|
+
enter: () => {
|
|
203
|
+
// Second flash for jet ignition
|
|
204
|
+
this.flashIntensity = 0.8;
|
|
205
|
+
Tweenetik.to(this, { flashIntensity: 0 }, 5, Easing.easeOutExpo);
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
stable: {
|
|
209
|
+
// Terminal state - button to restart
|
|
210
|
+
duration: CONFIG.durations.stable,
|
|
211
|
+
enter: () => {
|
|
212
|
+
// Black hole calms down after feeding
|
|
213
|
+
this.scene.bh.startStabilizing();
|
|
214
|
+
// Show replay button
|
|
215
|
+
if (this.replayButton) this.replayButton.visible = true;
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
restart() {
|
|
223
|
+
// Reset masses
|
|
224
|
+
this.scene.bh.mass = CONFIG.blackHole.initialMass;
|
|
225
|
+
this.scene.star.mass = CONFIG.star.initialMass;
|
|
226
|
+
this.scene.star.initialMass = CONFIG.star.initialMass;
|
|
227
|
+
|
|
228
|
+
// Store eccentricity
|
|
229
|
+
const eccentricity = CONFIG.star.eccentricity || 0;
|
|
230
|
+
this.scene.star.eccentricity = eccentricity;
|
|
231
|
+
|
|
232
|
+
// Calculate semi-major axis based on baseScale (smaller of width/height)
|
|
233
|
+
// This ensures the orbit fits on any aspect ratio screen
|
|
234
|
+
// initialOrbitRadius is a fraction of baseScale (e.g., 0.5 = half of smaller dimension)
|
|
235
|
+
let semiMajorAxis = (this.baseScale / 2) * CONFIG.star.initialOrbitRadius;
|
|
236
|
+
|
|
237
|
+
// Option to bypass constraints for full control over orbit size
|
|
238
|
+
if (!CONFIG.star.bypassConstraints) {
|
|
239
|
+
// Apply constraints to keep orbit somewhat on-screen
|
|
240
|
+
const starRadius = this.baseScale * CONFIG.starRadiusRatio;
|
|
241
|
+
const allowOffscreen = CONFIG.star.allowOffscreen ?? 0;
|
|
242
|
+
const effectiveMargin = starRadius * (1 - allowOffscreen);
|
|
243
|
+
|
|
244
|
+
const orbitOffsetX = (CONFIG.star.orbitCenterX || 0) * this.width;
|
|
245
|
+
const orbitOffsetY = (CONFIG.star.orbitCenterY || 0) * this.height;
|
|
246
|
+
|
|
247
|
+
const leftEdgeDist = (this.width / 2) + orbitOffsetX;
|
|
248
|
+
const rightEdgeDist = (this.width / 2) - orbitOffsetX;
|
|
249
|
+
const maxHorizontalExtent = Math.max(leftEdgeDist, rightEdgeDist) - effectiveMargin;
|
|
250
|
+
const maxSafeSemiMajorH = maxHorizontalExtent / (1 + eccentricity);
|
|
251
|
+
|
|
252
|
+
const practicalTilt = Math.min(Math.abs(this.camera._initialRotationX || 0.3) * 2, 0.6);
|
|
253
|
+
const topEdgeDist = (this.height / 2) + orbitOffsetY;
|
|
254
|
+
const bottomEdgeDist = (this.height / 2) - orbitOffsetY;
|
|
255
|
+
const maxVerticalDisplacement = Math.max(topEdgeDist, bottomEdgeDist) - effectiveMargin;
|
|
256
|
+
const tiltFactor = Math.abs(Math.sin(practicalTilt));
|
|
257
|
+
const maxSafeSemiMajorV = tiltFactor > 0.01 ? maxVerticalDisplacement / tiltFactor : Infinity;
|
|
258
|
+
|
|
259
|
+
semiMajorAxis = Math.min(semiMajorAxis, maxSafeSemiMajorH, maxSafeSemiMajorV);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.scene.star.semiMajorAxis = semiMajorAxis;
|
|
263
|
+
this.scene.star.initialSemiMajorAxis = semiMajorAxis;
|
|
264
|
+
// Keep orbitalRadius for compatibility (will be updated each frame)
|
|
265
|
+
this.scene.star.orbitalRadius = semiMajorAxis;
|
|
266
|
+
this.scene.star.initialOrbitalRadius = semiMajorAxis;
|
|
267
|
+
|
|
268
|
+
// Start angle: 0 = periapsis (right side), π = apoapsis (left side)
|
|
269
|
+
const startAngle = CONFIG.star.startAngle ?? Math.PI;
|
|
270
|
+
this.scene.star.phi = startAngle;
|
|
271
|
+
|
|
272
|
+
// Reset position to starting angle
|
|
273
|
+
const r = orbitalRadius(semiMajorAxis, eccentricity, startAngle);
|
|
274
|
+
const pos = polarToCartesian(r, startAngle);
|
|
275
|
+
this.scene.star.x = pos.x;
|
|
276
|
+
this.scene.star.z = pos.z;
|
|
277
|
+
|
|
278
|
+
// Reset velocity tracking to avoid spike after position reset
|
|
279
|
+
this.scene.star.resetVelocity();
|
|
280
|
+
|
|
281
|
+
// Clear tidal stream particles
|
|
282
|
+
if (this.scene.stream) {
|
|
283
|
+
this.scene.stream.clear();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Reset accretion disk
|
|
287
|
+
if (this.scene.disk) {
|
|
288
|
+
this.scene.disk.clear();
|
|
289
|
+
this.scene.disk.active = false;
|
|
290
|
+
this.scene.disk.lensingStrength = 0;
|
|
291
|
+
this.scene.disk.scale = 0;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Reset relativistic jets
|
|
295
|
+
if (this.scene.jets) {
|
|
296
|
+
this.scene.jets.clear();
|
|
297
|
+
}
|
|
298
|
+
this._lastJetPulse = -1;
|
|
299
|
+
|
|
300
|
+
// Reset black hole to dormant state
|
|
301
|
+
if (this.scene.bh) {
|
|
302
|
+
this.scene.bh.resetAwakening();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Reset flash
|
|
306
|
+
this.flashIntensity = 0;
|
|
307
|
+
|
|
308
|
+
// Reset starfield lensing to subtle base level
|
|
309
|
+
if (this.starField) {
|
|
310
|
+
this.starField.lensingStrength = 0.15;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Hide replay button
|
|
314
|
+
if (this.replayButton) this.replayButton.visible = false;
|
|
315
|
+
|
|
316
|
+
this.fsm.setState("approach");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Emit particles from the star's center position
|
|
321
|
+
*
|
|
322
|
+
* The trailing effect happens NATURALLY because:
|
|
323
|
+
* - Each frame, particles are emitted at star's CURRENT position
|
|
324
|
+
* - As star moves, older particles stay at their emission positions
|
|
325
|
+
* - This creates a trail without needing artificial offsets
|
|
326
|
+
*
|
|
327
|
+
* No velocity-based offset is applied because that only looks correct
|
|
328
|
+
* from specific camera angles. Emitting at center works from any angle.
|
|
329
|
+
*/
|
|
330
|
+
emitStreamParticles(dt, rate) {
|
|
331
|
+
const star = this.scene.star;
|
|
332
|
+
const stream = this.scene.stream;
|
|
333
|
+
|
|
334
|
+
if (!stream || star.mass <= 0) return;
|
|
335
|
+
|
|
336
|
+
// Use star's ACTUAL current radius (shrinks during disruption)
|
|
337
|
+
const currentRadius = star.currentRadius;
|
|
338
|
+
|
|
339
|
+
// Use star's ACTUAL tracked velocity for particle inheritance
|
|
340
|
+
const vx = star.velocityX || 0;
|
|
341
|
+
const vz = star.velocityZ || 0;
|
|
342
|
+
|
|
343
|
+
// Add radial offset to compensate for visual projection mismatch
|
|
344
|
+
// Calculate in full 3D space including Y
|
|
345
|
+
const starY = star.y || 0;
|
|
346
|
+
const dist = Math.sqrt(star.x * star.x + starY * starY + star.z * star.z) || 1;
|
|
347
|
+
const radialX = star.x / dist; // Unit vector pointing away from BH (3D)
|
|
348
|
+
const radialY = starY / dist;
|
|
349
|
+
const radialZ = star.z / dist;
|
|
350
|
+
|
|
351
|
+
// Make offset proportional to current orbital distance
|
|
352
|
+
// As orbit shrinks, offset shrinks proportionally
|
|
353
|
+
const orbitOffsetRatio = 1.09; // 80% of current orbital radius as offset
|
|
354
|
+
const orbitOffset = dist * orbitOffsetRatio;
|
|
355
|
+
|
|
356
|
+
// Also add tangential offset (along velocity direction) to compensate for arc lag
|
|
357
|
+
const speed = Math.sqrt(vx * vx + vz * vz) || 1;
|
|
358
|
+
const velDirX = vx / speed;
|
|
359
|
+
const velDirZ = vz / speed;
|
|
360
|
+
const tangentOffset = 15; // pixels ahead along orbit path
|
|
361
|
+
|
|
362
|
+
const emitX = star.x + radialX * orbitOffset + velDirX * tangentOffset;
|
|
363
|
+
const emitY = starY + radialY * orbitOffset;
|
|
364
|
+
const emitZ = star.z + radialZ * orbitOffset + velDirZ * tangentOffset;
|
|
365
|
+
|
|
366
|
+
for (let i = 0; i < rate; i++) {
|
|
367
|
+
stream.emit(
|
|
368
|
+
emitX, emitY, emitZ,
|
|
369
|
+
vx, 0, vz,
|
|
370
|
+
currentRadius,
|
|
371
|
+
star.rotation || 0,
|
|
372
|
+
star.currentColor // Pass star's current color for particle emission
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
update(dt) {
|
|
378
|
+
super.update(dt);
|
|
379
|
+
if (this.camera) this.camera.update(dt);
|
|
380
|
+
Tweenetik.updateAll(dt);
|
|
381
|
+
if (!this.fsm) return;
|
|
382
|
+
this.fsm.update(dt);
|
|
383
|
+
|
|
384
|
+
const state = this.fsm.state;
|
|
385
|
+
const progress = this.fsm.progress;
|
|
386
|
+
const star = this.scene.star;
|
|
387
|
+
const bh = this.scene.bh;
|
|
388
|
+
|
|
389
|
+
// Flash is now handled by Tweenetik in FSM enter callbacks
|
|
390
|
+
|
|
391
|
+
// Update info label
|
|
392
|
+
if (this.infoLabel) {
|
|
393
|
+
// Calculate display progress (disrupt uses stateTime, not duration-based progress)
|
|
394
|
+
let displayProgress;
|
|
395
|
+
if (state === "disrupt") {
|
|
396
|
+
displayProgress = Math.min(1, this.fsm.stateTime / CONFIG.durations.disrupt);
|
|
397
|
+
} else if (state === "stable") {
|
|
398
|
+
displayProgress = 1;
|
|
399
|
+
} else {
|
|
400
|
+
displayProgress = progress;
|
|
401
|
+
}
|
|
402
|
+
const pPercent = Math.round(displayProgress * 100);
|
|
403
|
+
|
|
404
|
+
const stateLabels = {
|
|
405
|
+
approach: "Star Approaching",
|
|
406
|
+
stretch: "Tidal Stretching",
|
|
407
|
+
disrupt: "Stellar Disruption",
|
|
408
|
+
accrete: "Debris Accretion",
|
|
409
|
+
flare: "Luminous Flare",
|
|
410
|
+
stable: "Stable Disk",
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const stateColors = {
|
|
414
|
+
approach: "#88f",
|
|
415
|
+
stretch: "#fa8",
|
|
416
|
+
disrupt: "#f88",
|
|
417
|
+
accrete: "#ff8",
|
|
418
|
+
flare: "#fff",
|
|
419
|
+
stable: "#8a8",
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
this.infoLabel.color = stateColors[state] || "#888";
|
|
423
|
+
|
|
424
|
+
if (state === "stable") {
|
|
425
|
+
this.infoLabel.text = `${stateLabels[state]}`;
|
|
426
|
+
} else {
|
|
427
|
+
this.infoLabel.text = `${stateLabels[state]}: ${pPercent}%`;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// === GRAVITATIONAL LENSING RAMP ===
|
|
432
|
+
// Black holes always warp spacetime - subtle base, INTENSE when feeding
|
|
433
|
+
if (this.starField) {
|
|
434
|
+
const baseLensing = 0.15; // Subtle base - dormant BH
|
|
435
|
+
if (state === "approach") {
|
|
436
|
+
this.starField.lensingStrength = baseLensing;
|
|
437
|
+
} else if (state === "stretch") {
|
|
438
|
+
// Gentle ramp: 0.15 -> 0.4
|
|
439
|
+
this.starField.lensingStrength = baseLensing + progress * 0.25;
|
|
440
|
+
} else if (state === "disrupt") {
|
|
441
|
+
// AGGRESSIVE ramp: 0.4 -> 2.0 (goes past 1.0 for dramatic effect)
|
|
442
|
+
const disruptProgress = Math.min(1, this.fsm.stateTime / CONFIG.durations.disrupt);
|
|
443
|
+
this.starField.lensingStrength = 0.4 + disruptProgress * 1.6;
|
|
444
|
+
} else {
|
|
445
|
+
// Max lensing during accrete/flare/stable
|
|
446
|
+
this.starField.lensingStrength = 2.0;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Store star's position before updates for accurate velocity computation
|
|
451
|
+
const oldStarX = star.x;
|
|
452
|
+
const oldStarZ = star.z;
|
|
453
|
+
|
|
454
|
+
// Approach phase: Elliptical orbit - star swings from apoapsis toward periapsis
|
|
455
|
+
if (state === "approach") {
|
|
456
|
+
star.tidalProgress = 0; // No distortion yet
|
|
457
|
+
|
|
458
|
+
const e = star.eccentricity || 0;
|
|
459
|
+
const a = star.semiMajorAxis;
|
|
460
|
+
|
|
461
|
+
// Current radius from ellipse equation
|
|
462
|
+
const r = orbitalRadius(a, e, star.phi);
|
|
463
|
+
star.orbitalRadius = r;
|
|
464
|
+
|
|
465
|
+
// Angular velocity varies with r² (Kepler's 2nd law: r²·dθ/dt = const)
|
|
466
|
+
// Faster at periapsis, slower at apoapsis
|
|
467
|
+
const baseOmega = keplerianOmega(a, bh.mass, 1.0, star.initialSemiMajorAxis);
|
|
468
|
+
const omega = baseOmega * (a * a) / (r * r);
|
|
469
|
+
|
|
470
|
+
star.phi += omega * dt * CONFIG.star.orbitSpeed;
|
|
471
|
+
|
|
472
|
+
// Store effective orbit speed for particle emission
|
|
473
|
+
star.effectiveOrbitSpeed = omega * r * CONFIG.star.orbitSpeed;
|
|
474
|
+
|
|
475
|
+
const pos = polarToCartesian(r, star.phi);
|
|
476
|
+
star.x = pos.x;
|
|
477
|
+
star.z = pos.z;
|
|
478
|
+
}
|
|
479
|
+
// Stretch phase: Elliptical orbit decays and circularizes
|
|
480
|
+
else if (state === "stretch") {
|
|
481
|
+
const stretchProgress = this.fsm.progress;
|
|
482
|
+
|
|
483
|
+
// Drive tidal distortion - starts subtle, builds through phase
|
|
484
|
+
// Use easeIn curve so distortion starts slowly then accelerates
|
|
485
|
+
star.tidalProgress = stretchProgress * stretchProgress;
|
|
486
|
+
|
|
487
|
+
// Semi-major axis decays
|
|
488
|
+
const a = decayingOrbitalRadius(
|
|
489
|
+
star.initialSemiMajorAxis,
|
|
490
|
+
CONFIG.star.decayRate * 0.3,
|
|
491
|
+
stretchProgress * 2
|
|
492
|
+
);
|
|
493
|
+
star.semiMajorAxis = a;
|
|
494
|
+
|
|
495
|
+
// Eccentricity decreases as orbit circularizes (tidal forces)
|
|
496
|
+
const e = star.eccentricity * (1 - stretchProgress * 0.5);
|
|
497
|
+
|
|
498
|
+
// Current radius from ellipse
|
|
499
|
+
const r = orbitalRadius(a, e, star.phi);
|
|
500
|
+
star.orbitalRadius = r;
|
|
501
|
+
|
|
502
|
+
// Angular velocity with Kepler's 2nd law
|
|
503
|
+
const baseOmega = keplerianOmega(a, bh.mass, 1.0, star.initialSemiMajorAxis);
|
|
504
|
+
const omega = baseOmega * (a * a) / (r * r);
|
|
505
|
+
const speedMultiplier = 1.1;
|
|
506
|
+
const phiStep = omega * dt * CONFIG.star.orbitSpeed * speedMultiplier;
|
|
507
|
+
star.phi += phiStep;
|
|
508
|
+
|
|
509
|
+
// Store effective orbit speed for particle emission
|
|
510
|
+
star.effectiveOrbitSpeed = omega * r * CONFIG.star.orbitSpeed * speedMultiplier;
|
|
511
|
+
|
|
512
|
+
const pos = polarToCartesian(r, star.phi);
|
|
513
|
+
star.x = pos.x;
|
|
514
|
+
star.z = pos.z;
|
|
515
|
+
|
|
516
|
+
// Start emitting particles during stretch (slowly at first)
|
|
517
|
+
if (this.scene.stream && stretchProgress > 0.1) {
|
|
518
|
+
// Compute current-frame velocity for accurate particle emission
|
|
519
|
+
if (dt > 0) {
|
|
520
|
+
star.velocityX = (star.x - oldStarX) / dt;
|
|
521
|
+
star.velocityZ = (star.z - oldStarZ) / dt;
|
|
522
|
+
}
|
|
523
|
+
const emitRate = 2 + Math.floor(stretchProgress * 15);
|
|
524
|
+
this.emitStreamParticles(dt, emitRate);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// Disrupt phase: Rapid decay with mass transfer
|
|
528
|
+
else if (state === "disrupt") {
|
|
529
|
+
star.tidalProgress = 1; // Max external distortion
|
|
530
|
+
|
|
531
|
+
// Use stateTime to calculate decay progress (event-based exit)
|
|
532
|
+
const decayTime = this.fsm.stateTime;
|
|
533
|
+
const disruptProgress = Math.min(1, decayTime / CONFIG.durations.disrupt);
|
|
534
|
+
|
|
535
|
+
// Continue decay from where stretch left off
|
|
536
|
+
const stretchEndAxis = decayingOrbitalRadius(
|
|
537
|
+
star.initialSemiMajorAxis,
|
|
538
|
+
CONFIG.star.decayRate * 0.3,
|
|
539
|
+
2 // stretch ended at progress=1, factor=2
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// Semi-major axis decays with easing - slow start, accelerates later
|
|
543
|
+
const decayEase = disruptProgress * disruptProgress; // easeIn quadratic
|
|
544
|
+
// Blend from stretch rate (0.3x) to full rate over first 50% of disrupt
|
|
545
|
+
const decayRateBlend = 0.3 + Math.min(disruptProgress * 2, 1) * 0.7; // 0.3 -> 1.0
|
|
546
|
+
const a = decayingOrbitalRadius(
|
|
547
|
+
stretchEndAxis,
|
|
548
|
+
CONFIG.star.decayRate * decayRateBlend,
|
|
549
|
+
decayEase * 5
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
// Eccentricity continues from stretch end (0.5 * initial) and decays to 0
|
|
553
|
+
const e = star.eccentricity * 0.5 * (1 - disruptProgress);
|
|
554
|
+
|
|
555
|
+
// Current radius from ellipse equation
|
|
556
|
+
const baseRadius = orbitalRadius(a, e, star.phi);
|
|
557
|
+
|
|
558
|
+
// === ORBITAL CHAOS ===
|
|
559
|
+
// Starts at 0, builds with easeIn curve for gradual onset
|
|
560
|
+
const chaosProgress = disruptProgress * disruptProgress; // easeIn
|
|
561
|
+
const chaos = chaosProgress * 0.6; // 0% -> 60%
|
|
562
|
+
const time = this.fsm.stateTime;
|
|
563
|
+
|
|
564
|
+
// Radial wobble - only after chaos builds
|
|
565
|
+
const radialWobble = chaos > 0.01
|
|
566
|
+
? Math.sin(time * 2.5) * chaos * baseRadius * 0.15
|
|
567
|
+
+ Math.sin(time * 5.8) * chaos * baseRadius * 0.08
|
|
568
|
+
: 0;
|
|
569
|
+
star.orbitalRadius = baseRadius + radialWobble;
|
|
570
|
+
|
|
571
|
+
// Angular velocity with Kepler's 2nd law (same as stretch phase)
|
|
572
|
+
const baseOmega = keplerianOmega(a, bh.mass, 1.0, star.initialSemiMajorAxis);
|
|
573
|
+
const omega = baseOmega * (a * a) / (star.orbitalRadius * star.orbitalRadius);
|
|
574
|
+
|
|
575
|
+
// Speed ramp: starts at 1.1x (matching stretch end), accelerates toward end
|
|
576
|
+
const speedRamp = Math.pow(disruptProgress, 4); // easeIn quartic
|
|
577
|
+
const speedMultiplier = 1.1 + speedRamp * 1.4; // 1.1x -> 2.5x
|
|
578
|
+
|
|
579
|
+
// Angular jitter only kicks in as chaos builds
|
|
580
|
+
const angularJitter = chaos > 0.05 ? Math.sin(time * 3.7) * chaos * 0.15 : 0;
|
|
581
|
+
const phiStep = omega * dt * CONFIG.star.orbitSpeed * (speedMultiplier + angularJitter);
|
|
582
|
+
star.phi += phiStep;
|
|
583
|
+
|
|
584
|
+
// Store effective orbit speed for particle emission
|
|
585
|
+
star.effectiveOrbitSpeed = omega * star.orbitalRadius * CONFIG.star.orbitSpeed * (speedMultiplier);
|
|
586
|
+
|
|
587
|
+
const pos = polarToCartesian(star.orbitalRadius, star.phi);
|
|
588
|
+
|
|
589
|
+
// Vertical wobble - only after chaos builds
|
|
590
|
+
const verticalWobble = chaos > 0.01
|
|
591
|
+
? Math.sin(time * 1.7) * chaos * baseRadius * 0.12
|
|
592
|
+
+ Math.cos(time * 3.9) * chaos * baseRadius * 0.06
|
|
593
|
+
: 0;
|
|
594
|
+
|
|
595
|
+
star.x = pos.x;
|
|
596
|
+
star.y = verticalWobble;
|
|
597
|
+
star.z = pos.z;
|
|
598
|
+
|
|
599
|
+
// Emit particles throughout disrupt phase (more than stretch)
|
|
600
|
+
if (this.scene.stream && star.mass > 0) {
|
|
601
|
+
// Compute current-frame velocity for accurate particle emission
|
|
602
|
+
if (dt > 0) {
|
|
603
|
+
star.velocityX = (star.x - oldStarX) / dt;
|
|
604
|
+
star.velocityZ = (star.z - oldStarZ) / dt;
|
|
605
|
+
}
|
|
606
|
+
const emitRate = 3 + Math.floor(disruptProgress * 20);
|
|
607
|
+
this.emitStreamParticles(dt, emitRate);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Mass transfer starts at configured percentage of disrupt phase
|
|
611
|
+
if (disruptProgress >= CONFIG.star.massTransferStart) {
|
|
612
|
+
const transferRate = (CONFIG.star.initialMass / (CONFIG.durations.disrupt * 0.5)) * dt;
|
|
613
|
+
|
|
614
|
+
star.mass = Math.max(0, star.mass - transferRate);
|
|
615
|
+
bh.mass += transferRate;
|
|
616
|
+
|
|
617
|
+
// AWAKEN the black hole as it feeds! This triggers the glow
|
|
618
|
+
bh.addConsumedMass(transferRate * 0.5);
|
|
619
|
+
|
|
620
|
+
// Force visual updates to reflect mass changes
|
|
621
|
+
star.updateVisual();
|
|
622
|
+
bh.updateVisual();
|
|
623
|
+
|
|
624
|
+
// Trigger accrete state when star mass is depleted
|
|
625
|
+
if (star.mass <= 0) {
|
|
626
|
+
this.fsm.trigger("starConsumed");
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Activate accretion disk at 80% progress (handles its own tweens)
|
|
631
|
+
if (disruptProgress >= 0.8 && this.scene.disk) {
|
|
632
|
+
this.scene.disk.activate();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// Accrete phase: ensure disk active
|
|
636
|
+
else if (state === "accrete") {
|
|
637
|
+
if (this.scene.disk && !this.scene.disk.active) {
|
|
638
|
+
this.scene.disk.activate();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Flare phase: jets fire continuously
|
|
642
|
+
else if (state === "flare") {
|
|
643
|
+
if (this.scene.jets) {
|
|
644
|
+
this.scene.jets.active = true;
|
|
645
|
+
this.scene.jets.intensity = 1; // Keep at full blast
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// Stable phase: jets fade out
|
|
649
|
+
else if (state === "stable") {
|
|
650
|
+
if (this.scene.jets && this.scene.jets.active) {
|
|
651
|
+
this.scene.jets.deactivate();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Update star z-order AFTER position is set (must be last in update)
|
|
656
|
+
// This ensures correct depth sorting relative to black hole
|
|
657
|
+
if (this.scene) {
|
|
658
|
+
this.scene.updateStarZOrder();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
onResize() {
|
|
663
|
+
this.updateScaledSizes();
|
|
664
|
+
if (this.camera) {
|
|
665
|
+
this.camera.perspective = this.baseScale * 1.8;
|
|
666
|
+
}
|
|
667
|
+
if (this.scene) {
|
|
668
|
+
// Apply orbit center offset
|
|
669
|
+
const orbitOffsetX = (CONFIG.star.orbitCenterX || 0) * this.width;
|
|
670
|
+
const orbitOffsetY = (CONFIG.star.orbitCenterY || 0) * this.height;
|
|
671
|
+
this.scene.x = this.width / 2 + orbitOffsetX;
|
|
672
|
+
this.scene.y = this.height / 2 + orbitOffsetY;
|
|
673
|
+
this.scene.onResize();
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
render() {
|
|
678
|
+
super.render();
|
|
679
|
+
|
|
680
|
+
// Draw flash overlay on top of everything
|
|
681
|
+
if (this.flashIntensity > 0) {
|
|
682
|
+
Painter.useCtx((ctx) => {
|
|
683
|
+
ctx.fillStyle = `rgba(255, 255, 255, ${this.flashIntensity})`;
|
|
684
|
+
ctx.fillRect(0, 0, this.width, this.height);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
window.addEventListener("load", () => {
|
|
691
|
+
const canvas = document.getElementById("game");
|
|
692
|
+
const demo = new TDEDemo(canvas);
|
|
693
|
+
demo.enableFluidSize();
|
|
694
|
+
demo.start();
|
|
695
|
+
});
|