<div class="blackhole-container" style="width:1000px;height:1000px;position:relative;background-color:#000">
<canvas style="position:absolute;top:0;left:0;width:100%;height:100%"></canvas>
<script>
// Self-executing function to encapsulate the code and keep it from interfering with other scripts.
(function() {
// This makes the element self-contained. It finds the canvas inside its own container.
const container = document.currentScript.parentElement;
const canvas = container.querySelector('canvas');
if (!container || !canvas) return;
const ctx = canvas.getContext('2d');
let particles = [];
// Using the particle count from your file.
const particleCount = 50000;
// --- Optimized Camera Settings ---
// The camera angle is fixed, so we pre-calculate the sine and cosine once,
// instead of in every frame. This is much more efficient.
const angleX = Math.PI / 2;
const cosX = Math.cos(angleX);
const sinX = Math.sin(angleX);
// These were the values from your file.
const singularityRadius = 100;
const photonSphereRadius = 100;
class Particle {
constructor(diskType) {
this.diskType = diskType; // 'main' or 'perpendicular'
this.reset();
}
reset() {
let radius;
// Using the exact size parameters from your file.
if (this.diskType === 'main') {
radius = photonSphereRadius + Math.random() * (350 / 25);
} else {
radius = photonSphereRadius + Math.random() * 350;
}
const angle = Math.random() * Math.PI * 2;
const tubeRadius = 15 * (1 - Math.pow(radius / 450, 2));
const tubeAngle = Math.random() * Math.PI * 2;
if (this.diskType === 'main') {
this.x = (radius + Math.cos(tubeAngle) * tubeRadius) * Math.cos(angle);
this.z = (radius + Math.cos(tubeAngle) * tubeRadius) * Math.sin(angle);
this.y = Math.sin(tubeAngle) * tubeRadius;
} else {
this.x = (radius + Math.cos(tubeAngle) * tubeRadius) * Math.cos(angle);
this.y = (radius + Math.cos(tubeAngle) * tubeRadius) * Math.sin(angle);
this.z = Math.sin(tubeAngle) * tubeRadius;
}
this.speed = (0.001 + (1 / radius) * 0.2) * 10;
this.angle = angle;
this.radius = radius;
this.life = Math.random() * 200 + 100;
this.age = 0;
}
update() {
this.age++;
this.angle += this.speed;
if (this.diskType === 'main') {
this.x = this.radius * Math.cos(this.angle);
this.z = this.radius * Math.sin(this.angle);
} else {
this.x = this.radius * Math.cos(this.angle);
this.y = this.radius * Math.sin(this.angle);
}
if (this.age >= this.life || this.radius < photonSphereRadius) {
this.reset();
}
}
project(rotX, rotY, rotZ) {
const fov = 1000;
let dist = fov + rotZ;
this.isBehind = rotZ > 0;
if (dist > 0) {
this.projectedX = (rotX * fov) / dist + centerX;
this.projectedY = (rotY * fov) / dist + centerY;
// Gravitational Lensing effect from your code.
if (this.isBehind && this.diskType === 'perpendicular') {
const dist2D = Math.sqrt(Math.pow(this.projectedX - centerX, 2) + Math.pow(this.projectedY - centerY, 2));
if (dist2D < photonSphereRadius * 3) {
const pullFactor = Math.pow(photonSphereRadius / dist2D, 2);
this.projectedY += (centerY - this.projectedY) * -pullFactor * 1.5;
}
}
this.size = Math.max(0, (2 - rotZ / 500) * 0.6);
} else {
this.size = -1;
}
}
draw() {
if (this.size <= 0) return;
const opacity = (1 - (this.age / this.life)) * 0.9;
ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`;
ctx.fillRect(this.projectedX, this.projectedY, this.size, this.size);
}
}
let centerX, centerY;
function init() {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
centerX = canvas.width / 2;
centerY = canvas.height / 2;
particles = [];
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle(i < particleCount / 2 ? 'main' : 'perpendicular'));
}
animate();
}
function drawSingularity() {
ctx.beginPath();
ctx.arc(centerX, centerY, singularityRadius, 0, Math.PI * 2);
ctx.fillStyle = '#000';
ctx.fill();
}
function animate() {
requestAnimationFrame(animate);
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const foregroundParticles = [];
const backgroundParticles = [];
particles.forEach(p => {
p.update();
// --- Simplified Rotation Logic ---
// This replaces the complex camera calculations with a much faster version
// that produces the exact same result for the fixed camera angle.
const y_rotated = p.y * cosX - p.z * sinX;
const z_rotated = p.y * sinX + p.z * cosX;
// No Y-axis rotation is needed since the camera is static.
const x_final = p.x;
const y_final = y_rotated;
const z_final = z_rotated;
p.project(x_final, y_final, z_final);
if (p.isBehind) backgroundParticles.push(p);
else foregroundParticles.push(p);
});
ctx.shadowBlur = 8;
ctx.shadowColor = "rgba(255, 255, 255, 0.2)";
backgroundParticles.forEach(p => p.draw());
ctx.shadowBlur = 0;
drawSingularity();
ctx.shadowBlur = 8;
ctx.shadowColor = "rgba(255, 255, 255, 0.2)";
foregroundParticles.forEach(p => p.draw());
ctx.shadowBlur = 0;
}
init();
})();
</script>
</div>