@guinetik/gcanvas 1.0.4 → 1.0.5
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/dist/CNAME +1 -0
- package/dist/animations.html +31 -0
- package/dist/basic.html +38 -0
- package/dist/baskara.html +31 -0
- package/dist/bezier.html +35 -0
- package/dist/beziersignature.html +29 -0
- package/dist/blackhole.html +28 -0
- package/dist/blob.html +35 -0
- package/dist/coordinates.html +698 -0
- package/dist/cube3d.html +23 -0
- package/dist/demos.css +303 -0
- package/dist/dino.html +42 -0
- package/dist/easing.html +28 -0
- package/dist/events.html +195 -0
- package/dist/fluent.html +647 -0
- package/dist/fluid-simple.html +22 -0
- package/dist/fluid.html +37 -0
- package/dist/fractals.html +36 -0
- package/dist/gameobjects.html +626 -0
- package/dist/gcanvas.es.js +517 -0
- package/dist/gcanvas.es.min.js +1 -1
- package/dist/gcanvas.umd.js +1 -1
- package/dist/gcanvas.umd.min.js +1 -1
- package/dist/genart.html +26 -0
- package/dist/gendream.html +26 -0
- package/dist/group.html +36 -0
- package/dist/home.html +587 -0
- package/dist/hyperbolic001.html +23 -0
- package/dist/hyperbolic002.html +23 -0
- package/dist/hyperbolic003.html +23 -0
- package/dist/hyperbolic004.html +23 -0
- package/dist/hyperbolic005.html +22 -0
- package/dist/index.html +398 -0
- package/dist/isometric.html +34 -0
- package/dist/js/animations.js +452 -0
- package/dist/js/basic.js +204 -0
- package/dist/js/baskara.js +751 -0
- package/dist/js/bezier.js +692 -0
- package/dist/js/beziersignature.js +241 -0
- package/dist/js/blackhole/accretiondisk.obj.js +379 -0
- package/dist/js/blackhole/blackhole.obj.js +318 -0
- package/dist/js/blackhole/index.js +409 -0
- package/dist/js/blackhole/particle.js +56 -0
- package/dist/js/blackhole/starfield.obj.js +218 -0
- package/dist/js/blob.js +2276 -0
- package/dist/js/coordinates.js +840 -0
- package/dist/js/cube3d.js +789 -0
- package/dist/js/dino.js +1420 -0
- package/dist/js/easing.js +477 -0
- package/dist/js/fluent.js +183 -0
- package/dist/js/fluid-simple.js +253 -0
- package/dist/js/fluid.js +527 -0
- package/dist/js/fractals.js +932 -0
- package/dist/js/fractalworker.js +93 -0
- package/dist/js/gameobjects.js +176 -0
- package/dist/js/genart.js +268 -0
- package/dist/js/gendream.js +209 -0
- package/dist/js/group.js +140 -0
- package/dist/js/hyperbolic001.js +310 -0
- package/dist/js/hyperbolic002.js +388 -0
- package/dist/js/hyperbolic003.js +319 -0
- package/dist/js/hyperbolic004.js +345 -0
- package/dist/js/hyperbolic005.js +340 -0
- package/dist/js/info-toggle.js +25 -0
- package/dist/js/isometric.js +863 -0
- package/dist/js/kerr.js +1547 -0
- package/dist/js/lavalamp.js +590 -0
- package/dist/js/layout.js +354 -0
- package/dist/js/mondrian.js +285 -0
- package/dist/js/opacity.js +275 -0
- package/dist/js/painter.js +484 -0
- package/dist/js/particles-showcase.js +514 -0
- package/dist/js/particles.js +299 -0
- package/dist/js/patterns.js +397 -0
- package/dist/js/penrose/artifact.js +69 -0
- package/dist/js/penrose/blackhole.js +121 -0
- package/dist/js/penrose/constants.js +73 -0
- package/dist/js/penrose/game.js +943 -0
- package/dist/js/penrose/lore.js +278 -0
- package/dist/js/penrose/penrosescene.js +892 -0
- package/dist/js/penrose/ship.js +216 -0
- package/dist/js/penrose/sounds.js +211 -0
- package/dist/js/penrose/voidparticle.js +55 -0
- package/dist/js/penrose/voidscene.js +258 -0
- package/dist/js/penrose/voidship.js +144 -0
- package/dist/js/penrose/wormhole.js +46 -0
- package/dist/js/pipeline.js +555 -0
- package/dist/js/plane3d.js +256 -0
- package/dist/js/platformer.js +1579 -0
- package/dist/js/scene.js +304 -0
- package/dist/js/scenes.js +320 -0
- package/dist/js/schrodinger.js +410 -0
- package/dist/js/schwarzschild.js +1015 -0
- package/dist/js/shapes.js +628 -0
- package/dist/js/space/alien.js +171 -0
- package/dist/js/space/boom.js +98 -0
- package/dist/js/space/boss.js +353 -0
- package/dist/js/space/buff.js +73 -0
- package/dist/js/space/bullet.js +102 -0
- package/dist/js/space/constants.js +85 -0
- package/dist/js/space/game.js +1884 -0
- package/dist/js/space/hud.js +112 -0
- package/dist/js/space/laserbeam.js +179 -0
- package/dist/js/space/lightning.js +277 -0
- package/dist/js/space/minion.js +192 -0
- package/dist/js/space/missile.js +212 -0
- package/dist/js/space/player.js +430 -0
- package/dist/js/space/powerup.js +90 -0
- package/dist/js/space/starfield.js +58 -0
- package/dist/js/space/starpower.js +90 -0
- package/dist/js/spacetime.js +559 -0
- package/dist/js/sphere3d.js +229 -0
- package/dist/js/sprite.js +473 -0
- package/dist/js/starfaux/config.js +118 -0
- package/dist/js/starfaux/enemy.js +353 -0
- package/dist/js/starfaux/hud.js +78 -0
- package/dist/js/starfaux/index.js +482 -0
- package/dist/js/starfaux/laser.js +182 -0
- package/dist/js/starfaux/player.js +468 -0
- package/dist/js/starfaux/terrain.js +560 -0
- package/dist/js/study001.js +275 -0
- package/dist/js/study002.js +366 -0
- package/dist/js/study003.js +331 -0
- package/dist/js/study004.js +389 -0
- package/dist/js/study005.js +209 -0
- package/dist/js/study006.js +194 -0
- package/dist/js/study007.js +192 -0
- package/dist/js/study008.js +413 -0
- package/dist/js/svgtween.js +204 -0
- package/dist/js/tde/accretiondisk.js +471 -0
- package/dist/js/tde/blackhole.js +219 -0
- package/dist/js/tde/blackholescene.js +209 -0
- package/dist/js/tde/config.js +59 -0
- package/dist/js/tde/index.js +820 -0
- package/dist/js/tde/jets.js +290 -0
- package/dist/js/tde/lensedstarfield.js +154 -0
- package/dist/js/tde/tdestar.js +297 -0
- package/dist/js/tde/tidalstream.js +372 -0
- package/dist/js/tde_old/blackhole.obj.js +354 -0
- package/dist/js/tde_old/debris.obj.js +791 -0
- package/dist/js/tde_old/flare.obj.js +239 -0
- package/dist/js/tde_old/index.js +448 -0
- package/dist/js/tde_old/star.obj.js +812 -0
- package/dist/js/tetris/config.js +157 -0
- package/dist/js/tetris/grid.js +286 -0
- package/dist/js/tetris/index.js +1195 -0
- package/dist/js/tetris/renderer.js +634 -0
- package/dist/js/tetris/tetrominos.js +280 -0
- package/dist/js/tiles.js +312 -0
- package/dist/js/tweendemo.js +79 -0
- package/dist/js/visibility.js +102 -0
- package/dist/kerr.html +28 -0
- package/dist/lavalamp.html +27 -0
- package/dist/layouts.html +37 -0
- package/dist/logo.svg +4 -0
- package/dist/loop.html +84 -0
- package/dist/mondrian.html +32 -0
- package/dist/og_image.png +0 -0
- package/dist/opacity.html +36 -0
- package/dist/painter.html +39 -0
- package/dist/particles-showcase.html +28 -0
- package/dist/particles.html +24 -0
- package/dist/patterns.html +33 -0
- package/dist/penrose-game.html +31 -0
- package/dist/pipeline.html +737 -0
- package/dist/plane3d.html +24 -0
- package/dist/platformer.html +43 -0
- package/dist/scene.html +33 -0
- package/dist/scenes.html +96 -0
- package/dist/schrodinger.html +27 -0
- package/dist/schwarzschild.html +27 -0
- package/dist/shapes.html +16 -0
- package/dist/space.html +85 -0
- package/dist/spacetime.html +27 -0
- package/dist/sphere3d.html +24 -0
- package/dist/sprite.html +18 -0
- package/dist/starfaux.html +22 -0
- package/dist/study001.html +23 -0
- package/dist/study002.html +23 -0
- package/dist/study003.html +23 -0
- package/dist/study004.html +23 -0
- package/dist/study005.html +22 -0
- package/dist/study006.html +24 -0
- package/dist/study007.html +24 -0
- package/dist/study008.html +22 -0
- package/dist/svgtween.html +29 -0
- package/dist/tde.html +28 -0
- package/dist/tetris3d.html +25 -0
- package/dist/tiles.html +28 -0
- package/dist/transforms.html +400 -0
- package/dist/tween.html +45 -0
- package/dist/visibility.html +33 -0
- package/package.json +1 -1
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { GameObject, Sphere3D } from "/gcanvas.es.min.js";
|
|
2
|
+
import { polarToCartesian } from "/gcanvas.es.min.js";
|
|
3
|
+
import { CONFIG } from "./config.js";
|
|
4
|
+
|
|
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
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class Star extends GameObject {
|
|
14
|
+
constructor(game, options = {}) {
|
|
15
|
+
super(game, options);
|
|
16
|
+
this.mass = options.initialMass ?? CONFIG.star.initialMass;
|
|
17
|
+
this.initialMass = this.mass; // Store for mass ratio calculations
|
|
18
|
+
this.phi = 0;
|
|
19
|
+
// Initialize with reasonable defaults, will be updated by onResize
|
|
20
|
+
this.baseRadius = game.baseScale ? game.baseScale * CONFIG.starRadiusRatio : 20;
|
|
21
|
+
this.currentRadius = this.baseRadius;
|
|
22
|
+
this.orbitalRadius = game.baseScale ? game.baseScale * CONFIG.star.initialOrbitRadius : 200;
|
|
23
|
+
this.initialOrbitalRadius = this.orbitalRadius; // Store initial for decay calculations
|
|
24
|
+
|
|
25
|
+
// Velocity tracking for particle emission
|
|
26
|
+
this.velocityX = 0;
|
|
27
|
+
this.velocityY = 0;
|
|
28
|
+
this.velocityZ = 0;
|
|
29
|
+
this._prevX = 0;
|
|
30
|
+
this._prevY = 0;
|
|
31
|
+
this._prevZ = 0;
|
|
32
|
+
|
|
33
|
+
// Use WebGL shaders for star rendering
|
|
34
|
+
this.useShader = options.useShader ?? true;
|
|
35
|
+
|
|
36
|
+
// Cumulative rotation for angular emission detail
|
|
37
|
+
this.rotation = 0;
|
|
38
|
+
// Angular velocity (rad/s) - accumulates smoothly instead of discrete recalc
|
|
39
|
+
this.angularVelocity = CONFIG.star.rotationSpeed ?? 0.5;
|
|
40
|
+
|
|
41
|
+
// Tidal disruption state
|
|
42
|
+
this.tidalStretch = 0; // 0 = spherical, 1 = max elongation
|
|
43
|
+
this.pulsationPhase = 0; // Oscillation phase
|
|
44
|
+
this.stressLevel = 0; // Surface chaos level
|
|
45
|
+
this.tidalProgress = 0; // External tidal progress from FSM (0-1)
|
|
46
|
+
this.tidalFlare = 0; // 0-1, sudden brightness burst at disruption start
|
|
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;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
init() {
|
|
56
|
+
// Initialize position on the orbit
|
|
57
|
+
const pos = polarToCartesian(this.orbitalRadius, this.phi);
|
|
58
|
+
this.x = pos.x;
|
|
59
|
+
this.z = pos.z;
|
|
60
|
+
|
|
61
|
+
// Initialize prev position to avoid velocity spike on first frame
|
|
62
|
+
this._prevX = this.x;
|
|
63
|
+
this._prevY = this.y || 0;
|
|
64
|
+
this._prevZ = this.z;
|
|
65
|
+
this.velocityX = 0;
|
|
66
|
+
this.velocityY = 0;
|
|
67
|
+
this.velocityZ = 0;
|
|
68
|
+
|
|
69
|
+
// Reset tidal state
|
|
70
|
+
this.tidalStretch = 0;
|
|
71
|
+
this.pulsationPhase = 0;
|
|
72
|
+
this.stressLevel = 0;
|
|
73
|
+
this.tidalProgress = 0;
|
|
74
|
+
this.tidalFlare = 0;
|
|
75
|
+
this.tidalWobble = 0;
|
|
76
|
+
this.angularVelocity = CONFIG.star.rotationSpeed ?? 0.5;
|
|
77
|
+
this.rotation = 0;
|
|
78
|
+
|
|
79
|
+
this.updateVisual();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reset velocity tracking (call after position changes like restart)
|
|
84
|
+
*/
|
|
85
|
+
resetVelocity() {
|
|
86
|
+
this._prevX = this.x;
|
|
87
|
+
this._prevY = this.y || 0;
|
|
88
|
+
this._prevZ = this.z;
|
|
89
|
+
this.velocityX = 0;
|
|
90
|
+
this.velocityY = 0;
|
|
91
|
+
this.velocityZ = 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
updateVisual() {
|
|
95
|
+
const massRatio = this.mass / this.initialMass;
|
|
96
|
+
|
|
97
|
+
// === NON-LINEAR SIZE COLLAPSE ===
|
|
98
|
+
// Use sqrt for resistance curve (star resists early, then collapses)
|
|
99
|
+
const collapseProgress = 1 - massRatio;
|
|
100
|
+
const effectiveMassRatio = 1 - Math.sqrt(collapseProgress);
|
|
101
|
+
|
|
102
|
+
// Base radius with non-linear collapse
|
|
103
|
+
this.currentRadius = this.baseRadius * Math.max(0.05, effectiveMassRatio);
|
|
104
|
+
|
|
105
|
+
// Don't update if star is consumed
|
|
106
|
+
if (this.currentRadius <= 0 || this.mass <= 0) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
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;
|
|
115
|
+
|
|
116
|
+
// Direction toward black hole (unit vector)
|
|
117
|
+
const dirX = -this.x * invDist;
|
|
118
|
+
const dirZ = -zVal * invDist;
|
|
119
|
+
|
|
120
|
+
// Proximity factor: closer to BH = more stretch
|
|
121
|
+
const proximityFactor = Math.max(0, 1 - dist / this.initialOrbitalRadius);
|
|
122
|
+
|
|
123
|
+
// Simplified stretch calculation
|
|
124
|
+
if (collapseProgress > 0.8) {
|
|
125
|
+
this.tidalStretch = (1 - collapseProgress) * 2;
|
|
126
|
+
} else {
|
|
127
|
+
this.tidalStretch = Math.min(1.8, this.tidalProgress * 1.2 + proximityFactor * 0.5);
|
|
128
|
+
}
|
|
129
|
+
|
|
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
|
+
}
|
|
135
|
+
|
|
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
|
|
139
|
+
|
|
140
|
+
// === ACTIVITY & ROTATION ===
|
|
141
|
+
const activityLevel = 0.3 + this.stressLevel * 0.7;
|
|
142
|
+
const baseRotationSpeed = CONFIG.star.rotationSpeed ?? 0.5;
|
|
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
|
|
153
|
+
} else {
|
|
154
|
+
g = 0.5;
|
|
155
|
+
b = 0.2;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const stressColor = [r, g, b];
|
|
159
|
+
this.currentColor = stressColor;
|
|
160
|
+
|
|
161
|
+
// Temperature calculation
|
|
162
|
+
const temperature = (CONFIG.star.temperature ?? 3800) + stress * stress * 2500;
|
|
163
|
+
|
|
164
|
+
// === VISUAL UPDATE ===
|
|
165
|
+
if (!this.visual) {
|
|
166
|
+
this.visual = new Sphere3D(this.currentRadius, {
|
|
167
|
+
color: CONFIG.star.color,
|
|
168
|
+
camera: this.game.camera,
|
|
169
|
+
useShader: this.useShader,
|
|
170
|
+
shaderType: "star",
|
|
171
|
+
shaderUniforms: {
|
|
172
|
+
uStarColor: stressColor,
|
|
173
|
+
uTemperature: temperature,
|
|
174
|
+
uActivityLevel: activityLevel,
|
|
175
|
+
uRotationSpeed: rotationSpeed,
|
|
176
|
+
uTidalStretch: this.tidalStretch,
|
|
177
|
+
uStretchDirX: dirX,
|
|
178
|
+
uStretchDirZ: dirZ,
|
|
179
|
+
uStressLevel: this.stressLevel,
|
|
180
|
+
uTidalFlare: this.tidalFlare,
|
|
181
|
+
uTidalWobble: this.tidalWobble,
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
this._lastGeometryRadius = this.currentRadius;
|
|
185
|
+
} else {
|
|
186
|
+
this.visual.radius = this.currentRadius;
|
|
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;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
onResize(baseRadius, orbitalRadius) {
|
|
219
|
+
this.baseRadius = baseRadius;
|
|
220
|
+
this.orbitalRadius = orbitalRadius;
|
|
221
|
+
this.initialOrbitalRadius = orbitalRadius;
|
|
222
|
+
|
|
223
|
+
// Update position to match new orbital radius
|
|
224
|
+
const pos = polarToCartesian(this.orbitalRadius, this.phi);
|
|
225
|
+
this.x = pos.x;
|
|
226
|
+
this.z = pos.z;
|
|
227
|
+
|
|
228
|
+
this.updateVisual();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
update(dt) {
|
|
232
|
+
super.update(dt);
|
|
233
|
+
|
|
234
|
+
// Calculate velocity from position change
|
|
235
|
+
const currentY = this.y || 0;
|
|
236
|
+
if (dt > 0) {
|
|
237
|
+
this.velocityX = (this.x - this._prevX) / dt;
|
|
238
|
+
this.velocityY = (currentY - this._prevY) / dt;
|
|
239
|
+
this.velocityZ = (this.z - this._prevZ) / dt;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Store current position for next frame
|
|
243
|
+
this._prevX = this.x;
|
|
244
|
+
this._prevY = currentY;
|
|
245
|
+
this._prevZ = this.z;
|
|
246
|
+
|
|
247
|
+
// Update self-rotation with smooth angular momentum conservation
|
|
248
|
+
// As star shrinks, angular velocity increases (I*ω = constant)
|
|
249
|
+
// But cap it when star is tiny (< 10% radius) - no point wasting frames
|
|
250
|
+
const radiusRatio = this.currentRadius / this.baseRadius;
|
|
251
|
+
|
|
252
|
+
if (radiusRatio > 0.1) {
|
|
253
|
+
// Base rotation speed from config
|
|
254
|
+
const baseSpeed = CONFIG.star.rotationSpeed ?? 0.5;
|
|
255
|
+
|
|
256
|
+
// Spin-up factor based on tidal progress (FSM-driven, smooth)
|
|
257
|
+
// Only significant spin-up during actual disruption (mass loss)
|
|
258
|
+
const massRatio = (this.mass || 1) / (this.initialMass || 1);
|
|
259
|
+
const massLoss = 1 - massRatio; // 0 = no loss, 1 = fully consumed
|
|
260
|
+
|
|
261
|
+
// Gentle spin-up from tidal stress, moderate spin-up from mass loss
|
|
262
|
+
// tidalProgress: 0-1 during stretch, 1 during disrupt
|
|
263
|
+
// massLoss: 0 during stretch, 0-1 during disrupt
|
|
264
|
+
const tidalSpinUp = 1 + this.tidalProgress * 0.3; // Up to 1.3x from tidal
|
|
265
|
+
const collapseSpinUp = 1 + massLoss * 1.5; // Up to 2.5x from collapse
|
|
266
|
+
|
|
267
|
+
const targetVelocity = baseSpeed * tidalSpinUp * collapseSpinUp;
|
|
268
|
+
|
|
269
|
+
// Very slow approach to target - no sudden jumps
|
|
270
|
+
const accelRate = 0.001;
|
|
271
|
+
this.angularVelocity += (targetVelocity - this.angularVelocity) * accelRate * dt;
|
|
272
|
+
|
|
273
|
+
// Hard cap on max spin (2.5 rad/s - calm, cosmic feel)
|
|
274
|
+
this.angularVelocity = Math.min(2.5, this.angularVelocity);
|
|
275
|
+
}
|
|
276
|
+
// else: keep current velocity, don't accelerate tiny remnant
|
|
277
|
+
|
|
278
|
+
this.rotation += this.angularVelocity * dt;
|
|
279
|
+
|
|
280
|
+
// Update breathing phase - slow, cosmic rhythm (0.3-0.5 Hz)
|
|
281
|
+
const breathingFreq = 0.3 + this.stressLevel * 0.2;
|
|
282
|
+
this.pulsationPhase += breathingFreq * dt * Math.PI * 2;
|
|
283
|
+
|
|
284
|
+
this.updateVisual();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
render() {
|
|
288
|
+
super.render();
|
|
289
|
+
if (this.mass > 0 && this.visual) {
|
|
290
|
+
// Sync visual position with star position
|
|
291
|
+
this.visual.x = this.x;
|
|
292
|
+
this.visual.y = this.y || 0;
|
|
293
|
+
this.visual.z = this.z;
|
|
294
|
+
this.visual.render();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { GameObject, Painter } from "/gcanvas.es.min.js";
|
|
2
|
+
import { applyGravitationalLensing } from "/gcanvas.es.min.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: 10000,
|
|
17
|
+
particleLifetime: 20, // 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: 1.2,
|
|
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
|
+
// Project black hole position once for all particles
|
|
263
|
+
// This is crucial when camera has moved (e.g., following the star)
|
|
264
|
+
const bhProjected = this.camera.project(0, 0, 0);
|
|
265
|
+
const bhScreenX = bhProjected.x;
|
|
266
|
+
const bhScreenY = bhProjected.y;
|
|
267
|
+
|
|
268
|
+
// Young particles stay invisible (appear to emerge from star)
|
|
269
|
+
const fadeInTime = 0.05; // seconds before particles become visible
|
|
270
|
+
const fadeInDuration = 0.1; // seconds to fade from invisible to full opacity
|
|
271
|
+
|
|
272
|
+
for (const p of this.particles) {
|
|
273
|
+
// Skip very young particles - they're "inside" the star
|
|
274
|
+
if (p.age < fadeInTime) continue;
|
|
275
|
+
|
|
276
|
+
const projected = this.camera.project(p.x, p.y, p.z);
|
|
277
|
+
|
|
278
|
+
// Skip if behind camera
|
|
279
|
+
if (projected.scale <= 0) continue;
|
|
280
|
+
|
|
281
|
+
// Fade in young particles (after fadeInTime threshold)
|
|
282
|
+
const fadeInProgress = Math.min(1, (p.age - fadeInTime) / fadeInDuration);
|
|
283
|
+
|
|
284
|
+
// Distance from BH for color
|
|
285
|
+
const dist = Math.sqrt(p.x * p.x + p.z * p.z);
|
|
286
|
+
const colorT = Math.min(1, dist / (p.initialDist || 1));
|
|
287
|
+
|
|
288
|
+
// Use particle's emitted color (star color at emission time)
|
|
289
|
+
const hotColor = p.emitColor || STREAM_CONFIG.colorHot;
|
|
290
|
+
|
|
291
|
+
// Lerp color: cool near BH, hot (star color) near initial position
|
|
292
|
+
const color = {
|
|
293
|
+
r: STREAM_CONFIG.colorCool.r + (hotColor.r - STREAM_CONFIG.colorCool.r) * colorT,
|
|
294
|
+
g: STREAM_CONFIG.colorCool.g + (hotColor.g - STREAM_CONFIG.colorCool.g) * colorT,
|
|
295
|
+
b: STREAM_CONFIG.colorCool.b + (hotColor.b - STREAM_CONFIG.colorCool.b) * colorT,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Fade with age (fade out at end of life) and fade in at birth
|
|
299
|
+
const fadeOutAlpha = Math.max(0, 1 - p.age / STREAM_CONFIG.particleLifetime);
|
|
300
|
+
const alpha = fadeOutAlpha * fadeInProgress; // Combine fade-in and fade-out
|
|
301
|
+
|
|
302
|
+
// Apply gravitational lensing to screen coordinates
|
|
303
|
+
// Scale lensing with BH's current (pulsing) radius
|
|
304
|
+
let screenX = projected.x;
|
|
305
|
+
let screenY = projected.y;
|
|
306
|
+
|
|
307
|
+
if (STREAM_CONFIG.lensing.enabled && this.bhRadius > 0) {
|
|
308
|
+
// Calculate particle position RELATIVE to black hole's screen position
|
|
309
|
+
const relX = screenX - bhScreenX;
|
|
310
|
+
const relY = screenY - bhScreenY;
|
|
311
|
+
|
|
312
|
+
const effectRadius = this.bhRadius * STREAM_CONFIG.lensing.effectRadiusMult;
|
|
313
|
+
const strength = this.bhRadius * STREAM_CONFIG.lensing.strengthMult;
|
|
314
|
+
const minDist = this.bhRadius * STREAM_CONFIG.lensing.minDistanceMult;
|
|
315
|
+
|
|
316
|
+
// Apply lensing in BH-relative space (lensing curves toward origin)
|
|
317
|
+
const lensed = applyGravitationalLensing(
|
|
318
|
+
relX, relY,
|
|
319
|
+
effectRadius,
|
|
320
|
+
strength,
|
|
321
|
+
STREAM_CONFIG.lensing.falloff,
|
|
322
|
+
minDist
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// Transform back to screen space
|
|
326
|
+
screenX = lensed.x + bhScreenX;
|
|
327
|
+
screenY = lensed.y + bhScreenY;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check if particle is visually inside the black hole
|
|
331
|
+
// Use pre-calculated BH screen position
|
|
332
|
+
const dxBH = screenX - bhScreenX;
|
|
333
|
+
const dyBH = screenY - bhScreenY;
|
|
334
|
+
const screenDistFromBH = Math.sqrt(dxBH * dxBH + dyBH * dyBH);
|
|
335
|
+
const insideBH = screenDistFromBH < this.bhRadius;
|
|
336
|
+
|
|
337
|
+
// Screen position = center + lensed offset
|
|
338
|
+
renderList.push({
|
|
339
|
+
x: cx + screenX,
|
|
340
|
+
y: cy + screenY,
|
|
341
|
+
z: projected.z,
|
|
342
|
+
size: p.size * projected.scale,
|
|
343
|
+
color: insideBH ? { r: 0, g: 0, b: 0 } : color,
|
|
344
|
+
alpha,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Sort back to front
|
|
349
|
+
renderList.sort((a, b) => b.z - a.z);
|
|
350
|
+
|
|
351
|
+
// Draw particles with reset transform (absolute screen coords)
|
|
352
|
+
Painter.useCtx((ctx) => {
|
|
353
|
+
// Reset to identity matrix - Scene3D has applied transforms we need to bypass
|
|
354
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
355
|
+
|
|
356
|
+
ctx.globalCompositeOperation = "lighter";
|
|
357
|
+
|
|
358
|
+
for (const item of renderList) {
|
|
359
|
+
const r = Math.round(item.color.r);
|
|
360
|
+
const g = Math.round(item.color.g);
|
|
361
|
+
const b = Math.round(item.color.b);
|
|
362
|
+
|
|
363
|
+
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${item.alpha})`;
|
|
364
|
+
ctx.beginPath();
|
|
365
|
+
ctx.arc(item.x, item.y, item.size, 0, Math.PI * 2);
|
|
366
|
+
ctx.fill();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
ctx.globalCompositeOperation = "source-over";
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|