<html> |
<head> |
<meta charset="UTF-8"> |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
<title>Molecular Fluid Mixing Simulation</title> |
<style> |
body { |
background: black; |
color: white; |
display: flex; |
flex-direction: column; |
align-items: center; |
} |
.controls { |
margin-top: 20px; |
display: flex; |
gap: 15px; |
} |
.slider-container { |
display: flex; |
flex-direction: column; |
align-items: center; |
} |
</style> |
</head> |
<body> |
<div class="container"> |
<canvas id="canvas" width="800" height="600"></canvas> |
<div class="controls"> |
<button id="resetBtn">Reset Simulation</button> |
<button id="pauseBtn">Pause</button> |
<div class="slider-container"> |
<label for="speedSlider">Molecular Speed</label> |
<input type="range" id="speedSlider" min="0.1" max="3" step="0.1" value="1"> |
</div> |
<div class="slider-container"> |
<label for="densitySlider">Molecule Density</label> |
<input type="range" id="densitySlider" min="100" max="1000" step="50" value="400"> |
</div> |
<div class="slider-container"> |
<label for="diffusionSlider">Diffusion Rate</label> |
<input type="range" id="diffusionSlider" min="0.1" max="2" step="0.1" value="0.8"> |
</div> |
</div> |
</div> |
<script> |
class Molecule { |
constructor(x, y, type, canvas) { |
this.x = x; |
this.y = y; |
this.vx = (Math.random() - 0.5) * 4; |
this.vy = (Math.random() - 0.5) * 4; |
this.type = type; // 'A' or 'B' |
this.radius = type === 'A' ? 3 : 4; |
this.mass = type === 'A' ? 1 : 1.5; |
this.color = type === 'A' ? '#ff6b6b' : '#4ecdc4'; |
this.canvas = canvas; |
this.trail = []; |
this.maxTrailLength = 10; |
} |
update(speedMultiplier, diffusionRate) { |
// Add random brownian motion |
this.vx += (Math.random() - 0.5) * 0.5 * diffusionRate; |
this.vy += (Math.random() - 0.5) * 0.5 * diffusionRate; |
// Apply friction |
this.vx *= 0.98; |
this.vy *= 0.98; |
// Limit velocity |
const maxVel = 3 * speedMultiplier; |
const vel = Math.sqrt(this.vx * this.vx + this.vy * this.vy); |
if (vel > maxVel) { |
this.vx = (this.vx / vel) * maxVel; |
this.vy = (this.vy / vel) * maxVel; |
} |
// Update position |
this.x += this.vx * speedMultiplier; |
this.y += this.vy * speedMultiplier; |
// Boundary collision |
if (this.x <= this.radius || this.x >= this.canvas.width - this.radius) { |
this.vx *= -0.8; |
this.x = Math.max(this.radius, Math.min(this.canvas.width - this.radius, this.x)); |
} |
if (this.y <= this.radius || this.y >= this.canvas.height - this.radius) { |
this.vy *= -0.8; |
this.y = Math.max(this.radius, Math.min(this.canvas.height - this.radius, this.y)); |
} |
// Update trail |
this.trail.push({x: this.x, y: this.y}); |
if (this.trail.length > this.maxTrailLength) { |
this.trail.shift(); |
} |
} |
draw(ctx) { |
// Draw trail |
ctx.strokeStyle = this.color; |
ctx.lineWidth = 1; |
ctx.globalAlpha = 0.3; |
ctx.beginPath(); |
for (let i = 1; i < this.trail.length; i++) { |
ctx.moveTo(this.trail[i-1].x, this.trail[i-1].y); |
ctx.lineTo(this.trail[i].x, this.trail[i].y); |
} |
ctx.stroke(); |
// Draw molecule |
ctx.globalAlpha = 0.8; |
const gradient = ctx.createRadialGradient( |
this.x - this.radius/3, this.y - this.radius/3, 0, |
this.x, this.y, this.radius |
); |
gradient.addColorStop(0, this.color); |
gradient.addColorStop(1, this.color + '80'); |
ctx.fillStyle = gradient; |
ctx.beginPath(); |
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); |
ctx.fill(); |
// Add shine effect |
ctx.globalAlpha = 0.6; |
ctx.fillStyle = 'white'; |
ctx.beginPath(); |
ctx.arc(this.x - this.radius/3, this.y - this.radius/3, this.radius/3, 0, Math.PI * 2); |
ctx.fill(); |
ctx.globalAlpha = 1; |
} |
checkCollision(other) { |
const dx = this.x - other.x; |
const dy = this.y - other.y; |
const distance = Math.sqrt(dx * dx + dy * dy); |
const minDistance = this.radius + other.radius; |
if (distance < minDistance) { |
// Collision response |
const angle = Math.atan2(dy, dx); |
const sin = Math.sin(angle); |
const cos = Math.cos(angle); |
// Rotate velocities |
const vx1 = this.vx * cos + this.vy * sin; |
const vy1 = this.vy * cos - this.vx * sin; |
const vx2 = other.vx * cos + other.vy * sin; |
const vy2 = other.vy * cos - other.vx * sin; |
// Conservation of momentum |
const finalVx1 = ((this.mass - other.mass) * vx1 + 2 * other.mass * vx2) / (this.mass + other.mass); |
const finalVx2 = ((other.mass - this.mass) * vx2 + 2 * this.mass * vx1) / (this.mass + other.mass); |
// Rotate velocities back |
this.vx = finalVx1 * cos - vy1 * sin; |
this.vy = vy1 * cos + finalVx1 * sin; |
other.vx = finalVx2 * cos - vy2 * sin; |
other.vy = vy2 * cos + finalVx2 * sin; |
// Separate molecules |
const overlap = minDistance - distance; |
const separationX = (dx / distance) * overlap * 0.5; |
const separationY = (dy / distance) * overlap * 0.5; |
this.x += separationX; |
this.y += separationY; |
other.x -= separationX; |
other.y -= separationY; |
} |
} |
} |
class FluidSimulation { |
constructor(canvasId) { |
this.canvas = document.getElementById(canvasId); |
this.ctx = this.canvas.getContext('2d'); |
this.molecules = []; |
this.animationId = null; |
this.isPaused = false; |
this.speedMultiplier = 1; |
this.diffusionRate = 0.8; |
this.initMolecules(400); |
this.bindEvents(); |
this.animate(); |
} |
initMolecules(count) { |
this.molecules = []; |
const halfWidth = this.canvas.width / 2; |
// Create fluid A molecules (left side) |
for (let i = 0; i < count / 2; i++) { |
const x = Math.random() * (halfWidth - 50) + 25; |
const y = Math.random() * (this.canvas.height - 50) + 25; |
this.molecules.push(new Molecule(x, y, 'A', this.canvas)); |
} |
// Create fluid B molecules (right side) |
for (let i = 0; i < count / 2; i++) { |
const x = Math.random() * (halfWidth - 50) + halfWidth + 25; |
const y = Math.random() * (this.canvas.height - 50) + 25; |
this.molecules.push(new Molecule(x, y, 'B', this.canvas)); |
} |
} |
animate() { |
if (!this.isPaused) { |
this.update(); |
this.draw(); |
} |
this.animationId = requestAnimationFrame(() => this.animate()); |
} |
update() { |
// Update molecules |
for (let molecule of this.molecules) { |
molecule.update(this.speedMultiplier, this.diffusionRate); |
} |
// Check collisions |
for (let i = 0; i < this.molecules.length; i++) { |
for (let j = i + 1; j < this.molecules.length; j++) { |
this.molecules[i].checkCollision(this.molecules[j]); |
} |
} |
} |
draw() { |
// Clear canvas with fade effect |
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; |
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); |
// Draw dividing line (fades over time) |
this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; |
this.ctx.lineWidth = 2; |
this.ctx.setLineDash([5, 5]); |
this.ctx.beginPath(); |
this.ctx.moveTo(this.canvas.width / 2, 0); |
this.ctx.lineTo(this.canvas.width / 2, this.canvas.height); |
this.ctx.stroke(); |
this.ctx.setLineDash([]); |
// Draw molecules |
for (let molecule of this.molecules) { |
molecule.draw(this.ctx); |
} |
} |
bindEvents() { |
document.getElementById('resetBtn').addEventListener('click', () => { |
this.initMolecules(parseInt(document.getElementById('densitySlider').value)); |
}); |
document.getElementById('pauseBtn').addEventListener('click', (e) => { |
this.isPaused = !this.isPaused; |
e.target.textContent = this.isPaused ? 'Resume' : 'Pause'; |
}); |
document.getElementById('speedSlider').addEventListener('input', (e) => { |
this.speedMultiplier = parseFloat(e.target.value); |
}); |
document.getElementById('densitySlider').addEventListener('input', (e) => { |
this.initMolecules(parseInt(e.target.value)); |
}); |
document.getElementById('diffusionSlider').addEventListener('input', (e) => { |
this.diffusionRate = parseFloat(e.target.value); |
}); |
} |
} |
// Initialize simulation when page loads |
window.addEventListener('load', () => { |
new FluidSimulation('canvas'); |
}); |
</script> |
</body> |
</html> |