terrain / index.html
aiqtech's picture
Update index.html
78ba202 verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>๋””์ง€ํ„ธ ๋“ฑ๊ณ ์„  ์ง€ํ˜• ์ง€๋„</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 1400px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 10px;
font-size: 2em;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.map-container {
position: relative;
margin: 20px auto;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
background: #f0f0f0;
}
canvas {
display: block;
cursor: crosshair;
width: 100%;
height: auto;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin: 20px 0;
flex-wrap: wrap;
align-items: center;
}
button {
padding: 12px 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
button:active {
transform: translateY(0);
}
.zoom-controls {
display: flex;
align-items: center;
gap: 10px;
background: white;
padding: 8px 15px;
border-radius: 25px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.zoom-btn {
width: 35px;
height: 35px;
padding: 0;
border-radius: 50%;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.scale-info {
font-size: 14px;
color: #666;
font-weight: 600;
min-width: 100px;
text-align: center;
}
.info-panel {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 15px;
border-radius: 10px;
margin-top: 20px;
display: flex;
justify-content: space-around;
flex-wrap: wrap;
gap: 15px;
}
.info-item {
text-align: center;
padding: 10px;
background: white;
border-radius: 8px;
min-width: 120px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.info-label {
font-size: 12px;
color: #666;
margin-bottom: 5px;
text-transform: uppercase;
letter-spacing: 1px;
}
.info-value {
font-size: 18px;
font-weight: bold;
color: #333;
}
.legend {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.legend-color {
width: 30px;
height: 20px;
border-radius: 4px;
border: 1px solid #ddd;
}
.legend-symbol {
font-size: 20px;
margin-right: 5px;
}
.tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
pointer-events: none;
z-index: 1000;
display: none;
transition: all 0.2s ease;
}
select {
padding: 10px 15px;
border-radius: 8px;
border: 2px solid #667eea;
background: white;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
select:hover {
border-color: #764ba2;
}
select:focus {
outline: none;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.map-size-info {
text-align: center;
color: #666;
font-size: 14px;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>๐Ÿ—บ๏ธ ๋””์ง€ํ„ธ ๋“ฑ๊ณ ์„  ์ง€ํ˜• ์ง€๋„</h1>
<div class="controls">
<button onclick="generateNewTerrain()">๐Ÿ”๏ธ ์ƒˆ๋กœ์šด ์ง€ํ˜• ์ƒ์„ฑ</button>
<button onclick="toggleContours()">๐Ÿ“Š ๋“ฑ๊ณ ์„  ํ‘œ์‹œ/์ˆจ๊ธฐ๊ธฐ</button>
<button onclick="toggleHeatmap()">๐ŸŒก๏ธ ํžˆํŠธ๋งต ์ „ํ™˜</button>
<button onclick="toggleRoads()">๐Ÿ›ฃ๏ธ ๋„๋กœ ํ‘œ์‹œ/์ˆจ๊ธฐ๊ธฐ</button>
<button onclick="toggleRivers()">๐Ÿ’ง ํ•˜์ฒœ ํ‘œ์‹œ/์ˆจ๊ธฐ๊ธฐ</button>
<select id="terrainType" onchange="changeTerrainType()">
<option value="mountain">โ›ฐ๏ธ ์‚ฐ์•… ์ง€ํ˜•</option>
<option value="valley">๐Ÿž๏ธ ๊ณ„๊ณก ์ง€ํ˜•</option>
<option value="plateau">๐Ÿ”๏ธ ๊ณ ์› ์ง€ํ˜•</option>
<option value="rural">๐Ÿ˜๏ธ ์ „์› ์ง€ํ˜•</option>
<option value="urban">๐Ÿ™๏ธ ๋„์‹œ ํ‰์›</option>
</select>
<div class="zoom-controls">
<button class="zoom-btn" onclick="zoomOut()">โˆ’</button>
<span class="scale-info" id="scaleInfo">1:5000</span>
<button class="zoom-btn" onclick="zoomIn()">+</button>
</div>
<button onclick="exportMap()">๐Ÿ’พ ์ง€๋„ ์ €์žฅ</button>
</div>
<div class="map-container">
<canvas id="mapCanvas"></canvas>
<div class="tooltip" id="tooltip"></div>
</div>
<div class="map-size-info" id="mapSizeInfo">
์ง€๋„ ๋ฒ”์œ„: 4km ร— 3km
</div>
<div class="info-panel">
<div class="info-item">
<div class="info-label">์ขŒํ‘œ</div>
<div class="info-value" id="coordinates">-</div>
</div>
<div class="info-item">
<div class="info-label">๊ณ ๋„</div>
<div class="info-value" id="elevation">-</div>
</div>
<div class="info-item">
<div class="info-label">๊ฒฝ์‚ฌ๋„</div>
<div class="info-value" id="slope">-</div>
</div>
<div class="info-item">
<div class="info-label">์‹ค์ œ ๊ฑฐ๋ฆฌ</div>
<div class="info-value" id="realDistance">-</div>
</div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background: #0d4f8b;"></div>
<span>์ˆ˜์—ญ (0-50m)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #61a861;"></div>
<span>ํ‰์ง€ (50-200m)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #f5deb3;"></div>
<span>๊ตฌ๋ฆ‰ (200-500m)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #8b4513;"></div>
<span>์‚ฐ์ง€ (500m+)</span>
</div>
<div class="legend-item">
<span class="legend-symbol">โ”</span>
<span>๋„๋กœ</span>
</div>
<div class="legend-item">
<span class="legend-symbol" style="color: #4682B4;">ใ€ฐ๏ธ</span>
<span>ํ•˜์ฒœ</span>
</div>
<div class="legend-item">
<span class="legend-symbol">๐Ÿ˜๏ธ</span>
<span>๋งˆ์„</span>
</div>
<div class="legend-item">
<span class="legend-symbol">๐Ÿข</span>
<span>๋„์‹œ</span>
</div>
</div>
</div>
<script>
const canvas = document.getElementById('mapCanvas');
const ctx = canvas.getContext('2d');
const tooltip = document.getElementById('tooltip');
// ์บ”๋ฒ„์Šค ํฌ๊ธฐ ์„ค์ •
const WIDTH = 800;
const HEIGHT = 600;
canvas.width = WIDTH;
canvas.height = HEIGHT;
// ์ถ•์ฒ™ ๊ด€๋ จ ๋ณ€์ˆ˜
const scales = [2500, 5000, 10000, 25000, 50000];
let currentScaleIndex = 1; // ๊ธฐ๋ณธ 1:5000
let currentScale = scales[currentScaleIndex];
let zoomLevel = 1; // ์คŒ ๋ ˆ๋ฒจ (0.5 ~ 4)
let offsetX = 0; // ๋ทฐํฌํŠธ ์˜คํ”„์…‹
let offsetY = 0;
// ์ง€ํ˜• ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ๋ณ€์ˆ˜๋“ค
let heightMap = [];
let roads = [];
let rivers = [];
let settlements = [];
let showContours = true;
let showHeatmap = false;
let showRoads = true;
let showRivers = true;
let terrainType = 'mountain';
let seed = Math.random() * 10000;
let isDragging = false;
let dragStartX = 0;
let dragStartY = 0;
// ๊ฐœ์„ ๋œ ๋…ธ์ด์ฆˆ ํ•จ์ˆ˜
function noise(x, y, scale, octaves, seed) {
let value = 0;
let amplitude = 1;
let frequency = scale;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
value += amplitude * simpleNoise(x * frequency + seed, y * frequency + seed);
maxValue += amplitude;
amplitude *= 0.5;
frequency *= 2;
}
return value / maxValue;
}
function simpleNoise(x, y) {
const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
return (n - Math.floor(n)) * 2 - 1;
}
// ์ง€ํ˜• ์ƒ์„ฑ ํ•จ์ˆ˜
function generateTerrain() {
seed = Math.random() * 10000; // ์ƒˆ๋กœ์šด ์‹œ๋“œ ์ƒ์„ฑ
heightMap = [];
roads = [];
rivers = [];
settlements = [];
const gridSize = 100;
// ๋†’์ด๋งต ์ƒ์„ฑ
for (let y = 0; y < gridSize; y++) {
heightMap[y] = [];
for (let x = 0; x < gridSize; x++) {
let height = 0;
switch(terrainType) {
case 'mountain':
height = noise(x, y, 0.02, 6, seed) * 0.6;
height += noise(x, y, 0.05, 4, seed + 100) * 0.3;
height += noise(x, y, 0.1, 2, seed + 200) * 0.1;
height = Math.pow(Math.abs(height), 1.2) * Math.sign(height);
break;
case 'valley':
const distX = (x - gridSize/2) / gridSize;
const distY = (y - gridSize/2) / gridSize;
const dist = Math.sqrt(distX*distX + distY*distY);
height = noise(x, y, 0.03, 4, seed) * 0.5;
height *= (1 - dist * 0.5);
height -= dist * 0.3;
break;
case 'plateau':
height = noise(x, y, 0.02, 3, seed) * 0.3;
if (height > 0.1) height = 0.3 + noise(x, y, 0.05, 2, seed + 100) * 0.1;
break;
case 'rural':
// ์ „์› ์ง€ํ˜• - ์™„๋งŒํ•œ ๊ตฌ๋ฆ‰๊ณผ ํ‰์ง€
height = noise(x, y, 0.015, 4, seed) * 0.2;
height += noise(x, y, 0.03, 2, seed + 100) * 0.1;
height = Math.max(0.1, height); // ์ตœ์†Œ ๋†’์ด ๋ณด์žฅ
break;
case 'urban':
// ๋„์‹œ ํ‰์› - ๋งค์šฐ ํ‰ํƒ„ํ•œ ์ง€ํ˜•
height = noise(x, y, 0.01, 2, seed) * 0.1 + 0.15;
break;
}
// ์ •๊ทœํ™” (0-1 ๋ฒ”์œ„)
height = Math.max(0, Math.min(1, (height + 1) / 2));
heightMap[y][x] = height;
}
}
// ๋„๋กœ ์ƒ์„ฑ
generateRoads(gridSize);
// ํ•˜์ฒœ ์ƒ์„ฑ
generateRivers(gridSize);
// ์ •์ฐฉ์ง€ ์ƒ์„ฑ
generateSettlements(gridSize);
}
// ๋„๋กœ ์ƒ์„ฑ
function generateRoads(gridSize) {
roads = [];
const numRoads = terrainType === 'urban' ? 8 : (terrainType === 'rural' ? 5 : 3);
for (let i = 0; i < numRoads; i++) {
const road = [];
let x = Math.random() * gridSize;
let y = Math.random() * gridSize;
const targetX = Math.random() * gridSize;
const targetY = Math.random() * gridSize;
// A* ๋˜๋Š” ๊ฐ„๋‹จํ•œ ๊ฒฝ๋กœ ์ฐพ๊ธฐ
for (let step = 0; step < 50; step++) {
road.push({ x: Math.floor(x), y: Math.floor(y) });
// ๋‚ฎ์€ ๊ณ ๋„ ์„ ํ˜ธ
const dx = targetX - x;
const dy = targetY - y;
const angle = Math.atan2(dy, dx);
// ๊ฒฝ์‚ฌ๋ฅผ ๊ณ ๋ คํ•œ ์ด๋™
let bestAngle = angle;
let minSlope = Infinity;
for (let a = -Math.PI/4; a <= Math.PI/4; a += Math.PI/8) {
const testAngle = angle + a;
const nx = x + Math.cos(testAngle) * 2;
const ny = y + Math.sin(testAngle) * 2;
if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) {
const slope = Math.abs(heightMap[Math.floor(ny)][Math.floor(nx)] -
heightMap[Math.floor(y)][Math.floor(x)]);
if (slope < minSlope) {
minSlope = slope;
bestAngle = testAngle;
}
}
}
x += Math.cos(bestAngle) * 2;
y += Math.sin(bestAngle) * 2;
if (Math.abs(x - targetX) < 2 && Math.abs(y - targetY) < 2) break;
}
roads.push(road);
}
}
// ํ•˜์ฒœ ์ƒ์„ฑ
function generateRivers(gridSize) {
rivers = [];
const numRivers = terrainType === 'valley' ? 3 : 2;
for (let i = 0; i < numRivers; i++) {
const river = [];
// ๋†’์€ ๊ณณ์—์„œ ์‹œ์ž‘
let x = Math.random() * gridSize;
let y = Math.random() * gridSize;
let highestPoint = heightMap[Math.floor(y)][Math.floor(x)];
// ๋” ๋†’์€ ์ง€์  ์ฐพ๊ธฐ
for (let j = 0; j < 10; j++) {
const tx = Math.random() * gridSize;
const ty = Math.random() * gridSize;
if (heightMap[Math.floor(ty)][Math.floor(tx)] > highestPoint) {
x = tx;
y = ty;
highestPoint = heightMap[Math.floor(ty)][Math.floor(tx)];
}
}
// ๋‚ฎ์€ ๊ณณ์œผ๋กœ ํ๋ฅด๊ธฐ
for (let step = 0; step < 100; step++) {
river.push({ x: Math.floor(x), y: Math.floor(y) });
// ๊ฐ€์žฅ ๋‚ฎ์€ ์ด์›ƒ ์ฐพ๊ธฐ
let lowestHeight = heightMap[Math.floor(y)][Math.floor(x)];
let nextX = x, nextY = y;
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
if (dx === 0 && dy === 0) continue;
const nx = Math.floor(x + dx);
const ny = Math.floor(y + dy);
if (nx >= 0 && nx < gridSize && ny >= 0 && ny < gridSize) {
if (heightMap[ny][nx] < lowestHeight) {
lowestHeight = heightMap[ny][nx];
nextX = x + dx + (Math.random() - 0.5) * 0.5;
nextY = y + dy + (Math.random() - 0.5) * 0.5;
}
}
}
}
if (nextX === x && nextY === y) break; // ๋” ์ด์ƒ ๋‚ฎ์€ ๊ณณ์ด ์—†์Œ
x = nextX;
y = nextY;
if (lowestHeight < 0.2) break; // ๋ฌผ์— ๋„๋‹ฌ
}
rivers.push(river);
}
}
// ์ •์ฐฉ์ง€ ์ƒ์„ฑ
function generateSettlements(gridSize) {
settlements = [];
if (terrainType === 'rural') {
// ๋งˆ์„ ์ƒ์„ฑ
const numVillages = 5 + Math.floor(Math.random() * 5);
for (let i = 0; i < numVillages; i++) {
let x, y;
do {
x = Math.random() * gridSize;
y = Math.random() * gridSize;
} while (heightMap[Math.floor(y)][Math.floor(x)] > 0.5 ||
heightMap[Math.floor(y)][Math.floor(x)] < 0.15);
settlements.push({
x: Math.floor(x),
y: Math.floor(y),
type: 'village',
size: 3 + Math.random() * 3
});
}
} else if (terrainType === 'urban') {
// ๋„์‹œ ์ƒ์„ฑ
const numCities = 2 + Math.floor(Math.random() * 2);
for (let i = 0; i < numCities; i++) {
let x = (i + 1) * gridSize / (numCities + 1) + (Math.random() - 0.5) * 20;
let y = gridSize / 2 + (Math.random() - 0.5) * 30;
settlements.push({
x: Math.floor(x),
y: Math.floor(y),
type: 'city',
size: 10 + Math.random() * 10
});
}
// ๋„์‹œ ์ฃผ๋ณ€ ์ž‘์€ ๋งˆ์„๋“ค
const numTowns = 5 + Math.floor(Math.random() * 5);
for (let i = 0; i < numTowns; i++) {
let x = Math.random() * gridSize;
let y = Math.random() * gridSize;
settlements.push({
x: Math.floor(x),
y: Math.floor(y),
type: 'town',
size: 5 + Math.random() * 5
});
}
}
}
// ์ง€๋„ ๊ทธ๋ฆฌ๊ธฐ
function drawMap() {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
// ์คŒ๊ณผ ์˜คํ”„์…‹ ์ ์šฉ
ctx.save();
ctx.translate(WIDTH/2, HEIGHT/2);
ctx.scale(zoomLevel, zoomLevel);
ctx.translate(-WIDTH/2 + offsetX, -HEIGHT/2 + offsetY);
const gridSize = heightMap.length;
const cellWidth = WIDTH / gridSize;
const cellHeight = HEIGHT / gridSize;
// ๋ฐฐ๊ฒฝ ๊ทธ๋ฆฌ๊ธฐ
for (let y = 0; y < gridSize; y++) {
for (let x = 0; x < gridSize; x++) {
const height = heightMap[y][x];
if (showHeatmap) {
const hue = (1 - height) * 240;
ctx.fillStyle = `hsl(${hue}, 70%, 50%)`;
} else {
ctx.fillStyle = getTerrainColor(height);
}
ctx.fillRect(
x * cellWidth,
y * cellHeight,
cellWidth + 1,
cellHeight + 1
);
}
}
// ํ•˜์ฒœ ๊ทธ๋ฆฌ๊ธฐ
if (showRivers) {
rivers.forEach(river => {
ctx.strokeStyle = '#4682B4';
ctx.lineWidth = 2;
ctx.beginPath();
river.forEach((point, index) => {
const x = point.x * cellWidth;
const y = point.y * cellHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
});
}
// ๋„๋กœ ๊ทธ๋ฆฌ๊ธฐ
if (showRoads) {
roads.forEach(road => {
ctx.strokeStyle = '#555';
ctx.lineWidth = 2;
ctx.setLineDash([5, 3]);
ctx.beginPath();
road.forEach((point, index) => {
const x = point.x * cellWidth;
const y = point.y * cellHeight;
if (index === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
});
ctx.stroke();
ctx.setLineDash([]);
});
}
// ๋“ฑ๊ณ ์„  ๊ทธ๋ฆฌ๊ธฐ
if (showContours) {
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
ctx.lineWidth = 1;
const contourLevels = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
for (let level of contourLevels) {
ctx.beginPath();
for (let y = 0; y < gridSize - 1; y++) {
for (let x = 0; x < gridSize - 1; x++) {
const corners = [
heightMap[y][x],
heightMap[y][x + 1],
heightMap[y + 1][x + 1],
heightMap[y + 1][x]
];
drawContourCell(
x * cellWidth,
y * cellHeight,
cellWidth,
cellHeight,
corners,
level
);
}
}
if (level % 0.2 === 0) {
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
} else {
ctx.lineWidth = 1;
ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
}
ctx.stroke();
}
}
// ์ •์ฐฉ์ง€ ๊ทธ๋ฆฌ๊ธฐ
settlements.forEach(settlement => {
const x = settlement.x * cellWidth;
const y = settlement.y * cellHeight;
if (settlement.type === 'city') {
// ๋„์‹œ
ctx.fillStyle = '#666';
ctx.fillRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
ctx.strokeStyle = '#333';
ctx.strokeRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
ctx.font = '20px Arial';
ctx.fillText('๐Ÿข', x - 10, y + 5);
} else if (settlement.type === 'village') {
// ๋งˆ์„
ctx.fillStyle = '#8B7355';
ctx.beginPath();
ctx.arc(x, y, settlement.size, 0, Math.PI * 2);
ctx.fill();
ctx.font = '16px Arial';
ctx.fillText('๐Ÿ˜๏ธ', x - 8, y + 5);
} else {
// ํƒ€์šด
ctx.fillStyle = '#999';
ctx.fillRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
ctx.font = '14px Arial';
ctx.fillText('๐Ÿช', x - 7, y + 5);
}
});
// ์ถ•์ฒ™ ๋ง‰๋Œ€ ๊ทธ๋ฆฌ๊ธฐ
ctx.restore(); // ์คŒ ํšจ๊ณผ ์ œ๊ฑฐ ํ›„ ์ถ•์ฒ™ ๋ง‰๋Œ€ ๊ทธ๋ฆฌ๊ธฐ
drawScaleBar();
}
// Marching squares ์•Œ๊ณ ๋ฆฌ์ฆ˜
function drawContourCell(x, y, width, height, corners, level) {
let state = 0;
if (corners[0] > level) state |= 1;
if (corners[1] > level) state |= 2;
if (corners[2] > level) state |= 4;
if (corners[3] > level) state |= 8;
switch(state) {
case 1: case 14:
drawLine(x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[0], corners[1], level), y);
break;
case 2: case 13:
drawLine(x + width * interpolate(corners[0], corners[1], level), y,
x + width, y + height * interpolate(corners[1], corners[2], level));
break;
case 3: case 12:
drawLine(x, y + height * interpolate(corners[0], corners[3], level),
x + width, y + height * interpolate(corners[1], corners[2], level));
break;
case 4: case 11:
drawLine(x + width, y + height * interpolate(corners[1], corners[2], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 5:
drawLine(x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[0], corners[1], level), y);
drawLine(x + width, y + height * interpolate(corners[1], corners[2], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 6: case 9:
drawLine(x + width * interpolate(corners[0], corners[1], level), y,
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 7: case 8:
drawLine(x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 10:
drawLine(x + width * interpolate(corners[0], corners[1], level), y,
x + width, y + height * interpolate(corners[1], corners[2], level));
drawLine(x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
}
}
function interpolate(v1, v2, level) {
return (level - v1) / (v2 - v1);
}
function drawLine(x1, y1, x2, y2) {
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
}
// ์ถ•์ฒ™ ๋ง‰๋Œ€ ๊ทธ๋ฆฌ๊ธฐ
function drawScaleBar() {
const barWidth = 100;
const barHeight = 10;
const x = WIDTH - barWidth - 20;
const y = HEIGHT - 30;
// ์‹ค์ œ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ (๋ฏธํ„ฐ)
const realDistance = (barWidth * currentScale) / 1000; // km๋กœ ๋ณ€ํ™˜
ctx.fillStyle = '#000';
ctx.fillRect(x, y, barWidth, barHeight);
ctx.fillStyle = '#fff';
ctx.fillRect(x, y, barWidth/2, barHeight);
ctx.fillStyle = '#000';
ctx.font = '12px Arial';
ctx.fillText(`0`, x - 5, y - 5);
ctx.fillText(`${realDistance.toFixed(1)}km`, x + barWidth - 20, y - 5);
}
// ์ง€ํ˜• ์ƒ‰์ƒ
function getTerrainColor(height) {
if (height < 0.15) return '#0d4f8b'; // ๋ฌผ
if (height < 0.25) return '#1e7eb8'; // ์–•์€ ๋ฌผ
if (height < 0.35) return '#61a861'; // ํ‰์ง€
if (height < 0.45) return '#8fc68f'; // ๋‚ฎ์€ ํ‰์ง€
if (height < 0.55) return '#c4d4aa'; // ์–ธ๋•
if (height < 0.65) return '#f5deb3'; // ๋†’์€ ์–ธ๋•
if (height < 0.75) return '#d2b48c'; // ๋‚ฎ์€ ์‚ฐ
if (height < 0.85) return '#8b4513'; // ์‚ฐ
return '#fff'; // ๋ˆˆ ๋ฎ์ธ ์‚ฐ
}
// ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
if (isDragging) {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
offsetX = dx / zoomLevel;
offsetY = dy / zoomLevel;
drawMap();
return;
}
// ์คŒ์„ ๊ณ ๋ คํ•œ ๋งˆ์šฐ์Šค ์ขŒํ‘œ ๊ณ„์‚ฐ
const x = ((e.clientX - rect.left) * (WIDTH / rect.width) - WIDTH/2) / zoomLevel + WIDTH/2 - offsetX;
const y = ((e.clientY - rect.top) * (HEIGHT / rect.height) - HEIGHT/2) / zoomLevel + HEIGHT/2 - offsetY;
const gridSize = heightMap.length;
const gridX = Math.floor(x / (WIDTH / gridSize));
const gridY = Math.floor(y / (HEIGHT / gridSize));
if (gridX >= 0 && gridX < gridSize && gridY >= 0 && gridY < gridSize) {
const height = heightMap[gridY][gridX];
const elevation = Math.round(height * 1000);
// ๊ฒฝ์‚ฌ๋„ ๊ณ„์‚ฐ
let slope = 0;
if (gridX > 0 && gridX < gridSize - 1 && gridY > 0 && gridY < gridSize - 1) {
const dx = heightMap[gridY][gridX + 1] - heightMap[gridY][gridX - 1];
const dy = heightMap[gridY + 1][gridX] - heightMap[gridY - 1][gridX];
slope = Math.round(Math.sqrt(dx * dx + dy * dy) * 100);
}
// ์‹ค์ œ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ (์คŒ ๋ ˆ๋ฒจ ๊ณ ๋ ค)
const mapWidthKm = (WIDTH * currentScale / zoomLevel) / 1000000;
const mapHeightKm = (HEIGHT * currentScale / zoomLevel) / 1000000;
const realX = (gridX * mapWidthKm / gridSize).toFixed(2);
const realY = (gridY * mapHeightKm / gridSize).toFixed(2);
document.getElementById('coordinates').textContent = `${gridX}, ${gridY}`;
document.getElementById('elevation').textContent = `${elevation}m`;
document.getElementById('slope').textContent = `${slope}ยฐ`;
document.getElementById('realDistance').textContent = `${realX}, ${realY}km`;
tooltip.style.display = 'block';
tooltip.style.left = e.clientX + 10 + 'px';
tooltip.style.top = e.clientY - 30 + 'px';
tooltip.textContent = `๊ณ ๋„: ${elevation}m`;
}
});
// ๋“œ๋ž˜๊ทธ ์ด๋ฒคํŠธ
canvas.addEventListener('mousedown', (e) => {
if (e.button === 0) { // ์™ผ์ชฝ ๋ฒ„ํŠผ
isDragging = true;
dragStartX = e.clientX - offsetX * zoomLevel;
dragStartY = e.clientY - offsetY * zoomLevel;
canvas.style.cursor = 'grabbing';
}
});
canvas.addEventListener('mouseup', () => {
isDragging = false;
canvas.style.cursor = 'crosshair';
});
canvas.addEventListener('mouseleave', () => {
isDragging = false;
canvas.style.cursor = 'crosshair';
tooltip.style.display = 'none';
document.getElementById('coordinates').textContent = '-';
document.getElementById('elevation').textContent = '-';
document.getElementById('slope').textContent = '-';
document.getElementById('realDistance').textContent = '-';
});
// ๋งˆ์šฐ์Šค ํœ  ์คŒ
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
const newZoom = zoomLevel * delta;
if (newZoom >= 0.5 && newZoom <= 4) {
zoomLevel = newZoom;
updateScaleInfo();
drawMap();
}
});
// ์ปจํŠธ๋กค ํ•จ์ˆ˜๋“ค
function generateNewTerrain() {
generateTerrain();
drawMap();
}
function toggleContours() {
showContours = !showContours;
drawMap();
}
function toggleHeatmap() {
showHeatmap = !showHeatmap;
drawMap();
}
function toggleRoads() {
showRoads = !showRoads;
drawMap();
}
function toggleRivers() {
showRivers = !showRivers;
drawMap();
}
function changeTerrainType() {
terrainType = document.getElementById('terrainType').value;
generateTerrain();
drawMap();
}
function zoomIn() {
if (currentScaleIndex > 0) {
currentScaleIndex--;
currentScale = scales[currentScaleIndex];
zoomLevel = Math.min(4, zoomLevel * 1.5);
offsetX = 0;
offsetY = 0;
updateScaleInfo();
drawMap();
}
}
function zoomOut() {
if (currentScaleIndex < scales.length - 1) {
currentScaleIndex++;
currentScale = scales[currentScaleIndex];
zoomLevel = Math.max(0.5, zoomLevel / 1.5);
offsetX = 0;
offsetY = 0;
updateScaleInfo();
drawMap();
}
}
function updateScaleInfo() {
const effectiveScale = currentScale / zoomLevel;
document.getElementById('scaleInfo').textContent = `1:${Math.round(effectiveScale)}`;
// ์ง€๋„ ์ „์ฒด ํฌ๊ธฐ ๊ณ„์‚ฐ (์คŒ ๊ณ ๋ ค)
const widthKm = (WIDTH * effectiveScale / 1000000).toFixed(1);
const heightKm = (HEIGHT * effectiveScale / 1000000).toFixed(1);
// ์ตœ๋Œ€ 20km x 15km ์ œํ•œ
const actualWidthKm = Math.min(20, widthKm);
const actualHeightKm = Math.min(15, heightKm);
document.getElementById('mapSizeInfo').textContent =
`์ง€๋„ ๋ฒ”์œ„: ${actualWidthKm}km ร— ${actualHeightKm}km (์คŒ: ${(zoomLevel * 100).toFixed(0)}%)`;
}
function exportMap() {
const link = document.createElement('a');
link.download = `topographic-map-${terrainType}-${Date.now()}.png`;
link.href = canvas.toDataURL();
link.click();
}
// ์ดˆ๊ธฐํ™”
generateTerrain();
drawMap();
updateScaleInfo();
// ํ™”๋ฉด ํฌ๊ธฐ ๋ณ€๊ฒฝ ๋Œ€์‘
window.addEventListener('resize', () => {
drawMap();
});
</script>
</body>
</html>