|
<!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'; |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
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) { |
|
|
|
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(); |
|
} |
|
} |
|
|
|
|
|
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> |