This commit is contained in:
33333-33333 2026-05-20 17:15:09 +09:00
commit e1bb10ff8a
12 changed files with 1336 additions and 526 deletions

View file

@ -1,8 +1,5 @@
import { MAP_W, MAP_H, CELL_SIZE, indexOf } from "./mapGenerator.js";
function clamp(v, a = 0, b = 1) {
return Math.max(a, Math.min(b, v));
}
import { clamp } from "./grid.js";
function distToNearest(points, x, y, fallback = 999) {
let best = fallback;
@ -197,12 +194,73 @@ function drawPath(ctx, path, color, width, dashed = false) {
ctx.restore();
}
function drawSegments(ctx, segments, color, width, dashed = false) {
function segmentPointKey(p) {
return `${p[0]},${p[1]}`;
}
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);
}
return chains;
}
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]);
}
next.push(out[out.length - 1]);
out = next;
}
return out;
}
function drawSegments(ctx, segments, color, width, dashed = false, smooth = 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;
}
for (const seg of segments) {
ctx.beginPath();
ctx.moveTo(seg[0][0] * CELL_SIZE, seg[0][1] * CELL_SIZE);
@ -375,13 +433,13 @@ function labelWithCollision(ctx, p, occupied) {
return false;
}
function drawLabels(ctx, points) {
function drawLabels(ctx, points, limit = Infinity) {
const occupied = [];
const prioritized = points
.filter((p) => p?.name)
.map((p) => ({ ...p, labelPriority: (p.isPrefecturalCapital ? 1000 : 0) + (p.population || 0) / 1000 + (p.kind === "Major Port" ? 160 : 0) + (p.kind === "Market Town" ? 55 : 0) + (p.kind?.includes("Castle") ? 45 : 0) }))
.map((p) => ({ ...p, labelPriority: (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) }))
.sort((a, b) => b.labelPriority - a.labelPriority);
for (const p of prioritized) labelWithCollision(ctx, p, occupied);
for (const p of prioritized.slice(0, limit)) labelWithCollision(ctx, p, occupied);
}
export function drawMap(canvas, map, options) {
@ -408,9 +466,9 @@ export function drawMap(canvas, map, options) {
for (const path of map.mainRivers) drawPath(ctx, path, waterBlue, 3.2);
drawHarborWorks(ctx, map);
if (map.regionalPrefectureBorders) drawSegments(ctx, map.regionalPrefectureBorders, "rgba(95,95,95,0.30)", 1.0);
drawSegments(ctx, map.prefectureBorder, "rgba(30,30,30,0.82)", 2.4);
drawSegments(ctx, map.prefectureBorder, "rgba(255,255,255,0.74)", 1.05);
if (map.regionalPrefectureBorders) drawSegments(ctx, map.regionalPrefectureBorders, mode === "all" ? "rgba(95,95,95,0.18)" : "rgba(95,95,95,0.30)", 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);
if (!showFeatures) return;
@ -419,11 +477,11 @@ export function drawMap(canvas, map, options) {
const showRoads = ["roads", "all", "development", "landuse"].includes(mode);
const showAdmin = ["admin", "all"].includes(mode);
if (showAdmin) drawSegments(ctx, map.adminBorders, "rgba(120,120,120,0.65)", 1.3);
if (showAdmin) drawSegments(ctx, map.adminBorders, mode === "all" ? "rgba(120,120,120,0.28)" : "rgba(120,120,120,0.65)", mode === "all" ? 0.9 : 1.3);
if (showHistory) {
for (const path of map.premodernRoads) drawPath(ctx, path, "rgba(150, 120, 90, 0.55)", 1.45, true);
for (const path of map.minorRoads) drawPath(ctx, path, "rgba(180, 150, 120, 0.62)", 1.05);
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);
}
if (showModern) {
@ -445,7 +503,7 @@ export function drawMap(canvas, map, options) {
}
if (showHistory) {
for (const p of map.villages) dot(ctx, p, 1.9, "rgba(120, 100, 80, 0.72)");
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)";
@ -480,13 +538,13 @@ export function drawMap(canvas, map, options) {
const important = [
...map.modernCities,
...map.ports,
...map.markets.slice(0, 10),
...map.castles.slice(0, 8),
...map.markets.slice(0, mode === "all" ? 6 : 10),
...map.castles.slice(0, mode === "all" ? 5 : 8),
...(map.satelliteCities || []),
...map.newTowns,
...map.externalGateways,
].filter((p) => p.insidePrefecture || p.kind === "External Gateway");
drawLabels(ctx, important);
drawLabels(ctx, important, mode === "all" ? 28 : Infinity);
}
}