Download index.html from aiqtech/terrain: direct link, hf CLI and curl.
- Browser
- Download file 38.8 kB
-
https://huggingface.co/spaces/aiqtech/terrain/resolve/main/index.html
- Command line
-
hf download hf://spaces/aiqtech/terrain/index.html
-
curl -L -o index.html https://huggingface.co/spaces/aiqtech/terrain/resolve/main/index.html
38.8 kB
| <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> |