terrain / index.html
aiqtech's picture
Update index.html
55df423 verified
raw
history blame
21.9 kB
<!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: 1200px;
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);
}
canvas {
display: block;
cursor: crosshair;
width: 100%;
height: auto;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin: 20px 0;
flex-wrap: wrap;
}
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);
}
.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: 20px;
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
}
.legend-color {
width: 30px;
height: 20px;
border-radius: 4px;
border: 1px solid #ddd;
}
.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);
}
</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="toggle3D()">๐ŸŽฎ 3D ๋ทฐ ์ „ํ™˜</button>
<select id="terrainType" onchange="changeTerrainType()">
<option value="mountain">โ›ฐ๏ธ ์‚ฐ์•… ์ง€ํ˜•</option>
<option value="valley">๐Ÿž๏ธ ๊ณ„๊ณก ์ง€ํ˜•</option>
<option value="plateau">๐Ÿ”๏ธ ๊ณ ์› ์ง€ํ˜•</option>
<option value="island">๐Ÿ๏ธ ์„ฌ ์ง€ํ˜•</option>
</select>
<button onclick="exportMap()">๐Ÿ’พ ์ง€๋„ ์ €์žฅ</button>
</div>
<div class="map-container">
<canvas id="mapCanvas"></canvas>
<div class="tooltip" id="tooltip"></div>
</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">10m</div>
</div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-color" style="background: #0d4f8b;"></div>
<span>๋‚ฎ์€ ๊ณ ๋„ (0-200m)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #61a861;"></div>
<span>์ค‘๊ฐ„ ๊ณ ๋„ (200-500m)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #f5deb3;"></div>
<span>๋†’์€ ๊ณ ๋„ (500-800m)</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background: #8b4513;"></div>
<span>์ตœ๊ณ  ๊ณ ๋„ (800m+)</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;
// ์ง€ํ˜• ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•  ๋ฐฐ์—ด
let heightMap = [];
let showContours = true;
let showHeatmap = false;
let is3D = false;
let terrainType = 'mountain';
// ๋…ธ์ด์ฆˆ ํ•จ์ˆ˜ (Perlin noise ๊ฐ„๋‹จ ๊ตฌํ˜„)
function noise(x, y, scale, octaves) {
let value = 0;
let amplitude = 1;
let frequency = scale;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
value += amplitude * simpleNoise(x * frequency, y * frequency);
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() {
heightMap = [];
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) * 0.6;
height += noise(x, y, 0.05, 4) * 0.3;
height += noise(x, y, 0.1, 2) * 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) * 0.5;
height *= (1 - dist * 0.5);
height -= dist * 0.3;
break;
case 'plateau':
// ๊ณ ์› ์ง€ํ˜•
height = noise(x, y, 0.02, 3) * 0.3;
if (height > 0.1) height = 0.3 + noise(x, y, 0.05, 2) * 0.1;
break;
case 'island':
// ์„ฌ ์ง€ํ˜•
const centerX = (x - gridSize/2) / gridSize;
const centerY = (y - gridSize/2) / gridSize;
const radius = Math.sqrt(centerX*centerX + centerY*centerY);
height = noise(x, y, 0.03, 5) * 0.6;
height *= Math.max(0, 1 - radius * 2);
break;
}
// ์ •๊ทœํ™” (0-1 ๋ฒ”์œ„)
height = Math.max(0, Math.min(1, (height + 1) / 2));
heightMap[y][x] = height;
}
}
}
// ๋“ฑ๊ณ ์„  ๊ณ„์‚ฐ
function calculateContours(interval) {
const contours = [];
const levels = [];
for (let level = 0; level <= 1; level += interval) {
levels.push(level);
}
return { contours, levels };
}
// ์ง€๋„ ๊ทธ๋ฆฌ๊ธฐ
function drawMap() {
ctx.clearRect(0, 0, WIDTH, HEIGHT);
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);
}
if (is3D) {
// 3D ํšจ๊ณผ
const offset = height * 10;
ctx.fillRect(
x * cellWidth + offset,
y * cellHeight - offset,
cellWidth + 1,
cellHeight + 1
);
} else {
ctx.fillRect(
x * cellWidth,
y * cellHeight,
cellWidth + 1,
cellHeight + 1
);
}
}
}
// ๋“ฑ๊ณ ์„  ๊ทธ๋ฆฌ๊ธฐ
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();
}
}
// ๊ทธ๋ฆฌ๋“œ ํ‘œ์‹œ
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 0.5;
for (let i = 0; i <= gridSize; i += 10) {
ctx.beginPath();
ctx.moveTo(i * cellWidth, 0);
ctx.lineTo(i * cellWidth, HEIGHT);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, i * cellHeight);
ctx.lineTo(WIDTH, i * cellHeight);
ctx.stroke();
}
}
// 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 getTerrainColor(height) {
if (height < 0.2) return '#0d4f8b'; // ๊นŠ์€ ๋ฌผ
if (height < 0.3) return '#1e7eb8'; // ์–•์€ ๋ฌผ
if (height < 0.4) return '#61a861'; // ๋‚ฎ์€ ์ง€๋Œ€ (์ดˆ๋ก)
if (height < 0.5) return '#8fc68f'; // ํ‰์ง€
if (height < 0.6) return '#c4d4aa'; // ์–ธ๋•
if (height < 0.7) return '#f5deb3'; // ๋†’์€ ์–ธ๋•
if (height < 0.8) return '#d2b48c'; // ๋‚ฎ์€ ์‚ฐ
if (height < 0.9) return '#8b4513'; // ์‚ฐ
return '#fff'; // ๋ˆˆ ๋ฎ์ธ ์‚ฐ๊ผญ๋Œ€๊ธฐ
}
// ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = (e.clientX - rect.left) * (WIDTH / rect.width);
const y = (e.clientY - rect.top) * (HEIGHT / rect.height);
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);
}
document.getElementById('coordinates').textContent = `${gridX}, ${gridY}`;
document.getElementById('elevation').textContent = `${elevation}m`;
document.getElementById('slope').textContent = `${slope}ยฐ`;
// ํˆดํŒ ํ‘œ์‹œ
tooltip.style.display = 'block';
tooltip.style.left = e.clientX + 10 + 'px';
tooltip.style.top = e.clientY - 30 + 'px';
tooltip.textContent = `๊ณ ๋„: ${elevation}m`;
}
});
canvas.addEventListener('mouseleave', () => {
tooltip.style.display = 'none';
document.getElementById('coordinates').textContent = '-';
document.getElementById('elevation').textContent = '-';
document.getElementById('slope').textContent = '-';
});
// ์ปจํŠธ๋กค ํ•จ์ˆ˜๋“ค
function generateNewTerrain() {
generateTerrain();
drawMap();
}
function toggleContours() {
showContours = !showContours;
drawMap();
}
function toggleHeatmap() {
showHeatmap = !showHeatmap;
drawMap();
}
function toggle3D() {
is3D = !is3D;
drawMap();
}
function changeTerrainType() {
terrainType = document.getElementById('terrainType').value;
generateTerrain();
drawMap();
}
function exportMap() {
const link = document.createElement('a');
link.download = 'topographic-map.png';
link.href = canvas.toDataURL();
link.click();
}
// ์ดˆ๊ธฐํ™”
generateTerrain();
drawMap();
// ํ™”๋ฉด ํฌ๊ธฐ ๋ณ€๊ฒฝ ๋Œ€์‘
window.addEventListener('resize', () => {
drawMap();
});
</script>
</body>
</html>