@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,471 @@
|
|
|
1
|
+
import { GameObject, Painter, Tweenetik, Easing } from "/gcanvas.es.min.js";
|
|
2
|
+
import { keplerianOmega } from "/gcanvas.es.min.js";
|
|
3
|
+
import { CONFIG } from "./config.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AccretionDisk - Keplerian particle disk with gravitational lensing
|
|
7
|
+
*
|
|
8
|
+
* Uses the same proven lensing formula as demos/js/blackhole.js:
|
|
9
|
+
* - Single-pass lensing that pushes particles outward
|
|
10
|
+
* - Einstein ring forms naturally from disk geometry
|
|
11
|
+
* - Doppler beaming for brightness variation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const DISK_CONFIG = {
|
|
15
|
+
// Orbital bounds (multiplier of BH radius)
|
|
16
|
+
innerRadiusMultiplier: 1.5,
|
|
17
|
+
outerRadiusMultiplier: 9.0, // Wide disk with margin from screen edges
|
|
18
|
+
|
|
19
|
+
// Particle properties
|
|
20
|
+
maxParticles: 10000,
|
|
21
|
+
particleLifetime: 1000,
|
|
22
|
+
spawnRate: 500,
|
|
23
|
+
|
|
24
|
+
// Orbital physics
|
|
25
|
+
baseOrbitalSpeed: 0.8,
|
|
26
|
+
|
|
27
|
+
// Decay mechanics
|
|
28
|
+
decayChanceBase: 0.0002,
|
|
29
|
+
decaySpeedFactor: 0.995,
|
|
30
|
+
|
|
31
|
+
// Disk geometry - thin disk with some spread
|
|
32
|
+
diskThickness: 0.006,
|
|
33
|
+
|
|
34
|
+
// Lensing - pushes particles outward to form Einstein ring
|
|
35
|
+
ringRadiusFactor: 1.8, // Higher = more margin between BH and ring
|
|
36
|
+
lensingFalloff: 1.8, // Slightly wider falloff
|
|
37
|
+
|
|
38
|
+
// Visual - heat gradient (white-hot inner to deep red outer)
|
|
39
|
+
colorHot: { r: 255, g: 250, b: 220 }, // Inner (white-hot)
|
|
40
|
+
colorMid: { r: 255, g: 160, b: 50 }, // Mid (orange)
|
|
41
|
+
colorCool: { r: 180, g: 40, b: 40 }, // Outer (deep red)
|
|
42
|
+
|
|
43
|
+
sizeMin: .8,
|
|
44
|
+
sizeMax: 1.2,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export class AccretionDisk extends GameObject {
|
|
48
|
+
constructor(game, options = {}) {
|
|
49
|
+
super(game, options);
|
|
50
|
+
|
|
51
|
+
this.camera = options.camera;
|
|
52
|
+
this.bhRadius = options.bhRadius ?? 50;
|
|
53
|
+
this.bhMass = options.bhMass ?? CONFIG.blackHole.initialMass;
|
|
54
|
+
|
|
55
|
+
// Disk bounds scale with BH radius
|
|
56
|
+
this.innerRadius = this.bhRadius * DISK_CONFIG.innerRadiusMultiplier;
|
|
57
|
+
this.outerRadius = this.bhRadius * DISK_CONFIG.outerRadiusMultiplier;
|
|
58
|
+
|
|
59
|
+
// State
|
|
60
|
+
this.active = false;
|
|
61
|
+
this.lensingStrength = 0; // Ramps up during activation
|
|
62
|
+
this.scale = 0; // For expand-from-BH animation
|
|
63
|
+
|
|
64
|
+
// Callback when particle falls into BH
|
|
65
|
+
this.onParticleConsumed = options.onParticleConsumed ?? null;
|
|
66
|
+
|
|
67
|
+
// Particle array
|
|
68
|
+
this.particles = [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Activate disk with expand-from-center animation
|
|
73
|
+
*/
|
|
74
|
+
activate() {
|
|
75
|
+
if (this.active) return;
|
|
76
|
+
this.active = true;
|
|
77
|
+
this.scale = 0.3; // Start partially expanded so it's visible immediately
|
|
78
|
+
this.lensingStrength = 0;
|
|
79
|
+
// Expansion from BH center - 2 seconds (was 4, felt too slow)
|
|
80
|
+
Tweenetik.to(this, { scale: 1 }, 2.0, Easing.easeOutQuart);
|
|
81
|
+
// Lensing ramps up alongside scale
|
|
82
|
+
Tweenetik.to(this, { lensingStrength: 1 }, 2.5, Easing.easeOutQuad);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
init() {
|
|
86
|
+
this.particles = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get heat-based color for particle at given radius
|
|
91
|
+
*/
|
|
92
|
+
getHeatColor(distance) {
|
|
93
|
+
const t = (distance - this.innerRadius) / (this.outerRadius - this.innerRadius);
|
|
94
|
+
|
|
95
|
+
let r, g, b;
|
|
96
|
+
if (t < 0.5) {
|
|
97
|
+
// Inner half: hot -> mid
|
|
98
|
+
const t2 = t * 2;
|
|
99
|
+
r = DISK_CONFIG.colorHot.r + (DISK_CONFIG.colorMid.r - DISK_CONFIG.colorHot.r) * t2;
|
|
100
|
+
g = DISK_CONFIG.colorHot.g + (DISK_CONFIG.colorMid.g - DISK_CONFIG.colorHot.g) * t2;
|
|
101
|
+
b = DISK_CONFIG.colorHot.b + (DISK_CONFIG.colorMid.b - DISK_CONFIG.colorHot.b) * t2;
|
|
102
|
+
} else {
|
|
103
|
+
// Outer half: mid -> cool
|
|
104
|
+
const t2 = (t - 0.5) * 2;
|
|
105
|
+
r = DISK_CONFIG.colorMid.r + (DISK_CONFIG.colorCool.r - DISK_CONFIG.colorMid.r) * t2;
|
|
106
|
+
g = DISK_CONFIG.colorMid.g + (DISK_CONFIG.colorCool.g - DISK_CONFIG.colorMid.g) * t2;
|
|
107
|
+
b = DISK_CONFIG.colorMid.b + (DISK_CONFIG.colorCool.b - DISK_CONFIG.colorMid.b) * t2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { r: Math.round(r), g: Math.round(g), b: Math.round(b) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Spawn a new particle at random position in disk
|
|
115
|
+
*/
|
|
116
|
+
spawnParticle() {
|
|
117
|
+
if (this.particles.length >= DISK_CONFIG.maxParticles) return;
|
|
118
|
+
|
|
119
|
+
// Balanced distribution with slight inner bias for lensing visibility
|
|
120
|
+
// Lower power = more particles near inner edge (where lensing is strongest)
|
|
121
|
+
const t = Math.pow(Math.random(), 0.6);
|
|
122
|
+
const distance = this.innerRadius + (this.outerRadius - this.innerRadius) * t;
|
|
123
|
+
|
|
124
|
+
const angle = Math.random() * Math.PI * 2;
|
|
125
|
+
|
|
126
|
+
// Keplerian orbital speed
|
|
127
|
+
const speed = keplerianOmega(distance, this.bhMass, DISK_CONFIG.baseOrbitalSpeed, this.outerRadius);
|
|
128
|
+
|
|
129
|
+
// Small vertical offset for thin disk
|
|
130
|
+
const baseScale = this.game.baseScale ?? Math.min(this.game.width, this.game.height);
|
|
131
|
+
const yOffset = (Math.random() - 0.5) * baseScale * DISK_CONFIG.diskThickness;
|
|
132
|
+
|
|
133
|
+
this.particles.push({
|
|
134
|
+
angle,
|
|
135
|
+
distance,
|
|
136
|
+
yOffset,
|
|
137
|
+
speed,
|
|
138
|
+
// Small random initial age prevents batch death
|
|
139
|
+
age: Math.random() * DISK_CONFIG.particleLifetime * 0.1, // Only 10% spread
|
|
140
|
+
isFalling: false,
|
|
141
|
+
size: DISK_CONFIG.sizeMin + Math.random() * (DISK_CONFIG.sizeMax - DISK_CONFIG.sizeMin),
|
|
142
|
+
baseColor: this.getHeatColor(distance),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Capture a particle from the tidal stream
|
|
148
|
+
* Converts Cartesian stream particle to polar disk orbit
|
|
149
|
+
*/
|
|
150
|
+
captureParticle(streamParticle) {
|
|
151
|
+
if (this.particles.length >= DISK_CONFIG.maxParticles) return;
|
|
152
|
+
|
|
153
|
+
const x = streamParticle.x;
|
|
154
|
+
const z = streamParticle.z;
|
|
155
|
+
const dist = Math.sqrt(x * x + z * z);
|
|
156
|
+
|
|
157
|
+
// Skip if outside disk bounds
|
|
158
|
+
if (dist < this.innerRadius || dist > this.outerRadius) return;
|
|
159
|
+
|
|
160
|
+
const angle = Math.atan2(z, x);
|
|
161
|
+
|
|
162
|
+
// Calculate tangential velocity from stream particle
|
|
163
|
+
const vx = streamParticle.vx ?? 0;
|
|
164
|
+
const vz = streamParticle.vz ?? 0;
|
|
165
|
+
const tangentVx = -z / dist;
|
|
166
|
+
const tangentVz = x / dist;
|
|
167
|
+
const tangentSpeed = vx * tangentVx + vz * tangentVz;
|
|
168
|
+
|
|
169
|
+
// Convert to angular velocity
|
|
170
|
+
const angularVelocity = Math.abs(tangentSpeed) / dist;
|
|
171
|
+
|
|
172
|
+
// Target Keplerian speed
|
|
173
|
+
const keplerianSpeed = keplerianOmega(dist, this.bhMass, DISK_CONFIG.baseOrbitalSpeed, this.outerRadius);
|
|
174
|
+
|
|
175
|
+
// Blend toward Keplerian (captured particles circularize)
|
|
176
|
+
const blendedSpeed = (angularVelocity + keplerianSpeed) / 2;
|
|
177
|
+
|
|
178
|
+
this.particles.push({
|
|
179
|
+
angle,
|
|
180
|
+
distance: dist,
|
|
181
|
+
yOffset: streamParticle.y ?? 0,
|
|
182
|
+
speed: blendedSpeed,
|
|
183
|
+
age: 0,
|
|
184
|
+
isFalling: false,
|
|
185
|
+
size: streamParticle.size ?? (DISK_CONFIG.sizeMin + Math.random() * (DISK_CONFIG.sizeMax - DISK_CONFIG.sizeMin)),
|
|
186
|
+
baseColor: this.getHeatColor(dist),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if particle should begin decay spiral
|
|
192
|
+
*/
|
|
193
|
+
checkDecay(p) {
|
|
194
|
+
// Higher decay chance near ISCO (innermost stable circular orbit)
|
|
195
|
+
const iscoProximity = (p.distance - this.innerRadius) / (this.outerRadius - this.innerRadius);
|
|
196
|
+
const ageDecayFactor = Math.min(1, p.age / DISK_CONFIG.particleLifetime);
|
|
197
|
+
|
|
198
|
+
// Particles near inner edge or old ones are more likely to fall
|
|
199
|
+
const decayChance = DISK_CONFIG.decayChanceBase *
|
|
200
|
+
(1 + 3 * (1 - iscoProximity)) *
|
|
201
|
+
(1 + ageDecayFactor);
|
|
202
|
+
|
|
203
|
+
if (Math.random() < decayChance) {
|
|
204
|
+
p.isFalling = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
update(dt) {
|
|
209
|
+
super.update(dt);
|
|
210
|
+
|
|
211
|
+
// Spawn new particles when active
|
|
212
|
+
if (this.active && this.particles.length < DISK_CONFIG.maxParticles) {
|
|
213
|
+
for (let i = 0; i < DISK_CONFIG.spawnRate; i++) {
|
|
214
|
+
this.spawnParticle();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Update particles
|
|
219
|
+
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
220
|
+
const p = this.particles[i];
|
|
221
|
+
p.age += dt;
|
|
222
|
+
|
|
223
|
+
// Remove old particles
|
|
224
|
+
if (p.age > DISK_CONFIG.particleLifetime) {
|
|
225
|
+
this.particles.splice(i, 1);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (p.isFalling) {
|
|
230
|
+
// Spiral inward - exponential decay
|
|
231
|
+
p.distance *= DISK_CONFIG.decaySpeedFactor;
|
|
232
|
+
p.angle += p.speed * dt * 2; // Accelerate as falls
|
|
233
|
+
p.yOffset *= 0.95; // Flatten toward equator
|
|
234
|
+
|
|
235
|
+
// Consumed by black hole
|
|
236
|
+
if (p.distance < this.bhRadius * 0.5) {
|
|
237
|
+
this.particles.splice(i, 1);
|
|
238
|
+
if (this.onParticleConsumed) {
|
|
239
|
+
this.onParticleConsumed();
|
|
240
|
+
}
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
// Normal Keplerian orbit
|
|
245
|
+
p.angle += p.speed * dt;
|
|
246
|
+
|
|
247
|
+
// Check for decay
|
|
248
|
+
this.checkDecay(p);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Build render list with camera-space lensing
|
|
255
|
+
* Uses the proven formula from demos/js/blackhole.js
|
|
256
|
+
*/
|
|
257
|
+
buildRenderList() {
|
|
258
|
+
const renderList = [];
|
|
259
|
+
if (!this.camera || this.particles.length === 0) return renderList;
|
|
260
|
+
|
|
261
|
+
const lensingStrength = this.lensingStrength;
|
|
262
|
+
|
|
263
|
+
for (const p of this.particles) {
|
|
264
|
+
// World coordinates (flat disk in x-z plane)
|
|
265
|
+
const scaledDist = p.distance * this.scale;
|
|
266
|
+
let x = Math.cos(p.angle) * scaledDist;
|
|
267
|
+
let y = p.yOffset * this.scale;
|
|
268
|
+
let z = Math.sin(p.angle) * scaledDist;
|
|
269
|
+
|
|
270
|
+
// Transform to camera space
|
|
271
|
+
const cosY = Math.cos(this.camera.rotationY);
|
|
272
|
+
const sinY = Math.sin(this.camera.rotationY);
|
|
273
|
+
let xCam = x * cosY - z * sinY;
|
|
274
|
+
let zCam = x * sinY + z * cosY;
|
|
275
|
+
|
|
276
|
+
const cosX = Math.cos(this.camera.rotationX);
|
|
277
|
+
const sinX = Math.sin(this.camera.rotationX);
|
|
278
|
+
let yCam = y * cosX - zCam * sinX;
|
|
279
|
+
zCam = y * sinX + zCam * cosX;
|
|
280
|
+
|
|
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) {
|
|
290
|
+
const ringRadius = this.bhRadius * DISK_CONFIG.ringRadiusFactor;
|
|
291
|
+
const lensFactor = Math.exp(-currentR / (this.bhRadius * DISK_CONFIG.lensingFalloff));
|
|
292
|
+
const warp = lensFactor * 1.2 * lensingStrength;
|
|
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
|
|
301
|
+
if (currentR > 0) {
|
|
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;
|
|
316
|
+
xCam *= ratio;
|
|
317
|
+
yCam *= ratio;
|
|
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
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Perspective projection
|
|
352
|
+
const perspectiveScale = this.camera.perspective / (this.camera.perspective + zCam);
|
|
353
|
+
const screenX = xCam * perspectiveScale;
|
|
354
|
+
const screenY = yCam * perspectiveScale;
|
|
355
|
+
|
|
356
|
+
// Skip particles behind camera
|
|
357
|
+
if (zCam < -this.camera.perspective + 10) continue;
|
|
358
|
+
|
|
359
|
+
// Doppler beaming - approaching side brighter
|
|
360
|
+
const velocityDir = Math.cos(p.angle + this.camera.rotationY);
|
|
361
|
+
const doppler = 1 + velocityDir * 0.4;
|
|
362
|
+
|
|
363
|
+
// Age-based fade
|
|
364
|
+
const ageRatio = p.age / DISK_CONFIG.particleLifetime;
|
|
365
|
+
const alpha = Math.max(0.3, 1 - Math.pow(ageRatio, 2.5));
|
|
366
|
+
|
|
367
|
+
// Color (redshift for falling particles)
|
|
368
|
+
let color = p.baseColor;
|
|
369
|
+
if (p.isFalling) {
|
|
370
|
+
const fallProgress = 1 - (p.distance / this.innerRadius);
|
|
371
|
+
color = {
|
|
372
|
+
r: Math.round(p.baseColor.r * (1 - fallProgress * 0.5)),
|
|
373
|
+
g: Math.round(p.baseColor.g * (1 - fallProgress * 0.7)),
|
|
374
|
+
b: Math.round(p.baseColor.b * (1 - fallProgress * 0.3)),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
renderList.push({
|
|
379
|
+
x: screenX,
|
|
380
|
+
y: screenY,
|
|
381
|
+
z: zCam,
|
|
382
|
+
scale: perspectiveScale,
|
|
383
|
+
color,
|
|
384
|
+
doppler,
|
|
385
|
+
alpha,
|
|
386
|
+
size: p.size,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Sort back to front for proper blending
|
|
391
|
+
renderList.sort((a, b) => b.z - a.z);
|
|
392
|
+
return renderList;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Clear all particles
|
|
397
|
+
*/
|
|
398
|
+
clear() {
|
|
399
|
+
this.particles = [];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Update BH radius - also updates disk bounds since they scale with BH
|
|
404
|
+
* Particles inside the event horizon are consumed; others remain in place
|
|
405
|
+
* and will naturally be replaced by new spawns at correct radii
|
|
406
|
+
*/
|
|
407
|
+
updateBHRadius(radius) {
|
|
408
|
+
this.bhRadius = radius;
|
|
409
|
+
// Disk bounds scale with BH radius
|
|
410
|
+
this.innerRadius = this.bhRadius * DISK_CONFIG.innerRadiusMultiplier;
|
|
411
|
+
this.outerRadius = this.bhRadius * DISK_CONFIG.outerRadiusMultiplier;
|
|
412
|
+
|
|
413
|
+
// Consume particles swallowed by event horizon (same threshold as update loop)
|
|
414
|
+
const consumeRadius = this.bhRadius * 0.5;
|
|
415
|
+
for (let i = this.particles.length - 1; i >= 0; i--) {
|
|
416
|
+
const p = this.particles[i];
|
|
417
|
+
|
|
418
|
+
if (p.distance < consumeRadius) {
|
|
419
|
+
this.particles.splice(i, 1);
|
|
420
|
+
if (this.onParticleConsumed) {
|
|
421
|
+
this.onParticleConsumed();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
render() {
|
|
428
|
+
super.render();
|
|
429
|
+
|
|
430
|
+
if (!this.active || !this.camera || this.particles.length === 0) return;
|
|
431
|
+
|
|
432
|
+
const cx = this.game.width / 2;
|
|
433
|
+
const cy = this.game.height / 2;
|
|
434
|
+
const baseScale = this.game.baseScale ?? Math.min(this.game.width, this.game.height);
|
|
435
|
+
const renderList = this.buildRenderList();
|
|
436
|
+
|
|
437
|
+
Painter.useCtx((ctx) => {
|
|
438
|
+
// Reset transform (bypass Scene3D transforms)
|
|
439
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
440
|
+
|
|
441
|
+
for (const item of renderList) {
|
|
442
|
+
const { r, g, b } = item.color;
|
|
443
|
+
const size = baseScale * 0.003 * item.scale;
|
|
444
|
+
if (size < 0.1) continue;
|
|
445
|
+
|
|
446
|
+
// Apply Doppler brightness
|
|
447
|
+
const dr = Math.min(255, Math.round(r * item.doppler));
|
|
448
|
+
const dg = Math.min(255, Math.round(g * item.doppler * 0.95));
|
|
449
|
+
const db = Math.min(255, Math.round(b * item.doppler * 0.9));
|
|
450
|
+
|
|
451
|
+
const finalAlpha = Math.max(0, Math.min(1, item.alpha * item.doppler));
|
|
452
|
+
|
|
453
|
+
// Core particle
|
|
454
|
+
ctx.fillStyle = `rgba(${dr}, ${dg}, ${db}, ${finalAlpha})`;
|
|
455
|
+
ctx.beginPath();
|
|
456
|
+
ctx.arc(cx + item.x, cy + item.y, size / 2, 0, Math.PI * 2);
|
|
457
|
+
ctx.fill();
|
|
458
|
+
|
|
459
|
+
// Additive glow for bright/close particles (from blackhole.js)
|
|
460
|
+
if (item.doppler > 1.1 && item.alpha > 0.5) {
|
|
461
|
+
ctx.globalCompositeOperation = "screen";
|
|
462
|
+
ctx.fillStyle = `rgba(${dr}, ${dg}, ${db}, ${finalAlpha * 0.4})`;
|
|
463
|
+
ctx.beginPath();
|
|
464
|
+
ctx.arc(cx + item.x, cy + item.y, size, 0, Math.PI * 2);
|
|
465
|
+
ctx.fill();
|
|
466
|
+
ctx.globalCompositeOperation = "source-over";
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { GameObject, Sphere3D, Painter, Easing } from "/gcanvas.es.min.js";
|
|
2
|
+
import { CONFIG } from "./config.js";
|
|
3
|
+
|
|
4
|
+
export class BlackHole extends GameObject {
|
|
5
|
+
constructor(game, options = {}) {
|
|
6
|
+
super(game, options);
|
|
7
|
+
this.mass = options.initialMass ?? CONFIG.blackHole.initialMass;
|
|
8
|
+
this.baseRadius = game.baseScale ? game.baseScale * CONFIG.bhRadiusRatio : 50;
|
|
9
|
+
this.currentRadius = this.baseRadius;
|
|
10
|
+
|
|
11
|
+
// Awakening state - BH starts dormant, wakes up as it feeds
|
|
12
|
+
this.awakeningLevel = 0; // 0 = dormant (pure black), 1 = fully awake
|
|
13
|
+
this.feedingPulse = 0; // Temporary glow boost when consuming
|
|
14
|
+
this.totalConsumed = 0; // Track total mass consumed
|
|
15
|
+
|
|
16
|
+
// Dynamic growth animation
|
|
17
|
+
this.growthSpurt = 0; // Overshoot when consuming (decays to 0)
|
|
18
|
+
this.breathPhase = 0; // Oscillation phase for breathing effect
|
|
19
|
+
this.targetRadius = this.baseRadius; // Smooth radius target
|
|
20
|
+
|
|
21
|
+
// Rotation - black holes spin!
|
|
22
|
+
this.rotation = 0;
|
|
23
|
+
this.rotationSpeed = options.rotationSpeed ?? 2.9; // Slow, ominous spin
|
|
24
|
+
|
|
25
|
+
// Use WebGL shaders for rendering
|
|
26
|
+
this.useShader = options.useShader ?? true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
init() {
|
|
30
|
+
this.updateVisual();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Add mass from consumed particles - triggers awakening and pulse
|
|
35
|
+
*
|
|
36
|
+
* @param {number} amount - Amount of mass to add
|
|
37
|
+
*
|
|
38
|
+
* Note: Feeding pulse is only triggered before the stable phase.
|
|
39
|
+
* Once stabilizing, particles can still be consumed but won't
|
|
40
|
+
* cause the visual pulse effect.
|
|
41
|
+
*/
|
|
42
|
+
addConsumedMass(amount) {
|
|
43
|
+
this.totalConsumed += amount;
|
|
44
|
+
|
|
45
|
+
// Skip pulse effects if we're in the stable phase
|
|
46
|
+
if (this.isStabilizing) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Awakening increases as BH feeds (slow ramp up)
|
|
51
|
+
const awakeningProgress = Math.min(1, this.totalConsumed * 0.1);
|
|
52
|
+
this.awakeningLevel = Math.max(this.awakeningLevel, awakeningProgress);
|
|
53
|
+
|
|
54
|
+
// Feeding pulse - temporary glow boost
|
|
55
|
+
this.feedingPulse = Math.min(1, this.feedingPulse + amount * 0.2);
|
|
56
|
+
|
|
57
|
+
// Growth spurt - overshoot effect when consuming
|
|
58
|
+
// More dramatic spurts as awakening increases
|
|
59
|
+
const spurtIntensity = 0.03 + this.awakeningLevel * 0.05;
|
|
60
|
+
this.growthSpurt = Math.min(0.15, this.growthSpurt + amount * spurtIntensity);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reset to dormant state
|
|
65
|
+
*/
|
|
66
|
+
resetAwakening() {
|
|
67
|
+
this.awakeningLevel = 0;
|
|
68
|
+
this.feedingPulse = 0;
|
|
69
|
+
this.totalConsumed = 0;
|
|
70
|
+
this.rotation = 0;
|
|
71
|
+
this.growthSpurt = 0;
|
|
72
|
+
this.breathPhase = 0;
|
|
73
|
+
this.isStabilizing = false; // Reset stabilization state
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
updateVisual() {
|
|
77
|
+
// Calculate how much mass has been absorbed (0 = none, 1 = full star)
|
|
78
|
+
const massAbsorbed = Math.max(0, this.mass - CONFIG.blackHole.initialMass);
|
|
79
|
+
const absorptionProgress = massAbsorbed / CONFIG.star.initialMass;
|
|
80
|
+
|
|
81
|
+
// Apply easing to make growth feel more organic
|
|
82
|
+
// easeOutCubic: rapid initial growth that slows as it fills
|
|
83
|
+
const easedProgress = Easing.easeOutCubic(absorptionProgress);
|
|
84
|
+
|
|
85
|
+
// Base radius from absorption with easing
|
|
86
|
+
const baseScale = this.baseRadius / CONFIG.bhRadiusRatio;
|
|
87
|
+
const radiusFraction = CONFIG.bhRadiusRatio +
|
|
88
|
+
easedProgress * (CONFIG.bhFinalRadiusRatio - CONFIG.bhRadiusRatio);
|
|
89
|
+
this.targetRadius = baseScale * radiusFraction;
|
|
90
|
+
|
|
91
|
+
// Breathing oscillation - subtle when dormant, stronger when awake
|
|
92
|
+
const breathAmplitude = 0.01 + this.awakeningLevel * 0.02 + this.feedingPulse * 0.03;
|
|
93
|
+
const breathSpeed = 1.5 + this.awakeningLevel * 0.5; // Faster when active
|
|
94
|
+
const breathOffset = Math.sin(this.breathPhase * breathSpeed) * breathAmplitude;
|
|
95
|
+
|
|
96
|
+
// Growth spurt overshoot effect (elastic rebound)
|
|
97
|
+
const spurtOffset = this.growthSpurt * (1 + Math.sin(this.breathPhase * 8) * 0.3);
|
|
98
|
+
|
|
99
|
+
// Combine all effects
|
|
100
|
+
this.currentRadius = this.targetRadius * (1 + breathOffset + spurtOffset);
|
|
101
|
+
|
|
102
|
+
if (this.currentRadius <= 0) {
|
|
103
|
+
this.currentRadius = this.baseRadius;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Edge brightness increases with awakening
|
|
107
|
+
const awakeFactor = this.awakeningLevel;
|
|
108
|
+
const pulseFactor = this.feedingPulse;
|
|
109
|
+
|
|
110
|
+
// For Canvas 2D fallback - gradient rendering
|
|
111
|
+
// Dormant: pure black edges (#101010)
|
|
112
|
+
// Awake: warmer edges with hint of orange/red glow
|
|
113
|
+
const edgeBase = 16 + Math.round(awakeFactor * 24 + pulseFactor * 16); // 16-56
|
|
114
|
+
const edgeR = Math.min(255, edgeBase + Math.round(awakeFactor * 40 + pulseFactor * 60));
|
|
115
|
+
const edgeG = Math.min(255, edgeBase + Math.round(awakeFactor * 20 + pulseFactor * 30));
|
|
116
|
+
const edgeB = edgeBase;
|
|
117
|
+
|
|
118
|
+
const midBase = 8 + Math.round(awakeFactor * 12 + pulseFactor * 8);
|
|
119
|
+
const midR = Math.min(255, midBase + Math.round(awakeFactor * 20 + pulseFactor * 30));
|
|
120
|
+
const midG = Math.min(255, midBase + Math.round(awakeFactor * 10 + pulseFactor * 15));
|
|
121
|
+
const midB = midBase;
|
|
122
|
+
|
|
123
|
+
const gradient = Painter.colors.radialGradient(
|
|
124
|
+
0, 0, 0.01 * this.currentRadius,
|
|
125
|
+
0, 0, this.currentRadius,
|
|
126
|
+
[
|
|
127
|
+
{ offset: 0, color: "#000" },
|
|
128
|
+
{ offset: 0.5, color: "#000" },
|
|
129
|
+
{ offset: 0.85, color: `rgb(${midR}, ${midG}, ${midB})` },
|
|
130
|
+
{ offset: 1, color: `rgb(${edgeR}, ${edgeG}, ${edgeB})` },
|
|
131
|
+
]
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (!this.core) {
|
|
135
|
+
this.core = new Sphere3D(this.currentRadius, {
|
|
136
|
+
color: gradient,
|
|
137
|
+
camera: this.game.camera,
|
|
138
|
+
stroke: null, // No wireframe
|
|
139
|
+
debug: false,
|
|
140
|
+
segments: 32, // Smoother sphere
|
|
141
|
+
// WebGL shader options
|
|
142
|
+
useShader: this.useShader,
|
|
143
|
+
shaderType: "blackHole",
|
|
144
|
+
shaderUniforms: {
|
|
145
|
+
uAwakeningLevel: awakeFactor,
|
|
146
|
+
uFeedingPulse: pulseFactor,
|
|
147
|
+
uRotation: this.rotation,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
this.core.radius = this.currentRadius;
|
|
152
|
+
this.core.color = gradient; // Keep gradient for Canvas 2D fallback
|
|
153
|
+
// Update shader uniforms
|
|
154
|
+
if (this.core.useShader) {
|
|
155
|
+
this.core.setShaderUniforms({
|
|
156
|
+
uAwakeningLevel: awakeFactor,
|
|
157
|
+
uFeedingPulse: pulseFactor,
|
|
158
|
+
uRotation: this.rotation,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
this.core._generateGeometry();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
update(dt) {
|
|
166
|
+
super.update(dt);
|
|
167
|
+
|
|
168
|
+
// Animate breathing phase
|
|
169
|
+
this.breathPhase += dt * Math.PI * 2; // Full cycle per second
|
|
170
|
+
|
|
171
|
+
// Spin the black hole - rotation speeds up when feeding
|
|
172
|
+
const spinMultiplier = 1 + this.feedingPulse * 2 + this.awakeningLevel * 0.5;
|
|
173
|
+
this.rotation += this.rotationSpeed * spinMultiplier * dt;
|
|
174
|
+
|
|
175
|
+
// Decay feeding pulse over time
|
|
176
|
+
if (this.feedingPulse > 0) {
|
|
177
|
+
this.feedingPulse = Math.max(0, this.feedingPulse - dt * 1.5);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Decay growth spurt with elastic damping
|
|
181
|
+
if (this.growthSpurt > 0) {
|
|
182
|
+
// Fast initial decay, slows down (feels like elastic settling)
|
|
183
|
+
const decayRate = 3 + this.growthSpurt * 5; // Faster when larger
|
|
184
|
+
this.growthSpurt = Math.max(0, this.growthSpurt - dt * decayRate);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Decay awakening level when stabilizing (slow cool-down)
|
|
188
|
+
// Minimum level is 0.3 - never goes fully dormant after feeding
|
|
189
|
+
if (this.isStabilizing && this.awakeningLevel > 0.3) {
|
|
190
|
+
this.awakeningLevel = Math.max(0.3, this.awakeningLevel - dt * 0.15);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this.updateVisual();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Start the stabilization phase - black hole calms down
|
|
198
|
+
*/
|
|
199
|
+
startStabilizing() {
|
|
200
|
+
this.isStabilizing = true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reset stabilization state
|
|
205
|
+
*/
|
|
206
|
+
resetStabilizing() {
|
|
207
|
+
this.isStabilizing = false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
onResize(baseRadius) {
|
|
211
|
+
this.baseRadius = baseRadius;
|
|
212
|
+
this.updateVisual();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
render() {
|
|
216
|
+
super.render();
|
|
217
|
+
this.core.render();
|
|
218
|
+
}
|
|
219
|
+
}
|