?
This commit is contained in:
parent
3ac2c116bd
commit
da9d8ef904
11 changed files with 612 additions and 344 deletions
515
renderer.js
515
renderer.js
|
|
@ -9,9 +9,9 @@ function distToNearest(points, x, y, fallback = 999) {
|
|||
function blendOutside(color, isInside) {
|
||||
if (isInside) return color;
|
||||
return [
|
||||
Math.round(color[0] * 0.55 + 112),
|
||||
Math.round(color[1] * 0.55 + 112),
|
||||
Math.round(color[2] * 0.55 + 112),
|
||||
Math.round(color[0] * 0.8 + 50),
|
||||
Math.round(color[1] * 0.8 + 50),
|
||||
Math.round(color[2] * 0.8 + 50),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -38,47 +38,36 @@ function terrainColorContinuous(map, fx, fy, mode) {
|
|||
let color;
|
||||
|
||||
if (map.sea[i]) {
|
||||
// Google Map風の海の色(明るい青)
|
||||
const depth = clamp((0.35 - fieldSample(map.elevation, fx, fy)) * 2.4);
|
||||
color = [Math.round(170 + depth * 10), Math.round(211 + depth * 15), Math.round(223 + depth * 20)];
|
||||
color = [Math.round(170 + depth * 5), Math.round(218 + depth * 10), Math.round(255 - depth * 5)];
|
||||
} else if (mode === "suitability") {
|
||||
const a = fieldSample(map.agriculture, fx, fy);
|
||||
const p = fieldSample(map.plain, fx, fy);
|
||||
const f = fieldSample(map.floodplain, fx, fy);
|
||||
color = [
|
||||
Math.round(230 - p * 25 + f * 15),
|
||||
Math.round(235 + a * 15),
|
||||
Math.round(210 - a * 25 + p * 20),
|
||||
Math.round(240 - p * 15 + f * 10),
|
||||
Math.round(242 + a * 10),
|
||||
Math.round(235 - a * 15 + p * 10),
|
||||
];
|
||||
} else if (mode === "development") {
|
||||
const x = Math.floor(fx);
|
||||
const y = Math.floor(fy);
|
||||
const baseIndex = indexOf(Math.max(0, Math.min(MAP_W - 1, x)), Math.max(0, Math.min(MAP_H - 1, y)));
|
||||
const dCity = distToNearest(map.modernCities, fx, fy);
|
||||
const dInd = distToNearest(map.industrialZones, fx, fy);
|
||||
const dLog = distToNearest(map.logisticsParks, fx, fy);
|
||||
const dNew = distToNearest(map.newTowns, fx, fy);
|
||||
const urban = clamp(1 - dCity / 25);
|
||||
const density = map.populationDensity ? fieldSample(map.populationDensity, fx, fy) : urban;
|
||||
const industrial = clamp(1 - dInd / 10);
|
||||
const logistics = clamp(1 - dLog / 9);
|
||||
const newTown = clamp(1 - dNew / 9);
|
||||
const base = 214 + map.plain[baseIndex] * 22;
|
||||
const base = 235;
|
||||
color = [
|
||||
Math.round(base + density * 30 + urban * 8 + industrial * 14),
|
||||
Math.round(base + density * 10 + logistics * 15 + newTown * 12),
|
||||
Math.round(208 + map.agriculture[baseIndex] * 22 + density * 18 + newTown * 22),
|
||||
Math.round(base + density * 20),
|
||||
Math.round(base + density * 5),
|
||||
Math.round(230 + density * 10),
|
||||
];
|
||||
} else {
|
||||
// 起伏の大きさが読めるよう、標高段彩をやや強める
|
||||
// 地形色を少し濃く(暗く)調整
|
||||
const e = fieldSample(map.elevation, fx, fy);
|
||||
const m = fieldSample(map.moisture, fx, fy);
|
||||
if (e > 0.82) color = [178, 170, 160];
|
||||
else if (e > 0.68) color = [198, 188, 164];
|
||||
else if (e > 0.52) color = [205, 222 + m * 5, 184];
|
||||
else if (e > 0.34) color = [224, 238 + m * 6, 206];
|
||||
else if (e > 0.24) color = [236, 245 + m * 5, 220];
|
||||
else color = [218, 232 + m * 6, 206];
|
||||
if (e > 0.82) color = [210, 205, 195];
|
||||
else if (e > 0.68) color = [218, 215, 205];
|
||||
else if (e > 0.52) color = [220, 225, 210];
|
||||
else if (e > 0.34) color = [225, 230, 215];
|
||||
else if (e > 0.24) color = [230, 235, 220];
|
||||
else color = [238, 242, 228];
|
||||
}
|
||||
|
||||
return blendOutside(color, isInside);
|
||||
|
|
@ -89,62 +78,31 @@ function discreteColor(map, x, y, mode) {
|
|||
let color;
|
||||
|
||||
if (map.sea[i]) {
|
||||
// Google Map like styled
|
||||
color = [170, 218, 255];
|
||||
} else if (mode === "landuse") {
|
||||
const colors = {
|
||||
0: [230, 242, 220], // farmland
|
||||
1: [235, 245, 225], // plain
|
||||
2: [235, 230, 220], // old city
|
||||
3: [224, 202, 190], // CBD
|
||||
4: [245, 240, 230], // suburb
|
||||
5: [220, 220, 225], // industrial area
|
||||
6: [225, 235, 225], // logistics area
|
||||
7: [238, 242, 248], // new town
|
||||
8: [248, 242, 230], // coastal development
|
||||
9: [225, 238, 220], // others
|
||||
0: [242, 248, 238],
|
||||
1: [248, 250, 245],
|
||||
2: [240, 238, 232],
|
||||
3: [245, 230, 220],
|
||||
4: [250, 248, 245],
|
||||
5: [235, 235, 240],
|
||||
6: [240, 245, 240],
|
||||
7: [245, 248, 252],
|
||||
8: [250, 248, 240],
|
||||
9: [240, 245, 238],
|
||||
};
|
||||
color = colors[map.landuse[i]] || colors[0];
|
||||
} else if (mode === "admin") {
|
||||
const palette = [
|
||||
[245, 235, 230],
|
||||
[235, 245, 235],
|
||||
[240, 240, 250],
|
||||
[250, 245, 230],
|
||||
[245, 240, 248],
|
||||
[230, 245, 245],
|
||||
[250, 240, 240],
|
||||
[240, 250, 235],
|
||||
[250, 245, 242], [245, 250, 245], [245, 245, 252],
|
||||
[252, 250, 242], [250, 245, 250], [242, 250, 250]
|
||||
];
|
||||
const a = map.adminId[i];
|
||||
color = a >= 0 ? palette[a % palette.length] : [220, 225, 220];
|
||||
} else if (mode === "terrain-debug") {
|
||||
const ridge = clamp(map.ridgeField[i] * 0.68 + (map.arcSpineField?.[i] || 0) * 0.42 + (map.branchRidgeField?.[i] || 0) * 0.34);
|
||||
const deposit = clamp((map.depositionField?.[i] || 0) * 5.0 + (map.depositionalLowland?.[i] || 0) * 0.48 + (map.alluvialFanField?.[i] || 0) * 0.34 + (map.deltaField?.[i] || 0) * 0.46);
|
||||
const valley = clamp(map.valleyField[i] * 0.72 + map.river[i] * 0.22);
|
||||
const barrier = clamp(map.naturalBarrierScore?.[i] || ridge);
|
||||
color = [
|
||||
Math.round(220 - deposit * 70 + ridge * 48),
|
||||
Math.round(226 + deposit * 38 + valley * 28 - barrier * 52),
|
||||
Math.round(214 + valley * 58 + barrier * 34 - ridge * 42),
|
||||
];
|
||||
} else if (mode === "admin-debug" || mode === "borders-debug") {
|
||||
const barrier = clamp(
|
||||
map.ridgeField[i] * 0.88 +
|
||||
Math.max(0, map.river[i] - 0.28) * 1.25 +
|
||||
Math.max(0, map.flowAccum[i] - 0.36) * 0.72 +
|
||||
map.slope[i] * 0.48 +
|
||||
Math.max(0, map.elevation[i] - 0.54) * 0.34
|
||||
);
|
||||
color = [
|
||||
Math.round(238 - barrier * 28),
|
||||
Math.round(242 - barrier * 88),
|
||||
Math.round(226 + barrier * 20),
|
||||
];
|
||||
color = a >= 0 ? palette[a % palette.length] : [240, 242, 240];
|
||||
} else {
|
||||
color = terrainColorContinuous(map, x, y, "terrain");
|
||||
}
|
||||
|
||||
return blendOutside(color, Boolean(map.prefectureMask[i]));
|
||||
}
|
||||
|
||||
|
|
@ -165,17 +123,12 @@ function drawBase(ctx, map, mode, continuousTerrain) {
|
|||
const eR = fieldSample(map.elevation, fx + 0.6, fy);
|
||||
const eU = fieldSample(map.elevation, fx, fy - 0.6);
|
||||
const eD = fieldSample(map.elevation, fx, fy + 0.6);
|
||||
const shade = clamp(0.9 + (eR - eL) * 1.0 + (eD - eU) * 0.65, 0.72, 1.18);
|
||||
const elevation = fieldSample(map.elevation, fx, fy);
|
||||
const contour = Math.abs((elevation * 16) - Math.round(elevation * 16));
|
||||
const majorContour = Math.abs((elevation * 8) - Math.round(elevation * 8));
|
||||
const isLand = !map.sea[indexOf(Math.max(0, Math.min(MAP_W - 1, Math.floor(fx))), Math.max(0, Math.min(MAP_H - 1, Math.floor(fy))))];
|
||||
const contourFactor = isLand && majorContour < 0.022 ? 0.86 : isLand && contour < 0.028 ? 0.94 : 1;
|
||||
const shade = clamp(0.95 + (eR - eL) * 0.6 + (eD - eU) * 0.4, 0.85, 1.08);
|
||||
|
||||
const ii = (py * width + px) * 4;
|
||||
img.data[ii] = Math.round(r * shade * contourFactor);
|
||||
img.data[ii + 1] = Math.round(g * shade * contourFactor);
|
||||
img.data[ii + 2] = Math.round(b * shade * contourFactor);
|
||||
img.data[ii] = Math.round(r * shade);
|
||||
img.data[ii + 1] = Math.round(g * shade);
|
||||
img.data[ii + 2] = Math.round(b * shade);
|
||||
img.data[ii + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
|
@ -195,7 +148,6 @@ function drawBase(ctx, map, mode, continuousTerrain) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.putImageData(img, 0, 0);
|
||||
}
|
||||
|
||||
|
|
@ -206,7 +158,7 @@ function drawPath(ctx, path, color, width, dashed = false) {
|
|||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
if (dashed) ctx.setLineDash([6, 5]);
|
||||
if (dashed) ctx.setLineDash([8, 6]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(path[0][0] * CELL_SIZE + CELL_SIZE / 2, path[0][1] * CELL_SIZE + CELL_SIZE / 2);
|
||||
for (let k = 1; k < path.length; k++) {
|
||||
|
|
@ -216,72 +168,64 @@ function drawPath(ctx, path, color, width, dashed = false) {
|
|||
ctx.restore();
|
||||
}
|
||||
|
||||
function segmentPointKey(p) {
|
||||
return `${p[0]},${p[1]}`;
|
||||
}
|
||||
// 魚の骨(私鉄記号)スタイルを描画するための専用関数
|
||||
function drawRailway(ctx, path, color, lineWidth, tickLen, spacing) {
|
||||
if (!path || path.length < 2) return;
|
||||
ctx.save();
|
||||
ctx.lineCap = "butt";
|
||||
ctx.lineJoin = "round";
|
||||
ctx.strokeStyle = color;
|
||||
|
||||
function chainSegments(segments) {
|
||||
const unused = segments.map((seg) => [seg[0], seg[1]]);
|
||||
const chains = [];
|
||||
while (unused.length) {
|
||||
const chain = unused.pop();
|
||||
let grew = true;
|
||||
while (grew) {
|
||||
grew = false;
|
||||
const head = segmentPointKey(chain[0]);
|
||||
const tail = segmentPointKey(chain[chain.length - 1]);
|
||||
for (let i = unused.length - 1; i >= 0; i--) {
|
||||
const [a, b] = unused[i];
|
||||
const ak = segmentPointKey(a);
|
||||
const bk = segmentPointKey(b);
|
||||
if (ak === tail) { chain.push(b); unused.splice(i, 1); grew = true; break; }
|
||||
if (bk === tail) { chain.push(a); unused.splice(i, 1); grew = true; break; }
|
||||
if (bk === head) { chain.unshift(a); unused.splice(i, 1); grew = true; break; }
|
||||
if (ak === head) { chain.unshift(b); unused.splice(i, 1); grew = true; break; }
|
||||
}
|
||||
}
|
||||
chains.push(chain);
|
||||
// 中心の実線を描画
|
||||
ctx.lineWidth = lineWidth;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(path[0][0] * CELL_SIZE + CELL_SIZE / 2, path[0][1] * CELL_SIZE + CELL_SIZE / 2);
|
||||
for (let k = 1; k < path.length; k++) {
|
||||
ctx.lineTo(path[k][0] * CELL_SIZE + CELL_SIZE / 2, path[k][1] * CELL_SIZE + CELL_SIZE / 2);
|
||||
}
|
||||
return chains;
|
||||
}
|
||||
ctx.stroke();
|
||||
|
||||
function chaikin(points, passes = 1) {
|
||||
let out = points;
|
||||
for (let pass = 0; pass < passes; pass++) {
|
||||
if (out.length < 3) return out;
|
||||
const next = [out[0]];
|
||||
for (let i = 0; i < out.length - 1; i++) {
|
||||
const a = out[i];
|
||||
const b = out[i + 1];
|
||||
next.push([a[0] * 0.75 + b[0] * 0.25, a[1] * 0.75 + b[1] * 0.25]);
|
||||
next.push([a[0] * 0.25 + b[0] * 0.75, a[1] * 0.25 + b[1] * 0.75]);
|
||||
// 棘(クロスハッチ)を描画
|
||||
ctx.lineWidth = 1.0;
|
||||
ctx.beginPath();
|
||||
let leftover = 0;
|
||||
for (let k = 0; k < path.length - 1; k++) {
|
||||
const x1 = path[k][0] * CELL_SIZE + CELL_SIZE / 2;
|
||||
const y1 = path[k][1] * CELL_SIZE + CELL_SIZE / 2;
|
||||
const x2 = path[k+1][0] * CELL_SIZE + CELL_SIZE / 2;
|
||||
const y2 = path[k+1][1] * CELL_SIZE + CELL_SIZE / 2;
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist === 0) continue;
|
||||
|
||||
// 法線(直角)ベクトル
|
||||
const nx = dx / dist;
|
||||
const ny = dy / dist;
|
||||
const px = -ny * (tickLen / 2);
|
||||
const py = nx * (tickLen / 2);
|
||||
|
||||
let d = (spacing / 2) + leftover;
|
||||
while (d < dist) {
|
||||
const cx = x1 + nx * d;
|
||||
const cy = y1 + ny * d;
|
||||
ctx.moveTo(cx + px, cy + py);
|
||||
ctx.lineTo(cx - px, cy - py);
|
||||
d += spacing;
|
||||
}
|
||||
next.push(out[out.length - 1]);
|
||||
out = next;
|
||||
leftover = d - dist;
|
||||
}
|
||||
return out;
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawSegments(ctx, segments, color, width, dashed = false, smooth = false) {
|
||||
function drawSegments(ctx, segments, color, width, dashed = false) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = width;
|
||||
ctx.lineCap = "round";
|
||||
ctx.lineJoin = "round";
|
||||
if (dashed) ctx.setLineDash([4, 4]);
|
||||
|
||||
if (smooth) {
|
||||
for (const chain of chainSegments(segments)) {
|
||||
const points = chaikin(chain, 1);
|
||||
if (points.length < 2) continue;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(points[0][0] * CELL_SIZE, points[0][1] * CELL_SIZE);
|
||||
for (let i = 1; i < points.length; i++) ctx.lineTo(points[i][0] * CELL_SIZE, points[i][1] * CELL_SIZE);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
return;
|
||||
}
|
||||
if (dashed) ctx.setLineDash([6, 5]);
|
||||
|
||||
for (const seg of segments) {
|
||||
ctx.beginPath();
|
||||
|
|
@ -296,15 +240,14 @@ function drawUrbanAreas(ctx, map, mode) {
|
|||
const visibleModes = ["all", "modern", "development", "landuse", "roads", "admin"];
|
||||
if (!visibleModes.includes(mode)) return;
|
||||
|
||||
// Google Map風の都市部の色
|
||||
const colors = {
|
||||
2: "rgba(235, 230, 220, 0.75)", // 旧市街 - 薄いベージュ
|
||||
3: "rgba(224, 202, 190, 0.9)", // 中心市街地 / CBD - cell fill
|
||||
4: "rgba(245, 242, 235, 0.7)", // 郊外 - 薄いクリーム
|
||||
5: "rgba(220, 220, 228, 0.8)", // 工業地域 - 薄いグレー
|
||||
6: "rgba(225, 235, 228, 0.75)", // 物流 - 薄い緑グレー
|
||||
7: "rgba(238, 242, 250, 0.75)", // ニュータウン - 薄い青白
|
||||
8: "rgba(248, 245, 238, 0.7)", // 沿道 - クリーム
|
||||
2: "rgba(225, 222, 215, 0.6)",
|
||||
3: "rgba(240, 220, 205, 0.85)",
|
||||
4: "rgba(242, 240, 235, 0.5)",
|
||||
5: "rgba(220, 220, 225, 0.6)",
|
||||
6: "rgba(225, 230, 225, 0.5)",
|
||||
7: "rgba(235, 240, 245, 0.6)",
|
||||
8: "rgba(245, 242, 235, 0.5)",
|
||||
};
|
||||
|
||||
ctx.save();
|
||||
|
|
@ -319,38 +262,22 @@ function drawUrbanAreas(ctx, map, mode) {
|
|||
const py = y * CELL_SIZE;
|
||||
ctx.fillStyle = colors[lu];
|
||||
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
const h = ((x * 92821 + y * 68917 + lu * 131) >>> 0);
|
||||
if (lu === 3 || lu === 2 || lu === 5 || lu === 6) {
|
||||
// 建物の表現を控えめに
|
||||
ctx.fillStyle = lu === 3 ? "rgba(200,200,200,0.25)" : "rgba(210,210,210,0.2)";
|
||||
if (h % 3 !== 0) ctx.fillRect(px + 1, py + 1, 2, 2);
|
||||
if (h % 5 !== 0) ctx.fillRect(px + 3, py + 2, 2, 2);
|
||||
if (h % 7 !== 0) ctx.fillRect(px + 2, py + 4, 2, 1.5);
|
||||
} else if (lu === 4 || lu === 7) {
|
||||
ctx.strokeStyle = lu === 7 ? "rgba(220,220,230,0.15)" : "rgba(200,190,180,0.15)";
|
||||
ctx.lineWidth = 0.8;
|
||||
ctx.beginPath();
|
||||
if (h % 2 === 0) {
|
||||
ctx.moveTo(px + 1, py + 1);
|
||||
ctx.lineTo(px + CELL_SIZE - 1, py + 1);
|
||||
ctx.moveTo(px + 1, py + 4);
|
||||
ctx.lineTo(px + CELL_SIZE - 1, py + 4);
|
||||
} else {
|
||||
ctx.moveTo(px + 1, py + 1);
|
||||
ctx.lineTo(px + 1, py + CELL_SIZE - 1);
|
||||
ctx.moveTo(px + 4, py + 1);
|
||||
ctx.lineTo(px + 4, py + CELL_SIZE - 1);
|
||||
}
|
||||
ctx.stroke();
|
||||
} else if (lu === 8) {
|
||||
ctx.strokeStyle = "rgba(180,170,160,0.2)";
|
||||
ctx.lineWidth = 0.9;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(px + 1, py + 3);
|
||||
ctx.lineTo(px + CELL_SIZE - 1, py + 3);
|
||||
ctx.stroke();
|
||||
}
|
||||
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);
|
||||
if (!map.prefectureMask[i] || map.sea[i]) continue;
|
||||
const v = clamp(field[i] || 0, 0, 1);
|
||||
if (v <= 0.12) continue;
|
||||
ctx.fillStyle = color(v);
|
||||
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
|
|
@ -361,91 +288,41 @@ function dot(ctx, p, radius, fill, stroke = "white") {
|
|||
ctx.arc(p.x * CELL_SIZE + CELL_SIZE / 2, p.y * CELL_SIZE + CELL_SIZE / 2, radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = fill;
|
||||
ctx.fill();
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.strokeStyle = stroke;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function squareIcon(ctx, p, size, fill, stroke = "white") {
|
||||
const x = p.x * CELL_SIZE + CELL_SIZE / 2;
|
||||
const y = p.y * CELL_SIZE + CELL_SIZE / 2;
|
||||
ctx.save();
|
||||
ctx.fillStyle = fill;
|
||||
ctx.strokeStyle = stroke;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.rect(x - size / 2, y - size / 2, size, size);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function triangleIcon(ctx, p, size, fill, stroke = "white") {
|
||||
const x = p.x * CELL_SIZE + CELL_SIZE / 2;
|
||||
const y = p.y * CELL_SIZE + CELL_SIZE / 2;
|
||||
ctx.save();
|
||||
ctx.fillStyle = fill;
|
||||
ctx.strokeStyle = stroke;
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y - size / 2);
|
||||
ctx.lineTo(x + size / 2, y + size / 2);
|
||||
ctx.lineTo(x - size / 2, y + size / 2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function railStationIcon(ctx, p) {
|
||||
squareIcon(ctx, p, 5.2, "rgba(255,255,255,0.96)", "rgba(55,55,55,0.92)");
|
||||
}
|
||||
|
||||
|
||||
function drawHarborWorks(ctx, map) {
|
||||
if (!map.harborWorks) return;
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(95, 120, 150, 0.9)";
|
||||
ctx.lineWidth = 2.2;
|
||||
ctx.lineCap = "round";
|
||||
for (const harbor of map.harborWorks) {
|
||||
for (const seg of harbor.segments || []) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(seg[0][0] * CELL_SIZE + CELL_SIZE / 2, seg[0][1] * CELL_SIZE + CELL_SIZE / 2);
|
||||
ctx.lineTo(seg[1][0] * CELL_SIZE + CELL_SIZE / 2, seg[1][1] * CELL_SIZE + CELL_SIZE / 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function boxesOverlap(a, b, pad = 2) {
|
||||
function boxesOverlap(a, b, pad = 3) {
|
||||
return !(a.x2 + pad < b.x1 || a.x1 - pad > b.x2 || a.y2 + pad < b.y1 || a.y1 - pad > b.y2);
|
||||
}
|
||||
|
||||
function labelWithCollision(ctx, p, occupied) {
|
||||
if (!p.name) return false;
|
||||
ctx.save();
|
||||
ctx.font = "11px ui-sans-serif, system-ui, sans-serif";
|
||||
ctx.font = "600 11.5px ui-sans-serif, system-ui, -apple-system, sans-serif";
|
||||
const baseX = p.x * CELL_SIZE + CELL_SIZE / 2;
|
||||
const baseY = p.y * CELL_SIZE + CELL_SIZE / 2;
|
||||
const textW = ctx.measureText(p.name).width;
|
||||
const textH = 12;
|
||||
const candidates = [
|
||||
[7, -5], [7, 12], [-textW - 7, -5], [-textW - 7, 12],
|
||||
[-textW / 2, -13], [-textW / 2, 20], [12, 2], [-textW - 12, 2],
|
||||
[7, -4], [7, 13], [-textW - 7, -4], [-textW - 7, 13],
|
||||
[-textW / 2, -12], [-textW / 2, 20], [12, 4], [-textW - 12, 4],
|
||||
];
|
||||
|
||||
for (const [ox, oy] of candidates) {
|
||||
const x = baseX + ox;
|
||||
const y = baseY + oy;
|
||||
const box = { x1: x - 2, y1: y - textH, x2: x + textW + 2, y2: y + 3 };
|
||||
const box = { x1: x - 2, y1: y - textH, x2: x + textW + 2, y2: y + 4 };
|
||||
if (box.x1 < 0 || box.y1 < 0 || box.x2 > MAP_W * CELL_SIZE || box.y2 > MAP_H * CELL_SIZE) continue;
|
||||
if (occupied.some((b) => boxesOverlap(box, b))) continue;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.fillStyle = "rgba(40,40,40,0.95)";
|
||||
|
||||
ctx.lineJoin = "round";
|
||||
ctx.lineWidth = 3.5;
|
||||
ctx.strokeStyle = "rgba(255, 255, 255, 0.95)";
|
||||
ctx.strokeText(p.name, x, y);
|
||||
|
||||
ctx.fillStyle = p.isPrefecturalCapital ? "#111111" : "#333333";
|
||||
ctx.fillText(p.name, x, y);
|
||||
occupied.push(box);
|
||||
ctx.restore();
|
||||
|
|
@ -459,7 +336,7 @@ function drawLabels(ctx, points, limit = Infinity) {
|
|||
const occupied = [];
|
||||
const prioritized = points
|
||||
.filter((p) => p?.name)
|
||||
.map((p) => ({ ...p, labelPriority: (p.labelPriorityBase || 0) + (p.isPrefecturalCapital ? 1000 : 0) + (p.population || 0) / 900 + (p.portClass === "major" ? 170 : p.portClass === "regional" ? 95 : 0) + (p.kind === "Market Town" ? 48 : 0) + (p.kind?.includes("Castle") ? 70 : 0) + (p.kind === "External Gateway" ? 60 : 0) + (p.kind === "Municipal Center" ? 34 : 0) }))
|
||||
.map((p) => ({ ...p, labelPriority: (p.labelPriorityBase || 0) + (p.isPrefecturalCapital ? 1000 : 0) + (p.population || 0) / 900 + (p.portClass === "major" ? 170 : p.portClass === "regional" ? 95 : 0) }))
|
||||
.sort((a, b) => b.labelPriority - a.labelPriority);
|
||||
for (const p of prioritized.slice(0, limit)) labelWithCollision(ctx, p, occupied);
|
||||
}
|
||||
|
|
@ -471,111 +348,113 @@ export function drawMap(canvas, map, options) {
|
|||
const mode = options.mode || "all";
|
||||
const showFeatures = options.showFeatures !== false;
|
||||
const showLabels = options.showLabels !== false;
|
||||
const continuousTerrain = true;
|
||||
|
||||
|
||||
const width = MAP_W * CELL_SIZE;
|
||||
const height = MAP_H * CELL_SIZE;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
drawBase(ctx, map, mode, continuousTerrain);
|
||||
// 1. Base Terrain & Urban
|
||||
drawBase(ctx, map, mode, true);
|
||||
drawUrbanAreas(ctx, map, mode);
|
||||
|
||||
// Rivers use the same hue family as the sea; hierarchy is expressed by width/opacity.
|
||||
const waterBlue = "rgba(170, 218, 255, 0.95)";
|
||||
// Small streams remain in the data model but are not drawn by default.
|
||||
for (const path of map.tributaryRivers || map.riverPaths || []) drawPath(ctx, path, "rgba(170, 218, 255, 0.78)", 1.35);
|
||||
for (const path of map.mainRivers) drawPath(ctx, path, waterBlue, 3.2);
|
||||
drawHarborWorks(ctx, map);
|
||||
// 2. Rivers
|
||||
const waterBlue = "rgba(160, 205, 240, 1)";
|
||||
for (const path of map.tributaryRivers || map.riverPaths || []) drawPath(ctx, path, "rgba(160, 205, 240, 0.8)", 1.5);
|
||||
for (const path of map.mainRivers) drawPath(ctx, path, waterBlue, 3.5);
|
||||
|
||||
const debugBorders = mode === "admin-debug" || mode === "borders-debug";
|
||||
if (map.regionalPrefectureBorders) drawSegments(ctx, map.regionalPrefectureBorders, debugBorders ? "rgba(40,40,40,0.82)" : mode === "all" ? "rgba(95,95,95,0.18)" : "rgba(95,95,95,0.30)", debugBorders ? 1.8 : 1.0, false, mode === "all");
|
||||
drawSegments(ctx, map.prefectureBorder, "rgba(30,30,30,0.82)", 2.4, false, true);
|
||||
drawSegments(ctx, map.prefectureBorder, "rgba(255,255,255,0.74)", 1.05, false, true);
|
||||
const showHistory = ["history", "all", "terrain"].includes(mode);
|
||||
const showModern = ["modern", "all", "development", "landuse", "roads", "admin-debug", "borders-debug"].includes(mode);
|
||||
const showRoads = ["roads", "all", "development"].includes(mode);
|
||||
const showAdmin = ["admin", "all", "admin-debug", "borders-debug"].includes(mode);
|
||||
|
||||
// 3. Borders
|
||||
if (showAdmin && map.adminBorders) {
|
||||
drawSegments(ctx, map.adminBorders, "rgba(255, 255, 255, 0.8)", 2.8, false);
|
||||
drawSegments(ctx, map.adminBorders, "rgba(150, 140, 150, 0.9)", 1.2, true);
|
||||
}
|
||||
if (mode === "admin-debug" || mode === "borders-debug") {
|
||||
drawDebugCells(ctx, map, map.naturalBarrierScore, (v) => `rgba(255, 120, 40, ${0.06 + v * 0.18})`);
|
||||
if (map.adminDebug?.compartmentBorders) drawSegments(ctx, map.adminDebug.compartmentBorders, "rgba(60, 110, 170, 0.42)", 0.8, true);
|
||||
if (map.regionalPrefectureBorders) drawSegments(ctx, map.regionalPrefectureBorders, "rgba(70, 55, 95, 0.95)", 2.4, false);
|
||||
}
|
||||
|
||||
drawSegments(ctx, map.prefectureBorder, "rgba(255, 255, 255, 0.95)", 5.0, false);
|
||||
drawSegments(ctx, map.prefectureBorder, "rgba(110, 90, 110, 1)", 2.2, true);
|
||||
|
||||
if (!showFeatures) return;
|
||||
|
||||
const showHistory = ["history", "all", "terrain", "suitability"].includes(mode);
|
||||
const showModern = ["modern", "all", "development", "landuse"].includes(mode);
|
||||
const showRoads = ["roads", "all", "development", "landuse"].includes(mode);
|
||||
const showAdmin = ["admin", "all", "admin-debug", "borders-debug"].includes(mode);
|
||||
|
||||
if (debugBorders && map.adminDebug?.compartmentBorders) drawSegments(ctx, map.adminDebug.compartmentBorders, "rgba(70,70,70,0.32)", 0.75);
|
||||
if (showAdmin) drawSegments(ctx, map.adminBorders, debugBorders ? "rgba(20,90,180,0.96)" : mode === "all" ? "rgba(120,120,120,0.28)" : "rgba(120,120,120,0.65)", debugBorders ? 2.1 : mode === "all" ? 0.9 : 1.3);
|
||||
if (showAdmin && mode !== "all") {
|
||||
for (const p of map.adminCenters || []) dot(ctx, p, 3.0, "rgba(255,255,255,0.96)", "rgba(70,90,120,0.85)");
|
||||
}
|
||||
|
||||
// 4. Casings (Outlines)
|
||||
if (showHistory) {
|
||||
for (const path of map.premodernRoads) drawPath(ctx, path, mode === "all" ? "rgba(150, 120, 90, 0.34)" : "rgba(150, 120, 90, 0.55)", mode === "all" ? 1.15 : 1.45, true);
|
||||
for (const path of map.minorRoads) drawPath(ctx, path, mode === "all" ? "rgba(180, 150, 120, 0.28)" : "rgba(180, 150, 120, 0.62)", mode === "all" ? 0.8 : 1.05);
|
||||
// 古い道は白の実線が引き立つように淡いケーシングを敷く
|
||||
for (const path of map.premodernRoads) drawPath(ctx, path, "rgba(210, 210, 210, 0.7)", 2.5);
|
||||
}
|
||||
|
||||
if (showModern) {
|
||||
// 鉄道 - 濃いグレー
|
||||
for (const path of map.railways) drawPath(ctx, path, "rgba(80, 80, 80, 0.9)", 2.8);
|
||||
for (const path of map.ringRailways || []) drawPath(ctx, path, "rgba(70, 70, 70, 0.82)", 2.1);
|
||||
for (const path of map.branchRailways) drawPath(ctx, path, "rgba(100, 100, 100, 0.82)", 1.9);
|
||||
for (const path of map.externalRailways) drawPath(ctx, path, "rgba(90, 90, 90, 0.9)", 2.4);
|
||||
}
|
||||
|
||||
if (showRoads) {
|
||||
// Google Map風の道路 - 白と黄色とオレンジ
|
||||
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.2);
|
||||
for (const path of map.icAccessRoads || []) drawPath(ctx, path, "rgba(255, 230, 150, 0.82)", 1.45);
|
||||
for (const path of map.ringRoads || []) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.0);
|
||||
for (const path of map.expressways) drawPath(ctx, path, "rgba(245, 140, 60, 0.95)", 4.0);
|
||||
for (const path of map.externalRoads) drawPath(ctx, path, "rgba(252, 210, 90, 0.85)", 2.5);
|
||||
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(245, 140, 60, 0.95)", 4.2);
|
||||
}
|
||||
|
||||
if (showHistory) {
|
||||
for (const p of map.villages) dot(ctx, p, mode === "all" ? 1.35 : 1.9, mode === "all" ? "rgba(120, 100, 80, 0.42)" : "rgba(120, 100, 80, 0.72)");
|
||||
for (const p of map.markets) dot(ctx, p, 4.8, "rgba(200, 130, 80, 0.95)");
|
||||
for (const p of map.ports) {
|
||||
const color = p.portClass === "major" ? "rgba(40, 105, 190, 0.98)" : p.portClass === "regional" ? "rgba(70, 130, 200, 0.95)" : p.portClass === "lake" ? "rgba(80, 155, 180, 0.92)" : "rgba(95, 150, 195, 0.82)";
|
||||
triangleIcon(ctx, p, p.portClass === "major" ? 8.2 : 6.5, color);
|
||||
if (showModern || showRoads) {
|
||||
// 鉄道のケーシング(白背景を敷いて視認性を保つ)
|
||||
for (const path of map.railways) drawPath(ctx, path, "rgba(255, 255, 255, 0.75)", 3.5);
|
||||
for (const path of map.branchRailways) drawPath(ctx, path, "rgba(255, 255, 255, 0.6)", 2.5);
|
||||
for (const path of map.externalRailways) drawPath(ctx, path, "rgba(255, 255, 255, 0.75)", 3.5);
|
||||
|
||||
// 幹線道路のケーシング(色を濃く)
|
||||
if (showRoads) {
|
||||
for (const path of map.expressways) drawPath(ctx, path, "rgba(80, 140, 100, 1)", 5.0);
|
||||
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(80, 140, 100, 1)", 5.0);
|
||||
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(190, 175, 140, 1)", 3.8);
|
||||
for (const path of map.ringRoads || []) drawPath(ctx, path, "rgba(190, 175, 140, 1)", 3.8);
|
||||
for (const path of map.externalRoads) drawPath(ctx, path, "rgba(190, 175, 140, 1)", 3.8);
|
||||
}
|
||||
for (const p of map.crossings) dot(ctx, p, 3.5, "rgba(255, 240, 180, 0.95)", "rgba(100,90,70,0.8)");
|
||||
for (const p of map.passes) dot(ctx, p, 3.9, "rgba(150, 120, 180, 0.95)");
|
||||
for (const p of map.castles) squareIcon(ctx, p, 7.0, "rgba(180, 70, 70, 0.96)");
|
||||
for (const p of map.castleRuins) dot(ctx, p, 3.2, "rgba(110, 90, 90, 0.9)", "rgba(220,220,220,0.8)");
|
||||
}
|
||||
|
||||
// 5. Fills (Inner colors) & Fishbones
|
||||
if (showHistory) {
|
||||
for (const path of map.premodernRoads) drawPath(ctx, path, "rgba(255, 255, 255, 1)", 1.2, false);
|
||||
}
|
||||
|
||||
if (showModern || showRoads) {
|
||||
// 鉄道の骨線描画(色, 線幅, 棘の長さ, 棘の間隔)
|
||||
for (const path of map.railways) drawRailway(ctx, path, "rgba(110, 110, 110, 1)", 1.5, 5.0, 7.0);
|
||||
for (const path of map.branchRailways) drawRailway(ctx, path, "rgba(140, 140, 140, 1)", 1.0, 4.0, 6.0);
|
||||
for (const path of map.externalRailways) drawRailway(ctx, path, "rgba(110, 110, 110, 1)", 1.5, 5.0, 7.0);
|
||||
|
||||
// 幹線道路の塗り(色を濃く)
|
||||
if (showRoads) {
|
||||
for (const path of map.expressways) drawPath(ctx, path, "rgba(110, 185, 130, 1)", 3.0);
|
||||
for (const path of map.externalExpressways) drawPath(ctx, path, "rgba(110, 185, 130, 1)", 3.0);
|
||||
for (const path of map.nationalRoads) drawPath(ctx, path, "rgba(245, 225, 130, 1)", 2.0);
|
||||
for (const path of map.ringRoads || []) drawPath(ctx, path, "rgba(245, 225, 130, 1)", 2.0);
|
||||
for (const path of map.externalRoads) drawPath(ctx, path, "rgba(245, 225, 130, 1)", 2.0);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Icons & Labels
|
||||
if (showModern) {
|
||||
for (const p of map.industrialZones) squareIcon(ctx, p, 6.5, "rgba(140, 140, 150, 0.96)");
|
||||
for (const p of map.stations) railStationIcon(ctx, p);
|
||||
for (const p of map.satelliteCities || []) dot(ctx, p, 4.8, "rgba(215, 95, 145, 0.95)");
|
||||
for (const p of map.newTowns) triangleIcon(ctx, p, 6.5, "rgba(180, 210, 240, 0.95)");
|
||||
for (const p of map.stations) dot(ctx, p, 2.5, "#fff", "#444");
|
||||
for (const p of map.modernCities) {
|
||||
const popRadius = p.population ? Math.min(9.2, 3.4 + Math.sqrt(p.population) / 360) : 4.7;
|
||||
const rankBoost = p.isPrefecturalCapital || p.rank === "Prefectural Capital" ? 1.6 : p.rank === "Regional Center" ? 0.45 : 0;
|
||||
dot(ctx, p, popRadius + rankBoost, "rgba(230, 100, 100, 0.95)");
|
||||
if (p.isPrefecturalCapital) dot(ctx, p, popRadius + 4.2, "rgba(255,255,255,0.0)", "rgba(180,60,60,0.95)");
|
||||
const popRadius = p.population ? Math.min(8.5, 3.5 + Math.sqrt(p.population) / 400) : 4.5;
|
||||
dot(ctx, p, popRadius, "rgba(240, 110, 110, 0.95)", "rgba(255,255,255,0.9)");
|
||||
if (p.isPrefecturalCapital) dot(ctx, p, popRadius + 3.0, "transparent", "rgba(200,80,80,0.9)");
|
||||
}
|
||||
if (mode === "admin-debug" || mode === "borders-debug") {
|
||||
for (const p of map.externalGateways || []) dot(ctx, p, 5.5, "rgba(255,255,255,0.95)", "rgba(40,80,170,0.95)");
|
||||
for (const p of map.transportDebug?.missingRequiredNodes || []) dot(ctx, p, 6.5, "rgba(255,80,80,0.95)", "rgba(80,20,20,0.95)");
|
||||
}
|
||||
}
|
||||
|
||||
if (showRoads) {
|
||||
for (const p of map.logisticsParks) squareIcon(ctx, p, 6.2, "rgba(110, 170, 140, 0.96)");
|
||||
for (const p of map.interchanges) dot(ctx, p, 4.1, "rgba(255, 255, 255, 0.98)", "rgba(220, 90, 60, 0.95)");
|
||||
for (const p of map.externalGateways) dot(ctx, p, 4.4, "rgba(255, 250, 200, 0.98)", "rgba(60,60,60,0.92)");
|
||||
}
|
||||
|
||||
if (showLabels) {
|
||||
const adminLabels = (map.adminCenters || [])
|
||||
.map((p) => ({ ...p, labelPriorityBase: mode === "admin" || mode === "admin-debug" || mode === "borders-debug" ? 90 : 8 }))
|
||||
.filter((p) => mode !== "all" || p.representativeFeatureName);
|
||||
if (mode === "admin") {
|
||||
drawLabels(ctx, map.adminCenters || [], Infinity);
|
||||
return;
|
||||
}
|
||||
if (mode === "admin-debug" || mode === "borders-debug") {
|
||||
drawLabels(ctx, [...(map.adminCenters || []), ...(map.externalGateways || [])], Infinity);
|
||||
return;
|
||||
}
|
||||
const important = [
|
||||
...map.modernCities,
|
||||
...map.ports,
|
||||
...map.markets.slice(0, mode === "all" ? 6 : 10),
|
||||
...map.castles.slice(0, mode === "all" ? 5 : 8),
|
||||
...(map.satelliteCities || []),
|
||||
...map.newTowns,
|
||||
...(mode === "admin" || mode === "admin-debug" || mode === "borders-debug" ? adminLabels : adminLabels.slice(0, 8)),
|
||||
...map.externalGateways,
|
||||
].filter((p) => p.insidePrefecture || p.kind === "External Gateway");
|
||||
|
||||
drawLabels(ctx, important, mode === "all" ? 34 : Infinity);
|
||||
drawLabels(ctx, important, 60);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue