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