Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PhaseFlow Particles</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <style> | |
| canvas { | |
| background: linear-gradient(135deg, #0f0c29, #302b63, #24243e); | |
| } | |
| .control-panel { | |
| backdrop-filter: blur(10px); | |
| background: rgba(15, 23, 42, 0.7); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-900 text-white overflow-hidden"> | |
| <div class="relative h-screen w-full"> | |
| <canvas id="particleCanvas" class="absolute top-0 left-0 w-full h-full"></canvas> | |
| <!-- Control Panel --> | |
| <div class="control-panel absolute top-4 right-4 rounded-xl p-4 shadow-lg z-10 w-80"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-xl font-bold flex items-center"> | |
| <i data-feather="settings" class="mr-2"></i> Controls | |
| </h2> | |
| <button id="resetBtn" class="bg-indigo-600 hover:bg-indigo-700 px-3 py-1 rounded-lg text-sm flex items-center"> | |
| <i data-feather="refresh-ccw" class="mr-1"></i> Reset | |
| </button> | |
| </div> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Particle Count: <span id="countValue">150</span></label> | |
| <input type="range" id="particleCount" min="50" max="300" value="150" class="w-full accent-indigo-500"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Min Distance: <span id="minDistValue">30</span>px</label> | |
| <input type="range" id="minDistance" min="10" max="100" value="30" class="w-full accent-indigo-500"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Max Distance: <span id="maxDistValue">120</span>px</label> | |
| <input type="range" id="maxDistance" min="50" max="200" value="120" class="w-full accent-indigo-500"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1">Interaction Strength: <span id="strengthValue">0.5</span></label> | |
| <input type="range" id="interactionStrength" min="0.1" max="2" step="0.1" value="0.5" class="w-full accent-indigo-500"> | |
| </div> | |
| <div class="flex space-x-2 pt-2"> | |
| <button id="pauseBtn" class="flex-1 bg-amber-600 hover:bg-amber-700 py-2 rounded-lg text-sm flex items-center justify-center"> | |
| <i data-feather="pause" class="mr-1"></i> Pause | |
| </button> | |
| <button id="infoBtn" class="flex-1 bg-cyan-600 hover:bg-cyan-700 py-2 rounded-lg text-sm flex items-center justify-center"> | |
| <i data-feather="info" class="mr-1"></i> Info | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Info Modal --> | |
| <div id="infoModal" class="hidden fixed inset-0 bg-black bg-opacity-70 z-20 flex items-center justify-center p-4"> | |
| <div class="control-panel rounded-xl p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-2xl font-bold">PhaseFlow Simulation</h2> | |
| <button id="closeInfo" class="text-gray-400 hover:text-white"> | |
| <i data-feather="x"></i> | |
| </button> | |
| </div> | |
| <div class="space-y-4 text-gray-200"> | |
| <p>Each particle has:</p> | |
| <ul class="list-disc pl-5 space-y-2"> | |
| <li><span class="font-semibold">Position (x, y)</span>: Location on the canvas</li> | |
| <li><span class="font-semibold">Phase (0-2π)</span>: Controls attraction/repulsion behavior | |
| <ul class="list-circle pl-5 mt-1"> | |
| <li>0: Strongly attracted</li> | |
| <li>π: Balanced (attracted & repulsed)</li> | |
| <li>2π: Strongly repulsed</li> | |
| </ul> | |
| </li> | |
| <li><span class="font-semibold">Value (2-100)</span>: Determines secondary magnetic attraction based on shared prime factors</li> | |
| </ul> | |
| <p class="pt-2">Particles influence each other through:</p> | |
| <ul class="list-disc pl-5 space-y-2"> | |
| <li>Phase interactions within min/max distance range</li> | |
| <li>Magnetic attraction to particles sharing prime factors</li> | |
| <li>Phase adjustment formula: (particle count × average phase) / average distance</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Initialize feather icons | |
| feather.replace(); | |
| // Canvas setup | |
| const canvas = document.getElementById('particleCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| // Simulation parameters | |
| let params = { | |
| particleCount: 150, | |
| minDistance: 30, | |
| maxDistance: 120, | |
| interactionStrength: 0.5, | |
| paused: false | |
| }; | |
| // Particle class | |
| class Particle { | |
| constructor(x, y) { | |
| this.x = x || Math.random() * canvas.width; | |
| this.y = y || Math.random() * canvas.height; | |
| this.phase = Math.PI + (Math.random() * 0.1 - 0.05); // PI ± 5% | |
| this.value = Math.floor(Math.random() * 99) + 2; // 2-100 | |
| this.vx = (Math.random() - 0.5) * 0.5; | |
| this.vy = (Math.random() - 0.5) * 0.5; | |
| this.radius = 3 + (this.value / 30); | |
| this.color = this.getPhaseColor(); | |
| this.primeFactors = this.getPrimeFactors(this.value); | |
| } | |
| getPrimeFactors(n) { | |
| const factors = []; | |
| let divisor = 2; | |
| while (n >= 2) { | |
| if (n % divisor === 0) { | |
| factors.push(divisor); | |
| n = n / divisor; | |
| } else { | |
| divisor++; | |
| } | |
| } | |
| return [...new Set(factors)]; // Unique factors | |
| } | |
| getPhaseColor() { | |
| // Map phase to color: 0=blue, π=green, 2π=red | |
| const hue = (this.phase / (2 * Math.PI)) * 360; | |
| return `hsl(${hue}, 80%, 60%)`; | |
| } | |
| update(particles) { | |
| if (params.paused) return; | |
| // Apply velocity | |
| this.x += this.vx; | |
| this.y += this.vy; | |
| // Boundary conditions | |
| if (this.x < 0) this.x = canvas.width; | |
| if (this.x > canvas.width) this.x = 0; | |
| if (this.y < 0) this.y = canvas.height; | |
| if (this.y > canvas.height) this.y = 0; | |
| // Interaction calculations | |
| let fx = 0; | |
| let fy = 0; | |
| let nearbyParticles = 0; | |
| let totalPhase = 0; | |
| let totalDistance = 0; | |
| for (let other of particles) { | |
| if (other === this) continue; | |
| const dx = other.x - this.x; | |
| const dy = other.y - this.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance > params.minDistance && distance < params.maxDistance) { | |
| // Primary phase-based force | |
| const force = Math.PI + Math.cos(this.phase) * params.interactionStrength / distance; | |
| fx += force * dx; | |
| fy += force * dy; | |
| // Collect data for phase influence | |
| nearbyParticles++; | |
| totalPhase += other.phase; | |
| totalDistance += distance; | |
| // Secondary magnetic attraction based on shared prime factors | |
| const sharedFactors = this.primeFactors.filter(factor => | |
| other.primeFactors.includes(factor)); | |
| if (sharedFactors.length > 0) { | |
| const magneticForce = sharedFactors.length * 0.05 / distance; | |
| this.vx += magneticForce * dx * 0.01; | |
| this.vy += magneticForce * dy * 0.01; | |
| } | |
| } | |
| } | |
| // Apply phase influence from nearby particles | |
| if (nearbyParticles > 0) { | |
| const avgPhase = totalPhase / nearbyParticles; | |
| const avgDistance = totalDistance / nearbyParticles; | |
| const phaseInfluence = (nearbyParticles * avgPhase) / avgDistance * 0.1; | |
| this.phase = (this.phase + phaseInfluence) % (2 * Math.PI); | |
| if (this.phase < 0) this.phase += 2 * Math.PI; | |
| } | |
| // Apply forces | |
| this.vx += fx * 0.01; | |
| this.vy += fy * 0.01; | |
| // Velocity damping | |
| this.vx *= 0.96; | |
| this.vy *= 0.96; | |
| // Update color based on new phase | |
| this.color = this.getPhaseColor(); | |
| } | |
| draw() { | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = this.color; | |
| ctx.fill(); | |
| // Draw value indicator | |
| ctx.beginPath(); | |
| ctx.arc(this.x, this.y, this.radius * 0.4, 0, Math.PI * 2); | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; | |
| ctx.fill(); | |
| } | |
| } | |
| // Create particles | |
| let particles = []; | |
| function initParticles() { | |
| particles = []; | |
| for (let i = 0; i < params.particleCount; i++) { | |
| particles.push(new Particle()); | |
| } | |
| } | |
| // Animation loop | |
| function animate() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| // Draw connections between close particles | |
| ctx.lineWidth = 0.5; | |
| for (let i = 0; i < particles.length; i++) { | |
| for (let j = i + 1; j < particles.length; j++) { | |
| const p1 = particles[i]; | |
| const p2 = particles[j]; | |
| const dx = p1.x - p2.x; | |
| const dy = p1.y - p2.y; | |
| const dist = Math.sqrt(dx * dx + dy * dy); | |
| if (dist < params.maxDistance) { | |
| const alpha = 1 - dist / params.maxDistance; | |
| ctx.strokeStyle = `rgba(100, 150, 255, ${alpha * 0.2})`; | |
| ctx.beginPath(); | |
| ctx.moveTo(p1.x, p1.y); | |
| ctx.lineTo(p2.x, p2.y); | |
| ctx.stroke(); | |
| } | |
| } | |
| } | |
| // Update and draw particles | |
| particles.forEach(particle => { | |
| particle.update(particles); | |
| particle.draw(); | |
| }); | |
| requestAnimationFrame(animate); | |
| } | |
| // Event listeners for controls | |
| document.getElementById('particleCount').addEventListener('input', function() { | |
| params.particleCount = parseInt(this.value); | |
| document.getElementById('countValue').textContent = this.value; | |
| initParticles(); | |
| }); | |
| document.getElementById('minDistance').addEventListener('input', function() { | |
| params.minDistance = parseInt(this.value); | |
| document.getElementById('minDistValue').textContent = this.value; | |
| }); | |
| document.getElementById('maxDistance').addEventListener('input', function() { | |
| params.maxDistance = parseInt(this.value); | |
| document.getElementById('maxDistValue').textContent = this.value; | |
| }); | |
| document.getElementById('interactionStrength').addEventListener('input', function() { | |
| params.interactionStrength = parseFloat(this.value); | |
| document.getElementById('strengthValue').textContent = this.value; | |
| }); | |
| document.getElementById('pauseBtn').addEventListener('click', function() { | |
| params.paused = !params.paused; | |
| const icon = this.querySelector('i'); | |
| if (params.paused) { | |
| icon.setAttribute('data-feather', 'play'); | |
| this.innerHTML = '<i data-feather="play" class="mr-1"></i> Resume'; | |
| } else { | |
| icon.setAttribute('data-feather', 'pause'); | |
| this.innerHTML = '<i data-feather="pause" class="mr-1"></i> Pause'; | |
| } | |
| feather.replace(); | |
| }); | |
| document.getElementById('resetBtn').addEventListener('click', function() { | |
| initParticles(); | |
| }); | |
| document.getElementById('infoBtn').addEventListener('click', function() { | |
| document.getElementById('infoModal').classList.remove('hidden'); | |
| }); | |
| document.getElementById('closeInfo').addEventListener('click', function() { | |
| document.getElementById('infoModal').classList.add('hidden'); | |
| }); | |
| // Handle window resize | |
| window.addEventListener('resize', function() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| }); | |
| // Initialize and start simulation | |
| initParticles(); | |
| animate(); | |
| </script> | |
| </body> | |
| </html> | |