@guinetik/gcanvas 1.0.0 → 1.0.2
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/coordinates.html +698 -0
- package/demos/cube3d.html +23 -0
- package/demos/demos.css +17 -3
- package/demos/dino.html +42 -0
- package/demos/fluid-simple.html +22 -0
- package/demos/fluid.html +37 -0
- package/demos/gameobjects.html +626 -0
- package/demos/index.html +19 -7
- package/demos/js/blob.js +18 -5
- package/demos/js/coordinates.js +840 -0
- package/demos/js/cube3d.js +789 -0
- package/demos/js/dino.js +1420 -0
- package/demos/js/fluid-simple.js +253 -0
- package/demos/js/fluid.js +527 -0
- package/demos/js/gameobjects.js +176 -0
- package/demos/js/plane3d.js +256 -0
- package/demos/js/platformer.js +1579 -0
- package/demos/js/sphere3d.js +229 -0
- package/demos/js/sprite.js +473 -0
- package/demos/js/tde/accretiondisk.js +65 -12
- 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 +24 -8
- package/demos/plane3d.html +24 -0
- package/demos/platformer.html +43 -0
- package/demos/sphere3d.html +24 -0
- package/demos/sprite.html +18 -0
- package/docs/README.md +230 -222
- package/docs/api/FluidSystem.md +173 -0
- package/docs/concepts/architecture-overview.md +204 -204
- package/docs/concepts/coordinate-system.md +384 -0
- package/docs/concepts/rendering-pipeline.md +279 -279
- package/docs/concepts/shapes-vs-gameobjects.md +187 -0
- package/docs/concepts/two-layer-architecture.md +229 -229
- package/docs/fluid-dynamics.md +99 -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/game.js +11 -5
- package/src/game/index.js +2 -1
- package/src/game/objects/index.js +3 -0
- package/src/game/objects/platformer-scene.js +411 -0
- package/src/game/objects/scene.js +14 -0
- package/src/game/objects/sprite.js +529 -0
- package/src/game/pipeline.js +20 -16
- 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 +123 -0
- package/src/game/ui/togglebutton.js +9 -3
- package/src/game/ui/tooltip.js +11 -4
- package/src/io/input.js +75 -45
- package/src/io/mouse.js +44 -19
- package/src/io/touch.js +35 -12
- 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/cube3d.js +599 -0
- package/src/shapes/index.js +3 -0
- package/src/shapes/plane3d.js +687 -0
- package/src/shapes/sphere3d.js +75 -6
- package/src/util/camera2d.js +315 -0
- package/src/util/camera3d.js +218 -12
- package/src/util/index.js +1 -0
- package/src/webgl/shaders/plane-shaders.js +332 -0
- package/src/webgl/shaders/sphere-shaders.js +4 -2
- 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
|
@@ -17,9 +17,9 @@ const DISK_CONFIG = {
|
|
|
17
17
|
outerRadiusMultiplier: 9.0, // Wide disk with margin from screen edges
|
|
18
18
|
|
|
19
19
|
// Particle properties
|
|
20
|
-
maxParticles:
|
|
21
|
-
particleLifetime:
|
|
22
|
-
spawnRate:
|
|
20
|
+
maxParticles: 10000,
|
|
21
|
+
particleLifetime: 1000,
|
|
22
|
+
spawnRate: 500,
|
|
23
23
|
|
|
24
24
|
// Orbital physics
|
|
25
25
|
baseOrbitalSpeed: 0.8,
|
|
@@ -40,8 +40,8 @@ const DISK_CONFIG = {
|
|
|
40
40
|
colorMid: { r: 255, g: 160, b: 50 }, // Mid (orange)
|
|
41
41
|
colorCool: { r: 180, g: 40, b: 40 }, // Outer (deep red)
|
|
42
42
|
|
|
43
|
-
sizeMin:
|
|
44
|
-
sizeMax: 2
|
|
43
|
+
sizeMin: .8,
|
|
44
|
+
sizeMax: 1.2,
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
export class AccretionDisk extends GameObject {
|
|
@@ -278,20 +278,73 @@ export class AccretionDisk extends GameObject {
|
|
|
278
278
|
let yCam = y * cosX - zCam * sinX;
|
|
279
279
|
zCam = y * sinX + zCam * cosX;
|
|
280
280
|
|
|
281
|
-
// === GRAVITATIONAL LENSING
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
// === GRAVITATIONAL LENSING ===
|
|
282
|
+
// Creates the Interstellar effect: disk curves around BH
|
|
283
|
+
|
|
284
|
+
// Camera tilt: 0 when edge-on, 1 when top-down
|
|
285
|
+
const cameraTilt = Math.abs(Math.sin(this.camera.rotationX));
|
|
286
|
+
const isBehind = zCam > 0;
|
|
287
|
+
const currentR = Math.sqrt(xCam * xCam + yCam * yCam);
|
|
288
|
+
|
|
289
|
+
if (lensingStrength > 0 && currentR < this.bhRadius * 6) {
|
|
285
290
|
const ringRadius = this.bhRadius * DISK_CONFIG.ringRadiusFactor;
|
|
286
291
|
const lensFactor = Math.exp(-currentR / (this.bhRadius * DISK_CONFIG.lensingFalloff));
|
|
287
292
|
const warp = lensFactor * 1.2 * lensingStrength;
|
|
288
293
|
|
|
294
|
+
// Determine upper/lower half for asymmetric effects
|
|
295
|
+
const angleRelativeToCamera = p.angle + this.camera.rotationY;
|
|
296
|
+
const isUpperHalf = Math.sin(angleRelativeToCamera) > 0;
|
|
297
|
+
|
|
298
|
+
// === RADIAL PUSH: Curves particles around BH silhouette ===
|
|
299
|
+
// Bottom ring should have TIGHTER radius (less expansion) at edge-on views
|
|
300
|
+
// But stay symmetric at top-down views
|
|
289
301
|
if (currentR > 0) {
|
|
290
|
-
|
|
302
|
+
let radialWarp = warp;
|
|
303
|
+
|
|
304
|
+
// Edge-on factor: 1 at edge-on, 0 at top-down
|
|
305
|
+
const edgeOnFactor = 1 - cameraTilt;
|
|
306
|
+
|
|
307
|
+
// Reduce radial expansion for bottom half, but only at edge-on angles
|
|
308
|
+
// This creates the tighter bottom ring radius seen in Interstellar
|
|
309
|
+
if (!isUpperHalf && isBehind) {
|
|
310
|
+
// At edge-on: bottom gets 40% of radial push (tight ring)
|
|
311
|
+
// At top-down: bottom gets 100% (symmetric circle)
|
|
312
|
+
radialWarp *= 1.0 - edgeOnFactor * 0.6;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const ratio = (currentR + ringRadius * radialWarp) / currentR;
|
|
291
316
|
xCam *= ratio;
|
|
292
317
|
yCam *= ratio;
|
|
293
|
-
}
|
|
294
|
-
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// === VERTICAL CURVES: Only when camera is tilted ===
|
|
321
|
+
if (cameraTilt > 0.05) {
|
|
322
|
+
// Arc shape - smooth curve
|
|
323
|
+
const arcWidth = this.bhRadius * 5.0;
|
|
324
|
+
const normalizedX = xCam / arcWidth;
|
|
325
|
+
const arcCurve = Math.max(0, Math.cos(normalizedX * Math.PI * 0.5));
|
|
326
|
+
|
|
327
|
+
// Depth factor - different for front vs back
|
|
328
|
+
const depthFactor = isBehind
|
|
329
|
+
? Math.min(1.0, zCam / (this.bhRadius * 3))
|
|
330
|
+
: Math.min(1.0, Math.abs(zCam) / (this.bhRadius * 3));
|
|
331
|
+
|
|
332
|
+
// Ring height - scales with tilt
|
|
333
|
+
const ringHeight = this.bhRadius * 2.0 * lensFactor * depthFactor * cameraTilt * lensingStrength;
|
|
334
|
+
|
|
335
|
+
// Apply vertical displacement
|
|
336
|
+
if (isBehind) {
|
|
337
|
+
// Back particles: upper half UP, lower half DOWN
|
|
338
|
+
if (isUpperHalf) {
|
|
339
|
+
yCam -= ringHeight * arcCurve;
|
|
340
|
+
} else {
|
|
341
|
+
// Bottom ring: less vertical displacement too
|
|
342
|
+
yCam += ringHeight * arcCurve * 0.5;
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
// Front particles: curve DOWN slightly
|
|
346
|
+
yCam += ringHeight * arcCurve * 0.4;
|
|
347
|
+
}
|
|
295
348
|
}
|
|
296
349
|
}
|
|
297
350
|
|
|
@@ -44,10 +44,10 @@ export class BlackHoleScene extends Scene3D {
|
|
|
44
44
|
this.Z = {
|
|
45
45
|
starBack: 10, // Star when behind BH
|
|
46
46
|
blackHole: 15, // BlackHole: dark shadow at back
|
|
47
|
-
disk:
|
|
47
|
+
disk: 1, // AccretionDisk: over the black hole
|
|
48
48
|
starFront: 25, // Star when in front of BH
|
|
49
49
|
stream: 30, // TidalStream: always on top of star and BH
|
|
50
|
-
jets:
|
|
50
|
+
jets: 1, // Jets: always on top
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
|
package/demos/js/tde/config.js
CHANGED
|
@@ -6,7 +6,7 @@ export const CONFIG = {
|
|
|
6
6
|
|
|
7
7
|
// Phase durations (seconds)
|
|
8
8
|
durations: {
|
|
9
|
-
approach:
|
|
9
|
+
approach: 12.0, // Stable wide orbit
|
|
10
10
|
stretch: 10.0, // Orbit begins to decay
|
|
11
11
|
disrupt: 20.0, // Mass transfer (event-based exit)
|
|
12
12
|
accrete: 1.0, // Debris accretion
|
|
@@ -45,7 +45,7 @@ export const CONFIG = {
|
|
|
45
45
|
startAngle: Math.PI * 1.85, // Start lower-right, comes FROM right, swings up and around
|
|
46
46
|
},
|
|
47
47
|
sceneOptions: {
|
|
48
|
-
starCount:
|
|
48
|
+
starCount: 5000,
|
|
49
49
|
},
|
|
50
50
|
|
|
51
51
|
// Accretion disk settings
|
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
|