@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,317 @@
|
|
|
1
|
+
import { GameObject, Sphere3D } from "../../../src/index.js";
|
|
2
|
+
import { polarToCartesian } from "../../../src/math/gr.js";
|
|
3
|
+
import { CONFIG } from "./config.js";
|
|
4
|
+
|
|
5
|
+
// Star shader configuration
|
|
6
|
+
const STAR_SHADER_CONFIG = {
|
|
7
|
+
useShader: true,
|
|
8
|
+
shaderType: "star",
|
|
9
|
+
shaderUniforms: {
|
|
10
|
+
uStarColor: [1.0, 0.85, 0.3], // Golden yellow
|
|
11
|
+
uTemperature: 5500, // K (slightly cooler than Sun)
|
|
12
|
+
uActivityLevel: 0.75, // Moderate surface activity
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class Star extends GameObject {
|
|
17
|
+
constructor(game, options = {}) {
|
|
18
|
+
super(game, options);
|
|
19
|
+
this.mass = options.initialMass ?? CONFIG.star.initialMass;
|
|
20
|
+
this.initialMass = this.mass; // Store for mass ratio calculations
|
|
21
|
+
this.phi = 0;
|
|
22
|
+
// Initialize with reasonable defaults, will be updated by onResize
|
|
23
|
+
this.baseRadius = game.baseScale ? game.baseScale * CONFIG.starRadiusRatio : 20;
|
|
24
|
+
this.currentRadius = this.baseRadius;
|
|
25
|
+
this.orbitalRadius = game.baseScale ? game.baseScale * CONFIG.star.initialOrbitRadius : 200;
|
|
26
|
+
this.initialOrbitalRadius = this.orbitalRadius; // Store initial for decay calculations
|
|
27
|
+
|
|
28
|
+
// Velocity tracking for particle emission
|
|
29
|
+
this.velocityX = 0;
|
|
30
|
+
this.velocityY = 0;
|
|
31
|
+
this.velocityZ = 0;
|
|
32
|
+
this._prevX = 0;
|
|
33
|
+
this._prevY = 0;
|
|
34
|
+
this._prevZ = 0;
|
|
35
|
+
|
|
36
|
+
// Use WebGL shaders for star rendering
|
|
37
|
+
this.useShader = options.useShader ?? true;
|
|
38
|
+
|
|
39
|
+
// Cumulative rotation for angular emission detail
|
|
40
|
+
this.rotation = 0;
|
|
41
|
+
// Angular velocity (rad/s) - accumulates smoothly instead of discrete recalc
|
|
42
|
+
this.angularVelocity = CONFIG.star.rotationSpeed ?? 0.5;
|
|
43
|
+
|
|
44
|
+
// Tidal disruption state
|
|
45
|
+
this.tidalStretch = 0; // 0 = spherical, 1 = max elongation
|
|
46
|
+
this.pulsationPhase = 0; // Oscillation phase
|
|
47
|
+
this.stressLevel = 0; // Surface chaos level
|
|
48
|
+
this.tidalProgress = 0; // External tidal progress from FSM (0-1)
|
|
49
|
+
this.tidalFlare = 0; // 0-1, sudden brightness burst at disruption start
|
|
50
|
+
this.tidalWobble = 0; // 0-1, violent geometry wobble during trauma
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
init() {
|
|
54
|
+
// Initialize position on the orbit
|
|
55
|
+
const pos = polarToCartesian(this.orbitalRadius, this.phi);
|
|
56
|
+
this.x = pos.x;
|
|
57
|
+
this.z = pos.z;
|
|
58
|
+
|
|
59
|
+
// Initialize prev position to avoid velocity spike on first frame
|
|
60
|
+
this._prevX = this.x;
|
|
61
|
+
this._prevY = this.y || 0;
|
|
62
|
+
this._prevZ = this.z;
|
|
63
|
+
this.velocityX = 0;
|
|
64
|
+
this.velocityY = 0;
|
|
65
|
+
this.velocityZ = 0;
|
|
66
|
+
|
|
67
|
+
// Reset tidal state
|
|
68
|
+
this.tidalStretch = 0;
|
|
69
|
+
this.pulsationPhase = 0;
|
|
70
|
+
this.stressLevel = 0;
|
|
71
|
+
this.tidalProgress = 0;
|
|
72
|
+
this.tidalFlare = 0;
|
|
73
|
+
this.tidalWobble = 0;
|
|
74
|
+
this.angularVelocity = CONFIG.star.rotationSpeed ?? 0.5;
|
|
75
|
+
this.rotation = 0;
|
|
76
|
+
|
|
77
|
+
this.updateVisual();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Reset velocity tracking (call after position changes like restart)
|
|
82
|
+
*/
|
|
83
|
+
resetVelocity() {
|
|
84
|
+
this._prevX = this.x;
|
|
85
|
+
this._prevY = this.y || 0;
|
|
86
|
+
this._prevZ = this.z;
|
|
87
|
+
this.velocityX = 0;
|
|
88
|
+
this.velocityY = 0;
|
|
89
|
+
this.velocityZ = 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
updateVisual() {
|
|
93
|
+
const massRatio = this.mass / this.initialMass;
|
|
94
|
+
|
|
95
|
+
// === NON-LINEAR SIZE COLLAPSE ===
|
|
96
|
+
// Star resists at first (internal pressure), then collapses rapidly
|
|
97
|
+
// Use a power curve: slow start, rapid end
|
|
98
|
+
const collapseProgress = 1 - massRatio; // 0 = full, 1 = gone
|
|
99
|
+
const resistanceCurve = Math.pow(collapseProgress, 0.5); // sqrt = resists early
|
|
100
|
+
const effectiveMassRatio = 1 - resistanceCurve;
|
|
101
|
+
|
|
102
|
+
// Base radius with non-linear collapse
|
|
103
|
+
this.currentRadius = this.baseRadius * Math.max(0.05, effectiveMassRatio);
|
|
104
|
+
|
|
105
|
+
// Don't update geometry if star is consumed
|
|
106
|
+
if (this.currentRadius <= 0 || this.mass <= 0) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// === TIDAL STRETCH (Spaghettification) ===
|
|
111
|
+
// Create comet/teardrop shape pointed toward black hole
|
|
112
|
+
const dist = Math.sqrt(this.x * this.x + (this.z || 0) * (this.z || 0)) || 1;
|
|
113
|
+
|
|
114
|
+
// Direction toward black hole (unit vector)
|
|
115
|
+
let dirX = -this.x / dist;
|
|
116
|
+
let dirZ = -(this.z || 0) / dist;
|
|
117
|
+
|
|
118
|
+
// Proximity factor: closer to BH = more stretch
|
|
119
|
+
let proximityFactor = Math.max(0, 1 - dist / this.initialOrbitalRadius);
|
|
120
|
+
|
|
121
|
+
// Calculate stretch amount based on phase and proximity
|
|
122
|
+
if (collapseProgress > 0.8) {
|
|
123
|
+
// Very late stage - reduce stretch as star becomes tiny
|
|
124
|
+
this.tidalStretch = (1 - collapseProgress) * 2;
|
|
125
|
+
} else {
|
|
126
|
+
// Main deformation: builds with tidalProgress and proximity
|
|
127
|
+
// tidalProgress is driven by FSM state (0 in approach, ramps in stretch/disrupt)
|
|
128
|
+
const baseStretch = this.tidalProgress * 1.2; // Up to 1.2 stretch
|
|
129
|
+
const proximityBoost = proximityFactor * 0.5; // Extra stretch when close
|
|
130
|
+
|
|
131
|
+
this.tidalStretch = baseStretch + proximityBoost;
|
|
132
|
+
this.tidalStretch = Math.min(1.8, this.tidalStretch); // Cap at 1.8
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// === BREATHING (Slow, ominous expansion/contraction) ===
|
|
136
|
+
// Very slow rhythm - like a dying star's final gasps
|
|
137
|
+
// No rapid bouncing - this should feel cosmic, not cartoonish
|
|
138
|
+
const breathingAmp = 0.03 * (1 - collapseProgress * 0.5); // Subtle, weakens as disrupted
|
|
139
|
+
const breathing = Math.sin(this.pulsationPhase) * breathingAmp;
|
|
140
|
+
|
|
141
|
+
// Apply breathing to radius (very subtle)
|
|
142
|
+
this.currentRadius *= (1 + breathing);
|
|
143
|
+
|
|
144
|
+
// === STRESS LEVEL ===
|
|
145
|
+
// Combines proximity and mass loss - drives surface chaos
|
|
146
|
+
// Use power curve so stress stays LOW for most of disruption, then ramps up sharply
|
|
147
|
+
// This gives more time to see the red-orange star with surface chaos
|
|
148
|
+
const rawStress = proximityFactor * 0.4 + collapseProgress * 0.6; // Reduced weights
|
|
149
|
+
// Power of 3 = stays low longer, ramps up sharply at the end
|
|
150
|
+
this.stressLevel = Math.min(1, Math.pow(rawStress, 2.5));
|
|
151
|
+
|
|
152
|
+
// === ACTIVITY & ROTATION ===
|
|
153
|
+
const activityLevel = 0.3 + this.stressLevel * 0.7; // 0.3 -> 1.0
|
|
154
|
+
|
|
155
|
+
// Angular momentum conservation: shrinking = faster spin
|
|
156
|
+
const baseRotationSpeed = CONFIG.star.rotationSpeed ?? 0.5;
|
|
157
|
+
const spinUpFactor = 1 / Math.max(0.2, effectiveMassRatio); // Inverse of size
|
|
158
|
+
const rotationSpeed = Math.min(10, baseRotationSpeed * spinUpFactor);
|
|
159
|
+
|
|
160
|
+
// === COLOR SHIFT ===
|
|
161
|
+
// Start deep red-orange, transition through orange → yellow → white
|
|
162
|
+
// This lets us see the tidal chaos on a colorful surface before brightening
|
|
163
|
+
//
|
|
164
|
+
// Phase 1 (stress 0-0.5): Deep red-orange, surface chaos building
|
|
165
|
+
// Phase 2 (stress 0.5-0.8): Shift to orange-yellow, intense activity
|
|
166
|
+
// Phase 3 (stress 0.8-1.0): Rapid shift to white-hot, death throes
|
|
167
|
+
|
|
168
|
+
// Temperature increases with stress (tidal heating is real physics!)
|
|
169
|
+
const tempShift = this.stressLevel * this.stressLevel * 2500; // Up to +2500K at max stress
|
|
170
|
+
const temperature = (CONFIG.star.temperature ?? 3800) + tempShift;
|
|
171
|
+
|
|
172
|
+
// Color transition: red-orange → orange → yellow → white
|
|
173
|
+
// R stays high, G increases with stress, B only increases late
|
|
174
|
+
let r = 1.0;
|
|
175
|
+
let g, b;
|
|
176
|
+
|
|
177
|
+
if (this.stressLevel < 0.5) {
|
|
178
|
+
// Phase 1: Deep red-orange → orange (stress 0-0.5)
|
|
179
|
+
const t = this.stressLevel * 2; // 0 to 1 over this phase
|
|
180
|
+
g = 0.35 + t * 0.25; // 0.35 → 0.6
|
|
181
|
+
b = 0.15 + t * 0.1; // 0.15 → 0.25
|
|
182
|
+
} else if (this.stressLevel < 0.8) {
|
|
183
|
+
// Phase 2: Orange → yellow-orange (stress 0.5-0.8)
|
|
184
|
+
const t = (this.stressLevel - 0.5) / 0.3; // 0 to 1
|
|
185
|
+
g = 0.6 + t * 0.2; // 0.6 → 0.8
|
|
186
|
+
b = 0.25 + t * 0.1; // 0.25 → 0.35
|
|
187
|
+
} else {
|
|
188
|
+
// Phase 3: Yellow-orange → white-hot (stress 0.8-1.0)
|
|
189
|
+
const t = (this.stressLevel - 0.8) / 0.2; // 0 to 1
|
|
190
|
+
g = 0.8 + t * 0.15; // 0.8 → 0.95
|
|
191
|
+
b = 0.35 + t * 0.5; // 0.35 → 0.85 (rapid blue increase = white)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const stressColor = [r, g, b];
|
|
195
|
+
|
|
196
|
+
// Expose current color for particle emission
|
|
197
|
+
this.currentColor = stressColor;
|
|
198
|
+
|
|
199
|
+
if (!this.visual) {
|
|
200
|
+
this.visual = new Sphere3D(this.currentRadius, {
|
|
201
|
+
color: CONFIG.star.color,
|
|
202
|
+
camera: this.game.camera,
|
|
203
|
+
useShader: this.useShader,
|
|
204
|
+
shaderType: "star",
|
|
205
|
+
shaderUniforms: {
|
|
206
|
+
uStarColor: stressColor,
|
|
207
|
+
uTemperature: temperature,
|
|
208
|
+
uActivityLevel: activityLevel,
|
|
209
|
+
uRotationSpeed: rotationSpeed,
|
|
210
|
+
uTidalStretch: this.tidalStretch,
|
|
211
|
+
uStretchDirX: dirX,
|
|
212
|
+
uStretchDirZ: dirZ,
|
|
213
|
+
uStressLevel: this.stressLevel,
|
|
214
|
+
uTidalFlare: this.tidalFlare,
|
|
215
|
+
uTidalWobble: this.tidalWobble,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
} else {
|
|
219
|
+
this.visual.radius = this.currentRadius;
|
|
220
|
+
if (this.visual.useShader) {
|
|
221
|
+
this.visual.setShaderUniforms({
|
|
222
|
+
uStarColor: stressColor,
|
|
223
|
+
uTemperature: temperature,
|
|
224
|
+
uActivityLevel: activityLevel,
|
|
225
|
+
uRotationSpeed: rotationSpeed,
|
|
226
|
+
uTidalStretch: this.tidalStretch,
|
|
227
|
+
uStretchDirX: dirX,
|
|
228
|
+
uStretchDirZ: dirZ,
|
|
229
|
+
uStressLevel: this.stressLevel,
|
|
230
|
+
uTidalFlare: this.tidalFlare,
|
|
231
|
+
uTidalWobble: this.tidalWobble,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
this.visual._generateGeometry();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
onResize(baseRadius, orbitalRadius) {
|
|
239
|
+
this.baseRadius = baseRadius;
|
|
240
|
+
this.orbitalRadius = orbitalRadius;
|
|
241
|
+
this.initialOrbitalRadius = orbitalRadius;
|
|
242
|
+
|
|
243
|
+
// Update position to match new orbital radius
|
|
244
|
+
const pos = polarToCartesian(this.orbitalRadius, this.phi);
|
|
245
|
+
this.x = pos.x;
|
|
246
|
+
this.z = pos.z;
|
|
247
|
+
|
|
248
|
+
this.updateVisual();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
update(dt) {
|
|
252
|
+
super.update(dt);
|
|
253
|
+
|
|
254
|
+
// Calculate velocity from position change
|
|
255
|
+
const currentY = this.y || 0;
|
|
256
|
+
if (dt > 0) {
|
|
257
|
+
this.velocityX = (this.x - this._prevX) / dt;
|
|
258
|
+
this.velocityY = (currentY - this._prevY) / dt;
|
|
259
|
+
this.velocityZ = (this.z - this._prevZ) / dt;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Store current position for next frame
|
|
263
|
+
this._prevX = this.x;
|
|
264
|
+
this._prevY = currentY;
|
|
265
|
+
this._prevZ = this.z;
|
|
266
|
+
|
|
267
|
+
// Update self-rotation with smooth angular momentum conservation
|
|
268
|
+
// As star shrinks, angular velocity increases (I*ω = constant)
|
|
269
|
+
// But cap it when star is tiny (< 10% radius) - no point wasting frames
|
|
270
|
+
const radiusRatio = this.currentRadius / this.baseRadius;
|
|
271
|
+
|
|
272
|
+
if (radiusRatio > 0.1) {
|
|
273
|
+
// Base rotation speed from config
|
|
274
|
+
const baseSpeed = CONFIG.star.rotationSpeed ?? 0.5;
|
|
275
|
+
|
|
276
|
+
// Spin-up factor based on tidal progress (FSM-driven, smooth)
|
|
277
|
+
// Only significant spin-up during actual disruption (mass loss)
|
|
278
|
+
const massRatio = (this.mass || 1) / (this.initialMass || 1);
|
|
279
|
+
const massLoss = 1 - massRatio; // 0 = no loss, 1 = fully consumed
|
|
280
|
+
|
|
281
|
+
// Gentle spin-up from tidal stress, moderate spin-up from mass loss
|
|
282
|
+
// tidalProgress: 0-1 during stretch, 1 during disrupt
|
|
283
|
+
// massLoss: 0 during stretch, 0-1 during disrupt
|
|
284
|
+
const tidalSpinUp = 1 + this.tidalProgress * 0.3; // Up to 1.3x from tidal
|
|
285
|
+
const collapseSpinUp = 1 + massLoss * 1.5; // Up to 2.5x from collapse
|
|
286
|
+
|
|
287
|
+
const targetVelocity = baseSpeed * tidalSpinUp * collapseSpinUp;
|
|
288
|
+
|
|
289
|
+
// Very slow approach to target - no sudden jumps
|
|
290
|
+
const accelRate = 0.001;
|
|
291
|
+
this.angularVelocity += (targetVelocity - this.angularVelocity) * accelRate * dt;
|
|
292
|
+
|
|
293
|
+
// Hard cap on max spin (2.5 rad/s - calm, cosmic feel)
|
|
294
|
+
this.angularVelocity = Math.min(2.5, this.angularVelocity);
|
|
295
|
+
}
|
|
296
|
+
// else: keep current velocity, don't accelerate tiny remnant
|
|
297
|
+
|
|
298
|
+
this.rotation += this.angularVelocity * dt;
|
|
299
|
+
|
|
300
|
+
// Update breathing phase - slow, cosmic rhythm (0.3-0.5 Hz)
|
|
301
|
+
const breathingFreq = 0.3 + this.stressLevel * 0.2;
|
|
302
|
+
this.pulsationPhase += breathingFreq * dt * Math.PI * 2;
|
|
303
|
+
|
|
304
|
+
this.updateVisual();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
render() {
|
|
308
|
+
super.render();
|
|
309
|
+
if (this.mass > 0 && this.visual) {
|
|
310
|
+
// Sync visual position with star position
|
|
311
|
+
this.visual.x = this.x;
|
|
312
|
+
this.visual.y = this.y || 0;
|
|
313
|
+
this.visual.z = this.z;
|
|
314
|
+
this.visual.render();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { GameObject, Painter } from "../../../src/index.js";
|
|
2
|
+
import { applyGravitationalLensing } from "../../../src/math/gr.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TidalStream - Simple particle stream from star to black hole
|
|
6
|
+
*
|
|
7
|
+
* Physics:
|
|
8
|
+
* - Particles emitted from star inherit star's velocity
|
|
9
|
+
* - Gravity attracts particles toward black hole (0,0,0)
|
|
10
|
+
* - Gravitational lensing bends particle paths near the BH
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Stream-specific config
|
|
14
|
+
const STREAM_CONFIG = {
|
|
15
|
+
gravity: 120000, // Strong gravity (linear falloff G/r)
|
|
16
|
+
maxParticles: 5000,
|
|
17
|
+
particleLifetime: 12, // Seconds - long lifetime so particles can orbit the BH
|
|
18
|
+
|
|
19
|
+
// Velocity inheritance - how much of star's velocity particles get
|
|
20
|
+
// Lower = particles emit more "from" the star, not ahead of it
|
|
21
|
+
velocityInheritance: 0.3,
|
|
22
|
+
|
|
23
|
+
// Inward velocity - particles should FALL toward BH, not orbit
|
|
24
|
+
// This is the key to making particles flow INTO the black hole
|
|
25
|
+
inwardVelocity: 8, // Base inward velocity toward BH
|
|
26
|
+
inwardSpread: 15, // Random spread on inward velocity
|
|
27
|
+
|
|
28
|
+
// Tangent spread for S-shape - higher = more spread along orbit direction
|
|
29
|
+
tangentSpread: Math.PI * 150, // Spread for visible S-shape
|
|
30
|
+
|
|
31
|
+
// Emission offset: 1.0 = star's BH-facing edge (L1 Lagrange point)
|
|
32
|
+
// Positive = toward BH, negative = away from BH
|
|
33
|
+
emissionOffset: -1 * Math.PI, // Larger numbers create bigger S-Shape. Negative PI works very well here for some reason makes the animation very cool.
|
|
34
|
+
|
|
35
|
+
// Drag factor - removes angular momentum so orbits decay
|
|
36
|
+
// 1.0 = no drag, 0.99 = slight drag, 0.95 = strong drag
|
|
37
|
+
drag: 0.994,
|
|
38
|
+
|
|
39
|
+
// Colors: match star shader at emission, cool as they approach BH
|
|
40
|
+
colorHot: { r: 255, g: 95, b: 45 }, // Deep red-orange (matches star shader initial)
|
|
41
|
+
colorCool: { r: 180, g: 40, b: 15 }, // Darker red near BH
|
|
42
|
+
|
|
43
|
+
// Particle size
|
|
44
|
+
sizeMin: 1,
|
|
45
|
+
sizeMax: 3,
|
|
46
|
+
|
|
47
|
+
// Gravitational lensing (visual effect)
|
|
48
|
+
// These are multipliers relative to the BH's current radius
|
|
49
|
+
lensing: {
|
|
50
|
+
enabled: true,
|
|
51
|
+
effectRadiusMult: 6.0, // Effect extends to 6x BH radius
|
|
52
|
+
strengthMult: 2.5, // Strength scales with BH radius
|
|
53
|
+
falloff: 0.008, // Exponential falloff (higher = tighter effect)
|
|
54
|
+
minDistanceMult: 0.2, // Min distance as fraction of BH radius
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export class TidalStream extends GameObject {
|
|
59
|
+
constructor(game, options = {}) {
|
|
60
|
+
super(game, options);
|
|
61
|
+
|
|
62
|
+
this.camera = options.camera;
|
|
63
|
+
this.scene = options.scene; // Scene reference for screen center
|
|
64
|
+
this.bhRadius = options.bhRadius ?? 50;
|
|
65
|
+
|
|
66
|
+
// Callbacks for particle lifecycle
|
|
67
|
+
this.onParticleConsumed = options.onParticleConsumed ?? null;
|
|
68
|
+
this.onParticleCaptured = options.onParticleCaptured ?? null;
|
|
69
|
+
|
|
70
|
+
// Particle array - simple flat structure
|
|
71
|
+
this.particles = [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
init() {
|
|
75
|
+
this.particles = [];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Emit a particle from the star
|
|
80
|
+
*
|
|
81
|
+
* For S-shape formation, particles need TANGENTIAL velocity spread:
|
|
82
|
+
* - Faster particles (more angular momentum) spiral outward
|
|
83
|
+
* - Slower particles (less angular momentum) spiral inward
|
|
84
|
+
* - This creates two opposing tails = S-shape
|
|
85
|
+
*
|
|
86
|
+
* @param {number} x - Star x position
|
|
87
|
+
* @param {number} y - Star y position
|
|
88
|
+
* @param {number} z - Star z position
|
|
89
|
+
* @param {number} vx - Star velocity x
|
|
90
|
+
* @param {number} vy - Star velocity y
|
|
91
|
+
* @param {number} vz - Star velocity z
|
|
92
|
+
* @param {number} starRadius - Current star radius (for position spread)
|
|
93
|
+
* @param {number} starRotation - Current star rotation (for angular offset)
|
|
94
|
+
* @param {Array<number>} starColor - Current star color as [r, g, b] normalized (0-1)
|
|
95
|
+
*/
|
|
96
|
+
emit(x, y, z, vx, vy, vz, starRadius, starRotation = 0, starColor = null) {
|
|
97
|
+
if (this.particles.length >= STREAM_CONFIG.maxParticles) return;
|
|
98
|
+
|
|
99
|
+
const dist = Math.sqrt(x * x + z * z) || 1;
|
|
100
|
+
|
|
101
|
+
// Direction toward BH in x-z plane (unit vector)
|
|
102
|
+
const radialX = -x / dist;
|
|
103
|
+
const radialZ = -z / dist;
|
|
104
|
+
|
|
105
|
+
// Emit from star center with spread for visible "bleeding" effect
|
|
106
|
+
// Larger spread = bigger emission hole on the star
|
|
107
|
+
const emitX = x + (Math.random() - 0.5) * starRadius * 0.8;
|
|
108
|
+
const emitY = y + (Math.random() - 0.5) * starRadius * 0.8;
|
|
109
|
+
const emitZ = z + (Math.random() - 0.5) * starRadius * 0.8;
|
|
110
|
+
|
|
111
|
+
// Tangent is perpendicular to radial - gives the orbital direction
|
|
112
|
+
const tangentX = -radialZ;
|
|
113
|
+
const tangentZ = radialX;
|
|
114
|
+
|
|
115
|
+
// Reduce inherited velocity so gravity can dominate
|
|
116
|
+
const inheritedVx = vx * STREAM_CONFIG.velocityInheritance;
|
|
117
|
+
const inheritedVz = vz * STREAM_CONFIG.velocityInheritance;
|
|
118
|
+
|
|
119
|
+
// INWARD velocity - particles flow TOWARD the black hole
|
|
120
|
+
// radialX, radialZ point toward BH (origin)
|
|
121
|
+
const inward = STREAM_CONFIG.inwardVelocity + (Math.random() - 0.5) * STREAM_CONFIG.inwardSpread;
|
|
122
|
+
|
|
123
|
+
// Small tangential spread for the S-shape variation
|
|
124
|
+
const tangent = (Math.random() - 0.5) * STREAM_CONFIG.tangentSpread;
|
|
125
|
+
|
|
126
|
+
// Store star color at emission time (convert from normalized 0-1 to 0-255)
|
|
127
|
+
const emitColor = starColor
|
|
128
|
+
? { r: starColor[0] * 255, g: starColor[1] * 255, b: starColor[2] * 255 }
|
|
129
|
+
: STREAM_CONFIG.colorHot;
|
|
130
|
+
|
|
131
|
+
this.particles.push({
|
|
132
|
+
x: emitX,
|
|
133
|
+
y: emitY,
|
|
134
|
+
z: emitZ,
|
|
135
|
+
|
|
136
|
+
// Velocity = inherited + INWARD toward BH + small tangent spread
|
|
137
|
+
vx: inheritedVx + radialX * inward + tangentX * tangent,
|
|
138
|
+
vy: vy,
|
|
139
|
+
vz: inheritedVz + radialZ * inward + tangentZ * tangent,
|
|
140
|
+
|
|
141
|
+
age: 0,
|
|
142
|
+
size: STREAM_CONFIG.sizeMin + Math.random() * (STREAM_CONFIG.sizeMax - STREAM_CONFIG.sizeMin),
|
|
143
|
+
|
|
144
|
+
// Track initial distance for color gradient
|
|
145
|
+
initialDist: dist,
|
|
146
|
+
|
|
147
|
+
// Store the star's color at emission time
|
|
148
|
+
emitColor,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
updateDiskBounds(innerRadius, outerRadius) {
|
|
153
|
+
// Don't override bhRadius here - it's set by updateBHRadius
|
|
154
|
+
// We only care about disk bounds for potential capture detection
|
|
155
|
+
this.diskInnerRadius = innerRadius;
|
|
156
|
+
this.diskOuterRadius = outerRadius;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Update all particles - just gravity
|
|
161
|
+
*/
|
|
162
|
+
update(dt) {
|
|
163
|
+
super.update(dt);
|
|
164
|
+
|
|
165
|
+
// Consume particles at the BH's visual edge (not inside it)
|
|
166
|
+
// Use 1.0x so particles disappear right at the event horizon
|
|
167
|
+
const accretionRadius = this.bhRadius * 1.1;
|
|
168
|
+
|
|
169
|
+
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
170
|
+
const p = this.particles[i];
|
|
171
|
+
|
|
172
|
+
p.age += dt;
|
|
173
|
+
|
|
174
|
+
// Remove old or accreted particles
|
|
175
|
+
if (p.age > STREAM_CONFIG.particleLifetime) {
|
|
176
|
+
this.particles.splice(i, 1);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Skip physics on first frame - let particle appear at spawn point first
|
|
181
|
+
// This prevents the "jump" where particles move before being rendered
|
|
182
|
+
if (p.age < dt * 1.5) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Distance to BH (at origin)
|
|
187
|
+
const dist = Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z);
|
|
188
|
+
|
|
189
|
+
// Accreted by black hole?
|
|
190
|
+
if (dist < accretionRadius) {
|
|
191
|
+
this.particles.splice(i, 1);
|
|
192
|
+
// Trigger callback - feeds the black hole's glow!
|
|
193
|
+
if (this.onParticleConsumed) {
|
|
194
|
+
this.onParticleConsumed();
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Gravity: F = G/r (linear falloff for better visuals)
|
|
200
|
+
// Linear falloff keeps gravity significant at larger distances
|
|
201
|
+
const gravity = STREAM_CONFIG.gravity / dist;
|
|
202
|
+
const dirX = -p.x * 2 / dist;
|
|
203
|
+
const dirY = -p.y * 2 / dist;
|
|
204
|
+
const dirZ = -p.z * 2 / dist;
|
|
205
|
+
|
|
206
|
+
// Apply gravity acceleration
|
|
207
|
+
p.vx += dirX * gravity * dt;
|
|
208
|
+
p.vy += dirY * gravity * dt;
|
|
209
|
+
p.vz += dirZ * gravity * dt;
|
|
210
|
+
|
|
211
|
+
// Apply drag - removes angular momentum so particles spiral inward
|
|
212
|
+
// Without drag, particles would orbit forever
|
|
213
|
+
p.vx *= STREAM_CONFIG.drag;
|
|
214
|
+
p.vy *= STREAM_CONFIG.drag;
|
|
215
|
+
p.vz *= STREAM_CONFIG.drag;
|
|
216
|
+
|
|
217
|
+
// Move particle
|
|
218
|
+
p.x += p.vx * dt;
|
|
219
|
+
p.y += p.vy * dt;
|
|
220
|
+
p.z += p.vz * dt;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Clear all particles
|
|
226
|
+
*/
|
|
227
|
+
clear() {
|
|
228
|
+
this.particles = [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Update BH radius (for accretion check)
|
|
233
|
+
*/
|
|
234
|
+
updateBHRadius(radius) {
|
|
235
|
+
this.bhRadius = radius;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Render particles
|
|
240
|
+
* We reset the canvas transform to identity since Scene3D applies its own
|
|
241
|
+
* transforms, and we need absolute screen coordinates for particle rendering.
|
|
242
|
+
*/
|
|
243
|
+
render() {
|
|
244
|
+
super.render();
|
|
245
|
+
|
|
246
|
+
if (!this.camera || this.particles.length === 0) return;
|
|
247
|
+
|
|
248
|
+
// Get the actual canvas transform that Scene3D has set up
|
|
249
|
+
// This is the same approach Sphere3D uses to get screen position
|
|
250
|
+
const ctx = Painter.ctx;
|
|
251
|
+
const transform = ctx.getTransform();
|
|
252
|
+
|
|
253
|
+
// TidalStream is at world (0,0,0), so Scene3D translated to:
|
|
254
|
+
// scene.x + project(0,0,0).x which is approximately scene.x
|
|
255
|
+
// We need to use this as our center, then add particle projections
|
|
256
|
+
const cx = transform.e;
|
|
257
|
+
const cy = transform.f;
|
|
258
|
+
|
|
259
|
+
// Build render list with projection
|
|
260
|
+
const renderList = [];
|
|
261
|
+
|
|
262
|
+
// Young particles stay invisible (appear to emerge from star)
|
|
263
|
+
const fadeInTime = 0.05; // seconds before particles become visible
|
|
264
|
+
const fadeInDuration = 0.1; // seconds to fade from invisible to full opacity
|
|
265
|
+
|
|
266
|
+
for (const p of this.particles) {
|
|
267
|
+
// Skip very young particles - they're "inside" the star
|
|
268
|
+
if (p.age < fadeInTime) continue;
|
|
269
|
+
|
|
270
|
+
const projected = this.camera.project(p.x, p.y, p.z);
|
|
271
|
+
|
|
272
|
+
// Skip if behind camera
|
|
273
|
+
if (projected.scale <= 0) continue;
|
|
274
|
+
|
|
275
|
+
// Fade in young particles (after fadeInTime threshold)
|
|
276
|
+
const fadeInProgress = Math.min(1, (p.age - fadeInTime) / fadeInDuration);
|
|
277
|
+
|
|
278
|
+
// Distance from BH for color
|
|
279
|
+
const dist = Math.sqrt(p.x * p.x + p.z * p.z);
|
|
280
|
+
const colorT = Math.min(1, dist / (p.initialDist || 1));
|
|
281
|
+
|
|
282
|
+
// Use particle's emitted color (star color at emission time)
|
|
283
|
+
const hotColor = p.emitColor || STREAM_CONFIG.colorHot;
|
|
284
|
+
|
|
285
|
+
// Lerp color: cool near BH, hot (star color) near initial position
|
|
286
|
+
const color = {
|
|
287
|
+
r: STREAM_CONFIG.colorCool.r + (hotColor.r - STREAM_CONFIG.colorCool.r) * colorT,
|
|
288
|
+
g: STREAM_CONFIG.colorCool.g + (hotColor.g - STREAM_CONFIG.colorCool.g) * colorT,
|
|
289
|
+
b: STREAM_CONFIG.colorCool.b + (hotColor.b - STREAM_CONFIG.colorCool.b) * colorT,
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// Fade with age (fade out at end of life) and fade in at birth
|
|
293
|
+
const fadeOutAlpha = Math.max(0, 1 - p.age / STREAM_CONFIG.particleLifetime);
|
|
294
|
+
const alpha = fadeOutAlpha * fadeInProgress; // Combine fade-in and fade-out
|
|
295
|
+
|
|
296
|
+
// Apply gravitational lensing to screen coordinates
|
|
297
|
+
// Scale lensing with BH's current (pulsing) radius
|
|
298
|
+
let screenX = projected.x;
|
|
299
|
+
let screenY = projected.y;
|
|
300
|
+
|
|
301
|
+
if (STREAM_CONFIG.lensing.enabled && this.bhRadius > 0) {
|
|
302
|
+
const effectRadius = this.bhRadius * STREAM_CONFIG.lensing.effectRadiusMult;
|
|
303
|
+
const strength = this.bhRadius * STREAM_CONFIG.lensing.strengthMult;
|
|
304
|
+
const minDist = this.bhRadius * STREAM_CONFIG.lensing.minDistanceMult;
|
|
305
|
+
|
|
306
|
+
const lensed = applyGravitationalLensing(
|
|
307
|
+
screenX, screenY,
|
|
308
|
+
effectRadius,
|
|
309
|
+
strength,
|
|
310
|
+
STREAM_CONFIG.lensing.falloff,
|
|
311
|
+
minDist
|
|
312
|
+
);
|
|
313
|
+
screenX = lensed.x;
|
|
314
|
+
screenY = lensed.y;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check if particle is visually inside the black hole
|
|
318
|
+
const screenDist = Math.sqrt(screenX * screenX + screenY * screenY);
|
|
319
|
+
const insideBH = screenDist < this.bhRadius;
|
|
320
|
+
|
|
321
|
+
// Screen position = center + lensed offset
|
|
322
|
+
renderList.push({
|
|
323
|
+
x: cx + screenX,
|
|
324
|
+
y: cy + screenY,
|
|
325
|
+
z: projected.z,
|
|
326
|
+
size: p.size * projected.scale,
|
|
327
|
+
color: insideBH ? { r: 0, g: 0, b: 0 } : color,
|
|
328
|
+
alpha,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Sort back to front
|
|
333
|
+
renderList.sort((a, b) => b.z - a.z);
|
|
334
|
+
|
|
335
|
+
// Draw particles with reset transform (absolute screen coords)
|
|
336
|
+
Painter.useCtx((ctx) => {
|
|
337
|
+
// Reset to identity matrix - Scene3D has applied transforms we need to bypass
|
|
338
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
339
|
+
|
|
340
|
+
ctx.globalCompositeOperation = "lighter";
|
|
341
|
+
|
|
342
|
+
for (const item of renderList) {
|
|
343
|
+
const r = Math.round(item.color.r);
|
|
344
|
+
const g = Math.round(item.color.g);
|
|
345
|
+
const b = Math.round(item.color.b);
|
|
346
|
+
|
|
347
|
+
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${item.alpha})`;
|
|
348
|
+
ctx.beginPath();
|
|
349
|
+
ctx.arc(item.x, item.y, item.size, 0, Math.PI * 2);
|
|
350
|
+
ctx.fill();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
ctx.globalCompositeOperation = "source-over";
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|