@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,812 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Star - Deformable stellar object for TDE demo
|
|
3
|
+
*
|
|
4
|
+
* A star on an elliptical orbit that gets tidally disrupted.
|
|
5
|
+
* Uses proper orbital mechanics with cyclone-like particle ejection.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
GameObject,
|
|
9
|
+
ParticleSystem,
|
|
10
|
+
Easing,
|
|
11
|
+
Painter,
|
|
12
|
+
} from "../../../src/index.js";
|
|
13
|
+
import { polarToCartesian } from "../../../src/math/gr.js";
|
|
14
|
+
|
|
15
|
+
const CONFIG = {
|
|
16
|
+
// Star colors (yellow-white to orange core)
|
|
17
|
+
colorOuter: { r: 255, g: 245, b: 220 },
|
|
18
|
+
colorCore: { r: 255, g: 200, b: 100 },
|
|
19
|
+
|
|
20
|
+
// Particle sizes - visible but not too big
|
|
21
|
+
sizeMin: 1,
|
|
22
|
+
sizeMax: 3,
|
|
23
|
+
coronaExtent: 1.5, // Particles extend to 1.5x the star radius (corona)
|
|
24
|
+
|
|
25
|
+
// Orbital physics - star starts FAR and falls IN
|
|
26
|
+
initialOrbitAngle: Math.PI * 0.8, // Start position
|
|
27
|
+
orbitSpeed: 0.4, // Slower orbit for more drama
|
|
28
|
+
periapsisRatio: 0.08, // Close approach (grazes BH)
|
|
29
|
+
apoapsisRatio: 0.6, // Start FARTHER out for more dramatic approach
|
|
30
|
+
|
|
31
|
+
// Tidal effects - DRAMATIC spaghettification like the reference image
|
|
32
|
+
stretchMaxFactor: 8.0, // More extreme stretching
|
|
33
|
+
tidalRadiusRatio: 0.12, // Where tidal forces rip the star
|
|
34
|
+
tearDropFactor: 2.5, // How much particles toward BH stretch more
|
|
35
|
+
|
|
36
|
+
// Star body glow (smaller than corona)
|
|
37
|
+
glowRadius: 1.4,
|
|
38
|
+
bodyRadius: 0.6, // Core body is smaller than particle cloud
|
|
39
|
+
|
|
40
|
+
// Depth perception - star SHRINKS as it falls toward BH
|
|
41
|
+
perspectiveMultiplier: 1.0, // Base scale (no extra multiplier)
|
|
42
|
+
// DRAMATIC size change as star approaches
|
|
43
|
+
distanceScaleMax: 2.5, // BIG when far from BH (at start)
|
|
44
|
+
distanceScaleMin: 0.3, // SMALL when close to BH (falling in)
|
|
45
|
+
|
|
46
|
+
// Particle streaming toward BH
|
|
47
|
+
streamSpeed: 80, // How fast particles stream toward BH
|
|
48
|
+
streamSpread: 0.3, // Angular spread when streaming
|
|
49
|
+
|
|
50
|
+
// STAR PARTICLE DRIFT - particles drift toward BH while still attached
|
|
51
|
+
// MUCH STRONGER for visible streaming effect from the start
|
|
52
|
+
driftStartProgress: 0.0, // Drift starts immediately
|
|
53
|
+
driftStrength: 300, // Stronger drift for visible streaming
|
|
54
|
+
driftAcceleration: 3.0, // Faster acceleration toward BH
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Simple particle data class for star particles.
|
|
59
|
+
*/
|
|
60
|
+
class StarParticle {
|
|
61
|
+
constructor(options = {}) {
|
|
62
|
+
// Offset from star center (local space)
|
|
63
|
+
this.offsetX = options.offsetX ?? 0;
|
|
64
|
+
this.offsetY = options.offsetY ?? 0;
|
|
65
|
+
this.offsetZ = options.offsetZ ?? 0;
|
|
66
|
+
|
|
67
|
+
// Original offset (for deformation)
|
|
68
|
+
this.baseOffsetX = this.offsetX;
|
|
69
|
+
this.baseOffsetY = this.offsetY;
|
|
70
|
+
this.baseOffsetZ = this.offsetZ;
|
|
71
|
+
|
|
72
|
+
// Distance from center (for release ordering)
|
|
73
|
+
this.baseDist = Math.sqrt(
|
|
74
|
+
this.baseOffsetX ** 2 + this.baseOffsetY ** 2 + this.baseOffsetZ ** 2,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Appearance
|
|
78
|
+
this.size = options.size ?? 3;
|
|
79
|
+
this.color = options.color ?? { r: 255, g: 240, b: 200, a: 1 };
|
|
80
|
+
|
|
81
|
+
// State
|
|
82
|
+
this.released = false;
|
|
83
|
+
this.releasedAt = 0;
|
|
84
|
+
|
|
85
|
+
// Spiral parameters (set on release)
|
|
86
|
+
this.spiralPhase = Math.random() * Math.PI * 2;
|
|
87
|
+
this.spiralSpeed = 0.5 + Math.random() * 1.5;
|
|
88
|
+
|
|
89
|
+
// DRIFT toward black hole (while still attached to star)
|
|
90
|
+
this.driftX = 0;
|
|
91
|
+
this.driftY = 0;
|
|
92
|
+
this.driftZ = 0;
|
|
93
|
+
this.driftVelX = 0;
|
|
94
|
+
this.driftVelY = 0;
|
|
95
|
+
this.driftVelZ = 0;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class Star extends GameObject {
|
|
100
|
+
/**
|
|
101
|
+
* @param {Game} game - Game instance
|
|
102
|
+
* @param {Object} options
|
|
103
|
+
* @param {Camera3D} options.camera - Camera for projection
|
|
104
|
+
* @param {number} options.radius - Star radius
|
|
105
|
+
* @param {number} options.particleCount - Number of particles
|
|
106
|
+
* @param {number} options.baseScale - Base scale for sizing
|
|
107
|
+
* @param {number} options.startDistance - Initial distance from black hole
|
|
108
|
+
*/
|
|
109
|
+
constructor(game, options = {}) {
|
|
110
|
+
super(game, options);
|
|
111
|
+
|
|
112
|
+
this.camera = options.camera;
|
|
113
|
+
this.radius = options.radius ?? 50;
|
|
114
|
+
this.particleCount = options.particleCount ?? 600;
|
|
115
|
+
this.baseScale = options.baseScale ?? 500;
|
|
116
|
+
this.startDistance = options.startDistance ?? 200;
|
|
117
|
+
|
|
118
|
+
// Orbital state
|
|
119
|
+
this.orbitAngle = CONFIG.initialOrbitAngle;
|
|
120
|
+
this.orbitRadius = this.startDistance;
|
|
121
|
+
this.periapsis = this.baseScale * CONFIG.periapsisRatio;
|
|
122
|
+
this.apoapsis = this.baseScale * CONFIG.apoapsisRatio;
|
|
123
|
+
|
|
124
|
+
// Star position (world space)
|
|
125
|
+
this.centerX = 0;
|
|
126
|
+
this.centerY = 0;
|
|
127
|
+
this.centerZ = 0;
|
|
128
|
+
|
|
129
|
+
// Star state
|
|
130
|
+
this.stretchFactor = 1.0;
|
|
131
|
+
this.intactRatio = 1.0;
|
|
132
|
+
this.phase = "approach";
|
|
133
|
+
|
|
134
|
+
// Cached render state (computed in update, used in draw)
|
|
135
|
+
this.screenX = 0;
|
|
136
|
+
this.screenY = 0;
|
|
137
|
+
this.effectiveRadius = 0;
|
|
138
|
+
this.cameraZ = 0;
|
|
139
|
+
this.perspectiveScale = 1.0; // Camera perspective scale
|
|
140
|
+
this.distanceScale = CONFIG.distanceScaleMax; // Start at max (far from BH)
|
|
141
|
+
|
|
142
|
+
// Particles
|
|
143
|
+
this.particles = [];
|
|
144
|
+
|
|
145
|
+
// Particle system for rendering
|
|
146
|
+
this.particleSystem = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Initialize the star.
|
|
151
|
+
*/
|
|
152
|
+
init() {
|
|
153
|
+
this.createParticleSystem();
|
|
154
|
+
this.initParticles();
|
|
155
|
+
this.resetPosition();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Create the particle system for rendering.
|
|
160
|
+
*/
|
|
161
|
+
createParticleSystem() {
|
|
162
|
+
this.particleSystem = new ParticleSystem(this.game, {
|
|
163
|
+
maxParticles: this.particleCount,
|
|
164
|
+
camera: this.camera,
|
|
165
|
+
depthSort: true,
|
|
166
|
+
blendMode: "lighter",
|
|
167
|
+
updaters: [],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Create spherical distribution of particles.
|
|
173
|
+
* Particles extend beyond the star body to form a corona.
|
|
174
|
+
*/
|
|
175
|
+
initParticles() {
|
|
176
|
+
this.particles = [];
|
|
177
|
+
|
|
178
|
+
// Corona radius - particles extend beyond the solid body
|
|
179
|
+
const coronaRadius = this.radius * CONFIG.coronaExtent;
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < this.particleCount; i++) {
|
|
182
|
+
// Spherical distribution
|
|
183
|
+
const u = Math.random();
|
|
184
|
+
const v = Math.random();
|
|
185
|
+
const theta = 2 * Math.PI * u;
|
|
186
|
+
const phi = Math.acos(2 * v - 1);
|
|
187
|
+
|
|
188
|
+
// Distribution biased toward outer edge for corona effect
|
|
189
|
+
// More particles in the corona, fewer in the core
|
|
190
|
+
const r = coronaRadius * Math.pow(Math.random(), 0.8);
|
|
191
|
+
|
|
192
|
+
const offsetX = r * Math.sin(phi) * Math.cos(theta);
|
|
193
|
+
const offsetY = r * Math.sin(phi) * Math.sin(theta);
|
|
194
|
+
const offsetZ = r * Math.cos(phi);
|
|
195
|
+
|
|
196
|
+
// Color based on distance from center (hotter core)
|
|
197
|
+
const distRatio = r / coronaRadius;
|
|
198
|
+
const color = this.lerpColor(
|
|
199
|
+
CONFIG.colorCore,
|
|
200
|
+
CONFIG.colorOuter,
|
|
201
|
+
distRatio,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Size - larger particles in the corona, smaller in core
|
|
205
|
+
// This makes the outer glow more prominent
|
|
206
|
+
const size =
|
|
207
|
+
CONFIG.sizeMin + (CONFIG.sizeMax - CONFIG.sizeMin) * distRatio;
|
|
208
|
+
|
|
209
|
+
// Alpha - brighter core, fainter corona
|
|
210
|
+
const alpha = 1.0 - distRatio * 0.5;
|
|
211
|
+
|
|
212
|
+
this.particles.push(
|
|
213
|
+
new StarParticle({
|
|
214
|
+
offsetX,
|
|
215
|
+
offsetY,
|
|
216
|
+
offsetZ,
|
|
217
|
+
size,
|
|
218
|
+
color: { ...color, a: alpha },
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
lerpColor(c1, c2, t) {
|
|
225
|
+
return {
|
|
226
|
+
r: c1.r + (c2.r - c1.r) * t,
|
|
227
|
+
g: c1.g + (c2.g - c1.g) * t,
|
|
228
|
+
b: c1.b + (c2.b - c1.b) * t,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
resetPosition() {
|
|
233
|
+
this.orbitAngle = CONFIG.initialOrbitAngle;
|
|
234
|
+
this.orbitRadius = this.startDistance;
|
|
235
|
+
const pos = polarToCartesian(this.orbitRadius, this.orbitAngle);
|
|
236
|
+
this.centerX = pos.x;
|
|
237
|
+
this.centerY = 0;
|
|
238
|
+
this.centerZ = pos.z;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
reset() {
|
|
242
|
+
this.stretchFactor = 1.0;
|
|
243
|
+
this.intactRatio = 1.0;
|
|
244
|
+
this.orbitAngle = CONFIG.initialOrbitAngle;
|
|
245
|
+
this.initParticles();
|
|
246
|
+
this.resetPosition();
|
|
247
|
+
this.visible = true;
|
|
248
|
+
// Reset all particle drift
|
|
249
|
+
for (const p of this.particles) {
|
|
250
|
+
p.driftX = 0;
|
|
251
|
+
p.driftY = 0;
|
|
252
|
+
p.driftZ = 0;
|
|
253
|
+
p.driftVelX = 0;
|
|
254
|
+
p.driftVelY = 0;
|
|
255
|
+
p.driftVelZ = 0;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
updateSizing(radius, baseScale) {
|
|
260
|
+
this.radius = radius;
|
|
261
|
+
this.baseScale = baseScale;
|
|
262
|
+
this.startDistance = baseScale * CONFIG.apoapsisRatio;
|
|
263
|
+
this.periapsis = baseScale * CONFIG.periapsisRatio;
|
|
264
|
+
this.apoapsis = baseScale * CONFIG.apoapsisRatio;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
startApproach() {
|
|
268
|
+
this.phase = "approach";
|
|
269
|
+
this.resetPosition();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
startStretch() {
|
|
273
|
+
this.phase = "stretch";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
startDisrupt() {
|
|
277
|
+
this.phase = "disrupt";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Apply tidal stretching during approach phase.
|
|
282
|
+
* This creates the initial teardrop deformation before full stretch phase.
|
|
283
|
+
*/
|
|
284
|
+
applyTidalStretch(intensity) {
|
|
285
|
+
if (intensity <= 0) return;
|
|
286
|
+
|
|
287
|
+
// Direction toward black hole
|
|
288
|
+
const dist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
|
|
289
|
+
const dirX = -this.centerX / dist;
|
|
290
|
+
const dirZ = -this.centerZ / dist;
|
|
291
|
+
|
|
292
|
+
// Mild stretching during approach
|
|
293
|
+
const stretchAmount = 1 + intensity * (CONFIG.stretchMaxFactor - 1) * 0.3;
|
|
294
|
+
|
|
295
|
+
for (const p of this.particles) {
|
|
296
|
+
if (p.released) continue;
|
|
297
|
+
|
|
298
|
+
const dot = p.baseOffsetX * dirX + p.baseOffsetZ * dirZ;
|
|
299
|
+
const normalizedDot = dot / (this.radius * CONFIG.coronaExtent);
|
|
300
|
+
|
|
301
|
+
// Teardrop effect - particles toward BH stretch more
|
|
302
|
+
const tearDropMult =
|
|
303
|
+
1 + Math.max(0, normalizedDot) * CONFIG.tearDropFactor * intensity;
|
|
304
|
+
const actualStretch = stretchAmount * tearDropMult;
|
|
305
|
+
const compressAmount = 1 / Math.sqrt(actualStretch);
|
|
306
|
+
|
|
307
|
+
p.offsetX =
|
|
308
|
+
p.baseOffsetX * compressAmount + dirX * dot * (actualStretch - 1);
|
|
309
|
+
p.offsetZ =
|
|
310
|
+
p.baseOffsetZ * compressAmount + dirZ * dot * (actualStretch - 1);
|
|
311
|
+
p.offsetY = p.baseOffsetY * compressAmount;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.stretchFactor = stretchAmount;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Apply particle drift toward black hole.
|
|
319
|
+
* Particles on the BH-facing side start drifting toward the BH
|
|
320
|
+
* while still attached to the star - creates streaming effect.
|
|
321
|
+
*/
|
|
322
|
+
applyParticleDrift(dt, intensity) {
|
|
323
|
+
if (intensity <= 0) return;
|
|
324
|
+
|
|
325
|
+
// Direction toward black hole (world space, from star center)
|
|
326
|
+
const starDist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
|
|
327
|
+
const toBHx = -this.centerX / starDist;
|
|
328
|
+
const toBHz = -this.centerZ / starDist;
|
|
329
|
+
|
|
330
|
+
for (const p of this.particles) {
|
|
331
|
+
if (p.released) continue;
|
|
332
|
+
|
|
333
|
+
// How much this particle faces the BH (positive = toward BH)
|
|
334
|
+
const offsetDist = Math.sqrt(p.offsetX ** 2 + p.offsetZ ** 2) || 1;
|
|
335
|
+
const facingBH = (p.offsetX * toBHx + p.offsetZ * toBHz) / offsetDist;
|
|
336
|
+
|
|
337
|
+
// Only particles facing the BH drift toward it
|
|
338
|
+
if (facingBH > 0) {
|
|
339
|
+
// Drift strength increases for particles more toward BH
|
|
340
|
+
const driftMult = facingBH * CONFIG.driftAcceleration * intensity;
|
|
341
|
+
|
|
342
|
+
// Accelerate toward BH
|
|
343
|
+
p.driftVelX += toBHx * CONFIG.driftStrength * driftMult * dt;
|
|
344
|
+
p.driftVelZ += toBHz * CONFIG.driftStrength * driftMult * dt;
|
|
345
|
+
p.driftVelY *= 0.95; // Dampen vertical drift
|
|
346
|
+
|
|
347
|
+
// Apply velocity to drift position
|
|
348
|
+
p.driftX += p.driftVelX * dt;
|
|
349
|
+
p.driftY += p.driftVelY * dt;
|
|
350
|
+
p.driftZ += p.driftVelZ * dt;
|
|
351
|
+
|
|
352
|
+
// Damping to prevent runaway
|
|
353
|
+
p.driftVelX *= 0.98;
|
|
354
|
+
p.driftVelZ *= 0.98;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Compute orbital radius for elliptical orbit.
|
|
361
|
+
* r = a(1-e²) / (1 + e*cos(θ))
|
|
362
|
+
*/
|
|
363
|
+
getOrbitalRadius(angle, progress) {
|
|
364
|
+
const a = (this.periapsis + this.apoapsis) / 2;
|
|
365
|
+
const e =
|
|
366
|
+
(this.apoapsis - this.periapsis) / (this.apoapsis + this.periapsis);
|
|
367
|
+
return (a * (1 - e * e)) / (1 + e * Math.cos(angle));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Update during approach phase - star spirals in on elliptical orbit.
|
|
372
|
+
*/
|
|
373
|
+
updateApproach(dt, progress) {
|
|
374
|
+
// Save previous position to calculate velocity
|
|
375
|
+
const prevX = this.centerX;
|
|
376
|
+
const prevZ = this.centerZ;
|
|
377
|
+
|
|
378
|
+
// Advance orbital angle (faster as it approaches periapsis - Kepler's 2nd law)
|
|
379
|
+
const speedFactor = 1 + progress * 2;
|
|
380
|
+
this.orbitAngle += dt * CONFIG.orbitSpeed * speedFactor;
|
|
381
|
+
|
|
382
|
+
// Interpolate toward periapsis
|
|
383
|
+
const t = Easing.easeInQuad(progress);
|
|
384
|
+
this.orbitRadius = Easing.lerp(this.startDistance, this.periapsis * 1.5, t);
|
|
385
|
+
|
|
386
|
+
// Update position
|
|
387
|
+
const pos = polarToCartesian(this.orbitRadius, this.orbitAngle);
|
|
388
|
+
this.centerX = pos.x;
|
|
389
|
+
this.centerZ = pos.z;
|
|
390
|
+
|
|
391
|
+
// Slight vertical oscillation
|
|
392
|
+
this.centerY = Math.sin(this.orbitAngle * 2) * this.baseScale * 0.02;
|
|
393
|
+
|
|
394
|
+
// Calculate velocity (units per second)
|
|
395
|
+
if (dt > 0) {
|
|
396
|
+
this.velocityX = (this.centerX - prevX) / dt;
|
|
397
|
+
this.velocityZ = (this.centerZ - prevZ) / dt;
|
|
398
|
+
this.velocityY = (this.centerY - 0) / dt; // approx
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Update during stretch phase - orbiting at periapsis, tidal stretching.
|
|
404
|
+
* Creates TEARDROP spaghettification like the reference image.
|
|
405
|
+
*/
|
|
406
|
+
updateStretch(dt, progress, bhPosition) {
|
|
407
|
+
const prevX = this.centerX;
|
|
408
|
+
const prevZ = this.centerZ;
|
|
409
|
+
|
|
410
|
+
// Continue orbiting faster at periapsis
|
|
411
|
+
const speedFactor = 2 + progress;
|
|
412
|
+
this.orbitAngle += dt * CONFIG.orbitSpeed * speedFactor;
|
|
413
|
+
|
|
414
|
+
// Stay near periapsis
|
|
415
|
+
const t = Easing.easeInOutQuad(progress);
|
|
416
|
+
this.orbitRadius = Easing.lerp(this.periapsis * 1.5, this.periapsis, t);
|
|
417
|
+
|
|
418
|
+
const pos = polarToCartesian(this.orbitRadius, this.orbitAngle);
|
|
419
|
+
this.centerX = pos.x;
|
|
420
|
+
this.centerZ = pos.z;
|
|
421
|
+
this.centerY = Math.sin(this.orbitAngle * 2) * this.baseScale * 0.015;
|
|
422
|
+
|
|
423
|
+
// Calculate velocity
|
|
424
|
+
if (dt > 0) {
|
|
425
|
+
this.velocityX = (this.centerX - prevX) / dt;
|
|
426
|
+
this.velocityZ = (this.centerZ - prevZ) / dt;
|
|
427
|
+
this.velocityY = (this.centerY - 0) / dt;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Apply tidal stretching - increases dramatically with progress
|
|
431
|
+
this.stretchFactor =
|
|
432
|
+
1 + (CONFIG.stretchMaxFactor - 1) * Easing.easeInQuad(progress);
|
|
433
|
+
|
|
434
|
+
// Direction toward black hole
|
|
435
|
+
const dist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
|
|
436
|
+
const dirX = -this.centerX / dist;
|
|
437
|
+
const dirZ = -this.centerZ / dist;
|
|
438
|
+
|
|
439
|
+
// TEARDROP deformation - particles facing BH stretch MORE
|
|
440
|
+
for (const p of this.particles) {
|
|
441
|
+
if (p.released) continue;
|
|
442
|
+
|
|
443
|
+
// How much this particle faces the BH (-1 to 1)
|
|
444
|
+
const dot = p.baseOffsetX * dirX + p.baseOffsetZ * dirZ;
|
|
445
|
+
const normalizedDot = dot / (this.radius * CONFIG.coronaExtent);
|
|
446
|
+
|
|
447
|
+
// Particles toward BH (dot > 0) stretch MORE than those away
|
|
448
|
+
// This creates the teardrop/spaghetti shape
|
|
449
|
+
const tearDropMult =
|
|
450
|
+
1 + Math.max(0, normalizedDot) * CONFIG.tearDropFactor * progress;
|
|
451
|
+
const stretchAmount = this.stretchFactor * tearDropMult;
|
|
452
|
+
const compressAmount = 1 / Math.sqrt(stretchAmount);
|
|
453
|
+
|
|
454
|
+
// Apply asymmetric stretching
|
|
455
|
+
p.offsetX =
|
|
456
|
+
p.baseOffsetX * compressAmount + dirX * dot * (stretchAmount - 1);
|
|
457
|
+
p.offsetZ =
|
|
458
|
+
p.baseOffsetZ * compressAmount + dirZ * dot * (stretchAmount - 1);
|
|
459
|
+
p.offsetY = p.baseOffsetY * compressAmount;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Update during disrupt phase - extreme stretching, particles spiral off.
|
|
465
|
+
* Creates extreme spaghettification as star is torn apart.
|
|
466
|
+
*/
|
|
467
|
+
updateDisrupt(dt, progress) {
|
|
468
|
+
const prevX = this.centerX;
|
|
469
|
+
const prevZ = this.centerZ;
|
|
470
|
+
|
|
471
|
+
// Continue orbiting past periapsis
|
|
472
|
+
const speedFactor = 3 - progress * 1.5;
|
|
473
|
+
this.orbitAngle += dt * CONFIG.orbitSpeed * speedFactor;
|
|
474
|
+
|
|
475
|
+
// Move outward slightly as star is disrupted
|
|
476
|
+
const t = Easing.easeOutQuad(progress);
|
|
477
|
+
this.orbitRadius = Easing.lerp(this.periapsis, this.periapsis * 1.5, t);
|
|
478
|
+
|
|
479
|
+
const pos = polarToCartesian(this.orbitRadius, this.orbitAngle);
|
|
480
|
+
this.centerX = pos.x;
|
|
481
|
+
this.centerZ = pos.z;
|
|
482
|
+
|
|
483
|
+
// Calculate velocity
|
|
484
|
+
if (dt > 0) {
|
|
485
|
+
this.velocityX = (this.centerX - prevX) / dt;
|
|
486
|
+
this.velocityZ = (this.centerZ - prevZ) / dt;
|
|
487
|
+
this.velocityY = (this.centerY - 0) / dt;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// EXTREME stretching during disruption
|
|
491
|
+
this.stretchFactor = CONFIG.stretchMaxFactor * (1 + progress * 3);
|
|
492
|
+
|
|
493
|
+
const dist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
|
|
494
|
+
const dirX = -this.centerX / dist;
|
|
495
|
+
const dirZ = -this.centerZ / dist;
|
|
496
|
+
|
|
497
|
+
// TEARDROP deformation continues with even more extreme stretching
|
|
498
|
+
for (const p of this.particles) {
|
|
499
|
+
if (p.released) continue;
|
|
500
|
+
|
|
501
|
+
const dot = p.baseOffsetX * dirX + p.baseOffsetZ * dirZ;
|
|
502
|
+
const normalizedDot = dot / (this.radius * CONFIG.coronaExtent);
|
|
503
|
+
|
|
504
|
+
// Even more extreme teardrop during disruption
|
|
505
|
+
const tearDropMult =
|
|
506
|
+
1 + Math.max(0, normalizedDot) * CONFIG.tearDropFactor * 2;
|
|
507
|
+
const stretchAmount = this.stretchFactor * tearDropMult;
|
|
508
|
+
const compressAmount = 1 / Math.sqrt(stretchAmount);
|
|
509
|
+
|
|
510
|
+
p.offsetX =
|
|
511
|
+
p.baseOffsetX * compressAmount + dirX * dot * (stretchAmount - 1);
|
|
512
|
+
p.offsetZ =
|
|
513
|
+
p.baseOffsetZ * compressAmount + dirZ * dot * (stretchAmount - 1);
|
|
514
|
+
p.offsetY = p.baseOffsetY * compressAmount;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Release particles that stream toward the black hole.
|
|
520
|
+
* ONLY particles on BH-facing side release, and they ALWAYS flow toward BH.
|
|
521
|
+
*/
|
|
522
|
+
releaseParticles(progress) {
|
|
523
|
+
const released = [];
|
|
524
|
+
|
|
525
|
+
// Direction toward black hole from star center
|
|
526
|
+
const starDist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
|
|
527
|
+
const toBHx = -this.centerX / starDist;
|
|
528
|
+
const toBHz = -this.centerZ / starDist;
|
|
529
|
+
|
|
530
|
+
// Release rate increases with progress
|
|
531
|
+
const releaseRate = 0.003 + progress * progress * 0.02;
|
|
532
|
+
// Allow up to 60% to be released organically
|
|
533
|
+
const maxReleased = 0.6;
|
|
534
|
+
if (this.intactRatio < 1 - maxReleased) return released;
|
|
535
|
+
|
|
536
|
+
for (const p of this.particles) {
|
|
537
|
+
if (p.released) continue;
|
|
538
|
+
|
|
539
|
+
// How much this particle faces the BH (1 = directly toward BH, -1 = away)
|
|
540
|
+
const offsetDist = Math.sqrt(p.offsetX ** 2 + p.offsetZ ** 2) || 1;
|
|
541
|
+
const facingBH = (p.offsetX * toBHx + p.offsetZ * toBHz) / offsetDist;
|
|
542
|
+
|
|
543
|
+
// ONLY release particles on BH-facing side (facingBH > 0)
|
|
544
|
+
// Particles on the far side of star should NOT release
|
|
545
|
+
if (facingBH < 0.1) continue;
|
|
546
|
+
|
|
547
|
+
// Outer corona releases easier
|
|
548
|
+
const stretchedDist = Math.sqrt(
|
|
549
|
+
p.offsetX ** 2 + p.offsetY ** 2 + p.offsetZ ** 2,
|
|
550
|
+
);
|
|
551
|
+
const releaseScore = facingBH * 0.6 + (stretchedDist / this.radius) * 0.4;
|
|
552
|
+
|
|
553
|
+
// Threshold decreases with progress
|
|
554
|
+
const threshold = 0.6 - progress * 0.5;
|
|
555
|
+
|
|
556
|
+
if (releaseScore > threshold && Math.random() < releaseRate) {
|
|
557
|
+
p.released = true;
|
|
558
|
+
p.releasedAt = progress;
|
|
559
|
+
|
|
560
|
+
// World position of released particle
|
|
561
|
+
const worldX = this.centerX + p.offsetX + p.driftX;
|
|
562
|
+
const worldY = this.centerY + p.offsetY + p.driftY;
|
|
563
|
+
const worldZ = this.centerZ + p.offsetZ + p.driftZ;
|
|
564
|
+
|
|
565
|
+
// Inherit Star Velocity + Ejection Kick
|
|
566
|
+
// This is key for the "S" shape - conservation of momentum
|
|
567
|
+
const ejectionSpeed = 10 + Math.random() * 20;
|
|
568
|
+
|
|
569
|
+
released.push({
|
|
570
|
+
x: worldX,
|
|
571
|
+
y: worldY,
|
|
572
|
+
z: worldZ,
|
|
573
|
+
// Velocity = Star Velocity + Ejection (toward BH)
|
|
574
|
+
vx: (this.velocityX || 0) + toBHx * ejectionSpeed,
|
|
575
|
+
vy: (this.velocityY || 0) + (Math.random() - 0.5) * 5,
|
|
576
|
+
vz: (this.velocityZ || 0) + toBHz * ejectionSpeed,
|
|
577
|
+
size: p.size,
|
|
578
|
+
color: { ...p.color },
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
this.intactRatio =
|
|
584
|
+
this.particles.filter((p) => !p.released).length / this.particles.length;
|
|
585
|
+
return released;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Release all remaining particles streaming toward BH.
|
|
590
|
+
*/
|
|
591
|
+
releaseAllParticles() {
|
|
592
|
+
const released = [];
|
|
593
|
+
|
|
594
|
+
const starDist = Math.sqrt(this.centerX ** 2 + this.centerZ ** 2) || 1;
|
|
595
|
+
const tangentX = this.centerZ / starDist;
|
|
596
|
+
const tangentZ = -this.centerX / starDist;
|
|
597
|
+
|
|
598
|
+
for (const p of this.particles) {
|
|
599
|
+
if (p.released) continue;
|
|
600
|
+
|
|
601
|
+
p.released = true;
|
|
602
|
+
|
|
603
|
+
const worldX = this.centerX + p.offsetX;
|
|
604
|
+
const worldY = this.centerY + p.offsetY;
|
|
605
|
+
const worldZ = this.centerZ + p.offsetZ;
|
|
606
|
+
|
|
607
|
+
// Stream toward BH center
|
|
608
|
+
const toCenter = Math.sqrt(worldX ** 2 + worldZ ** 2) || 1;
|
|
609
|
+
const toCenterX = -worldX / toCenter;
|
|
610
|
+
const toCenterZ = -worldZ / toCenter;
|
|
611
|
+
|
|
612
|
+
const speed = CONFIG.streamSpeed * (0.4 + Math.random() * 0.6);
|
|
613
|
+
const spread = (Math.random() - 0.5) * CONFIG.streamSpread * 2;
|
|
614
|
+
|
|
615
|
+
released.push({
|
|
616
|
+
x: worldX,
|
|
617
|
+
y: worldY,
|
|
618
|
+
z: worldZ,
|
|
619
|
+
vx:
|
|
620
|
+
toCenterX * speed * 0.6 +
|
|
621
|
+
tangentX * speed * 0.4 +
|
|
622
|
+
spread * tangentX * speed,
|
|
623
|
+
vy: -Math.abs(p.offsetY) * 0.3,
|
|
624
|
+
vz:
|
|
625
|
+
toCenterZ * speed * 0.6 +
|
|
626
|
+
tangentZ * speed * 0.4 +
|
|
627
|
+
spread * tangentZ * speed,
|
|
628
|
+
size: p.size,
|
|
629
|
+
color: { ...p.color },
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
this.intactRatio = 0;
|
|
634
|
+
this.visible = false;
|
|
635
|
+
return released;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Update - compute render state for use in draw().
|
|
640
|
+
*/
|
|
641
|
+
update(dt) {
|
|
642
|
+
super.update(dt);
|
|
643
|
+
|
|
644
|
+
// Compute screen position and effective radius for rendering
|
|
645
|
+
if (this.camera && this.visible && this.intactRatio > 0.01) {
|
|
646
|
+
const projected = this.camera.project(
|
|
647
|
+
this.centerX,
|
|
648
|
+
this.centerY,
|
|
649
|
+
this.centerZ,
|
|
650
|
+
);
|
|
651
|
+
this.screenX = this.game.width / 2 + projected.x;
|
|
652
|
+
this.screenY = this.game.height / 2 + projected.y;
|
|
653
|
+
|
|
654
|
+
// Calculate camera-space z for z-ordering
|
|
655
|
+
this.cameraZ = this.getCameraZ();
|
|
656
|
+
|
|
657
|
+
// DISTANCE-BASED SCALING: Star SHRINKS as it approaches black hole
|
|
658
|
+
// This creates the effect of falling "into" the scene toward the BH
|
|
659
|
+
const distToBH = Math.sqrt(
|
|
660
|
+
this.centerX ** 2 + this.centerY ** 2 + this.centerZ ** 2,
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
// Normalize: 0 at periapsis (close), 1 at apoapsis (far)
|
|
664
|
+
const normalizedDist = Math.max(
|
|
665
|
+
0,
|
|
666
|
+
Math.min(
|
|
667
|
+
1,
|
|
668
|
+
(distToBH - this.periapsis) / (this.apoapsis - this.periapsis),
|
|
669
|
+
),
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
// Scale: big when far (1.8x), small when close (0.4x)
|
|
673
|
+
this.distanceScale =
|
|
674
|
+
CONFIG.distanceScaleMin +
|
|
675
|
+
(CONFIG.distanceScaleMax - CONFIG.distanceScaleMin) * normalizedDist;
|
|
676
|
+
|
|
677
|
+
// Combined scale: distance + camera perspective + consumption
|
|
678
|
+
this.perspectiveScale = projected.scale;
|
|
679
|
+
const shrinkFactor = Math.pow(this.intactRatio, 0.7);
|
|
680
|
+
|
|
681
|
+
this.effectiveRadius =
|
|
682
|
+
this.radius *
|
|
683
|
+
shrinkFactor *
|
|
684
|
+
this.distanceScale *
|
|
685
|
+
CONFIG.perspectiveMultiplier;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Get the star's z-position in camera space.
|
|
691
|
+
* Used for z-ordering relative to black hole.
|
|
692
|
+
*/
|
|
693
|
+
getCameraZ() {
|
|
694
|
+
if (!this.camera) return 0;
|
|
695
|
+
|
|
696
|
+
// Transform star center to camera space
|
|
697
|
+
const cosY = Math.cos(this.camera.rotationY);
|
|
698
|
+
const sinY = Math.sin(this.camera.rotationY);
|
|
699
|
+
let zCam = this.centerX * sinY + this.centerZ * cosY;
|
|
700
|
+
|
|
701
|
+
const cosX = Math.cos(this.camera.rotationX);
|
|
702
|
+
const sinX = Math.sin(this.camera.rotationX);
|
|
703
|
+
zCam = this.centerY * sinX + zCam * cosX;
|
|
704
|
+
|
|
705
|
+
return zCam;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Draw the star body and particle texture.
|
|
710
|
+
* Uses CAMERA PERSPECTIVE for correct size scaling.
|
|
711
|
+
* ALPHA IS CONSTANT - no transparency during approach/disruption.
|
|
712
|
+
*/
|
|
713
|
+
render() {
|
|
714
|
+
super.render();
|
|
715
|
+
if (!this.visible) return;
|
|
716
|
+
if (this.intactRatio <= 0.01) return;
|
|
717
|
+
|
|
718
|
+
// SIZE shrinks when disrupted, but ALPHA stays constant (no transparency)
|
|
719
|
+
const sizeFactor =
|
|
720
|
+
this.intactRatio > 0.6 ? 1.0 : Math.pow(this.intactRatio / 0.6, 0.5);
|
|
721
|
+
|
|
722
|
+
const cx = this.game.width / 2;
|
|
723
|
+
const cy = this.game.height / 2;
|
|
724
|
+
|
|
725
|
+
// Use DISTANCE-BASED scaling (computed in update)
|
|
726
|
+
// Star shrinks as it approaches BH
|
|
727
|
+
const distScale = this.distanceScale || 1.0;
|
|
728
|
+
|
|
729
|
+
// Base size scaled by DISTANCE TO BLACK HOLE
|
|
730
|
+
// Far = big (1.8x), Close = small (0.4x)
|
|
731
|
+
const baseVisualSize =
|
|
732
|
+
this.radius * distScale * CONFIG.perspectiveMultiplier;
|
|
733
|
+
|
|
734
|
+
Painter.useCtx((ctx) => {
|
|
735
|
+
// Draw each star particle with distance-based scaling
|
|
736
|
+
for (const p of this.particles) {
|
|
737
|
+
if (p.released) continue;
|
|
738
|
+
|
|
739
|
+
// World position INCLUDING DRIFT toward black hole
|
|
740
|
+
const wx = this.centerX + p.offsetX + p.driftX;
|
|
741
|
+
const wy = this.centerY + p.offsetY + p.driftY;
|
|
742
|
+
const wz = this.centerZ + p.offsetZ + p.driftZ;
|
|
743
|
+
|
|
744
|
+
// Project through camera
|
|
745
|
+
const projected = this.camera.project(wx, wy, wz);
|
|
746
|
+
const screenX = cx + projected.x;
|
|
747
|
+
const screenY = cy + projected.y;
|
|
748
|
+
|
|
749
|
+
// Skip if behind camera
|
|
750
|
+
if (projected.scale <= 0) continue;
|
|
751
|
+
|
|
752
|
+
// Size uses DISTANCE SCALE - shrinks as star approaches BH
|
|
753
|
+
const size =
|
|
754
|
+
p.size * distScale * sizeFactor * CONFIG.perspectiveMultiplier;
|
|
755
|
+
|
|
756
|
+
// Draw particle - ALPHA IS CONSTANT (no transparency!)
|
|
757
|
+
const alpha = p.color.a;
|
|
758
|
+
ctx.fillStyle = `rgba(${Math.round(p.color.r)}, ${Math.round(p.color.g)}, ${Math.round(p.color.b)}, ${alpha})`;
|
|
759
|
+
ctx.beginPath();
|
|
760
|
+
ctx.arc(screenX, screenY, size / 2, 0, Math.PI * 2);
|
|
761
|
+
ctx.fill();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// Draw star core with distance scale - ALPHA IS CONSTANT
|
|
765
|
+
const coreRadius = baseVisualSize * CONFIG.bodyRadius * sizeFactor;
|
|
766
|
+
const glowRadius = baseVisualSize * CONFIG.glowRadius * sizeFactor;
|
|
767
|
+
|
|
768
|
+
if (coreRadius > 1) {
|
|
769
|
+
// Outer glow - FULL OPACITY, scales with distance
|
|
770
|
+
const outerGradient = ctx.createRadialGradient(
|
|
771
|
+
this.screenX,
|
|
772
|
+
this.screenY,
|
|
773
|
+
coreRadius * 0.5,
|
|
774
|
+
this.screenX,
|
|
775
|
+
this.screenY,
|
|
776
|
+
glowRadius,
|
|
777
|
+
);
|
|
778
|
+
outerGradient.addColorStop(0, "rgba(255, 250, 220, 0.8)");
|
|
779
|
+
outerGradient.addColorStop(0.3, "rgba(255, 230, 180, 0.5)");
|
|
780
|
+
outerGradient.addColorStop(0.6, "rgba(255, 180, 100, 0.2)");
|
|
781
|
+
outerGradient.addColorStop(1, "rgba(255, 150, 50, 0)");
|
|
782
|
+
|
|
783
|
+
ctx.fillStyle = outerGradient;
|
|
784
|
+
ctx.shadowColor = "rgba(255, 220, 150, 0.6)";
|
|
785
|
+
ctx.shadowBlur = 20 * distScale; // Glow scales with distance
|
|
786
|
+
ctx.beginPath();
|
|
787
|
+
ctx.arc(this.screenX, this.screenY, glowRadius, 0, Math.PI * 2);
|
|
788
|
+
ctx.fill();
|
|
789
|
+
|
|
790
|
+
// Star body core - FULL OPACITY
|
|
791
|
+
ctx.shadowBlur = 0;
|
|
792
|
+
const bodyGradient = ctx.createRadialGradient(
|
|
793
|
+
this.screenX,
|
|
794
|
+
this.screenY,
|
|
795
|
+
0,
|
|
796
|
+
this.screenX,
|
|
797
|
+
this.screenY,
|
|
798
|
+
coreRadius,
|
|
799
|
+
);
|
|
800
|
+
bodyGradient.addColorStop(0, "rgba(255, 255, 255, 1)");
|
|
801
|
+
bodyGradient.addColorStop(0.3, "rgba(255, 255, 240, 1)");
|
|
802
|
+
bodyGradient.addColorStop(0.6, "rgba(255, 240, 200, 0.95)");
|
|
803
|
+
bodyGradient.addColorStop(1, "rgba(255, 220, 150, 0.9)");
|
|
804
|
+
|
|
805
|
+
ctx.fillStyle = bodyGradient;
|
|
806
|
+
ctx.beginPath();
|
|
807
|
+
ctx.arc(this.screenX, this.screenY, coreRadius, 0, Math.PI * 2);
|
|
808
|
+
ctx.fill();
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
}
|