<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>简化的生命自动机</title>
<style>
/* 样式优化 - 右侧紧凑控制面板 */
body {
margin: 0;
padding: 0;
background-color: black;
font-family: Arial, sans-serif;
color: #e6e6e6;
width: 100vw;
height: 100vh;
overflow: hidden;
position: relative;
}
/* 画布样式 - 占据大部分空间 */
canvas {
position: fixed;
top: 0;
left: 0;
width: calc(100vw - 300px); /* 留出右侧控制面板空间 */
height: 100vh;
background-color: black;
z-index: 0;
}
/* 右侧控制面板 */
.control-panel {
position: fixed;
top: 0;
right: 0;
width: 280px;
height: 100vh;
background-color: rgba(10, 10, 20, 0.95);
border-left: 1px solid rgba(74, 124, 255, 0.3);
z-index: 10;
padding: 10px;
overflow-y: auto;
box-sizing: border-box;
}
/* 标题样式 */
h1 {
margin: 0 0 10px 0;
font-size: 16px;
text-align: center;
color: #4a7cff;
padding-bottom: 5px;
border-bottom: 1px solid rgba(74, 124, 255, 0.3);
}
/* 按钮基础样式 - 紧凑化 */
button {
padding: 4px 8px;
margin: 2px 0;
cursor: pointer;
background-color: rgba(22, 33, 62, 0.5);
color: white;
border: 1px solid rgba(74, 124, 255, 0.3);
border-radius: 3px;
font-size: 12px;
transition: background-color 0.2s;
width: 100%;
box-sizing: border-box;
}
button:hover {
background-color: rgba(74, 124, 255, 0.3);
}
/* 控制区域样式 - 紧凑化 */
.compact-section {
margin-bottom: 10px;
padding: 5px;
background-color: rgba(15, 20, 30, 0.5);
border-radius: 3px;
border: 1px solid rgba(74, 124, 255, 0.2);
}
/* 描述文本样式 */
.axis-description {
font-size: 11px;
line-height: 1.2;
margin-bottom: 8px;
padding: 5px;
background-color: rgba(15, 20, 30, 0.5);
border-radius: 3px;
}
/* 颜色选择器样式 - 紧凑化 */
.color-selector {
font-size: 12px;
margin-bottom: 8px;
}
.color-option-container {
display: inline-flex;
align-items: center;
margin-right: 5px;
margin-bottom: 5px;
font-size: 11px;
}
.color-option {
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 3px;
cursor: pointer;
border: 1px solid #fff;
}
/* 输入框样式 - 紧凑化 */
input[type="number"] {
padding: 4px;
border: 1px solid rgba(74, 124, 255, 0.3);
border-radius: 3px;
background-color: rgba(26, 26, 46, 0.5);
color: #ffffff;
font-size: 12px;
width: 60px;
}
/* 标签样式 */
label {
font-size: 12px;
margin-right: 5px;
}
/* 按钮组样式 */
.button-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 5px;
margin-bottom: 8px;
}
/* 全局数量控制样式 */
.global-count-control {
display: flex;
align-items: center;
gap: 5px;
margin-bottom: 8px;
font-size: 11px;
}
/* 滚动条样式优化 */
.control-panel::-webkit-scrollbar {
width: 4px;
}
.control-panel::-webkit-scrollbar-track {
background: rgba(10, 10, 20, 0.5);
}
.control-panel::-webkit-scrollbar-thumb {
background: rgba(74, 124, 255, 0.5);
border-radius: 2px;
}
/* 球类型控制器容器样式 */
.ball-type-controls-container {
max-height: 200px;
overflow-y: auto;
margin-top: 5px;
}
/* 减少文本间距 */
p, span {
margin: 0;
padding: 0;
}
.generator-header {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
text-align: center;
}
.generator-controls {
display: flex;
gap: 15px;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
/* 球类型控制器容器 */
.ball-type-controls {
width: 500px;
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
/* 轴线控制器样式 */
.axis-controller {
background-color: rgba(22, 33, 62, 0.2);
border: 2px solid rgba(15, 52, 96, 0.2);
border-radius: 4px;
position: relative;
padding: 15px;
height: 80px;
width: 500px;
margin: 0 auto;
}
.axis-controller-header {
position: absolute;
top: 10px;
left: 20px;
right: 20px;
font-weight: bold;
display: flex;
align-items: center;
gap: 8px;
}
.controller-color-indicator {
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #333;
}
.count-control {
margin-left: auto;
display: flex;
align-items: center;
gap: 5px;
}
.count-control input {
width: 60px;
padding: 5px;
border: 1px solid #0f3460;
border-radius: 4px;
background-color: #1a1a2e;
color: #e6e6e6;
}
.force-controls {
margin-left: 180px;
margin-top: 20px;
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.force-control-label {
font-weight: bold;
}
.force-control-item {
display: flex;
align-items: center;
gap: 5px;
}
.force-color-indicator {
width: 16px;
height: 16px;
border-radius: '50%';
border: 1px solid #333;
}
.force-control-item input {
width: 60px;
padding: 3px;
border: 1px solid #0f3460;
border-radius: 3px;
background-color: #1a1a2e;
color: #e6e6e6;
}
.color-selector {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.color-option-container {
display: flex;
align-items: center;
cursor: pointer;
padding: 5px;
border-radius: 4px;
transition: background-color 0.2s;
}
.color-option-container:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.color-option {
width: 30px;
height: 30px;
border-radius: 50%;
cursor: pointer;
border: 3px solid transparent;
transition: transform 0.1s;
margin-right: 5px;
}
.color-option:hover {
transform: scale(1.1);
}
.color-option.selected {
border-color: #e6e6e6;
}
.number-control {
display: flex;
align-items: center;
gap: 10px;
}
.number-control label {
white-space: nowrap;
}
.number-control input {
width: 60px;
padding: 5px;
border: 1px solid #0f3460;
border-radius: 4px;
background-color: #1a1a2e;
color: #e6e6e6;
}
.add-balls-btn {
background-color: #0f3460;
}
.add-balls-btn:hover {
background-color: #0a2644;
}
/* 错误诊断区域 */
.diagnostics-container {
margin: 10px 0;
padding: 10px;
background-color: rgba(15, 52, 96, 0.2);
border: 1px solid rgba(15, 52, 96, 0.2);
border-radius: 4px;
min-height: 20px;
font-family: Arial, sans-serif;
font-size: 14px;
z-index: 1;
position: relative;
}
</style>
</head>
<body>
<!-- 右侧控制面板 -->
<div class="control-panel">
<h1>控制面板</h1>
<!-- 控制器说明 -->
<div class="compact-section">
<div class="axis-description">
<strong>说明:</strong>
- 球有空间隔断,不重叠<br>
- 数字控制吸引力/斥力(-1到1)<br>
- 正值吸引,负值排斥
</div>
</div>
<!-- 颜色选择器 -->
<div class="compact-section">
<div class="color-selector">
<span>颜色:</span><br>
<div class="color-option-container">
<div class="color-option" style="background-color: #FF6B6B;" data-color="#FF6B6B"></div>
<span>红</span>
</div>
<div class="color-option-container">
<div class="color-option" style="background-color: #4ECDC4;" data-color="#4ECDC4"></div>
<span>青</span>
</div>
<div class="color-option-container">
<div class="color-option" style="background-color: #FFE66D;" data-color="#FFE66D"></div>
<span>黄</span>
</div>
<div class="color-option-container">
<div class="color-option" style="background-color: #1A535C;" data-color="#1A535C"></div>
<span>深</span>
</div>
<div class="color-option-container">
<div class="color-option" style="background-color: #FF9F1C;" data-color="#FF9F1C"></div>
<span>橙</span>
</div>
<div class="color-option-container">
<div class="color-option" style="background-color: #7B2CBF;" data-color="#7B2CBF"></div>
<span>紫</span>
</div>
</div>
</div>
<!-- 全局数量控制 -->
<div class="compact-section">
<div class="global-count-control">
<label for="globalCountInput">数量:</label>
<input type="number" id="globalCountInput" min="1" max="1000" value="10">
</div>
</div>
<!-- 速度控制 -->
<div class="compact-section">
<div class="speed-control">
<label for="speedSlider">速度倍率: <span id="speedDisplay">1x</span></label>
<input type="range" id="speedSlider" min="1" max="50" value="1" style="width: 100%;">
</div>
</div>
<!-- 边界控制 -->
<div class="compact-section">
<div class="boundary-control">
<label for="boundaryType">边界类型:</label>
<select id="boundaryType" style="width: 100%; margin-top: 5px; padding: 4px; border: 1px solid rgba(74, 124, 255, 0.3); border-radius: 3px; background-color: rgba(26, 26, 46, 0.5); color: #ffffff; font-size: 12px;">
<option value="none">无边界</option>
<option value="circle">培养皿边界</option>
<option value="square">方形边界</option>
</select>
</div>
</div>
<!-- 操作按钮组 -->
<div class="compact-section">
<div class="button-group">
<button id="addBallsBtn">添加球</button>
<button id="globalRandomBtn">随机参数</button>
</div>
<div class="button-group">
<button id="startBtn">开始</button>
<button id="pauseBtn">暂停</button>
</div>
<button id="resetBtn">重置</button>
</div>
<!-- 球类型控制器容器 -->
<div class="compact-section">
<div class="ball-type-controls-container">
<div id="ballTypeControls" class="ball-type-controls">
<!-- 控制器将动态添加到这里 -->
</div>
</div>
</div>
</div>
<!-- 画布 - 占据大部分空间 -->
<canvas id="canvas"></canvas>
<script>
// 获取Canvas和控制按钮
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 设置画布尺寸
function setupCanvas() {
// 计算可用宽度(减去右侧控制面板的宽度)
const controlPanelWidth = 300; // 与CSS中保持一致
const availableWidth = window.innerWidth - controlPanelWidth;
// 设置画布尺寸
canvas.width = availableWidth;
canvas.height = window.innerHeight;
// 设置画布样式
canvas.style.width = `${availableWidth}px`;
canvas.style.height = '100%';
}
setupCanvas();
// 添加窗口大小变化事件监听器,自动调整画布尺寸
window.addEventListener('resize', setupCanvas);
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resetBtn = document.getElementById('resetBtn');
const addBallsBtn = document.getElementById('addBallsBtn');
const colorOptions = document.querySelectorAll('.color-option');
const ballTypeControls = document.getElementById('ballTypeControls');
// 模拟状态
const simulationState = {
isRunning: false,
balls: [],
ballTypes: {}, // 存储不同颜色球的参数
selectedColor: '#FF6B6B', // 默认选中红色
// 视图相关状态
viewOffset: { x: 0, y: 0 },
viewScale: 1.0, // 视图缩放比例
isDragging: false,
lastMousePos: { x: 0, y: 0 },
// 速度控制
speedMultiplier: 1, // 速度倍率 (1-50倍)
// 边界设置
boundaryType: 'none', // none, circle, square
boundaryRadius: 1000, // 圆形边界半径(初始设为明显大于页面加载时的视域)
boundarySize: { width: 2000, height: 1600 } // 方形边界大小(初始设为明显大于页面加载时的视域)
};
// 基础状态变量
let animationId = null;
// 球类定义
class Ball {
constructor(x, y, color, type) {
this.x = typeof x === 'number' && !isNaN(x) ? x : 0;
this.y = typeof y === 'number' && !isNaN(y) ? y : 0;
this.vx = 0;
this.vy = 0;
this.radius = 2;
this.color = color || '#FFFFFF';
this.type = type || 'default';
}
draw() {
try {
if (typeof ctx !== 'undefined' && ctx) {
ctx.beginPath();
const x = typeof this.x === 'number' && !isNaN(this.x) ? this.x : 0;
const y = typeof this.y === 'number' && !isNaN(this.y) ? this.y : 0;
const radius = typeof this.radius === 'number' && !isNaN(this.radius) && this.radius > 0 ? this.radius : 2;
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fillStyle = this.color || '#FFFFFF';
ctx.fill();
ctx.closePath();
}
} catch (error) {
console.error('Ball.draw() 方法执行失败:', error);
}
}
update() {
try {
const vx = typeof this.vx === 'number' && !isNaN(this.vx) ? this.vx : 0;
const vy = typeof this.vy === 'number' && !isNaN(this.vy) ? this.vy : 0;
// 只更新位置,不进行边界检测
this.x = (typeof this.x === 'number' && !isNaN(this.x) ? this.x : 0) + vx;
this.y = (typeof this.y === 'number' && !isNaN(this.y) ? this.y : 0) + vy;
// 移除边界碰撞检测,让小球可以自由移动
} catch (error) {
console.error('Ball.update() 方法执行失败:', error);
}
}
}
// 为颜色选择器添加事件监听
colorOptions.forEach(option => {
option.addEventListener('click', () => {
colorOptions.forEach(opt => opt.classList.remove('selected'));
option.classList.add('selected');
simulationState.selectedColor = option.getAttribute('data-color');
});
});
// 为全局数量控制添加事件监听器
const globalCountInput = document.getElementById('globalCountInput');
if (globalCountInput) {
// 更新全局数量的函数
function updateAllBallTypesCount(count) {
try {
count = parseInt(count) || 1;
// 确保数量在有效范围内
count = Math.max(1, Math.min(1000, count));
globalCountInput.value = count;
// 更新所有球类型的数量
if (simulationState && simulationState.ballTypes) {
for (var typeId in simulationState.ballTypes) {
if (simulationState.ballTypes.hasOwnProperty(typeId)) {
simulationState.ballTypes[typeId].defaultCount = count;
}
}
}
} catch (error) {
console.error('更新全局数量时出错:', error);
}
}
// 输入框变化时更新
globalCountInput.addEventListener('change', function() {
updateAllBallTypesCount(this.value);
});
}
// 为全局一键随机按钮添加事件监听器
const globalRandomBtn = document.getElementById('globalRandomBtn');
if (globalRandomBtn) {
globalRandomBtn.addEventListener('click', function() {
try {
// 遍历所有球类型
for (var typeId in simulationState.ballTypes) {
if (simulationState.ballTypes.hasOwnProperty(typeId)) {
var randomForces = {};
// 为每个目标球类型生成随机力参数
for (var otherTypeId in simulationState.ballTypes) {
if (simulationState.ballTypes.hasOwnProperty(otherTypeId)) {
// 生成-1到1之间的随机数,保留一位小数
var randomValue = (Math.random() * 2 - 1).toFixed(1);
randomForces[otherTypeId] = parseFloat(randomValue);
}
}
// 更新simulationState中的力参数
simulationState.ballTypes[typeId].forces = randomForces;
}
}
// 更新所有控制器的UI显示
if (window.forceControllers) {
window.forceControllers.forEach(controller => {
try {
controller.update();
} catch (error) {
console.error('更新力场控制器时出错:', error);
}
});
}
} catch (error) {
console.error('生成全局随机力参数时出错:', error);
}
});
}
// 默认选中第一个颜色
colorOptions[0].classList.add('selected');
// 获取颜色的中文名称
function getColorName(color) {
var colorNames = {
'#FF6B6B': '红色',
'#4ECDC4': '青色',
'#FFE66D': '黄色',
'#45B7D1': '蓝色',
'#96CEB4': '绿色',
'#FFEAA7': '黄色',
'#DDA0DD': '紫色',
'#FFA07A': '橙色',
'#1A535C': '深青色',
'#FF9F1C': '橙色',
'#7B2CBF': '紫色',
'#F08080': '浅红色',
'#000000': '黑色',
'#FFFFFF': '白色'
};
return colorNames[color] || color;
}
// 创建新的球类型控制器
function createBallTypeController(color) {
try {
if (!color || typeof color !== 'string') {
console.error('无效的颜色参数:', color);
return;
}
if (!simulationState) {
console.error('模拟状态未初始化');
return;
}
if (!ballTypeControls || ballTypeControls.nodeType !== Node.ELEMENT_NODE) {
console.error('球类型控制器容器不存在');
return;
}
var typeId = color;
if (!simulationState.ballTypes) {
simulationState.ballTypes = {};
}
if (simulationState.ballTypes[typeId]) {
return;
}
// 创建新的球类型参数
simulationState.ballTypes[typeId] = {
color: color,
defaultCount: 10,
forces: {}
};
// 创建控制器HTML
var controller = document.createElement('div');
controller.className = 'axis-controller';
controller.setAttribute('data-type-id', typeId);
var safeColor = color.replace(/"/g, '"');
var safeTypeId = typeId.replace(/"/g, '"').replace(/[#]/g, '');
var colorName = getColorName ? getColorName(color) : '未知颜色';
var safeColorName = colorName.replace(/"/g, '"');
controller.innerHTML =
'<div class="axis-controller-header">' +
'<div class="controller-color-indicator" style="background-color: ' + safeColor + ';" data-type-id="' + safeTypeId + '" data-color="' + safeColor + '"></div>' +
'<span>' + safeColorName + '</span>' +
'<button class="delete-color-btn" title="删除此颜色" style="background: none; border: none; color: red; cursor: pointer; font-size: 16px; margin-left: auto; position: relative; z-index: 10;">×</button>' +
'</div>' +
'<div class="force-controls" style="position: relative; z-index: 20;">' +
'<div class="force-inputs" id="force-inputs-' + safeTypeId + '">' +
'</div>' +
'</div>';
// 添加到容器
try {
ballTypeControls.appendChild(controller);
} catch (appendError) {
console.error('将控制器添加到容器失败:', appendError);
return;
}
// 使用全局数量控制,不再需要单独的数量输入事件监听
// 为删除按钮添加事件监听
var deleteBtn = controller.querySelector('.delete-color-btn');
if (deleteBtn) {
deleteBtn.addEventListener('click', function() {
try {
// 从simulationState中删除该颜色类型
if (simulationState && simulationState.ballTypes && simulationState.ballTypes[typeId]) {
delete simulationState.ballTypes[typeId];
// 从其他颜色类型的forces中移除对此颜色的引用
for (var otherTypeId in simulationState.ballTypes) {
if (simulationState.ballTypes.hasOwnProperty(otherTypeId) &&
simulationState.ballTypes[otherTypeId].forces &&
simulationState.ballTypes[otherTypeId].forces[typeId] !== undefined) {
delete simulationState.ballTypes[otherTypeId].forces[typeId];
}
}
// 更新所有控制器的forceInputs
for (var otherTypeId in simulationState.ballTypes) {
if (simulationState.ballTypes.hasOwnProperty(otherTypeId)) {
var otherSafeTypeId = otherTypeId.replace(/"/g, '"').replace(/[#]/g, '');
var otherController = document.querySelector('.axis-controller[data-type-id="' + otherSafeTypeId + '"]');
if (otherController) {
// 重新创建作用力输入框
var recreateForceInputs = function(otherTypeId) {
var otherSafeTypeId = otherTypeId.replace(/"/g, '"').replace(/[#]/g, '');
var forceInputsContainer = document.getElementById('force-inputs-' + otherSafeTypeId);
if (!forceInputsContainer) return;
forceInputsContainer.innerHTML = '';
for (var otherOtherTypeId in simulationState.ballTypes) {
if (simulationState.ballTypes.hasOwnProperty(otherOtherTypeId)) {
var otherOtherColorName = getColorName ? getColorName(otherOtherTypeId) : '未知颜色';
var otherOtherSafeTypeId = otherOtherTypeId.replace(/"/g, '"').replace(/[#]/g, '');
var forceControlItem = document.createElement('div');
forceControlItem.className = 'force-control-item';
forceControlItem.style.display = 'flex';
forceControlItem.style.alignItems = 'center';
forceControlItem.style.gap = '5px';
forceControlItem.style.minWidth = '150px';
var forceLabel = document.createElement('label');
forceLabel.htmlFor = 'force-' + otherSafeTypeId + '-to-' + otherOtherSafeTypeId;
forceLabel.style.display = 'flex';
forceLabel.style.alignItems = 'center';
forceLabel.style.gap = '5px';
forceLabel.style.fontSize = '12px';
var colorIndicator = document.createElement('div');
colorIndicator.className = 'force-color-indicator';
colorIndicator.style.width = '16px';
colorIndicator.style.height = '16px';
colorIndicator.style.borderRadius = '50%';
colorIndicator.style.backgroundColor = otherOtherTypeId;
colorIndicator.style.border = '1px solid #333';
var labelText = document.createTextNode('对' + otherOtherColorName + ':');
forceLabel.appendChild(colorIndicator);
forceLabel.appendChild(labelText);
var forceInput = document.createElement('input');
forceInput.type = 'number';
forceInput.id = 'force-' + otherSafeTypeId + '-to-' + otherOtherSafeTypeId;
forceInput.min = '-1';
forceInput.max = '1';
forceInput.step = '0.1';
forceInput.value = simulationState.ballTypes[otherTypeId].forces[otherOtherTypeId] || '0';
forceInput.style.width = '45px';
forceInput.style.padding = '3px';
forceInput.style.border = '1px solid #0f3460';
forceInput.style.borderRadius = '3px';
// 添加事件监听
(function(otherTypeId, otherOtherTypeId) {
forceInput.addEventListener('change', function() {
try {
var forceValue = parseFloat(this.value) || 0;
if (simulationState && simulationState.ballTypes && simulationState.ballTypes[otherTypeId]) {
simulationState.ballTypes[otherTypeId].forces = simulationState.ballTypes[otherTypeId].forces || {};
simulationState.ballTypes[otherTypeId].forces[otherOtherTypeId] = forceValue;
}
} catch (error) {
console.error('处理力输入变化时出错:', error);
}
});
})(otherTypeId, otherOtherTypeId);
forceControlItem.appendChild(forceLabel);
forceControlItem.appendChild(forceInput);
forceInputsContainer.appendChild(forceControlItem);
}
}
};
recreateForceInputs(otherTypeId);
}
}
}
// 从DOM中移除控制器
controller.remove();
// 清除画布上的所有小球
simulationState.balls = [];
// 直接开始新的渲染循环即可,不需要generateBalls函数
}
} catch (error) {
console.error('删除颜色时出错:', error);
}
});
}
// 创建作用力输入框
function createForceInputs() {
var forceInputsContainer = document.getElementById('force-inputs-' + safeTypeId);
if (!forceInputsContainer) return;
// 设置容器样式,使其能随label增多而变宽,并且label之间更紧凑
forceInputsContainer.style.display = 'flex';
forceInputsContainer.style.flexWrap = 'wrap';
forceInputsContainer.style.justifyContent = 'flex-start';
forceInputsContainer.style.gap = '5px'; // 减小间距使其更紧凑
forceInputsContainer.style.width = 'auto'; // 让容器宽度自适应内容
forceInputsContainer.innerHTML = '';
for (var otherTypeId in simulationState.ballTypes) {
if (simulationState.ballTypes.hasOwnProperty(otherTypeId)) {
var otherColorName = getColorName(otherTypeId);
var otherSafeTypeId = otherTypeId.replace(/"/g, '"').replace(/[#]/g, '');
var forceControlItem = document.createElement('div');
forceControlItem.className = 'force-control-item';
forceControlItem.style.display = 'flex';
forceControlItem.style.alignItems = 'center';
forceControlItem.style.gap = '5px';
// 移除固定宽度限制,让容器能随label增多而变宽
forceControlItem.style.minWidth = '150px'; // 保留最小宽度以确保可用性
var forceLabel = document.createElement('label');
forceLabel.htmlFor = 'force-' + safeTypeId + '-to-' + otherSafeTypeId;
forceLabel.style.display = 'flex';
forceLabel.style.alignItems = 'center';
forceLabel.style.gap = '5px';
forceLabel.style.fontSize = '12px';
var colorIndicator = document.createElement('div');
colorIndicator.className = 'force-color-indicator';
colorIndicator.style.width = '16px';
colorIndicator.style.height = '16px';
colorIndicator.style.borderRadius = '50%';
colorIndicator.style.backgroundColor = otherTypeId;
colorIndicator.style.border = '1px solid #333';
var labelText = document.createTextNode('对' + otherColorName + ':');
forceLabel.appendChild(colorIndicator);
forceLabel.appendChild(labelText);
var forceInput = document.createElement('input');
forceInput.type = 'number';
forceInput.id = 'force-' + safeTypeId + '-to-' + otherSafeTypeId;
forceInput.min = '-1';
forceInput.max = '1';
forceInput.step = '0.1';
forceInput.value = simulationState.ballTypes[typeId].forces[otherTypeId] || '0';
// 减小input宽度使其更紧凑
forceInput.style.width = '45px';
forceInput.style.padding = '3px';
forceInput.style.border = '1px solid #0f3460';
forceInput.style.borderRadius = '3px';
forceInput.style.backgroundColor = '#1a1a2e';
forceInput.style.color = '#e6e6e6';
forceInput.addEventListener('change', function(otherTypeId) {
return function() {
var value = parseFloat(this.value);
if (!isNaN(value)) {
value = Math.max(-1, Math.min(1, value));
this.value = value;
simulationState.ballTypes[typeId].forces[otherTypeId] = value;
} else {
this.value = '0';
simulationState.ballTypes[typeId].forces[otherTypeId] = 0;
}
};
}(otherTypeId));
forceControlItem.appendChild(forceLabel);
forceControlItem.appendChild(forceInput);
forceInputsContainer.appendChild(forceControlItem);
}
}
}
createForceInputs();
if (!window.forceControllers) {
window.forceControllers = [];
}
window.forceControllers.push({
typeId: typeId,
update: createForceInputs
});
} catch (error) {
console.error('创建球类型控制器时出错:', error);
}
}
// 当添加新球类型时,更新所有力场控制器
function onBallTypeAdded(typeId) {
if (window.forceControllers) {
window.forceControllers.forEach(controller => {
try {
controller.update();
} catch (error) {
console.error('更新力场控制器时出错:', error);
}
});
}
if (typeId && simulationState.ballTypes[typeId] && simulationState.ballTypes[typeId].forces) {
var newTypeParams = simulationState.ballTypes[typeId];
for (var existingTypeId in simulationState.ballTypes) {
if (simulationState.ballTypes.hasOwnProperty(existingTypeId)) {
if (!newTypeParams.forces[existingTypeId]) {
newTypeParams.forces[existingTypeId] = 0;
}
}
}
}
}
// 添加球的函数 - 在全局作用域中定义
function addBalls(color, count) {
var problemsDiagnostics = document.getElementById('problems_and_diagnostics');
console.log('addBalls函数被调用,参数:', {color, count});
try {
if (!color || typeof color !== 'string') {
var errorMsg = '无效的颜色参数: ' + color;
console.error(errorMsg);
if (problemsDiagnostics) {
problemsDiagnostics.innerHTML = '<span style="color: red;">错误: ' + errorMsg + '</span>';
}
return;
}
count = parseInt(count);
if (isNaN(count) || count <= 0) {
var errorMsg = '无效的数量参数: ' + count;
console.error(errorMsg);
if (problemsDiagnostics) {
problemsDiagnostics.innerHTML = '<span style="color: red;">错误: ' + errorMsg + '</span>';
}
return;
}
var typeId = color;
console.log('使用typeId:', typeId);
if (!simulationState) {
console.error('模拟状态未初始化');
if (problemsDiagnostics) {
problemsDiagnostics.innerHTML = '<span style="color: red;">错误: 模拟状态未初始化</span>';
}
return;
}
if (!simulationState.ballTypes) {
simulationState.ballTypes = {};
}
if (!simulationState.balls) {
simulationState.balls = [];
}
// 创建球类型控制器(如果不存在)
if (!simulationState.ballTypes[typeId]) {
try {
console.log('创建球类型控制器:', typeId);
createBallTypeController(color);
} catch (createError) {
var errorMsg = '创建球类型控制器失败: ' + createError.message;
console.error(errorMsg);
if (problemsDiagnostics) {
problemsDiagnostics.innerHTML = '<span style="color: red;">错误: ' + errorMsg + '</span>';
}
return;
}
}
// 生成球
console.log('开始生成', count, '个球');
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const ballRadius = 2;
// 初始化边界大小(确保大于初始视域)
if (simulationState.boundaryRadius === 0 || simulationState.boundarySize.width === 0) {
initializeBoundarySize();
}
// 生成真正均匀分布的点
function generateUniformPointInCircle(radius, centerX, centerY) {
// 使用Math.sqrt确保面积上的均匀分布,避免中心密集
const r = Math.sqrt(Math.random()) * (radius - ballRadius);
const angle = Math.random() * Math.PI * 2;
return {
x: centerX + Math.cos(angle) * r,
y: centerY + Math.sin(angle) * r
};
}
function generateUniformPointInSquare(width, height, centerX, centerY) {
// 线性均匀分布
const halfWidth = (width / 2) - ballRadius;
const halfHeight = (height / 2) - ballRadius;
return {
x: centerX + (Math.random() * 2 - 1) * halfWidth,
y: centerY + (Math.random() * 2 - 1) * halfHeight
};
}
function generateUniformPointNoBoundary() {
// 无边界时,在较大区域内均匀分布
const distributionWidth = canvas.width * 3; // 进一步扩大分布范围
const distributionHeight = canvas.height * 3;
return {
x: centerX + (Math.random() * 2 - 1) * distributionWidth,
y: centerY + (Math.random() * 2 - 1) * distributionHeight
};
}
// 空间分区算法确保在整个边界内均匀分布
function generateSpatialPartitionedPoints(count) {
const points = [];
const maxAttempts = 10000; // 防止无限循环
let attempts = 0;
// 根据边界类型确定分区数量
const partitions = Math.max(5, Math.ceil(Math.sqrt(count / 10)));
// 计算每个分区的目标点数
const targetPointsPerPartition = Math.ceil(count / (partitions * partitions));
// 为每个分区生成点
for (let px = 0; px < partitions; px++) {
for (let py = 0; py < partitions; py++) {
// 计算当前分区的点数量
const pointsInThisPartition = Math.min(targetPointsPerPartition, count - points.length);
if (pointsInThisPartition <= 0) break;
// 根据边界类型计算分区边界
let partitionMinX, partitionMaxX, partitionMinY, partitionMaxY;
switch(simulationState.boundaryType) {
case 'circle':
// 圆形边界的分区
const angleStart = (px / partitions) * Math.PI * 2;
const angleEnd = ((px + 1) / partitions) * Math.PI * 2;
const radiusStart = (py / partitions) * simulationState.boundaryRadius;
const radiusEnd = ((py + 1) / partitions) * simulationState.boundaryRadius;
// 在环形扇区生成点
for (let i = 0; i < pointsInThisPartition; i++) {
const r = radiusStart + Math.random() * (radiusEnd - radiusStart);
const angle = angleStart + Math.random() * (angleEnd - angleStart);
const x = centerX + Math.cos(angle) * r;
const y = centerY + Math.sin(angle) * r;
points.push({ x, y });
}
break;
case 'square':
// 方形边界的分区
const halfWidth = simulationState.boundarySize.width / 2;
const halfHeight = simulationState.boundarySize.height / 2;
partitionMinX = centerX - halfWidth + (px / partitions) * simulationState.boundarySize.width;
partitionMaxX = centerX - halfWidth + ((px + 1) / partitions) * simulationState.boundarySize.width;
partitionMinY = centerY - halfHeight + (py / partitions) * simulationState.boundarySize.height;
partitionMaxY = centerY - halfHeight + ((py + 1) / partitions) * simulationState.boundarySize.height;
// 在分区内生成点
for (let i = 0; i < pointsInThisPartition; i++) {
const x = partitionMinX + Math.random() * (partitionMaxX - partitionMinX);
const y = partitionMinY + Math.random() * (partitionMaxY - partitionMinY);
points.push({ x, y });
}
break;
default:
// 无边界时使用方形分区
partitionMinX = centerX - canvas.width * 1.5 + (px / partitions) * canvas.width * 3;
partitionMaxX = centerX - canvas.width * 1.5 + ((px + 1) / partitions) * canvas.width * 3;
partitionMinY = centerY - canvas.height * 1.5 + (py / partitions) * canvas.height * 3;
partitionMaxY = centerY - canvas.height * 1.5 + ((py + 1) / partitions) * canvas.height * 3;
for (let i = 0; i < pointsInThisPartition; i++) {
const x = partitionMinX + Math.random() * (partitionMaxX - partitionMinX);
const y = partitionMinY + Math.random() * (partitionMaxY - partitionMinY);
points.push({ x, y });
}
}
}
// 检查是否已生成足够的点
if (points.length >= count) break;
}
// 如果生成的点不足,补充生成
while (points.length < count && attempts < maxAttempts) {
let point;
switch(simulationState.boundaryType) {
case 'circle':
point = generateUniformPointInCircle(simulationState.boundaryRadius, centerX, centerY);
break;
case 'square':
point = generateUniformPointInSquare(simulationState.boundarySize.width, simulationState.boundarySize.height, centerX, centerY);
break;
default:
point = generateUniformPointNoBoundary();
}
points.push(point);
attempts++;
}
return points;
}
// 直接使用空间分区算法生成均匀分布的点
const points = generateSpatialPartitionedPoints(count);
// 创建球
for (let i = 0; i < points.length; i++) {
try {
const { x, y } = points[i];
var newBall = new Ball(x, y, color, typeId);
simulationState.balls.push(newBall);
} catch (ballError) {
console.error('创建单个球失败:', ballError);
}
}
console.log('球生成完成,当前球总数:', simulationState.balls.length);
if (problemsDiagnostics) {
problemsDiagnostics.innerHTML = '';
}
if (typeof clearCanvas === 'function') {
clearCanvas();
}
} catch (error) {
console.error('addBalls函数执行错误:', error);
var errorMsg = '添加球函数执行失败: ' + error.message;
if (problemsDiagnostics) {
problemsDiagnostics.innerHTML = '<span style="color: red;">错误: ' + errorMsg + '</span>';
}
}
}
// 确保函数在全局作用域中可用
window.addBalls = addBalls;
// 指数衰减力场计算函数 - 全局范围内力随距离指数衰减
function calculateForce(ball1, ball2) {
var dx = ball2.x - ball1.x;
var dy = ball2.y - ball1.y;
var distanceSq = dx * dx + dy * dy;
// 如果距离太近,使用最小距离避免除零错误
if (distanceSq < 1) {
distanceSq = 1;
}
var distance = Math.sqrt(distanceSq);
// 获取球类型参数
var params1 = simulationState.ballTypes[ball1.type];
var params2 = simulationState.ballTypes[ball2.type];
if (!params1 || !params2) {
return { fx: 0, fy: 0 };
}
// 用户设置的作用力
var userForce1 = params1.forces && params1.forces[ball2.type] || 0;
// 对同颜色球添加额外的吸引力,保持同色聚合特性
var isSameColor = ball1.type === ball2.type;
var forceStrength = 0;
// 指数衰减力场核心逻辑:全局范围内力随距离指数衰减
// 同颜色球的吸引力(除非用户明确设置了很强的排斥力)
if (isSameColor && userForce1 > -0.5) {
// 同色球的引力场 - 指数衰减模型
var baseAttractionStrength = 800;
var decayFactor = 0.015; // 控制衰减速度
// 指数衰减公式:力 = 基础强度 * e^(-距离 * 衰减系数)
forceStrength = baseAttractionStrength * Math.exp(-distance * decayFactor);
} else {
// 用户设置的力场,保持正值吸引、负值排斥的逻辑
if (userForce1 !== 0) {
// 指数衰减力场:全局范围内力随距离指数衰减
var baseForceStrength = userForce1 > 0 ? 600 : 800;
var decayFactor = 0.02; // 控制衰减速度
// 指数衰减公式:力 = 基础强度 * e^(-距离 * 衰减系数)
forceStrength = (userForce1 > 0 ? 1 : -1) * baseForceStrength * Math.abs(userForce1) * Math.exp(-distance * decayFactor);
}
}
// 计算力的分量,确保力的方向正确
var fx = (dx / distance) * forceStrength;
var fy = (dy / distance) * forceStrength;
return { fx: fx, fy: fy };
}
// 空间隔断机制 - 结合圆形力场和硬性边界效果
function maintainSeparation() {
var balls = simulationState.balls;
var hardBoundaryDistance = 6; // 硬性边界距离(空气墙)
var repulsionDistance = 15; // 圆形力场斥力作用距离
var maxRepulsionForce = 30; // 最大斥力强度
for (var i = 0; i < balls.length; i++) {
var ballA = balls[i];
if (!ballA) continue;
for (var j = i + 1; j < balls.length; j++) {
var ballB = balls[j];
if (!ballB) continue;
var dx = ballB.x - ballA.x;
var dy = ballB.y - ballA.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// 1. 硬性边界 - 空气墙效果(绝对不可越过)
if (distance < hardBoundaryDistance && distance > 0) {
// 计算需要移动的距离,确保两球之间至少保持hardBoundaryDistance
var separation = hardBoundaryDistance - distance;
var unitX = dx / distance;
var unitY = dy / distance;
// 直接调整位置,创建硬性边界效果
ballA.x -= unitX * separation * 0.5;
ballA.y -= unitY * separation * 0.5;
ballB.x += unitX * separation * 0.5;
ballB.y += unitY * separation * 0.5;
// 额外增加速度调整,使碰撞更自然
var bounceFactor = 0.3;
ballA.vx -= unitX * bounceFactor;
ballA.vy -= unitY * bounceFactor;
ballB.vx += unitX * bounceFactor;
ballB.vy += unitY * bounceFactor;
}
// 2. 圆形力场斥力模型(在硬性边界之外,但在斥力范围内)
else if (distance >= hardBoundaryDistance && distance < repulsionDistance) {
// 计算斥力强度,基于距离的倒数平方关系,形成完美的圆形力场
// 从hardBoundaryDistance开始,距离越小,斥力越大
var relativeDistance = distance - hardBoundaryDistance;
var maxRelativeDistance = repulsionDistance - hardBoundaryDistance;
var forceFactor = 1 - (relativeDistance / maxRelativeDistance); // 0到1之间
var forceMagnitude = maxRepulsionForce * Math.pow(forceFactor, 2);
// 计算单位方向向量
var unitX = dx / distance;
var unitY = dy / distance;
// 计算斥力分量
var repulsionX = unitX * forceMagnitude;
var repulsionY = unitY * forceMagnitude;
// 应用斥力(注意方向相反),增大斥力系数使效果更明显
ballA.vx = (ballA.vx || 0) - repulsionX * 0.0001;
ballA.vy = (ballA.vy || 0) - repulsionY * 0.0001;
ballB.vx = (ballB.vx || 0) + repulsionX * 0.0001;
ballB.vy = (ballB.vy || 0) + repulsionY * 0.0001;
}
}
}
}
// 清空画布
function clearCanvas() {
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// 简化的动画循环
function animate() {
try {
if (!simulationState) {
animationId = requestAnimationFrame(animate);
return;
}
// 物理更新只在模拟运行时执行
if (simulationState.isRunning) {
// 考虑速度倍率,可能需要执行多次物理更新
var speedMultiplier = simulationState.speedMultiplier || 1;
var updateSteps = Math.ceil(speedMultiplier);
var stepFactor = speedMultiplier / updateSteps;
// 执行多次物理更新以实现快进效果
for (var step = 0; step < updateSteps; step++) {
if (Array.isArray(simulationState.balls)) {
var forces = [];
for (var i = 0; i < simulationState.balls.length; i++) {
forces[i] = { fx: 0, fy: 0 };
}
var balls = simulationState.balls;
var ballA, ballB;
var force;
// 简化的力计算:只考虑附近球体
for (var i = 0; i < balls.length; i++) {
ballA = balls[i];
if (!ballA) continue;
for (var j = 0; j < balls.length; j++) {
if (i === j) continue;
ballB = balls[j];
if (!ballB) continue;
var dx = ballB.x - ballA.x;
var dy = ballB.y - ballA.y;
var distance = Math.sqrt(dx * dx + dy * dy);
// 全局范围内计算相互作用,使用指数衰减模型
force = calculateForce(ballA, ballB);
if (force && typeof force.fx === 'number' && typeof force.fy === 'number') {
forces[i].fx += force.fx;
forces[i].fy += force.fy;
}
}
}
var balls = simulationState.balls;
var ball;
var maxSpeed = 4;
var speedLimitSq = maxSpeed * maxSpeed;
for (var i = 0; i < balls.length; i++) {
ball = balls[i];
if (!ball) continue;
if (forces[i]) {
// 应用力并添加速度阻尼,考虑步长因子
// 由于使用指数衰减模型,调整力的强度参数
var forceMultiplier = 0.000005 * stepFactor;
ball.vx = ((ball.vx || 0) * 0.97) + (forces[i].fx * forceMultiplier);
ball.vy = ((ball.vy || 0) * 0.97) + (forces[i].fy * forceMultiplier);
}
var vx = ball.vx || 0;
var vy = ball.vy || 0;
var speedSq = vx * vx + vy * vy;
if (speedSq > speedLimitSq) {
var factor = maxSpeed / Math.sqrt(speedSq);
ball.vx = vx * factor;
ball.vy = vy * factor;
}
// 考虑步长因子更新位置
ball.x = (ball.x || 0) + ball.vx * stepFactor;
ball.y = (ball.y || 0) + ball.vy * stepFactor;
// 应用边界碰撞检测
checkBoundaryCollision(ball);
}
// 应用空间隔断机制
maintainSeparation();
}
}
}
// 无论模拟是否运行,都执行渲染,这样可以在开始前控制视图
if (typeof clearCanvas === 'function') {
clearCanvas();
}
if (ctx && Array.isArray(simulationState.balls)) {
var viewOffset = simulationState.viewOffset || {x: 0, y: 0};
var offsetX = viewOffset.x || 0;
var offsetY = viewOffset.y || 0;
var viewScale = simulationState.viewScale || 1.0;
ctx.save();
// 应用缩放变换
ctx.translate(offsetX + canvas.width / 2, offsetY + canvas.height / 2);
ctx.scale(viewScale, viewScale);
ctx.translate(-canvas.width / 2, -canvas.height / 2);
var balls = simulationState.balls;
var ball;
for (var i = 0; i < balls.length; i++) {
ball = balls[i];
if (ball) {
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.radius || 2, 0, Math.PI * 2);
ctx.fillStyle = ball.color || '#ffffff';
ctx.fill();
ctx.closePath();
}
}
// 绘制边界
drawBoundary();
ctx.restore();
}
} catch (animateError) {
console.error('动画循环执行失败:', animateError);
}
try {
animationId = requestAnimationFrame(animate);
} catch (requestError) {
console.error('请求下一帧动画失败:', requestError);
setTimeout(animate, 16);
}
}
// 重置模拟
function resetSimulation() {
simulationState.isRunning = false;
simulationState.balls = [];
simulationState.viewOffset = { x: 0, y: 0 };
simulationState.viewScale = 1.0;
// 重置速度
simulationState.speedMultiplier = 1;
if (speedSlider) speedSlider.value = 1;
if (speedDisplay) speedDisplay.textContent = '1x';
// 初始化边界大小,确保大于初始视域
initializeBoundarySize();
// 如果是随机边界,重新生成
if (simulationState.boundaryType === 'random') {
generateRandomBoundary();
}
for (var typeId in simulationState.ballTypes) {
if (simulationState.ballTypes.hasOwnProperty(typeId)) {
if (!simulationState.ballTypes[typeId].affinities) {
simulationState.ballTypes[typeId].affinities = [];
}
}
}
clearCanvas();
// 获取全局数量设置
var globalCount = 10; // 默认值
var globalCountInput = document.getElementById('globalCountInput');
if (globalCountInput) {
globalCount = parseInt(globalCountInput.value);
if (isNaN(globalCount) || globalCount <= 0) {
globalCount = 10;
}
// 确保全局数量应用到所有球类型
if (simulationState.ballTypes) {
for (var ballTypeId in simulationState.ballTypes) {
simulationState.ballTypes[ballTypeId].defaultCount = globalCount;
}
}
}
for (var typeId in simulationState.ballTypes) {
if (simulationState.ballTypes.hasOwnProperty(typeId)) {
var color = typeId;
// 使用全局设置的数量
var count = globalCount;
if (typeof window.addBalls === 'function') {
window.addBalls(color, count);
} else {
var errorMsg = '添加球时出错: addBalls函数未定义';
console.error(errorMsg);
var problemsDiagnostics = document.getElementById('problems_and_diagnostics');
if (problemsDiagnostics) {
problemsDiagnostics.innerHTML = '<span style="color: red;">错误: ' + errorMsg + '</span>';
}
}
}
}
}
// 事件监听器
startBtn.addEventListener('click', function() {
simulationState.isRunning = true;
});
pauseBtn.addEventListener('click', function() {
simulationState.isRunning = false;
});
resetBtn.addEventListener('click', resetSimulation);
addBallsBtn.addEventListener('click', function() {
try {
if (!simulationState) {
console.error('模拟状态未初始化');
return;
}
var color = simulationState.selectedColor;
if (!color) {
console.error('未选择颜色');
return;
}
var typeId = color;
// 优先使用全局数量控制输入框的值
var count = 10; // 默认值
var globalCountInput = document.getElementById('globalCountInput');
if (globalCountInput) {
count = parseInt(globalCountInput.value);
// 确保数量有效
if (isNaN(count) || count <= 0) {
count = 10;
}
}
// 将全局数量应用到所有球类型
if (globalCountInput && simulationState.ballTypes) {
for (var ballTypeId in simulationState.ballTypes) {
simulationState.ballTypes[ballTypeId].defaultCount = count;
}
}
if (!isNaN(count) && count > 0) {
if (typeof window.addBalls === 'function') {
window.addBalls(color, count);
} else {
var errorMsg = '添加球时出错: addBalls函数未定义';
console.error(errorMsg);
var problemsDiagnostics = document.getElementById('problems_and_diagnostics');
if (problemsDiagnostics) {
problemsDiagnostics.innerHTML = '<span style="color: red;">错误: ' + errorMsg + '</span>';
}
}
onBallTypeAdded(typeId);
if (typeof clearCanvas === 'function') {
clearCanvas();
if (ctx && typeof ctx.save === 'function') {
ctx.save();
var offsetX = 0;
var offsetY = 0;
if (simulationState.viewOffset) {
offsetX = simulationState.viewOffset.x || 0;
offsetY = simulationState.viewOffset.y || 0;
}
ctx.translate(offsetX, offsetY);
if (Array.isArray(simulationState.balls)) {
for (var i = 0; i < simulationState.balls.length; i++) {
var ball = simulationState.balls[i];
if (ball && typeof ball.draw === 'function') {
ball.draw();
}
}
}
ctx.restore();
}
}
}
} catch (error) {
console.error('添加球时出错:', error);
}
});
// 视图拖动功能
canvas.addEventListener('mousedown', function(e) {
var rect = canvas.getBoundingClientRect();
simulationState.isDragging = true;
simulationState.lastMousePos = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
e.preventDefault();
});
document.addEventListener('mousemove', function(e) {
if (simulationState.isDragging) {
var rect = canvas.getBoundingClientRect();
var currentX = e.clientX - rect.left;
var currentY = e.clientY - rect.top;
var dx = currentX - simulationState.lastMousePos.x;
var dy = currentY - simulationState.lastMousePos.y;
simulationState.viewOffset.x += dx;
simulationState.viewOffset.y += dy;
simulationState.lastMousePos = { x: currentX, y: currentY };
}
});
document.addEventListener('mouseup', function() {
simulationState.isDragging = false;
});
document.addEventListener('mouseleave', function() {
simulationState.isDragging = false;
});
// 边界类型选择事件监听
const boundaryTypeSelect = document.getElementById('boundaryType');
if (boundaryTypeSelect) {
boundaryTypeSelect.addEventListener('change', function() {
simulationState.boundaryType = this.value;
});
}
// 滚轮缩放功能
canvas.addEventListener('wheel', function(e) {
e.preventDefault();
var rect = canvas.getBoundingClientRect();
var mouseX = e.clientX - rect.left;
var mouseY = e.clientY - rect.top;
// 缩放前的鼠标位置(世界坐标)
var worldX = (mouseX - canvas.width / 2 - simulationState.viewOffset.x) / simulationState.viewScale + canvas.width / 2;
var worldY = (mouseY - canvas.height / 2 - simulationState.viewOffset.y) / simulationState.viewScale + canvas.height / 2;
// 计算缩放因子
var scaleFactor = e.deltaY > 0 ? 0.9 : 1.1;
var newScale = simulationState.viewScale * scaleFactor;
// 限制缩放范围
newScale = Math.max(0.1, Math.min(5.0, newScale));
simulationState.viewScale = newScale;
// 调整偏移量,使鼠标指向的点在缩放后保持不变
simulationState.viewOffset.x = mouseX - canvas.width / 2 - (worldX - canvas.width / 2) * newScale;
simulationState.viewOffset.y = mouseY - canvas.height / 2 - (worldY - canvas.height / 2) * newScale;
});
// 速度控制事件监听
const speedSlider = document.getElementById('speedSlider');
const speedDisplay = document.getElementById('speedDisplay');
if (speedSlider && speedDisplay) {
speedSlider.addEventListener('input', function() {
simulationState.speedMultiplier = parseInt(this.value);
speedDisplay.textContent = simulationState.speedMultiplier + 'x';
});
}
// 重置模拟时重置速度和缩放
const originalResetSimulation = resetSimulation;
resetSimulation = function() {
originalResetSimulation();
simulationState.speedMultiplier = 1;
simulationState.viewScale = 1.0;
if (speedSlider) speedSlider.value = 1;
if (speedDisplay) speedDisplay.textContent = '1x';
};
// 边界碰撞检测函数
function checkBoundaryCollision(ball) {
if (!ball) return;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const ballRadius = ball.radius || 2;
switch(simulationState.boundaryType) {
case 'circle':
// 圆形边界碰撞检测
const dx = ball.x - centerX;
const dy = ball.y - centerY;
const distance = Math.sqrt(dx * dx + dy * dy);
const maxDistance = simulationState.boundaryRadius - ballRadius;
if (distance > maxDistance) {
// 计算碰撞后的反弹
const angle = Math.atan2(dy, dx);
ball.x = centerX + Math.cos(angle) * maxDistance;
ball.y = centerY + Math.sin(angle) * maxDistance;
// 反弹速度计算
const normalX = dx / distance;
const normalY = dy / distance;
const dotProduct = ball.vx * normalX + ball.vy * normalY;
ball.vx = (ball.vx - 2 * dotProduct * normalX) * 0.8; // 0.8是弹性系数
ball.vy = (ball.vy - 2 * dotProduct * normalY) * 0.8;
}
break;
case 'square':
// 方形边界碰撞检测
const halfWidth = simulationState.boundarySize.width / 2;
const halfHeight = simulationState.boundarySize.height / 2;
const leftBound = centerX - halfWidth + ballRadius;
const rightBound = centerX + halfWidth - ballRadius;
const topBound = centerY - halfHeight + ballRadius;
const bottomBound = centerY + halfHeight - ballRadius;
// X方向碰撞
if (ball.x < leftBound) {
ball.x = leftBound;
ball.vx = -ball.vx * 0.8;
} else if (ball.x > rightBound) {
ball.x = rightBound;
ball.vx = -ball.vx * 0.8;
}
// Y方向碰撞
if (ball.y < topBound) {
ball.y = topBound;
ball.vy = -ball.vy * 0.8;
} else if (ball.y > bottomBound) {
ball.y = bottomBound;
ball.vy = -ball.vy * 0.8;
}
break;
}
}
// 检测线段与圆的碰撞
function checkLineCircleCollision(p1, p2, ball) {
const ballRadius = ball.radius || 2;
// 计算线段向量
const lineVecX = p2.x - p1.x;
const lineVecY = p2.y - p1.y;
// 计算球到线段起点的向量
const circleToLineStartX = ball.x - p1.x;
const circleToLineStartY = ball.y - p1.y;
// 计算投影长度
const lineLengthSq = lineVecX * lineVecX + lineVecY * lineVecY;
const dotProduct = circleToLineStartX * lineVecX + circleToLineStartY * lineVecY;
const projection = Math.max(0, Math.min(1, dotProduct / lineLengthSq));
// 计算投影点
const projectionX = p1.x + projection * lineVecX;
const projectionY = p1.y + projection * lineVecY;
// 计算球到投影点的距离
const dx = ball.x - projectionX;
const dy = ball.y - projectionY;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance <= ballRadius) {
// 计算法线向量(单位向量)
const normalX = dx / distance;
const normalY = dy / distance;
// 计算反弹后的速度
const dotProductWithNormal = ball.vx * normalX + ball.vy * normalY;
const reflectX = ball.vx - 2 * dotProductWithNormal * normalX;
const reflectY = ball.vy - 2 * dotProductWithNormal * normalY;
return { collided: true, normalX, normalY, reflectX, reflectY };
}
return { collided: false };
}
// 初始化边界大小,确保明显大于页面加载时的视域
function initializeBoundarySize() {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// 设置边界大小为初始视域的2倍,确保明显大于页面首次加载时的视域
const boundaryMultiplier = 2.0;
// 更新圆形边界半径
const maxViewportSize = Math.max(canvas.width, canvas.height);
simulationState.boundaryRadius = (maxViewportSize / 2) * boundaryMultiplier;
// 更新方形边界大小
simulationState.boundarySize = {
width: canvas.width * boundaryMultiplier,
height: canvas.height * boundaryMultiplier
};
}
// 绘制边界
function drawBoundary() {
if (!ctx) return;
ctx.save();
ctx.strokeStyle = 'rgba(74, 124, 255, 0.7)';
ctx.lineWidth = 2;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
switch(simulationState.boundaryType) {
case 'circle':
// 绘制圆形边界
ctx.beginPath();
ctx.arc(centerX, centerY, simulationState.boundaryRadius, 0, Math.PI * 2);
ctx.stroke();
break;
case 'square':
// 绘制方形边界
const halfWidth = simulationState.boundarySize.width / 2;
const halfHeight = simulationState.boundarySize.height / 2;
ctx.strokeRect(
centerX - halfWidth,
centerY - halfHeight,
simulationState.boundarySize.width,
simulationState.boundarySize.height
);
break;
}
ctx.restore();
}
// 初始化动画循环
animate();
</script>
</body>
</html>
3096

被折叠的 条评论
为什么被折叠?



