Spaces:
Sleeping
Sleeping
| import * as THREE from "three"; | |
| import type { BakedMapGeometry, GroundPalette } from "./content/maps/mapTypes"; | |
| const TEX_SIZE = 4096; | |
| // Lighten a hex color by mixing with white at ratio t (0=original, 1=white) | |
| function lighten(hex: string, t: number): string { | |
| const n = parseInt(hex.replace("#", ""), 16); | |
| const r = ((n >> 16) & 0xff), g = ((n >> 8) & 0xff), b = (n & 0xff); | |
| const mix = (c: number) => Math.round(c + (255 - c) * t); | |
| return `#${[mix(r), mix(g), mix(b)].map(v => v.toString(16).padStart(2, "0")).join("")}`; | |
| } | |
| function fillPolygon(ctx: CanvasRenderingContext2D, ring: number[], size: number) { | |
| if (ring.length < 6) return; | |
| ctx.beginPath(); | |
| ctx.moveTo(ring[0] * size, (1 - ring[1]) * size); | |
| for (let i = 2; i < ring.length; i += 2) { | |
| ctx.lineTo(ring[i] * size, (1 - ring[i + 1]) * size); | |
| } | |
| ctx.closePath(); | |
| ctx.fill(); | |
| } | |
| export function renderMapGeometry( | |
| geom: BakedMapGeometry, | |
| palette: GroundPalette, | |
| ): THREE.CanvasTexture { | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = TEX_SIZE; | |
| canvas.height = TEX_SIZE; | |
| const ctx = canvas.getContext("2d")!; | |
| const s = TEX_SIZE; | |
| // --- Land base --- | |
| ctx.fillStyle = palette.base; | |
| ctx.fillRect(0, 0, s, s); | |
| // --- Land use --- | |
| for (const lu of geom.landUse) { | |
| switch (lu.kind) { | |
| case "forest": | |
| ctx.fillStyle = lighten(palette.base, -0.15); | |
| break; | |
| case "scrub": | |
| ctx.fillStyle = lighten(palette.base, -0.08); | |
| break; | |
| case "urban": | |
| ctx.fillStyle = lighten(palette.base, 0.18); | |
| break; | |
| case "farmland": | |
| ctx.fillStyle = lighten(palette.base, 0.1); | |
| break; | |
| } | |
| fillPolygon(ctx, lu.ring, s); | |
| } | |
| // --- Water --- | |
| ctx.fillStyle = palette.colors[0] ?? "#0369a1"; | |
| for (const ring of geom.waterRings) { | |
| fillPolygon(ctx, ring, s); | |
| } | |
| // Roads are rendered as 3D ribbon geometry by ScatterRenderer; omitting them here | |
| // prevents duplicate road representations when geom is available. | |
| // --- Runways --- | |
| ctx.fillStyle = "#94a3b8"; | |
| for (const rwy of geom.runways) { | |
| const cx = rwy.cx * s; | |
| const cy = (1 - rwy.cy) * s; | |
| const len = rwy.length * s; | |
| const wid = rwy.width * s; | |
| ctx.save(); | |
| ctx.translate(cx, cy); | |
| ctx.rotate(rwy.heading * Math.PI / 180); | |
| ctx.fillRect(-len / 2, -wid / 2, len, wid); | |
| ctx.restore(); | |
| } | |
| // --- Port markers --- | |
| const portScale = s / 512; | |
| ctx.fillStyle = "#e2e8f0"; | |
| for (const port of geom.ports) { | |
| const px = port.x * s, py = (1 - port.y) * s; | |
| ctx.beginPath(); | |
| ctx.arc(px, py, 3 * portScale, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| const tex = new THREE.CanvasTexture(canvas); | |
| tex.anisotropy = 16; | |
| return tex; | |
| } | |
| // Fallback palette-only texture when no geometry is available | |
| export function renderPaletteFallback(palette: GroundPalette): THREE.CanvasTexture { | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = canvas.height = 256; | |
| const ctx = canvas.getContext("2d")!; | |
| ctx.fillStyle = palette.base; | |
| ctx.fillRect(0, 0, 256, 256); | |
| // Subtle noise-like variation using palette colors | |
| for (let i = 0; i < 400; i++) { | |
| const x = Math.random() * 256, y = Math.random() * 256; | |
| const r = 8 + Math.random() * 24; | |
| ctx.globalAlpha = 0.06 + Math.random() * 0.06; | |
| ctx.fillStyle = palette.colors[Math.floor(Math.random() * palette.colors.length)] ?? palette.base; | |
| ctx.beginPath(); | |
| ctx.arc(x, y, r, 0, Math.PI * 2); | |
| ctx.fill(); | |
| } | |
| ctx.globalAlpha = 1; | |
| const tex = new THREE.CanvasTexture(canvas); | |
| return tex; | |
| } | |