| <!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"> |
| |
| <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"> |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| const canvas = document.getElementById('simulationCanvas'); |
| const ctx = canvas.getContext('2d'); |
| let width = canvas.width; |
| let height = canvas.height; |
| |
| |
| 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(); |
| |
| |
| let timeScale = 1; |
| let trailLength = 200; |
| let isPaused = false; |
| |
| |
| let particles = []; |
| let blackHoles = []; |
| let trails = []; |
| |
| |
| let lastTime = 0; |
| let fps = 0; |
| let frameCount = 0; |
| let lastFpsUpdate = 0; |
| |
| |
| 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'); |
| |
| |
| 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.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); |
| }); |
| |
| |
| 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; |
| |
| |
| 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; |
| |
| |
| draggedParticle.vx = (x - dragStartX) * 0.1; |
| draggedParticle.vy = (y - dragStartY) * 0.1; |
| |
| |
| draggedParticle.x = x; |
| draggedParticle.y = y; |
| |
| dragStartX = x; |
| dragStartY = y; |
| } |
| }); |
| |
| canvas.addEventListener('mouseup', () => { |
| draggedParticle = null; |
| }); |
| |
| canvas.addEventListener('mouseleave', () => { |
| draggedParticle = null; |
| }); |
| |
| |
| 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; |
| |
| |
| 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); |
| |
| |
| 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; |
| |
| |
| for (let i = particles.length - 1; i >= 0; i--) { |
| const p = particles[i]; |
| |
| |
| let ax = 0; |
| let ay = 0; |
| let absorbed = false; |
| |
| |
| for (const bh of blackHoles) { |
| const { fx, fy, absorbed: isAbsorbed } = calculateGravity(p, bh); |
| ax += fx / p.mass; |
| ay += fy / p.mass; |
| absorbed = absorbed || isAbsorbed; |
| } |
| |
| |
| if (absorbed) { |
| particles.splice(i, 1); |
| continue; |
| } |
| |
| |
| p.vx += ax * scaledDeltaTime; |
| p.vy += ay * scaledDeltaTime; |
| |
| |
| p.x += p.vx * scaledDeltaTime; |
| p.y += p.vy * scaledDeltaTime; |
| |
| |
| p.trail.push({ x: p.x, y: p.y }); |
| if (p.trail.length > trailLength) { |
| p.trail.shift(); |
| } |
| |
| |
| 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() { |
| |
| ctx.clearRect(0, 0, width, height); |
| |
| |
| 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(); |
| } |
| } |
| |
| |
| 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(); |
| |
| |
| 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(); |
| } |
| |
| |
| for (const bh of blackHoles) { |
| |
| 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(); |
| |
| |
| 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(); |
| |
| |
| 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) { |
| |
| const deltaTime = timestamp - lastTime; |
| lastTime = timestamp; |
| |
| |
| 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); |
| } |
| |
| |
| function initSimulation() { |
| |
| addBlackHole(width / 2, height / 2, 2000, 15, 2); |
| |
| |
| addParticleCluster(20, 5, 3, 2); |
| |
| |
| requestAnimationFrame(gameLoop); |
| } |
| |
| |
| 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> |