<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
<title>蚁群算法模拟 - 1080x1920 手机适配版</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background-color: #000;
position: fixed;
top: 0; left: 0;
font-family: Arial, sans-serif;
}
#app-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 1080px;
height: 1920px;
}
#gameCanvas {
position: absolute;
top: 0; left: 0;
width: 1080px;
height: 1920px;
image-rendering: crisp-edges;
border-radius: 12px;
}
.controls {
position: absolute;
top: 1700px;
left: 0;
width: 1080px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 14px;
font-size: 30px;
color: #333;
background-color: rgba(240, 240, 240, 0.9);
pointer-events: auto;
z-index: 10;
border-radius: 12px 12px 0 0;
}
.slider-control {
display: flex;
align-items: center;
gap: 10px;
}
label {
display: inline-block;
width: 38%;
min-width: 200px;
}
input[type="range"] {
flex: 1;
height: 16px;
-webkit-appearance: none;
background: #ddd;
border-radius: 8px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 40px;
height: 40px;
background: #007BFF;
border-radius: 50%;
cursor: pointer;
}
span.value {
display: inline-block;
width: 18%;
min-width: 80px;
text-align: right;
font-weight: bold;
color: #007BFF;
}
/* 按钮统一样式 */
button {
padding: 16px 0;
background-color: #28a745;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 34px;
width: 48%; /* 两个按钮并排 */
margin: 0;
transition: background-color 0.3s ease;
}
#homeButton {
background-color: #007BFF;
}
#homeButton:hover,
#resetButton:hover {
background-color: #0056b3;
}
#resetButton:hover {
background-color: #218838;
}
</style>
</head>
<body>
<div id="app-container">
<!-- 主画布 -->
<canvas id="gameCanvas"></canvas>
<!-- 控制面板 -->
<div class="controls">
<div class="slider-control">
<label for="totalAnts">蚂蚁总数:</label>
<input type="range" id="totalAnts" min="400" max="1000" value="600" step="50" />
<span class="value" id="totalAntsValue">600</span>
</div>
<div class="slider-control">
<label for="persistence">信息素持久:</label>
<input type="range" id="persistence" min="1" max="5" value="2" step="0.1" />
<span class="value" id="persistenceValue">x2</span>
</div>
<!-- 两个按钮并排 -->
<div style="display: flex; justify-content: space-around; width: 100%;">
<button id="resetButton">重置系统</button>
<button id="homeButton">返回首页</button>
</div>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// === 尺寸参数(1080x1920 手机竖屏)===
const TARGET_WIDTH = 1080;
const TARGET_HEIGHT = 1920; // ✅ 标准手机比例
const TITLE_HEIGHT = 120; // 标题栏
const CANVAS_AREA_HEIGHT = 1500; // 主可视化区域
const INFO_PANEL_HEIGHT = 80; // 图例+统计区
// 设备像素比适配
const dpr = window.devicePixelRatio || 1;
canvas.width = TARGET_WIDTH * dpr;
canvas.height = TARGET_HEIGHT * dpr;
ctx.scale(dpr, dpr);
canvas.style.width = `${TARGET_WIDTH}px`;
canvas.style.height = `${TARGET_HEIGHT}px`;
// === 固定内部参数 ===
const PHEROMONE_BOOST = 80;
const TRAIL_EVAP_BASE = 20;
// === 全局变量 ===
let nest = {}, food = {};
let obstacleInstances = [];
const NEST_RADIUS = 20;
const FOOD_RADIUS = 20;
const OBSTACLE_WIDTH = 200;
const OBSTACLE_HEIGHT = 50;
const OBSTACLE_RADIUS = 15;
// === 信息素网格 ===
const PHEROMONE_RES = 15;
const GRID_COLS = Math.ceil(TARGET_WIDTH / PHEROMONE_RES);
const GRID_ROWS = Math.ceil(CANVAS_AREA_HEIGHT / PHEROMONE_RES);
let trailGrid = Array(GRID_COLS).fill(null).map(() => Array(GRID_ROWS).fill(0));
// === 统计数据 ===
let solvedAnts = 0;
let totalAntsCount = 0;
let lastBirthTime = 0;
let antIdCounter = 0;
let ants = [];
// === 工具函数 ===
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function randint(min, max) {
return Math.floor(rand(min, max));
}
// === 障碍物类 ===
class Obstacle {
constructor(data) {
Object.assign(this, data);
this.width = this.width || OBSTACLE_WIDTH;
this.height = this.height || OBSTACLE_HEIGHT;
this.angle = this.angle || 0;
}
draw() {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.fillStyle = '#555';
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
this.roundRect(-this.width / 2, -this.height / 2, this.width, this.height, OBSTACLE_RADIUS);
ctx.fill();
ctx.stroke();
ctx.restore();
}
roundRect(x, y, w, h, r) {
if (r > Math.min(w, h) / 2) r = Math.min(w, h) / 2;
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
}
containsPoint(px, py) {
const dx = px - this.x;
const dy = py - this.y;
const cosA = Math.cos(-this.angle);
const sinA = Math.sin(-this.angle);
const rx = dx * cosA - dy * sinA;
const ry = dx * sinA + dy * cosA;
return Math.abs(rx) < this.width / 2 && Math.abs(ry) < this.height / 2;
}
}
// === 初始化环境 ===
function resetEnvironment() {
ants = [];
trailGrid.forEach(row => row.fill(0));
solvedAnts = 0;
totalAntsCount = 0;
antIdCounter = 0;
lastBirthTime = performance.now();
const minY = TITLE_HEIGHT + INFO_PANEL_HEIGHT + FOOD_RADIUS + 60;
const maxY = TITLE_HEIGHT + CANVAS_AREA_HEIGHT - NEST_RADIUS - 60;
nest = {
x: rand(NEST_RADIUS + 100, TARGET_WIDTH - NEST_RADIUS - 100),
y: rand(maxY - 160, maxY)
};
food = {
x: rand(FOOD_RADIUS + 100, TARGET_WIDTH - FOOD_RADIUS - 100),
y: rand(minY, minY + 100)
};
const midY = (nest.y + food.y) / 2;
const centerX = (nest.x + food.x) / 2;
const gapSize = rand(120, 240);
const offsetFromCenter = rand(-100, 100);
const leftX = centerX - gapSize / 2 - 100 + offsetFromCenter;
const rightX = centerX + gapSize / 2 + 100 + offsetFromCenter;
const heightVariation = rand(-80, 80);
obstacleInstances = [
new Obstacle({ x: leftX, y: midY, angle: 0 }),
new Obstacle({ x: leftX, y: midY - 100 + heightVariation, angle: rand(-0.2, 0.2) }),
new Obstacle({ x: rightX, y: midY, angle: 0 }),
new Obstacle({
x: centerX + offsetFromCenter,
y: midY - 180,
angle: rand(-Math.PI / 6, Math.PI / 6)
}),
new Obstacle({
x: rand(200, centerX - 100),
y: TITLE_HEIGHT + 300,
width: 150,
height: 40,
angle: rand(-Math.PI / 8, Math.PI / 8)
}),
new Obstacle({
x: rand(centerX + 100, TARGET_WIDTH - 200),
y: TITLE_HEIGHT + CANVAS_AREA_HEIGHT - 300,
width: 150,
height: 40,
angle: rand(-Math.PI / 8, Math.PI / 8)
}),
new Obstacle({
x: centerX + rand(-150, 150),
y: midY + rand(-100, 100),
width: 20,
height: 120,
angle: 0
}),
new Obstacle({
x: rand(300, TARGET_WIDTH - 300),
y: rand(TITLE_HEIGHT + 500, CANVAS_AREA_HEIGHT + TITLE_HEIGHT - 500),
width: 80,
height: 80,
angle: rand(-Math.PI / 6, Math.PI / 6)
})
];
}
// === 蚂蚁类 ===
class Ant {
constructor() {
this.id = antIdCounter++;
this.x = nest.x;
this.y = nest.y;
this.angle = Math.random() * Math.PI * 2;
this.hasFood = false;
this.alreadyCounted = false;
this.stuckTimer = 0;
this.lastX = this.x;
this.lastY = this.y;
this.backtrackSteps = 0;
this.maxStuckSteps = 60;
this.ownPheromone = new Set();
}
distanceTo(x, y) {
return Math.hypot(this.x - x, this.y - y);
}
getTrailAt(x, y) {
const gx = Math.floor(x / PHEROMONE_RES);
const gy = Math.floor((y - TITLE_HEIGHT) / PHEROMONE_RES);
if (gx >= 0 && gx < GRID_COLS && gy >= 0 && gy < GRID_ROWS) {
const key = `${gx}-${gy}`;
return this.ownPheromone.has(key) ? 0 : trailGrid[gx][gy];
}
return 0;
}
leaveTrail(amount) {
const gx = Math.floor(this.x / PHEROMONE_RES);
const gy = Math.floor((this.y - TITLE_HEIGHT) / PHEROMONE_RES);
if (gx >= 0 && gx < GRID_COLS && gy >= 0 && gy < GRID_ROWS) {
trailGrid[gx][gy] += amount;
this.ownPheromone.add(`${gx}-${gy}`);
}
}
checkCollision() {
return obstacleInstances.some(obs => obs.containsPoint(this.x, this.y));
}
avoidObstacles() {
const senseDist = 30;
const offsets = [-Math.PI/6, -Math.PI/8, 0, Math.PI/8, Math.PI/6].sort(() => Math.random() - 0.5);
for (let offset of offsets) {
const tx = this.x + Math.cos(this.angle + offset) * senseDist;
const ty = this.y + Math.sin(this.angle + offset) * senseDist;
if (!obstacleInstances.some(obs => obs.containsPoint(tx, ty))) {
this.angle += offset * 0.6;
return;
}
}
this.angle += (Math.random() - 0.5) * Math.PI / 6;
}
update() {
const speed = 2.0;
const senseDist = 40;
if (this.distanceTo(this.lastX, this.lastY) < 1.5) this.stuckTimer++;
else this.stuckTimer = 0;
this.lastX = this.x;
this.lastY = this.y;
if (this.stuckTimer > this.maxStuckSteps) {
if (this.backtrackSteps <= 0) {
this.backtrackSteps = 20;
this.angle += Math.PI + (Math.random() - 0.5) * 0.5;
} else {
this.x += Math.cos(this.angle) * speed * 1.5;
this.y += Math.sin(this.angle) * speed * 1.5;
this.backtrackSteps--;
return;
}
} else {
this.backtrackSteps = 0;
}
if (this.checkCollision()) this.avoidObstacles();
if (!this.hasFood && this.distanceTo(food.x, food.y) < FOOD_RADIUS + 5) {
this.hasFood = true;
if (!this.alreadyCounted) {
solvedAnts++;
this.alreadyCounted = true;
}
this.ownPheromone.clear();
}
if (this.hasFood && this.distanceTo(nest.x, nest.y) < NEST_RADIUS + 5) {
this.hasFood = false;
this.alreadyCounted = false;
this.angle = Math.random() * Math.PI * 2;
this.ownPheromone.clear();
return;
}
if (this.hasFood) {
const targetAngle = Math.atan2(nest.y - this.y, nest.x - this.x);
const diff = ((targetAngle - this.angle + Math.PI * 3) % (Math.PI * 2)) - Math.PI;
if (Math.abs(diff) > 0.1) this.angle += Math.sign(diff) * 0.08;
else this.angle += (Math.random() - 0.5) * 0.1;
const angles = [];
for (let i = 0; i < 7; i++) {
const t = (i - 3) / 6;
const offset = t * Math.PI * 0.7;
const px = this.x + Math.cos(this.angle + offset) * senseDist;
const py = this.y + Math.sin(this.angle + offset) * senseDist;
angles.push({ offset, strength: this.getTrailAt(px, py) });
}
angles.sort((a, b) => b.strength - a.strength);
if (angles[0].strength > angles[1].strength * 1.3 && Math.random() < 0.6) {
this.angle += angles[0].offset * 0.4;
}
this.leaveTrail(PHEROMONE_BOOST / 40);
} else {
if (Math.random() < 0.4) this.angle += (Math.random() - 0.5) * 0.4;
}
const nx = this.x + Math.cos(this.angle) * speed;
const ny = this.y + Math.sin(this.angle) * speed;
if (!obstacleInstances.some(obs => obs.containsPoint(nx, ny))) {
this.x = nx;
this.y = ny;
} else {
this.avoidObstacles();
}
if (this.x <= 0 || this.x >= TARGET_WIDTH) this.angle = Math.PI - this.angle;
if (this.y <= TITLE_HEIGHT || this.y >= TITLE_HEIGHT + CANVAS_AREA_HEIGHT) this.angle = -this.angle;
this.angle = ((this.angle + Math.PI * 2) % (Math.PI * 2));
}
draw() {
ctx.fillStyle = this.hasFood ? '#FF4500' : '#333';
ctx.beginPath();
ctx.arc(this.x, this.y, 4, 0, Math.PI * 2);
ctx.fill();
}
}
// 初始化
resetEnvironment();
// === 更新UI显示 ===
function updateUI() {
document.getElementById('totalAntsValue').textContent = document.getElementById('totalAnts').value;
document.getElementById('persistenceValue').textContent = 'x' + document.getElementById('persistence').value;
}
['totalAnts', 'persistence'].forEach(id =>
document.getElementById(id).addEventListener('input', updateUI)
);
document.getElementById('resetButton').addEventListener('click', resetEnvironment);
// 设置“返回首页”按钮跳转到指定本地文件
document.getElementById('homeButton').addEventListener('click', function () {
window.location.href = 'file:///Users/wanghanyi/Desktop/自然算法/Algorithm%20Visualizer-2.html';
});
updateUI();
// === 响应式缩放适配 ===
function resizeToFit() {
const container = document.getElementById('app-container');
const w = window.innerWidth;
const h = window.innerHeight;
const scale = Math.min(w / TARGET_WIDTH, h / TARGET_HEIGHT);
container.style.transform = `translate(-50%, -50%) scale(${scale})`;
}
window.addEventListener('resize', resizeToFit);
resizeToFit();
setTimeout(resizeToFit, 100);
// === 主动画循环 ===
function animate() {
const now = performance.now();
ctx.clearRect(0, 0, TARGET_WIDTH, TARGET_HEIGHT);
// 背景
ctx.fillStyle = '#f9f9f9';
ctx.fillRect(0, 0, TARGET_WIDTH, TARGET_HEIGHT);
// 标题栏
ctx.fillStyle = '#007BFF';
ctx.fillRect(0, 0, TARGET_WIDTH, TITLE_HEIGHT);
ctx.fillStyle = 'white';
ctx.font = 'bold 52px Arial';
ctx.textAlign = 'center';
ctx.fillText('蚁群算法', TARGET_WIDTH / 2, TITLE_HEIGHT - 30);
// 图例
const items = [
{ color: '#007BFF', label: '巢穴' },
{ color: '#FF8C00', label: '食物源' },
{ shape: 'rect', color: '#555', stroke: '#333', label: '障碍物' },
{ color: '#333', label: '探索中' },
{ color: '#FF4500', label: '携带食物' }
];
const legendStartX = 30;
const legendStartY = TITLE_HEIGHT + 20;
for (let i = 0; i < items.length; i++) {
const row = Math.floor(i / 3);
const col = i % 3;
const x = legendStartX + col * 200;
const y = legendStartY + row * 50;
const item = items[i];
if (item.shape === 'rect') {
ctx.fillStyle = item.color;
ctx.fillRect(x, y - 10, 20, 15);
ctx.strokeStyle = item.stroke || '#000';
ctx.lineWidth = 1.5;
ctx.strokeRect(x, y - 10, 20, 15);
} else {
ctx.fillStyle = item.color;
ctx.beginPath();
ctx.arc(x + 10, y, 8, 0, Math.PI * 2);
ctx.fill();
}
ctx.fillStyle = 'black';
ctx.font = '26px Arial';
ctx.textAlign = 'left';
ctx.fillText(item.label, x + 30, y + 8);
}
// 统计信息
const rate = totalAntsCount ? ((solvedAnts / totalAntsCount) * 100).toFixed(1) : 0;
ctx.textAlign = 'right';
ctx.font = 'bold 36px Arial';
ctx.fillStyle = 'black';
ctx.fillText(`数量: ${ants.length}`, TARGET_WIDTH - 30, TITLE_HEIGHT + 40);
ctx.fillText(`成功率: ${rate}%`, TARGET_WIDTH - 30, TITLE_HEIGHT + 80);
// 信息素绘制与衰减
const persistenceFactor = parseFloat(document.getElementById('persistence').value);
const baseEvapRate = (TRAIL_EVAP_BASE / 1000) / persistenceFactor;
for (let x = 0; x < GRID_COLS; x++) {
for (let y = 0; y < GRID_ROWS; y++) {
const val = trailGrid[x][y];
if (val > 0.01) {
const alpha = Math.min(val * 0.06, 0.9);
const r = Math.floor(150 + 105 * Math.min(val * 0.04, 1));
const g = Math.floor(80 + 75 * Math.min(val * 0.025, 1));
ctx.fillStyle = `rgba(${r}, ${g}, 0, ${alpha})`;
ctx.fillRect(x * PHEROMONE_RES, TITLE_HEIGHT + y * PHEROMONE_RES, PHEROMONE_RES, PHEROMONE_RES);
trailGrid[x][y] = val * (1 - baseEvapRate) * 0.995;
if (trailGrid[x][y] < 0.01) trailGrid[x][y] = 0;
}
}
}
// 绘制障碍物、巢穴、食物
obstacleInstances.forEach(obs => obs.draw());
ctx.fillStyle = '#007BFF';
ctx.beginPath();
ctx.arc(nest.x, nest.y, NEST_RADIUS, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#FF8C00';
ctx.beginPath();
ctx.arc(food.x, food.y, FOOD_RADIUS, 0, Math.PI * 2);
ctx.fill();
// 出生控制
const maxTotal = parseInt(document.getElementById('totalAnts').value);
const birthInterval = 100;
if (ants.length < maxTotal && now - lastBirthTime > birthInterval) {
ants.push(new Ant());
totalAntsCount++;
lastBirthTime = now;
}
// 更新所有蚂蚁
ants.forEach(ant => {
ant.update();
ant.draw();
});
requestAnimationFrame(animate);
}
lastBirthTime = performance.now();
animate();
</script>
</body>
</html>
请将程序中的巢穴和食物的圆放大,然后在中间各自加上emoji
最新发布