This commit is contained in:
33333-33333 2026-05-29 14:31:42 +09:00
commit f2d0306d96
13 changed files with 1434 additions and 516 deletions

View file

@ -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 });
}