game / index.html
loes12zu's picture
Add 2 files
9460d72 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Black Hole Particle Simulator</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script>
<style>
canvas {
background-color: #0f172a;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.particle {
position: absolute;
border-radius: 50%;
pointer-events: none;
}
.black-hole {
position: absolute;
border-radius: 50%;
pointer-events: none;
box-shadow: 0 0 15px 5px rgba(255, 255, 255, 0.3);
}
.event-horizon {
position: absolute;
border-radius: 50%;
border: 1px dashed rgba(255, 255, 255, 0.5);
pointer-events: none;
}
.trail {
position: absolute;
border-radius: 50%;
pointer-events: none;
opacity: 0.6;
}
.slider-thumb::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #3b82f6;
cursor: pointer;
}
.slider-thumb::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #3b82f6;
cursor: pointer;
}
</style>
</head>
<body class="bg-gray-900 text-white min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="mb-8 text-center">
<h1 class="text-4xl font-bold text-blue-400 mb-2">Black Hole Particle Simulator</h1>
<p class="text-gray-300">Explore gravitational interactions between particles and black holes</p>
</header>
<div class="flex flex-col lg:flex-row gap-6">
<!-- Controls Panel -->
<div class="w-full lg:w-1/4 bg-gray-800 p-6 rounded-lg shadow-lg">
<h2 class="text-xl font-semibold mb-4 text-blue-300">Controls</h2>
<div class="space-y-6">
<!-- Simulation Controls -->
<div>
<h3 class="text-lg font-medium mb-2 text-gray-300">Simulation</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Time Scale</label>
<input type="range" id="timeScale" min="0.1" max="5" step="0.1" value="1" class="w-full slider-thumb">
<div class="flex justify-between text-xs text-gray-400">
<span>0.1x</span>
<span id="timeScaleValue">1.0x</span>
<span>5.0x</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Trail Length</label>
<input type="range" id="trailLength" min="0" max="1000" step="10" value="200" class="w-full slider-thumb">
<div class="flex justify-between text-xs text-gray-400">
<span>0</span>
<span id="trailLengthValue">200</span>
<span>1000</span>
</div>
</div>
<div class="flex space-x-2">
<button id="pauseBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded flex-1">
<i class="fas fa-pause mr-2"></i> Pause
</button>
<button id="resetBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded flex-1">
<i class="fas fa-redo mr-2"></i> Reset
</button>
</div>
</div>
</div>
<!-- Black Hole Controls -->
<div>
<h3 class="text-lg font-medium mb-2 text-gray-300">Black Hole</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Gravity Constant</label>
<input type="range" id="bhGravity" min="0.1" max="10" step="0.1" value="2" class="w-full slider-thumb">
<div class="flex justify-between text-xs text-gray-400">
<span>0.1</span>
<span id="bhGravityValue">2.0</span>
<span>10.0</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Mass</label>
<input type="range" id="bhMass" min="100" max="10000" step="100" value="2000" class="w-full slider-thumb">
<div class="flex justify-between text-xs text-gray-400">
<span>100</span>
<span id="bhMassValue">2000</span>
<span>10000</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Size</label>
<input type="range" id="bhSize" min="5" max="50" step="1" value="15" class="w-full slider-thumb">
<div class="flex justify-between text-xs text-gray-400">
<span>5</span>
<span id="bhSizeValue">15</span>
<span>50</span>
</div>
</div>
<button id="addBlackHoleBtn" class="w-full bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded">
<i class="fas fa-plus-circle mr-2"></i> Add Black Hole
</button>
</div>
</div>
<!-- Particle Controls -->
<div>
<h3 class="text-lg font-medium mb-2 text-gray-300">Particle</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium mb-1">Mass</label>
<input type="range" id="particleMass" min="1" max="100" step="1" value="5" class="w-full slider-thumb">
<div class="flex justify-between text-xs text-gray-400">
<span>1</span>
<span id="particleMassValue">5</span>
<span>100</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Size</label>
<input type="range" id="particleSize" min="1" max="10" step="1" value="3" class="w-full slider-thumb">
<div class="flex justify-between text-xs text-gray-400">
<span>1</span>
<span id="particleSizeValue">3</span>
<span>10</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Initial Speed</label>
<input type="range" id="particleSpeed" min="0" max="10" step="0.1" value="2" class="w-full slider-thumb">
<div class="flex justify-between text-xs text-gray-400">
<span>0</span>
<span id="particleSpeedValue">2.0</span>
<span>10.0</span>
</div>
</div>
<div>
<label class="block text-sm font-medium mb-1">Count</label>
<input type="range" id="particleCount" min="1" max="100" step="1" value="20" class="w-full slider-thumb">
<div class="flex justify-between text-xs text-gray-400">
<span>1</span>
<span id="particleCountValue">20</span>
<span>100</span>
</div>
</div>
<button id="addParticlesBtn" class="w-full bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded">
<i class="fas fa-atom mr-2"></i> Add Particles
</button>
</div>
</div>
</div>
</div>
<!-- Simulation Canvas -->
<div class="w-full lg:w-3/4">
<div class="relative">
<canvas id="simulationCanvas" width="800" height="600" class="w-full"></canvas>
<div id="stats" class="absolute top-2 left-2 bg-black bg-opacity-50 p-2 rounded text-sm">
Particles: <span id="particleCountDisplay">0</span> |
Black Holes: <span id="blackHoleCountDisplay">0</span> |
FPS: <span id="fpsDisplay">0</span>
</div>
</div>
<div class="mt-4 bg-gray-800 p-4 rounded-lg">
<h3 class="text-lg font-medium mb-2 text-blue-300">Instructions</h3>
<ul class="list-disc pl-5 space-y-1 text-gray-300">
<li>Click "Add Black Hole" to place a black hole at a random position</li>
<li>Click "Add Particles" to generate a cluster of particles</li>
<li>Click on the canvas to place a black hole at that position</li>
<li>Drag particles to give them initial velocity</li>
<li>Adjust sliders to change simulation parameters</li>
</ul>
</div>
</div>
</div>
</div>
<script>
// Simulation variables
const canvas = document.getElementById('simulationCanvas');
const ctx = canvas.getContext('2d');
let width = canvas.width;
let height = canvas.height;
// Resize canvas when window resizes
function resizeCanvas() {
const container = canvas.parentElement;
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
width = canvas.width;
height = canvas.height;
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
// Physics parameters
let timeScale = 1;
let trailLength = 200;
let isPaused = false;
// Objects in simulation
let particles = [];
let blackHoles = [];
let trails = [];
// Stats
let lastTime = 0;
let fps = 0;
let frameCount = 0;
let lastFpsUpdate = 0;
// Control elements
const timeScaleSlider = document.getElementById('timeScale');
const timeScaleValue = document.getElementById('timeScaleValue');
const trailLengthSlider = document.getElementById('trailLength');
const trailLengthValue = document.getElementById('trailLengthValue');
const pauseBtn = document.getElementById('pauseBtn');
const resetBtn = document.getElementById('resetBtn');
const bhGravitySlider = document.getElementById('bhGravity');
const bhGravityValue = document.getElementById('bhGravityValue');
const bhMassSlider = document.getElementById('bhMass');
const bhMassValue = document.getElementById('bhMassValue');
const bhSizeSlider = document.getElementById('bhSize');
const bhSizeValue = document.getElementById('bhSizeValue');
const addBlackHoleBtn = document.getElementById('addBlackHoleBtn');
const particleMassSlider = document.getElementById('particleMass');
const particleMassValue = document.getElementById('particleMassValue');
const particleSizeSlider = document.getElementById('particleSize');
const particleSizeValue = document.getElementById('particleSizeValue');
const particleSpeedSlider = document.getElementById('particleSpeed');
const particleSpeedValue = document.getElementById('particleSpeedValue');
const particleCountSlider = document.getElementById('particleCount');
const particleCountValue = document.getElementById('particleCountValue');
const addParticlesBtn = document.getElementById('addParticlesBtn');
const particleCountDisplay = document.getElementById('particleCountDisplay');
const blackHoleCountDisplay = document.getElementById('blackHoleCountDisplay');
const fpsDisplay = document.getElementById('fpsDisplay');
// Event listeners for controls
timeScaleSlider.addEventListener('input', () => {
timeScale = parseFloat(timeScaleSlider.value);
timeScaleValue.textContent = timeScale.toFixed(1) + 'x';
});
trailLengthSlider.addEventListener('input', () => {
trailLength = parseInt(trailLengthSlider.value);
trailLengthValue.textContent = trailLength;
});
pauseBtn.addEventListener('click', () => {
isPaused = !isPaused;
pauseBtn.innerHTML = isPaused ?
'<i class="fas fa-play mr-2"></i> Play' :
'<i class="fas fa-pause mr-2"></i> Pause';
});
resetBtn.addEventListener('click', resetSimulation);
bhGravitySlider.addEventListener('input', () => {
const value = parseFloat(bhGravitySlider.value);
bhGravityValue.textContent = value.toFixed(1);
blackHoles.forEach(bh => bh.gravityConstant = value);
});
bhMassSlider.addEventListener('input', () => {
const value = parseInt(bhMassSlider.value);
bhMassValue.textContent = value;
blackHoles.forEach(bh => bh.mass = value);
});
bhSizeSlider.addEventListener('input', () => {
const value = parseInt(bhSizeSlider.value);
bhSizeValue.textContent = value;
blackHoles.forEach(bh => bh.radius = value);
});
addBlackHoleBtn.addEventListener('click', () => {
const gravity = parseFloat(bhGravitySlider.value);
const mass = parseInt(bhMassSlider.value);
const size = parseInt(bhSizeSlider.value);
const x = Math.random() * (width - 100) + 50;
const y = Math.random() * (height - 100) + 50;
addBlackHole(x, y, mass, size, gravity);
});
particleMassSlider.addEventListener('input', () => {
const value = parseInt(particleMassSlider.value);
particleMassValue.textContent = value;
});
particleSizeSlider.addEventListener('input', () => {
const value = parseInt(particleSizeSlider.value);
particleSizeValue.textContent = value;
});
particleSpeedSlider.addEventListener('input', () => {
const value = parseFloat(particleSpeedSlider.value);
particleSpeedValue.textContent = value.toFixed(1);
});
particleCountSlider.addEventListener('input', () => {
const value = parseInt(particleCountSlider.value);
particleCountValue.textContent = value;
});
addParticlesBtn.addEventListener('click', () => {
const mass = parseInt(particleMassSlider.value);
const size = parseInt(particleSizeSlider.value);
const speed = parseFloat(particleSpeedSlider.value);
const count = parseInt(particleCountSlider.value);
addParticleCluster(count, mass, size, speed);
});
// Canvas interaction
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const gravity = parseFloat(bhGravitySlider.value);
const mass = parseInt(bhMassSlider.value);
const size = parseInt(bhSizeSlider.value);
addBlackHole(x, y, mass, size, gravity);
});
// Particle dragging
let draggedParticle = null;
let dragStartX = 0;
let dragStartY = 0;
canvas.addEventListener('mousedown', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Find the closest particle
let closestDist = Infinity;
let closestParticle = null;
for (const particle of particles) {
const dist = Math.sqrt((x - particle.x) ** 2 + (y - particle.y) ** 2);
if (dist < particle.radius + 10 && dist < closestDist) {
closestDist = dist;
closestParticle = particle;
}
}
if (closestParticle) {
draggedParticle = closestParticle;
dragStartX = x;
dragStartY = y;
}
});
canvas.addEventListener('mousemove', (e) => {
if (draggedParticle) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Calculate velocity based on drag distance
draggedParticle.vx = (x - dragStartX) * 0.1;
draggedParticle.vy = (y - dragStartY) * 0.1;
// Update position
draggedParticle.x = x;
draggedParticle.y = y;
dragStartX = x;
dragStartY = y;
}
});
canvas.addEventListener('mouseup', () => {
draggedParticle = null;
});
canvas.addEventListener('mouseleave', () => {
draggedParticle = null;
});
// Simulation functions
function resetSimulation() {
particles = [];
blackHoles = [];
trails = [];
updateStats();
}
function addBlackHole(x, y, mass, radius, gravityConstant) {
blackHoles.push({
x,
y,
mass,
radius,
gravityConstant,
color: '#9d00ff'
});
updateStats();
}
function addParticle(x, y, mass, radius, vx = 0, vy = 0) {
particles.push({
x,
y,
mass,
radius,
vx,
vy,
color: `hsl(${Math.random() * 60 + 180}, 80%, 60%)`,
trail: []
});
updateStats();
}
function addParticleCluster(count, mass, radius, speed) {
const centerX = width / 2;
const centerY = height / 2;
const clusterRadius = Math.min(width, height) * 0.3;
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * clusterRadius;
const x = centerX + Math.cos(angle) * distance;
const y = centerY + Math.sin(angle) * distance;
// Give particles some initial velocity (tangential to center)
const vx = -Math.sin(angle) * speed;
const vy = Math.cos(angle) * speed;
addParticle(x, y, mass, radius, vx, vy);
}
}
function updateStats() {
particleCountDisplay.textContent = particles.length;
blackHoleCountDisplay.textContent = blackHoles.length;
}
function calculateGravity(particle, blackHole) {
const dx = blackHole.x - particle.x;
const dy = blackHole.y - particle.y;
const distSq = dx * dx + dy * dy;
const dist = Math.sqrt(distSq);
// Avoid division by zero and extreme forces at very small distances
if (dist < blackHole.radius) {
return { fx: 0, fy: 0, absorbed: true };
}
const forceMagnitude = blackHole.gravityConstant * blackHole.mass * particle.mass / distSq;
const fx = forceMagnitude * dx / dist;
const fy = forceMagnitude * dy / dist;
return { fx, fy, absorbed: false };
}
function updateSimulation(deltaTime) {
if (isPaused) return;
const scaledDeltaTime = deltaTime * timeScale;
// Update particles
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
// Reset acceleration
let ax = 0;
let ay = 0;
let absorbed = false;
// Calculate forces from all black holes
for (const bh of blackHoles) {
const { fx, fy, absorbed: isAbsorbed } = calculateGravity(p, bh);
ax += fx / p.mass;
ay += fy / p.mass;
absorbed = absorbed || isAbsorbed;
}
// Remove absorbed particles
if (absorbed) {
particles.splice(i, 1);
continue;
}
// Update velocity
p.vx += ax * scaledDeltaTime;
p.vy += ay * scaledDeltaTime;
// Update position
p.x += p.vx * scaledDeltaTime;
p.y += p.vy * scaledDeltaTime;
// Add to trail
p.trail.push({ x: p.x, y: p.y });
if (p.trail.length > trailLength) {
p.trail.shift();
}
// Bounce off walls (optional)
const bounce = 0.8;
if (p.x - p.radius < 0) {
p.x = p.radius;
p.vx = -p.vx * bounce;
}
if (p.x + p.radius > width) {
p.x = width - p.radius;
p.vx = -p.vx * bounce;
}
if (p.y - p.radius < 0) {
p.y = p.radius;
p.vy = -p.vy * bounce;
}
if (p.y + p.radius > height) {
p.y = height - p.radius;
p.vy = -p.vy * bounce;
}
}
}
function renderSimulation() {
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw trails
for (const p of particles) {
if (p.trail.length > 1) {
ctx.beginPath();
ctx.moveTo(p.trail[0].x, p.trail[0].y);
for (let i = 1; i < p.trail.length; i++) {
const point = p.trail[i];
ctx.lineTo(point.x, point.y);
}
ctx.strokeStyle = p.color.replace(')', ', 0.3)').replace('hsl', 'hsla');
ctx.lineWidth = 1;
ctx.stroke();
}
}
// Draw particles
for (const p of particles) {
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fillStyle = p.color;
ctx.fill();
// Glow effect
const gradient = ctx.createRadialGradient(
p.x, p.y, p.radius,
p.x, p.y, p.radius * 2
);
gradient.addColorStop(0, p.color);
gradient.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = gradient;
ctx.fill();
}
// Draw black holes
for (const bh of blackHoles) {
// Event horizon (gravitational influence)
const influenceRadius = Math.sqrt(bh.mass * bh.gravityConstant) * 5;
ctx.beginPath();
ctx.arc(bh.x, bh.y, influenceRadius, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
ctx.stroke();
// Black hole itself
const gradient = ctx.createRadialGradient(
bh.x, bh.y, bh.radius * 0.3,
bh.x, bh.y, bh.radius
);
gradient.addColorStop(0, '#000000');
gradient.addColorStop(1, bh.color);
ctx.beginPath();
ctx.arc(bh.x, bh.y, bh.radius, 0, Math.PI * 2);
ctx.fillStyle = gradient;
ctx.fill();
// Accretion disk
ctx.beginPath();
ctx.arc(bh.x, bh.y, bh.radius * 1.5, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(157, 0, 255, 0.5)';
ctx.lineWidth = 2;
ctx.stroke();
ctx.beginPath();
ctx.arc(bh.x, bh.y, bh.radius * 2, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(157, 0, 255, 0.3)';
ctx.lineWidth = 1;
ctx.stroke();
}
}
function gameLoop(timestamp) {
// Calculate delta time
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
// Update FPS counter
frameCount++;
if (timestamp - lastFpsUpdate >= 1000) {
fps = Math.round(frameCount * 1000 / (timestamp - lastFpsUpdate));
frameCount = 0;
lastFpsUpdate = timestamp;
fpsDisplay.textContent = fps;
}
updateSimulation(deltaTime / 1000);
renderSimulation();
requestAnimationFrame(gameLoop);
}
// Initialize with some objects
function initSimulation() {
// Add a central black hole
addBlackHole(width / 2, height / 2, 2000, 15, 2);
// Add some particles
addParticleCluster(20, 5, 3, 2);
// Start the game loop
requestAnimationFrame(gameLoop);
}
// Start the simulation
initSimulation();
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=loes12zu/game" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>