@guinetik/gcanvas 1.0.0 → 1.0.1
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/demos/fluid-simple.html +22 -0
- package/demos/fluid.html +37 -0
- package/demos/index.html +2 -0
- package/demos/js/blob.js +18 -5
- package/demos/js/fluid-simple.js +253 -0
- package/demos/js/fluid.js +527 -0
- package/demos/js/tde/accretiondisk.js +64 -11
- package/demos/js/tde/blackholescene.js +2 -2
- package/demos/js/tde/config.js +2 -2
- package/demos/js/tde/index.js +152 -27
- package/demos/js/tde/lensedstarfield.js +32 -25
- package/demos/js/tde/tdestar.js +78 -98
- package/demos/js/tde/tidalstream.js +23 -7
- package/docs/README.md +230 -222
- package/docs/api/FluidSystem.md +173 -0
- package/docs/concepts/architecture-overview.md +204 -204
- package/docs/concepts/rendering-pipeline.md +279 -279
- package/docs/concepts/two-layer-architecture.md +229 -229
- package/docs/fluid-dynamics.md +97 -0
- package/docs/getting-started/first-game.md +354 -354
- package/docs/getting-started/installation.md +175 -157
- package/docs/modules/collision/README.md +2 -2
- package/docs/modules/fluent/README.md +6 -6
- package/docs/modules/game/README.md +303 -303
- package/docs/modules/isometric-camera.md +2 -2
- package/docs/modules/isometric.md +1 -1
- package/docs/modules/painter/README.md +328 -328
- package/docs/modules/particle/README.md +3 -3
- package/docs/modules/shapes/README.md +221 -221
- package/docs/modules/shapes/base/euclidian.md +123 -123
- package/docs/modules/shapes/base/shape.md +262 -262
- package/docs/modules/shapes/base/transformable.md +243 -243
- package/docs/modules/state/README.md +2 -2
- package/docs/modules/util/README.md +1 -1
- package/docs/modules/util/camera3d.md +3 -3
- package/docs/modules/util/scene3d.md +1 -1
- package/package.json +3 -1
- package/readme.md +19 -5
- package/src/collision/collision.js +75 -0
- package/src/game/index.js +2 -1
- package/src/game/pipeline.js +3 -3
- package/src/game/systems/FluidSystem.js +835 -0
- package/src/game/systems/index.js +11 -0
- package/src/game/ui/button.js +39 -18
- package/src/game/ui/cursor.js +14 -0
- package/src/game/ui/fps.js +12 -4
- package/src/game/ui/index.js +2 -0
- package/src/game/ui/stepper.js +549 -0
- package/src/game/ui/theme.js +121 -0
- package/src/game/ui/togglebutton.js +9 -3
- package/src/game/ui/tooltip.js +11 -4
- package/src/math/fluid.js +507 -0
- package/src/math/index.js +2 -0
- package/src/mixins/anchor.js +17 -7
- package/src/motion/tweenetik.js +16 -0
- package/src/shapes/index.js +1 -0
- package/src/util/camera3d.js +218 -12
- package/types/fluent.d.ts +361 -0
- package/types/game.d.ts +303 -0
- package/types/index.d.ts +144 -5
- package/types/math.d.ts +361 -0
- package/types/motion.d.ts +271 -0
- package/types/particle.d.ts +373 -0
- package/types/shapes.d.ts +107 -9
- package/types/util.d.ts +353 -0
- package/types/webgl.d.ts +109 -0
- package/disk_example.png +0 -0
- package/tde.png +0 -0
package/demos/js/tde/index.js
CHANGED
|
@@ -34,9 +34,9 @@ export class TDEDemo extends Game {
|
|
|
34
34
|
this.updateScaledSizes();
|
|
35
35
|
|
|
36
36
|
this.camera = new Camera3D({
|
|
37
|
-
rotationX: 0.
|
|
37
|
+
rotationX: 0.2,
|
|
38
38
|
rotationY: 0,
|
|
39
|
-
rotationZ: 0,
|
|
39
|
+
rotationZ: 0.0,
|
|
40
40
|
perspective: this.baseScale * 1.8, // Zoomed out for wider view
|
|
41
41
|
autoRotate: true,
|
|
42
42
|
autoRotateSpeed: 0.08,
|
|
@@ -81,13 +81,37 @@ export class TDEDemo extends Game {
|
|
|
81
81
|
});
|
|
82
82
|
applyAnchor(this.infoLabel, {
|
|
83
83
|
anchor: Position.BOTTOM_LEFT,
|
|
84
|
-
anchorOffsetX: -
|
|
85
|
-
anchorMargin: 20,
|
|
84
|
+
anchorOffsetX: -70,
|
|
86
85
|
});
|
|
87
86
|
this.infoLabel.zIndex = 100;
|
|
88
87
|
this.pipeline.add(this.infoLabel);
|
|
89
88
|
|
|
90
|
-
//
|
|
89
|
+
// Star View toggle button (above phase text)
|
|
90
|
+
this.starViewButton = new Button(this, {
|
|
91
|
+
width: 120,
|
|
92
|
+
height: 32,
|
|
93
|
+
text: "★ Star View",
|
|
94
|
+
font: "14px monospace",
|
|
95
|
+
colorDefaultBg: "rgba(0, 0, 0, 0.6)",
|
|
96
|
+
colorDefaultStroke: "#666",
|
|
97
|
+
colorDefaultText: "#aaa",
|
|
98
|
+
colorHoverBg: "rgba(30, 30, 30, 0.8)",
|
|
99
|
+
colorHoverStroke: "#44aaff",
|
|
100
|
+
colorHoverText: "#44aaff",
|
|
101
|
+
colorPressedBg: "rgba(50, 50, 50, 0.9)",
|
|
102
|
+
colorPressedStroke: "#66ccff",
|
|
103
|
+
colorPressedText: "#66ccff",
|
|
104
|
+
onClick: () => this.toggleStarView(),
|
|
105
|
+
});
|
|
106
|
+
applyAnchor(this.starViewButton, {
|
|
107
|
+
anchor: Position.BOTTOM_LEFT,
|
|
108
|
+
anchorMargin: 20,
|
|
109
|
+
anchorOffsetY: -30, // Above the phase text
|
|
110
|
+
});
|
|
111
|
+
this.starViewButton.zIndex = 100;
|
|
112
|
+
this.pipeline.add(this.starViewButton);
|
|
113
|
+
|
|
114
|
+
// Replay button (above star view button)
|
|
91
115
|
this.replayButton = new Button(this, {
|
|
92
116
|
width: 120,
|
|
93
117
|
height: 32,
|
|
@@ -107,7 +131,7 @@ export class TDEDemo extends Game {
|
|
|
107
131
|
applyAnchor(this.replayButton, {
|
|
108
132
|
anchor: Position.BOTTOM_LEFT,
|
|
109
133
|
anchorMargin: 20,
|
|
110
|
-
anchorOffsetY: -
|
|
134
|
+
anchorOffsetY: -70, // Above the star view button
|
|
111
135
|
});
|
|
112
136
|
this.replayButton.zIndex = 100;
|
|
113
137
|
this.pipeline.add(this.replayButton);
|
|
@@ -115,15 +139,22 @@ export class TDEDemo extends Game {
|
|
|
115
139
|
// FPS Counter (bottom right)
|
|
116
140
|
this.fpsCounter = new FPSCounter(this, {
|
|
117
141
|
font: "12px monospace",
|
|
118
|
-
color: "#666",
|
|
119
|
-
});
|
|
120
|
-
applyAnchor(this.fpsCounter, {
|
|
121
142
|
anchor: Position.BOTTOM_RIGHT,
|
|
122
|
-
anchorMargin:
|
|
143
|
+
anchorMargin: 0,
|
|
144
|
+
color: "#00FF00",
|
|
123
145
|
});
|
|
124
146
|
this.fpsCounter.zIndex = 100;
|
|
125
147
|
this.pipeline.add(this.fpsCounter);
|
|
126
148
|
|
|
149
|
+
// Camera Rotation Text (bottom right)
|
|
150
|
+
this.cameraRotationText = new Text(this, `Camera Rotation: X:${this.camera.rotationX.toFixed(2)} Y:${this.camera.rotationY.toFixed(2)} Z:${this.camera.rotationZ.toFixed(2)}`, {
|
|
151
|
+
font: "12px monospace",
|
|
152
|
+
anchor: Position.BOTTOM_CENTER,
|
|
153
|
+
color: "#ACACAC",
|
|
154
|
+
});
|
|
155
|
+
this.cameraRotationText.zIndex = 100;
|
|
156
|
+
this.pipeline.add(this.cameraRotationText);
|
|
157
|
+
|
|
127
158
|
this.initStateMachine();
|
|
128
159
|
|
|
129
160
|
// Initialize state properly (same as restart)
|
|
@@ -150,25 +181,26 @@ export class TDEDemo extends Game {
|
|
|
150
181
|
if (star) {
|
|
151
182
|
// === VIOLENT BRIGHTNESS FLARE ===
|
|
152
183
|
star.tidalFlare = 2.0;
|
|
153
|
-
// Fade
|
|
154
|
-
Tweenetik.to(star, { tidalFlare: 0 },
|
|
184
|
+
// Fade slowly - easeInQuad keeps it bright longer before dropping
|
|
185
|
+
Tweenetik.to(star, { tidalFlare: 0 }, 8.0, Easing.easeInQuad);
|
|
155
186
|
|
|
156
187
|
// === GEOMETRY WOBBLE - comet-like trauma ===
|
|
157
188
|
// Spike the wobble high - violent shaking
|
|
158
189
|
star.tidalWobble = 1.2;
|
|
159
|
-
// Fade back to stable over
|
|
160
|
-
Tweenetik.to(star, { tidalWobble: 0.1 },
|
|
190
|
+
// Fade back to stable over 8 seconds (star tries to recover)
|
|
191
|
+
Tweenetik.to(star, { tidalWobble: 0.1 }, 8.0, Easing.easeOutElastic);
|
|
161
192
|
|
|
162
193
|
// === SUDDEN STRETCH SPIKE - comet shape ===
|
|
163
194
|
// Force immediate elongation like panels 2-3 in reference
|
|
164
195
|
star.tidalStretch = 0.8;
|
|
165
196
|
// Ease back toward spherical (but not fully - it can't recover)
|
|
166
|
-
Tweenetik.to(star, { tidalStretch: 0.
|
|
197
|
+
Tweenetik.to(star, { tidalStretch: 0.3 }, 6.0, Easing.easeInQuad);
|
|
167
198
|
|
|
168
|
-
// === STRESS SPIKE ===
|
|
169
|
-
star.stressLevel = 0.
|
|
170
|
-
//
|
|
171
|
-
|
|
199
|
+
// === STRESS SPIKE (controls white color) ===
|
|
200
|
+
star.stressLevel = 0.8;
|
|
201
|
+
// Stay stressed longer - easeInQuad keeps it white before fading
|
|
202
|
+
// End at 0.4 (still visibly stressed, not back to original)
|
|
203
|
+
Tweenetik.to(star, { stressLevel: 0.4 }, 10.0, Easing.easeInQuad);
|
|
172
204
|
|
|
173
205
|
// === PARTICLE BURST ===
|
|
174
206
|
if (this.scene.stream) {
|
|
@@ -219,6 +251,41 @@ export class TDEDemo extends Game {
|
|
|
219
251
|
});
|
|
220
252
|
}
|
|
221
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Toggle between star-following camera view and default view
|
|
256
|
+
*/
|
|
257
|
+
toggleStarView() {
|
|
258
|
+
const star = this.scene.star;
|
|
259
|
+
|
|
260
|
+
if (this.camera.isFollowing()) {
|
|
261
|
+
// Switch back to default view
|
|
262
|
+
this.camera.unfollow(true); // true = animate back to initial position
|
|
263
|
+
this.starViewButton.text = "★ Star View";
|
|
264
|
+
|
|
265
|
+
// Re-enable auto-rotate
|
|
266
|
+
this.camera.autoRotate = true;
|
|
267
|
+
} else {
|
|
268
|
+
// Follow the star, looking at the black hole
|
|
269
|
+
// Position camera just above the star's surface (north pole perspective)
|
|
270
|
+
// Very close offset = immersive "standing on the star" feel
|
|
271
|
+
const offsetHeight = star.currentRadius * 0.3; // Just above surface
|
|
272
|
+
|
|
273
|
+
this.camera.follow(star, {
|
|
274
|
+
offsetX: 0,
|
|
275
|
+
offsetY: offsetHeight, // Barely above the star's "north pole"
|
|
276
|
+
offsetZ: 0,
|
|
277
|
+
lookAt: true,
|
|
278
|
+
lookAtTarget: this.scene.bh, // Look at black hole
|
|
279
|
+
lerp: 0.12, // Slightly snappier follow for immersion
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
this.starViewButton.text = "◉ BH View";
|
|
283
|
+
|
|
284
|
+
// Disable auto-rotate while following
|
|
285
|
+
this.camera.autoRotate = false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
222
289
|
restart() {
|
|
223
290
|
// Reset masses
|
|
224
291
|
this.scene.bh.mass = CONFIG.blackHole.initialMass;
|
|
@@ -278,6 +345,21 @@ export class TDEDemo extends Game {
|
|
|
278
345
|
// Reset velocity tracking to avoid spike after position reset
|
|
279
346
|
this.scene.star.resetVelocity();
|
|
280
347
|
|
|
348
|
+
// Kill any lingering tweens on the star (from previous run's stretch phase)
|
|
349
|
+
Tweenetik.killTarget(this.scene.star);
|
|
350
|
+
|
|
351
|
+
// Reset star's tidal state (color, stress, stretch, etc.)
|
|
352
|
+
this.scene.star.tidalStretch = 0;
|
|
353
|
+
this.scene.star.pulsationPhase = 0;
|
|
354
|
+
this.scene.star.stressLevel = 0;
|
|
355
|
+
this.scene.star.tidalProgress = 0;
|
|
356
|
+
this.scene.star.tidalFlare = 0;
|
|
357
|
+
this.scene.star.tidalWobble = 0;
|
|
358
|
+
this.scene.star.angularVelocity = CONFIG.star.rotationSpeed ?? 0.5;
|
|
359
|
+
this.scene.star.rotation = 0;
|
|
360
|
+
this.scene.star.currentColor = null; // Force recalc
|
|
361
|
+
this.scene.star.updateVisual(); // Apply reset immediately
|
|
362
|
+
|
|
281
363
|
// Clear tidal stream particles
|
|
282
364
|
if (this.scene.stream) {
|
|
283
365
|
this.scene.stream.clear();
|
|
@@ -310,6 +392,14 @@ export class TDEDemo extends Game {
|
|
|
310
392
|
this.starField.lensingStrength = 0.15;
|
|
311
393
|
}
|
|
312
394
|
|
|
395
|
+
// Reset camera to default view if following
|
|
396
|
+
if (this.camera.isFollowing()) {
|
|
397
|
+
this.camera.unfollow(false); // Don't animate, just reset
|
|
398
|
+
this.camera.reset();
|
|
399
|
+
if (this.starViewButton) this.starViewButton.text = "★ Star View";
|
|
400
|
+
this.camera.autoRotate = true;
|
|
401
|
+
}
|
|
402
|
+
|
|
313
403
|
// Hide replay button
|
|
314
404
|
if (this.replayButton) this.replayButton.visible = false;
|
|
315
405
|
|
|
@@ -381,6 +471,8 @@ export class TDEDemo extends Game {
|
|
|
381
471
|
if (!this.fsm) return;
|
|
382
472
|
this.fsm.update(dt);
|
|
383
473
|
|
|
474
|
+
this.cameraRotationText.text = `Camera Rotation: X:${this.camera.rotationX.toFixed(2)} Y:${this.camera.rotationY.toFixed(2)}`;
|
|
475
|
+
|
|
384
476
|
const state = this.fsm.state;
|
|
385
477
|
const progress = this.fsm.progress;
|
|
386
478
|
const star = this.scene.star;
|
|
@@ -586,15 +678,42 @@ export class TDEDemo extends Game {
|
|
|
586
678
|
|
|
587
679
|
const pos = polarToCartesian(star.orbitalRadius, star.phi);
|
|
588
680
|
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
681
|
+
// === ORBITAL PLANE TILT ===
|
|
682
|
+
// As chaos builds, the orbital plane itself starts wobbling
|
|
683
|
+
// This creates the effect of the orbit tilting dramatically
|
|
684
|
+
let tiltX = 0; // Tilt around X-axis (makes orbit bob front-to-back)
|
|
685
|
+
let tiltZ = 0; // Tilt around Z-axis (makes orbit bob left-to-right)
|
|
686
|
+
|
|
687
|
+
if (chaos > 0.005) {
|
|
688
|
+
// Max tilt in radians (~30 degrees = 0.52 rad) - very dramatic!
|
|
689
|
+
const maxTilt = 0.52 * (chaos / 0.6); // Scale with chaos (0.6 is max chaos)
|
|
690
|
+
|
|
691
|
+
// Multiple frequencies for organic, unstable feel
|
|
692
|
+
// Slower base frequencies so the tilt is visible, faster harmonics for jitter
|
|
693
|
+
tiltX = Math.sin(time * 0.8) * maxTilt
|
|
694
|
+
+ Math.sin(time * 2.1) * maxTilt * 0.5
|
|
695
|
+
+ Math.sin(time * 4.5) * maxTilt * 0.15; // High freq jitter
|
|
696
|
+
tiltZ = Math.cos(time * 0.6) * maxTilt * 0.8
|
|
697
|
+
+ Math.sin(time * 1.9) * maxTilt * 0.4
|
|
698
|
+
+ Math.cos(time * 5.2) * maxTilt * 0.1; // High freq jitter
|
|
699
|
+
}
|
|
594
700
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
701
|
+
// Apply orbital plane tilt rotation
|
|
702
|
+
// Rotate around X-axis: affects Y and Z
|
|
703
|
+
const cosX = Math.cos(tiltX);
|
|
704
|
+
const sinX = Math.sin(tiltX);
|
|
705
|
+
let newY = -pos.z * sinX;
|
|
706
|
+
let newZ = pos.z * cosX;
|
|
707
|
+
|
|
708
|
+
// Rotate around Z-axis: affects X and Y
|
|
709
|
+
const cosZ = Math.cos(tiltZ);
|
|
710
|
+
const sinZ = Math.sin(tiltZ);
|
|
711
|
+
const finalX = pos.x * cosZ - newY * sinZ;
|
|
712
|
+
const finalY = pos.x * sinZ + newY * cosZ;
|
|
713
|
+
|
|
714
|
+
star.x = finalX;
|
|
715
|
+
star.y = finalY;
|
|
716
|
+
star.z = newZ;
|
|
598
717
|
|
|
599
718
|
// Emit particles throughout disrupt phase (more than stretch)
|
|
600
719
|
if (this.scene.stream && star.mass > 0) {
|
|
@@ -623,6 +742,12 @@ export class TDEDemo extends Game {
|
|
|
623
742
|
|
|
624
743
|
// Trigger accrete state when star mass is depleted
|
|
625
744
|
if (star.mass <= 0) {
|
|
745
|
+
// If camera was following the star, switch back to default view
|
|
746
|
+
if (this.camera.isFollowing()) {
|
|
747
|
+
this.camera.unfollow(true);
|
|
748
|
+
if (this.starViewButton) this.starViewButton.text = "★ Star View";
|
|
749
|
+
this.camera.autoRotate = true;
|
|
750
|
+
}
|
|
626
751
|
this.fsm.trigger("starConsumed");
|
|
627
752
|
}
|
|
628
753
|
}
|
|
@@ -24,7 +24,8 @@ const LENSING_CONFIG = {
|
|
|
24
24
|
minDistance: 5,
|
|
25
25
|
|
|
26
26
|
// Occlusion radius multiplier (stars within BH radius * this are hidden)
|
|
27
|
-
|
|
27
|
+
// The dark shadow region extends to ~2.6x the event horizon (photon sphere)
|
|
28
|
+
occlusionMultiplier: 2.6,
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
export class LensedStarfield extends StarField {
|
|
@@ -68,49 +69,55 @@ export class LensedStarfield extends StarField {
|
|
|
68
69
|
const lensingPower = LENSING_CONFIG.baseStrength * this.lensingStrength;
|
|
69
70
|
const effectRadius = LENSING_CONFIG.effectRadiusPixels;
|
|
70
71
|
|
|
72
|
+
// Project black hole position once for all stars
|
|
73
|
+
// This is crucial when camera has moved (e.g., following the star)
|
|
74
|
+
const bhProjected = this.camera.project(0, 0, 0);
|
|
75
|
+
const bhScreenX = bhProjected.x;
|
|
76
|
+
const bhScreenY = bhProjected.y;
|
|
77
|
+
|
|
71
78
|
Painter.useCtx((ctx) => {
|
|
72
79
|
ctx.globalCompositeOperation = "source-over";
|
|
73
80
|
|
|
74
81
|
for (const star of this.stars) {
|
|
75
|
-
// === CAMERA
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
let xCam = star.x * cosY - star.z * sinY;
|
|
79
|
-
let zCam = star.x * sinY + star.z * cosY;
|
|
80
|
-
|
|
81
|
-
const cosX = Math.cos(this.camera.rotationX);
|
|
82
|
-
const sinX = Math.sin(this.camera.rotationX);
|
|
83
|
-
let yCam = star.y * cosX - zCam * sinX;
|
|
84
|
-
zCam = star.y * sinX + zCam * cosX;
|
|
82
|
+
// === USE CAMERA.PROJECT FOR PROPER POSITION HANDLING ===
|
|
83
|
+
// This accounts for camera position (translation) not just rotation
|
|
84
|
+
const projected = this.camera.project(star.x, star.y, star.z);
|
|
85
85
|
|
|
86
86
|
// Skip stars behind camera
|
|
87
|
-
if (
|
|
87
|
+
if (projected.scale <= 0) continue;
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
let screenX = projected.x;
|
|
90
|
+
let screenY = projected.y;
|
|
91
|
+
const perspectiveScale = projected.scale;
|
|
92
|
+
|
|
93
|
+
// === GRAVITATIONAL LENSING (relative to BH screen position) ===
|
|
94
|
+
// Only apply to stars "behind" the black hole (positive z after projection)
|
|
95
|
+
if (lensingPower > 0 && projected.z > 0) {
|
|
96
|
+
// Calculate position relative to BH's screen position
|
|
97
|
+
const relX = screenX - bhScreenX;
|
|
98
|
+
const relY = screenY - bhScreenY;
|
|
93
99
|
|
|
94
|
-
// === GRAVITATIONAL LENSING (screen space) ===
|
|
95
|
-
// Only apply to stars "behind" the black hole (zCam > 0)
|
|
96
|
-
if (lensingPower > 0 && zCam > 0) {
|
|
97
100
|
const lensed = applyGravitationalLensing(
|
|
98
|
-
|
|
101
|
+
relX, relY,
|
|
99
102
|
effectRadius,
|
|
100
103
|
lensingPower,
|
|
101
104
|
LENSING_CONFIG.falloff,
|
|
102
105
|
LENSING_CONFIG.minDistance
|
|
103
106
|
);
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
|
|
108
|
+
// Transform back to screen space
|
|
109
|
+
screenX = lensed.x + bhScreenX;
|
|
110
|
+
screenY = lensed.y + bhScreenY;
|
|
106
111
|
}
|
|
107
112
|
|
|
108
|
-
// === OCCLUSION CHECK ===
|
|
113
|
+
// === OCCLUSION CHECK (relative to BH screen position) ===
|
|
109
114
|
// Hide stars that fall within the black hole's occlusion radius
|
|
110
115
|
if (this.blackHole) {
|
|
111
116
|
const bhScreenRadius = this.blackHole.currentRadius * LENSING_CONFIG.occlusionMultiplier;
|
|
112
|
-
const
|
|
113
|
-
|
|
117
|
+
const dxBH = screenX - bhScreenX;
|
|
118
|
+
const dyBH = screenY - bhScreenY;
|
|
119
|
+
const distFromBH = Math.sqrt(dxBH * dxBH + dyBH * dyBH);
|
|
120
|
+
if (distFromBH < bhScreenRadius) continue;
|
|
114
121
|
}
|
|
115
122
|
|
|
116
123
|
// Final screen position
|
package/demos/js/tde/tdestar.js
CHANGED
|
@@ -2,15 +2,12 @@ import { GameObject, Sphere3D } from "../../../src/index.js";
|
|
|
2
2
|
import { polarToCartesian } from "../../../src/math/gr.js";
|
|
3
3
|
import { CONFIG } from "./config.js";
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
uTemperature: 5500, // K (slightly cooler than Sun)
|
|
12
|
-
uActivityLevel: 0.75, // Moderate surface activity
|
|
13
|
-
},
|
|
5
|
+
// Performance tuning: reduce update frequency for expensive operations
|
|
6
|
+
const PERF_CONFIG = {
|
|
7
|
+
geometryUpdateThreshold: 0.02, // Only regenerate geometry if radius changes by 2%
|
|
8
|
+
uniformUpdateInterval: 2, // Update shader uniforms every N frames
|
|
9
|
+
breathingEnabled: true, // Toggle breathing effect
|
|
10
|
+
stressColorEnabled: true, // Toggle dynamic color shifts
|
|
14
11
|
};
|
|
15
12
|
|
|
16
13
|
export class Star extends GameObject {
|
|
@@ -48,6 +45,11 @@ export class Star extends GameObject {
|
|
|
48
45
|
this.tidalProgress = 0; // External tidal progress from FSM (0-1)
|
|
49
46
|
this.tidalFlare = 0; // 0-1, sudden brightness burst at disruption start
|
|
50
47
|
this.tidalWobble = 0; // 0-1, violent geometry wobble during trauma
|
|
48
|
+
|
|
49
|
+
// Performance optimization state
|
|
50
|
+
this._frameCount = 0;
|
|
51
|
+
this._lastGeometryRadius = 0;
|
|
52
|
+
this._cachedUniforms = null;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
init() {
|
|
@@ -93,109 +95,73 @@ export class Star extends GameObject {
|
|
|
93
95
|
const massRatio = this.mass / this.initialMass;
|
|
94
96
|
|
|
95
97
|
// === NON-LINEAR SIZE COLLAPSE ===
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
const resistanceCurve = Math.pow(collapseProgress, 0.5); // sqrt = resists early
|
|
100
|
-
const effectiveMassRatio = 1 - resistanceCurve;
|
|
98
|
+
// Use sqrt for resistance curve (star resists early, then collapses)
|
|
99
|
+
const collapseProgress = 1 - massRatio;
|
|
100
|
+
const effectiveMassRatio = 1 - Math.sqrt(collapseProgress);
|
|
101
101
|
|
|
102
102
|
// Base radius with non-linear collapse
|
|
103
103
|
this.currentRadius = this.baseRadius * Math.max(0.05, effectiveMassRatio);
|
|
104
104
|
|
|
105
|
-
// Don't update
|
|
105
|
+
// Don't update if star is consumed
|
|
106
106
|
if (this.currentRadius <= 0 || this.mass <= 0) {
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
// === TIDAL STRETCH (
|
|
111
|
-
|
|
112
|
-
const
|
|
110
|
+
// === TIDAL STRETCH (Simplified) ===
|
|
111
|
+
const zVal = this.z || 0;
|
|
112
|
+
const distSq = this.x * this.x + zVal * zVal;
|
|
113
|
+
const dist = Math.sqrt(distSq) || 1;
|
|
114
|
+
const invDist = 1 / dist;
|
|
113
115
|
|
|
114
116
|
// Direction toward black hole (unit vector)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
const dirX = -this.x * invDist;
|
|
118
|
+
const dirZ = -zVal * invDist;
|
|
117
119
|
|
|
118
120
|
// Proximity factor: closer to BH = more stretch
|
|
119
|
-
|
|
121
|
+
const proximityFactor = Math.max(0, 1 - dist / this.initialOrbitalRadius);
|
|
120
122
|
|
|
121
|
-
//
|
|
123
|
+
// Simplified stretch calculation
|
|
122
124
|
if (collapseProgress > 0.8) {
|
|
123
|
-
// Very late stage - reduce stretch as star becomes tiny
|
|
124
125
|
this.tidalStretch = (1 - collapseProgress) * 2;
|
|
125
126
|
} else {
|
|
126
|
-
|
|
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
|
|
127
|
+
this.tidalStretch = Math.min(1.8, this.tidalProgress * 1.2 + proximityFactor * 0.5);
|
|
133
128
|
}
|
|
134
129
|
|
|
135
|
-
// === BREATHING (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// Apply breathing to radius (very subtle)
|
|
142
|
-
this.currentRadius *= (1 + breathing);
|
|
130
|
+
// === BREATHING (Optional, can be disabled for performance) ===
|
|
131
|
+
if (PERF_CONFIG.breathingEnabled) {
|
|
132
|
+
const breathingAmp = 0.03 * (1 - collapseProgress * 0.5);
|
|
133
|
+
this.currentRadius *= (1 + Math.sin(this.pulsationPhase) * breathingAmp);
|
|
134
|
+
}
|
|
143
135
|
|
|
144
|
-
// === STRESS LEVEL ===
|
|
145
|
-
|
|
146
|
-
|
|
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));
|
|
136
|
+
// === STRESS LEVEL (Simplified power curve) ===
|
|
137
|
+
const rawStress = proximityFactor * 0.4 + collapseProgress * 0.6;
|
|
138
|
+
this.stressLevel = Math.min(1, rawStress * rawStress * rawStress); // Cubic approximation
|
|
151
139
|
|
|
152
140
|
// === ACTIVITY & ROTATION ===
|
|
153
|
-
const activityLevel = 0.3 + this.stressLevel * 0.7;
|
|
154
|
-
|
|
155
|
-
// Angular momentum conservation: shrinking = faster spin
|
|
141
|
+
const activityLevel = 0.3 + this.stressLevel * 0.7;
|
|
156
142
|
const baseRotationSpeed = CONFIG.star.rotationSpeed ?? 0.5;
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
143
|
+
const rotationSpeed = Math.min(10, baseRotationSpeed / Math.max(0.2, effectiveMassRatio));
|
|
144
|
+
|
|
145
|
+
// === COLOR SHIFT (Simplified linear interpolation) ===
|
|
146
|
+
let r = 1.0, g, b;
|
|
147
|
+
const stress = this.stressLevel;
|
|
148
|
+
|
|
149
|
+
if (PERF_CONFIG.stressColorEnabled) {
|
|
150
|
+
// Simplified color: lerp from red-orange to white based on stress
|
|
151
|
+
g = 0.35 + stress * 0.6; // 0.35 → 0.95
|
|
152
|
+
b = 0.15 + stress * 0.7; // 0.15 → 0.85
|
|
187
153
|
} else {
|
|
188
|
-
|
|
189
|
-
|
|
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)
|
|
154
|
+
g = 0.5;
|
|
155
|
+
b = 0.2;
|
|
192
156
|
}
|
|
193
157
|
|
|
194
158
|
const stressColor = [r, g, b];
|
|
195
|
-
|
|
196
|
-
// Expose current color for particle emission
|
|
197
159
|
this.currentColor = stressColor;
|
|
198
160
|
|
|
161
|
+
// Temperature calculation
|
|
162
|
+
const temperature = (CONFIG.star.temperature ?? 3800) + stress * stress * 2500;
|
|
163
|
+
|
|
164
|
+
// === VISUAL UPDATE ===
|
|
199
165
|
if (!this.visual) {
|
|
200
166
|
this.visual = new Sphere3D(this.currentRadius, {
|
|
201
167
|
color: CONFIG.star.color,
|
|
@@ -215,23 +181,37 @@ export class Star extends GameObject {
|
|
|
215
181
|
uTidalWobble: this.tidalWobble,
|
|
216
182
|
},
|
|
217
183
|
});
|
|
184
|
+
this._lastGeometryRadius = this.currentRadius;
|
|
218
185
|
} else {
|
|
219
186
|
this.visual.radius = this.currentRadius;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
187
|
+
|
|
188
|
+
// Only update shader uniforms every N frames
|
|
189
|
+
this._frameCount++;
|
|
190
|
+
if (this._frameCount >= PERF_CONFIG.uniformUpdateInterval) {
|
|
191
|
+
this._frameCount = 0;
|
|
192
|
+
|
|
193
|
+
if (this.visual.useShader) {
|
|
194
|
+
this.visual.setShaderUniforms({
|
|
195
|
+
uStarColor: stressColor,
|
|
196
|
+
uTemperature: temperature,
|
|
197
|
+
uActivityLevel: activityLevel,
|
|
198
|
+
uRotationSpeed: rotationSpeed,
|
|
199
|
+
uTidalStretch: this.tidalStretch,
|
|
200
|
+
uStretchDirX: dirX,
|
|
201
|
+
uStretchDirZ: dirZ,
|
|
202
|
+
uStressLevel: this.stressLevel,
|
|
203
|
+
uTidalFlare: this.tidalFlare,
|
|
204
|
+
uTidalWobble: this.tidalWobble,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Only regenerate geometry if radius changed significantly
|
|
210
|
+
const radiusChange = Math.abs(this.currentRadius - this._lastGeometryRadius) / this._lastGeometryRadius;
|
|
211
|
+
if (radiusChange > PERF_CONFIG.geometryUpdateThreshold) {
|
|
212
|
+
this.visual._generateGeometry();
|
|
213
|
+
this._lastGeometryRadius = this.currentRadius;
|
|
233
214
|
}
|
|
234
|
-
this.visual._generateGeometry();
|
|
235
215
|
}
|
|
236
216
|
}
|
|
237
217
|
|