| <!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> |
|
|
|
|