<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>二维圆形颗粒密集堆积模拟</title> <style> body { font-family: Arial, sans-serif; margin: 20px; display: flex; flex-direction: column; align-items: center; background-color: #f5f5f5; } .container { display: flex; width: 100%; justify-content: space-between; margin-bottom: 20px; background-color: white; border-radius: 10px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); overflow: hidden; } .control-panel { padding: 20px; width: 300px; background-color: white; } .canvas-container { flex: 1; margin-left: 20px; background-color: white; padding: 20px; } canvas { background-color: #f0f0f0; border-radius: 5px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); } label { display: block; margin-bottom: 8px; font-weight: bold; color: #333; } input { width: 100%; padding: 8px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .button-group { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; } button { padding: 10px 15px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.3s; } button:hover { background-color: #45a049; } .status { margin-top: 20px; padding: 15px; background-color: #f8f8f8; border-radius: 5px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); } .status p { margin: 5px 0; } h1 { color: #333; margin-bottom: 20px; } h3 { margin-top: 0; color: #333; border-bottom: 1px solid #ddd; padding-bottom: 10px; } </style></head><body> <h1>二维圆形颗粒密集堆积模拟</h1>
<div class="container"> <div class="control-panel"> <h3>参数设置</h3> <label for="height">矩形高度 H (px):</label> <input type="number" id="height" value="400" min="100" max="1000">
<label for="width">矩形宽度 L (px):</label> <input type="number" id="width" value="400" min="100" max="1000">
<label for="minRadius">颗粒最小半径 (px):</label> <input type="number" id="minRadius" value="10" min="5" max="50">
<label for="maxRadius">颗粒最大半径 (px):</label> <input type="number" id="maxRadius" value="30" min="10" max="100">
<label for="porosity">目标孔隙率 e (0-1):</label> <input type="number" id="porosity" value="0.35" step="0.01" min="0" max="1">
<label for="compression">压缩比 β (0-1):</label> <input type="number" id="compression" value="0.2" step="0.01" min="0" max="1">
<label for="paddingAdjust">边界保护层调整值 (px):</label> <input type="number" id="paddingAdjust" value="0" min="-50" max="50">
<label for="forceDistanceAdjust">力作用范围调整值 (px):</label> <input type="number" id="forceDistanceAdjust" value="0" min="-50" max="50">
<label for="springConstant">弹性系数 k:</label> <input type="number" id="springConstant" value="0.1" step="0.01" min="0.01" max="1">
<div class="button-group"> <button id="generate">生成初始配置</button> <button id="animate">开始压缩</button> <button id="save">保存数据</button> <button id="reset">重置</button> </div>
<div class="status"> <p>状态: 准备就绪</p> <p>颗粒数量: 0</p> <p>当前孔隙率: 0</p> <p>边界保护层: 0</p> <p>力作用范围: 0</p> </div> </div>
<div class="canvas-container"> <canvas id="particleCanvas"></canvas> </div> </div>
<script> const canvas = document.getElementById('particleCanvas'); const ctx = canvas.getContext('2d'); const generateBtn = document.getElementById('generate'); const animateBtn = document.getElementById('animate'); const saveBtn = document.getElementById('save'); const resetBtn = document.getElementById('reset'); const statusElement = document.querySelector('.status');
function resizeCanvas() { canvas.width = document.querySelector('.canvas-container').offsetWidth - 40; canvas.height = Math.min(600, window.innerHeight - 200); }
resizeCanvas(); window.addEventListener('resize', resizeCanvas);
let params = { height: 400, width: 400, minRadius: 10, maxRadius: 30, porosity: 0.35, compression: 0.2, padding: 8, springConstant: 0.1, forceDistance: 40, expandedWidth: 0, targetWidth: 0 };
let particles = []; let animationId = null; let isAnimating = false; let currentStep = 0; let totalSteps = 1000; let scale = 1;
function updateParams() { const height = parseInt(document.getElementById('height').value); const width = parseInt(document.getElementById('width').value); const minRadius = parseInt(document.getElementById('minRadius').value); const maxRadius = parseInt(document.getElementById('maxRadius').value); const porosity = parseFloat(document.getElementById('porosity').value); const compression = parseFloat(document.getElementById('compression').value);
const paddingDefault = Math.floor(height / 50); const forceDistanceDefault = Math.floor(height / 10);
const paddingAdjust = parseInt(document.getElementById('paddingAdjust').value) || 0; const forceDistanceAdjust = parseInt(document.getElementById('forceDistanceAdjust').value) || 0;
params.padding = paddingDefault + paddingAdjust; params.forceDistance = forceDistanceDefault + forceDistanceAdjust;
params.padding = Math.max(5, Math.min(50, params.padding)); params.forceDistance = Math.max(10, Math.min(200, params.forceDistance));
params.height = height; params.width = width; params.minRadius = minRadius; params.maxRadius = maxRadius; params.porosity = porosity; params.compression = compression; params.springConstant = parseFloat(document.getElementById('springConstant').value);
params.expandedWidth = params.width * (1 + params.compression); params.targetWidth = params.width;
calculateScale(); }
function calculateScale() { const contentWidth = params.expandedWidth; const contentHeight = params.height; const canvasWidth = canvas.width; const canvasHeight = canvas.height;
const scaleX = canvasWidth / contentWidth; const scaleY = canvasHeight / contentHeight; scale = Math.min(scaleX, scaleY); }
function updateStatus() { const statusText = isAnimating ? '正在模拟...' : '准备就绪'; const particleCount = particles.length; const currentPorosity = calculatePorosity();
statusElement.innerHTML = ` <p>状态: ${statusText}</p> <p>颗粒数量: ${particleCount}</p> <p>当前孔隙率: ${currentPorosity.toFixed(2)}</p> <p>边界保护层: ${params.padding}</p> <p>力作用范围: ${params.forceDistance}</p> `; }
function calculatePorosity() { if (particles.length === 0) return 0;
const currentWidth = params.expandedWidth - (params.expandedWidth - params.targetWidth) * (currentStep / totalSteps); const totalArea = currentWidth * params.height; let filledArea = 0;
particles.forEach(particle => { filledArea += Math.PI * particle.radius * particle.radius; });
return 1 - filledArea / totalArea; }
function generateInitialParticles() { updateParams(); particles = [];
const targetPorosity = params.porosity / (1 - params.compression); const maxParticles = Math.floor((params.expandedWidth * params.height * (1 - targetPorosity)) / (Math.PI * Math.pow(params.minRadius, 2)));
let attempts = 0; const maxAttempts = 10000;
while (particles.length < maxParticles && attempts < maxAttempts) { const radius = params.minRadius + Math.random() * (params.maxRadius - params.minRadius);
const x = params.padding + radius + Math.random() * (params.expandedWidth - 2 * (params.padding + radius)); const y = params.padding + radius + Math.random() * (params.height - 2 * (params.padding + radius));
let overlap = false; for (const particle of particles) { const dx = x - particle.x; const dy = y - particle.y; const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < radius + particle.radius) { overlap = true; break; } }
if (!overlap) { particles.push({ x, y, radius, vx: 0, vy: 0 }); }
attempts++; }
updateStatus(); drawParticles(); }
function drawParticles() { ctx.clearRect(0, 0, canvas.width, canvas.height);
const currentWidth = params.expandedWidth - (params.expandedWidth - params.targetWidth) * (currentStep / totalSteps); const leftBoundary = params.padding + (params.expandedWidth - currentWidth) / 2; const rightBoundary = leftBoundary + currentWidth - 2 * params.padding;
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 2); ctx.scale(scale, scale); ctx.translate(-params.expandedWidth / 2, -params.height / 2);
ctx.strokeStyle = '#333'; ctx.lineWidth = 2 / scale; ctx.strokeRect(leftBoundary, params.padding, currentWidth - 2 * params.padding, params.height - 2 * params.padding);
particles.forEach(particle => { ctx.beginPath(); ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); const hue = Math.floor(Math.random() * 360); ctx.fillStyle = `hsl(${hue}, 70%, 60%)`; ctx.fill(); ctx.strokeStyle = '#333'; ctx.lineWidth = 1 / scale; ctx.stroke(); });
ctx.restore(); }
function simulateCompression() { if (isAnimating) return;
isAnimating = true; currentStep = 0;
function animate() { if (currentStep >= totalSteps) { isAnimating = false; updateStatus(); return; }
currentStep++;
updateParticles();
drawParticles();
animationId = requestAnimationFrame(animate); }
animate(); }
function updateParticles() { const currentWidth = params.expandedWidth - (params.expandedWidth - params.targetWidth) * (currentStep / totalSteps); const leftBoundary = params.padding + (params.expandedWidth - currentWidth) / 2; const rightBoundary = leftBoundary + currentWidth - 2 * params.padding;
for (const particle of particles) { particle.vx = 0; particle.vy = 0; }
for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { applyDistanceForce(particles[i], particles[j]); } }
for (const particle of particles) { handleBoundaryCollision(particle, leftBoundary, rightBoundary); }
for (const particle of particles) { particle.vx *= 0.99; particle.vy *= 0.99;
particle.x += particle.vx; particle.y += particle.vy; } }
function applyDistanceForce(p1, p2) { const dx = p2.x - p1.x; const dy = p2.y - p1.y; const distance = Math.sqrt(dx * dx + dy * dy); const minDistance = p1.radius + p2.radius;
if (distance < params.forceDistance) { let forceMagnitude = 0;
if (distance < minDistance) { forceMagnitude = params.springConstant * (minDistance - distance) * 2; } else { forceMagnitude = params.springConstant * (params.forceDistance - distance) / params.forceDistance; }
const nx = dx / distance; const ny = dy / distance;
p1.vx -= forceMagnitude * nx; p1.vy -= forceMagnitude * ny; p2.vx += forceMagnitude * nx; p2.vy += forceMagnitude * ny; } }
function handleBoundaryCollision(particle, leftBoundary, rightBoundary) { if (particle.x - particle.radius < leftBoundary) { particle.x = leftBoundary + particle.radius; particle.vx *= -0.5; }
if (particle.x + particle.radius > rightBoundary) { particle.x = rightBoundary - particle.radius; particle.vx *= -0.5; }
if (particle.y - particle.radius < params.padding) { particle.y = params.padding + particle.radius; particle.vy *= -0.5; }
if (particle.y + particle.radius > params.height - params.padding) { particle.y = params.height - params.padding - particle.radius; particle.vy *= -0.5; } }
function saveData() { if (particles.length === 0) return;
const currentWidth = params.expandedWidth - (params.expandedWidth - params.targetWidth) * (currentStep / totalSteps); const leftBoundary = params.padding + (params.expandedWidth - currentWidth) / 2;
let csvContent = "ID,X,Y,Radius\n";
particles.forEach((particle, index) => { const adjustedX = particle.x - leftBoundary; const adjustedY = particle.y - params.padding;
csvContent += `${index + 1},${adjustedX},${adjustedY},${particle.radius}\n`; });
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.setAttribute('href', url); link.setAttribute('download', 'particle_data.csv'); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); }
generateBtn.addEventListener('click', generateInitialParticles); animateBtn.addEventListener('click', simulateCompression); saveBtn.addEventListener('click', saveData); resetBtn.addEventListener('click', () => { if (animationId) { cancelAnimationFrame(animationId); animationId = null; } particles = []; isAnimating = false; updateStatus(); drawParticles(); });
updateParams(); drawParticles(); </script></body></html>