|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>PhasePulse Particles Simulator</title> |
|
|
<link rel="icon" type="image/x-icon" href="/static/favicon.ico"> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
<script src="https://unpkg.com/feather-icons"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
|
|
<script src="https://cdn.jsdelivr.net/npm/p5@1.7.0/lib/p5.min.js"></script> |
|
|
</head> |
|
|
<body class="bg-gray-900 text-white"> |
|
|
<div class="container mx-auto px-4 py-8"> |
|
|
|
|
|
<header class="text-center mb-12"> |
|
|
<h1 class="text-4xl md:text-6xl font-bold bg-gradient-to-r from-purple-500 to-blue-500 bg-clip-text text-transparent mb-4"> |
|
|
PhasePulse Particles 🔬 |
|
|
</h1> |
|
|
<p class="text-lg text-gray-300 max-w-2xl mx-auto"> |
|
|
A dynamic particle simulation where mathematical relationships create beautiful emergent behavior |
|
|
</p> |
|
|
</header> |
|
|
|
|
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
|
|
|
|
|
<div class="lg:col-span-1 bg-gray-800 rounded-xl p-6"> |
|
|
<h2 class="text-2xl font-bold mb-6 flex items-center"> |
|
|
<i data-feather="sliders" class="mr-3"></i> |
|
|
Simulation Controls |
|
|
</h2> |
|
|
|
|
|
<div class="space-y-6"> |
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-medium mb-2">Number of Particles</label> |
|
|
<input type="range" min="10" max="200" value="50" |
|
|
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" |
|
|
id="particleCount"> |
|
|
<div class="flex justify-between text-xs text-gray-400"> |
|
|
<span>10</span> |
|
|
<span id="currentCount">50</span> |
|
|
<span>200</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div> |
|
|
<label class="block text-sm font-medium mb-2">Interaction Distance</label> |
|
|
<div class="grid grid-cols-2 gap-4"> |
|
|
<div> |
|
|
<label class="text-xs text-gray-400">Min Distance</label> |
|
|
<input type="range" min="10" max="100" value="30" |
|
|
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" |
|
|
id="minDistance"> |
|
|
</div> |
|
|
<div> |
|
|
<label class="text-xs text-gray-400">Max Distance</label> |
|
|
<input type="range" min="50" max="300" value="150" |
|
|
class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" |
|
|
id="maxDistance"> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="space-y-4"> |
|
|
<button id="resetSim" class="w-full bg-purple-600 hover:bg-purple-700 text-white py-2 px-4 rounded-lg transition-colors flex items-center justify-center"> |
|
|
<i data-feather="refresh-cw" class="mr-2 w-4 h-4"></i> |
|
|
Reset Simulation |
|
|
</button> |
|
|
<button id="pauseSim" class="w-full bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded-lg transition-colors flex items-center justify-center"> |
|
|
<i data-feather="pause" class="mr-2 w-4 h-4"></i> |
|
|
Pause/Resume |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="bg-gray-700 rounded-lg p-4"> |
|
|
<h3 class="font-semibold mb-2">Simulation Stats</h3> |
|
|
<div class="text-sm space-y-1"> |
|
|
<div class="flex justify-between"> |
|
|
<span>Active Particles:</span> |
|
|
<span id="activeParticles">50</span> |
|
|
</div> |
|
|
<div class="flex justify-between"> |
|
|
<span>Average Phase:</span> |
|
|
<span id="avgPhase">3.14</span> |
|
|
</div> |
|
|
<div class="flex justify-between"> |
|
|
<span>Prime Bonds:</span> |
|
|
<span id="primeBonds">0</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="lg:col-span-2"> |
|
|
<div class="bg-gray-800 rounded-xl p-4"> |
|
|
<div id="simulationCanvas" class="w-full h-96 bg-gray-900 rounded-lg"></div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="mt-6 bg-gray-800 rounded-xl p-6"> |
|
|
<h3 class="text-xl font-bold mb-4 flex items-center"> |
|
|
<i data-feather="info" class="mr-2"></i> |
|
|
How It Works |
|
|
</h3> |
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-300"> |
|
|
<div> |
|
|
<h4 class="font-semibold text-purple-400 mb-2">Phase Dynamics</h4> |
|
|
<p>Particles have phases (0-2π) controlling attraction/repulsion. Phase 0 = maximum attraction, 2π = maximum repulsion.</p> |
|
|
</div> |
|
|
<div> |
|
|
<h4 class="font-semibold text-blue-400 mb-2">Prime Connections</h4> |
|
|
<p>Particles form special bonds with others that share prime factors in their values, creating unique clustering patterns.</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
|
|
|
class ParticleSystem { |
|
|
constructor() { |
|
|
this.particles = []; |
|
|
this.isRunning = true; |
|
|
this.canvas = null; |
|
|
this.ctx = null; |
|
|
this.minDistance = 30; |
|
|
this.maxDistance = 150; |
|
|
|
|
|
this.setupCanvas(); |
|
|
this.initializeParticles(50); |
|
|
this.animate(); |
|
|
} |
|
|
|
|
|
setupCanvas() { |
|
|
const container = document.getElementById('simulationCanvas'); |
|
|
this.canvas = document.createElement('canvas'); |
|
|
this.canvas.width = container.clientWidth; |
|
|
this.canvas.height = container.clientHeight; |
|
|
container.appendChild(this.canvas); |
|
|
this.ctx = this.canvas.getContext('2d'); |
|
|
} |
|
|
|
|
|
initializeParticles(count) { |
|
|
this.particles = []; |
|
|
for (let i = 0; i < count; i++) { |
|
|
this.particles.push({ |
|
|
x: Math.random() * this.canvas.width, |
|
|
y: Math.random() * this.canvas.height, |
|
|
phase: Math.PI + (Math.random() * 0.1 - 0.05), |
|
|
value: this.getRandomValue(), |
|
|
vx: (Math.random() - 0.5) * 2, |
|
|
vy: (Math.random() - 0.5) * 2, |
|
|
connections: [] |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
getRandomValue() { |
|
|
|
|
|
return Math.floor(Math.random() * 99) + 2; |
|
|
} |
|
|
|
|
|
getPrimeFactors(n) { |
|
|
const factors = []; |
|
|
let divisor = 2; |
|
|
while (n >= 2) { |
|
|
if (n % divisor === 0) { |
|
|
factors.push(divisor); |
|
|
n = n / divisor; |
|
|
} else { |
|
|
divisor++; |
|
|
} |
|
|
} |
|
|
return factors; |
|
|
} |
|
|
|
|
|
sharePrimeFactors(a, b) { |
|
|
const factorsA = this.getPrimeFactors(a); |
|
|
const factorsB = this.getPrimeFactors(b); |
|
|
return factorsA.some(factor => factorsB.includes(factor)); |
|
|
} |
|
|
|
|
|
update() { |
|
|
if (!this.isRunning) return; |
|
|
|
|
|
|
|
|
this.particles.forEach(particle => { |
|
|
particle.x += particle.vx; |
|
|
particle.y += particle.vy; |
|
|
|
|
|
|
|
|
if (particle.x < 0 || particle.x > this.canvas.width) particle.vx *= -1; |
|
|
if (particle.y < 0 || particle.y > this.canvas.height) particle.vy *= -1; |
|
|
|
|
|
|
|
|
particle.x = Math.max(0, Math.min(this.canvas.width, particle.x)); |
|
|
particle.y = Math.max(0, Math.min(this.canvas.height, particle.y)); |
|
|
}); |
|
|
|
|
|
|
|
|
this.updatePhases(); |
|
|
} |
|
|
|
|
|
updatePhases() { |
|
|
this.particles.forEach((particle, i) => { |
|
|
let totalPhase = 0; |
|
|
let count = 0; |
|
|
let totalDistance = 0; |
|
|
|
|
|
this.particles.forEach((other, j) => { |
|
|
if (i === j) return; |
|
|
|
|
|
const dx = particle.x - other.x; |
|
|
const dy = particle.y - other.y; |
|
|
const distance = Math.sqrt(dx * dx + dy * dy); |
|
|
|
|
|
if (distance >= this.minDistance && distance <= this.maxDistance) { |
|
|
totalPhase += other.phase; |
|
|
totalDistance += distance; |
|
|
count++; |
|
|
} |
|
|
}); |
|
|
|
|
|
if (count > 0) { |
|
|
const avgPhase = totalPhase / count; |
|
|
const avgDistance = totalDistance / count; |
|
|
const phaseInfluence = (count * avgPhase) / avgDistance; |
|
|
|
|
|
|
|
|
particle.phase += (phaseInfluence - particle.phase) * 0.01; |
|
|
particle.phase = Math.max(0, Math.min(2 * Math.PI, particle.phase)); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
draw() { |
|
|
this.ctx.fillStyle = 'rgb(17, 24, 39)'; |
|
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); |
|
|
|
|
|
|
|
|
this.particles.forEach(particle => { |
|
|
particle.connections = []; |
|
|
this.particles.forEach(other => { |
|
|
if (particle === other) return; |
|
|
|
|
|
const dx = particle.x - other.x; |
|
|
const dy = particle.y - other.y; |
|
|
const distance = Math.sqrt(dx * dx + dy * dy); |
|
|
|
|
|
|
|
|
if (this.sharePrimeFactors(particle.value, other.value) && distance < 100) { |
|
|
particle.connections.push(other); |
|
|
|
|
|
this.ctx.strokeStyle = 'rgba(139, 92, 246, 0.3)'; |
|
|
this.ctx.lineWidth = 2; |
|
|
this.ctx.beginPath(); |
|
|
this.ctx.moveTo(particle.x, particle.y); |
|
|
this.ctx.lineTo(other.x, other.y); |
|
|
this.ctx.stroke(); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
this.particles.forEach(particle => { |
|
|
|
|
|
const hue = (particle.phase / (2 * Math.PI)) * 240; |
|
|
this.ctx.fillStyle = `hsl(${hue}, 70%, 60%)`; |
|
|
|
|
|
|
|
|
const size = 5 + (particle.value / 100) * 10; |
|
|
|
|
|
this.ctx.beginPath(); |
|
|
this.ctx.arc(particle.x, particle.y, size, 0, 2 * Math.PI); |
|
|
this.ctx.fill(); |
|
|
|
|
|
|
|
|
this.ctx.strokeStyle = 'white'; |
|
|
this.ctx.lineWidth = 1; |
|
|
this.ctx.beginPath(); |
|
|
this.ctx.arc(particle.x, particle.y, size + 3, 0, particle.phase, false); |
|
|
this.ctx.stroke(); |
|
|
}); |
|
|
|
|
|
|
|
|
this.updateStats(); |
|
|
} |
|
|
|
|
|
updateStats() { |
|
|
const avgPhase = this.particles.reduce((sum, p) => sum + p.phase, 0) / this.particles.length; |
|
|
let primeBonds = 0; |
|
|
|
|
|
this.particles.forEach(p => { |
|
|
primeBonds += p.connections.length; |
|
|
}); |
|
|
|
|
|
document.getElementById('activeParticles').textContent = this.particles.length; |
|
|
document.getElementById('avgPhase').textContent = avgPhase.toFixed(2); |
|
|
document.getElementById('primeBonds').textContent = primeBonds / 2; |
|
|
} |
|
|
|
|
|
animate() { |
|
|
this.update(); |
|
|
this.draw(); |
|
|
requestAnimationFrame(() => this.animate()); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let simulation; |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
simulation = new ParticleSystem(); |
|
|
feather.replace(); |
|
|
|
|
|
|
|
|
document.getElementById('particleCount').addEventListener('input', function(e) { |
|
|
const count = parseInt(e.target.value); |
|
|
document.getElementById('currentCount').textContent = count; |
|
|
simulation.initializeParticles(count); |
|
|
}); |
|
|
|
|
|
document.getElementById('minDistance').addEventListener('input', function(e) { |
|
|
simulation.minDistance = parseInt(e.target.value); |
|
|
}); |
|
|
|
|
|
document.getElementById('maxDistance').addEventListener('input', function(e) { |
|
|
simulation.maxDistance = parseInt(e.target.value); |
|
|
}); |
|
|
|
|
|
document.getElementById('resetSim').addEventListener('click', function() { |
|
|
const count = parseInt(document.getElementById('particleCount').value); |
|
|
simulation.initializeParticles(count); |
|
|
}); |
|
|
|
|
|
document.getElementById('pauseSim').addEventListener('click', function() { |
|
|
simulation.isRunning = !simulation.isRunning; |
|
|
const button = document.getElementById('pauseSim'); |
|
|
const icon = button.querySelector('i'); |
|
|
if (simulation.isRunning) { |
|
|
icon.setAttribute('data-feather', 'pause'); |
|
|
} else { |
|
|
icon.setAttribute('data-feather', 'play'); |
|
|
} |
|
|
feather.replace(); |
|
|
}); |
|
|
|
|
|
|
|
|
window.addEventListener('resize', function() { |
|
|
const container = document.getElementById('simulationCanvas'); |
|
|
simulation.canvas.width = container.clientWidth; |
|
|
simulation.canvas.height = container.clientHeight; |
|
|
}); |
|
|
}); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|
|
|
|