407 lines
15 KiB
HTML
407 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Cellular Automata Map Generator</title>
|
|
<style>
|
|
body {
|
|
font-family: 'Arial', sans-serif;
|
|
margin: 0;
|
|
padding: 20px;
|
|
background: linear-gradient(135deg, #2c3e50, #3498db);
|
|
min-height: 100vh;
|
|
color: white;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
backdrop-filter: blur(10px);
|
|
border-radius: 15px;
|
|
padding: 30px;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
h1 {
|
|
text-align: center;
|
|
margin-bottom: 30px;
|
|
font-size: 2.5em;
|
|
background: linear-gradient(45deg, #f39c12, #e74c3c);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.controls {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
margin-bottom: 30px;
|
|
padding: 20px;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.control-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
label {
|
|
font-weight: bold;
|
|
color: #ecf0f1;
|
|
font-size: 14px;
|
|
}
|
|
|
|
input, select, button {
|
|
padding: 10px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
input, select {
|
|
background: rgba(255, 255, 255, 0.9);
|
|
color: #2c3e50;
|
|
}
|
|
|
|
input:focus, select:focus {
|
|
outline: none;
|
|
box-shadow: 0 0 10px rgba(52, 152, 219, 0.5);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
button {
|
|
background: linear-gradient(45deg, #e74c3c, #f39c12);
|
|
color: white;
|
|
cursor: pointer;
|
|
font-weight: bold;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
button:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 6px 20px rgba(231, 76, 60, 0.4);
|
|
}
|
|
|
|
button:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.canvas-container {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
canvas {
|
|
border: 3px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 10px;
|
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
canvas:hover {
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.info {
|
|
text-align: center;
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
background: rgba(46, 204, 113, 0.1);
|
|
border-radius: 8px;
|
|
border-left: 4px solid #2ecc71;
|
|
}
|
|
|
|
.legend {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 30px;
|
|
margin-top: 15px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.legend-color {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 4px;
|
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
}
|
|
|
|
@keyframes generate {
|
|
0% { transform: scale(1); }
|
|
50% { transform: scale(0.95); }
|
|
100% { transform: scale(1); }
|
|
}
|
|
|
|
.generating {
|
|
animation: generate 0.5s ease-in-out;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>Cellular Automata Map Generator</h1>
|
|
|
|
<div class="controls">
|
|
<div class="control-group">
|
|
<label for="width">Width:</label>
|
|
<input type="number" id="width" value="100" min="20" max="200">
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label for="height">Height:</label>
|
|
<input type="number" id="height" value="80" min="20" max="150">
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label for="fillPercent">Fill Percent:</label>
|
|
<input type="range" id="fillPercent" value="45" min="20" max="80">
|
|
<span id="fillValue">45%</span>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label for="iterations">Iterations:</label>
|
|
<input type="number" id="iterations" value="5" min="1" max="20">
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label for="wallThreshold">Wall Threshold:</label>
|
|
<input type="number" id="wallThreshold" value="4" min="1" max="8">
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<label for="mapType">Map Type:</label>
|
|
<select id="mapType">
|
|
<option value="cave">Cave System</option>
|
|
<option value="island">Island Terrain</option>
|
|
<option value="maze">Maze-like</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<button onclick="generateMap()">Generate Map</button>
|
|
</div>
|
|
|
|
<div class="control-group">
|
|
<button onclick="animateGeneration()">Animate Process</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="canvas-container">
|
|
<canvas id="mapCanvas" width="800" height="640"></canvas>
|
|
</div>
|
|
|
|
<div class="legend">
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #34495e;"></div>
|
|
<span>Wall</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #ecf0f1;"></div>
|
|
<span>Floor</span>
|
|
</div>
|
|
<div class="legend-item">
|
|
<div class="legend-color" style="background: #3498db;"></div>
|
|
<span>Water (Island mode)</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="info">
|
|
<strong>How it works:</strong> Cellular automata uses simple rules applied iteratively. Each cell becomes a wall or floor based on its neighbors.
|
|
This creates organic, cave-like structures perfect for game maps!
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
class CellularAutomataGenerator {
|
|
constructor(width, height) {
|
|
this.width = width;
|
|
this.height = height;
|
|
this.map = [];
|
|
this.canvas = document.getElementById('mapCanvas');
|
|
this.ctx = this.canvas.getContext('2d');
|
|
this.cellSize = Math.min(800 / width, 640 / height);
|
|
|
|
// Adjust canvas size
|
|
this.canvas.width = width * this.cellSize;
|
|
this.canvas.height = height * this.cellSize;
|
|
}
|
|
|
|
initializeMap(fillPercent, mapType) {
|
|
this.map = [];
|
|
for (let x = 0; x < this.width; x++) {
|
|
this.map[x] = [];
|
|
for (let y = 0; y < this.height; y++) {
|
|
if (x === 0 || x === this.width - 1 || y === 0 || y === this.height - 1) {
|
|
this.map[x][y] = 1; // Border walls
|
|
} else {
|
|
let fillChance = fillPercent / 100;
|
|
|
|
if (mapType === 'island') {
|
|
// Distance from center affects probability
|
|
let centerX = this.width / 2;
|
|
let centerY = this.height / 2;
|
|
let distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
|
|
let maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2);
|
|
fillChance *= (1 - distance / maxDistance) * 1.5;
|
|
} else if (mapType === 'maze') {
|
|
fillChance *= 0.6; // Lower fill for maze-like structures
|
|
}
|
|
|
|
this.map[x][y] = Math.random() < fillChance ? 1 : 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
smoothMap(wallThreshold) {
|
|
let newMap = [];
|
|
|
|
for (let x = 0; x < this.width; x++) {
|
|
newMap[x] = [];
|
|
for (let y = 0; y < this.height; y++) {
|
|
let neighborWalls = this.getNeighborWallCount(x, y);
|
|
|
|
if (neighborWalls > wallThreshold) {
|
|
newMap[x][y] = 1;
|
|
} else if (neighborWalls < wallThreshold) {
|
|
newMap[x][y] = 0;
|
|
} else {
|
|
newMap[x][y] = this.map[x][y];
|
|
}
|
|
}
|
|
}
|
|
|
|
this.map = newMap;
|
|
}
|
|
|
|
getNeighborWallCount(gridX, gridY) {
|
|
let wallCount = 0;
|
|
|
|
for (let neighborX = gridX - 1; neighborX <= gridX + 1; neighborX++) {
|
|
for (let neighborY = gridY - 1; neighborY <= gridY + 1; neighborY++) {
|
|
if (neighborX >= 0 && neighborX < this.width && neighborY >= 0 && neighborY < this.height) {
|
|
if (neighborX !== gridX || neighborY !== gridY) {
|
|
wallCount += this.map[neighborX][neighborY];
|
|
}
|
|
} else {
|
|
wallCount++; // Out of bounds counts as wall
|
|
}
|
|
}
|
|
}
|
|
|
|
return wallCount;
|
|
}
|
|
|
|
drawMap(mapType) {
|
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
|
|
for (let x = 0; x < this.width; x++) {
|
|
for (let y = 0; y < this.height; y++) {
|
|
let color;
|
|
if (this.map[x][y] === 1) {
|
|
color = '#34495e'; // Wall
|
|
} else {
|
|
if (mapType === 'island' && this.isNearEdge(x, y, 5)) {
|
|
color = '#3498db'; // Water for island edges
|
|
} else {
|
|
color = '#ecf0f1'; // Floor
|
|
}
|
|
}
|
|
|
|
this.ctx.fillStyle = color;
|
|
this.ctx.fillRect(x * this.cellSize, y * this.cellSize, this.cellSize, this.cellSize);
|
|
|
|
// Add subtle border for better visibility
|
|
this.ctx.strokeStyle = 'rgba(0,0,0,0.1)';
|
|
this.ctx.lineWidth = 0.5;
|
|
this.ctx.strokeRect(x * this.cellSize, y * this.cellSize, this.cellSize, this.cellSize);
|
|
}
|
|
}
|
|
}
|
|
|
|
isNearEdge(x, y, distance) {
|
|
return x < distance || x >= this.width - distance ||
|
|
y < distance || y >= this.height - distance;
|
|
}
|
|
|
|
async generateWithAnimation(fillPercent, iterations, wallThreshold, mapType) {
|
|
this.initializeMap(fillPercent, mapType);
|
|
this.drawMap(mapType);
|
|
|
|
await this.delay(500);
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
this.smoothMap(wallThreshold);
|
|
this.drawMap(mapType);
|
|
await this.delay(300);
|
|
}
|
|
}
|
|
|
|
delay(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
}
|
|
|
|
let generator;
|
|
|
|
// Update fill percent display
|
|
document.getElementById('fillPercent').addEventListener('input', function() {
|
|
document.getElementById('fillValue').textContent = this.value + '%';
|
|
});
|
|
|
|
function generateMap() {
|
|
const width = parseInt(document.getElementById('width').value);
|
|
const height = parseInt(document.getElementById('height').value);
|
|
const fillPercent = parseInt(document.getElementById('fillPercent').value);
|
|
const iterations = parseInt(document.getElementById('iterations').value);
|
|
const wallThreshold = parseInt(document.getElementById('wallThreshold').value);
|
|
const mapType = document.getElementById('mapType').value;
|
|
|
|
generator = new CellularAutomataGenerator(width, height);
|
|
generator.initializeMap(fillPercent, mapType);
|
|
|
|
for (let i = 0; i < iterations; i++) {
|
|
generator.smoothMap(wallThreshold);
|
|
}
|
|
|
|
generator.drawMap(mapType);
|
|
|
|
// Add generation animation
|
|
document.getElementById('mapCanvas').classList.add('generating');
|
|
setTimeout(() => {
|
|
document.getElementById('mapCanvas').classList.remove('generating');
|
|
}, 500);
|
|
}
|
|
|
|
async function animateGeneration() {
|
|
const width = parseInt(document.getElementById('width').value);
|
|
const height = parseInt(document.getElementById('height').value);
|
|
const fillPercent = parseInt(document.getElementById('fillPercent').value);
|
|
const iterations = parseInt(document.getElementById('iterations').value);
|
|
const wallThreshold = parseInt(document.getElementById('wallThreshold').value);
|
|
const mapType = document.getElementById('mapType').value;
|
|
|
|
generator = new CellularAutomataGenerator(width, height);
|
|
await generator.generateWithAnimation(fillPercent, iterations, wallThreshold, mapType);
|
|
}
|
|
|
|
// Generate initial map
|
|
window.addEventListener('load', generateMap);
|
|
</script>
|
|
</body>
|
|
</html> |