hmm
This commit is contained in:
parent
112e6bf86b
commit
f2d0306d96
13 changed files with 1434 additions and 516 deletions
274
renderer.js
274
renderer.js
|
|
@ -1,12 +1,37 @@
|
|||
import { CELL_SIZE, MAP_H, MAP_W, clamp, indexOf, inside } from "./mapUtils.js";
|
||||
import { CELL_SIZE, MAP_H, MAP_W, clamp } from "./mapUtils.js";
|
||||
|
||||
|
||||
const segmentVectorCache = new WeakMap();
|
||||
const pathVectorCache = new WeakMap();
|
||||
const coastlineCache = new WeakMap();
|
||||
const rasterBorderCache = new WeakMap();
|
||||
const baseImageCache = new WeakMap();
|
||||
const MAX_BASE_CACHE_IMAGES = 4;
|
||||
|
||||
function mapWidth(map) {
|
||||
return Math.max(1, Math.floor(Number.isFinite(map?.width) ? map.width : MAP_W));
|
||||
}
|
||||
|
||||
function mapHeight(map) {
|
||||
return Math.max(1, Math.floor(Number.isFinite(map?.height) ? map.height : MAP_H));
|
||||
}
|
||||
|
||||
function cellIndex(map, x, y) {
|
||||
return y * mapWidth(map) + x;
|
||||
}
|
||||
|
||||
function insideMap(map, x, y) {
|
||||
return x >= 0 && y >= 0 && x < mapWidth(map) && y < mapHeight(map);
|
||||
}
|
||||
|
||||
function mapPixelWidth(map) {
|
||||
return mapWidth(map) * CELL_SIZE;
|
||||
}
|
||||
|
||||
function mapPixelHeight(map) {
|
||||
return mapHeight(map) * CELL_SIZE;
|
||||
}
|
||||
|
||||
function pointKey(p) {
|
||||
return `${p[0]},${p[1]}`;
|
||||
}
|
||||
|
|
@ -183,16 +208,18 @@ function getCoastlineSegments(map) {
|
|||
if (cached) return cached;
|
||||
|
||||
const segments = [];
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
const w = mapWidth(map);
|
||||
const h = mapHeight(map);
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = cellIndex(map, x, y);
|
||||
const a = Boolean(map.sea[i]);
|
||||
if (x + 1 < MAP_W) {
|
||||
const b = Boolean(map.sea[indexOf(x + 1, y)]);
|
||||
if (x + 1 < w) {
|
||||
const b = Boolean(map.sea[cellIndex(map, x + 1, y)]);
|
||||
if (a !== b) segments.push([[x + 1, y], [x + 1, y + 1]]);
|
||||
}
|
||||
if (y + 1 < MAP_H) {
|
||||
const b = Boolean(map.sea[indexOf(x, y + 1)]);
|
||||
if (y + 1 < h) {
|
||||
const b = Boolean(map.sea[cellIndex(map, x, y + 1)]);
|
||||
if (a !== b) segments.push([[x, y + 1], [x + 1, y + 1]]);
|
||||
}
|
||||
}
|
||||
|
|
@ -244,10 +271,10 @@ function drawVectorSegments(ctx, segments, color, width, dashed = false, vectorO
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
function sampleCellIndex(fx, fy) {
|
||||
const x = Math.max(0, Math.min(MAP_W - 1, Math.floor(fx)));
|
||||
const y = Math.max(0, Math.min(MAP_H - 1, Math.floor(fy)));
|
||||
return indexOf(x, y);
|
||||
function sampleCellIndex(map, fx, fy) {
|
||||
const x = Math.max(0, Math.min(mapWidth(map) - 1, Math.floor(fx)));
|
||||
const y = Math.max(0, Math.min(mapHeight(map) - 1, Math.floor(fy)));
|
||||
return cellIndex(map, x, y);
|
||||
}
|
||||
|
||||
function seaCoverageSample(map, fx, fy) {
|
||||
|
|
@ -261,7 +288,7 @@ function seaCoverageSample(map, fx, fy) {
|
|||
[-0.26, -0.26], [0.26, -0.26], [-0.26, 0.26], [0.26, 0.26],
|
||||
];
|
||||
let sum = 0;
|
||||
for (const [ox, oy] of offsets) sum += fieldSample(map.sea, fx + ox, fy + oy);
|
||||
for (const [ox, oy] of offsets) sum += fieldSample(map, map.sea, fx + ox, fy + oy);
|
||||
return clamp(sum / offsets.length);
|
||||
}
|
||||
|
||||
|
|
@ -293,20 +320,21 @@ function blendOutside(color, isInside) {
|
|||
];
|
||||
}
|
||||
|
||||
function fieldSample(field, fx, fy) {
|
||||
const sx = Math.max(0, Math.min(MAP_W - 1, fx));
|
||||
const sy = Math.max(0, Math.min(MAP_H - 1, fy));
|
||||
function fieldSample(map, field, fx, fy) {
|
||||
if (!field) return 0;
|
||||
const sx = Math.max(0, Math.min(mapWidth(map) - 1, fx));
|
||||
const sy = Math.max(0, Math.min(mapHeight(map) - 1, fy));
|
||||
const x0 = Math.floor(sx);
|
||||
const y0 = Math.floor(sy);
|
||||
const x1 = Math.max(0, Math.min(MAP_W - 1, x0 + 1));
|
||||
const y1 = Math.max(0, Math.min(MAP_H - 1, y0 + 1));
|
||||
const x1 = Math.max(0, Math.min(mapWidth(map) - 1, x0 + 1));
|
||||
const y1 = Math.max(0, Math.min(mapHeight(map) - 1, y0 + 1));
|
||||
const tx = sx - x0;
|
||||
const ty = sy - y0;
|
||||
|
||||
const a = field[indexOf(x0, y0)];
|
||||
const b = field[indexOf(x1, y0)];
|
||||
const c = field[indexOf(x0, y1)];
|
||||
const d = field[indexOf(x1, y1)];
|
||||
const a = field[cellIndex(map, x0, y0)] || 0;
|
||||
const b = field[cellIndex(map, x1, y0)] || 0;
|
||||
const c = field[cellIndex(map, x0, y1)] || 0;
|
||||
const d = field[cellIndex(map, x1, y1)] || 0;
|
||||
|
||||
return ((a * (1 - tx) + b * tx) * (1 - ty)) + ((c * (1 - tx) + d * tx) * ty);
|
||||
}
|
||||
|
|
@ -329,20 +357,20 @@ function interpolateColorStops(value, stops) {
|
|||
}
|
||||
|
||||
function terrainColorContinuous(map, fx, fy, mode) {
|
||||
const i = sampleCellIndex(fx, fy);
|
||||
const i = sampleCellIndex(map, fx, fy);
|
||||
const isInside = Boolean(map.prefectureMask[i]);
|
||||
|
||||
let color;
|
||||
|
||||
const waterCoverage = seaCoverageSample(map, fx, fy);
|
||||
const depth = clamp(((map.seaLevel ?? 0.285) + 0.065 - fieldSample(map.elevation, fx, fy)) * 2.4);
|
||||
const depth = clamp(((map.seaLevel ?? 0.285) + 0.065 - fieldSample(map, map.elevation, fx, fy)) * 2.4);
|
||||
const waterColor = [Math.round(174 - depth * 7), Math.round(203 + depth * 2), Math.round(224 + depth * 5)];
|
||||
|
||||
let landColor;
|
||||
if (mode === "development") {
|
||||
const dCity = distToNearest(map.modernCities, fx, fy);
|
||||
const urban = clamp(1 - dCity / 25);
|
||||
const density = map.populationDensity ? fieldSample(map.populationDensity, fx, fy) : urban;
|
||||
const density = map.populationDensity ? fieldSample(map, map.populationDensity, fx, fy) : urban;
|
||||
const base = 235;
|
||||
landColor = [
|
||||
Math.round(base + density * 20),
|
||||
|
|
@ -352,7 +380,7 @@ function terrainColorContinuous(map, fx, fy, mode) {
|
|||
} else {
|
||||
// 地形の基底色は標高のみに従わせる。
|
||||
// 谷や微地形の見え方は陰影側で制御し、谷底だけが不自然に茶色化しないようにする。
|
||||
const e = fieldSample(map.elevation, fx, fy);
|
||||
const e = fieldSample(map, map.elevation, fx, fy);
|
||||
landColor = interpolateColorStops(clamp(e), [
|
||||
[0.20, [231, 236, 223]],
|
||||
[0.30, [223, 231, 214]],
|
||||
|
|
@ -378,11 +406,11 @@ function terrainColorContinuous(map, fx, fy, mode) {
|
|||
|
||||
function terrainShadeContinuous(map, fx, fy) {
|
||||
const step = 0.50;
|
||||
const eC = fieldSample(map.elevation, fx, fy);
|
||||
const eL = fieldSample(map.elevation, fx - step, fy);
|
||||
const eR = fieldSample(map.elevation, fx + step, fy);
|
||||
const eU = fieldSample(map.elevation, fx, fy - step);
|
||||
const eD = fieldSample(map.elevation, fx, fy + step);
|
||||
const eC = fieldSample(map, map.elevation, fx, fy);
|
||||
const eL = fieldSample(map, map.elevation, fx - step, fy);
|
||||
const eR = fieldSample(map, map.elevation, fx + step, fy);
|
||||
const eU = fieldSample(map, map.elevation, fx, fy - step);
|
||||
const eD = fieldSample(map, map.elevation, fx, fy + step);
|
||||
|
||||
// x は東向き, y は南向き。法線は (-dz/dx, -dz/dy, 1)。
|
||||
// 描画では地形生成上の差分をやや誇張し、粗いDEMでも山腹の起伏を読ませる。
|
||||
|
|
@ -398,14 +426,14 @@ function terrainShadeContinuous(map, fx, fy) {
|
|||
const lz = 0.7071067811865476;
|
||||
const hill = clamp((nx * lx + ny * ly + nz * lz) / nLen * 0.58 + 0.48);
|
||||
|
||||
const slope = map.slope ? fieldSample(map.slope, fx, fy) : 0;
|
||||
const valley = map.valleyField ? fieldSample(map.valleyField, fx, fy) : 0;
|
||||
const ravine = map.visibleRavineField ? fieldSample(map.visibleRavineField, fx, fy) : 0;
|
||||
const tex = map.surfaceTextureField ? fieldSample(map.surfaceTextureField, fx, fy) : 0;
|
||||
const rvL = map.visibleRavineField ? fieldSample(map.visibleRavineField, fx - 0.90, fy) : 0;
|
||||
const rvR = map.visibleRavineField ? fieldSample(map.visibleRavineField, fx + 0.90, fy) : 0;
|
||||
const rvU = map.visibleRavineField ? fieldSample(map.visibleRavineField, fx, fy - 0.90) : 0;
|
||||
const rvD = map.visibleRavineField ? fieldSample(map.visibleRavineField, fx, fy + 0.90) : 0;
|
||||
const slope = map.slope ? fieldSample(map, map.slope, fx, fy) : 0;
|
||||
const valley = map.valleyField ? fieldSample(map, map.valleyField, fx, fy) : 0;
|
||||
const ravine = map.visibleRavineField ? fieldSample(map, map.visibleRavineField, fx, fy) : 0;
|
||||
const tex = map.surfaceTextureField ? fieldSample(map, map.surfaceTextureField, fx, fy) : 0;
|
||||
const rvL = map.visibleRavineField ? fieldSample(map, map.visibleRavineField, fx - 0.90, fy) : 0;
|
||||
const rvR = map.visibleRavineField ? fieldSample(map, map.visibleRavineField, fx + 0.90, fy) : 0;
|
||||
const rvU = map.visibleRavineField ? fieldSample(map, map.visibleRavineField, fx, fy - 0.90) : 0;
|
||||
const rvD = map.visibleRavineField ? fieldSample(map, map.visibleRavineField, fx, fy + 0.90) : 0;
|
||||
const ravineRelief = (rvL - rvR) * 0.26 + (rvU - rvD) * 0.20;
|
||||
const concavity = clamp(((eL + eR + eU + eD) * 0.25 - eC) * 9.0, -0.18, 0.18);
|
||||
|
||||
|
|
@ -419,7 +447,7 @@ function terrainShadeContinuous(map, fx, fy) {
|
|||
}
|
||||
|
||||
function discreteColor(map, x, y, mode) {
|
||||
const i = indexOf(x, y);
|
||||
const i = cellIndex(map, x, y);
|
||||
let color;
|
||||
|
||||
if (map.sea[i]) {
|
||||
|
|
@ -451,35 +479,39 @@ function discreteColor(map, x, y, mode) {
|
|||
return blendOutside(color, Boolean(map.prefectureMask[i]));
|
||||
}
|
||||
|
||||
function baseCacheKey(mode, continuousTerrain) {
|
||||
function baseCacheKey(mode, continuousTerrain, renderScale = 1) {
|
||||
const continuousModes = ["terrain", "development", "all"];
|
||||
const scaleKey = Math.round((renderScale || 1) * 20) / 20;
|
||||
if (continuousTerrain && continuousModes.includes(mode)) {
|
||||
return `continuous:${mode === "all" ? "terrain" : mode}`;
|
||||
return `continuous:${mode === "all" ? "terrain" : mode}:${scaleKey}`;
|
||||
}
|
||||
return `discrete:${mode}`;
|
||||
return `discrete:${mode}:${scaleKey}`;
|
||||
}
|
||||
|
||||
function getCachedBaseImage(ctx, map, mode, continuousTerrain) {
|
||||
function getCachedBaseCanvas(ctx, map, mode, continuousTerrain, renderScale = 1) {
|
||||
let cache = baseImageCache.get(map);
|
||||
if (!cache) {
|
||||
cache = new Map();
|
||||
baseImageCache.set(map, cache);
|
||||
}
|
||||
|
||||
const key = baseCacheKey(mode, continuousTerrain);
|
||||
let image = cache.get(key);
|
||||
if (image) return image;
|
||||
const key = baseCacheKey(mode, continuousTerrain, renderScale);
|
||||
let canvas = cache.get(key);
|
||||
if (canvas) return canvas;
|
||||
|
||||
const width = MAP_W * CELL_SIZE;
|
||||
const height = MAP_H * CELL_SIZE;
|
||||
const sourceWidth = mapPixelWidth(map);
|
||||
const sourceHeight = mapPixelHeight(map);
|
||||
const targetScale = Math.max(0.35, Math.min(1, renderScale || 1));
|
||||
const width = Math.max(1, Math.round(sourceWidth * targetScale));
|
||||
const height = Math.max(1, Math.round(sourceHeight * targetScale));
|
||||
const img = ctx.createImageData(width, height);
|
||||
const continuousModes = ["terrain", "development", "all"];
|
||||
|
||||
if (continuousTerrain && continuousModes.includes(mode)) {
|
||||
for (let py = 0; py < height; py++) {
|
||||
const fy = py / CELL_SIZE;
|
||||
const fy = (py / Math.max(1, height)) * mapHeight(map);
|
||||
for (let px = 0; px < width; px++) {
|
||||
const fx = px / CELL_SIZE;
|
||||
const fx = (px / Math.max(1, width)) * mapWidth(map);
|
||||
const [r, g, b] = terrainColorContinuous(map, fx, fy, mode === "all" ? "terrain" : mode);
|
||||
const shade = terrainShadeContinuous(map, fx, fy);
|
||||
|
||||
|
|
@ -491,8 +523,10 @@ function getCachedBaseImage(ctx, map, mode, continuousTerrain) {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const w = mapWidth(map);
|
||||
const h = mapHeight(map);
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const [r, g, b] = discreteColor(map, x, y, mode);
|
||||
for (let dy = 0; dy < CELL_SIZE; dy++) {
|
||||
for (let dx = 0; dx < CELL_SIZE; dx++) {
|
||||
|
|
@ -506,13 +540,53 @@ function getCachedBaseImage(ctx, map, mode, continuousTerrain) {
|
|||
}
|
||||
}
|
||||
}
|
||||
cache.set(key, img);
|
||||
|
||||
canvas = document.createElement("canvas");
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const bctx = canvas.getContext("2d");
|
||||
bctx.putImageData(img, 0, 0);
|
||||
cache.set(key, canvas);
|
||||
if (cache.size > MAX_BASE_CACHE_IMAGES) cache.delete(cache.keys().next().value);
|
||||
return img;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function drawBase(ctx, map, mode, continuousTerrain) {
|
||||
ctx.putImageData(getCachedBaseImage(ctx, map, mode, continuousTerrain), 0, 0);
|
||||
function drawBase(ctx, map, mode, continuousTerrain, renderScale = 1) {
|
||||
// putImageData ignores the current transform, so it made the terrain layer
|
||||
// appear unzoomed while vector layers scaled. Cache the raster into an
|
||||
// offscreen canvas and draw it with drawImage so wheel zoom applies to terrain.
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(getCachedBaseCanvas(ctx, map, mode, continuousTerrain, renderScale), 0, 0, mapPixelWidth(map), mapPixelHeight(map));
|
||||
}
|
||||
|
||||
|
||||
function getRasterBorderSegments(map, fieldName) {
|
||||
const field = map?.[fieldName];
|
||||
if (!field) return [];
|
||||
let cacheByField = rasterBorderCache.get(field);
|
||||
if (cacheByField?.segments) return cacheByField.segments;
|
||||
const segments = [];
|
||||
const w = mapWidth(map);
|
||||
const h = mapHeight(map);
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = cellIndex(map, x, y);
|
||||
const id = field[i];
|
||||
if (id < 0 || map.sea?.[i]) continue;
|
||||
if (x + 1 < w) {
|
||||
const ri = cellIndex(map, x + 1, y);
|
||||
const rid = field[ri];
|
||||
if (!map.sea?.[ri] && rid >= 0 && rid !== id) segments.push([[x + 0.5, y], [x + 0.5, y + 1]]);
|
||||
}
|
||||
if (y + 1 < h) {
|
||||
const di = cellIndex(map, x, y + 1);
|
||||
const did = field[di];
|
||||
if (!map.sea?.[di] && did >= 0 && did !== id) segments.push([[x, y + 0.5], [x + 1, y + 0.5]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
rasterBorderCache.set(field, { segments });
|
||||
return segments;
|
||||
}
|
||||
|
||||
function drawRiverPath(ctx, map, path, color, widthFn, alpha = 1) {
|
||||
|
|
@ -523,8 +597,8 @@ function drawRiverPath(ctx, map, path, color, widthFn, alpha = 1) {
|
|||
for (let k = 0; k < path.length - 1; k++) {
|
||||
const [x1, y1] = path[k];
|
||||
const [x2, y2] = path[k + 1];
|
||||
const i1 = indexOf(x1, y1);
|
||||
const i2 = indexOf(x2, y2);
|
||||
const i1 = cellIndex(map, Math.round(x1), Math.round(y1));
|
||||
const i2 = cellIndex(map, Math.round(x2), Math.round(y2));
|
||||
const strength = Math.max((map.river?.[i1] || 0) + (map.flowAccum?.[i1] || 0) * 0.95, (map.river?.[i2] || 0) + (map.flowAccum?.[i2] || 0) * 0.95);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.globalAlpha = alpha;
|
||||
|
|
@ -558,7 +632,7 @@ function landOnlySubpaths(map, path, minCells = 2) {
|
|||
let cur = [];
|
||||
for (const p of path) {
|
||||
const [x, y] = p;
|
||||
const land = inside(x, y) && !map.sea[indexOf(x, y)];
|
||||
const land = insideMap(map, x, y) && !map.sea[cellIndex(map, x, y)];
|
||||
if (land) {
|
||||
cur.push(p);
|
||||
} else if (cur.length >= minCells) {
|
||||
|
|
@ -660,9 +734,11 @@ function drawUrbanAreas(ctx, map, mode) {
|
|||
const cbdColor = "rgba(215, 175, 172, 0.84)";
|
||||
|
||||
ctx.save();
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
const w = mapWidth(map);
|
||||
const h = mapHeight(map);
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = cellIndex(map, x, y);
|
||||
const areaMask = map.humanRegionMask || map.prefectureMask;
|
||||
if (areaMask && !areaMask[i]) continue;
|
||||
const lu = map.landuse[i];
|
||||
|
|
@ -684,9 +760,11 @@ function drawUrbanAreas(ctx, map, mode) {
|
|||
function drawDebugCells(ctx, map, field, color) {
|
||||
if (!field) return;
|
||||
ctx.save();
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
const w = mapWidth(map);
|
||||
const h = mapHeight(map);
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = cellIndex(map, x, y);
|
||||
const debugMask = map.humanRegionMask || map.prefectureMask;
|
||||
if (!debugMask[i] || map.sea[i]) continue;
|
||||
const raw = field[i] || 0;
|
||||
|
|
@ -744,9 +822,11 @@ function drawPrefectureRegionFill(ctx, map, mode) {
|
|||
const alpha = mode === "borders-debug" ? 0.34 : 0.18;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
for (let y = 0; y < MAP_H; y++) {
|
||||
for (let x = 0; x < MAP_W; x++) {
|
||||
const i = indexOf(x, y);
|
||||
const w = mapWidth(map);
|
||||
const h = mapHeight(map);
|
||||
for (let y = 0; y < h; y++) {
|
||||
for (let x = 0; x < w; x++) {
|
||||
const i = cellIndex(map, x, y);
|
||||
const id = ids[i];
|
||||
if (map.sea[i] || id < 0) continue;
|
||||
const [r, g, b] = prefectureRegionColor(id);
|
||||
|
|
@ -806,7 +886,7 @@ function labelWithCollision(ctx, p, occupied) {
|
|||
const x = baseX + ox;
|
||||
const y = baseY + oy;
|
||||
const box = { x1: x - 3, y1: y - textH, x2: x + textW + 3, y2: y + 5 };
|
||||
if (box.x1 < 0 || box.y1 < 0 || box.x2 > MAP_W * CELL_SIZE || box.y2 > MAP_H * CELL_SIZE) continue;
|
||||
if (box.x1 < 0 || box.y1 < 0 || box.x2 > (ctx.__mapPixelWidth || MAP_W * CELL_SIZE) || box.y2 > (ctx.__mapPixelHeight || MAP_H * CELL_SIZE)) continue;
|
||||
const overlaps = occupied.filter((b) => boxesOverlap(box, b, isPrefectureLabel ? 5 : isMunicipalityLabel ? 1 : 3));
|
||||
if (!overlaps.length) {
|
||||
ctx.lineJoin = "round";
|
||||
|
|
@ -849,11 +929,11 @@ function drawLabels(ctx, points, limit = Infinity, occupied = null) {
|
|||
return used;
|
||||
}
|
||||
|
||||
function drawScaleBar(ctx) {
|
||||
function drawScaleBar(ctx, cellScreenSize = CELL_SIZE) {
|
||||
const kmPerCell = 0.5;
|
||||
const targetKm = 25;
|
||||
const lengthCells = Math.max(8, Math.round(targetKm / kmPerCell));
|
||||
const lengthPx = lengthCells * CELL_SIZE;
|
||||
const lengthPx = lengthCells * cellScreenSize;
|
||||
const margin = 14;
|
||||
const x = margin;
|
||||
const y = margin + 18;
|
||||
|
|
@ -893,23 +973,33 @@ export function drawMap(canvas, map, options) {
|
|||
const showFeatures = options.showFeatures !== false;
|
||||
const showLabels = options.showLabels !== false;
|
||||
const continuousTerrain = options.continuousTerrain !== false;
|
||||
const zoom = Math.min(Math.max(Number(options.zoom) || 1, 0.55), 2.8);
|
||||
|
||||
const width = MAP_W * CELL_SIZE;
|
||||
const height = MAP_H * CELL_SIZE;
|
||||
if (canvas.width !== width) canvas.width = width;
|
||||
if (canvas.height !== height) canvas.height = height;
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
const outputWidth = MAP_W * CELL_SIZE;
|
||||
const outputHeight = MAP_H * CELL_SIZE;
|
||||
const sourceWidth = mapPixelWidth(map);
|
||||
const sourceHeight = mapPixelHeight(map);
|
||||
const drawScale = Math.min(outputWidth / Math.max(1, sourceWidth), outputHeight / Math.max(1, sourceHeight));
|
||||
const drawOffsetX = (outputWidth - sourceWidth * drawScale) * 0.5;
|
||||
const drawOffsetY = (outputHeight - sourceHeight * drawScale) * 0.5;
|
||||
const cellScreenSize = CELL_SIZE * drawScale;
|
||||
const terrainRenderScale = continuousTerrain ? clamp(drawScale * (options.fastTerrain ? 0.48 : 1.35), 0.30, 1) : 1;
|
||||
|
||||
if (canvas.width !== outputWidth) canvas.width = outputWidth;
|
||||
if (canvas.height !== outputHeight) canvas.height = outputHeight;
|
||||
ctx.clearRect(0, 0, outputWidth, outputHeight);
|
||||
ctx.save();
|
||||
ctx.translate(width * (1 - zoom) * 0.5, height * (1 - zoom) * 0.5);
|
||||
ctx.scale(zoom, zoom);
|
||||
ctx.translate(drawOffsetX, drawOffsetY);
|
||||
ctx.scale(drawScale, drawScale);
|
||||
ctx.__mapPixelWidth = sourceWidth;
|
||||
ctx.__mapPixelHeight = sourceHeight;
|
||||
const finish = () => {
|
||||
ctx.restore();
|
||||
drawScaleBar(ctx);
|
||||
drawScaleBar(ctx, cellScreenSize);
|
||||
delete ctx.__mapPixelWidth;
|
||||
delete ctx.__mapPixelHeight;
|
||||
};
|
||||
|
||||
// 1. Base Terrain & Urban
|
||||
drawBase(ctx, map, mode, continuousTerrain);
|
||||
drawBase(ctx, map, mode, continuousTerrain, terrainRenderScale);
|
||||
drawUrbanAreas(ctx, map, mode);
|
||||
const coastSegments = getCoastlineSegments(map);
|
||||
drawVectorSegments(ctx, coastSegments, "rgba(120, 175, 210, 0.22)", 2.2, false, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
|
||||
|
|
@ -926,7 +1016,7 @@ export function drawMap(canvas, map, options) {
|
|||
let tailCount = 0;
|
||||
for (let k = 0; k < path.length; k++) {
|
||||
const [x, y] = path[k];
|
||||
const i = indexOf(x, y);
|
||||
const i = cellIndex(map, Math.round(x), Math.round(y));
|
||||
const strength = (map.river?.[i] || 0) + (map.flowAccum?.[i] || 0) * 0.75;
|
||||
peak = Math.max(peak, strength);
|
||||
if (k >= tailStart) {
|
||||
|
|
@ -977,9 +1067,11 @@ export function drawMap(canvas, map, options) {
|
|||
const showPrefectureRegions = ["all", "admin", "borders-debug"].includes(mode);
|
||||
if (showPrefectureRegions) drawPrefectureRegionFill(ctx, map, mode);
|
||||
|
||||
if (showAdmin && map.adminBorders) {
|
||||
drawVectorSegments(ctx, map.adminBorders, "rgba(255, 255, 255, 0.8)", 2.8, false, { iterations: 1, tolerance: 0.05, offsetX: -0.5, offsetY: -0.5 });
|
||||
drawVectorSegments(ctx, map.adminBorders, "rgba(150, 140, 150, 0.9)", 1.2, true, { iterations: 1, tolerance: 0.05, offsetX: -0.5, offsetY: -0.5 });
|
||||
const adminBorderSegments = map.adminId ? getRasterBorderSegments(map, "adminId") : map.adminBorders;
|
||||
const prefectureBorderSegments = map.prefectureRegionId ? getRasterBorderSegments(map, "prefectureRegionId") : map.regionalPrefectureBorders;
|
||||
if (showAdmin && adminBorderSegments?.length) {
|
||||
drawVectorSegments(ctx, adminBorderSegments, "rgba(255, 255, 255, 0.8)", 2.8, false, { iterations: 1, tolerance: 0.05, offsetX: -0.5, offsetY: -0.5 });
|
||||
drawVectorSegments(ctx, adminBorderSegments, "rgba(150, 140, 150, 0.9)", 1.2, true, { iterations: 1, tolerance: 0.05, offsetX: -0.5, offsetY: -0.5 });
|
||||
}
|
||||
if (mode === "borders-debug") {
|
||||
// Keep the natural barrier heatmap subtle. A dense cell fill can look like
|
||||
|
|
@ -990,13 +1082,13 @@ export function drawMap(canvas, map, options) {
|
|||
}
|
||||
if (showTransportDebug) drawTransportDebug(ctx, map);
|
||||
|
||||
if (showPrefectureRegions && map.regionalPrefectureBorders) {
|
||||
drawVectorSegments(ctx, map.regionalPrefectureBorders, "rgba(255, 255, 255, 0.90)", 4.2, false, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
|
||||
drawVectorSegments(ctx, map.regionalPrefectureBorders, "rgba(82, 60, 102, 0.96)", 1.8, true, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
|
||||
if (showPrefectureRegions && prefectureBorderSegments?.length) {
|
||||
drawVectorSegments(ctx, prefectureBorderSegments, "rgba(255, 255, 255, 0.90)", 4.2, false, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
|
||||
drawVectorSegments(ctx, prefectureBorderSegments, "rgba(82, 60, 102, 0.96)", 1.8, true, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
|
||||
}
|
||||
|
||||
if (!showPrefectureRegions) {
|
||||
const finalPrefectureBorders = (map.regionalPrefectureBorders && map.regionalPrefectureBorders.length) ? map.regionalPrefectureBorders : map.prefectureBorder;
|
||||
const finalPrefectureBorders = (prefectureBorderSegments && prefectureBorderSegments.length) ? prefectureBorderSegments : map.prefectureBorder;
|
||||
drawVectorSegments(ctx, finalPrefectureBorders, "rgba(255, 255, 255, 0.95)", 5.0, false, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
|
||||
drawVectorSegments(ctx, finalPrefectureBorders, "rgba(110, 90, 110, 1)", 2.2, true, { iterations: 2, tolerance: 0.06, offsetX: -0.5, offsetY: -0.5 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue